From 958888298d4ba2aab0c8e8c025834e1870a6c3ea Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Fri, 19 Apr 2024 15:24:18 +0200 Subject: [PATCH 1/6] Retrieve original model value from solution. --- src/EcologicalNetworksDynamics.jl | 28 +++++++++++++------ src/Framework/method_macro.jl | 6 +++++ src/Framework/system.jl | 14 ++++++++++ src/Internals/Internals.jl | 1 + src/Internals/inputs/biological_rates.jl | 2 ++ src/Internals/inputs/environment.jl | 2 ++ src/Internals/inputs/foodwebs.jl | 2 ++ src/Internals/inputs/functional_response.jl | 3 +++ src/Internals/inputs/producer_growth.jl | 3 +++ src/Internals/model/model_parameters.jl | 8 +++++- src/Internals/model/simulate.jl | 16 ++++++++++- src/dedicate_framework_to_model.jl | 11 -------- src/methods/main.jl | 20 +++++++++++--- src/output-analysis.jl | 2 -- test/user/06-post-simulation.jl | 30 +++++++++++++++++++++ 15 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 test/user/06-post-simulation.jl diff --git a/src/EcologicalNetworksDynamics.jl b/src/EcologicalNetworksDynamics.jl index 23c8f0f2..adc6613e 100644 --- a/src/EcologicalNetworksDynamics.jl +++ b/src/EcologicalNetworksDynamics.jl @@ -21,6 +21,18 @@ argerr(mess) = throw(ArgumentError(mess)) const Option{T} = Union{Nothing,T} const SparseMatrix{T} = SparseMatrixCSC{T,Int64} +# Basic equivalence relation for recursive use. +function equal_fields(a::T, b::T; ignore = Set{Symbol}()) where {T} + for name in fieldnames(T) + if name in ignore + continue + end + u, v = getfield.((a, b), name) + u == v || return false + end + true +end + include("./AliasingDicts/AliasingDicts.jl") using .AliasingDicts @@ -55,14 +67,6 @@ export add!, properties, blueprints, components include("./dedicate_framework_to_model.jl") -#------------------------------------------------------------------------------------------- -# Analysis tools working on the output of the simulation. -include("output-analysis.jl") -export richness -export persistence -export shannon_diversity -export total_biomass - #------------------------------------------------------------------------------------------- # "Outer" parts: develop user-facing stuff here. @@ -90,6 +94,14 @@ include("./methods/main.jl") include("./default_model.jl") include("./nontrophic_layers.jl") +#------------------------------------------------------------------------------------------- +# Analysis tools working on the output of the simulation. +include("output-analysis.jl") +export richness +export persistence +export shannon_diversity +export total_biomass + # Avoid Revise interruptions when redefining methods and properties. Framework.REVISING = true diff --git a/src/Framework/method_macro.jl b/src/Framework/method_macro.jl index e17acb15..ae1d5730 100644 --- a/src/Framework/method_macro.jl +++ b/src/Framework/method_macro.jl @@ -293,6 +293,12 @@ macro method(input...) end fn(s._value, args...; kwargs...) end + # TODO: if unexistent, then + # defining a fallback `fn(value, args...; _system=s, kwargs...)` method here + # would enable that implementors of `fn` take decisions + # depending on the whole system value, and components currently available. + # In particular, it would avoid the need + # to define both `_simulate` and `simulate` in the exposed lib. end, ) diff --git a/src/Framework/system.jl b/src/Framework/system.jl index b1bf2f2b..beff85aa 100644 --- a/src/Framework/system.jl +++ b/src/Framework/system.jl @@ -173,6 +173,20 @@ function properties(s::System{V}) where {V} end export properties +#------------------------------------------------------------------------------------------- + +# Basic recursive equivalence relation. +function equal_fields(a::T, b::T) where {T} + for name in fieldnames(T) + u, v = getfield.((a, b), name) + u == v || return false + end + true +end +Base.:(==)(a::System{U}, b::System{V}) where {U,V} = U == V && equal_fields(a, b) +Base.:(==)(a::Blueprint{U}, b::Blueprint{V}) where {U,V} = + U == V && typeof(a) == typeof(b) && equal_fields(a, b) + #------------------------------------------------------------------------------------------- # Display. function Base.show(io::IO, sys::System) diff --git a/src/Internals/Internals.jl b/src/Internals/Internals.jl index 4987a248..c0a9bb2c 100644 --- a/src/Internals/Internals.jl +++ b/src/Internals/Internals.jl @@ -58,6 +58,7 @@ const Option{T} = Union{Nothing,T} # Since parts of the API is being extracted out of this module to survive, # authorize using it here. using ..EcologicalNetworksDynamics +const equal_fields = EcologicalNetworksDynamics.equal_fields include("./macros.jl") include("./inputs/foodwebs.jl") diff --git a/src/Internals/inputs/biological_rates.jl b/src/Internals/inputs/biological_rates.jl index 91bc6362..d582597a 100644 --- a/src/Internals/inputs/biological_rates.jl +++ b/src/Internals/inputs/biological_rates.jl @@ -16,6 +16,8 @@ mutable struct BioRates end #### end #### +Base.:(==)(a::BioRates, b::BioRates) = equal_fields(a, b) + #### Type display #### """ One line [`BioRates`](@ref) display. diff --git a/src/Internals/inputs/environment.jl b/src/Internals/inputs/environment.jl index 79d42c62..4d1836e2 100644 --- a/src/Internals/inputs/environment.jl +++ b/src/Internals/inputs/environment.jl @@ -2,6 +2,8 @@ mutable struct Environment T::Float64 end +Base.:(==)(a::Environment, b::Environment) = equal_fields(a, b) + """ One line Environment display. """ diff --git a/src/Internals/inputs/foodwebs.jl b/src/Internals/inputs/foodwebs.jl index 4378a8d2..6794c17a 100644 --- a/src/Internals/inputs/foodwebs.jl +++ b/src/Internals/inputs/foodwebs.jl @@ -34,6 +34,8 @@ mutable struct FoodWeb <: EcologicalNetwork new(A, sp, M, mc, mth, Dict(Symbol(s) => i for (i, s) in enumerate(sp))) end +Base.:(==)(a::FoodWeb, b::FoodWeb) = equal_fields(a, b) + """ FoodWeb( A::AbstractMatrix; diff --git a/src/Internals/inputs/functional_response.jl b/src/Internals/inputs/functional_response.jl index d66b9812..15131c91 100644 --- a/src/Internals/inputs/functional_response.jl +++ b/src/Internals/inputs/functional_response.jl @@ -27,6 +27,9 @@ struct LinearResponse <: FunctionalResponse end #### end #### +Base.:(==)(a::U, b::V) where {U<:FunctionalResponse,V<:FunctionalResponse} = + U == V && equal_fields(a, b) + #### Type display #### """ One line display FunctionalResponse diff --git a/src/Internals/inputs/producer_growth.jl b/src/Internals/inputs/producer_growth.jl index 4c0df856..dd3b7dec 100644 --- a/src/Internals/inputs/producer_growth.jl +++ b/src/Internals/inputs/producer_growth.jl @@ -30,6 +30,9 @@ mutable struct NutrientIntake <: ProducerGrowth NutrientIntake(args...) = new(args...) end +Base.:(==)(a::U, b::V) where {U<:ProducerGrowth,V<:ProducerGrowth} = + U == V && equal_fields(a, b) + """ length(n::NutrientIntake) diff --git a/src/Internals/model/model_parameters.jl b/src/Internals/model/model_parameters.jl index c6e99939..443b42b0 100644 --- a/src/Internals/model/model_parameters.jl +++ b/src/Internals/model/model_parameters.jl @@ -46,10 +46,16 @@ mutable struct ModelParameters ModelParameters(args...) = new(args...) end # Required to fork the system. -# Ok as long as the value above contains no critical self-references. +# Self-references (very) fortunately are not a problem: +# https://discourse.julialang.org/t/how-is-deepcopy-so-clever-regarding-aliasing-and-self-reference/113235 Base.copy(m::ModelParameters) = deepcopy(m) #### end #### +# Ignore cached values for equivalence relation. +const model_equivalence_ignored_fields = Set([:_cache]) +Base.:(==)(a::ModelParameters, b::ModelParameters) = + equal_fields(a, b; ignore = model_equivalence_ignored_fields) + #### Type display #### """ One line ModelParameters display. diff --git a/src/Internals/model/simulate.jl b/src/Internals/model/simulate.jl index 8fc0c86a..eb418023 100644 --- a/src/Internals/model/simulate.jl +++ b/src/Internals/model/simulate.jl @@ -120,8 +120,16 @@ function simulate( ExtinctionCallback(extinction_threshold, params, verbose), ), diff_code_data = (dudt!, params), + # FROM THE FUTURE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # Record original user component-based model within the solution. + # The design should change during refactoring of the internals. + model = nothing, kwargs..., ) + isnothing(model) || + model._value === params || + throw("Inconsistent input to `simulate`: this is a bug in the package.") + # Interpret parameters and check them for consistency. S = richness(params) all(B0 .>= 0) || @@ -178,7 +186,13 @@ function simulate( u0 = B0 end - p = (params = data, extinct_sp = extinct_sp, original_params = params) + p = ( + params = data, + extinct_sp = extinct_sp, + original_params = params, + # Own the copy to not allow post-simulation modifications. + model = isnothing(model) ? nothing : copy(model), + ) timespan = (t0, tmax) problem = ODEProblem(fun, u0, timespan, p) sol = solve( diff --git a/src/dedicate_framework_to_model.jl b/src/dedicate_framework_to_model.jl index 04f90424..dc1049f8 100644 --- a/src/dedicate_framework_to_model.jl +++ b/src/dedicate_framework_to_model.jl @@ -93,17 +93,6 @@ Base.getproperty(v::InnerParms, p::Symbol) = Framework.unchecked_getproperty(v, Base.setproperty!(v::InnerParms, p::Symbol, rhs) = Framework.unchecked_setproperty!(v, p, rhs) -# For some reason this needs to be made explicit? -# Compare field by field for identity. -function Base.:(==)(a::ModelBlueprint, b::ModelBlueprint) - typeof(a) === typeof(b) || return false - for name in fieldnames(typeof(a)) - u, v = getfield.((a, b), name) - u == v || return false - end - true -end - # Skip _-prefixed properties. function properties(s::Model) res = [] diff --git a/src/methods/main.jl b/src/methods/main.jl index c1a56655..673a0598 100644 --- a/src/methods/main.jl +++ b/src/methods/main.jl @@ -1,8 +1,16 @@ # The methods defined here depends on several components, # which is the reason they live after all components specifications. +import SciMLBase: AbstractODESolution +const Solution = AbstractODESolution + # Major purpose of the whole model specification: simulate dynamics. -function simulate(model::InnerParms, u0, tmax::Integer; kwargs...) +# TODO: This actual system method is useful to check required components +# but is is *not* the function exposed +# because a reference to the original model needs to be forwarded down to the internals +# to save a copy next to the results, +# and the @method macro misses the feature of providing this reference yet. +function _simulate(model::InnerParms, u0, tmax::Integer; kwargs...) # Depart from the legacy Internal defaults. @kwargs_helpers kwargs @@ -22,8 +30,14 @@ function simulate(model::InnerParms, u0, tmax::Integer; kwargs...) Internals.simulate(model, u0; tmax, extinction_threshold, callback, verbose, kwargs...) end -@method simulate depends(FunctionalResponse, ProducerGrowth, Metabolism, Mortality) -export simulate +@method _simulate depends(FunctionalResponse, ProducerGrowth, Metabolism, Mortality) + +# This exposed method does forward reference down to the internals.. +simulate(model::Model, args...; kwargs...) = _simulate(model, args...; model, kwargs...) +# .. so that we *can* retrieve the original model from the simulation result. +get_model(sol::Solution) = copy(sol.prob.p.model) # (owned copy to not leak aliases) +export simulate, get_model + # Re-expose from internals so it works with the new API. extinction_callback(m, thr; verbose = false) = Internals.ExtinctionCallback(thr, m, verbose) diff --git a/src/output-analysis.jl b/src/output-analysis.jl index cc5b830c..80aa37a2 100644 --- a/src/output-analysis.jl +++ b/src/output-analysis.jl @@ -1,5 +1,3 @@ -import SciMLBase: AbstractODESolution - """ richness(solution::Solution; threshold = 0) diff --git a/test/user/06-post-simulation.jl b/test/user/06-post-simulation.jl new file mode 100644 index 00000000..78091723 --- /dev/null +++ b/test/user/06-post-simulation.jl @@ -0,0 +1,30 @@ +# Check post-simulation utils. + +using Random +Random.seed!(12) + +#------------------------------------------------------------------------------------------- +@testset "Retrieve model from simulation result." begin + + m = default_model(Foodweb([:a => :b, :b => :c])) + sol = simulate(m, 0.5, 500) + + # Retrieve model from the solution obtained. + msol = get_model(sol) + @test msol == m + + # The value we get is a fresh copy of the state at simulation time. + @test msol !== m # *Not* an alias. + + # Cannot be corrupted afterwards from the original value. + @test m.K[:c] == 1 + m.K[:c] = 2 + @test m.K[:c] == 2 # Okay to keep working on original value. + @test msol.K[:c] == 1 # Still true: simulation was done with 1, not 2. + + # Cannot be corrupted afterwards from the retrieved value itself. + msol.K[:c] = 3 + @test msol.K[:c] == 3 # Okay to work on this one: user owns it. + @test get_model(sol).K[:c] == 1 # Still true. + +end From 89da5e6fd03d6c1c285eb828317e8bc3b3146867 Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Fri, 19 Apr 2024 16:21:00 +0200 Subject: [PATCH 2/6] Minor fixes to basic output analyses functions. --- docs/make.jl | 2 +- src/EcologicalNetworksDynamics.jl | 4 -- src/output-analysis.jl | 70 +++++++++++++++---------------- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 54aec1e9..57374ca3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -45,4 +45,4 @@ makedocs(; ], ) -deploydocs(; repo = "github.com/BecksLab/EcologicalNetworksDynamics.jl", devbranch = "doc") +deploydocs(; repo = "github.com/BecksLab/EcologicalNetworksDynamics.jl", devbranch = "dev") diff --git a/src/EcologicalNetworksDynamics.jl b/src/EcologicalNetworksDynamics.jl index adc6613e..53ab5273 100644 --- a/src/EcologicalNetworksDynamics.jl +++ b/src/EcologicalNetworksDynamics.jl @@ -97,10 +97,6 @@ include("./nontrophic_layers.jl") #------------------------------------------------------------------------------------------- # Analysis tools working on the output of the simulation. include("output-analysis.jl") -export richness -export persistence -export shannon_diversity -export total_biomass # Avoid Revise interruptions when redefining methods and properties. Framework.REVISING = true diff --git a/src/output-analysis.jl b/src/output-analysis.jl index 80aa37a2..3e3060d8 100644 --- a/src/output-analysis.jl +++ b/src/output-analysis.jl @@ -1,5 +1,5 @@ """ -richness(solution::Solution; threshold = 0) + richness(solution::Solution; threshold = 0) Return the number of alive species at each timestep of the simulation. `solution` is the output of [`simulate`](@ref). @@ -14,7 +14,8 @@ Let's start with a simple example where the richness remains constant: julia> foodweb = Foodweb([0 0; 1 0]) m = default_model(foodweb) B0 = [0.5, 0.5] - sol = simulate(m, B0) + tmax = 100 + sol = simulate(m, B0, tmax) richness_trajectory = richness(sol) all(richness_trajectory .== 2) # At each timestep, there are 2 alive species. true @@ -26,36 +27,33 @@ We expect to observe a decrease in richness from 1 to 0 over time. ```jldoctest julia> B0 = [0, 0.5] # The producer is extinct at the beginning. - sol = simulate(m, B0) + sol = simulate(m, B0, 1_000) richness_trajectory = richness(sol) richness_trajectory[1] == 1 && richness_trajectory[end] == 0 true ``` """ -richness(solution::AbstractODESolution; threshold = 0) = richness.(solution.u; threshold) +richness(solution::Solution; threshold = 0) = richness.(solution.u; threshold) +export richness """ - richness(vec::AbstractVector; threshold = 0) + richness(biomasses::AbstractVector; threshold = 0) -Return the number of alive species given a biomass vector `vec`. +Return the number of alive species given a biomass vector. By default, species are considered extinct if their biomass is 0. But, this `threshold` can be changed using the corresponding keyword argument. # Examples ```jldoctest -julia> foodweb = Foodweb([0 0; 1 0]) - m = default_model(foodweb) - B0 = [0.5, 0.5] - sol = simulate(m, B0) - richness(sol[end]) # Richness at the end of the simulation. -2.0 +julia> richness([0.2, 0, 0.3]) # Only two species are non-extinct in this biomass vector. +2 ``` """ -richness(vec::AbstractVector; threshold = 0) = count(>(threshold), vec) +richness(biomasses::AbstractVector; threshold = 0) = count(>(threshold), biomasses) """ - persistence(solution::AbstractODESolution; threshold = 0) + persistence(solution::Solution; threshold = 0) Fraction of alive species at each timestep of the simulation. See [`richness`](@ref) for details. @@ -67,25 +65,25 @@ julia> S = 20 # Initial number of species. foodweb = Foodweb(:niche; S = 20, C = 0.1) m = default_model(foodweb) B0 = rand(S) - sol = simulate(m, B0) + sol = simulate(m, B0, 2000) all(persistence(sol) .== richness(sol) / S) true ``` """ -function persistence(solution::AbstractODESolution; threshold = 0) - persistence.(solution.u; threshold) -end +persistence(solution::Solution; threshold = 0) = persistence.(solution.u; threshold) +export persistence """ - persistence(vec::AbstractVector; threshold = 0) + persistence(biomasses::AbstractVector; threshold = 0) -Fraction of alive species given a biomass vector `vec`. +Fraction of alive species given a biomass vector. See [`richness`](@ref) for details. """ -persistence(vec::AbstractVector; threshold = 0) = richness(vec; threshold) / length(vec) +persistence(biomasses::AbstractVector; threshold = 0) = + richness(biomasses; threshold) / length(biomasses) """ - total_biomass(solution::AbstractODESolution) + total_biomass(solution::Solution) Total biomass of a community at each timestep of the simulation. `solution` is the output of [`simulate`](@ref). @@ -100,18 +98,19 @@ so we can observe the consumer's biomass decrease over time. julia> foodweb = Foodweb([0 0; 1 0]) m = default_model(foodweb) B0 = [0, 0.5] # The producer is extinct at the beginning. - sol = simulate(m, B0) + sol = simulate(m, B0, 1_000) biomass_trajectory = total_biomass(sol) biomass_trajectory[1] == 0.5 && biomass_trajectory[end] == 0 true ``` """ -total_biomass(solution::AbstractODESolution) = total_biomass.(solution.u) +total_biomass(solution::Solution) = total_biomass.(solution.u) +export total_biomass """ - total_biomass(vec::AbstractVector) + total_biomass(biomasses::AbstractVector) -Total biomass of a community given a biomass vector `vec`. +Total biomass of a community given a biomass vector. # Examples @@ -120,10 +119,10 @@ julia> total_biomass([0.5, 1.5]) # 0.5 + 1.5 = 2.0 2.0 ``` """ -total_biomass(vec::AbstractVector) = sum(vec) +total_biomass(biomasses::AbstractVector) = sum(biomasses) """ - shannon_diversity(solution::AbstractODESolution; threshold = 0) + shannon_diversity(solution::Solution; threshold = 0) Shannon diversity index at each timestep of the simulation. `solution` is the output of [`simulate`](@ref). @@ -142,20 +141,21 @@ as the biomass of the species diverge from each other. julia> foodweb = Foodweb([0 0; 1 0]) m = default_model(foodweb) B0 = [0.5, 0.5] # Even biomass, maximal shannon diversity. - sol = simulate(m, B0) + sol = simulate(m, B0, 1_000) shannon_trajectory = shannon_diversity(sol) biomass_trajectory[1] > biomass_trajectory[end] true ``` """ -function shannon_diversity(solution::AbstractODESolution; threshold = 0) +shannon_diversity(solution::Solution; threshold = 0) = shannon_diversity.(solution.u; threshold) -end +export shannon_diversity """ - shannon_diversity(vec::AbstractVector; threshold = 0) + shannon_diversity(biomasses::AbstractVector; threshold = 0) + +Shannon diversity index given a biomass vector. -Shannon diversitty index given a biomass vector `vec Shannon diversity is a measure of species diversity based on the entropy. According to the Shannon index, for a same number of species, the more evenly the biomass is distributed among them, @@ -176,8 +176,8 @@ true We observe as we decrease the biomass of the third species, the shannon diversity tends to 2, as we tend towards an effective two-species community. """ -function shannon_diversity(vec::AbstractVector; threshold = 0) - x = filter(>(threshold), vec) +function shannon_diversity(biomasses::AbstractVector; threshold = 0) + x = filter(>(threshold), biomasses) p = x ./ sum(x) exp(-sum(p .* log.(p))) end From 05d1a3ad838652598f92530e51ded0e8f4635f1b Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Mon, 22 Apr 2024 09:49:26 +0200 Subject: [PATCH 3/6] Update Manifest.toml to reproduce exact extinction dates. DifferentialEquations seems to have changed its default solver, which made extinction dates non-reproducible? --- Manifest.toml | 547 +++++++++++++++++---------- test/internals/model/test-zombies.jl | 2 +- test/user/04-default_model.jl | 10 + test/user/05-basic_pipelines.jl | 34 +- test/user/runtests.jl | 4 +- 5 files changed, 372 insertions(+), 225 deletions(-) diff --git a/Manifest.toml b/Manifest.toml index 2ae6b0d2..83f77f14 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,13 +1,18 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.10.2" +julia_version = "1.10.4" manifest_format = "2.0" project_hash = "4431f0dd80c721fac47d7a8f4e667cf2a0e9643e" [[deps.ADTypes]] -git-tree-sha1 = "016833eb52ba2d6bea9fcb50ca295980e728ee24" +git-tree-sha1 = "aa4d425271a914d8c4af6ad9fccb6eb3aec662c7" uuid = "47edcb42-4c32-4615-8424-f2b9edc5f35b" -version = "0.2.7" +version = "1.6.1" +weakdeps = ["ChainRulesCore", "EnzymeCore"] + + [deps.ADTypes.extensions] + ADTypesChainRulesCoreExt = "ChainRulesCore" + ADTypesEnzymeCoreExt = "EnzymeCore" [[deps.ANSIColoredPrinters]] git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" @@ -19,6 +24,27 @@ git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" version = "0.4.5" +[[deps.Accessors]] +deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "LinearAlgebra", "MacroTools", "Markdown", "Test"] +git-tree-sha1 = "f61b15be1d76846c0ce31d3fcfac5380ae53db6a" +uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" +version = "0.1.37" + + [deps.Accessors.extensions] + AccessorsAxisKeysExt = "AxisKeys" + AccessorsIntervalSetsExt = "IntervalSets" + AccessorsStaticArraysExt = "StaticArrays" + AccessorsStructArraysExt = "StructArrays" + AccessorsUnitfulExt = "Unitful" + + [deps.Accessors.weakdeps] + AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + Requires = "ae029012-a4dd-5104-9daa-d747884805df" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + [[deps.Adapt]] deps = ["LinearAlgebra", "Requires"] git-tree-sha1 = "6a55b747d1812e699320963ffde36f1ebdda4099" @@ -29,47 +55,57 @@ weakdeps = ["StaticArrays"] [deps.Adapt.extensions] AdaptStaticArraysExt = "StaticArrays" +[[deps.AliasTables]] +deps = ["PtrArrays", "Random"] +git-tree-sha1 = "9876e1e164b144ca45e9e3198d0b689cadfed9ff" +uuid = "66dad0bd-aa9a-41b7-9441-69ab47430ed8" +version = "1.1.3" + [[deps.ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" version = "1.1.1" [[deps.ArnoldiMethod]] deps = ["LinearAlgebra", "Random", "StaticArrays"] -git-tree-sha1 = "62e51b39331de8911e4a7ff6f5aaf38a5f4cc0ae" +git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6" uuid = "ec485272-7323-5ecc-a04f-4719b315124d" -version = "0.2.0" +version = "0.4.0" [[deps.ArrayInterface]] -deps = ["Adapt", "LinearAlgebra", "SparseArrays", "SuiteSparse"] -git-tree-sha1 = "44691067188f6bd1b2289552a23e4b7572f4528d" +deps = ["Adapt", "LinearAlgebra"] +git-tree-sha1 = "8c5b39db37c1d0340bf3b14895fba160c2d6cbb5" uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" -version = "7.9.0" +version = "7.14.0" [deps.ArrayInterface.extensions] ArrayInterfaceBandedMatricesExt = "BandedMatrices" ArrayInterfaceBlockBandedMatricesExt = "BlockBandedMatrices" ArrayInterfaceCUDAExt = "CUDA" + ArrayInterfaceCUDSSExt = "CUDSS" ArrayInterfaceChainRulesExt = "ChainRules" ArrayInterfaceGPUArraysCoreExt = "GPUArraysCore" ArrayInterfaceReverseDiffExt = "ReverseDiff" - ArrayInterfaceStaticArraysCoreExt = "StaticArraysCore" + ArrayInterfaceSparseArraysExt = "SparseArrays" + ArrayInterfaceStaticArraysExt = "StaticArrays" ArrayInterfaceTrackerExt = "Tracker" [deps.ArrayInterface.weakdeps] BandedMatrices = "aae01518-5342-5314-be14-df237901396f" BlockBandedMatrices = "ffab5731-97b5-5995-9138-79e8c1846df0" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" + CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" ChainRules = "082447d4-558c-5d27-93f4-14fc19e9eca2" GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267" - StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Tracker = "9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c" [[deps.ArrayLayouts]] deps = ["FillArrays", "LinearAlgebra"] -git-tree-sha1 = "2aeaeaff72cdedaa0b5f30dfb8c1f16aefdac65d" +git-tree-sha1 = "ce2ca959f932f5dad70697dd93133d1167cf1e4e" uuid = "4c555306-a7a7-4459-81d9-ec55ddd5c99a" -version = "1.7.0" +version = "1.10.2" weakdeps = ["SparseArrays"] [deps.ArrayLayouts.extensions] @@ -80,9 +116,9 @@ uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" [[deps.BandedMatrices]] deps = ["ArrayLayouts", "FillArrays", "LinearAlgebra", "PrecompileTools"] -git-tree-sha1 = "c946c5014cf4cdbfacacb363b110e7bffba3e742" +git-tree-sha1 = "71f605effb24081b09cae943ba39ef9ca90c04f4" uuid = "aae01518-5342-5314-be14-df237901396f" -version = "1.6.1" +version = "1.7.2" weakdeps = ["SparseArrays"] [deps.BandedMatrices.extensions] @@ -92,29 +128,27 @@ weakdeps = ["SparseArrays"] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" [[deps.BitFlags]] -git-tree-sha1 = "2dc09997850d68179b69dafb58ae806167a32b1b" +git-tree-sha1 = "0691e34b3bb8be9307330f88d1a3c3f25466c24d" uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35" -version = "0.1.8" +version = "0.1.9" [[deps.BitTwiddlingConvenienceFunctions]] deps = ["Static"] -git-tree-sha1 = "0c5f81f47bbbcf4aea7b2959135713459170798b" +git-tree-sha1 = "f21cfd4950cb9f0587d5067e69405ad2acd27b87" uuid = "62783981-4cbd-42fc-bca8-16325de8dc4b" -version = "0.1.5" +version = "0.1.6" [[deps.BoundaryValueDiffEq]] -deps = ["ADTypes", "Adapt", "ArrayInterface", "BandedMatrices", "ConcreteStructs", "DiffEqBase", "FastAlmostBandedMatrices", "ForwardDiff", "LinearAlgebra", "LinearSolve", "NonlinearSolve", "PreallocationTools", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase", "Setfield", "SparseArrays", "SparseDiffTools", "Tricks", "TruncatedStacktraces", "UnPack"] -git-tree-sha1 = "3ff968887be48760b0e9e8650c2d05c96cdea9d8" +deps = ["ADTypes", "Adapt", "ArrayInterface", "BandedMatrices", "ConcreteStructs", "DiffEqBase", "FastAlmostBandedMatrices", "FastClosures", "ForwardDiff", "LinearAlgebra", "LinearSolve", "Logging", "NonlinearSolve", "OrdinaryDiffEq", "PreallocationTools", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase", "Setfield", "SparseArrays", "SparseDiffTools"] +git-tree-sha1 = "4e746d02f1d7ef513c1441ee58f3b20f5d10ad03" uuid = "764a87c0-6b3e-53db-9096-fe964310641d" -version = "5.6.3" +version = "5.9.0" [deps.BoundaryValueDiffEq.extensions] BoundaryValueDiffEqODEInterfaceExt = "ODEInterface" - BoundaryValueDiffEqOrdinaryDiffEqExt = "OrdinaryDiffEq" [deps.BoundaryValueDiffEq.weakdeps] ODEInterface = "54ca160b-1b9f-5127-a996-1867f4bc2a2c" - OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" [[deps.CEnum]] git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc" @@ -123,9 +157,9 @@ version = "0.5.0" [[deps.CPUSummary]] deps = ["CpuId", "IfElse", "PrecompileTools", "Static"] -git-tree-sha1 = "601f7e7b3d36f18790e2caf83a882d88e9b71ff1" +git-tree-sha1 = "5a97e67919535d6841172016c9530fd69494e5ec" uuid = "2a0fbf3d-bb9c-48f3-b0a9-814d99fd7ab9" -version = "0.2.4" +version = "0.2.6" [[deps.Calculus]] deps = ["LinearAlgebra"] @@ -135,9 +169,9 @@ version = "0.5.1" [[deps.ChainRulesCore]] deps = ["Compat", "LinearAlgebra"] -git-tree-sha1 = "575cd02e080939a33b6df6c5853d14924c08e35b" +git-tree-sha1 = "71acdbf594aab5bbb2cec89b208c41b4c411e49f" uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" -version = "1.23.0" +version = "1.24.0" weakdeps = ["SparseArrays"] [deps.ChainRulesCore.extensions] @@ -145,15 +179,15 @@ weakdeps = ["SparseArrays"] [[deps.CloseOpenIntervals]] deps = ["Static", "StaticArrayInterface"] -git-tree-sha1 = "70232f82ffaab9dc52585e0dd043b5e0c6b714f1" +git-tree-sha1 = "05ba0d07cd4fd8b7a39541e31a7b0254704ea581" uuid = "fb6a15b2-703c-40df-9091-08a04967cfa9" -version = "0.1.12" +version = "0.1.13" [[deps.CodecZlib]] deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "59939d8a997469ee05c4b4944560a820f9ba0d73" +git-tree-sha1 = "b8fe8546d52ca154ac556809e10c75e6e7430ac8" uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.4" +version = "0.7.5" [[deps.CommonSolve]] git-tree-sha1 = "0eee5eb66b1cf62cd6ad1b460238e60e4b09400c" @@ -166,11 +200,16 @@ git-tree-sha1 = "7b8a93dba8af7e3b42fecabf646260105ac373f7" uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" version = "0.3.0" +[[deps.CommonWorldInvalidations]] +git-tree-sha1 = "ae52d1c52048455e85a387fbee9be553ec2b68d0" +uuid = "f70d9fcc-98c5-4d4a-abd7-e4cdeebd8ca8" +version = "1.0.0" + [[deps.Compat]] deps = ["TOML", "UUIDs"] -git-tree-sha1 = "c955881e3c981181362ae4088b35995446298b80" +git-tree-sha1 = "b1c55339b7c6c350ee89f2c1604299660525b248" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.14.0" +version = "4.15.0" weakdeps = ["Dates", "LinearAlgebra"] [deps.Compat.extensions] @@ -179,7 +218,16 @@ weakdeps = ["Dates", "LinearAlgebra"] [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.1.0+0" +version = "1.1.1+0" + +[[deps.CompositionsBase]] +git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" +uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b" +version = "0.1.2" +weakdeps = ["InverseFunctions"] + + [deps.CompositionsBase.extensions] + CompositionsBaseInverseFunctionsExt = "InverseFunctions" [[deps.ConcreteStructs]] git-tree-sha1 = "f749037478283d372048690eb3b5f92a79432b34" @@ -188,15 +236,15 @@ version = "0.2.3" [[deps.ConcurrentUtilities]] deps = ["Serialization", "Sockets"] -git-tree-sha1 = "6cbbd4d241d7e6579ab354737f4dd95ca43946e1" +git-tree-sha1 = "ea32b83ca4fefa1768dc84e504cc0a94fb1ab8d1" uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" -version = "2.4.1" +version = "2.4.2" [[deps.ConstructionBase]] deps = ["LinearAlgebra"] -git-tree-sha1 = "260fd2400ed2dab602a7c15cf10c1933c59930a2" +git-tree-sha1 = "d8a9c0b6ac2d9081bf76324b39c78ca3ce4f0c98" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" -version = "1.5.5" +version = "1.5.6" [deps.ConstructionBase.extensions] ConstructionBaseIntervalSetsExt = "IntervalSets" @@ -224,9 +272,9 @@ version = "1.16.0" [[deps.DataStructures]] deps = ["Compat", "InteractiveUtils", "OrderedCollections"] -git-tree-sha1 = "0f4b5d62a88d8f59003e43c25a8a90de9eb76317" +git-tree-sha1 = "1d0a14036acb104d9e89698bd408f63ab58cdc82" uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.18.18" +version = "0.18.20" [[deps.DataValueInterfaces]] git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" @@ -244,17 +292,18 @@ version = "0.4.1" [[deps.DelayDiffEq]] deps = ["ArrayInterface", "DataStructures", "DiffEqBase", "LinearAlgebra", "Logging", "OrdinaryDiffEq", "Printf", "RecursiveArrayTools", "Reexport", "SciMLBase", "SimpleNonlinearSolve", "SimpleUnPack"] -git-tree-sha1 = "bfae672496149b369172eae6296290a381df2bdf" +git-tree-sha1 = "f84e4ef36cb68b77fe10c77bdf59c980709f6fdf" uuid = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb" -version = "5.47.1" +version = "5.47.4" [[deps.DiffEqBase]] -deps = ["ArrayInterface", "DataStructures", "DocStringExtensions", "EnumX", "EnzymeCore", "FastBroadcast", "ForwardDiff", "FunctionWrappers", "FunctionWrappersWrappers", "LinearAlgebra", "Logging", "Markdown", "MuladdMacro", "Parameters", "PreallocationTools", "PrecompileTools", "Printf", "RecursiveArrayTools", "Reexport", "SciMLBase", "SciMLOperators", "Setfield", "SparseArrays", "Static", "StaticArraysCore", "Statistics", "Tricks", "TruncatedStacktraces"] -git-tree-sha1 = "b19b2bb1ecd1271334e4b25d605e50f75e68fcae" +deps = ["ArrayInterface", "ConcreteStructs", "DataStructures", "DocStringExtensions", "EnumX", "EnzymeCore", "FastBroadcast", "FastClosures", "ForwardDiff", "FunctionWrappers", "FunctionWrappersWrappers", "LinearAlgebra", "Logging", "Markdown", "MuladdMacro", "Parameters", "PreallocationTools", "PrecompileTools", "Printf", "RecursiveArrayTools", "Reexport", "SciMLBase", "SciMLOperators", "Setfield", "SparseArrays", "Static", "StaticArraysCore", "Statistics", "Tricks", "TruncatedStacktraces"] +git-tree-sha1 = "d1e8a4642e28b0945bde6e2e1ac569b9e0abd728" uuid = "2b5f629d-d688-5b77-993f-72d75c75574e" -version = "6.148.0" +version = "6.151.5" [deps.DiffEqBase.extensions] + DiffEqBaseCUDAExt = "CUDA" DiffEqBaseChainRulesCoreExt = "ChainRulesCore" DiffEqBaseDistributionsExt = "Distributions" DiffEqBaseEnzymeExt = ["ChainRulesCore", "Enzyme"] @@ -267,6 +316,7 @@ version = "6.148.0" DiffEqBaseUnitfulExt = "Unitful" [deps.DiffEqBase.weakdeps] + CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" @@ -280,16 +330,16 @@ version = "6.148.0" [[deps.DiffEqCallbacks]] deps = ["DataStructures", "DiffEqBase", "ForwardDiff", "Functors", "LinearAlgebra", "Markdown", "NonlinearSolve", "Parameters", "RecipesBase", "RecursiveArrayTools", "SciMLBase", "StaticArraysCore"] -git-tree-sha1 = "a731383bbafb87d496fb5e66f60c40e4a5f8f726" +git-tree-sha1 = "c959cfd2657d16beada157a74d52269e8556500e" uuid = "459566f4-90b8-5000-8ac3-15dfb0a30def" -version = "3.4.0" +version = "3.6.2" weakdeps = ["OrdinaryDiffEq", "Sundials"] [[deps.DiffEqNoiseProcess]] deps = ["DiffEqBase", "Distributions", "GPUArraysCore", "LinearAlgebra", "Markdown", "Optim", "PoissonRandom", "QuadGK", "Random", "Random123", "RandomNumbers", "RecipesBase", "RecursiveArrayTools", "Requires", "ResettableStacks", "SciMLBase", "StaticArraysCore", "Statistics"] -git-tree-sha1 = "65cbbe1450ced323b4b17228ccd96349d96795a7" +git-tree-sha1 = "ed0158e758723b4d429afbbb5d98c5afd3458dc1" uuid = "77a26b50-5914-5dd7-bc55-306e6241c503" -version = "5.21.0" +version = "5.22.0" [deps.DiffEqNoiseProcess.extensions] DiffEqNoiseProcessReverseDiffExt = "ReverseDiff" @@ -315,6 +365,42 @@ git-tree-sha1 = "81042254a307980b8ab5b67033aca26c2e157ebb" uuid = "0c46a032-eb83-5123-abaf-570d42b7fbaa" version = "7.13.0" +[[deps.DifferentiationInterface]] +deps = ["ADTypes", "Compat", "DocStringExtensions", "FillArrays", "LinearAlgebra", "PackageExtensionCompat", "SparseArrays", "SparseMatrixColorings"] +git-tree-sha1 = "c81579b549a00edf31582d318fec06523e0b607a" +uuid = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +version = "0.5.9" + + [deps.DifferentiationInterface.extensions] + DifferentiationInterfaceChainRulesCoreExt = "ChainRulesCore" + DifferentiationInterfaceDiffractorExt = "Diffractor" + DifferentiationInterfaceEnzymeExt = "Enzyme" + DifferentiationInterfaceFastDifferentiationExt = "FastDifferentiation" + DifferentiationInterfaceFiniteDiffExt = "FiniteDiff" + DifferentiationInterfaceFiniteDifferencesExt = "FiniteDifferences" + DifferentiationInterfaceForwardDiffExt = "ForwardDiff" + DifferentiationInterfacePolyesterForwardDiffExt = "PolyesterForwardDiff" + DifferentiationInterfaceReverseDiffExt = "ReverseDiff" + DifferentiationInterfaceSymbolicsExt = "Symbolics" + DifferentiationInterfaceTapirExt = "Tapir" + DifferentiationInterfaceTrackerExt = "Tracker" + DifferentiationInterfaceZygoteExt = ["Zygote", "ForwardDiff"] + + [deps.DifferentiationInterface.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + Diffractor = "9f5e2b26-1114-432f-b630-d3fe2085c51c" + Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" + FastDifferentiation = "eb9bf01b-bf85-4b60-bf87-ee5de06c00be" + FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41" + FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + PolyesterForwardDiff = "98d1487c-24ca-40b6-b7ab-df2af84e126b" + ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267" + Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" + Tapir = "07d77754-e150-4737-8c94-cd238a1fb45b" + Tracker = "9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c" + Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + [[deps.Distances]] deps = ["LinearAlgebra", "Statistics", "StatsAPI"] git-tree-sha1 = "66c4c81f259586e8f002eacebc177e1fb06363b0" @@ -331,10 +417,10 @@ deps = ["Random", "Serialization", "Sockets"] uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" [[deps.Distributions]] -deps = ["FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] -git-tree-sha1 = "7c302d7a5fec5214eb8a5a4c466dcf7a51fcf169" +deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] +git-tree-sha1 = "9c405847cc7ecda2dc921ccf18b47ca150d7317e" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" -version = "0.25.107" +version = "0.25.109" [deps.Distributions.extensions] DistributionsChainRulesCoreExt = "ChainRulesCore" @@ -354,9 +440,9 @@ version = "0.9.3" [[deps.Documenter]] deps = ["ANSIColoredPrinters", "AbstractTrees", "Base64", "CodecZlib", "Dates", "DocStringExtensions", "Downloads", "Git", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "MarkdownAST", "Pkg", "PrecompileTools", "REPL", "RegistryInstances", "SHA", "TOML", "Test", "Unicode"] -git-tree-sha1 = "4a40af50e8b24333b9ec6892546d9ca5724228eb" +git-tree-sha1 = "76deb8c15f37a3853f13ea2226b8f2577652de05" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "1.3.0" +version = "1.5.0" [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] @@ -375,9 +461,9 @@ uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" version = "1.0.4" [[deps.EnzymeCore]] -git-tree-sha1 = "59c44d8fbc651c0395d8a6eda64b05ce316f58b4" +git-tree-sha1 = "d445df66dd8761a4c27df950db89c6a3a0629fe7" uuid = "f151be2c-9106-41f4-ab19-57ee4f262869" -version = "0.6.5" +version = "0.7.7" weakdeps = ["Adapt"] [deps.EnzymeCore.extensions] @@ -391,9 +477,9 @@ version = "0.1.10" [[deps.Expat_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "4558ab818dcceaab612d1bb8c19cee87eda2b83c" +git-tree-sha1 = "1c6317308b9dc757616f0b5cb379db10494443a7" uuid = "2e619515-83b5-522b-bb60-26c02a35a201" -version = "2.5.0+0" +version = "2.6.2+0" [[deps.ExponentialUtilities]] deps = ["Adapt", "ArrayInterface", "GPUArraysCore", "GenericSchur", "LinearAlgebra", "PrecompileTools", "Printf", "SparseArrays", "libblastrampoline_jll"] @@ -406,22 +492,28 @@ git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" version = "0.1.10" +[[deps.Expronicon]] +deps = ["MLStyle", "Pkg", "TOML"] +git-tree-sha1 = "fc3951d4d398b5515f91d7fe5d45fc31dccb3c9b" +uuid = "6b7a57c9-7cc1-4fdf-b7f5-e857abae3636" +version = "0.8.5" + [[deps.Extents]] -git-tree-sha1 = "2140cd04483da90b2da7f99b2add0750504fc39c" +git-tree-sha1 = "94997910aca72897524d2237c41eb852153b0f65" uuid = "411431e0-e8b7-467b-b5e0-f676ba4f2910" -version = "0.1.2" +version = "0.1.3" [[deps.FastAlmostBandedMatrices]] deps = ["ArrayInterface", "ArrayLayouts", "BandedMatrices", "ConcreteStructs", "LazyArrays", "LinearAlgebra", "MatrixFactorizations", "PrecompileTools", "Reexport"] -git-tree-sha1 = "178316d87f883f0702e79d9c83a8049484c9f619" +git-tree-sha1 = "a92b5820ea38da3b50b626cc55eba2b074bb0366" uuid = "9d29842c-ecb8-4973-b1e9-a27b1157504e" -version = "0.1.0" +version = "0.1.3" [[deps.FastBroadcast]] deps = ["ArrayInterface", "LinearAlgebra", "Polyester", "Static", "StaticArrayInterface", "StrideArraysCore"] -git-tree-sha1 = "a6e756a880fc419c8b41592010aebe6a5ce09136" +git-tree-sha1 = "ab1b34570bcdf272899062e1a56285a53ecaae08" uuid = "7034ab61-46d4-4ed7-9d0f-46aef9175898" -version = "0.2.8" +version = "0.3.5" [[deps.FastClosures]] git-tree-sha1 = "acebe244d53ee1b461970f8910c235b259e772ef" @@ -430,18 +522,18 @@ version = "0.3.2" [[deps.FastLapackInterface]] deps = ["LinearAlgebra"] -git-tree-sha1 = "0a59c7d1002f3131de53dc4568a47d15a44daef7" +git-tree-sha1 = "cbf5edddb61a43669710cbc2241bc08b36d9e660" uuid = "29a986be-02c6-4525-aec4-84b980013641" -version = "2.0.2" +version = "2.0.4" [[deps.FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" [[deps.FillArrays]] -deps = ["LinearAlgebra", "Random"] -git-tree-sha1 = "5b93957f6dcd33fc343044af3d48c215be2562f1" +deps = ["LinearAlgebra"] +git-tree-sha1 = "0653c0a2396a6da5bc4766c43041ef5fd3efbe57" uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" -version = "1.9.3" +version = "1.11.0" weakdeps = ["PDMats", "SparseArrays", "Statistics"] [deps.FillArrays.extensions] @@ -451,9 +543,9 @@ weakdeps = ["PDMats", "SparseArrays", "Statistics"] [[deps.FiniteDiff]] deps = ["ArrayInterface", "LinearAlgebra", "Requires", "Setfield", "SparseArrays"] -git-tree-sha1 = "bc0c5092d6caaea112d3c8e3b238d61563c58d5f" +git-tree-sha1 = "2de436b72c3422940cbe1367611d137008af7ec3" uuid = "6a86dc24-6348-571c-b903-95158fe2bd41" -version = "2.23.0" +version = "2.23.1" [deps.FiniteDiff.extensions] FiniteDiffBandedMatricesExt = "BandedMatrices" @@ -488,9 +580,9 @@ version = "0.1.3" [[deps.Functors]] deps = ["LinearAlgebra"] -git-tree-sha1 = "8ae30e786837ce0a24f5e2186938bf3251ab94b2" +git-tree-sha1 = "8a66c07630d6428eaab3506a0eabfcf4a9edea05" uuid = "d9f16b24-f501-4c13-a1f2-28368ffc5196" -version = "0.4.8" +version = "0.4.11" [[deps.Future]] deps = ["Random"] @@ -504,15 +596,15 @@ version = "0.1.6" [[deps.GenericSchur]] deps = ["LinearAlgebra", "Printf"] -git-tree-sha1 = "fb69b2a645fa69ba5f474af09221b9308b160ce6" +git-tree-sha1 = "af49a0851f8113fcfae2ef5027c6d49d0acec39b" uuid = "c145ed77-6b09-5dd9-b285-bf645a82121e" -version = "0.5.3" +version = "0.5.4" [[deps.GeoInterface]] deps = ["Extents"] -git-tree-sha1 = "d4f85701f569584f2cff7ba67a137d03f0cfb7d0" +git-tree-sha1 = "9fff8990361d5127b770e3454488360443019bb3" uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" -version = "1.3.3" +version = "1.3.5" [[deps.Git]] deps = ["Git_jll"] @@ -522,27 +614,27 @@ version = "1.3.1" [[deps.Git_jll]] deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "12945451c5d0e2d0dca0724c3a8d6448b46bbdf9" +git-tree-sha1 = "d18fb8a1f3609361ebda9bf029b60fd0f120c809" uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" -version = "2.44.0+1" +version = "2.44.0+2" [[deps.Graphs]] deps = ["ArnoldiMethod", "Compat", "DataStructures", "Distributed", "Inflate", "LinearAlgebra", "Random", "SharedArrays", "SimpleTraits", "SparseArrays", "Statistics"] -git-tree-sha1 = "899050ace26649433ef1af25bc17a815b3db52b7" +git-tree-sha1 = "ebd18c326fa6cee1efb7da9a3b45cf69da2ed4d9" uuid = "86223c79-3864-5bf0-83f7-82e725a168b6" -version = "1.9.0" +version = "1.11.2" [[deps.HTTP]] deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] -git-tree-sha1 = "995f762e0182ebc50548c434c171a5bb6635f8e4" +git-tree-sha1 = "d1d712be3164d61d1fb98e7ce9bcbc6cc06b45ed" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "1.10.4" +version = "1.10.8" [[deps.HostCPUFeatures]] deps = ["BitTwiddlingConvenienceFunctions", "IfElse", "Libdl", "Static"] -git-tree-sha1 = "eb8fed28f4994600e29beef49744639d985a04b2" +git-tree-sha1 = "8e070b599339d622e9a081d17230d74a5c473293" uuid = "3e5b6fbb-0976-4d2c-9146-d79de83f2fb0" -version = "0.1.16" +version = "0.1.17" [[deps.HypergeometricFunctions]] deps = ["DualNumbers", "LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"] @@ -552,9 +644,9 @@ version = "0.3.23" [[deps.IOCapture]] deps = ["Logging", "Random"] -git-tree-sha1 = "8b72179abc660bfab5e28472e019392b97d0985c" +git-tree-sha1 = "b6d6bfdd7ce25b0f9b2f6b3dd56b2673a66c8770" uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" -version = "0.2.4" +version = "0.2.5" [[deps.IfElse]] git-tree-sha1 = "debdd00ffef04665ccbb3e150747a77560e8fad1" @@ -562,20 +654,30 @@ uuid = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173" version = "0.1.1" [[deps.Inflate]] -git-tree-sha1 = "ea8031dea4aff6bd41f1df8f2fdfb25b33626381" +git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" -version = "0.1.4" +version = "0.1.5" [[deps.IntelOpenMP_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "5fdf2fe6724d8caabf43b557b84ce53f3b7e2f6b" +git-tree-sha1 = "14eb2b542e748570b56446f4c50fbfb2306ebc45" uuid = "1d5cc7b8-4909-519e-a0f8-d0f5ad9712d0" -version = "2024.0.2+0" +version = "2024.2.0+0" [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +[[deps.InverseFunctions]] +deps = ["Test"] +git-tree-sha1 = "18c59411ece4838b18cd7f537e56cf5e41ce5bfd" +uuid = "3587e190-3f89-42d0-90ee-14403ec27112" +version = "0.1.15" +weakdeps = ["Dates"] + + [deps.InverseFunctions.extensions] + DatesExt = "Dates" + [[deps.IrrationalConstants]] git-tree-sha1 = "630b497eafcc20001bba38a4651b327dcfc491d2" uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" @@ -599,15 +701,12 @@ uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" version = "0.21.4" [[deps.JumpProcesses]] -deps = ["ArrayInterface", "DataStructures", "DiffEqBase", "DocStringExtensions", "FunctionWrappers", "Graphs", "LinearAlgebra", "Markdown", "PoissonRandom", "Random", "RandomNumbers", "RecursiveArrayTools", "Reexport", "SciMLBase", "StaticArrays", "UnPack"] -git-tree-sha1 = "c451feb97251965a9fe40bacd62551a72cc5902c" +deps = ["ArrayInterface", "DataStructures", "DiffEqBase", "DocStringExtensions", "FunctionWrappers", "Graphs", "LinearAlgebra", "Markdown", "PoissonRandom", "Random", "RandomNumbers", "RecursiveArrayTools", "Reexport", "SciMLBase", "StaticArrays", "SymbolicIndexingInterface", "UnPack"] +git-tree-sha1 = "f12000093078e3dea1ee15de8bb35cfdc0014d97" uuid = "ccbc3e58-028d-4f4c-8cd5-9ae44345cda5" -version = "9.10.1" +version = "9.12.0" weakdeps = ["FastBroadcast"] - [deps.JumpProcesses.extensions] - JumpProcessFastBroadcastExt = "FastBroadcast" - [[deps.KLU]] deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse_jll"] git-tree-sha1 = "07649c499349dad9f08dde4243a4c597064663e9" @@ -616,15 +715,15 @@ version = "0.6.0" [[deps.Krylov]] deps = ["LinearAlgebra", "Printf", "SparseArrays"] -git-tree-sha1 = "8a6837ec02fe5fb3def1abc907bb802ef11a0729" +git-tree-sha1 = "267dad6b4b7b5d529c76d40ff48d33f7e94cb834" uuid = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7" -version = "0.9.5" +version = "0.9.6" [[deps.LayoutPointers]] deps = ["ArrayInterface", "LinearAlgebra", "ManualMemory", "SIMDTypes", "Static", "StaticArrayInterface"] -git-tree-sha1 = "62edfee3211981241b57ff1cedf4d74d79519277" +git-tree-sha1 = "a9eaadb366f5493a5654e843864c13d8b107548c" uuid = "10f19ff3-798f-405d-979b-55457f8fc047" -version = "0.1.15" +version = "0.1.17" [[deps.LazilyInitializedFields]] git-tree-sha1 = "8f7f3cabab0fd1800699663533b6d5cb3fc0e612" @@ -632,15 +731,23 @@ uuid = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf" version = "1.2.2" [[deps.LazyArrays]] -deps = ["ArrayLayouts", "FillArrays", "LinearAlgebra", "MacroTools", "MatrixFactorizations", "SparseArrays"] -git-tree-sha1 = "9cfca23ab83b0dfac93cb1a1ef3331ab9fe596a5" +deps = ["ArrayLayouts", "FillArrays", "LinearAlgebra", "MacroTools", "SparseArrays"] +git-tree-sha1 = "b8ea0abe6cc872996e87356951d286d25d485aba" uuid = "5078a376-72f3-5289-bfd5-ec5146d43c02" -version = "1.8.3" -weakdeps = ["StaticArrays"] +version = "2.1.9" [deps.LazyArrays.extensions] + LazyArraysBandedMatricesExt = "BandedMatrices" + LazyArraysBlockArraysExt = "BlockArrays" + LazyArraysBlockBandedMatricesExt = "BlockBandedMatrices" LazyArraysStaticArraysExt = "StaticArrays" + [deps.LazyArrays.weakdeps] + BandedMatrices = "aae01518-5342-5314-be14-df237901396f" + BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" + BlockBandedMatrices = "ffab5731-97b5-5995-9138-79e8c1846df0" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + [[deps.LazyArtifacts]] deps = ["Artifacts", "Pkg"] uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" @@ -695,15 +802,16 @@ deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" [[deps.LinearSolve]] -deps = ["ArrayInterface", "ChainRulesCore", "ConcreteStructs", "DocStringExtensions", "EnumX", "FastLapackInterface", "GPUArraysCore", "InteractiveUtils", "KLU", "Krylov", "Libdl", "LinearAlgebra", "MKL_jll", "Markdown", "PrecompileTools", "Preferences", "RecursiveFactorization", "Reexport", "SciMLBase", "SciMLOperators", "Setfield", "SparseArrays", "Sparspak", "StaticArraysCore", "UnPack"] -git-tree-sha1 = "73d8f61f8d27f279edfbafc93faaea93ea447e94" +deps = ["ArrayInterface", "ChainRulesCore", "ConcreteStructs", "DocStringExtensions", "EnumX", "FastLapackInterface", "GPUArraysCore", "InteractiveUtils", "KLU", "Krylov", "LazyArrays", "Libdl", "LinearAlgebra", "MKL_jll", "Markdown", "PrecompileTools", "Preferences", "RecursiveFactorization", "Reexport", "SciMLBase", "SciMLOperators", "Setfield", "SparseArrays", "Sparspak", "StaticArraysCore", "UnPack"] +git-tree-sha1 = "b2e2dba60642e07c062eb3143770d7e234316772" uuid = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" -version = "2.27.0" +version = "2.30.2" [deps.LinearSolve.extensions] LinearSolveBandedMatricesExt = "BandedMatrices" LinearSolveBlockDiagonalsExt = "BlockDiagonals" LinearSolveCUDAExt = "CUDA" + LinearSolveCUDSSExt = "CUDSS" LinearSolveEnzymeExt = ["Enzyme", "EnzymeCore"] LinearSolveFastAlmostBandedMatricesExt = ["FastAlmostBandedMatrices"] LinearSolveHYPREExt = "HYPRE" @@ -718,6 +826,7 @@ version = "2.27.0" BandedMatrices = "aae01518-5342-5314-be14-df237901396f" BlockDiagonals = "0a1fb500-61f7-11e9-3c65-f5ef3456f9f0" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" + CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" FastAlmostBandedMatrices = "9d29842c-ecb8-4973-b1e9-a27b1157504e" @@ -731,9 +840,9 @@ version = "2.27.0" [[deps.LogExpFunctions]] deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "18144f3e9cbe9b15b070288eef858f71b291ce37" +git-tree-sha1 = "a2d09619db4e765091ee5c6ffe8872849de0feea" uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "0.3.27" +version = "0.3.28" [deps.LogExpFunctions.extensions] LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" @@ -756,9 +865,9 @@ version = "1.0.3" [[deps.LoopVectorization]] deps = ["ArrayInterface", "CPUSummary", "CloseOpenIntervals", "DocStringExtensions", "HostCPUFeatures", "IfElse", "LayoutPointers", "LinearAlgebra", "OffsetArrays", "PolyesterWeave", "PrecompileTools", "SIMDTypes", "SLEEFPirates", "Static", "StaticArrayInterface", "ThreadingUtilities", "UnPack", "VectorizationBase"] -git-tree-sha1 = "0f5648fbae0d015e3abe5867bca2b362f67a5894" +git-tree-sha1 = "8084c25a250e00ae427a379a5b607e7aed96a2dd" uuid = "bdcacae8-1622-11e9-2a5c-532679323890" -version = "0.12.166" +version = "0.12.171" weakdeps = ["ChainRulesCore", "ForwardDiff", "SpecialFunctions"] [deps.LoopVectorization.extensions] @@ -766,10 +875,15 @@ weakdeps = ["ChainRulesCore", "ForwardDiff", "SpecialFunctions"] SpecialFunctionsExt = "SpecialFunctions" [[deps.MKL_jll]] -deps = ["Artifacts", "IntelOpenMP_jll", "JLLWrappers", "LazyArtifacts", "Libdl"] -git-tree-sha1 = "72dc3cf284559eb8f53aa593fe62cb33f83ed0c0" +deps = ["Artifacts", "IntelOpenMP_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "oneTBB_jll"] +git-tree-sha1 = "f046ccd0c6db2832a9f639e2c669c6fe867e5f4f" uuid = "856f044c-d86e-5d09-b602-aeab76dc8ba7" -version = "2024.0.0+0" +version = "2024.2.0+0" + +[[deps.MLStyle]] +git-tree-sha1 = "bc38dff0548128765760c79eb7388a4b37fae2c8" +uuid = "d8e11817-5142-5d16-987a-aa16d5891078" +version = "0.4.17" [[deps.MacroTools]] deps = ["Markdown", "Random"] @@ -800,15 +914,19 @@ version = "0.1.2" [[deps.MatrixFactorizations]] deps = ["ArrayLayouts", "LinearAlgebra", "Printf", "Random"] -git-tree-sha1 = "78f6e33434939b0ac9ba1df81e6d005ee85a7396" +git-tree-sha1 = "07c98fdf57c9b45b987cf250c4bdc7200fa39eb2" uuid = "a3b82374-2e81-5b9e-98ce-41277c0e4c87" -version = "2.1.0" +version = "3.0.0" +weakdeps = ["BandedMatrices"] + + [deps.MatrixFactorizations.extensions] + MatrixFactorizationsBandedMatricesExt = "BandedMatrices" [[deps.MaybeInplace]] deps = ["ArrayInterface", "LinearAlgebra", "MacroTools", "SparseArrays"] -git-tree-sha1 = "a85c6a98c9e5a2a7046bc1bb89f28a3241e1de4d" +git-tree-sha1 = "1b9e613f2ca3b6cdcbfe36381e17ca2b66d4b3a1" uuid = "bb5d69b7-63fc-4a16-80bd-7e42200c7bdb" -version = "0.1.1" +version = "0.1.3" [[deps.MbedTLS]] deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] @@ -823,9 +941,9 @@ version = "2.28.2+1" [[deps.Missings]] deps = ["DataAPI"] -git-tree-sha1 = "f66bdc5de519e8f8ae43bdc598782d35a25b1272" +git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" -version = "1.1.0" +version = "1.2.0" [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" @@ -862,10 +980,10 @@ uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" version = "1.2.0" [[deps.NonlinearSolve]] -deps = ["ADTypes", "ArrayInterface", "ConcreteStructs", "DiffEqBase", "FastBroadcast", "FastClosures", "FiniteDiff", "ForwardDiff", "LazyArrays", "LineSearches", "LinearAlgebra", "LinearSolve", "MaybeInplace", "PrecompileTools", "Preferences", "Printf", "RecursiveArrayTools", "Reexport", "SciMLBase", "SimpleNonlinearSolve", "SparseArrays", "SparseDiffTools", "StaticArraysCore", "TimerOutputs"] -git-tree-sha1 = "13232c70f50a05f98c7206190ab33dd48fa39c5b" +deps = ["ADTypes", "ArrayInterface", "ConcreteStructs", "DiffEqBase", "FastBroadcast", "FastClosures", "FiniteDiff", "ForwardDiff", "LazyArrays", "LineSearches", "LinearAlgebra", "LinearSolve", "MaybeInplace", "PrecompileTools", "Preferences", "Printf", "RecursiveArrayTools", "Reexport", "SciMLBase", "SimpleNonlinearSolve", "SparseArrays", "SparseDiffTools", "StaticArraysCore", "SymbolicIndexingInterface", "TimerOutputs"] +git-tree-sha1 = "3adb1e5945b5a6b1eaee754077f25ccc402edd7f" uuid = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" -version = "3.8.1" +version = "3.13.1" [deps.NonlinearSolve.extensions] NonlinearSolveBandedMatricesExt = "BandedMatrices" @@ -894,9 +1012,9 @@ version = "3.8.1" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [[deps.OffsetArrays]] -git-tree-sha1 = "6a731f2b5c03157418a20c12195eb4b74c8f8621" +git-tree-sha1 = "1a27764e945a152f7ca7efa04de513d473e9542e" uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -version = "1.13.0" +version = "1.14.1" weakdeps = ["Adapt"] [deps.OffsetArrays.extensions] @@ -914,15 +1032,15 @@ version = "0.8.1+2" [[deps.OpenSSL]] deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] -git-tree-sha1 = "af81a32750ebc831ee28bdaaba6e1067decef51e" +git-tree-sha1 = "38cb508d080d21dc1128f7fb04f20387ed4c0af4" uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c" -version = "1.4.2" +version = "1.4.3" [[deps.OpenSSL_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "60e3045590bd104a16fefb12836c00c0ef8c7f8c" +git-tree-sha1 = "a028ee3cb5641cccc4c24e90c36b0a4f7707bdf5" uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.0.13+0" +version = "3.0.14+0" [[deps.OpenSpecFun_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] @@ -931,10 +1049,10 @@ uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" version = "0.5.5+0" [[deps.Optim]] -deps = ["Compat", "FillArrays", "ForwardDiff", "LineSearches", "LinearAlgebra", "NLSolversBase", "NaNMath", "PackageExtensionCompat", "Parameters", "PositiveFactorizations", "Printf", "SparseArrays", "StatsBase"] -git-tree-sha1 = "d1223e69af90b6d26cea5b6f3b289b3148ba702c" +deps = ["Compat", "FillArrays", "ForwardDiff", "LineSearches", "LinearAlgebra", "NLSolversBase", "NaNMath", "Parameters", "PositiveFactorizations", "Printf", "SparseArrays", "StatsBase"] +git-tree-sha1 = "d9b79c4eed437421ac4285148fcadf42e0700e89" uuid = "429524aa-4258-5aef-a3af-852621145aeb" -version = "1.9.3" +version = "1.9.4" [deps.Optim.extensions] OptimMOIExt = "MathOptInterface" @@ -948,10 +1066,10 @@ uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" version = "1.6.3" [[deps.OrdinaryDiffEq]] -deps = ["ADTypes", "Adapt", "ArrayInterface", "DataStructures", "DiffEqBase", "DocStringExtensions", "ExponentialUtilities", "FastBroadcast", "FastClosures", "FillArrays", "FiniteDiff", "ForwardDiff", "FunctionWrappersWrappers", "IfElse", "InteractiveUtils", "LineSearches", "LinearAlgebra", "LinearSolve", "Logging", "MacroTools", "MuladdMacro", "NonlinearSolve", "Polyester", "PreallocationTools", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase", "SciMLOperators", "SimpleNonlinearSolve", "SimpleUnPack", "SparseArrays", "SparseDiffTools", "StaticArrayInterface", "StaticArrays", "TruncatedStacktraces"] -git-tree-sha1 = "91079af18db922354197eeae2a17b177079e24c1" +deps = ["ADTypes", "Adapt", "ArrayInterface", "DataStructures", "DiffEqBase", "DocStringExtensions", "EnumX", "ExponentialUtilities", "FastBroadcast", "FastClosures", "FillArrays", "FiniteDiff", "ForwardDiff", "FunctionWrappersWrappers", "IfElse", "InteractiveUtils", "LineSearches", "LinearAlgebra", "LinearSolve", "Logging", "MacroTools", "MuladdMacro", "NonlinearSolve", "Polyester", "PreallocationTools", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase", "SciMLOperators", "SciMLStructures", "SimpleNonlinearSolve", "SimpleUnPack", "SparseArrays", "SparseDiffTools", "Static", "StaticArrayInterface", "StaticArrays", "TruncatedStacktraces"] +git-tree-sha1 = "a8b2d333cd90562b58b977b4033739360b37fb1f" uuid = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" -version = "6.74.1" +version = "6.87.0" [[deps.PCRE2_jll]] deps = ["Artifacts", "Libdl"] @@ -995,15 +1113,15 @@ version = "0.4.4" [[deps.Polyester]] deps = ["ArrayInterface", "BitTwiddlingConvenienceFunctions", "CPUSummary", "IfElse", "ManualMemory", "PolyesterWeave", "Requires", "Static", "StaticArrayInterface", "StrideArraysCore", "ThreadingUtilities"] -git-tree-sha1 = "8df43bbe60029526dd628af7e9951f5af680d4d7" +git-tree-sha1 = "9ff799e8fb8ed6717710feee3be3bc20645daa97" uuid = "f517fe37-dbe3-4b94-8317-1923a5111588" -version = "0.7.10" +version = "0.7.15" [[deps.PolyesterWeave]] deps = ["BitTwiddlingConvenienceFunctions", "CPUSummary", "IfElse", "Static", "ThreadingUtilities"] -git-tree-sha1 = "240d7170f5ffdb285f9427b92333c3463bf65bf6" +git-tree-sha1 = "645bed98cd47f72f67316fd42fc47dee771aefcd" uuid = "1d0040c9-8b98-4ee7-8388-3f51789ca0ad" -version = "0.2.1" +version = "0.2.2" [[deps.PositiveFactorizations]] deps = ["LinearAlgebra"] @@ -1013,9 +1131,9 @@ version = "0.2.4" [[deps.PreallocationTools]] deps = ["Adapt", "ArrayInterface", "ForwardDiff"] -git-tree-sha1 = "b6665214f2d0739f2d09a17474dd443b9139784a" +git-tree-sha1 = "406c29a7f46706d379a3bce45671b4e3a39ddfbc" uuid = "d236fae5-4411-538c-8e31-a6e3d9e00b46" -version = "0.4.20" +version = "0.4.22" [deps.PreallocationTools.extensions] PreallocationToolsReverseDiffExt = "ReverseDiff" @@ -1039,11 +1157,16 @@ version = "1.4.3" deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +[[deps.PtrArrays]] +git-tree-sha1 = "f011fbb92c4d401059b2212c05c0601b70f8b759" +uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" +version = "1.2.0" + [[deps.QuadGK]] deps = ["DataStructures", "LinearAlgebra"] -git-tree-sha1 = "9b23c31e76e333e6fb4c1595ae6afa74966a729e" +git-tree-sha1 = "e237232771fdafbae3db5c31275303e056afaa9f" uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" -version = "2.9.4" +version = "2.10.1" [[deps.REPL]] deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] @@ -1073,9 +1196,9 @@ version = "1.3.4" [[deps.RecursiveArrayTools]] deps = ["Adapt", "ArrayInterface", "DocStringExtensions", "GPUArraysCore", "IteratorInterfaceExtensions", "LinearAlgebra", "RecipesBase", "SparseArrays", "StaticArraysCore", "Statistics", "SymbolicIndexingInterface", "Tables"] -git-tree-sha1 = "a94d22ca9ad49a7a169ecbc5419c59b9793937cc" +git-tree-sha1 = "b450d967a770fb13d0e26358f58375e20361cf9c" uuid = "731186ca-8d62-57ce-b412-fbd966d074cd" -version = "3.12.0" +version = "3.26.0" [deps.RecursiveArrayTools.extensions] RecursiveArrayToolsFastBroadcastExt = "FastBroadcast" @@ -1097,9 +1220,9 @@ version = "3.12.0" [[deps.RecursiveFactorization]] deps = ["LinearAlgebra", "LoopVectorization", "Polyester", "PrecompileTools", "StrideArraysCore", "TriangularSolve"] -git-tree-sha1 = "8bc86c78c7d8e2a5fe559e3721c0f9c9e303b2ed" +git-tree-sha1 = "6db1a75507051bc18bfa131fbc7c3f169cc4b2f6" uuid = "f2c3362d-daeb-58d1-803e-2bc74f2840b4" -version = "0.2.21" +version = "0.2.23" [[deps.Reexport]] git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" @@ -1131,16 +1254,16 @@ uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa" version = "0.7.1" [[deps.Rmath_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "6ed52fdd3382cf21947b15e8870ac0ddbff736da" +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "d483cd324ce5cf5d61b77930f0bbd6cb61927d21" uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f" -version = "0.4.0+0" +version = "0.4.2+0" [[deps.RuntimeGeneratedFunctions]] deps = ["ExprTools", "SHA", "Serialization"] -git-tree-sha1 = "6aacc5eefe8415f47b3e34214c1d79d2674a0ba2" +git-tree-sha1 = "04c968137612c4a5629fa531334bb81ad5680f00" uuid = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47" -version = "0.5.12" +version = "0.5.13" [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" @@ -1153,15 +1276,15 @@ version = "0.1.0" [[deps.SLEEFPirates]] deps = ["IfElse", "Static", "VectorizationBase"] -git-tree-sha1 = "3aac6d68c5e57449f5b9b865c9ba50ac2970c4cf" +git-tree-sha1 = "456f610ca2fbd1c14f5fcf31c6bfadc55e7d66e0" uuid = "476501e8-09a2-5ece-8869-fb82de89a1fa" -version = "0.6.42" +version = "0.6.43" [[deps.SciMLBase]] -deps = ["ADTypes", "ArrayInterface", "CommonSolve", "ConstructionBase", "Distributed", "DocStringExtensions", "EnumX", "FunctionWrappersWrappers", "IteratorInterfaceExtensions", "LinearAlgebra", "Logging", "Markdown", "PrecompileTools", "Preferences", "Printf", "RecipesBase", "RecursiveArrayTools", "Reexport", "RuntimeGeneratedFunctions", "SciMLOperators", "SciMLStructures", "StaticArraysCore", "Statistics", "SymbolicIndexingInterface", "Tables"] -git-tree-sha1 = "088123999a9a8fa7ff386a82048c6ed24b2b7d07" +deps = ["ADTypes", "Accessors", "ArrayInterface", "CommonSolve", "ConstructionBase", "Distributed", "DocStringExtensions", "EnumX", "Expronicon", "FunctionWrappersWrappers", "IteratorInterfaceExtensions", "LinearAlgebra", "Logging", "Markdown", "PrecompileTools", "Preferences", "Printf", "RecipesBase", "RecursiveArrayTools", "Reexport", "RuntimeGeneratedFunctions", "SciMLOperators", "SciMLStructures", "StaticArraysCore", "Statistics", "SymbolicIndexingInterface", "Tables"] +git-tree-sha1 = "380a059a9fd18a56d98e50ed98d40e1c1202e662" uuid = "0bca4576-84f4-4d90-8ffe-ffa030f20462" -version = "2.30.2" +version = "2.46.0" [deps.SciMLBase.extensions] SciMLBaseChainRulesCoreExt = "ChainRulesCore" @@ -1189,9 +1312,10 @@ uuid = "c0aeaf25-5076-4817-a8d5-81caf7dfa961" version = "0.3.8" [[deps.SciMLStructures]] -git-tree-sha1 = "5833c10ce83d690c124beedfe5f621b50b02ba4d" +deps = ["ArrayInterface"] +git-tree-sha1 = "cfdd1200d150df1d3c055cc72ee6850742e982d7" uuid = "53ae85a6-f571-4167-b2af-e1d143709226" -version = "1.1.0" +version = "1.4.1" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -1212,24 +1336,20 @@ uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7" version = "1.1.0" [[deps.SimpleNonlinearSolve]] -deps = ["ADTypes", "ArrayInterface", "ConcreteStructs", "DiffEqBase", "DiffResults", "FastClosures", "FiniteDiff", "ForwardDiff", "LinearAlgebra", "MaybeInplace", "PrecompileTools", "Reexport", "SciMLBase", "StaticArraysCore"] -git-tree-sha1 = "a535ae5083708f59e75d5bb3042c36d1be9bc778" +deps = ["ADTypes", "ArrayInterface", "ConcreteStructs", "DiffEqBase", "DiffResults", "DifferentiationInterface", "FastClosures", "FiniteDiff", "ForwardDiff", "LinearAlgebra", "MaybeInplace", "PrecompileTools", "Reexport", "SciMLBase", "Setfield", "StaticArraysCore"] +git-tree-sha1 = "03c21a4c373c7c3aa77611430068badaa073d740" uuid = "727e6d20-b764-4bd8-a329-72de5adea6c7" -version = "1.6.0" +version = "1.11.0" [deps.SimpleNonlinearSolve.extensions] SimpleNonlinearSolveChainRulesCoreExt = "ChainRulesCore" - SimpleNonlinearSolvePolyesterForwardDiffExt = "PolyesterForwardDiff" SimpleNonlinearSolveReverseDiffExt = "ReverseDiff" - SimpleNonlinearSolveStaticArraysExt = "StaticArrays" SimpleNonlinearSolveTrackerExt = "Tracker" SimpleNonlinearSolveZygoteExt = "Zygote" [deps.SimpleNonlinearSolve.weakdeps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - PolyesterForwardDiff = "98d1487c-24ca-40b6-b7ab-df2af84e126b" ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267" - StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Tracker = "9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" @@ -1260,9 +1380,9 @@ version = "1.10.0" [[deps.SparseDiffTools]] deps = ["ADTypes", "Adapt", "ArrayInterface", "Compat", "DataStructures", "FiniteDiff", "ForwardDiff", "Graphs", "LinearAlgebra", "PackageExtensionCompat", "Random", "Reexport", "SciMLOperators", "Setfield", "SparseArrays", "StaticArrayInterface", "StaticArrays", "Tricks", "UnPack", "VertexSafeGraphs"] -git-tree-sha1 = "a616ac46c38da60ac05cecf52064d44732edd05e" +git-tree-sha1 = "469f51f8c4741ce944be2c0b65423b518b1405b0" uuid = "47a9eef4-7e08-11e9-0b38-333d64bd3804" -version = "2.17.0" +version = "2.19.0" [deps.SparseDiffTools.extensions] SparseDiffToolsEnzymeExt = "Enzyme" @@ -1278,6 +1398,12 @@ version = "2.17.0" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" +[[deps.SparseMatrixColorings]] +deps = ["ADTypes", "Compat", "DocStringExtensions", "LinearAlgebra", "Random", "SparseArrays"] +git-tree-sha1 = "277e10c002cd780a752bded3b95a8cbc791d646b" +uuid = "0a514795-09f3-496d-8182-132a7b665d35" +version = "0.3.5" + [[deps.Sparspak]] deps = ["Libdl", "LinearAlgebra", "Logging", "OffsetArrays", "Printf", "SparseArrays", "Test"] git-tree-sha1 = "342cf4b449c299d8d1ceaf00b7a49f4fbc7940e7" @@ -1286,25 +1412,25 @@ version = "0.3.9" [[deps.SpecialFunctions]] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "e2cfc4012a19088254b3950b85c3c1d8882d864d" +git-tree-sha1 = "2f5d4697f21388cbe1ff299430dd169ef97d7e14" uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "2.3.1" +version = "2.4.0" weakdeps = ["ChainRulesCore"] [deps.SpecialFunctions.extensions] SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" [[deps.Static]] -deps = ["IfElse"] -git-tree-sha1 = "d2fdac9ff3906e27f7a618d47b676941baa6c80c" +deps = ["CommonWorldInvalidations", "IfElse", "PrecompileTools"] +git-tree-sha1 = "87d51a3ee9a4b0d2fe054bdd3fc2436258db2603" uuid = "aedffcd0-7271-4cad-89d0-dc628f76c6d3" -version = "0.8.10" +version = "1.1.1" [[deps.StaticArrayInterface]] deps = ["ArrayInterface", "Compat", "IfElse", "LinearAlgebra", "PrecompileTools", "Requires", "SparseArrays", "Static", "SuiteSparse"] -git-tree-sha1 = "5d66818a39bb04bf328e92bc933ec5b4ee88e436" +git-tree-sha1 = "8963e5a083c837531298fc41599182a759a87a6d" uuid = "0d7ed370-da01-4f52-bd93-41d350b8b718" -version = "1.5.0" +version = "1.5.1" weakdeps = ["OffsetArrays", "StaticArrays"] [deps.StaticArrayInterface.extensions] @@ -1313,9 +1439,9 @@ weakdeps = ["OffsetArrays", "StaticArrays"] [[deps.StaticArrays]] deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] -git-tree-sha1 = "bf074c045d3d5ffd956fa0a461da38a44685d6b2" +git-tree-sha1 = "eeafab08ae20c62c44c8399ccb9354a04b80db50" uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "1.9.3" +version = "1.9.7" weakdeps = ["ChainRulesCore", "Statistics"] [deps.StaticArrays.extensions] @@ -1323,9 +1449,9 @@ weakdeps = ["ChainRulesCore", "Statistics"] StaticArraysStatisticsExt = "Statistics" [[deps.StaticArraysCore]] -git-tree-sha1 = "36b3d696ce6366023a0ea192b4cd442268995a0d" +git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682" uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" -version = "1.4.2" +version = "1.4.3" [[deps.Statistics]] deps = ["LinearAlgebra", "SparseArrays"] @@ -1340,41 +1466,38 @@ version = "1.7.0" [[deps.StatsBase]] deps = ["DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] -git-tree-sha1 = "1d77abd07f617c4868c33d4f5b9e1dbb2643c9cf" +git-tree-sha1 = "5cf7606d6cef84b543b483848d4ae08ad9832b21" uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -version = "0.34.2" +version = "0.34.3" [[deps.StatsFuns]] deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] git-tree-sha1 = "cef0472124fab0695b58ca35a77c6fb942fdab8a" uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" version = "1.3.1" +weakdeps = ["ChainRulesCore", "InverseFunctions"] [deps.StatsFuns.extensions] StatsFunsChainRulesCoreExt = "ChainRulesCore" StatsFunsInverseFunctionsExt = "InverseFunctions" - [deps.StatsFuns.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" - [[deps.SteadyStateDiffEq]] deps = ["ConcreteStructs", "DiffEqBase", "DiffEqCallbacks", "LinearAlgebra", "Reexport", "SciMLBase"] -git-tree-sha1 = "3875ef009bc726f12c8af2ea9a8bb115ff545d6d" +git-tree-sha1 = "1158cfdf0da5b0eacdfcfba7c16b174a37bdf6c7" uuid = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f" -version = "2.1.0" +version = "2.2.0" [[deps.StochasticDiffEq]] deps = ["Adapt", "ArrayInterface", "DataStructures", "DiffEqBase", "DiffEqNoiseProcess", "DocStringExtensions", "FiniteDiff", "ForwardDiff", "JumpProcesses", "LevyArea", "LinearAlgebra", "Logging", "MuladdMacro", "NLsolve", "OrdinaryDiffEq", "Random", "RandomNumbers", "RecursiveArrayTools", "Reexport", "SciMLBase", "SciMLOperators", "SparseArrays", "SparseDiffTools", "StaticArrays", "UnPack"] -git-tree-sha1 = "97e5d0b7e5ec2e68eec6626af97c59e9f6b6c3d0" +git-tree-sha1 = "b47f8ccc5bd06d5f7a643bf6671365ab9d6595d9" uuid = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" -version = "6.65.1" +version = "6.67.0" [[deps.StrideArraysCore]] -deps = ["ArrayInterface", "CloseOpenIntervals", "IfElse", "LayoutPointers", "ManualMemory", "SIMDTypes", "Static", "StaticArrayInterface", "ThreadingUtilities"] -git-tree-sha1 = "d6415f66f3d89c615929af907fdc6a3e17af0d8c" +deps = ["ArrayInterface", "CloseOpenIntervals", "IfElse", "LayoutPointers", "LinearAlgebra", "ManualMemory", "SIMDTypes", "Static", "StaticArrayInterface", "ThreadingUtilities"] +git-tree-sha1 = "f35f6ab602df8413a50c4a25ca14de821e8605fb" uuid = "7792a7ef-975c-4747-a70f-980b88e8d1da" -version = "0.5.2" +version = "0.5.7" [[deps.StringCases]] git-tree-sha1 = "9d2c2ff94838df91866a16832cb0de4449abd54c" @@ -1403,10 +1526,10 @@ uuid = "fb77eaff-e24c-56d4-86b1-d163f2edb164" version = "5.2.2+0" [[deps.SymbolicIndexingInterface]] -deps = ["MacroTools", "RuntimeGeneratedFunctions"] -git-tree-sha1 = "f7b1fc9fc2bc938436b7684c243be7d317919056" +deps = ["Accessors", "ArrayInterface", "RuntimeGeneratedFunctions", "StaticArraysCore"] +git-tree-sha1 = "9c490ee01823dc443da25bf9225827e3cdd2d7e9" uuid = "2efcf032-c050-4f8e-a9bb-153293bab1f5" -version = "0.3.11" +version = "0.3.26" [[deps.TOML]] deps = ["Dates"] @@ -1420,10 +1543,10 @@ uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" version = "1.0.1" [[deps.Tables]] -deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "OrderedCollections", "TableTraits"] -git-tree-sha1 = "cb76cf677714c095e535e3501ac7954732aeea2d" +deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] +git-tree-sha1 = "598cd7c1f68d1e205689b1c2fe65a9f85846f297" uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -version = "1.11.1" +version = "1.12.0" [[deps.Tar]] deps = ["ArgTools", "SHA"] @@ -1447,14 +1570,14 @@ version = "0.5.2" [[deps.TimerOutputs]] deps = ["ExprTools", "Printf"] -git-tree-sha1 = "f548a9e9c490030e545f72074a41edfd0e5bcdd7" +git-tree-sha1 = "5a13ae8a41237cff5ecf34f73eb1b8f42fff6531" uuid = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" -version = "0.5.23" +version = "0.5.24" [[deps.TranscodingStreams]] -git-tree-sha1 = "a09c933bebed12501890d8e92946bbab6a1690f1" +git-tree-sha1 = "96612ac5365777520c3c5396314c8cf7408f436a" uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.10.5" +version = "0.11.1" weakdeps = ["Random", "Test"] [deps.TranscodingStreams.extensions] @@ -1462,14 +1585,14 @@ weakdeps = ["Random", "Test"] [[deps.TriangularSolve]] deps = ["CloseOpenIntervals", "IfElse", "LayoutPointers", "LinearAlgebra", "LoopVectorization", "Polyester", "Static", "VectorizationBase"] -git-tree-sha1 = "fadebab77bf3ae041f77346dd1c290173da5a443" +git-tree-sha1 = "be986ad9dac14888ba338c2554dcfec6939e1393" uuid = "d5829a12-d9aa-46ab-831f-fb7c9ab06edf" -version = "0.1.20" +version = "0.2.1" [[deps.Tricks]] -git-tree-sha1 = "eae1bb484cd63b36999ee58be2de6c178105112f" +git-tree-sha1 = "7822b97e99a1672bfb1b49b668a6d46d58d8cbcb" uuid = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775" -version = "0.1.8" +version = "0.1.9" [[deps.TruncatedStacktraces]] deps = ["InteractiveUtils", "MacroTools", "Preferences"] @@ -1496,9 +1619,9 @@ uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" [[deps.VectorizationBase]] deps = ["ArrayInterface", "CPUSummary", "HostCPUFeatures", "IfElse", "LayoutPointers", "Libdl", "LinearAlgebra", "SIMDTypes", "Static", "StaticArrayInterface"] -git-tree-sha1 = "7209df901e6ed7489fe9b7aa3e46fb788e15db85" +git-tree-sha1 = "e7f5b81c65eb858bed630fe006837b935518aca5" uuid = "3d5dd08c-fd9d-11e8-17fa-ed2836048c2f" -version = "0.21.65" +version = "0.21.70" [[deps.VertexSafeGraphs]] deps = ["Graphs"] @@ -1521,6 +1644,12 @@ deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" version = "1.52.0+1" +[[deps.oneTBB_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "7d0ea0f4895ef2f5cb83645fa689e52cb55cf493" +uuid = "1317d2d5-d96f-522e-a858-c73665f53c3e" +version = "2021.12.0+0" + [[deps.p7zip_jll]] deps = ["Artifacts", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" diff --git a/test/internals/model/test-zombies.jl b/test/internals/model/test-zombies.jl index 30de3d89..f07ff57c 100644 --- a/test/internals/model/test-zombies.jl +++ b/test/internals/model/test-zombies.jl @@ -104,7 +104,7 @@ end params = ModelParameters(fw; functional_response) logger = TestLogger() with_logger(logger) do - simulates(params, init; tmax = 1_000_000, verbose = true) + simulates(params, init; tmax = 1_000_000, verbose = true, compare_atol = 1e-4) end # Test that the `simulate` @info messages never contain empty vector of new extinct # species. diff --git a/test/user/04-default_model.jl b/test/user/04-default_model.jl index edb4fcd0..3b68b88f 100644 --- a/test/user/04-default_model.jl +++ b/test/user/04-default_model.jl @@ -1,3 +1,11 @@ +module TestDefaultModel + +using EcologicalNetworksDynamics +using Test + +Value = EcologicalNetworksDynamics.InnerParms # To make @sysfails work. +import ..Main: @sysfails, @argfails + @testset "Default model." begin fw = Foodweb([:a => (:b, :c), :b => (:c, :d)]) @@ -157,3 +165,5 @@ ) end + +end diff --git a/test/user/05-basic_pipelines.jl b/test/user/05-basic_pipelines.jl index feb80b84..f4f18bfe 100644 --- a/test/user/05-basic_pipelines.jl +++ b/test/user/05-basic_pipelines.jl @@ -1,6 +1,10 @@ # Check the most simple uses of the package. # Stability desired. +module TestBasicPipelines + +using EcologicalNetworksDynamics +using Test using Random Random.seed!(12) @@ -12,7 +16,7 @@ Random.seed!(12) B0 = [0.5, 0.5, 0.5] tmax = 500 sol = simulate(m, B0, tmax) - @test sol.u[end] ≈ [0.6505703879774151, 0.1889414733543331, 0.4164973283173464] + @test sol.u[end] ≈ [0.650538195504723, 0.1889822425600466, 0.41652432660982636] end @@ -33,7 +37,7 @@ end # Simulate. sol = simulate(m, 0.5, 500) # (all initial values to 0.5, simulate up to t=500) - @test sol.u[end] ≈ [0.6505703879774151, 0.1889414733543331, 0.4164973283173464] + @test sol.u[end] ≈ [0.650538195504723, 0.1889822425600466, 0.41652432660982636] end @@ -51,7 +55,7 @@ end ) sol = simulate(m, [0.5, 0.5, 0.5], 500) - @test sol.u[end] ≈ [0.6505703879774151, 0.1889414733543331, 0.4164973283173464] + @test sol.u[end] ≈ [0.650538195504723, 0.1889822425600466, 0.41652432660982636] end @@ -72,7 +76,7 @@ end # (this produces a system copy on every '+') sol = simulate(m, 0.5, 500) - @test sol.u[end] ≈ [0.6505703879774151, 0.1889414733543331, 0.4164973283173464] + @test sol.u[end] ≈ [0.650538195504723, 0.1889822425600466, 0.41652432660982636] end @@ -122,11 +126,11 @@ end sol = simulate(m, 0.5, 500) @test sol.u[end] ≈ [ - 0.6871892011471322, - 0.24497058086035212, - 0.2034744714268744, - 0.0, - 0.00012545266696651692, + 0.6871886226766561 + 0.24497075882300934 + 0.20347429368194783 + 0.0 + 0.00012602216433316475 ] end @@ -151,11 +155,11 @@ end sol = simulate(m, 0.5, 500) @test sol.u[end] ≈ [ - 0.6871892011471322, - 0.24497058086035212, - 0.2034744714268744, - 0.0, - 0.00012545266696651692, + 0.6871886226766561 + 0.24497075882300934 + 0.20347429368194783 + 0.0 + 0.00012602216433316475 ] end @@ -176,3 +180,5 @@ end ] end + +end diff --git a/test/user/runtests.jl b/test/user/runtests.jl index b48f9876..b5308220 100644 --- a/test/user/runtests.jl +++ b/test/user/runtests.jl @@ -1,8 +1,10 @@ module TestUser using EcologicalNetworksDynamics -using ..TestFailures +using Random using Test +using ..TestFailures +using ..TestTopologies Value = EcologicalNetworksDynamics.InnerParms # To make @sysfails work. import ..Main: @sysfails, @argfails From b8f22f93ada39e7ae9156ce8551e87166dec0d0b Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Thu, 23 May 2024 14:32:03 +0200 Subject: [PATCH 4/6] Explicit model argument type by @expose_data. --- src/expose_data.jl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/expose_data.jl b/src/expose_data.jl index e01052f5..f6f63b2a 100644 --- a/src/expose_data.jl +++ b/src/expose_data.jl @@ -346,12 +346,14 @@ macro expose_data( ref_prop = esc(ref_prop_name) if cached - push_res!(quote - $ref_prop(model) = get_cached(model, $spropname, $ref_fn) - end) + push_res!( + quote + $ref_prop(model::InnerParms) = get_cached(model, $spropname, $ref_fn) + end, + ) else push_res!(quote - $ref_prop(model) = $ref_fn(model) + $ref_prop(model::InnerParms) = $ref_fn(model) end) end @@ -593,11 +595,11 @@ macro expose_data( if generate_view push_res!(quote - $get_prop(model) = $View(model) + $get_prop(model::InnerParms) = $View(model) end) else push_res!(quote - $get_prop(model) = $get_fn(model) + $get_prop(model::InnerParms) = $get_fn(model) end) end push_res!(quote From 69629aa8c727380fbdb5342e727b61bae975d447 Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Mon, 29 Jul 2024 11:44:29 +0200 Subject: [PATCH 5/6] Fix test macro hygiene. --- test/test_failures.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_failures.jl b/test/test_failures.jl index 1cbbccbb..72c2c19e 100644 --- a/test/test_failures.jl +++ b/test/test_failures.jl @@ -70,19 +70,19 @@ function failswith(src, mod, xp, exception_pattern, expect_expansion_failure) Was expecting: $exception_pattern.") end # Test actual generated code. - # TODO: `e` is unhygienic in there and did clash once with 'e'fficiency: fix! + @gensym e esc(quote try $code - catch e + catch $e try # Evaluate the exception pattern # in the invocation context, at execution time. - $_check_exception($exception_pattern, e) + $_check_exception($exception_pattern, $e) $Test.@test true # Count as one for the surrounding @testset. - catch e + catch $e @error $"The tested code did not fail as expected: $loc" - rethrow(e) + rethrow($e) end else $error($"Unexpected success at $loc\nWas expecting: $exception_pattern") From ad4878dc3258965028ce064e1b7104dfe53f96af Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Fri, 19 Apr 2024 17:02:01 +0200 Subject: [PATCH 6/6] Introduce `Topology` values to address #151. These values describe the model under a topological perspective: nodes and their neibouhring relations. Nodes and edges are typed into various 'compartments'. Nodes can be "removed" from topologies while leaving tombstones to maintain indices validity. This enables various topological analyses of the model network like `disconnected_components()`, `isolated_producers()` or `starving_consumers()`. --- src/EcologicalNetworksDynamics.jl | 14 +- src/Framework/Framework.jl | 2 + src/GraphDataInputs/GraphDataInputs.jl | 1 + src/GraphDataInputs/check.jl | 2 +- src/GraphDataInputs/convert.jl | 74 +- src/GraphDataInputs/types.jl | 8 +- src/Internals/Internals.jl | 3 + src/Internals/model/model_parameters.jl | 3 + src/Topologies/Topologies.jl | 452 +++++++++++ src/Topologies/checks.jl | 100 +++ src/Topologies/display.jl | 118 +++ src/Topologies/edges_from_matrices.jl | 173 +++++ src/Topologies/queries.jl | 22 + src/Topologies/unchecked_queries.jl | 210 +++++ src/basic_topology_queries.jl | 373 +++++++++ src/components/foodweb.jl | 14 +- .../nontrophic_layers/competition.jl | 4 +- .../nontrophic_layers/facilitation.jl | 4 +- .../nontrophic_layers/interference.jl | 4 +- src/components/nontrophic_layers/main.jl | 1 + .../nontrophic_components_utils.jl | 15 +- src/components/nontrophic_layers/refuge.jl | 3 +- src/components/nutrients/main.jl | 2 + src/components/nutrients/nodes.jl | 57 +- src/components/species.jl | 25 + src/{output-analysis.jl => diversity.jl} | 0 src/graph_views.jl | 2 + src/methods/main.jl | 50 -- src/simulate.jl | 164 ++++ src/solution_queries.jl | 60 ++ src/topology.jl | 195 +++++ test/graph_data_inputs/convert.jl | 35 +- test/runtests.jl | 1 + test/topologies.jl | 732 ++++++++++++++++++ test/user/06-model_topology.jl | 225 ++++++ test/user/06-post-simulation.jl | 30 - test/user/07-post-simulation.jl | 126 +++ test/user/data_components/nutrients/nodes.jl | 10 + test/user/data_components/species.jl | 10 + 39 files changed, 3218 insertions(+), 106 deletions(-) create mode 100644 src/Topologies/Topologies.jl create mode 100644 src/Topologies/checks.jl create mode 100644 src/Topologies/display.jl create mode 100644 src/Topologies/edges_from_matrices.jl create mode 100644 src/Topologies/queries.jl create mode 100644 src/Topologies/unchecked_queries.jl create mode 100644 src/basic_topology_queries.jl rename src/{output-analysis.jl => diversity.jl} (100%) delete mode 100644 src/methods/main.jl create mode 100644 src/simulate.jl create mode 100644 src/solution_queries.jl create mode 100644 src/topology.jl create mode 100644 test/topologies.jl create mode 100644 test/user/06-model_topology.jl delete mode 100644 test/user/06-post-simulation.jl create mode 100644 test/user/07-post-simulation.jl diff --git a/src/EcologicalNetworksDynamics.jl b/src/EcologicalNetworksDynamics.jl index 53ab5273..9456421f 100644 --- a/src/EcologicalNetworksDynamics.jl +++ b/src/EcologicalNetworksDynamics.jl @@ -4,6 +4,7 @@ using Crayons using MacroTools using OrderedCollections using SparseArrays +using Graphs #------------------------------------------------------------------------------------------- # Shared API internals. @@ -39,6 +40,11 @@ using .AliasingDicts include("./multiplex_api.jl") using .MultiplexApi +# Types to represent the model under a pure topological perspective. +include("./Topologies/Topologies.jl") +using .Topologies +# (will be part of the internals after their refactoring) + #------------------------------------------------------------------------------------------- # "Inner" parts: legacy internals. @@ -88,15 +94,13 @@ include("./expose_data.jl") # The actual user-facing components of the package are defined there, # connecting them to the internals via the framework. include("./components/main.jl") -include("./methods/main.jl") # Additional exposed utils built on top of components and methods. include("./default_model.jl") include("./nontrophic_layers.jl") - -#------------------------------------------------------------------------------------------- -# Analysis tools working on the output of the simulation. -include("output-analysis.jl") +include("./simulate.jl") +include("./topology.jl") +include("./diversity.jl") # Avoid Revise interruptions when redefining methods and properties. Framework.REVISING = true diff --git a/src/Framework/Framework.jl b/src/Framework/Framework.jl index b7cb6e73..b824caa5 100644 --- a/src/Framework/Framework.jl +++ b/src/Framework/Framework.jl @@ -96,6 +96,8 @@ module Framework # - [ ] `depends(other_method_name)` to inherit all dependent components. # - [ ] Recurring pattern: various blueprints types provide 'the same component': reify. # - [ ] Namespace properties into like system.namespace.namespace.property. +# - [ ] Hooks need to trigger when special components combination become available. +# See for instance the expansion of `Nutrients.Nodes`. using Crayons using MacroTools diff --git a/src/GraphDataInputs/GraphDataInputs.jl b/src/GraphDataInputs/GraphDataInputs.jl index 010a60ed..7f04dac8 100644 --- a/src/GraphDataInputs/GraphDataInputs.jl +++ b/src/GraphDataInputs/GraphDataInputs.jl @@ -250,6 +250,7 @@ using OrderedCollections import ..SparseMatrix import ..argerr import ..checkfails +import ..join_elided # Unhygienically define `loc` variable in macros to point to invocation line. # Assumes __source__ is available. diff --git a/src/GraphDataInputs/check.jl b/src/GraphDataInputs/check.jl index d220039e..760db9a8 100644 --- a/src/GraphDataInputs/check.jl +++ b/src/GraphDataInputs/check.jl @@ -267,7 +267,7 @@ function outspace((i, j), (n, m)) end either(symbols) = length(symbols) == 1 ? "$(repr(first(symbols)))" : - "either " * join(repr.(sort(collect(symbols))), ", ", " or ") + "either " * join_elided(sort(collect(symbols)), ", ", " or "; max = 12) #------------------------------------------------------------------------------------------- # Assuming the above check passed, check references against a template. diff --git a/src/GraphDataInputs/convert.jl b/src/GraphDataInputs/convert.jl index 70894692..811ace59 100644 --- a/src/GraphDataInputs/convert.jl +++ b/src/GraphDataInputs/convert.jl @@ -125,6 +125,31 @@ function graphdataconvert(::Type{BinMap{<:Any}}, input; expected_I = nothing) res end +# The binary case *can* accept boolean masks. +function graphdataconvert( + ::Type{BinMap{<:Any}}, + input::AbstractVector{Bool}; + expected_I = Int64, +) + res = BinMap{expected_I}() + for (i, val) in enumerate(input) + val && push!(res, i) + end + res +end + +function graphdataconvert( + ::Type{BinMap{<:Any}}, + input::AbstractSparseVector{Bool,I}; + expected_I = I, +) where {I} + res = BinMap{expected_I}() + for i in findnz(input)[1] + push!(res, i) + end + res +end + #------------------------------------------------------------------------------------------- # Similar, nested logic for adjacency maps. @@ -193,6 +218,38 @@ function graphdataconvert(::Type{BinAdjacency{<:Any}}, input; expected_I = nothi res end +# The binary case *can* accept boolean matrices. +function graphdataconvert( + ::Type{BinAdjacency{<:Any}}, + input::AbstractMatrix{Bool}, + expected_I = Int64, +) + res = BinAdjacency{expected_I}() + for (i, row) in enumerate(eachrow(input)) + adj_line = BinMap(j for (j, val) in enumerate(row) if val) + isempty(adj_line) && continue + res[i] = adj_line + end + res +end + +function graphdataconvert( + ::Type{BinAdjacency{<:Any}}, + input::AbstractSparseMatrix{Bool,I}, + expected_I = I, +) where {I} + res = BinAdjacency{expected_I}() + nzi, nzj, _ = findnz(input) + for (i, j) in zip(nzi, nzj) + if haskey(res, i) + push!(res[i], j) + else + res[i] = BinMap([j]) + end + end + res +end + # Alias if types matches exactly. graphdataconvert(::Type{Map{<:Any,T}}, input::Map{Symbol,T}) where {T} = input graphdataconvert(::Type{Map{<:Any,T}}, input::Map{Int64,T}) where {T} = input @@ -273,9 +330,11 @@ end # Example usage: # @tographdata var {Sym, Scal, SpVec}{Float64} # @tographdata var YSN{Float64} -macro tographdata(var, input) +macro tographdata(var::Symbol, input) @defloc - var isa Symbol || argerr("Not a variable: $(repr(var)) at $loc.") + tographdata(loc, var, input) +end +function tographdata(loc, var, input) @capture(input, types_{Target_} | types_{}) isnothing(types) && argerr("Invalid @tographdata target types at $loc.\n\ Expected @tographdata var {aliases...}{Target}. \ @@ -317,3 +376,14 @@ function _tographdata(vsym, var, targets) The value received is $(repr(var)) ::$(typeof(var)).") end export @tographdata + +# Convenience to re-bind in local scope, avoiding the akward following pattern: +# long_var_name = @tographdata long_var_name <...> +# In favour of: +# @tographdata! long_var_name <...> +macro tographdata!(var::Symbol, input) + @defloc + evar = esc(var) + :($evar = $(tographdata(loc, var, input))) +end +export @tographdata! diff --git a/src/GraphDataInputs/types.jl b/src/GraphDataInputs/types.jl index 7b8e16a3..464935eb 100644 --- a/src/GraphDataInputs/types.jl +++ b/src/GraphDataInputs/types.jl @@ -217,7 +217,7 @@ inspace((a, b), (x, y)) = inspace(a, x) && inspace(b, y) # Pretty display for maps and adjacency lists. display_short(map::Map) = "{$(join(("$(repr(k)): $v" for (k, v) in map), ", "))}" -function display_long(map::Map, level = 0) +function display_long(map::Map; level = 0) res = "{" ind(n) = "\n" * repeat(" ", level + n) for (k, v) in map @@ -227,7 +227,7 @@ function display_long(map::Map, level = 0) end display_short(map::BinMap) = "{$(join(("$(repr(k))" for k in map), ", "))}" -function display_long(map::BinMap, level = 0) +function display_long(map::BinMap; level = 0) res = "{" ind(n) = "\n" * repeat(" ", level + n) for k in map @@ -238,11 +238,11 @@ end display_short(adj::Union{Adjacency,BinAdjacency}) = "{$(join(("$(repr(k)): $(display_short(list))" for (k, list) in adj), ", "))}" -function display_long(adj::Union{Adjacency,BinAdjacency}, level = 0) +function display_long(adj::Union{Adjacency,BinAdjacency}; level = 0) res = "{" ind(n) = "\n" * repeat(" ", level + n) for (k, list) in adj - res *= ind(1) * "$(repr(k)) => $(display_long(list, level + 1))," + res *= ind(1) * "$(repr(k)) => $(display_long(list; level = level + 1))," end res * ind(0) * "}" end diff --git a/src/Internals/Internals.jl b/src/Internals/Internals.jl index c0a9bb2c..0b611215 100644 --- a/src/Internals/Internals.jl +++ b/src/Internals/Internals.jl @@ -60,6 +60,9 @@ const Option{T} = Union{Nothing,T} using ..EcologicalNetworksDynamics const equal_fields = EcologicalNetworksDynamics.equal_fields +# Part of future refactoring here. +const Topology = EcologicalNetworksDynamics.Topologies.Topology + include("./macros.jl") include("./inputs/foodwebs.jl") include("./inputs/nontrophic_interactions.jl") diff --git a/src/Internals/model/model_parameters.jl b/src/Internals/model/model_parameters.jl index 443b42b0..11907ce3 100644 --- a/src/Internals/model/model_parameters.jl +++ b/src/Internals/model/model_parameters.jl @@ -11,6 +11,7 @@ mutable struct ModelParameters # These don't exactly have an 'empty' variant. network::Option{EcologicalNetwork} biorates::BioRates # (this one does but all values are initially 'nothing' inside) + _topology::Topology # This will actually be part of the future refactoring. functional_response::Option{FunctionalResponse} producer_growth::Option{ProducerGrowth} # Since 'foodweb' is still a mandatory input to construct interaction layers, @@ -38,6 +39,7 @@ mutable struct ModelParameters NoTemperatureResponse(), nothing, BioRates(), + Topology(), repeat([nothing], 3)..., Dict(), Dict(), @@ -181,6 +183,7 @@ function ModelParameters( temperature_response, network, biorates, + Topology(), functional_response, producer_growth, nothing, diff --git a/src/Topologies/Topologies.jl b/src/Topologies/Topologies.jl new file mode 100644 index 00000000..50033f56 --- /dev/null +++ b/src/Topologies/Topologies.jl @@ -0,0 +1,452 @@ +module Topologies + +using OrderedCollections +using Graphs +using SparseArrays + +argerr(mess) = throw(ArgumentError(mess)) + +# Mark removed nodes. +struct Tombstone end + +""" +Values of this type are constructed from a model value, +to represent its pure topology: + + - Nodes identity and types: species, nutrients, patches.. + - Edges types: trophic interaction, non-trophic interactions, migration corridors.. + +Nodes and edge information can be queried using either labels or indices. + +Values of this type are supposed not to be mutated, +as they carry faithful topological information reflecting the model +at the moment it has been extracted from it. + +However, nodes *may* be removed to represent *e.g.* species extinction, +and study the topological consequences or removing them. +The indices and labels remain stable after removal, +always consistent with their indices from the model value when extracted. +As a consequence: tombstones remain, +and every node in the topology can be queried +for having been 'removed' or not. +No tombstone remain for edges: once removed, there is no trace left of them. + +Node types and edge types constitute the various "compartments" of the topology: +equivalence classes gathering all nodes/edges with the same type. + +There are two ways of querying nodes information with indices: + + - Using *absolute* indices, uniquely identifying nodes within the whole topology. + - Using *relative* indices, uniquely identifying nodes within their *compartment*. + +Two newtypes types `Abs` and `Rel` are used in the API to protect against mixing them up. +""" +struct Topology + # List/index possible types for nodes and edges. + # Types cannot be removed. + node_types_labels::Vector{Symbol} # [type index: type label] + node_types_index::Dict{Symbol,Int} # {type label: type index} + edge_types_labels::Vector{Symbol} + edge_types_index::Dict{Symbol,Int} + + # List nodes and their associated types. + # Nodes are *sorted by type*: + # so that all nodes with the same type are stored contiguously in this array. + # Nodes can't be removed from this list, so their indices remain stable. + nodes_labels::Vector{Symbol} # [node absolute index: node label] + nodes_index::Dict{Symbol,Int} # {node label: node absolute index} + nodes_types::Vector{UnitRange{Int}} # [type index: (start, end) of nodes with this type] + + # Topological information: paired, layered adjacency lists. + # Tombstones marking nodes removal are stored here. + outgoing::Vector{Union{Tombstone,Vector{OrderedSet{Int}}}} + incoming::Vector{Union{Tombstone,Vector{OrderedSet{Int}}}} + # [node: [edgetype: {nodeid}]] + # ^--------------------------^- : Adjacency list: one entry per node 'N'. + # ^-------------------^- : One entry per edge type or a tombstone (removed node). + # ^--------^- : One entry per neighbour of 'N': its absolute index. + + # Cached redundant information + # that would otherwise be non-O(1) to calculate. + n_edges::Vector{Int} # Per edge type. + n_nodes::Vector{Int} # Per node type, not counting tombstones. + + Topology() = new([], Dict(), [], Dict(), [], Dict(), [], [], [], [], []) +end +export Topology + +# Wrap an absolute node index. +struct Abs + abs::Int # Use `.abs` to avoid mistaking with `.rel`. +end + +# Wrap a relative node index. +struct Rel + rel::Int # Use `.rel` to avoid mistaking with `.abs`. +end + +# When exposing indices +# explicit whether they mean relative or absolute. +const IRef = Union{Int,Symbol} +const RelRef = Union{Rel,Symbol} +relative(i::Int) = Rel(i) +absolute(i::Int) = Abs(i) +relative(lab::Symbol) = lab +absolute(lab::Symbol) = lab +# A combination of Relative + Node type info constitutes an absolute node ref. +const AbsRef = Union{Abs,Symbol,Tuple{RelRef,IRef}} + +# Move boilerplate interface to dedicated files. +include("unchecked_queries.jl") +const U = Unchecked # Official, stable alias to ease refs to unchecked queries. + +include("checks.jl") +include("queries.jl") +include("display.jl") + +#------------------------------------------------------------------------------------------- +# Construction primitives. + +# Only push whole slices of nodes of a new type at once. +function add_nodes!(g::Topology, labels, type::Symbol) + + # Check whole transaction before commiting. + has_node_type(g, type) && + argerr("Node type $(repr(type)) already exists in the topology.") + has_edge_type(g, type) && + argerr("Node type $(repr(type)) would be confused with edge type $(repr(type)).") + labels = check_new_nodes_labels(g, labels) + + # Add new node type. + push!(g.node_types_labels, type) + g.node_types_index[type] = length(g.node_types_labels) + + # Add new associated nodes. + nindex = g.nodes_index + nlabs = g.nodes_labels + n_before = length(nlabs) + for new_lab in labels + push!(nlabs, new_lab) + nindex[new_lab] = length(nlabs) + for adj in (g.outgoing, g.incoming) + # Need an entry for every edge type. + entry = Vector{OrderedSet{Int}}() + for _ in 1:n_edge_types(g) + push!(entry, OrderedSet()) + end + push!(adj, entry) + end + end + + # Update value. + n_after = length(nlabs) + push!(g.nodes_types, n_before+1:n_after) + push!(g.n_nodes, n_after - n_before) + + g +end +export add_nodes! + +function add_edge_type!(g::Topology, type::Symbol) + + # Check transaction. + haskey(g.edge_types_index, type) && + argerr("Edge type $(repr(type)) already exists in the topology.") + haskey(g.node_types_index, type) && + argerr("Edge type $(repr(type)) would be confused with node type $(repr(type)).") + + # Commit. + push!(g.edge_types_labels, type) + g.edge_types_index[type] = length(g.edge_types_labels) + for adj in (g.outgoing, g.incoming) + for node in adj + node isa Tombstone && continue + push!(node, OrderedSet{Int}()) + end + end + push!(g.n_edges, 0) + + g +end +export add_edge_type! + +function add_edge!(g::Topology, type::IRef, source::AbsRef, target::AbsRef) + # Check transaction. + check_edge_type(g, type) + check_node_ref(g, source) + check_node_ref(g, target) + i_type = U.edge_type_index(g, type) + i_source = U.node_abs_index(g, source) + i_target = U.node_abs_index(g, target) + check_live_node(g, i_source, source) + check_live_node(g, i_target, target) + U.has_edge(g, i_type, i_source, i_target) && + argerr("There is already an edge of type $(repr(type)) \ + between nodes $(repr(source)) and $(repr(target)).") + # Commit. + _add_edge!(g, i_type, i_source, i_target) +end +# (this "commit" part is also used when importing edges from matrices) +function _add_edge!(g::Topology, i_type::Int, i_source::Abs, i_target::Abs) + push!(g.outgoing[i_source.abs][i_type], i_target.abs) + push!(g.incoming[i_target.abs][i_type], i_source.abs) + g.n_edges[i_type] += 1 + g +end +export add_edge! + +include("./edges_from_matrices.jl") + +#------------------------------------------------------------------------------------------- +# Remove all neighbours of this node and replace it with a tombstone. + +# The exposed version is checked. +function remove_node!(g::Topology, node::RelRef, type::IRef) + # Check transaction. + check_node_type(g, type) + check_node_ref(g, node, type) + i_node = U.node_abs_index(g, (node, type)) + U.is_live(g, i_node) || alreadyerr(node) + i_type = U.node_type_index(g, type) + _remove_node!(g, i_node, i_type) +end + +# Not specifying the type requires a linear search for it. +function remove_node!(g::Topology, node::AbsRef) + # Check transaction. + check_node_ref(g, node) + i_node = U.node_abs_index(g, node) + U.is_live(g, i_node) || alreadyerr(node) + i_type = U.type_index_of_node(g, node) + _remove_node!(g, i_node, i_type) +end + +alreadyerr(node) = argerr("Node $(repr(node)) was already removed from this topology.") + +# Commit. +function _remove_node!(g::Topology, i_node::Abs, i_type::Int) + # Assumes the node is valid and live, and that the type does correspond. + g.n_edges .-= length.(g.outgoing[i_node.abs]) + g.n_edges .-= length.(g.incoming[i_node.abs]) + ts = Tombstone() + g.outgoing[i_node.abs] = ts + g.incoming[i_node.abs] = ts + for adjacency in (g.outgoing, g.incoming) + for other in adjacency + other isa Tombstone && continue + for neighbours in other + pop!(neighbours, i_node.abs, nothing) + end + end + end + g.n_nodes[i_type] -= 1 + g +end + +export remove_node! + +#------------------------------------------------------------------------------------------- +""" + adjacency_matrix( + g::Topology, + source::Symbol, + edge::Symbol, + target::Symbol, + transpose = false, + prune = false, + ) + +Construct a sparse binary matrix representing a restriction of the topology +to the given source/target nodes compartment and the given edge compartment. +The result entry `[i, j]` is true if edge i → j exist (outgoing matrix). +If `transpose` is set, the entry is true if edge `j → i` exists instead (incoming matrix). +Entries are false if either `i` or `j` has been removed from the topology. +If `prune` is set, remove line/columns corresponding removed nodes. +""" +function adjacency_matrix( + g::Topology, + source::Symbol, + edge::Symbol, + target::Symbol; + transpose = false, + prune = true, +) + check_node_type(g, source) + check_node_type(g, target) + check_edge_type(g, edge) + si = U.node_type_index(g, source) + ti = U.node_type_index(g, target) + ei = U.edge_type_index(g, edge) + if prune + pruned_adjacency_matrix(g, si, ei, ti, transpose) + else + full_adjacency_matrix(g, si, ei, ti, transpose) + end +end +export adjacency_matrix + +function full_adjacency_matrix(g::Topology, s::Int, e::Int, t::Int, transpose::Bool) + # Query result dimensions. + n_source = U.n_nodes_including_removed(g, s) + n_target = U.n_nodes_including_removed(g, t) + # Permute on transposition. + (line, col) = transpose ? (t, s) : (s, t) + n, m = transpose ? (n_target, n_source) : (n_source, n_target) + it = transpose ? U.incoming_adjacency(g, s, e, t) : U.outgoing_adjacency(g, s, e, t) + # Construct matrix. + res = spzeros(Bool, n, m) + for (iabs, neighbours) in it + i = U.node_rel_index(g, iabs, line).rel + for jabs in neighbours + j = U.node_rel_index(g, jabs, col).rel + res[i, j] = true + end + end + res +end + +function pruned_adjacency_matrix(g::Topology, s::Int, e::Int, t::Int, transpose::Bool) + # Watch the mapping from "pre-indices" (before pruning) / "post-indices" (after pruning). + pre_n_source = U.n_nodes_including_removed(g, s) + pre_n_target = U.n_nodes_including_removed(g, t) + post_n_source = U.n_nodes(g, s) # (only live nodes) + post_n_target = U.n_nodes(g, t) + + if (pre_n_source, pre_n_target) == (post_n_source, post_n_target) + return full_adjacency_matrix(g, s, e, t, transpose) # (simpler algorithm) + end + + # Permute on transposition. + (line, col) = transpose ? (t, s) : (s, t) + prn, prm = transpose ? (pre_n_target, pre_n_source) : (pre_n_source, pre_n_target) + n, m = transpose ? (post_n_target, post_n_source) : (post_n_source, post_n_target) + + # One pass nodes to prepare a pre -> index mapping. + (i_map, j_map) = map([(prn, line), (prm, col)]) do (prn, type) + map = [] + skips = 0 + for i_pre in 1:prn + abs = U.node_abs_index(g, Rel(i_pre), type) + i_post = if U.is_removed(g, abs) + skips += 1 + 0 + else + i_pre - skips + end + push!(map, i_post) + end + map + end + + # One pass over the edges to fill up the result. + res = spzeros(Bool, n, m) + it = transpose ? U.incoming_adjacency(g, s, e, t) : U.outgoing_adjacency(g, s, e, t) + for (iabs, neighbours) in it + pre_i = U.node_rel_index(g, iabs, line).rel + i = i_map[pre_i] + for jabs in neighbours + pre_j = U.node_rel_index(g, jabs, col).rel + j = j_map[pre_j] + res[i, j] = true + end + end + res +end + +#------------------------------------------------------------------------------------------- +# Iterate over disconnected components within the topology. +# Every component is yielded as a separate new topology, +# with tombstones in the right places. + +function disconnected_components(g::Topology) + # Construct a simpler graph representation + # with all nodes and edges compartments pooled together. + graph = SimpleDiGraph() + for _ in 1:length(g.nodes_labels) + add_vertex!(graph) + end + for (i_src, et) in enumerate(g.outgoing) + et isa Tombstone && continue + for targets in et, i_tgt in targets + Graphs.add_edge!(graph, i_src, i_tgt) + end + end + # Use it to run disconnection algorithm. + Iterators.map( + Iterators.filter(weakly_connected_components(graph)) do component_nodes + # Removed nodes result in degenerated singleton components. + # Dismiss them. + !(length(component_nodes) == 1 && U.is_removed(g, Abs(first(component_nodes)))) + end, + ) do component_nodes + # Construct a whole new value with only these nodes remaining. + new = Topology() + # All types are copied as-is. + append!(new.node_types_labels, g.node_types_labels) + append!(new.edge_types_labels, g.edge_types_labels) + for (k, v) in g.node_types_index + new.node_types_index[k] = v + push!(new.n_nodes, 0) + end + for (k, v) in g.edge_types_index + new.edge_types_index[k] = v + push!(new.n_edges, 0) + end + # All nodes are copied as-is. + append!(new.nodes_labels, g.nodes_labels) + append!(new.nodes_types, g.nodes_types) + for (k, v) in g.nodes_index + new.nodes_index[k] = v + end + # But only the ones in this component are reinserted with their neighbours, + # the others become tombstones. + ts = Tombstone() + component_nodes = Set(component_nodes) + i_node_type = 1 + for i_node in 1:length(new.nodes_labels) + if i_node > last(new.nodes_types[i_node_type]) + i_node_type += 1 + end + inc = g.incoming[i_node] + out = g.outgoing[i_node] + if i_node in component_nodes && !(out isa Tombstone) + new_in = Vector{OrderedSet{Int}}() + new_out = Vector{OrderedSet{Int}}() + for (i_edge_type, (in_et, out_et)) in enumerate(zip(inc, out)) + in_entry = OrderedSet() + out_entry = OrderedSet() + first = true + for (et, entry) in ((in_et, in_entry), (out_et, out_entry)) + for adj in et + adj in component_nodes || continue + push!(entry, adj) + new.n_edges[i_edge_type] += first + end + first = false + end + push!(new_in, in_entry) + push!(new_out, out_entry) + end + push!(new.incoming, new_in) + push!(new.outgoing, new_out) + new.n_nodes[i_node_type] += 1 + else + push!(new.incoming, ts) + push!(new.outgoing, ts) + end + end + new + end +end +export disconnected_components + +# Compare for equality field-by-field. +function Base.:(==)(a::Topology, b::Topology) + for fname in fieldnames(Topology) + fa, fb = getfield.((a, b), fname) + fa == fb || return false + end + true +end + +end diff --git a/src/Topologies/checks.jl b/src/Topologies/checks.jl new file mode 100644 index 00000000..c1a08083 --- /dev/null +++ b/src/Topologies/checks.jl @@ -0,0 +1,100 @@ +# Raise errors on invalid input, useful to check exposed queries. + +G = Topology +#------------------------------------------------------------------------------------------- +# Check indices validity. + +has_index(i, n) = 1 <= i <= n +err_index(i, n, what) = argerr("Invalid $what index ($i) \ + when there $(n > 1 ? "are" : "is") $n $what$(s(n)).") +function check_index(i, n, what) + has_index(i, n) || err_index(i, n, what isa String ? what : what()) + i +end + +has_node_type(g::G, i::Int) = has_index(i, length(g.node_types_labels)) +check_node_type(g::G, i::Int) = check_index(i, length(g.node_types_labels), "node type") + +has_edge_type(g::G, i::Int) = has_index(i, length(g.edge_types_labels)) +check_edge_type(g::G, i::Int) = check_index(i, length(g.edge_types_labels), "edge type") + +has_node_ref(g::G, i::Abs) = has_index(i.abs, length(g.nodes_labels)) +check_node_ref(g::G, i::Abs) = check_index(i.abs, length(g.nodes_labels), "node") + +# Check relative indices ASSUMING the node type is valid. +has_node_ref(g::G, i::Rel, type::IRef) = + has_index(i.rel, U.n_nodes_including_removed(g, type)) +check_node_ref(g::G, i::Rel, type::IRef) = check_index( + i.rel, + U.n_nodes_including_removed(g, type), + () -> "$(repr(U.node_type_label(g, type))) node", +) + +#------------------------------------------------------------------------------------------- +# Check labels validity. + +has_label(lab, set) = lab in set +function err_label(lab, set, what) + valid = if isempty(set) + "There are no labels in this topology yet." + else + "Valid labels within this topology are $(join(sort(repr.(set)), ", ", " and "))." + end + argerr("Invalid $what label: $(repr(lab)). $valid") +end +function check_label(lab, set, what) + has_label(lab, set) || err_label(lab, set, what isa String ? what : what()) + lab +end + +has_node_type(g::G, lab::Symbol) = has_label(lab, keys(g.node_types_index)) +check_node_type(g::G, lab::Symbol) = check_label(lab, keys(g.node_types_index), "node type") + +has_edge_type(g::G, lab::Symbol) = has_label(lab, keys(g.edge_types_index)) +check_edge_type(g::G, lab::Symbol) = check_label(lab, keys(g.edge_types_index), "edge type") + +has_node_ref(g::G, lab::Symbol) = has_label(lab, keys(g.nodes_index)) +check_node_ref(g::G, lab::Symbol) = check_label(lab, keys(g.nodes_index), "node") + +# Check "relative labels" ASSUMING the node type is valid. +has_node_ref(g::G, lab::Symbol, type::IRef) = has_label(lab, U._node_labels(g, type)) +check_node_ref(g::G, lab::Symbol, type::IRef) = check_label( + lab, + U._nodes_labels(g, type), + () -> "$(repr(U.node_type_label(g, type))) node", +) + +#------------------------------------------------------------------------------------------- +# Check node liveliness, assuming the reference is valid. + +function check_live_node(g::G, node::AbsRef, original_ref::AbsRef = node) + # (use the original reference to trace back to actual user input + # and improve error message) + U.is_removed(g, node) && + argerr("Node $(repr(original_ref)) has been removed from this topology.") + node +end + +#------------------------------------------------------------------------------------------- +# Check node labels availability. + +function check_new_nodes_labels(g::G, labels::Vector{Symbol}) + for new_lab in labels + if has_node_ref(g, new_lab) + argerr("Label :$new_lab was already given \ + to a node of type \ + $(repr(U.type_of_node(g, new_lab))).") + end + end + labels +end + +function check_new_nodes_labels(g::G, labels) + try + labels = Symbol[Symbol(l) for l in labels] + catch + argerr("The labels provided cannot be iterated into a collection of symbols. \ + Received: $(repr(labels)).") + end + check_new_nodes_labels(g, labels) +end diff --git a/src/Topologies/display.jl b/src/Topologies/display.jl new file mode 100644 index 00000000..e043d230 --- /dev/null +++ b/src/Topologies/display.jl @@ -0,0 +1,118 @@ +import ..Display: join_elided + +s(n) = n > 1 ? "s" : "" +are(n) = n > 1 ? "are" : "is" +function _th(n) + b, i = n ÷ 10, n % 10 + b == 1 && return "th" + i == 1 && return "st" + i == 2 && return "nd" + i == 3 && return "rd" + "th" +end +th(n) = "$n$(_th(n))" + +function Base.show(io::IO, g::Topology) + n_nt = n_node_types(g) + n_et = n_edge_types(g) + n_n = sum((U.n_nodes(g, i) for i in 1:n_nt); init = 0) + n_e = sum((U.n_edges(g, i) for i in 1:n_et); init = 0) + print( + io, + "Topology(\ + $n_nt node type$(s(n_nt)), \ + $n_et edge type$(s(n_et)), \ + $n_n node$(s(n_n)), \ + $n_e edge$(s(n_e))\ + )", + ) +end + +function Base.show(io::IO, ::MIME"text/plain", g::Topology) + elision_limit = 16 + n_nt = n_node_types(g) + n_et = n_edge_types(g) + n_n = sum((U.n_nodes(g, i) for i in 1:n_nt); init = 0) + n_e = sum((U.n_edges(g, i) for i in 1:n_et); init = 0) + print( + io, + "Topology for $n_nt node type$(s(n_nt)) \ + and $n_et edge type$(s(n_et)) \ + with $n_n node$(s(n_n)) and $n_e edge$(s(n_e))", + ) + if n_n > 0 + print(io, ":") + else + print(".") + end + println(io, "\n Nodes:") + for (i_type, type) in enumerate(_node_types(g)) + i_type > 1 && println(io) + live = Symbol[] + tomb = Symbol[] # Collect removed nodes to display at the end. + for i_node in U.nodes_abs_indices(g, i_type) + node = U.node_label(g, i_node) + if U.is_removed(g, i_node) + push!(tomb, node) + continue + end + push!(live, node) + end + print(io, " $(repr(type)) => [$(join_elided(live, ", "; max =elision_limit))]") + if !isempty(tomb) + print(io, " ") + end + end + n_e > 0 && print(io, "\n Edges:") + for (i_type, type) in enumerate(_edge_types(g)) + print(io, "\n $(repr(type))") + display_line(src, targets) = print( + io, + "\n $(repr(src)) => [$(join_elided(targets, ", "; max=elision_limit))]", + ) + last = nothing # Save last in case we use vertical elision. + i = 0 + for (i_source, _neighbours) in U._outgoing_adjacency(g, i_type) + i += 1 + isempty(_neighbours) && continue + source = U.node_label(g, Abs(i_source)) + targets = sort(collect(imap(i -> U.node_label(g, Abs(i)), _neighbours))) + if i <= elision_limit + display_line(source, targets) + end + last = (source, targets) + end + if isnothing(last) + print(io, " ") + end + if i > elision_limit + print(io, "\n ...") + end + if i >= elision_limit + (source, targets) = last + display_line(source, targets) + end + end +end + +# A debug display to just screen through the whole value. +debug(g::Topology) = + for fn in fieldnames(Topology) + val = getfield(g, fn) + if fn in (:incoming, :outgoing) + println("$fn: ") + for (i_adj, adj) in enumerate(val) + print(" $i_adj: ") + if adj isa Tombstone + println("") + continue + end + for (i_et, et) in enumerate(adj) + print("\n $i_et: $([i for i in et])") + end + println() + end + else + println("$fn: $val") + end + end diff --git a/src/Topologies/edges_from_matrices.jl b/src/Topologies/edges_from_matrices.jl new file mode 100644 index 00000000..8d9920be --- /dev/null +++ b/src/Topologies/edges_from_matrices.jl @@ -0,0 +1,173 @@ +# Add a bunch of edges from a square matrix input corresponding to one node compartment. +# The matrix size must match the total number of nodes in this compartment, +# *including* (blank) removed nodes. +function add_edges_within_node_type!( + top::Topology, + node_type::IRef, + edge_type::IRef, + e::AbstractSparseMatrix{Bool}, +) + # Check transaction. + check_node_type(top, node_type) + check_edge_type(top, edge_type) + i_edge_type = U.edge_type_index(top, edge_type) + indices = U._nodes_abs_range(top, node_type) + n = length(indices) + size(e) == (n, n) || argerr("The given edges matrix should be of size ($n, $n) \ + because there $(are(n)) $n node$(s(n)) \ + of type $(repr(U.node_type_label(top, node_type))). \ + Received instead: $(size(e)).") + # (matrix indices start from 1, not all node indices) + offset = U.node_index_offset(top, node_type) + sources, targets, _ = findnz(e) + sources .+= offset + targets .+= offset + + # Check that no edge would point to a removed node. + for (indices, dim) in ((sources, "row"), (targets, "column")) + for i_node in indices + a_n = Abs(i_node) + if U.is_removed(top, a_n) + i_matrix = i_node - offset + # Clarify error in case the offset is relevant. + par = if offset == 0 + " (index $i_node)" + else + " (index $i_node: $(th(i_matrix)) \ + within the $(repr(U.node_type_label(top, node_type))) node type)" + end + argerr("Node $(repr(U.node_label(top, a_n)))$par \ + has been removed from this topology, \ + but the given matrix has a nonzero entry in \ + $dim $(i_matrix).") + end + end + end + + # Check that no edge already exists within the topology. + for (i_src, i_tgt) in zip(sources, targets) + a_s = Abs(i_src) + a_t = Abs(i_tgt) + if U.has_edge(top, i_edge_type, a_s, a_t) + etype = U.edge_type_label(top, i_edge_type) + src = U.node_label(top, a_s) + tgt = U.node_label(top, a_t) + i_matrix = i_src - offset + j_matrix = i_tgt - offset + par = if offset == 0 + " (indices $i_src and $i_tgt)" + else + " (indices $i_src and $i_tgt: \ + resp. $(th(i_matrix)) and $(th(j_matrix)) \ + within node type $(repr(U.node_type_label(top, node_type))))" + end + argerr("There is already an edge of type $(repr(etype)) \ + between nodes $(repr(src)) and $(repr(tgt))$par, \ + but the given matrix has a nonzero entry in \ + ($i_matrix, $j_matrix).") + end + end + + # Commit. + for (i_src, i_tgt) in zip(sources, targets) + _add_edge!(top, i_edge_type, Abs(i_src), Abs(i_tgt)) + end + + top +end +add_edges_within_node_type!(top::Topology, n::IRef, e::IRef, m::Matrix{Bool}) = + add_edges_within_node_type!(top, n, e, sparse(m)) +export add_edges_within_node_type! + +# ========================================================================================== +# Same logic, but *accross* two node types. +# (mostly duplicated from above) + +function add_edges_accross_node_types!( + top::Topology, + source_node_type::IRef, + target_node_type::IRef, + edge_type::IRef, + e::AbstractSparseMatrix{Bool}, +) + # Check transaction. + check_node_type(top, source_node_type) + check_node_type(top, target_node_type) + check_edge_type(top, edge_type) + i_edge_type = U.edge_type_index(top, edge_type) + source_indices = U._nodes_abs_range(top, source_node_type) + target_indices = U._nodes_abs_range(top, target_node_type) + source_indices == target_indices && argerr("Source node types and target node types \ + are the same ($(repr(source_node_type))). \ + Use $add_edges_within_node_type! \ + method instead.") + n = length(source_indices) + m = length(target_indices) + size(e) == (n, m) || argerr("The given edges matrix should be of size ($n, $m) \ + because there $(are(n)) $n node$(s(n)) of type \ + $(repr(U.node_type_label(top, source_node_type))) \ + and $m node$(s(m)) of type \ + $(repr(U.node_type_label(top, target_node_type))). \ + Received instead: $(size(e)).") + # (matrix indices start from 1, not all node indices) + source_offset = U.node_index_offset(top, source_node_type) + target_offset = U.node_index_offset(top, target_node_type) + sources, targets, _ = findnz(e) + sources .+= source_offset + targets .+= target_offset + + # Check that no edge would point to a removed node. + for (indices, dim, offset, node_type) in ( + (sources, "row", source_offset, source_node_type), + (targets, "column", target_offset, target_node_type), + ) + for i_node in indices + a_n = Abs(i_node) + if U.is_removed(top, a_n) + i_matrix = i_node - offset + # Clarify error in case the offset is relevant. + par = if offset == 0 + "" + else + " (index $i_node: $(th(i_matrix)) \ + within the $(repr(U.node_type_label(top, node_type))) node type)" + end + argerr("Node $(repr(U.node_label(top, a_n)))$par \ + has been removed from this topology, \ + but the given matrix has a nonzero entry in \ + $dim $(i_matrix).") + end + end + end + + # Check that no edge already exists within the topology. + for (i_src, i_tgt) in zip(sources, targets) + a_s = Abs(i_src) + a_t = Abs(i_tgt) + if U.has_edge(top, i_edge_type, a_s, a_t) + etype = U.edge_type_label(top, i_edge_type) + src = U.node_label(top, a_s) + tgt = U.node_label(top, a_t) + i_matrix = i_src - source_offset + j_matrix = i_tgt - target_offset + par = " (indices $i_src and $i_tgt: \ + resp. $(th(i_matrix)) and $(th(j_matrix)) \ + within node types $(repr(U.node_type_label(top, source_node_type))) \ + and $(repr(U.node_type_label(top, target_node_type))))" + argerr("There is already an edge of type $(repr(etype)) \ + between nodes $(repr(src)) and $(repr(tgt))$par, \ + but the given matrix has a nonzero entry in \ + ($i_matrix, $j_matrix).") + end + end + + # Commit. + for (i_src, i_tgt) in zip(sources, targets) + _add_edge!(top, i_edge_type, Abs(i_src), Abs(i_tgt)) + end + + top +end +add_edges_accross_node_types!(top::Topology, n::IRef, m::IRef, t::IRef, e::Matrix{Bool}) = + add_edges_accross_node_types!(top, n, m, t, sparse(e)) +export add_edges_accross_node_types! diff --git a/src/Topologies/queries.jl b/src/Topologies/queries.jl new file mode 100644 index 00000000..d191d844 --- /dev/null +++ b/src/Topologies/queries.jl @@ -0,0 +1,22 @@ +# Build over the Unchecked module and checking functions +# to expose checked queries. + +const imap = Iterators.map +idmap(x) = imap(identity, x) # Useful to not leak refs to private collections. + +# Information about types. +n_node_types(g::Topology) = length(g.node_types_labels) +n_edge_types(g::Topology) = length(g.edge_types_labels) +export n_node_types, n_edge_types + +_node_types(g::Topology) = g.node_types_labels +_edge_types(g::Topology) = g.edge_types_labels +node_types(g::Topology) = idmap(_node_types(g)) +edge_types(g::Topology) = idmap(_edge_types(g)) +export node_types, edge_types + +is_node_type(g::Topology, i::Int) = 1 <= i <= length(g.node_types_labels) +is_edge_type(g::Topology, i::Int) = 1 <= i <= length(g.edge_types_labels) +is_node_type(g::Topology, lab::Symbol) = lab in keys(g.node_types_index) +is_edge_type(g::Topology, lab::Symbol) = lab in keys(g.edge_types_index) +export is_node_type, is_edge_type diff --git a/src/Topologies/unchecked_queries.jl b/src/Topologies/unchecked_queries.jl new file mode 100644 index 00000000..ada76125 --- /dev/null +++ b/src/Topologies/unchecked_queries.jl @@ -0,0 +1,210 @@ +# Basic queries all assume that input references are valid +# so they don't need to check input with implicit tests +# and don't bother with producing error messages. +# Namespace them all under this module as they share this property. +# Methods that would leak references are protected with a '_' prefix. +module Unchecked + +import ..Topologies: Topology as G, Tombstone, Abs, Rel, AbsRef, RelRef, IRef + +const imap = Iterators.map +const ifilter = Iterators.filter +idmap(x) = imap(identity, x) # Useful to not leak refs to private collections. + +# ========================================================================================== +# Types. + +node_type_label(g::G, i::Int) = g.node_types_labels[i] +node_type_index(g::G, lab::Symbol) = g.node_types_index[lab] +node_type_label(::G, lab::Symbol) = lab +node_type_index(::G, i::Int) = i +edge_type_label(g::G, i::Int) = g.edge_types_labels[i] +edge_type_index(g::G, lab::Symbol) = g.edge_types_index[lab] +edge_type_label(::G, lab::Symbol) = lab +edge_type_index(::G, i::Int) = i + +# ========================================================================================== +# Nodes. + +# General information. +n_nodes(g::G, type::IRef) = g.n_nodes[node_type_index(g, type)] +n_nodes_including_removed(g::G, type::IRef) = + length(g.nodes_types[node_type_index(g, type)]) +_nodes_abs_range(g::G, type::IRef) = # Okay to leak (immutable) but not abs-wrapped.. + g.nodes_types[node_type_index(g, type)] +nodes_abs_indices(g::G, type::IRef) = imap(Abs, g.nodes_types[node_type_index(g, type)]) +_nodes_labels(g::G, type::IRef) = g.nodes_labels[_nodes_abs_range(g, type)] +node_labels(g::G, type::IRef) = idmap(_nodes_labels(g, type)) + +# Particular information about nodes. +node_label(g::G, i::Abs) = g.nodes_labels[i.abs] +node_label(::G, lab::Symbol) = lab +node_label(g::G, (rel, type)::Tuple{RelRef,IRef}) = + node_label(g, node_abs_index(g, rel, type)) +node_abs_index(g::G, label::Symbol) = Abs(g.nodes_index[label]) +node_abs_index(::G, abs::Abs) = abs +# Append correct offset to convert between relative / absolute indices. +first_node_abs_index(g::G, type::IRef) = first(nodes_abs_indices(g, type)) +node_index_offset(g::G, type::IRef) = first_node_abs_index(g, type).abs - 1 +node_abs_index(g::G, relative_index::Rel, type::IRef) = + Abs(relative_index.rel + node_index_offset(g, type)) +node_rel_index(g::G, node::AbsRef, type::IRef) = + Rel(node_abs_index(g, node).abs - node_index_offset(g, type)) +node_abs_index(g::G, (rel, type)::Tuple{Rel,IRef}) = node_abs_index(g, rel, type) +# For consistency, ignore the node type if not useful, ASSUMING it has been checked. +node_abs_index(g::G, (lab, _)::Tuple{Symbol,IRef}) = node_abs_index(g, lab) + +# Querying node type requires a linear search, +# but it is generally assumed that if you know the node, then you already know its type. +type_index_of_node(g::G, node::AbsRef) = + findfirst(range -> node_abs_index(g, node).abs in range, g.nodes_types) +type_of_node(g::G, node::AbsRef) = node_type_label(g, type_index_of_node(g, node)) +# But it is O(1) to check whether a given node is of the given type. +function is_node_of_type(g::G, node::AbsRef, type::IRef) + i_type = node_type_index(g, type) + i_node = node_abs_index(g, node) + i_node.abs in g.nodes_types[i_type] +end + +is_removed(g::G, node::AbsRef) = g.outgoing[node_abs_index(g, node).abs] isa Tombstone +is_live(g::G, node::AbsRef) = !is_removed(g, node) + +# Iterate over only live nodes (absolute indices). +live_node_indices(g::G, type::IRef) = imap(Abs, ifilter(_nodes_abs_range(g, type)) do i + is_live(g, Abs(i)) +end) +live_node_labels(g::G, type::IRef) = + imap(live_node_indices(g, type)) do i + node_label(g, i) + end + +# ========================================================================================== +# Edges. + +n_edges(g::G, type) = g.n_edges[edge_type_index(g, type)] + +# Direct neighbourhood when querying particular edge type. +# (assuming focal node is not a tombstone) +function _outgoing_indices(g::G, node::AbsRef, edge_type::IRef) + i_type = edge_type_index(g, edge_type) + _outgoing_indices(g, node)[i_type] +end +function _incoming_indices(g::G, node::AbsRef, edge_type::IRef) + i_type = edge_type_index(g, edge_type) + _incoming_indices(g, node)[i_type] +end +outgoing_indices(g::G, node::AbsRef, type::IRef) = + imap(Abs, _outgoing_indices(g, node, type)) +incoming_indices(g::G, node::AbsRef, type::IRef) = + imap(Abs, _incoming_indices(g, node, type)) +outgoing_labels(g::G, node::AbsRef, type::IRef) = + imap(i -> g.nodes_labels[i], _outgoing_indices(g, node, type)) +incoming_labels(g::G, node, type::IRef) = + imap(i -> g.nodes_labels[i], _incoming_indices(g, node, type)) + +# Direct neighbourhood: return twolevel slices: +# first a slice over edge types, then nested neighbours with this edge type. +# (assuming focal node is not a tombstone) +function _outgoing_indices(g::G, node::AbsRef) + i_node = node_abs_index(g, node) + g.outgoing[i_node.abs] +end +function _incoming_indices(g::G, node::AbsRef) + i_node = node_abs_index(g, node) + g.incoming[i_node.abs] +end +outgoing_indices(g::G, node::AbsRef) = + imap(enumerate(_outgoing_indices(g, node))) do (i_edge_type, _neighbours) + (i_edge_type, imap(Abs, _neighbours)) + end +incoming_indices(g::G, node::AbsRef) = + imap(enumerate(_incoming_indices(g, node))) do (i_edge_type, _neighbours) + (i_edge_type, imap(Abs, _neighbours)) + end +outgoing_labels(g::G, node::AbsRef) = + imap(enumerate(_outgoing_indices(g, node))) do (i_edge, _neighbours) + (g.edge_types_labels[i_edge], imap(i_node -> g.nodes_labels[i_node], _neighbours)) + end +incoming_labels(g::G, node::AbsRef) = + imap(enumerate(_incoming_indices(g, node))) do (i_edge, _neighbours) + (g.edge_types_labels[i_edge], imap(i_node -> g.nodes_labels[i_node], _neighbours)) + end + + +# Filter adjacency iterators given one particular edge type. +# Also return twolevel iterators: focal node, then its neighbours. +function _outgoing_adjacency(g::G, edge_type::IRef) + i_type = edge_type_index(g, edge_type) + imap(ifilter(enumerate(g.outgoing)) do (_, node) + !(node isa Tombstone) + end) do (i, _neighbours) + (i, _neighbours[i_type]) + end +end +function _incoming_adjacency(g::G, edge_type::IRef) + i_type = edge_type_index(g, edge_type) + imap(ifilter(enumerate(g.incoming)) do (_, node) + !(node isa Tombstone) + end) do (i, _neighbours) + (i, _neighbours[i_type]) + end +end +outgoing_adjacency(g::G, edge_type::IRef) = + imap(_outgoing_adjacency(g, edge_type)) do (i_node, _neighbours) + (Abs(i_node), imap(Abs, _neighbours)) + end +incoming_adjacency(g::G, edge_type::IRef) = + imap(_incoming_adjacency(g, edge_type)) do (i_node, _neighbours) + (Abs(i_node), imap(Abs, _neighbours)) + end +outgoing_adjacency_labels(g::G, edge_type::IRef) = + imap(_outgoing_adjacency(g, edge_type)) do (i_node, _neighbours) + (node_label(g, i_node), imap(i -> node_label(g, i), _neighbours)) + end +incoming_edges_labels(g::G, edge_type::IRef) = + imap(_incoming_adjacency(g, edge_type)) do (i_node, _neighbours) + (node_label(g, i_node), imap(i -> node_label(g, i), _neighbours)) + end + +# Same, but query particular end nodes types. +function outgoing_adjacency(g::G, source_type::IRef, edge_type::IRef, target_type::IRef) + i_et = edge_type_index(g, edge_type) + src_range = _nodes_abs_range(g, source_type) + tgt_range = _nodes_abs_range(g, target_type) + imap( + ifilter(zip(src_range, g.outgoing[src_range])) do (_, node) + !(node isa Tombstone) + end, + ) do (i_node, _neighbours) + (Abs(i_node), imap(Abs, ifilter(in(tgt_range), _neighbours[i_et]))) + end +end +function incoming_adjacency(g::G, source_type::IRef, edge_type::IRef, target_type::IRef) + i_et = edge_type_index(g, edge_type) + src_range = _nodes_abs_range(g, source_type) + tgt_range = _nodes_abs_range(g, target_type) + imap( + ifilter(zip(tgt_range, g.incoming[tgt_range])) do (_, node) + !(node isa Tombstone) + end, + ) do (i_node, _neighbours) + (Abs(i_node), imap(Abs, ifilter(in(src_range), _neighbours[i_et]))) + end +end +outgoing_adjacency_labels(g::G, source_type::IRef, edge_type::IRef, target_type::IRef) = + imap(outgoing_adjacency(g, source_type, edge_type, target_type)) do (i_node, neighbours) + (node_label(g, i_node), imap(i -> node_label(g, i), neighbours)) + end +incoming_adjacency_labels(g::G, source_type::IRef, edge_type::IRef, target_type::IRef) = + imap(incoming_adjacency(g, source_type, edge_type, target_type)) do (i_node, neighbours) + (node_label(g, i_node), imap(i -> node_label(g, i), neighbours)) + end + +function has_edge(g::G, type, source::AbsRef, target::AbsRef) + type = edge_type_index(g, type) + source = node_abs_index(g, source) + target = node_abs_index(g, target) + target.abs in g.outgoing[source.abs][type] +end + +end diff --git a/src/basic_topology_queries.jl b/src/basic_topology_queries.jl new file mode 100644 index 00000000..cdb10481 --- /dev/null +++ b/src/basic_topology_queries.jl @@ -0,0 +1,373 @@ +# ========================================================================================== +# Counts. + +""" + n_live_species(m::Model; kwargs...) + n_live_species(sol::Solution; kwargs...) + n_live_species(g::Topology) + +Number of live species within the topology. +See [`topology`](@ref). +""" +function n_live_species(g::Topology) + check_species(g) + U.n_nodes(g, :species) +end +n_live_species(m::InnerParms; kwargs...) = n_live_species(get_topology(m; kwargs...)) +@method n_live_species depends(Species) +n_live_species(sol::Solution; kwargs...) = n_live_species(get_topology(sol; kwargs...)) +export n_live_species + +""" + n_live_nutrients(m::Model; kwargs...) + n_live_nutrients(sol::Model; kwargs...) + n_live_nutrients(g::Topology) + +Number of live nutrients within the topology. +See [`topology`](@ref). +""" +function n_live_nutrients(g::Topology) + check_nutrients(g) + U.n_nodes(g, :nutrients) +end +n_live_nutrients(m::InnerParms; kwargs...) = n_live_nutrients(get_topology(m; kwargs...)) +@method n_live_nutrients depends(Nutrients.Nodes) +n_live_nutrients(sol::Solution; kwargs...) = n_live_nutrients(get_topology(sol; kwargs...)) +export n_live_nutrients + +#------------------------------------------------------------------------------------------- +# Foodweb-dependent: can't work only with a `Topology` value because static species +# properties are needed, and these are only stored within the model, +# not in the topology with species removed. + +# TODO: the following code is DUPLICATED for producers/consumers/preys/tops: factorize. +""" + n_live_producers(m::Model; kwargs...) + n_live_producers(sol::Solution; kwargs...) + n_live_producers(g::Topology, producers_indices) ⚠* + +Number of live producers within the topology after simulation. +See [`topology`](@ref). +⚠*: Assumes consistent indices from the same model: will be removed in a future version. +""" +n_live_producers(m::InnerParms; kwargs...) = + n_live_producers(get_topology(m; kwargs...), m.producers_indices) +@method n_live_producers depends(Foodweb) +n_live_producers(sol::Solution; kwargs...) = + n_live_producers(get_topology(sol; kwargs...), get_model(sol).producers_indices) +function n_live_producers(g::Topology, producers_indices) + check_species(g) + sp = U.node_type_index(g, :species) + count = 0 + for i_prod in producers_indices + count += U.is_live(g, (T.Rel(i_prod), sp)) + end + count +end +export n_live_producers + +""" + n_live_consumers(m::Model; kwargs...) + n_live_consumers(sol::Solution; kwargs...) + n_live_consumers(g::Topology, consumers_indices) ⚠* + +Number of live consumers within the topology after simulation. +See [`topology`](@ref). +⚠*: Assumes consistent indices from the same model: will be removed in a future version. +""" +n_live_consumers(m::InnerParms; kwargs...) = + n_live_consumers(get_topology(m; kwargs...), m.consumers_indices) +@method n_live_consumers depends(Foodweb) +n_live_consumers(sol::Solution; kwargs...) = + n_live_consumers(get_topology(sol; kwargs...), get_model(sol).consumers_indices) +function n_live_consumers(g::Topology, consumers_indices) + check_species(g) + sp = U.node_type_index(g, :species) + count = 0 + for i_prod in consumers_indices + count += U.is_live(g, (T.Rel(i_prod), sp)) + end + count +end +export n_live_consumers + +""" + n_live_preys(m::Model; kwargs...) + n_live_preys(sol::Solution; kwargs...) + n_live_preys(g::Topology, preys_indices) ⚠* + +Number of live preys within the topology after simulation. +See [`topology`](@ref). +⚠*: Assumes consistent indices from the same model: will be removed in a future version. +""" +n_live_preys(m::InnerParms; kwargs...) = + n_live_preys(get_topology(m; kwargs...), m.preys_indices) +@method n_live_preys depends(Foodweb) +n_live_preys(sol::Solution; kwargs...) = + n_live_preys(get_topology(sol; kwargs...), get_model(sol).preys_indices) +function n_live_preys(g::Topology, preys_indices) + check_species(g) + sp = U.node_type_index(g, :species) + count = 0 + for i_prod in preys_indices + count += U.is_live(g, (T.Rel(i_prod), sp)) + end + count +end +export n_live_preys + +""" + n_live_tops(m::Model; kwargs...) + n_live_tops(sol::Solution; kwargs...) + n_live_tops(g::Topology, tops_indices) ⚠* + +Number of live tops within the topology after simulation. +See [`topology`](@ref). +⚠*: Assumes consistent indices from the same model: will be removed in a future version. +""" +n_live_tops(m::InnerParms; kwargs...) = + n_live_tops(get_topology(m; kwargs...), m.tops_indices) +@method n_live_tops depends(Foodweb) +n_live_tops(sol::Solution; kwargs...) = + n_live_tops(get_topology(sol; kwargs...), get_model(sol).tops_indices) +function n_live_tops(g::Topology, tops_indices) + check_species(g) + sp = U.node_type_index(g, :species) + count = 0 + for i_prod in tops_indices + count += U.is_live(g, (T.Rel(i_prod), sp)) + end + count +end +export n_live_tops + +# ========================================================================================== +# Iterators. + +""" + live_species(m::Model; kwargs...) + live_species(sol::Solution; kwargs...) + live_species(g::Topology) + +Iterate over relative indices of live species within the topology. +See [`topology`](@ref). +""" +function live_species(g::Topology) + check_species(g) + sp = U.node_type_index(g, :species) + imap(U.live_node_indices(g, sp)) do abs + U.node_rel_index(g, abs, sp).rel + end +end +live_species(m::InnerParms; kwargs...) = live_species(get_topology(m; kwargs...)) +@method live_species depends(Species) +live_species(sol::Solution; kwargs...) = live_species(get_topology(sol; kwargs...)) +export live_species + +""" + live_nutrients(m::Model; kwargs...) + live_nutrients(sol::Solution; kwargs...) + live_nutrients(g::Topology) + +Iterate over relative indices of live nutrients within the topology. +See [`topology`](@ref). +""" +function live_nutrients(g::Topology) + check_nutrients(g) + sp = U.node_type_index(g, :nutrients) + imap(U.live_node_indices(g, sp)) do abs + U.node_rel_index(g, abs, sp).rel + end +end +live_nutrients(m::InnerParms; kwargs...) = live_nutrients(get_topology(m; kwargs...)) +@method live_nutrients depends(Nutrients.Nodes) +live_nutrients(sol::Solution; kwargs...) = live_nutrients(get_topology(sol; kwargs...)) +export live_nutrients + +#------------------------------------------------------------------------------------------- +# Foodweb-dependent (see `Foodweb-dependent` above). + +""" + trophic_adjacency(m::Model; kwargs...) + trophic_adjacency(sol::Solution; kwargs...) + trophic_adjacency(g::Topology) + +Produce a two-level iterators yielding predators on first level +and all its preys on the second level. +This only includes :species nodes (and not *eg.* :nutrients). +See [`topology`](@ref). +""" +function trophic_adjacency(g::Topology) + check_species(g) + check_trophic(g) + U.outgoing_adjacency_labels(g, :species, :trophic, :species) +end +trophic_adjacency(m::InnerParms; kwargs...) = trophic_adjacency(get_topology(m; kwargs...)) +@method trophic_adjacency depends(Foodweb) +trophic_adjacency(sol::Solution; kwargs...) = + trophic_adjacency(get_topology(sol; kwargs...)) +export trophic_adjacency + +# TODO: the following code is DUPLICATED for producers/consumers/preys/tops: factorize. +""" + live_producers(m::Model; kwargs...) + live_producers(s::Solution; kwargs...) + live_producers(g::Topology, producers_indices) ⚠* + +Iterate over relative indices of live producer species after simulation. +See [`topology`](@ref). +⚠*: Assumes consistent indices from the same model: will be removed in a future version. +""" +live_producers(m::InnerParms; kwargs...) = + live_producers(get_topology(m; kwargs...), m.producers_indices) +@method live_producers depends(Foodweb) +live_producers(sol::Solution; kwargs...) = + live_producers(get_topology(sol; kwargs...), get_model(sol).producers_indices) +function live_producers(g::Topology, producers_indices) + check_species(g) + sp = U.node_type_index(g, :species) + abs(i_rel) = U.node_abs_index(g, T.Rel(i_rel), sp) + imap(ifilter(imap(abs, producers_indices)) do abs_prod + U.is_live(g, abs_prod) + end) do abs_prod + U.node_rel_index(g, abs_prod, sp).rel + end +end +export live_producers + +""" + live_consumers(m::Model; kwargs...) + live_consumers(s::Solution; kwargs...) + live_consumers(g::Topology, consumers_indices) ⚠* + +Iterate over relative indices of live consumer species after simulation. +See [`topology`](@ref). +⚠*: Assumes consistent indices from the same model: will be removed in a future version. +""" +live_consumers(m::InnerParms; kwargs...) = + live_consumers(get_topology(m; kwargs...), m.consumers_indices) +@method live_consumers depends(Foodweb) +live_consumers(sol::Solution; kwargs...) = + live_consumers(get_topology(sol; kwargs...), get_model(sol).consumers_indices) +function live_consumers(g::Topology, consumers_indices) + check_species(g) + sp = U.node_type_index(g, :species) + abs(i_rel) = U.node_abs_index(g, T.Rel(i_rel), sp) + imap(ifilter(imap(abs, consumers_indices)) do abs_prod + U.is_live(g, abs_prod) + end) do abs_prod + U.node_rel_index(g, abs_prod, sp).rel + end +end +export live_consumers + +""" + live_preys(m::Model; kwargs...) + live_preys(s::Solution; kwargs...) + live_preys(g::Topology, preys_indices) ⚠* + +Iterate over relative indices of live prey species after simulation. +See [`topology`](@ref). +⚠*: Assumes consistent indices from the same model: will be removed in a future version. +""" +live_preys(m::InnerParms; kwargs...) = + live_preys(get_topology(m; kwargs...), m.preys_indices) +@method live_preys depends(Foodweb) +live_preys(sol::Solution; kwargs...) = + live_preys(get_topology(sol; kwargs...), get_model(sol).preys_indices) +function live_preys(g::Topology, preys_indices) + check_species(g) + sp = U.node_type_index(g, :species) + abs(i_rel) = U.node_abs_index(g, T.Rel(i_rel), sp) + imap(ifilter(imap(abs, preys_indices)) do abs_prod + U.is_live(g, abs_prod) + end) do abs_prod + U.node_rel_index(g, abs_prod, sp).rel + end +end +export live_preys + +""" + live_tops(m::Model; kwargs...) + live_tops(s::Solution; kwargs...) + live_tops(g::Topology, tops_indices) ⚠* + +Iterate over relative indices of live top species after simulation. +See [`topology`](@ref). +⚠*: Assumes consistent indices from the same model: will be removed in a future version. +""" +live_tops(m::InnerParms; kwargs...) = live_tops(get_topology(m; kwargs...), m.tops_indices) +@method live_tops depends(Foodweb) +live_tops(sol::Solution; kwargs...) = + live_tops(get_topology(sol; kwargs...), get_model(sol).tops_indices) +function live_tops(g::Topology, tops_indices) + check_species(g) + sp = U.node_type_index(g, :species) + abs(i_rel) = U.node_abs_index(g, T.Rel(i_rel), sp) + imap(ifilter(imap(abs, tops_indices)) do abs_prod + U.is_live(g, abs_prod) + end) do abs_prod + U.node_rel_index(g, abs_prod, sp).rel + end +end +export live_tops + +# ========================================================================================== +# Adjacency matrices. + +""" + adjacency_matrix(g::Topology, source, edge, target; transpose = false; prune = true) + +Produce a boolean sparse matrix representing the connections of the given edge type, +from the given source node compartment (lines) \ +to the given target node compartment (colums). +Flip dimensions if `transpose` is set. +Lower `prune` to keep lines and columns for the nodes marked as removed. +See [`topology`](@ref). +""" +function adjacency_matrix( + g::Topology, + source::Symbol, + edge::Symbol, + target::Symbol; + transpose = false, + prune = true, +) + # Same, but with stricter input signature. + Topologies.adjacency_matrix(g, source, edge, target; transpose, prune) +end +export adjacency_matrix + +""" + species_adjacency_matrix(g::Topology, edge::Symbol; kwargs...) + +Restriction of [`adjacency_matrix`](@ref) to only `:species` compartments. +""" +function species_adjacency_matrix(g::Topology, edge::Symbol; kwargs...) + adjacency_matrix(g, :species, edge, :species; kwargs...) +end +export species_adjacency_matrix + +""" + foodweb_matrix(g::Topology; kwargs...) + +Restriction of [`species_adjacency_matrix`](@ref) +to only `:species` compartment and `:trophic` links. +""" +foodweb_matrix(g::Topology; kwargs...) = species_adjacency_matrix(g, :trophic; kwargs...) +export foodweb_matrix + + +# ========================================================================================== +# Common checks to raise useful error messages. + +check_node_compartment(g::Topology, lab::Symbol) = + is_node_type(g, lab) || + argerr("The given topology has no $(repr(lab)) node compartment.") +check_edge_compartment(g::Topology, lab::Symbol) = + is_edge_type(g, lab) || + argerr("The given topology has no $(repr(lab)) edge compartment.") + +check_species(g::Topology) = check_node_compartment(g, :species) +check_nutrients(g::Topology) = check_node_compartment(g, :nutrients) +check_trophic(g::Topology) = check_edge_compartment(g, :trophic) diff --git a/src/components/foodweb.jl b/src/components/foodweb.jl index 707017e7..acccbc25 100644 --- a/src/components/foodweb.jl +++ b/src/components/foodweb.jl @@ -120,7 +120,7 @@ A model `m` with a `Foodweb` has the following properties. * the `sparse` index yields indices valid within the whole collection of species. * the `dense` index yields indices only valid within the restricted collection of species of either kind. - - Distinguishing betwen `preys` (species with incoming trophic links) + - Distinguishing between `preys` (species with incoming trophic links) and `tops` predators (species without incoming trophic links) works the same way. - `m.producers_links`: boolean matrix highlighting potential links between producers. - `m.herbivorous_links`: highlight only consumer-to-producer trophic links. @@ -355,6 +355,16 @@ function F.expand!(m, bp::Foodweb) fw = m.network fw.A = A fw.method = "from component" # (internals legacy) + + # Add trophic edges to the topology. + top = m._topology + add_edge_type!(top, :trophic) + add_edges_within_node_type!(top, :species, :trophic, A) + + # TODO: this should happen with components-combinations-triggered-hooks + # (see Nutrient.Nodes expansion) + Topologies.has_node_type(top, :nutrients) && Nutrients.connect_producers_to_nutrients(m) + end @component Foodweb implies(Species) @@ -367,7 +377,7 @@ export TrophicLayer # ========================================================================================== # Foodweb queries. -# Topology. +# Topology as a matrix. @expose_data edges begin property(trophic_links, A) get(TrophicLinks{Bool}, sparse, "trophic link") diff --git a/src/components/nontrophic_layers/competition.jl b/src/components/nontrophic_layers/competition.jl index e20810ce..25984565 100644 --- a/src/components/nontrophic_layers/competition.jl +++ b/src/components/nontrophic_layers/competition.jl @@ -58,7 +58,7 @@ function F.expand!(model, bp::CompetitionTopologyFromRawEdges) ind = model._species_index (; A) = bp @to_sparse_matrix_if_adjacency A ind ind - model._scratch[:competition_links] = A + expand_topology!(model, :competition, A) end @component CompetitionTopologyFromRawEdges requires(Foodweb) @@ -92,7 +92,7 @@ end function F.expand!(model, bp::RandomCompetitionTopology) A = random_links(model, bp, Internals.potential_competition_links) - model._scratch[:competition_links] = A + expand_topology!(model, :competition, A) end @component RandomCompetitionTopology requires(Foodweb) diff --git a/src/components/nontrophic_layers/facilitation.jl b/src/components/nontrophic_layers/facilitation.jl index f27e187a..6e660f19 100644 --- a/src/components/nontrophic_layers/facilitation.jl +++ b/src/components/nontrophic_layers/facilitation.jl @@ -59,7 +59,7 @@ function F.expand!(model, bp::FacilitationTopologyFromRawEdges) ind = model._species_index (; A) = bp @to_sparse_matrix_if_adjacency A ind ind - model._scratch[:facilitation_links] = A + expand_topology!(model, :facilitation, A) end @component FacilitationTopologyFromRawEdges requires(Foodweb) @@ -95,7 +95,7 @@ end function F.expand!(model, bp::RandomFacilitationTopology) A = random_links(model, bp, Internals.potential_facilitation_links) - model._scratch[:facilitation_links] = A + expand_topology!(model, :facilitation, A) end @component RandomFacilitationTopology requires(Foodweb) diff --git a/src/components/nontrophic_layers/interference.jl b/src/components/nontrophic_layers/interference.jl index fd5e358c..36e22ddf 100644 --- a/src/components/nontrophic_layers/interference.jl +++ b/src/components/nontrophic_layers/interference.jl @@ -60,7 +60,7 @@ function F.expand!(model, bp::InterferenceTopologyFromRawEdges) ind = model._species_index (; A) = bp @to_sparse_matrix_if_adjacency A ind ind - model._scratch[:interference_links] = A + expand_topology!(model, :interference, A) end @component InterferenceTopologyFromRawEdges requires(Foodweb) @@ -96,7 +96,7 @@ end function F.expand!(model, bp::RandomInterferenceTopology) A = random_links(model, bp, Internals.potential_interference_links) - model._scratch[:interference_links] = A + expand_topology!(model, :interference, A) end @component RandomInterferenceTopology requires(Foodweb) diff --git a/src/components/nontrophic_layers/main.jl b/src/components/nontrophic_layers/main.jl index 3572a436..5c0f572a 100644 --- a/src/components/nontrophic_layers/main.jl +++ b/src/components/nontrophic_layers/main.jl @@ -8,6 +8,7 @@ using .EN.AliasingDicts using .EN.GraphDataInputs using .EN.KwargsHelpers using .EN.MultiplexApi +using .EN.Topologies import .EN: Option, argerr, Internals, @species_index, ModelBlueprint, fields_from_kwargs const F = Framework using SparseArrays diff --git a/src/components/nontrophic_layers/nontrophic_components_utils.jl b/src/components/nontrophic_layers/nontrophic_components_utils.jl index ac3f54f1..a97ee9a5 100644 --- a/src/components/nontrophic_layers/nontrophic_components_utils.jl +++ b/src/components/nontrophic_layers/nontrophic_components_utils.jl @@ -65,6 +65,16 @@ function fields_from_multiplex_parms(int::Symbol, d::MultiplexParametersDict) res end +# ========================================================================================== +# Expand topologies. + +function expand_topology!(model, nti, A) + model._scratch[Symbol(nti, :_links)] = A + g = model._topology + add_edge_type!(g, nti) + add_edges_within_node_type!(g, :species, nti, A) +end + # ========================================================================================== # Check/expand random topologies. @@ -139,10 +149,13 @@ end # ========================================================================================== # Check/expand full layer components. +has_nontrophic_layers(model) = model.network isa Internals.MultiplexNetwork +export has_nontrophic_layers + # The application procedure differs # whether the NTI layer is the first to be set or not. function set_layer!(model, interaction, layer) - if model.network isa Internals.FoodWeb + if !has_nontrophic_layers(model) # First NTI component to be added. # Switch from plain foodweb to a multiplex network. S = model.richness diff --git a/src/components/nontrophic_layers/refuge.jl b/src/components/nontrophic_layers/refuge.jl index f17374be..c0e97798 100644 --- a/src/components/nontrophic_layers/refuge.jl +++ b/src/components/nontrophic_layers/refuge.jl @@ -59,6 +59,7 @@ function F.expand!(model, bp::RefugeTopologyFromRawEdges) (; A) = bp @to_sparse_matrix_if_adjacency A ind ind model._scratch[:refuge_links] = A + expand_topology!(model, :refuge, A) end @component RefugeTopologyFromRawEdges requires(Foodweb) @@ -94,7 +95,7 @@ end function F.expand!(model, bp::RandomRefugeTopology) A = random_links(model, bp, Internals.potential_refuge_links) - model._scratch[:refuge_links] = A + expand_topology!(model, :refuge, A) end @component RandomRefugeTopology requires(Foodweb) diff --git a/src/components/nutrients/main.jl b/src/components/nutrients/main.jl index 7e3fc6c1..438fe0d4 100644 --- a/src/components/nutrients/main.jl +++ b/src/components/nutrients/main.jl @@ -12,6 +12,8 @@ using .EN.Framework import .EN: Framework as F, Internals, ModelBlueprint, join_elided, @component, @expose_data using OrderedCollections using SparseArrays +using .EN.Topologies +argerr = EN.argerr # The compartment defining nutrients nodes, akin to `Species`. include("./nodes.jl") diff --git a/src/components/nutrients/nodes.jl b/src/components/nutrients/nodes.jl index d8e2930f..677e2a29 100644 --- a/src/components/nutrients/nodes.jl +++ b/src/components/nutrients/nodes.jl @@ -32,11 +32,41 @@ function F.check(_, bp::RawNodes) end end -function add_nutrients!(model, names) +function add_nutrients!(m, names) # Store in the scratch, and only alias to model.producer_growth # if the corresponding component is loaded. - model._scratch[:nutrients_names] = names - model._scratch[:nutrients_index] = OrderedDict(n => i for (i, n) in enumerate(names)) + m._scratch[:nutrients_names] = names + m._scratch[:nutrients_index] = OrderedDict(n => i for (i, n) in enumerate(names)) + + # Update topology. + top = m._topology + add_nodes!(top, names, :nutrients) + + # For now, consider that the only presence of nutrients + # implies that every producer species is topologically connected to every nutrient. + # TODO: maybe this should be alleviated in case feeding coefficients are zero. + # In this situation, the edges would only appear when adding + # concentration/half-saturation coefficients. + + # TODO: This is only possible if a foodweb already exists, + # which leads us to a feature gap in the framework: + # things need to happen only when special components combinations occur, + # so as not to require that `Model() + Foodweb() + Nutrients()` + # behaves differently than `Model() + Nutrients() + Foodweb()`. + # Whatever the order here, the following should only happen on the second '+'. + # For now, work around this by having: + # - `Foodweb` expansion check for `Nutrients.Node` presence. + # - `Nutrients.Node` expansion check for `Foodweb` presence. + # But this will not scale. + Topologies.has_edge_type(top, :trophic) && connect_producers_to_nutrients(m) + # ^^^^^ + # + TODO: the above should be something like `has_component(m, Foodweb)` instead. +end + +# Either called when adding Nutrients.Nodes to a model with a Foodweb, or the opposite. +function connect_producers_to_nutrients(m) + edges = repeat(m._producers_mask, 1, m.n_nutrients) + add_edges_accross_node_types!(m._topology, :species, :nutrients, :trophic, edges) end F.expand!(model, bp::Nodes) = add_nutrients!(model, bp.names) @@ -76,6 +106,8 @@ end F.componentof(::Type{<:Nodes}) = Nodes # ========================================================================================== +# See similar methods in Species component. + @expose_data graph begin property(nutrients_richness, n_nutrients) get(m -> length(m._scratch[:nutrients_names])) @@ -96,6 +128,25 @@ end depends(Nutrients.Nodes) end +@expose_data graph begin + property(nutrient_label) + ref_cache( + m -> + (i) -> begin + names = m._nutrients_names + n = length(names) + if 1 <= i <= length(names) + names[i] + else + (are, s) = n > 1 ? ("are", "s") : ("is", "") + argerr("Invalid index ($(i)) when there $are $n nutrient$s name$s.") + end + end, + ) + get(m -> m._nutrient_label) + depends(Nutrients.Nodes) +end + # ========================================================================================== macro nutrients_index() esc(:(index(m -> m._nutrients_index))) diff --git a/src/components/species.jl b/src/components/species.jl index a5bec809..f3d5fe29 100644 --- a/src/components/species.jl +++ b/src/components/species.jl @@ -88,6 +88,7 @@ function F.expand!(model, bp::Species) # but this will be refactored. fw = Internals.FoodWeb(bp.names) model.network = fw + add_nodes!(model._topology, bp.names, :species) # Keep reference safe in case we later switch to a multiplex network, # and want to add the layers one by one. model._foodweb = fw @@ -123,6 +124,30 @@ end depends(Species) end +# Get a closure able to convert species indices into the corresponding labels +# defined within the model. +@expose_data graph begin + property(species_label) + ref_cache( + m -> + (i) -> begin + names = m._species_names + n = length(names) + if 1 <= i <= length(names) + names[i] + else + (are, s) = n > 1 ? ("are", "s") : ("is", "") + argerr("Invalid index ($(i)) when there $are $n species name$s.") + end + end, + ) + # This technically leaks a reference to the inner model as `m.species_label.m`, + # but closure captures being accessible as fields is an implementation detail + # and no one should rely on it. + get(m -> m._species_label) + depends(Species) +end + # ========================================================================================== # Numerous views into species nodes will make use of this index. macro species_index() diff --git a/src/output-analysis.jl b/src/diversity.jl similarity index 100% rename from src/output-analysis.jl rename to src/diversity.jl diff --git a/src/graph_views.jl b/src/graph_views.jl index fe1903c8..2610e06d 100644 --- a/src/graph_views.jl +++ b/src/graph_views.jl @@ -149,6 +149,8 @@ Base.setindex!(v::NodesView, rhs, i) = Base.setindex!(v::EdgesView, rhs, i, j) = throw(ViewError(typeof(v), "This view into graph edges data is read-only.")) +SparseArrays.findnz(m::AbstractEdgesView) = findnz(m._ref) + # ========================================================================================== # All possible variants of additional index checking in implementors. diff --git a/src/methods/main.jl b/src/methods/main.jl deleted file mode 100644 index 673a0598..00000000 --- a/src/methods/main.jl +++ /dev/null @@ -1,50 +0,0 @@ -# The methods defined here depends on several components, -# which is the reason they live after all components specifications. - -import SciMLBase: AbstractODESolution -const Solution = AbstractODESolution - -# Major purpose of the whole model specification: simulate dynamics. -# TODO: This actual system method is useful to check required components -# but is is *not* the function exposed -# because a reference to the original model needs to be forwarded down to the internals -# to save a copy next to the results, -# and the @method macro misses the feature of providing this reference yet. -function _simulate(model::InnerParms, u0, tmax::Integer; kwargs...) - # Depart from the legacy Internal defaults. - @kwargs_helpers kwargs - - # No default simulation time anymore. - given(:tmax) && argerr("Received two values for 'tmax': $tmax and $(take!(:tmax)).") - - # Lower threshold. - extinction_threshold = take_or!(:extinction_threshold, 1e-12, Any) - extinction_threshold = @tographdata extinction_threshold {Scalar, Vector}{Float64} - - # Shoo. - verbose = take_or!(:verbose, false) - - # No TerminateSteadyState. - extc = extinction_callback(model, extinction_threshold; verbose) - callback = take_or!(:callbacks, Internals.CallbackSet(extc)) - - Internals.simulate(model, u0; tmax, extinction_threshold, callback, verbose, kwargs...) -end -@method _simulate depends(FunctionalResponse, ProducerGrowth, Metabolism, Mortality) - -# This exposed method does forward reference down to the internals.. -simulate(model::Model, args...; kwargs...) = _simulate(model, args...; model, kwargs...) -# .. so that we *can* retrieve the original model from the simulation result. -get_model(sol::Solution) = copy(sol.prob.p.model) # (owned copy to not leak aliases) -export simulate, get_model - - -# Re-expose from internals so it works with the new API. -extinction_callback(m, thr; verbose = false) = Internals.ExtinctionCallback(thr, m, verbose) -export extinction_callback -@method extinction_callback depends( - FunctionalResponse, - ProducerGrowth, - Metabolism, - Mortality, -) diff --git a/src/simulate.jl b/src/simulate.jl new file mode 100644 index 00000000..86e7297c --- /dev/null +++ b/src/simulate.jl @@ -0,0 +1,164 @@ +# Major purpose of the whole model specification: simulate dynamics. + +import SciMLBase: AbstractODESolution +const Solution = AbstractODESolution + +# TODO: This actual system method is useful to check required components +# but is is *not* the function exposed +# because a reference to the original model needs to be forwarded down to the internals +# to save a copy next to the results, +# and the @method macro misses the feature of providing this reference yet. +function _simulate(model::InnerParms, u0, tmax::Number; kwargs...) + # Depart from the legacy Internal defaults. + @kwargs_helpers kwargs + + # No default simulation time anymore. + given(:tmax) && argerr("Received two values for 'tmax': $tmax and $(take!(:tmax)).") + + # If set, produce an @info message + # to warn user about possible degenerated network topologies. + deg_top_arg = :show_degenerated_biomass_graph_properties + deg_top = take_or!(deg_top_arg, true) + + # Lower threshold. + extinction_threshold = take_or!(:extinction_threshold, 1e-12, Any) + extinction_threshold = @tographdata extinction_threshold {Scalar, Vector}{Float64} + + # Shoo. + verbose = take_or!(:show_extinction_events, false) + + # No TerminateSteadyState. + extc = extinction_callback(model, extinction_threshold; verbose) + callback = take_or!(:callbacks, Internals.CallbackSet(extc)) + + out = Internals.simulate( + model, + u0; + tmax, + extinction_threshold, + callback, + verbose, + left()..., + ) + + deg_top && show_degenerated_biomass_graph_properties( + model, + out.u[end][get_species_indices(out)], + deg_top_arg, + ) + + out +end +@method _simulate depends(FunctionalResponse, ProducerGrowth, Metabolism, Mortality) + +""" + simulate(model::Model, u0, tmax::Number; kwargs...) + +The major feature of the ecological model: +transform the model value into a set of ODEs +and attempt to resolve them numerically +to construct simulated biomasses trajectories. + + - `u0`: Initial biomass(es). + - `tmax`: Maximum simulation time. + - `t0 = 0`: Starting simulation date. + - `extinction_threshold = 1e-5`: Biomass(es) values for which species are considered extinct. + - `show_extinction_events = false`: Raise to display events during simulation. + - `...`: additional arguments are passed to `DifferentialEquations.solve`. + +Simulation results in a `Solution` object +produced by the underlying `DifferentialEquations` package. +This object contains an inner copy of the simulated model, +which may then be retrieved with `get_model()`. +""" +simulate(model::Model, u0, tmax::Number; kwargs...) = + _simulate(model, u0, tmax; model, kwargs...) +# .. so that we *can* retrieve the original model from the simulation result. +export simulate + +include("./solution_queries.jl") + +# Re-expose from internals so it works with the new API. +extinction_callback(m, thr; verbose = false) = Internals.ExtinctionCallback(thr, m, verbose) +export extinction_callback +@method extinction_callback depends( + FunctionalResponse, + ProducerGrowth, + Metabolism, + Mortality, +) + +# Collect topology diagnostics after simulation and decide whether to display them or not. +function show_degenerated_biomass_graph_properties(model::InnerParms, biomass, arg) + g = get_topology(model; without_species = biomass .<= 0.0) + diagnostics = [] + # Consume iterator to return lengths without collecting allocated yielded values. + function count(it) + res = 0 + for _ in it + res += 1 + end + res + end + pi = model.producers_indices + ci = model.consumers_indices + for comp in disconnected_components(g) + sp = live_species(comp) + prods = live_producers(comp, pi) + cons = live_consumers(comp, ci) + ip = isolated_producers(comp, pi) + sc = starving_consumers(comp, pi, ci) + push!(diagnostics, collect.((sp, prods, cons, ip, sc))) + end + # Don't display if there is only 1 component with no degenerated nodes. + nc = length(diagnostics) + display = if nc > 1 + true + else + (_, _, _, ip, sc) = diagnostics[1] + length(sc) > 0 || length(ip) > 0 + end + if display + s(n) = n > 1 ? "s" : "" + m = "The biomass graph at the end of simulation" + if nc > 1 + m *= " contains $nc disconnected components:\n" + else + m *= " contains degenerated species nodes:\n" + end + vec(i_species) = "[$(join_elided(model.species_label.(sort(i_species)), ", "))]" + for (sp, prods, cons, ip, sc) in diagnostics + n_sp, n_prods, n_cons, n_ip, n_sc = length.((sp, prods, cons, ip, sc)) + m *= "Connected component with $n_sp species:\n" + if n_prods > 0 + m *= " - " + if n_ip == n_prods + m *= "/!\\ $n_ip isolated producer$(s(n_ip)) $(vec(ip))" + else + m *= "$n_prods producer$(s(n_prods)) $(vec(prods))" + if n_ip > 0 + m *= " /!\\ including $n_ip isolated producer$(s(n_ip)) $(vec(ip))" + end + end + m *= '\n' + end + if n_cons > 0 + m *= " - " + if n_sc == n_cons + m *= "/!\\ $n_sc starving consumer$(s(n_sc)) $(vec(sc))" + else + m *= "$n_cons consumer$(s(n_cons)) $(vec(cons))" + if n_sc > 0 + m *= " /!\\ including $n_sc starving consumer$(s(n_sc)) $(vec(sc))" + end + end + m *= '\n' + end + end + m *= "This message is meant to attract your attention \ + regarding the meaning of downstream analyses \ + depending on the simulated biomasses values.\n\ + You can silent it with `$arg=false`." + @info m + end +end diff --git a/src/solution_queries.jl b/src/solution_queries.jl new file mode 100644 index 00000000..8a6b5430 --- /dev/null +++ b/src/solution_queries.jl @@ -0,0 +1,60 @@ +# The "Solution" object carries a lot of meaning +# since it represents the state of some ecological model after some dynamics. +# TODO: should it be newtyped to feature `sol.property` like the model? + +# ========================================================================================== +# Basic info. + +""" + get_model(sol::Solution) + +Retrieve a copy of the model used for this simulation. +""" +get_model(sol::Solution) = copy(sol.prob.p.model) # (copy to not leak aliases) +export get_model + +""" + get_species_indices(sol::Solution) + +Retrieve the correct indices to extract species-related data from simulation output. +""" +function get_species_indices(sol::Solution) + m = get_model(sol) + 1:(m.n_species) +end +export get_species_indices + +""" + get_nutrients_indices(sol::Solution) + +Retrieve the correct indices to extract nutrients-related data from simulation output. +""" +function get_nutrients_indices(sol::Solution) + m = get_model(sol) + N = m.n_nutrients + S = m.n_species + (S+1):(S+N) +end +export get_nutrients_indices + +# ========================================================================================== +# Extinctions and their effects on topology. + +""" + get_extinctions(sol::Solution; date = nothing) + +Extract list of extinct species indices and their extinction dates +from the solution returned by `simulate()`. +If a simulation date is provided, +restrict to the list of species extinct in the simulation at this date. +""" +function get_extinctions(sol::Solution; date::Option{Number} = nothing) + if isnothing(date) + date = Inf + else + s, e = sol.t[1], sol.t[end] + s <= date <= e || argerr("Invalid date for a simulation in t = [$s, $e]: $date.") + end + Dict(i => d for (i, d) in Internals.get_extinct_species(sol) if d <= date) +end +export get_extinctions diff --git a/src/topology.jl b/src/topology.jl new file mode 100644 index 00000000..bb3f09e5 --- /dev/null +++ b/src/topology.jl @@ -0,0 +1,195 @@ +# Here are topology-related methods +# that are dedicated to topologies extracted from the ecological model, +# ie. with :species / :trophic compartments *etc.* + +# Convenience local aliases. +const T = Topologies +const U = Topologies.Unchecked +const imap = Iterators.map +const ifilter = Iterators.filter + +""" + get_topology(model::Model; without_species = [], without_nutrients = []) + get_topology(sol::Solution, date = nothing) + +Extract model topology to study topological consequences of extinctions. +When called on a static model, nodes can be explicitly removed during extraction. +When called on a simulation result, extinct nodes are automatically removed +with extra arguments passed to [`extinctions`](@ref). +""" +function get_topology(model::InnerParms; without_species = [], without_nutrients = []) + @tographdata! without_species K{:bin} + @tographdata! without_nutrients K{:bin} + g = deepcopy(model._topology) + removes = [] + if !isempty(without_species) + check_species(g) + spi = model.species_index + @check_refs_if_list without_species "species" spi + push!(removes, (:species, without_species, spi)) + end + if !isempty(without_nutrients) + check_nutrients(g) + nti = model.nutrients_index + @check_refs_if_list without_nutrients "nutrients" nti + push!(removes, (:nutrients, without_nutrients, nti)) + end + for (compartment, without, index) in removes + i_cp = U.node_type_index(g, compartment) + for node in without + # TODO: GraphDataInputs should permit to automatically cast to indices + # and avoid this check. + i_node = T.Rel(node isa Symbol ? index[node] : node) + T.remove_node!(g, i_node, i_cp) + end + end + g +end +@method get_topology depends() read_as(topology) + +function get_topology(sol::Solution; kwargs...) + m = get_model(sol) + g = m.topology + for i in keys(get_extinctions(sol; kwargs...)) + T.remove_node!(g, T.Rel(i), :species) + end + g +end +export get_topology + +include("./basic_topology_queries.jl") + +""" + remove_species!(g::Topology, species) + +Remove species from the given topology to study topological consequences of extinctions. +Tombstones will remain in place so that species indices remain stable, +but all incoming and outgoin edges will be forgotten. +""" +# TODO: provide a checked transactional vectored version, +# accepting Iterator or sparse/dense boolean masks. +function remove_species!(g::Topology, species::Integer) + check_species(g) + sp = U.node_type_index(g, :species) + T.check_node_ref(g, species, sp) + T.remove_node!(g, T.Rel(species), sp) +end +function remove_species!(g::Topology, species::Union{Symbol,AbstractString,Char}) + check_species(g) + sp = U.node_type_index(g, :species) + species = Symbol(species) + T.check_node_ref(g, species, sp) + rel = U.node_rel_index(g, species, sp) + T.remove_node!(g, rel, sp) +end +export remove_species! + +""" + isolated_producers(m::Model; kwargs...) + isolated_producers(sol::Solution; kwargs...) + isolated_producers(g::Topology, producers_indices) ⚠* + +Iterate over isolated producers nodes, +*i.e.* producers without incoming or outgoing edges, +either in the static model topology or during/after simulation. +See [`topology`](@ref). + + - ⚠ : Assumes consistent indices from the same model: will be removed in a future version. +""" +isolated_producers(m::InnerParms; kwargs...) = + isolated_producers(get_topology(m; kwargs...), m.producers_indices) +@method isolated_producers depends(Foodweb) + +isolated_producers(sol::Solution; kwargs...) = + isolated_producers(get_topology(sol; kwargs...), get_model(sol).producers_indices) +export isolated_producers + +# Unexposed underlying primitive: assumes that indices are consistent within the topology. +function isolated_producers(g::Topology, producers_indices) + sp = U.node_type_index(g, :species) + abs(i_rel) = U.node_abs_index(g, T.Rel(i_rel), sp) + unwrap(i) = i.abs + imap(unwrap, ifilter(imap(abs, producers_indices)) do i_prod + inc = g.incoming[i_prod.abs] + inc isa T.Tombstone && return false + any(!isempty, inc) && return false + out = g.outgoing[i_prod.abs] + any(!isempty, out) && return false + true + end) +end + +""" + starving_consumers(m::Model; kwargs...) + starving_consumers(sol::Solution; kwargs...) + starving_consumers(g::Topology, producers_indices, consumers_indices) ⚠* + +Iterate over starving consumers nodes, +*i.e.* consumers with no directed trophic path to a producer, +either in the static model topology +or after simulation. +See [`topology`](@ref). + + - ⚠ : Assumes consistent indices from the same model: will be removed in a future version. +""" +starving_consumers(m::InnerParms; kwargs...) = + starving_consumers(get_topology(m; kwargs...), m.producers_indices, m.consumers_indices) +@method starving_consumers depends(Foodweb) + +function starving_consumers(sol::Solution; kwargs...) + (; producers_indices, consumers_indices) = get_model(sol) + starving_consumers(get_topology(sol; kwargs...), producers_indices, consumers_indices) +end +export starving_consumers + +# Unexposed underlying primitive: assumes that indices are consistent within the topology. +function starving_consumers(g::Topology, producers_indices, consumers_indices) + consumers_indices = Set(consumers_indices) + sp = U.node_type_index(g, :species) + tr = U.edge_type_index(g, :trophic) + abs(i_rel) = U.node_abs_index(g, T.Rel(i_rel), sp) + rel(i_abs) = U.node_rel_index(g, i_abs, sp).rel + live(i_abs) = U.is_live(g, i_abs) + unwrap(i) = i.abs + + # Collect all current (live) producers and consumers. + producers = Set(ifilter(live, imap(abs, producers_indices))) + consumers = Set(ifilter(live, imap(abs, consumers_indices))) + + # Visit the graph from producers up to consumers, + # and remove all consumers founds. + to_visit = producers + found = Set{T.Abs}() + while !isempty(to_visit) + i = pop!(to_visit) + if rel(i) in consumers_indices + pop!(consumers, i) + end + push!(found, i) + for up in U.incoming_indices(g, i, tr) + up in found && continue + push!(to_visit, up) + end + end + + # The remaining consumers are starving. + imap(unwrap, consumers) +end + +""" + disconnected_components(m::Model; kwargs...) + disconnected_components(sol::Model; kwargs...) + disconnected_components(g::Topology) + +Iterate over the disconnected component within the topology. +This create a collection of topologies +with all the same compartments and nodes indices, +but with different nodes marked as removed to constitute the various components. +See [`topology`](@ref). +""" +T.disconnected_components(m::Model; kwargs...) = + disconnected_components(get_topology(m; kwargs...)) +T.disconnected_components(sol::Solution; kwargs...) = + disconnected_components(get_topology(sol; kwargs...)) +# Direct re-export from Topologies. +export disconnected_components diff --git a/test/graph_data_inputs/convert.jl b/test/graph_data_inputs/convert.jl index ae922ec8..0bb8023c 100644 --- a/test/graph_data_inputs/convert.jl +++ b/test/graph_data_inputs/convert.jl @@ -155,6 +155,15 @@ res = @tographdata input K{:bin} @test aliased(input, res) + # Accept boolean masks. + input = Bool[1, 0, 1, 1, 0] + res = @tographdata input K{:bin} + @test same_type_value(res, OrderedSet([1, 3, 4])) + + input = sparse(Bool[1, 0, 1, 1, 0]) + res = @tographdata input K{:bin} + @test same_type_value(res, OrderedSet([1, 3, 4])) + # Still, use Bool as expected for ternary true/false/miss logic. input = [1 => true, 3 => false] res = @tographdata input K{Bool} @@ -221,6 +230,23 @@ res = @tographdata input A{:bin} @test aliased(input, res) + # Accept boolean matrices. + input = Bool[ + 0 1 0 + 0 0 0 + 1 0 1 + ] + res = @tographdata input A{:bin} + @test same_type_value(res, OrderedDict(1 => OrderedSet([2]), 3 => OrderedSet([1, 3]))) + + input = sparse(Bool[ + 0 1 0 + 0 0 0 + 1 0 1 + ]) + res = @tographdata input A{:bin} + @test same_type_value(res, OrderedDict(1 => OrderedSet([2]), 3 => OrderedSet([1, 3]))) + # Ternary logic. input = [1 => [5 => true, 7 => false], (2, ([7, false], 9 => true))] res = @tographdata input A{Bool} @@ -232,6 +258,13 @@ ), ) + #--------------------------------------------------------------------------------------- + # Convenience variable replacing. + + var = 'a' + @tographdata! var YSV{Float64} + @test same_type_value(var, :a) + # ====================================================================================== # Exposed conversion failures. @@ -361,7 +394,7 @@ # ====================================================================================== # Invalid uses. - @xargfails((@tographdata 4 + 5 YSV{Bool}), ["Not a variable: :(4 + 5) at"]) + @failswith((@tographdata 4 + 5 YSV{Bool}), MethodError, expansion) @failswith((@tographdata nope YSV{Bool}), UndefVarError(:nope)) @xargfails( (@tographdata input NOPE), diff --git a/test/runtests.jl b/test/runtests.jl index a10f4e89..c70a4fa7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -17,6 +17,7 @@ sep("Test System/Blueprints/Components framework.") include("./framework/runtests.jl") sep("Test API utils.") +include("./topologies.jl") include("./aliasing_dicts.jl") include("./multiplex_api.jl") include("./graph_data_inputs/runtests.jl") diff --git a/test/topologies.jl b/test/topologies.jl new file mode 100644 index 00000000..689d74ae --- /dev/null +++ b/test/topologies.jl @@ -0,0 +1,732 @@ +module TestTopologies + +using EcologicalNetworksDynamics.Topologies +using Test +import ..Main: @argfails + +# Having correct 'show'/display implies that numerous internals are working correctly. +function check_display(top, short, long) + @test "$top" == short + io = IOBuffer() + show(IOContext(io, :limit => true, :displaysize => (20, 40)), "text/plain", top) + @test String(take!(io)) == long +end + +@testset "Topology primitives" begin + + top = Topology() + add_nodes!(top, Symbol.(collect("abcd")), :species) + add_nodes!(top, Symbol.(collect("uv")), :nutrients) + add_edge_type!(top, :trophic) + add_edge_type!(top, :mutualism) + add_edge_type!(top, :interference) + add_edge!(top, :trophic, :a, :b) + add_edge!(top, :trophic, :a, :c) + add_edge!(top, :trophic, :c, :b) + add_edge!(top, :trophic, :b, :d) + add_edge!(top, :trophic, :d, :u) + add_edge!(top, :trophic, :b, :v) + add_edge!(top, :mutualism, :a, :d) + add_edge!(top, :interference, :a, :c) + + #! format: off + check_display(top, + "Topology(2 node types, 3 edge types, 6 nodes, 8 edges)", + raw"Topology for 2 node types and 3 edge types with 6 nodes and 8 edges: + Nodes: + :species => [:a, :b, :c, :d] + :nutrients => [:u, :v] + Edges: + :trophic + :a => [:b, :c] + :b => [:d, :v] + :c => [:b] + :d => [:u] + :mutualism + :a => [:d] + :interference + :a => [:c]", + ) + #! format: on + + # Extract binary matrices: + @test adjacency_matrix(top, :species, :trophic, :species) == Bool[ + 0 1 1 0 + 0 0 0 1 + 0 1 0 0 + 0 0 0 0 + ] + @test adjacency_matrix(top, :species, :mutualism, :species) == Bool[ + 0 0 0 1 + 0 0 0 0 + 0 0 0 0 + 0 0 0 0 + ] + @test adjacency_matrix(top, :species, :trophic, :nutrients) == Bool[ + 0 0 + 0 1 + 0 0 + 1 0 + ] + @test adjacency_matrix(top, :nutrients, :trophic, :species) == Bool[ + 0 0 0 0 + 0 0 0 0 + ] + + # Transposed version. + transpose = true + @test adjacency_matrix(top, :species, :trophic, :species; transpose) == Bool[ + 0 0 0 0 + 1 0 1 0 + 1 0 0 0 + 0 1 0 0 + ] + @test adjacency_matrix(top, :species, :mutualism, :species; transpose) == Bool[ + 0 0 0 0 + 0 0 0 0 + 0 0 0 0 + 1 0 0 0 + ] + @test adjacency_matrix(top, :species, :trophic, :nutrients; transpose) == Bool[ + 0 0 0 1 + 0 1 0 0 + ] + @test adjacency_matrix(top, :nutrients, :trophic, :species; transpose) == Bool[ + 0 0 + 0 0 + 0 0 + 0 0 + ] + + g = deepcopy(top) + remove_node!(g, :b) + + #! format: off + check_display(g, + "Topology(2 node types, 3 edge types, 5 nodes, 4 edges)", + raw"Topology for 2 node types and 3 edge types with 5 nodes and 4 edges: + Nodes: + :species => [:a, :c, :d] + :nutrients => [:u, :v] + Edges: + :trophic + :a => [:c] + :d => [:u] + :mutualism + :a => [:d] + :interference + :a => [:c]", + ) + #! format: on + + # Pruned adjacency matrices. + @test adjacency_matrix(g, :species, :trophic, :species) == Bool[ + 0 1 0 + 0 0 0 + 0 0 0 + ] + @test adjacency_matrix(g, :species, :mutualism, :species) == Bool[ + 0 0 1 + 0 0 0 + 0 0 0 + ] + @test adjacency_matrix(g, :species, :trophic, :nutrients) == Bool[ + 0 0 + 0 0 + 1 0 + ] + @test adjacency_matrix(g, :nutrients, :trophic, :species) == Bool[ + 0 0 0 + 0 0 0 + ] + + # Transposed + pruned. + transpose = true + @test adjacency_matrix(g, :species, :trophic, :species; transpose) == Bool[ + 0 0 0 + 1 0 0 + 0 0 0 + ] + @test adjacency_matrix(g, :species, :mutualism, :species; transpose) == Bool[ + 0 0 0 + 0 0 0 + 1 0 0 + ] + @test adjacency_matrix(g, :species, :trophic, :nutrients; transpose) == Bool[ + 0 0 1 + 0 0 0 + ] + @test adjacency_matrix(g, :nutrients, :trophic, :species; transpose) == Bool[ + 0 0 + 0 0 + 0 0 + ] + + # Request full matrix anyway. + @test adjacency_matrix(g, :species, :trophic, :species; prune = false) == Bool[ + 0 0 1 0 + 0 0 0 0 + 0 0 0 0 + 0 0 0 0 + ] + @test adjacency_matrix(g, :species, :mutualism, :species; prune = false) == Bool[ + 0 0 0 1 + 0 0 0 0 + 0 0 0 0 + 0 0 0 0 + ] + @test adjacency_matrix(g, :species, :trophic, :nutrients; prune = false) == Bool[ + 0 0 + 0 0 + 0 0 + 1 0 + ] + @test adjacency_matrix(g, :nutrients, :trophic, :species; prune = false) == Bool[ + 0 0 0 0 + 0 0 0 0 + ] + + # Transposed version. + transpose = true + @test adjacency_matrix(g, :species, :trophic, :species; prune = false, transpose) == + Bool[ + 0 0 0 0 + 0 0 0 0 + 1 0 0 0 + 0 0 0 0 + ] + @test adjacency_matrix(g, :species, :mutualism, :species; prune = false, transpose) == + Bool[ + 0 0 0 0 + 0 0 0 0 + 0 0 0 0 + 1 0 0 0 + ] + @test adjacency_matrix(g, :species, :trophic, :nutrients; prune = false, transpose) == + Bool[ + 0 0 0 1 + 0 0 0 0 + ] + @test adjacency_matrix(g, :nutrients, :trophic, :species; prune = false, transpose) == + Bool[ + 0 0 + 0 0 + 0 0 + 0 0 + ] + + + # Optionally provide node type so it's not searched. + h = deepcopy(top) + remove_node!(h, :b, :species) + @test g == h + + # Input guards. + @argfails( + add_nodes!(top, :a, :newtype), + "The labels provided cannot be iterated into a collection of symbols. Received: :a." + ) + @argfails( + add_nodes!(top, [:a], :newtype), + "Label :a was already given to a node of type :species." + ) + @argfails( + add_nodes!(top, [:x], :species), + "Node type :species already exists in the topology." + ) + @argfails( + add_nodes!(top, [:x], :mutualism), + "Node type :mutualism would be confused with edge type :mutualism." + ) + @argfails( + add_edge_type!(top, :mutualism), + "Edge type :mutualism already exists in the topology." + ) + @argfails( + add_edge_type!(top, :species), + "Edge type :species would be confused with node type :species." + ) + @argfails( + add_edge!(top, :x, :a, :b), + "Invalid edge type label: :x. \ + Valid labels within this topology \ + are :interference, :mutualism and :trophic." + ) + @argfails( + add_edge!(top, :trophic, :x, :b), + "Invalid node label: :x. \ + Valid labels within this topology \ + are :a, :b, :c, :d, :u and :v." + ) + @argfails( + add_edge!(g, :trophic, :a, :b), + "Node :b has been removed \ + from this topology." + ) + @argfails( + add_edge!(top, :trophic, :a, :b), + "There is already an edge of type :trophic between nodes :a and :b." + ) + @argfails( + remove_node!(g, :x), + "Invalid node label: :x. \ + Valid labels within this topology \ + are :a, :b, :c, :d, :u and :v.", + ) + @argfails( + remove_node!(g, :a, :x), + "Invalid node type label: :x. \ + Valid labels within this topology \ + are :nutrients and :species.", + ) + @argfails(remove_node!(g, :b), "Node :b was already removed from this topology.") + @argfails( + remove_node!(g, :b, :species), + "Node :b was already removed \ + from this topology." + ) + @argfails( + remove_node!(top, :b, :nutrients), + "Invalid :nutrients node label: :b. \ + Valid labels within this topology \ + are :u and :v." + ) + + # ====================================================================================== + # Add a whole bunch of edges at once. + + #--------------------------------------------------------------------------------------- + # Within a node compartment. + + f = add_edges_within_node_type!( + deepcopy(g), + :species, + :trophic, + Bool[ + 0 0 0 1 + 0 0 0 0 + 1 0 0 0 + 0 0 1 0 + ], + ) + #! format: off + check_display(f, + "Topology(2 node types, 3 edge types, 5 nodes, 7 edges)", + raw"Topology for 2 node types and 3 edge types with 5 nodes and 7 edges: + Nodes: + :species => [:a, :c, :d] + :nutrients => [:u, :v] + Edges: + :trophic + :a => [:c, :d] + :c => [:a] + :d => [:c, :u] + :mutualism + :a => [:d] + :interference + :a => [:c]", + ) + #! format: on + + # Node indices are correctly offset based on their types. + f = add_edges_within_node_type!( + deepcopy(g), + :nutrients, + :mutualism, # (say) + Bool[ + 0 1 + 0 0 + ], + ) + #! format: off + check_display(f, + "Topology(2 node types, 3 edge types, 5 nodes, 5 edges)", + raw"Topology for 2 node types and 3 edge types with 5 nodes and 5 edges: + Nodes: + :species => [:a, :c, :d] + :nutrients => [:u, :v] + Edges: + :trophic + :a => [:c] + :d => [:u] + :mutualism + :a => [:d] + :u => [:v] + :interference + :a => [:c]", + ) + e = Bool[;;] # (https://github.com/domluna/JuliaFormatter.jl/issues/837) + #! format: on + + @argfails( + add_edges_within_node_type!(deepcopy(g), :x, :trophic, e), + "Invalid node type label: :x. \ + Valid labels within this topology \ + are :nutrients and :species." + ) + + @argfails( + add_edges_within_node_type!(deepcopy(g), :species, :x, e), + "Invalid edge type label: :x. \ + Valid labels within this topology \ + are :interference, :mutualism and :trophic." + ) + + @argfails( + add_edges_within_node_type!(deepcopy(g), :species, :trophic, e), + "The given edges matrix should be of size (4, 4) \ + because there are 4 nodes of type :species. \ + Received instead: (0, 0)." + ) + + @argfails( + add_edges_within_node_type!( + deepcopy(g), + :species, + :trophic, + Bool[ + 0 1 1 1 + 0 0 0 0 + 1 0 0 0 + 0 0 1 0 + ], + ), + "Node :b (index 2) has been removed from this topology, \ + but the given matrix has a nonzero entry in column 2." + ) + + # Watch offset. + f = remove_node!(deepcopy(g), :u, :nutrients) + @argfails( + add_edges_within_node_type!( + f, + :nutrients, + :trophic, + Bool[ + 0 1 + 0 0 + ], + ), + "Node :u (index 5: 1st within the :nutrients node type) \ + has been removed from this topology, \ + but the given matrix has a nonzero entry in row 1." + ) + + @argfails( + add_edges_within_node_type!( + deepcopy(g), + :species, + :mutualism, + Bool[ + 0 0 1 1 + 0 0 0 0 + 1 0 0 0 + 0 0 1 0 + ], + ), + "There is already an edge of type :mutualism between nodes \ + :a and :d (indices 1 and 4), \ + but the given matrix has a nonzero entry in (1, 4)." + ) + + # Watch offset. + f = add_edge!(deepcopy(g), :mutualism, :u, :v) + @argfails( + add_edges_within_node_type!( + f, + :nutrients, + :mutualism, + Bool[ + 0 1 + 0 0 + ], + ), + "There is already an edge of type :mutualism between nodes \ + :u and :v (indices 5 and 6: resp. 1st and 2nd within node type :nutrients), \ + but the given matrix has a nonzero entry in (1, 2)." + ) + + #--------------------------------------------------------------------------------------- + # Accross node compartments. + + f = add_edges_accross_node_types!( + deepcopy(g), + :species, + :nutrients, + :trophic, + Bool[ + 0 1 + 0 0 + 1 0 + 0 0 + ], + ) + #! format: off + check_display(f, + "Topology(2 node types, 3 edge types, 5 nodes, 6 edges)", + raw"Topology for 2 node types and 3 edge types with 5 nodes and 6 edges: + Nodes: + :species => [:a, :c, :d] + :nutrients => [:u, :v] + Edges: + :trophic + :a => [:c, :v] + :c => [:u] + :d => [:u] + :mutualism + :a => [:d] + :interference + :a => [:c]", + ) + e = Bool[;;] # (https://github.com/domluna/JuliaFormatter.jl/issues/837) + #! format: on + + @argfails( + add_edges_accross_node_types!(deepcopy(g), :x, :nutrients, :trophic, e), + "Invalid node type label: :x. \ + Valid labels within this topology \ + are :nutrients and :species." + ) + + @argfails( + add_edges_accross_node_types!(deepcopy(g), :species, :x, :trophic, e), + "Invalid node type label: :x. \ + Valid labels within this topology \ + are :nutrients and :species." + ) + + @argfails( + add_edges_accross_node_types!(deepcopy(g), :species, :nutrients, :x, e), + "Invalid edge type label: :x. \ + Valid labels within this topology \ + are :interference, :mutualism and :trophic." + ) + + @argfails( + add_edges_accross_node_types!(deepcopy(g), :species, :species, :trophic, e), + "Source node types and target node types are the same (:species). \ + Use $add_edges_within_node_type! method instead." + ) + + @argfails( + add_edges_accross_node_types!(deepcopy(g), :species, :nutrients, :trophic, e), + "The given edges matrix should be of size (4, 2) \ + because there are 4 nodes of type :species \ + and 2 nodes of type :nutrients. Received instead: (0, 0)." + ) + + # Missing source node. + @argfails( + add_edges_accross_node_types!( + deepcopy(g), + :species, + :nutrients, + :trophic, + Bool[ + 0 1 + 1 0 + 1 0 + 0 0 + ], + ), + "Node :b has been removed from this topology, \ + but the given matrix has a nonzero entry in row 2." + ) + + # Missing target node. + f = remove_node!(deepcopy(g), :u, :nutrients) + @argfails( + add_edges_accross_node_types!( + f, + :species, + :nutrients, + :trophic, + Bool[ + 0 1 + 0 0 + 1 0 + 0 0 + ], + ), + "Node :u (index 5: 1st within the :nutrients node type) \ + has been removed from this topology, \ + but the given matrix has a nonzero entry in column 1." + ) + + @argfails( + add_edges_accross_node_types!( + deepcopy(g), + :species, + :nutrients, + :trophic, + Bool[ + 0 1 + 0 0 + 1 0 + 1 0 + ], + ), + "There is already an edge of type :trophic between nodes \ + :d and :u (indices 4 and 5: \ + resp. 4th and 1st within node types :species and :nutrients), \ + but the given matrix has a nonzero entry in (4, 1)." + ) +end + +@testset "Disconnected components." begin + + top = Topology() + add_nodes!(top, Symbol.(collect("abcd")), :species) + add_nodes!(top, Symbol.(collect("uv")), :nutrients) + add_edge_type!(top, :trophic) + add_edge_type!(top, :mutualism) + add_edge_type!(top, :interference) + add_edge!(top, :trophic, :a, :b) + add_edge!(top, :trophic, :b, :u) + add_edge!(top, :trophic, :c, :d) + add_edge!(top, :trophic, :d, :v) + add_edge!(top, :mutualism, :a, :u) + add_edge!(top, :interference, :c, :v) + + x, y = disconnected_components(top) + #! format: off + check_display(x, + "Topology(2 node types, 3 edge types, 3 nodes, 3 edges)", + raw"Topology for 2 node types and 3 edge types with 3 nodes and 3 edges: + Nodes: + :species => [:a, :b] + :nutrients => [:u] + Edges: + :trophic + :a => [:b] + :b => [:u] + :mutualism + :a => [:u] + :interference ", + ) + check_display(y, + "Topology(2 node types, 3 edge types, 3 nodes, 3 edges)", + raw"Topology for 2 node types and 3 edge types with 3 nodes and 3 edges: + Nodes: + :species => [:c, :d] + :nutrients => [:v] + Edges: + :trophic + :c => [:d] + :d => [:v] + :mutualism + :interference + :c => [:v]", + ) + #! format: on + + # Check adjacency matrices on separate components. - - - - - - - - - - - - - - - - - - - + @test adjacency_matrix(top, :species, :trophic, :species) == Bool[ + 0 1 0 0 + 0 0 0 0 + 0 0 0 1 + 0 0 0 0 + ] + @test adjacency_matrix(top, :species, :trophic, :nutrients) == Bool[ + 0 0 + 1 0 + 0 0 + 0 1 + ] + @test adjacency_matrix(x, :species, :trophic, :species) == Bool[ + 0 1 + 0 0 + ] + @test adjacency_matrix(y, :species, :trophic, :species) == Bool[ + 0 1 + 0 0 + ] + #! format: off + @test adjacency_matrix(x, :species, :trophic, :nutrients) == Bool[ + 0 + 1;; + ] + @test adjacency_matrix(y, :species, :trophic, :nutrients) == Bool[ + 0 + 1;; + ] + #! format: on + + transpose = true # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @test adjacency_matrix(top, :species, :trophic, :species; transpose) == Bool[ + 0 0 0 0 + 1 0 0 0 + 0 0 0 0 + 0 0 1 0 + ] + @test adjacency_matrix(top, :species, :trophic, :nutrients; transpose) == Bool[ + 0 1 0 0 + 0 0 0 1 + ] + @test adjacency_matrix(x, :species, :trophic, :species; transpose) == Bool[ + 0 0 + 1 0 + ] + @test adjacency_matrix(y, :species, :trophic, :species; transpose) == Bool[ + 0 0 + 1 0 + ] + @test adjacency_matrix(x, :species, :trophic, :nutrients; transpose) == Bool[0 1] + @test adjacency_matrix(y, :species, :trophic, :nutrients; transpose) == Bool[0 1] + + # Without pruning. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @test adjacency_matrix(x, :species, :trophic, :species; prune = false) == Bool[ + 0 1 0 0 + 0 0 0 0 + 0 0 0 0 + 0 0 0 0 + ] + @test adjacency_matrix(y, :species, :trophic, :species; prune = false) == Bool[ + 0 0 0 0 + 0 0 0 0 + 0 0 0 1 + 0 0 0 0 + ] + @test adjacency_matrix(x, :species, :trophic, :nutrients; prune = false) == Bool[ + 0 0 + 1 0 + 0 0 + 0 0 + ] + @test adjacency_matrix(y, :species, :trophic, :nutrients; prune = false) == Bool[ + 0 0 + 0 0 + 0 0 + 0 1 + ] + + transpose = true # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @test adjacency_matrix(x, :species, :trophic, :species; transpose, prune = false) == + Bool[ + 0 0 0 0 + 1 0 0 0 + 0 0 0 0 + 0 0 0 0 + ] + @test adjacency_matrix(y, :species, :trophic, :species; transpose, prune = false) == + Bool[ + 0 0 0 0 + 0 0 0 0 + 0 0 0 0 + 0 0 1 0 + ] + @test adjacency_matrix(x, :species, :trophic, :nutrients; transpose, prune = false) == + Bool[ + 0 1 0 0 + 0 0 0 0 + ] + @test adjacency_matrix(y, :species, :trophic, :nutrients; transpose, prune = false) == + Bool[ + 0 0 0 0 + 0 0 0 1 + ] + + +end + +end diff --git a/test/user/06-model_topology.jl b/test/user/06-model_topology.jl new file mode 100644 index 00000000..14505320 --- /dev/null +++ b/test/user/06-model_topology.jl @@ -0,0 +1,225 @@ +module TestModelTopology + +using EcologicalNetworksDynamics +import ..TestTopologies: check_display +using Test +using Random + +@testset "Basic topology queries." begin + + m = Model( + Foodweb([:a => [:b, :c], :b => :d, :c => :d, :e => [:c], :f => :g]), + Nutrients.Nodes(2), + ) + m += NontrophicInteractions.RefugeTopology(; A = [:g => :c, :d => :g]) + + g = m.topology + @test n_live_species(g) == 7 + @test n_live_nutrients(g) == 2 + + g = get_topology(m; without_species = [:c, :f], without_nutrients = [:n1]) + @test n_live_species(g) == 5 + @test n_live_nutrients(g) == 1 + + without = (; without_species = [:c, :f]) + @test n_live_producers(m; without...) == 2 + @test n_live_consumers(m; without...) == 3 + + sp(it) = m.species_label.(collect(it)) + nt(it) = m.nutrient_label.(collect(it)) + labs(str) = Symbol.(collect(str)) + @test sp(live_species(g)) == labs("abdeg") + @test nt(live_nutrients(g)) == [:n2] + @test sp(live_producers(m; without...)) == labs("dg") + @test sp(live_consumers(m; without...)) == labs("abe") + + #! format: off + @test adjacency_matrix(g, :species, :trophic, :nutrients) == Bool[ + 0 + 0 + 1 + 0 + 1;; + ] + #! format: on + + @test species_adjacency_matrix(g, :refuge) == Bool[ + 0 0 0 0 0 # :a + 0 0 0 0 0 # :b (c pruned) + 0 0 0 0 1 # :d + 0 0 0 0 0 # :e (f pruned) + 0 0 0 0 0 # :g + ] + + @test foodweb_matrix(g) == Bool[ + 0 1 0 0 0 + 0 0 1 0 0 + 0 0 0 0 0 + 0 0 0 0 0 + 0 0 0 0 0 + ] + + @test foodweb_matrix(g; transpose = true) == Bool[ + 0 0 0 0 0 + 1 0 0 0 0 + 0 1 0 0 0 + 0 0 0 0 0 + 0 0 0 0 0 + ] + + @test foodweb_matrix(g; prune = false) == Bool[ + 0 1 0 0 0 0 0 + 0 0 0 1 0 0 0 + 0 0 0 0 0 0 0 # :c included + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 # :f included + 0 0 0 0 0 0 0 + ] + +end + +@testset "Analyze biomass foodweb topology after species removals." begin + + m = Model(Foodweb([:a => [:b, :c], :b => :d, :c => :d, :e => [:c, :f], :g => :h])) + g = m.topology + + # Sort to ease testing. + sortadj(g) = sort( + collect([pred => sort(collect(preys)) for (pred, preys) in trophic_adjacency(g)]), + ) + + @test sortadj(g) == [ + :a => [:b, :c], + :b => [:d], + :c => [:d], + :d => [], + :e => [:c, :f], + :f => [], + :g => [:h], + :h => [], + ] + + # This graph has two disconnected components. + function check_components(g, n) + dc = collect(disconnected_components(g)) + @test length(dc) == n + dc + end + u, v = check_components(g, 2) + #! format: off + @test sortadj(u) == [ + :a => [:b, :c], + :b => [:d], + :c => [:d], + :d => [], + :e => [:c, :f], + :f => [], + ] + @test sortadj(v) == [ + :g => [:h], + :h => [], + ] + #! format: on + + # But no degenerated species yet. + check_set(fn, tops, expected, indices...) = + for top in tops + @test Set(m.species_label.(fn(top, indices...))) == Set(expected) + end + prods = m.producers_indices + cons = m.consumers_indices + check_set(isolated_producers, (g, u, v), [], prods) + check_set(starving_consumers, (g, u, v), [], prods, cons) + + # Removing species changes the situation. + mask = [name in "cg" for name in "abcdefgh"] + g = get_topology(m; without_species = mask) + + # Now there are three disconnected components. + u, v, w = check_components(g, 3) + @test sortadj(u) == [:a => [:b], :b => [:d], :d => []] + @test sortadj(v) == [:e => [:f], :f => []] + @test sortadj(w) == [:h => []] + + # A few quirks appear regarding foreseeable equilibrium state. + check_set(isolated_producers, (g, w), [:h], prods) + check_set(isolated_producers, (u, v), [], prods) + check_set(starving_consumers, (g, u, v, w), [], prods, cons) + + # The more extinct species the more quirks. + remove_species!(g, :d) + u, v, w = check_components(g, 3) + @test sortadj(u) == [:a => [:b], :b => []] + @test sortadj(v) == [:e => [:f], :f => []] + @test sortadj(w) == [:h => []] + check_set(isolated_producers, (g, w), [:h], prods) + check_set(starving_consumers, (g, u), [:a, :b], prods, cons) + check_set(isolated_producers, (u, v), [], prods) + check_set(starving_consumers, (v, v), [], prods, cons) + + # Producers connected by nutrients are not considered isolated anymore, + # and the corresponding topology is not anymore disconnected. + m += Nutrients.Nodes([:u]) + g = m.topology + @test length(collect(disconnected_components(g))) == 1 + + # Obtaining starving consumers is possible on extinction, + # but not isolated producers. + for name in "bcg" + remove_species!(g, name) + end + u, v = check_components(g, 2) + check_set(isolated_producers, (u, v), [], prods) + check_set(starving_consumers, (u,), [:a], prods, cons) + check_set(starving_consumers, (v,), [], prods, cons) + + # Even if the very last producer is only connected to its nutrient source. + for name in "adef" + remove_species!(g, name) + end + u, = check_components(g, 1) + check_set(isolated_producers, (u,), [], prods) + check_set(starving_consumers, (u,), [], prods, cons) + +end + +@testset "Elided display." begin + + Random.seed!(12) + foodweb = Foodweb(:niche; S = 50, C = 0.2) + m = default_model(foodweb, Nutrients.Nodes(5)) +#! format: off + check_display( + m.topology, + "Topology(2 node types, 1 edge type, 55 nodes, 516 edges)", + raw"Topology for 2 node types and 1 edge type with 55 nodes and 516 edges: + Nodes: + :species => [:s1, :s2, :s3, :s4, :s5, :s6, :s7, :s8, :s9, :s10, :s11, :s12, :s13, :s14, :s15, ..., :s50] + :nutrients => [:n1, :n2, :n3, :n4, :n5] + Edges: + :trophic + :s1 => [:s25, :s26, :s27, :s28, :s29, :s30, :s31, :s32, :s33, :s34, :s35, :s36, :s37, :s38, :s39, :s40] + :s2 => [:s1, :s10, :s11, :s12, :s13, :s14, :s15, :s16, :s17, :s18, :s19, :s2, :s20, :s21, :s22, ..., :s9] + :s3 => [:s1, :s10, :s11, :s12, :s13, :s14, :s15, :s16, :s17, :s18, :s19, :s2, :s20, :s21, :s22, ..., :s9] + :s4 => [:s21, :s22, :s23, :s24, :s25, :s26, :s27, :s28, :s29, :s30, :s31, :s32] + :s5 => [:s38, :s39, :s40, :s41, :s42] + :s6 => [:s1, :s10, :s11, :s12, :s13, :s14, :s15, :s16, :s17, :s18, :s19, :s2, :s20, :s21, :s22, ..., :s9] + :s7 => [:s37, :s38, :s39, :s40, :s41] + :s8 => [:s10, :s11, :s12, :s13, :s14, :s15, :s16, :s17, :s3, :s4, :s5, :s6, :s7, :s8, :s9] + :s9 => [:s23, :s24, :s25, :s26, :s27, :s28, :s29, :s30] + :s10 => [:s12, :s13, :s14, :s15, :s16, :s17, :s18, :s19, :s20] + :s11 => [:s28, :s29, :s30, :s31, :s32, :s33, :s34, :s35, :s36, :s37, :s38] + :s12 => [:s18, :s19, :s20] + :s13 => [:s13, :s14, :s15, :s16, :s17, :s18, :s19, :s20, :s21, :s22, :s23, :s24, :s25, :s26, :s27, ..., :s29] + :s14 => [:s18, :s19, :s20, :s21, :s22, :s23, :s24, :s25, :s26, :s27, :s28, :s29, :s30, :s31] + :s15 => [:s10, :s11, :s12, :s13, :s14, :s15, :s16, :s17, :s18, :s19, :s20, :s21, :s22, :s23, :s24, ..., :s9] + :s16 => [:s23, :s24, :s25, :s26, :s27] + ... + :s50 => [:n1, :n2, :n3, :n4, :n5]", + ) +#! format: on + +end + +end diff --git a/test/user/06-post-simulation.jl b/test/user/06-post-simulation.jl deleted file mode 100644 index 78091723..00000000 --- a/test/user/06-post-simulation.jl +++ /dev/null @@ -1,30 +0,0 @@ -# Check post-simulation utils. - -using Random -Random.seed!(12) - -#------------------------------------------------------------------------------------------- -@testset "Retrieve model from simulation result." begin - - m = default_model(Foodweb([:a => :b, :b => :c])) - sol = simulate(m, 0.5, 500) - - # Retrieve model from the solution obtained. - msol = get_model(sol) - @test msol == m - - # The value we get is a fresh copy of the state at simulation time. - @test msol !== m # *Not* an alias. - - # Cannot be corrupted afterwards from the original value. - @test m.K[:c] == 1 - m.K[:c] = 2 - @test m.K[:c] == 2 # Okay to keep working on original value. - @test msol.K[:c] == 1 # Still true: simulation was done with 1, not 2. - - # Cannot be corrupted afterwards from the retrieved value itself. - msol.K[:c] = 3 - @test msol.K[:c] == 3 # Okay to work on this one: user owns it. - @test get_model(sol).K[:c] == 1 # Still true. - -end diff --git a/test/user/07-post-simulation.jl b/test/user/07-post-simulation.jl new file mode 100644 index 00000000..fdaf6258 --- /dev/null +++ b/test/user/07-post-simulation.jl @@ -0,0 +1,126 @@ +module TestPostSimulation + +using EcologicalNetworksDynamics +using Test +using Random +import ..Main: @argfails + +Random.seed!(12) + +@testset "Retrieve model from simulation result." begin + + m = default_model(Foodweb([:a => :b, :b => :c])) + sol = simulate(m, 0.5, 500) + + # Retrieve model from the solution obtained. + msol = get_model(sol) + @test msol == m + + # The value we get is a fresh copy of the state at simulation time. + @test msol !== m # *Not* an alias. + + # Cannot be corrupted afterwards from the original value. + @test m.K[:c] == 1 + m.K[:c] = 2 + @test m.K[:c] == 2 # Okay to keep working on original value. + @test msol.K[:c] == 1 # Still true: simulation was done with 1, not 2. + + # Cannot be corrupted afterwards from the retrieved value itself. + msol.K[:c] = 3 + @test msol.K[:c] == 3 # Okay to work on this one: user owns it. + @test get_model(sol).K[:c] == 1 # Still true. + +end + +@testset "Retrieve correct trajectory indices from simulation results" begin + + m = default_model(Foodweb([:a => :b, :b => :c]), Nutrients.Nodes(2)) + sol = simulate(m, 0.5, 500; N0 = 0.2) + + # Pick correct values within the trajectory. + sp = get_species_indices(sol) + nt = get_nutrients_indices(sol) + @test sp == 1:3 + @test nt == 4:5 + @test sol.u[1][sp] == [0.5, 0.5, 0.5] + @test sol.u[1][nt] == [0.2, 0.2] + +end + + +@testset "Retrieve extinct species." begin + + m = default_model(Foodweb([:a => :b, :b => :c]), Mortality([0, 1, 0])) + sol = simulate(m, 0.5, 600; show_degenerated_biomass_graph_properties = false) + @test get_extinctions(sol) == Dict([1 => 256.8040524344076, 2 => 484.0702074171516]) + +end + +@testset "Retrieve topology from simulation result." begin + + m = default_model(Foodweb([:a => :b, :b => :c]), Mortality([0, 1, 0])) + # An information message is displayed in case the resulting topology is degenerated. + sol = @test_logs ( + :info, + """ + The biomass graph at the end of simulation contains degenerated species nodes: + Connected component with 1 species: + - /!\\ 1 isolated producer [:c] + This message is meant to attract your attention \ + regarding the meaning of downstream analyses \ + depending on the simulated biomasses values. + You can silent it with `show_degenerated_biomass_graph_properties=false`.""", + ) simulate(m, 0.5, 600) + top = get_topology(sol) + + # Only the producer remains in there. + @test collect(live_species(top)) == [3] + + # Test on wider graph. + m = default_model( + Foodweb([:a => [:b, :c], :b => :d, :c => :d, :e => [:c, :f], :g => :h]), + Mortality([ + :a => 0, + :b => 0, + # These three get extinct. + :c => 1, + :d => 10, + :e => 1, + :f => 0, + :g => 0, + :h => 0, + ]), + ) + sol = @test_logs ( + :info, + """ + The biomass graph at the end of simulation contains 3 disconnected components: + Connected component with 2 species: + - /!\\ 2 starving consumers [:a, :b] + Connected component with 1 species: + - /!\\ 1 isolated producer [:f] + Connected component with 2 species: + - 1 producer [:h] + - 1 consumer [:g] + This message is meant to attract your attention \ + regarding the meaning of downstream analyses \ + depending on the simulated biomasses values. + You can silent it with `show_degenerated_biomass_graph_properties=false`.""", + ) simulate(m, 0.5, 100) + @test get_extinctions(sol) == + Dict([3 => 22.565016968038158, 4 => 23.16730328349786, 5 => 61.763749935102005]) + + # Scroll back in time. + @test get_extinctions(sol; date = 60) == + Dict([3 => 22.565016968038158, 4 => 23.16730328349786]) + @test get_extinctions(sol; date = 23) == Dict([3 => 22.565016968038158]) + @test get_extinctions(sol; date = 20) == Dict([]) + + @argfails( + get_extinctions(sol; date = 150), + "Invalid date for a simulation in t = [0.0, 100.0]: 150." + ) + +end + +end diff --git a/test/user/data_components/nutrients/nodes.jl b/test/user/data_components/nutrients/nodes.jl index 81d191ac..1f4affa1 100644 --- a/test/user/data_components/nutrients/nodes.jl +++ b/test/user/data_components/nutrients/nodes.jl @@ -6,6 +6,11 @@ @test m.n_nutrients == m.nutrients_richness == 3 @test m.nutrients_names == [:n1, :n2, :n3] + # Get a closure to convert index to label. + lab = m.nutrient_label + @test lab(1) == :n1 + @test lab.([1, 2, 3]) == [:n1, :n2, :n3] + m = Model(Nutrients.Nodes([:a, :b, :c])) @test m.nutrients_index == OrderedDict(:a => 1, :b => 2, :c => 3) @@ -15,6 +20,11 @@ "Nutrients 1 and 3 are both named :a." ) + @argfails( + Model(Nutrients.Nodes(2)).nutrient_label(3), + "Invalid index (3) when there are 2 nutrients names." + ) + # But blueprints exist to construct it from a foodweb. n = Nutrients.Nodes(:one_per_producer) m = Model(Foodweb([:a => :b, :c => :d])) + n diff --git a/test/user/data_components/species.jl b/test/user/data_components/species.jl index f04b424a..98b494f8 100644 --- a/test/user/data_components/species.jl +++ b/test/user/data_components/species.jl @@ -9,6 +9,11 @@ @test m.species_index == Dict(:a => 1, :b => 2, :c => 3) @test m.species_names == [:a, :b, :c] + # Get a closure to convert index to label. + lab = m.species_label + @test lab(1) == :a + @test lab.([1, 2, 3]) == [:a, :b, :c] + # Default names. @test Species(3).names == [:s1, :s2, :s3] @@ -18,6 +23,11 @@ "Species 1 and 3 are both named :a." ) + @argfails( + Model(Species([:a, :b])).species_label(3), + "Invalid index (3) when there are 2 species names." + ) + # Cannot query without the component. @sysfails( Model().richness,