From 496ebe926cb62febaf0e76da9a1862f1b90ecb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Lajaaiti?= Date: Wed, 6 Mar 2024 18:03:03 +0100 Subject: [PATCH 1/2] Fix deep bug within Internals.simulate. `isoutofdomain` only checked for species biomasses and not nutrients. --- src/Internals/model/simulate.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internals/model/simulate.jl b/src/Internals/model/simulate.jl index e759c127a..8fc0c86af 100644 --- a/src/Internals/model/simulate.jl +++ b/src/Internals/model/simulate.jl @@ -185,7 +185,7 @@ function simulate( problem, alg; callback = callback, - isoutofdomain = (u, p, t) -> any(x -> x < 0, u[species_indices(params)]), + isoutofdomain = (u, p, t) -> any(x -> x < 0, u), kwargs..., ) sol From 84bf807cf5bb5c43eed6d8d79f910903f8975cea Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Tue, 20 Feb 2024 14:37:08 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20Update=20defaults.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `BodyMass`: - `Z=10` (Brose2006) - This blueprint is not embedded anymore by `ClassicResponse` by default, because `BodyMass` is typically already in the system when the `ClassicResponse` is added, as it is useful to calculate other parameters. - `simulate`: - `tmax` argument becomes mandatory and positional. - Don't use `TerminateSteadyState` by default. - `extinction_threshold` is lowered to 1e-12: if species should go extinct they will. - `verbose = false`, phew. - `Nutrients`: - Nutrients.Nodes(:one_per_producer). - New default values from Brose2008. --- src/components/functional_responses.jl | 5 +- src/components/nutrients/nodes.jl | 70 ++++++++++++++++--- src/components/producer_growth.jl | 22 ++++-- src/default_model.jl | 33 +++------ src/methods/main.jl | 30 +++++++- test/user/04-default_model.jl | 24 +++---- test/user/05-basic_pipelines.jl | 70 +++++++++---------- test/user/code_components/classic_response.jl | 9 ++- test/user/code_components/nutrient_intake.jl | 45 +++++------- .../nutrients/concentration.jl | 2 +- .../nutrients/half_saturation.jl | 2 +- test/user/data_components/nutrients/nodes.jl | 15 +++- test/user/data_components/nutrients/supply.jl | 2 +- .../data_components/nutrients/turnover.jl | 2 +- 14 files changed, 203 insertions(+), 128 deletions(-) diff --git a/src/components/functional_responses.jl b/src/components/functional_responses.jl index f70f595a9..0a1628ca5 100644 --- a/src/components/functional_responses.jl +++ b/src/components/functional_responses.jl @@ -57,7 +57,10 @@ mutable struct ClassicResponse <: FunctionalResponse ClassicResponse, kwargs; default = ( - M = (Z = 1,), + # Don't bring BodyMass by default, + # because it has typically already been added before + # as it is useful to calculate numerous other parameters. + M = nothing, e = :Miele2019, h = 2, w = :homogeneous, diff --git a/src/components/nutrients/nodes.jl b/src/components/nutrients/nodes.jl index 20b18dfc1..d8e2930f1 100644 --- a/src/components/nutrients/nodes.jl +++ b/src/components/nutrients/nodes.jl @@ -1,14 +1,27 @@ # Nutrients nodes compartments, akin to `Nodes`. +# Two possible blueprints because their number can be inferred from producer species. +# ========================================================================================== # Call it 'Nodes' because the module is already named 'Nutrients'. -mutable struct Nodes <: ModelBlueprint +abstract type Nodes <: ModelBlueprint end +# Don't export, to encourage disambiguated access as `Nutrients.Nodes`. + +Nodes(raw) = + if raw isa Symbol + NodesFromFoodweb(raw) + else + RawNodes(raw) + end + +# Akin to Species. +mutable struct RawNodes <: Nodes names::Vector{Symbol} - Nodes(names) = new(Symbol.(names)) - Nodes(n::Integer) = new([Symbol(:n, i) for i in 1:n]) - Nodes(names::Vector{Symbol}) = new(names) + RawNodes(names) = new(Symbol.(names)) + RawNodes(n::Integer) = new([Symbol(:n, i) for i in 1:n]) + RawNodes(names::Vector{Symbol}) = new(names) end -function F.check(_, bp::Nodes) +function F.check(_, bp::RawNodes) (; names) = bp # Forbid duplicates (triangular check). for (i, a) in enumerate(names) @@ -19,15 +32,48 @@ function F.check(_, bp::Nodes) end end -function F.expand!(model, bp::Nodes) +function add_nutrients!(model, names) # Store in the scratch, and only alias to model.producer_growth # if the corresponding component is loaded. - model._scratch[:nutrients_names] = bp.names - model._scratch[:nutrients_index] = OrderedDict(n => i for (i, n) in enumerate(bp.names)) + model._scratch[:nutrients_names] = names + model._scratch[:nutrients_index] = OrderedDict(n => i for (i, n) in enumerate(names)) +end + +F.expand!(model, bp::Nodes) = add_nutrients!(model, bp.names) + +@component RawNodes + +#------------------------------------------------------------------------------------------- +mutable struct NodesFromFoodweb <: Nodes + method::Symbol + function NodesFromFoodweb(method) + method = Symbol(method) + @check_symbol method (:one_per_producer,) + new(method) + end +end + +function F.early_check(_, bp::NodesFromFoodweb) + (; method) = bp + @check_symbol method (:one_per_producer,) end -@component Nodes -# Don't export to encourage disambiguated access as `Nutrients.Nodes`. +function F.expand!(model, bp::NodesFromFoodweb) + (; method) = bp + (; n_producers) = model + names = @build_from_symbol( + method, + :one_per_producer => [Symbol(:n, i) for i in 1:n_producers], + ) + add_nutrients!(model, names) +end + +@component NodesFromFoodweb requires(Foodweb) + +#------------------------------------------------------------------------------------------- +@conflicts(RawNodes, NodesFromFoodweb) +# Temporary semantic fix before framework refactoring. +F.componentof(::Type{<:Nodes}) = Nodes # ========================================================================================== @expose_data graph begin @@ -56,7 +102,9 @@ macro nutrients_index() end # ========================================================================================== -function F.display(model, ::Type{Nodes}) +display_short(bp::Nodes; kwargs...) = display_short(bp, Nodes; kwargs...) +display_long(bp::Nodes; kwargs...) = display_long(bp, Nodes; kwargs...) +function F.display(model, ::Type{<:Nodes}) N = model.n_nutrients names = model.nutrients_names "Nutrients: $N ($(join_elided(names, ", ")))" diff --git a/src/components/producer_growth.jl b/src/components/producer_growth.jl index 056584d4e..f8c29505d 100644 --- a/src/components/producer_growth.jl +++ b/src/components/producer_growth.jl @@ -50,20 +50,28 @@ mutable struct NutrientIntake <: ProducerGrowth supply::Option{Nutrients.Supply} concentration::Option{Nutrients.Concentration} half_saturation::Option{Nutrients.HalfSaturation} - function NutrientIntake(nodes = nothing; kwargs...) - (!isnothing(nodes) && haskey(kwargs, :nodes)) && - argerr("Nodes specified once as plain argument ($(repr(nodes))) \ - and once as keyword argument (nodes = $(kwargs[:nodes])).") + function NutrientIntake(nodes = missing; kwargs...) + nodes = if haskey(kwargs, :nodes) + ismissing(nodes) || + argerr("Nodes specified once as plain argument ($(repr(nodes))) \ + and once as keyword argument (nodes = $(kwargs[:nodes])).") + kwargs[:nodes] + elseif ismissing(nodes) + :one_per_producer + else + nodes + end fields = fields_from_kwargs( NutrientIntake, kwargs; + # Values from Brose2008. default = (; r = :Miele2019, nodes, turnover = 0.25, - supply = 10, - concentration = 1, - half_saturation = 1, + supply = 4, + concentration = 0.5, + half_saturation = 0.15, ), ) new(fields...) diff --git a/src/default_model.jl b/src/default_model.jl index e99957a43..3eb4a7190 100644 --- a/src/default_model.jl +++ b/src/default_model.jl @@ -104,25 +104,6 @@ function default_model( # println(" collect from caller") # Pick from caller input. collect!(take_from_caller!(need)) - elseif need <: BodyMass && - !collected(need) && - !excluded(need) && - given(ClassicResponse) && - any(B <: BodyMass for B in F.brings(input[ClassicResponse])) - # println(" special bodymass case") - # Very special hack-patch to pick body mass - # from a given classic response if given. - # TODO: The proper fix is to reify the tree structure - # of brought sub-blueprints, use dedicated data structures to query it, - # and allow general editions of the blueprints given here - # within the default model. - # This will be better done after the framework has been refactored. - remove_hook!(BodyMass) - bp = deepcopy(input[ClassicResponse]) - bm = bp.M - bp.M = nothing - input[ClassicResponse] = bp - collect!(bm) elseif !collected(need) && !excluded(need) && haskey(hooks, need) # println(" collect from hooks") collect!(pop!(hooks, need)) @@ -210,7 +191,7 @@ function default_model( #--------------------------------------------------------------------------------------- # Not always required but often missing. - hooks[BodyMass] = BodyMass(; Z = 1) + hooks[BodyMass] = BodyMass(; Z = 10) hooks[MetabolicClass] = MetabolicClass(:all_invertebrates) #--------------------------------------------------------------------------------------- @@ -223,10 +204,12 @@ function default_model( () -> if nutrients_given NutrientIntake(; r = tb!(GrowthRate, temperature_given ? :Binzer2016 : :Miele2019), + # Pick defaults from Brose2008. + nodes = tb!(N.Nodes, :one_per_producer), turnover = tb!(N.Turnover, 0.25), - supply = tb!(N.Supply, 10), - concentration = tb!(N.Concentration, 1), - half_saturation = tb!(N.HalfSaturation, 1), + supply = tb!(N.Supply, 4), + concentration = tb!(N.Concentration, 0.5), + half_saturation = tb!(N.HalfSaturation, 0.15), ) elseif temperature_given LogisticGrowth(; @@ -250,7 +233,7 @@ function default_model( FunctionalResponse, () -> if temperature_given ClassicResponse(; - M = tb!(BodyMass, (; Z = 1)), + M = nothing, e = tb!(Efficiency, :Miele2019), h = tb!(HillExponent, 2), w = tb!(ConsumersPreferences, :homogeneous), @@ -260,7 +243,7 @@ function default_model( ) elseif nti_given ClassicResponse(; - M = tb!(BodyMass, (; Z = 1)), + M = nothing, e = tb!(Efficiency, :Miele2019), h = tb!(HillExponent, 2), w = tb!(ConsumersPreferences, :homogeneous), diff --git a/src/methods/main.jl b/src/methods/main.jl index 278fd0289..c1a566558 100644 --- a/src/methods/main.jl +++ b/src/methods/main.jl @@ -2,11 +2,35 @@ # which is the reason they live after all components specifications. # Major purpose of the whole model specification: simulate dynamics. -simulate(model, u0; kwargs...) = Internals.simulate(model, u0; kwargs...) +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) -@method extinction_callback depends(FunctionalResponse, ProducerGrowth, Metabolism, Mortality) +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/test/user/04-default_model.jl b/test/user/04-default_model.jl index 97e066deb..edb4fcd07 100644 --- a/test/user/04-default_model.jl +++ b/test/user/04-default_model.jl @@ -35,7 +35,7 @@ # Pick another functional response. m = default_model(fw, ClassicResponse()) @test has_component(m, ClassicResponse) - @test maximum(m.attack_rate) == 50 + @test maximum(m.attack_rate) ≈ 334.1719587843073 # Pick yet another functional response. m = default_model(fw, LinearResponse()) @@ -71,7 +71,7 @@ @test has_component(m, NutrientIntake) @test m.n_nutrients == 3 @test m.nutrients_turnover == [1 / 4, 1 / 4, 1 / 4] - @test m.nutrients_concentration == ones(2, 3) + @test m.nutrients_concentration == 0.5 .* ones(2, 3) @sysfails( m.K, Property(K), @@ -84,12 +84,12 @@ @test m.K == [0, 0, 1, 1] # Any nutrient component triggers this default. - m = default_model(fw, Nutrients.Turnover([1, 2, 3, 4])) + m = default_model(fw, Nutrients.Turnover(0.8)) @test !has_component(m, LogisticGrowth) @test has_component(m, NutrientIntake) - @test m.n_nutrients == 4 - @test m.nutrients_turnover == [1, 2, 3, 4] - @test m.nutrients_concentration == ones(2, 4) + @test m.n_nutrients == m.n_producers == 2 + @test m.nutrients_turnover == [0.8, 0.8] + @test m.nutrients_concentration == 0.5 .* ones(2, 2) @sysfails( m.K, Property(K), @@ -97,12 +97,12 @@ ) # Tweak directly from inside the aggregated blueprint. - m = default_model(fw, NutrientIntake(; turnover = [1, 2])) + m = default_model(fw, NutrientIntake(; turnover = 0.8)) @test !has_component(m, LogisticGrowth) @test has_component(m, NutrientIntake) - @test m.n_nutrients == 2 - @test m.nutrients_turnover == [1, 2] - @test m.nutrients_concentration == ones(2, 2) + @test m.n_nutrients == m.n_producers == 2 + @test m.nutrients_turnover == [0.8, 0.8] + @test m.nutrients_concentration == 0.5 .* ones(2, 2) @sysfails( m.K, Property(K), @@ -111,8 +111,8 @@ # Combine if meaningful. m = default_model(fw, Temperature(), NutrientIntake(; turnover = [1, 2])) - @test m.nutrients_supply == [10, 10] - @test m.attack_rate[1, 2] == 2.0452306245234897e-6 + @test m.nutrients_supply == [4, 4] + @test m.attack_rate[1, 2] == 7.686741690921419e-7 # Add multiplex layers. m = default_model(fw, CompetitionLayer(; A = (C = 0.2, sym = true), I = 2)) diff --git a/test/user/05-basic_pipelines.jl b/test/user/05-basic_pipelines.jl index 3057e6785..feb80b843 100644 --- a/test/user/05-basic_pipelines.jl +++ b/test/user/05-basic_pipelines.jl @@ -10,8 +10,9 @@ Random.seed!(12) fw = Foodweb([0 1 0; 0 0 1; 0 0 0]) # (inline matrix input) m = default_model(fw) B0 = [0.5, 0.5, 0.5] - sol = simulate(m, B0) - @test sol.u[end] ≈ [0.04885048633720308, 0.1890242584230083, 0.22059687801237501] + tmax = 500 + sol = simulate(m, B0, tmax) + @test sol.u[end] ≈ [0.6505703879774151, 0.1889414733543331, 0.4164973283173464] end @@ -23,7 +24,7 @@ end # Add components one by one. add!(m, Foodweb([:a => :b, :b => :c])) # (named adjacency input) - add!(m, BodyMass(1)) + add!(m, BodyMass(; Z = 10)) add!(m, MetabolicClass(:all_invertebrates)) add!(m, BioenergeticResponse(; w = :homogeneous, half_saturation_density = 0.5)) add!(m, LogisticGrowth(; r = 1, K = 1)) @@ -31,8 +32,8 @@ end add!(m, Mortality(0)) # Simulate. - sol = simulate(m, 0.5) # (all initial values to 0.5) - @test sol.u[end] ≈ [0.04885048633720308, 0.1890242584230083, 0.22059687801237501] + 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] end @@ -41,7 +42,7 @@ end m = Model( Foodweb([:a => :b, :b => :c]), - BodyMass(1), + BodyMass(; Z = 10), MetabolicClass(:all_invertebrates), BioenergeticResponse(), LogisticGrowth(), @@ -49,8 +50,8 @@ end Mortality(0), ) - sol = simulate(m, [0.5, 0.5, 0.5]) - @test sol.u[end] ≈ [0.04885048633720308, 0.1890242584230083, 0.22059687801237501] + sol = simulate(m, [0.5, 0.5, 0.5], 500) + @test sol.u[end] ≈ [0.6505703879774151, 0.1889414733543331, 0.4164973283173464] end @@ -59,7 +60,7 @@ end # Construct blueprints independently from each other. fw = Foodweb([:a => :b, :b => :c]) - bm = BodyMass(1) + bm = BodyMass(; Z = 10) mc = MetabolicClass(:all_invertebrates) be = BioenergeticResponse() lg = LogisticGrowth() @@ -70,8 +71,8 @@ end m = Model() + fw + bm + mc + be + lg + mb + mt # (this produces a system copy on every '+') - sol = simulate(m, 0.5) - @test sol.u[end] ≈ [0.04885048633720308, 0.1890242584230083, 0.22059687801237501] + sol = simulate(m, 0.5, 500) + @test sol.u[end] ≈ [0.6505703879774151, 0.1889414733543331, 0.4164973283173464] end @@ -87,8 +88,8 @@ end # If provided, the default will not be used. m = default_model(fw, ClassicResponse()) - sol = simulate(m, 0.5) - @test sol.u[end] ≈ [0.2246409590398916, 0.09112832180307448, 0.5444058436109662] + sol = simulate(m, 0.5, 500) + @test sol.u[end] ≈ [0.30245442377904147, 0.1507782858041653, 0.8351420883977096] end @@ -101,8 +102,8 @@ end FacilitationLayer(; A = (L = 1,)), ) - sol = simulate(m, 0.5) - @test sol.u[end] ≈ [0.24726844778226592, 0.09114742274197872, 0.6904984843155931] + sol = simulate(m, 0.5, 500) + @test sol.u[end] ≈ [0.3073034568564342, 0.15077826302332667, 0.8791058938977693] end @@ -119,13 +120,13 @@ end ), ) - sol = simulate(m, 0.5) + sol = simulate(m, 0.5, 500) @test sol.u[end] ≈ [ - 0.29247159105315307 - 0.14537825150324735 - 0.12875174646007237 - 0.0 - 4.8215132134864145e-5 + 0.6871892011471322, + 0.24497058086035212, + 0.2034744714268744, + 0.0, + 0.00012545266696651692, ] end @@ -148,13 +149,13 @@ end # Access them with convenience aliases. m += layers[:facilitation] + layers[:c] + layers["ref"] + layers['i'] - sol = simulate(m, 0.5) + sol = simulate(m, 0.5, 500) @test sol.u[end] ≈ [ - 0.29247159105315307 - 0.14537825150324735 - 0.12875174646007237 - 0.0 - 4.8215132134864145e-5 + 0.6871892011471322, + 0.24497058086035212, + 0.2034744714268744, + 0.0, + 0.00012545266696651692, ] end @@ -163,16 +164,15 @@ end @testset "Nutrient Intake." begin # With nutrients (instead of logistic growth). - m = default_model(Foodweb([2 => 1, 3 => 2]), NutrientIntake(; concentration = [1 0.5])) + m = default_model(Foodweb([2 => 1, 3 => 2]), NutrientIntake(2; concentration = [1 0.5])) B0, N0 = rand(3), rand(2) - sol = simulate(m, B0; N0) - m._value.producer_growth.concentration + sol = simulate(m, B0, 500; N0) @test sol.u[end] ≈ [ - 0.28423925333678635, - 0.18879238451408806, - 0.1534109945177679, - 9.707009178714864, - 9.853504589357435, + 1.7872795749078765, + 0.18898223629746858, + 1.8337090324346232, + 0.06670978415974765, + 2.0333548914773587, ] end diff --git a/test/user/code_components/classic_response.jl b/test/user/code_components/classic_response.jl index 0774b5606..d5fbce2eb 100644 --- a/test/user/code_components/classic_response.jl +++ b/test/user/code_components/classic_response.jl @@ -4,14 +4,17 @@ base = Model(Foodweb(:niche; S = 5, C = 0.2), MetabolicClass(:all_invertebrates)) - # Code component blueprint brings all required data components with it. + # Code component blueprint brings all required data components with it.. clr = ClassicResponse() - @test clr.M.Z == 1 + @test isnothing(clr.M) # .. except for the body mass. @test clr.w == ConsumersPreferences(:homogeneous) @test clr.h == HillExponent(2) @test clr.handling_time.h_t == :Miele2019 - m = base + clr + # The body mass is typically brought another way. + bm = BodyMass(1) # (use constant mass to ease later tests) + + m = base + bm + clr @test m.h == 2 @test m.M == ones(5) @test m.intraspecific_interference == zeros(5) diff --git a/test/user/code_components/nutrient_intake.jl b/test/user/code_components/nutrient_intake.jl index 3b416b9df..b9d0aca32 100644 --- a/test/user/code_components/nutrient_intake.jl +++ b/test/user/code_components/nutrient_intake.jl @@ -2,45 +2,38 @@ import .Nutrients as N - Random.seed!(12) - base = Model( - Foodweb(:niche; S = 5, C = 0.2), + Foodweb([:a => :b, :c => [:d, :e]]), # 3 producers. BodyMass(1), MetabolicClass(:all_invertebrates), ) # Default blueprints. ni = NutrientIntake() - @test isnothing(ni.nodes) # Don't pick a default number of nodes. + @test ni.nodes == N.Nodes(:one_per_producer) @test ni.turnover == N.Turnover(0.25) @test ni.r.allometry[:p][:a] == 1 - @test ni.supply == N.Supply(10) - @test ni.concentration == N.Concentration(1) - @test ni.half_saturation == N.HalfSaturation(1) + @test ni.supply == N.Supply(4) + @test ni.concentration == N.Concentration(0.5) + @test ni.half_saturation == N.HalfSaturation(0.15) - m = base + Nutrients.Nodes(2) + ni - @test m.nutrients_turnover == [0.25, 0.25] - @test m.nutrients_supply == [10, 10] - @test m.nutrients_concentration == ones(3, 2) - @test m.nutrients_half_saturation == ones(3, 2) + m = base + ni + @test m.nutrients_turnover == [0.25, 0.25, 0.25] + @test m.nutrients_supply == [4, 4, 4] + @test m.nutrients_concentration == 0.5 .* ones(3, 3) + @test m.nutrients_half_saturation == 0.15 .* ones(3, 3) # Customize sub-blueprints. - ni = NutrientIntake(; turnover = [1, 2, 3, 4]) - @test ni.turnover == N.Turnover([1, 2, 3, 4]) + ni = NutrientIntake(; turnover = [1, 2, 3]) + @test ni.turnover == N.Turnover([1, 2, 3]) - # No need to explicitly add nodes since it can be inferred from the above. - m = base + ni - @test m.nutrients_names == [:n1, :n2, :n3, :n4] - @test m.nutrients_turnover == [1, 2, 3, 4] - @test m.nutrients_supply == [10, 10, 10, 10] - @test m.nutrients_concentration == ones(3, 4) - @test m.nutrients_half_saturation == ones(3, 4) - - # Although the number of nodes may be specified/brought by the blueprint. + # The exact number of nodes may be specified/brought by the blueprint. m = base + NutrientIntake(2) @test m.nutrients_names == [:n1, :n2] @test m.nutrients_turnover == [0.25, 0.25] + @test m.nutrients_supply == [4, 4] + @test m.nutrients_concentration == 0.5 .* ones(3, 2) + @test m.nutrients_half_saturation == 0.15 .* ones(3, 2) m = base + NutrientIntake([:u, :v]) @test m.nutrients_names == [:u, :v] @@ -51,7 +44,7 @@ # Watch consistency. @sysfails( - base + Nutrients.Nodes([:a, :b, :c]) + NutrientIntake(; supply = [1, 2]), + base + NutrientIntake(; supply = [1, 2]), Check(NutrientIntake, N.SupplyFromRawValues), "Invalid size for parameter 's': expected (3,), got (2,)." ) @@ -61,9 +54,9 @@ "blueprint also brings '$(Nutrients.Nodes)', which is already in the system." ) @sysfails( - base + Nutrients.Nodes(3) + NutrientIntake(; turnover = [1, 2]), + base + Nutrients.Nodes(1) + NutrientIntake(nothing; turnover = [1, 2]), Check(NutrientIntake, Nutrients.TurnoverFromRawValues), - "Invalid size for parameter 't': expected (3,), got (2,)." + "Invalid size for parameter 't': expected (1,), got (2,)." ) @sysfails( base + NutrientIntake(3; turnover = [1, 2]), diff --git a/test/user/data_components/nutrients/concentration.jl b/test/user/data_components/nutrients/concentration.jl index 50a6cf84d..c48b8ca82 100644 --- a/test/user/data_components/nutrients/concentration.jl +++ b/test/user/data_components/nutrients/concentration.jl @@ -43,7 +43,7 @@ FR = EN.Nutrients.ConcentrationFromRawValues @sysfails( base + Nutrients.Concentration(5), Check(FR), - "missing required component '$(Nutrients.Nodes)': implied." + "missing a required component '$(Nutrients.Nodes)': implied." ) end diff --git a/test/user/data_components/nutrients/half_saturation.jl b/test/user/data_components/nutrients/half_saturation.jl index 5cdc97109..177c25a33 100644 --- a/test/user/data_components/nutrients/half_saturation.jl +++ b/test/user/data_components/nutrients/half_saturation.jl @@ -39,7 +39,7 @@ FR = EN.Nutrients.HalfSaturationFromRawValues @sysfails( base + Nutrients.HalfSaturation(5), Check(FR), - "missing required component '$(Nutrients.Nodes)': implied." + "missing a required component '$(Nutrients.Nodes)': implied." ) end diff --git a/test/user/data_components/nutrients/nodes.jl b/test/user/data_components/nutrients/nodes.jl index ed41fb345..81d191acb 100644 --- a/test/user/data_components/nutrients/nodes.jl +++ b/test/user/data_components/nutrients/nodes.jl @@ -1,5 +1,6 @@ @testset "Nutrients nodes component." begin + # At its core, a raw, autonomous compartment. n = Nutrients.Nodes(3) m = Model(n) @test m.n_nutrients == m.nutrients_richness == 3 @@ -10,8 +11,20 @@ @sysfails( Model(Nutrients.Nodes([:a, :b, :a])), - Check(Nutrients.Nodes), + Check(Nutrients.RawNodes), "Nutrients 1 and 3 are both named :a." ) + # But blueprints exist to construct it from a foodweb. + n = Nutrients.Nodes(:one_per_producer) + m = Model(Foodweb([:a => :b, :c => :d])) + n + @test m.n_nutrients == 2 + @test m.nutrients_names == [:n1, :n2] + + @sysfails( + Model(n), + Check(Nutrients.NodesFromFoodweb), + "missing required component '$Foodweb'.", + ) + end diff --git a/test/user/data_components/nutrients/supply.jl b/test/user/data_components/nutrients/supply.jl index 4ca7f8def..4169fb75b 100644 --- a/test/user/data_components/nutrients/supply.jl +++ b/test/user/data_components/nutrients/supply.jl @@ -47,7 +47,7 @@ FR = Nutrients.SupplyFromRawValues @sysfails( Model(Nutrients.Supply(5)), Check(FR), - "missing required component '$(Nutrients.Nodes)': implied." + "missing a required component '$(Nutrients.Nodes)': implied." ) end diff --git a/test/user/data_components/nutrients/turnover.jl b/test/user/data_components/nutrients/turnover.jl index 2e25f8e3d..30dfd3f3a 100644 --- a/test/user/data_components/nutrients/turnover.jl +++ b/test/user/data_components/nutrients/turnover.jl @@ -47,7 +47,7 @@ FR = Nutrients.TurnoverFromRawValues @sysfails( Model(Nutrients.Turnover(5)), Check(FR), - "missing required component '$(Nutrients.Nodes)': implied." + "missing a required component '$(Nutrients.Nodes)': implied." ) end