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/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 23c8f0f2..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. @@ -21,12 +22,29 @@ 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 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. @@ -55,14 +73,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. @@ -84,11 +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") +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/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/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 4987a248..0b611215 100644 --- a/src/Internals/Internals.jl +++ b/src/Internals/Internals.jl @@ -58,6 +58,10 @@ 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 + +# Part of future refactoring here. +const Topology = EcologicalNetworksDynamics.Topologies.Topology 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..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(), @@ -46,10 +48,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. @@ -175,6 +183,7 @@ function ModelParameters( temperature_response, network, biorates, + Topology(), functional_response, producer_growth, nothing, 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/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/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/output-analysis.jl b/src/diversity.jl similarity index 68% rename from src/output-analysis.jl rename to src/diversity.jl index cc5b830c..3e3060d8 100644 --- a/src/output-analysis.jl +++ b/src/diversity.jl @@ -1,7 +1,5 @@ -import SciMLBase: AbstractODESolution - """ -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). @@ -16,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 @@ -28,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. @@ -69,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). @@ -102,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 @@ -122,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). @@ -144,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, @@ -178,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 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 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 c1a56655..00000000 --- a/src/methods/main.jl +++ /dev/null @@ -1,36 +0,0 @@ -# The methods defined here depends on several components, -# which is the reason they live after all components specifications. - -# Major purpose of the whole model specification: simulate dynamics. -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) -export simulate - -# 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/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/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/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") 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/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/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/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, 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