diff --git a/src/GraphDataInputs/check.jl b/src/GraphDataInputs/check.jl index fac54813c..8e1ecc69f 100644 --- a/src/GraphDataInputs/check.jl +++ b/src/GraphDataInputs/check.jl @@ -226,7 +226,6 @@ function check_list_refs( # If edges space for source and target are the same, allow elision. if list isa UAdjacency (space isa Int64 || space isa Index) && (space = (space, space)) - allow_missing || argerr("Dense adjacency lists checking is unimplemented yet.") end # Possibly infer the (integer) space from a template if none is given. @@ -339,6 +338,7 @@ end #------------------------------------------------------------------------------------------- # Checking missing refs, either against a template or against the whole space if none. +# (ASSUMING all refs are valid) miss_refs(map::UMap, n::Int64, ::Nothing) = length(map) < n miss_refs(map::UMap, x::Index, ::Nothing) = length(map) < length(x) function miss_refs(map::UMap, _, template::AbstractSparseVector) @@ -346,13 +346,39 @@ function miss_refs(map::UMap, _, template::AbstractSparseVector) length(map) < length(nz) end +function miss_refs(adj::UAdjacency, (m, n)::Tuple{Int64,Int64}, ::Nothing) + length(adj) < m && return true + for (_, sub) in adj + length(sub) < n && return true + end + false +end +function miss_refs(adj::UAdjacency, (x, y)::Tuple{Index,Index}, ::Nothing) + (m, n) = length.((x, y)) + miss_refs(adj, (m, n), nothing) +end +function miss_refs(adj::UAdjacency, _, template::AbstractSparseMatrix) + nz, _ = findnz(template) + sum(length(sub) for (_, sub) in adj) < length(nz) +end + needles(n::Int64, ::Nothing) = ((i,) for i in 1:n) needles(x::Index, ::Nothing) = ((k,) for k in keys(x)) +needles((m, n)::Tuple{Int64,Int64}, ::Nothing) = ((i, j) for i in 1:m, j in 1:n) +needles((x, y)::Tuple{Index,Index}, ::Nothing) = ((p, q) for p in keys(x), q in keys(y)) needles(::Int64, template::AbstractSparseVector) = ((i,) for i in findnz(template)[1]) +needles(::Tuple{Int64,Int64}, template::AbstractSparseMatrix) = + (access for access in zip(findnz(template)[1:2]...)) function needles(x::Index, template::AbstractSparseVector) revmap = Dict(i => n for (n, i) in x) sort!(collect((revmap[i],) for i in findnz(template)[1])) end +function needles((x, y)::Tuple{Index,Index}, template::AbstractSparseMatrix) + xrev = Dict(i => m for (m, i) in x) + yrev = Dict(j => n for (n, j) in y) + res = collect((xrev[i], yrev[j]) for (i, j) in zip(findnz(template)[1:2]...)) + sort!(res) +end check_missing_refs(list, space, template, name, item) = if miss_refs(list, space, template) diff --git a/src/GraphDataInputs/expand.jl b/src/GraphDataInputs/expand.jl index 16bf517df..341fd9544 100644 --- a/src/GraphDataInputs/expand.jl +++ b/src/GraphDataInputs/expand.jl @@ -147,6 +147,7 @@ function to_dense_vector(map::AbstractMap{Int64,T}) where {T} end res end + function to_dense_vector(map::AbstractMap{Symbol,T}, index) where {T} res = Vector{T}(undef, length(map)) for (key, value) in map @@ -155,6 +156,7 @@ function to_dense_vector(map::AbstractMap{Symbol,T}, index) where {T} end res end + export to_dense_vector # Assuming all indices are valid. @@ -165,6 +167,7 @@ function to_sparse_vector(map::AbstractMap{Int64,T}, n::Int64) where {T} end res end + function to_sparse_vector(map::AbstractMap{Symbol,T}, index) where {T} res = spzeros(T, length(index)) for (key, value) in map @@ -173,6 +176,7 @@ function to_sparse_vector(map::AbstractMap{Symbol,T}, index) where {T} end res end + function to_sparse_vector(map::AbstractBinMap{Int64}, n::Int64) res = spzeros(Bool, n) for i in map @@ -180,6 +184,7 @@ function to_sparse_vector(map::AbstractBinMap{Int64}, n::Int64) end res end + function to_sparse_vector(map::AbstractBinMap{Symbol}, index) res = spzeros(Bool, length(index)) for key in map @@ -188,6 +193,7 @@ function to_sparse_vector(map::AbstractBinMap{Symbol}, index) end res end + export to_sparse_vector # Accommodate slight signature variations in case an index is always used. @@ -202,9 +208,34 @@ to_sparse_vector(map::AbstractBinMap{Int64}, index) = to_sparse_vector(map, leng #------------------------------------------------------------------------------------------- # Assuming the input is a correctly checked adjacency list, -# expand to a sparse matrix. +# expand to a dense or sparse matrix. # This may require a label-to-indices mapping referred to as "index" (yup, confusing). +# Assuming all indices have been given. +function to_dense_matrix(adj::AbstractAdjacency{Int64,T}) where {T} + res = Matrix{T}(undef, (length(adj), length(last(first(adj))))) + for (i, list) in adj + for (j, value) in list + res[i, j] = value + end + end + res +end + +function to_dense_matrix(adj::AbstractAdjacency{Symbol,T}, i_index, j_index) where {T} + res = Matrix{T}(undef, (length(adj), length(last(first(adj))))) + for (ikey, list) in adj + for (jkey, value) in list + i = i_index[ikey] + j = j_index[jkey] + res[i, j] = value + end + end + res +end + +export to_dense_matrix + function to_sparse_matrix(adj::AbstractAdjacency{Int64,T}, n::Int64, m::Int64) where {T} res = spzeros(T, (n, m)) for (i, list) in adj @@ -214,6 +245,7 @@ function to_sparse_matrix(adj::AbstractAdjacency{Int64,T}, n::Int64, m::Int64) w end res end + function to_sparse_matrix(adj::AbstractAdjacency{Symbol,T}, i_index, j_index) where {T} res = spzeros(T, (length(i_index), length(j_index))) for (ikey, list) in adj @@ -225,6 +257,7 @@ function to_sparse_matrix(adj::AbstractAdjacency{Symbol,T}, i_index, j_index) wh end res end + function to_sparse_matrix(adj::AbstractBinAdjacency{Int64}, n::Int64, m::Int64) res = spzeros(Bool, (n, m)) for (i, list) in adj @@ -234,6 +267,7 @@ function to_sparse_matrix(adj::AbstractBinAdjacency{Int64}, n::Int64, m::Int64) end res end + function to_sparse_matrix(adj::AbstractBinAdjacency{Symbol}, i_index, j_index) res = spzeros(Bool, (length(i_index), length(j_index))) for (ikey, list) in adj @@ -245,9 +279,21 @@ function to_sparse_matrix(adj::AbstractBinAdjacency{Symbol}, i_index, j_index) end res end + export to_sparse_matrix # Accommodate slight signature variations in case an index is always used. +function to_dense_matrix(adj::AbstractAdjacency{Int64,T}, i_index, j_index) where {T} + m, i = length.((adj, i_index)) + n, j = length.((last(first(adj)), j_index)) + m == i || argerr( + "Cannot produce a dense matrix with $m outer values and $i outer references.", + ) + n == j || argerr( + "Cannot produce a dense matrix with $n inner values and $j inner references.", + ) + to_dense_matrix(adj) +end to_sparse_matrix(map::AbstractAdjacency{Int64,T}, i_index, j_index) where {T} = to_sparse_matrix(map, length(i_index), length(j_index)) to_sparse_matrix(map::AbstractBinAdjacency{Int64}, i_index, j_index) = @@ -335,6 +381,16 @@ macro to_sparse_vector_if_map(var::Symbol, index) end export @to_sparse_vector_if_map +macro to_dense_matrix_if_adjacency(var::Symbol, i_index, j_index) + var, i_index, j_index = esc.((var, i_index, j_index)) + quote + $var isa + Union{AbstractDict{<:Any,<:AbstractDict},AbstractDict{<:Any,<:AbstractSet}} && + ($var = to_dense_matrix($var, $i_index, $j_index)) + end +end +export @to_dense_matrix_if_adjacency + macro to_sparse_matrix_if_adjacency(var::Symbol, i_index, j_index) var, i_index, j_index = esc.((var, i_index, j_index)) quote diff --git a/src/components/display.jl b/src/components/display.jl index 7a0e096b1..15087a97d 100644 --- a/src/components/display.jl +++ b/src/components/display.jl @@ -1,14 +1,19 @@ -# Display matrix nonzero values ranges. -function showrange(io::IO, m::SparseMatrix) +# Display values ranges. +function showrange(io::IO, values) + min, max = extrema(values) + if min == max + print(io, "$min") + else + print(io, "$min to $max") + end +end + +# Restrict to nonzero values. +function showrange(io::IO, m::AbstractSparseArray) nz = findnz(m)[3] if isempty(nz) print(io, "·") else - min, max = extrema(nz) - if min == max - print(io, "$min") - else - print(io, "$min to $max") - end + showrange(io, nz) end end diff --git a/src/components/main.jl b/src/components/main.jl index b1c0a9d83..fa726cc1f 100644 --- a/src/components/main.jl +++ b/src/components/main.jl @@ -82,29 +82,29 @@ include("./foodweb.jl") # Biorates and other values parametrizing the ODE. # (typical example 'nodes' data) include("./body_mass.jl") -# include("./metabolic_class.jl") +include("./metabolic_class.jl") -# # Useful global values to calculate other biorates. -# # (typical example 'graph' data) -# include("./temperature.jl") +# Useful global values to calculate other biorates. +# (typical example 'graph' data) +include("./temperature.jl") -# # Replicated/adapted from the above. -# # TODO: factorize subsequent repetitions there. -# # Easier once the Internals become more consistent? -# include("./hill_exponent.jl") # <- First, good example of 'graph' component. Read first. -# include("./growth_rate.jl") # <- First, good example of 'node' component. Read first. -# include("./efficiency.jl") # <- First, good example of 'edges' component. Read first. -# include("./carrying_capacity.jl") -# include("./mortality.jl") -# include("./metabolism.jl") -# include("./maximum_consumption.jl") -# include("./producers_competition.jl") -# include("./consumers_preferences.jl") -# include("./handling_time.jl") -# include("./attack_rate.jl") -# include("./half_saturation_density.jl") -# include("./intraspecific_interference.jl") -# include("./consumption_rate.jl") +# Replicated/adapted from the above. +# TODO: factorize subsequent repetitions there. +# Easier once the Internals become more consistent? +include("./hill_exponent.jl") # <- First, good example of 'graph' component. Read first. +include("./growth_rate.jl") # <- First, good example of 'node' component. Read first. +include("./efficiency.jl") # <- First, good example of 'edges' component. Read first. +include("./carrying_capacity.jl") +include("./mortality.jl") +include("./metabolism.jl") +include("./maximum_consumption.jl") +include("./producers_competition.jl") +include("./consumers_preferences.jl") +include("./handling_time.jl") +include("./attack_rate.jl") +include("./half_saturation_density.jl") +include("./intraspecific_interference.jl") +include("./consumption_rate.jl") # Namespace nutrients data. include("./nutrients/main.jl") diff --git a/src/components/nutrients/concentration.jl b/src/components/nutrients/concentration.jl index 1018d90cd..9742224f3 100644 --- a/src/components/nutrients/concentration.jl +++ b/src/components/nutrients/concentration.jl @@ -7,67 +7,119 @@ # - sparse, with the size of their compartment (eg. S with missing values for consumers). # - dense, with the size of the filtered compartment (eg. n_producers) # and then care must be taken while indexing it. +# Whether to use the first or the second option should be clarified +# on internals refactoring. -# ========================================================================================== -abstract type Concentration <: ModelBlueprint end -# All subtypes must require(Nutrients.Nodes,Foodweb). +# (reassure JuliaLS) +(false) && (local Concentration, _Concentration) -Concentration(c) = ConcentrationFromRawValues(c) -export Concentration +# ========================================================================================== +module Concentration_ +include("../blueprint_modules.jl") +include("../blueprint_modules_identifiers.jl") +import .EN: Foodweb, Nutrients #------------------------------------------------------------------------------------------- -# TODO: it used to be possible to specify concentration row-wise.. -# but is that really a good idea regarding the confusion with column-wise? -# Also disallow adjacency list input because of the same confusion. -mutable struct ConcentrationFromRawValues <: Concentration - c::@GraphData {Scalar, Matrix}{Float64} - ConcentrationFromRawValues(c) = new(@tographdata c SM{Float64}) +mutable struct Raw <: Blueprint + c::Matrix{Float64} + nutrients::Brought(Nutrients.Nodes) + Raw(c, nt = Nutrients._Nodes) = new(Float64.(c), nt) end +F.implied_blueprint_for(bp::Raw, ::Nutrients._Nodes) = Nutrients.Nodes(size(bp.c)[2]) +@blueprint Raw "producers × nutrients concentration matrix" +export Raw -F.can_imply(bp::ConcentrationFromRawValues, ::Type{Nutrients.Nodes}) = !(bp.c isa Real) -Nutrients.Nodes(bp::ConcentrationFromRawValues) = Nutrients.Nodes(size(bp.c, 2)) +F.early_check(bp::Raw) = check_edges(check, bp.c) +check(c, ref = nothing) = check_value(>=(0), c, ref, :c, "Not a positive value") -function F.check(model, bp::ConcentrationFromRawValues) - (; n_producers, n_nutrients) = model +function F.late_check(raw, bp::Raw) (; c) = bp - @check_size_if_matrix c (n_producers, n_nutrients) + P = @get raw.producers.number + N = @get raw.nutrients.number + @check_size c (P, N) end -function F.expand!(model, bp::ConcentrationFromRawValues) - (; n_producers, n_nutrients) = model - (; c) = bp - @to_size_if_scalar Real c (n_producers, n_nutrients) - model._scratch[:nutrients_concentration] = c +F.expand!(raw, bp::Raw) = expand!(raw, bp.c) +expand!(raw, c) = raw._scratch[:nutrients_concentration] = c + +#------------------------------------------------------------------------------------------- +mutable struct Flat <: Blueprint + c::Float64 end +@blueprint Flat "uniform concentration value" depends(Foodweb, Nutrients.Nodes) +export Flat -@component ConcentrationFromRawValues requires(Foodweb) implies(Nutrients.Nodes) -export ConcentrationFromRawValues +F.early_check(bp::Flat) = check(bp.c) +function F.expand!(raw, bp::Flat) + P = @get raw.producers.number + N = @get raw.nutrients.number + expand!(raw, to_size(bp.c, (P, N))) +end #------------------------------------------------------------------------------------------- -# @conflicts(ConcentrationFromRawValues) # Keep in case more alternate blueprints are added. -# Temporary semantic fix before framework refactoring. -F.componentof(::Type{<:Concentration}) = Concentration +mutable struct Adjacency <: Blueprint + c::@GraphData Adjacency{Float64} + nutrients::Brought(Nutrients.Nodes) + Adjacency(c, nt = Nutrients._Nodes) = new(@tographdata(c, Adjacency{Float64}), nt) +end +function F.implied_blueprint_for(bp::Adjacency, ::Nutrients._Nodes) + # HERE: this should've been done for every such adjacency implication, right? + space = refspace_inner(bp.c) + if space isa Integer + Nutrients.Nodes(space) + else + Nutrients.Nodes(keys(space)) + end +end +@blueprint Adjacency "[producer => [nutrient => concentration]] map" +export Adjacency + +F.early_check(bp::Adjacency) = check_edges(check, bp.c) +function F.late_check(raw, bp::Adjacency) + (; c) = bp + p_index = @ref raw.producers.sparse_index + n_index = @ref raw.nutrients.index + @check_list_refs c "producer trophic" (p_index, n_index) dense +end + +function F.expand!(raw, bp::Adjacency) + p_index = @ref raw.producers.dense_index + n_index = @ref raw.nutrients.index + c = to_dense_matrix(bp.c, p_index, n_index) + expand!(raw, c) +end -# ========================================================================================== -@expose_data edges begin - property(nutrients_concentration) - get(Concentrations{Float64}, "producer-to-nutrient link") - ref(m -> m._scratch[:nutrients_concentration]) - write!((m, rhs, i, j) -> (m._nutrients_concentration[i, j] = rhs)) - row_index(m -> m._producers_dense_index) - col_index(m -> m._nutrients_index) - depends(Concentration) end # ========================================================================================== -display_short(bp::Concentration; kwargs...) = display_short(bp, Concentration; kwargs...) -display_long(bp::Concentration; kwargs...) = display_long(bp, Concentration; kwargs...) -function F.display(model, ::Type{<:Concentration}) - c = model.nutrients_concentration - min, max = minimum(c), maximum(c) - "Nutrients concentration: " * if min == max - "$min" +@component begin + Concentration{Internal} + requires(Foodweb, Nutrients.Nodes) + blueprints(Concentration_) +end +export Concentration + +function (::_Concentration)(c) + c = @tographdata c {Scalar, Matrix, Adjacency}{Float64} + if c isa Real + Concentration.Flat(c) + elseif c isa AbstractMatrix + Concentration.Raw(c) else - "ranging from $min to $max." + Concentration.Adjacency(c) end end + +@expose_data edges begin + property(nutrients.concentration) + depends(Concentration) + @nutrients_index + ref(raw -> raw._scratch[:nutrients_concentration]) + get(Concentations{Float64}, "nutrient") + write!((raw, rhs::Real, i, j) -> Concentration_.check(rhs, (i, j))) +end + +function F.shortline(io::IO, model::Model, ::_Concentration) + print(io, "Nutrients concentration: ") + showrange(io, model.nutrients._concentration) +end diff --git a/src/components/nutrients/main.jl b/src/components/nutrients/main.jl index 19cccf0c8..dcabd2b8f 100644 --- a/src/components/nutrients/main.jl +++ b/src/components/nutrients/main.jl @@ -14,6 +14,7 @@ import .EN: Model, argerr, join_elided, + showrange, @component, @expose_data, @get, @@ -43,7 +44,7 @@ include("./nodes.jl") # Further node/edges components regarding this compartment. include("./turnover.jl") include("./supply.jl") -# include("./concentration.jl") +include("./concentration.jl") # include("./half_saturation.jl") end diff --git a/test/graph_data_inputs/check.jl b/test/graph_data_inputs/check.jl index ac0d7a4c8..9e98a101a 100644 --- a/test/graph_data_inputs/check.jl +++ b/test/graph_data_inputs/check.jl @@ -373,12 +373,6 @@ for source [3].") ) - # Cannot check densely against a 2D template. - @argfails( - (@check_list_refs v :item template(full) dense), - "Dense adjacency lists checking is unimplemented yet." - ) - # Empty space. @failswith( (@check_list_refs v :item 0), @@ -386,6 +380,13 @@ the reference space for 'item' is empty.") ) + # Missing dense ref. + v = gc((@GraphData A{Float64}), [1 => [5 => 100]]) + @failswith( + (@check_list_refs v :item template(full) dense), + CheckError("Missing 'item' edge index in 'v': no value specified for [3, 1].") + ) + # With labels. for (v, (v1,)) in [ (gc((@GraphData A{Float64}), [:a => [:e => 100], :c => [:a => 200]]), (100.0,)), @@ -419,12 +420,6 @@ for source [:c].") ) - # Cannot check densely against a 2D template. - @argfails( - (@check_list_refs v :item full_index template(full) dense), - "Dense adjacency lists checking is unimplemented yet." - ) - # Empty space. @failswith( (@check_list_refs v :item Dict{Symbol,Int64}()), @@ -432,6 +427,13 @@ the reference space for 'item' is empty.") ) + # Missing dense refs. + v = gc((@GraphData A{Float64}), [:a => [:e => 100]]) + @failswith( + (@check_list_refs v :item full_index template(full) dense), + CheckError("Missing 'item' edge label in 'v': no value specified for [:c, :a].") + ) + # Indices space automatically inferred from labels index. v = gc((@GraphData A{Float64}), [3 => [5 => 100]]) @test @check_list_refs v :item full_index diff --git a/test/graph_data_inputs/expand.jl b/test/graph_data_inputs/expand.jl index 2a0c5f97b..15b746380 100644 --- a/test/graph_data_inputs/expand.jl +++ b/test/graph_data_inputs/expand.jl @@ -187,6 +187,29 @@ #--------------------------------------------------------------------------------------- # Matrices from adjacency lists. + # To dense data (watch not to miss data). + small, large = + (Dict(Symbol(c) => i for (i, c) in enumerate(letters)) for letters in ("ab", "ABC")) + + input = gc( + (@GraphData A{Float64}), + [1 => [3 => 5, 1 => 8, 2 => 4], 2 => [2 => 9, 3 => 7, 1 => 3]], + ) + @test to_dense_matrix(input) == [ + 8 4 5 + 3 9 7 + ] + + input = gc( + (@GraphData A{Float64}), + [:a => [:C => 5, :A => 8, :B => 4], :b => [:B => 9, :C => 7, :A => 3]], + ) + @test to_dense_matrix(input, small, large) == [ + 8 4 5 + 3 9 7 + ] + + # To sparse data. small, large = ( Dict(Symbol(c) => i for (i, c) in enumerate(letters)) for letters in ("abc", "ABCDE") @@ -334,4 +357,22 @@ @to_sparse_matrix_if_adjacency input small large @test input == "not a map" + # Dense matrix. + small, large = + (Dict(Symbol(c) => i for (i, c) in enumerate(letters)) for letters in ("ab", "ABC")) + + input = gc( + (@GraphData A{Float64}), + [1 => [1 => 8, 2 => 5, 3 => 4], 2 => [2 => 7, 1 => 9, 3 => 2]], + ) + @to_dense_matrix_if_adjacency input small large + @test input == [ + 8 5 4 + 9 7 2 + ] + + input = "not a map" + @to_dense_matrix_if_adjacency input small large + @test input == "not a map" + end diff --git a/test/runtests.jl b/test/runtests.jl index c96910164..aecd0309f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,7 +20,7 @@ sep("Test API utils.") # include("./topologies.jl") # include("./aliasing_dicts.jl") # include("./multiplex_api.jl") -# include("./graph_data_inputs/runtests.jl") +include("./graph_data_inputs/runtests.jl") sep("Test user-facing behaviour.") include("./user/runtests.jl") diff --git a/test/user/03-components.jl b/test/user/03-components.jl index 1c205d41a..a771840ef 100644 --- a/test/user/03-components.jl +++ b/test/user/03-components.jl @@ -20,26 +20,27 @@ import .EN: WriteError only = [ "./data_components/species.jl" "./data_components/foodweb.jl" - # "./data_components/body_mass.jl" - # "./data_components/metabolic_class.jl" - # "./data_components/temperature.jl" - # "./data_components/hill_exponent.jl" - # "./data_components/growth_rate.jl" - # "./data_components/efficiency.jl" - # "./data_components/carrying_capacity.jl" - # "./data_components/mortality.jl" - # "./data_components/metabolism.jl" - # "./data_components/maximum_consumption.jl" - # "./data_components/producers_competition.jl" - # "./data_components/consumers_preferences.jl" - # "./data_components/handling_time.jl" - # "./data_components/attack_rate.jl" - # "./data_components/half_saturation_density.jl" - # "./data_components/intraspecific_interference.jl" - # "./data_components/consumption_rate.jl" + "./data_components/body_mass.jl" + "./data_components/metabolic_class.jl" + "./data_components/temperature.jl" + "./data_components/hill_exponent.jl" + "./data_components/growth_rate.jl" + "./data_components/efficiency.jl" + "./data_components/carrying_capacity.jl" + "./data_components/mortality.jl" + "./data_components/metabolism.jl" + "./data_components/maximum_consumption.jl" + "./data_components/producers_competition.jl" + "./data_components/consumers_preferences.jl" + "./data_components/handling_time.jl" + "./data_components/attack_rate.jl" + "./data_components/half_saturation_density.jl" + "./data_components/intraspecific_interference.jl" + "./data_components/consumption_rate.jl" "./data_components/nutrients/nodes.jl" "./data_components/nutrients/turnover.jl" "./data_components/nutrients/supply.jl" + "./data_components/nutrients/concentration.jl" ] # Only run these if specified. if isempty(only) for subfolder in ["./data_components", "./code_components"] diff --git a/test/user/data_components/nutrients/concentration.jl b/test/user/data_components/nutrients/concentration.jl index c48b8ca82..1c8eddbab 100644 --- a/test/user/data_components/nutrients/concentration.jl +++ b/test/user/data_components/nutrients/concentration.jl @@ -1,49 +1,139 @@ -FR = EN.Nutrients.ConcentrationFromRawValues - @testset "Nutrients concentration component." begin - # Mostly adapted from efficiency. + # Mostly duplicated from Efficiency. fw = Foodweb([:a => [:b, :c]]) base = Model(fw, Nutrients.Nodes(3)) - #--------------------------------------------------------------------------------------- - # Construct from raw values. - cn = Nutrients.Concentration([ 1 2 3 4 5 6 ]) m = base + cn - @test m.nutrients_concentration == [ + @test m.nutrients.concentration == [ + 1 2 3 + 4 5 6 + ] + @test typeof(cn) === Nutrients.Concentration.Raw + + # Adjacency list. + cn = Nutrients.Concentration([ + :b => [:n1 => 1, :n2 => 2, :n3 => 3], + :c => [:n2 => 5, :n3 => 6, :n1 => 4], + ]) + m = base + cn + @test m.nutrients.concentration == [ 1 2 3 4 5 6 ] - @test typeof(cn) === FR + @test typeof(cn) === Nutrients.Concentration.Adjacency + + # Scalar. + cn = Nutrients.Concentration(2) + m = base + cn + @test m.nutrients.concentration == [ + 2 2 2 + 2 2 2 + ] + @test typeof(cn) === Nutrients.Concentration.Flat + + #--------------------------------------------------------------------------------------- + # Imply Nutrients. + + c = [ + 1 2 3 + 4 5 6 + ] + m = Model(fw, Nutrients.Concentration(c)) + @test has_component(m, Nutrients.Nodes) + @test m.nutrients.concentration == c + @test m.nutrients.names == [:n1, :n2, :n3] + @test Model( + fw, + Nutrients.Concentration([ + :b => [:x => 1, :y => 2, :z => 3], + :c => [:z => 5, :x => 6, :y => 4], + ]), + ).nutrients.names == [:x, :y, :z] + + # ====================================================================================== + # Input guards. + + # Invalid values. + @sysfails( + base + Nutrients.Concentration([ + 0 1 -2 + 3 0 4 + ]), + Check( + early, + [Nutrients.Concentration.Raw], + "Not a positive value: c[1, 3] = -2.0.", + ) + ) - # Only valid dimensions allowed. + @sysfails( + base + Nutrients.Concentration([ + :b => [:n1 => 1, :n2 => 2, :n3 => 3], + :c => [:n2 => 5, :n3 => -6, :n1 => 4], + ]), + Check( + early, + [Nutrients.Concentration.Adjacency], + "Not a positive value: c[:c, :n3] = -6.0.", + ) + ) + + @sysfails( + base + Nutrients.Concentration(-5), + Check(early, [Nutrients.Concentration.Flat], "Not a positive value: c = -5.0.") + ) + + # Invalid size. @sysfails( base + Nutrients.Concentration([ 0 1 3 0 ]), - Check(FR), - "Invalid size for parameter 'c': expected (2, 3), got (2, 2).", + Check( + late, + [Nutrients.Concentration.Raw], + "Invalid size for parameter 'c': expected (2, 3), got (2, 2).", + ) ) - # Implies nutrients component. - base = Model(fw) - m = base + Nutrients.Concentration([ - 1 2 3 - 4 5 6 - ]) - @test m.nutrients_names == [:n1, :n2, :n3] + # Non-dense input. + @sysfails( + Model( + fw, + Nutrients.Concentration([ + :b => [:x => 1, :y => 2], + :c => [:z => 5, :x => 6, :y => 4], + ]), + ), + Check( + late, + [Nutrients.Concentration.Adjacency], + "Missing 'producer trophic' edge label in 'c': \ + no value specified for [:b, :z].", + ) + ) - # Unless we can't infer it. + # Respect template. @sysfails( - base + Nutrients.Concentration(5), - Check(FR), - "missing a required component '$(Nutrients.Nodes)': implied." + Model( + fw, + Nutrients.Concentration([ + :b => [:x => 1, :y => 2, :z => 3], + :a => [:z => 5, :x => 6, :y => 4], + ]), + ), + Check( + late, + [Nutrients.Concentration.Adjacency], + "Invalid 'producer trophic' edge label in 'c'. \ + Expected either :b or :c, got instead: [:a] (5.0).", + ) ) end