From f5591a08a3b2a1b29ac76963c1bd43b57ca6705c Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Mon, 18 Nov 2024 14:53:13 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Bugs=20with=20int-indexed=20map?= =?UTF-8?q?=20/=20adjacency=20inputs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/GraphDataInputs/check.jl | 22 +++++++- src/GraphDataInputs/expand.jl | 10 ++++ src/components/body_mass.jl | 2 +- src/components/carrying_capacity.jl | 4 +- src/components/consumption_rate.jl | 4 +- src/components/efficiency.jl | 2 +- src/components/foodweb.jl | 26 ++++------ src/components/growth_rate.jl | 4 +- src/components/half_saturation_density.jl | 4 +- src/components/intraspecific_interference.jl | 4 +- src/components/main.jl | 50 +++++++++---------- src/components/maximum_consumption.jl | 4 +- src/components/metabolic_class.jl | 2 +- src/components/metabolism.jl | 2 +- src/components/mortality.jl | 2 +- src/components/nontrophic_layers/main.jl | 2 +- .../nontrophic_layers/nti_modules.jl | 2 +- src/components/nutrients/concentration.jl | 11 +--- src/components/nutrients/half_saturation.jl | 11 +--- src/components/nutrients/nodes.jl | 6 +++ src/components/nutrients/supply.jl | 2 +- src/components/nutrients/turnover.jl | 2 +- src/components/species.jl | 7 +++ test/graph_data_inputs/check.jl | 12 +++++ test/graph_data_inputs/expand.jl | 3 ++ test/runtests.jl | 2 +- test/user/03-components.jl | 50 +++++++++---------- test/user/data_components/attack_rate.jl | 25 +++++++--- test/user/data_components/body_mass.jl | 8 +++ .../user/data_components/carrying_capacity.jl | 10 ++-- .../data_components/consumers_preferences.jl | 26 +++++++--- test/user/data_components/consumption_rate.jl | 10 ++-- test/user/data_components/efficiency.jl | 24 ++++++--- test/user/data_components/foodweb.jl | 8 +++ test/user/data_components/growth_rate.jl | 10 ++-- .../half_saturation_density.jl | 10 ++-- test/user/data_components/handling_time.jl | 25 +++++++--- .../intraspecific_interference.jl | 12 +++-- .../data_components/maximum_consumption.jl | 10 ++-- test/user/data_components/metabolic_class.jl | 7 +++ test/user/data_components/metabolism.jl | 23 +++++++-- test/user/data_components/mortality.jl | 22 ++++++-- .../nutrients/concentration.jl | 14 +++++- .../nutrients/half_saturation.jl | 15 +++++- test/user/data_components/nutrients/nodes.jl | 10 ++++ test/user/data_components/nutrients/supply.jl | 8 +++ .../data_components/nutrients/turnover.jl | 8 +++ test/user/data_components/species.jl | 21 ++++++++ 48 files changed, 373 insertions(+), 185 deletions(-) diff --git a/src/GraphDataInputs/check.jl b/src/GraphDataInputs/check.jl index 8e1ecc69f..615a6ad55 100644 --- a/src/GraphDataInputs/check.jl +++ b/src/GraphDataInputs/check.jl @@ -184,6 +184,24 @@ export @check_template # Int64, (Int64, Int64), Index or (Index, Index). # So the space is *also* an index to convert labels to integers. +#------------------------------------------------------------------------------------------- +# Check that the given value is a valid (dense) {ref -> indice} index. +function check_index(index::AbstractDict{Symbol,Int}) + n = length(index) + notfound = OrderedSet(1:n) + for (ref, i) in index + 1 <= i <= n || argerr("Invalid index: received $n reference$(n > 1 ? "s" : "") \ + but one of them is [$i] ($(repr(ref))).") + if i in notfound + pop!(notfound, i) + end + end + for i in notfound + argerr("Invalid index: no reference given for index [$i].") + end +end +export check_index + #------------------------------------------------------------------------------------------- # Check references against a general reference space. @@ -218,8 +236,8 @@ function check_list_refs( end if !(isnothing(template) || isnothing(space)) a, b = size(template), space - (a == b || a == (b,)) || - argerr("Inconsistent template size ($a) vs. references space ($b).") + (a == b || a == (b,) || a == (b, b)) || + argerr("Inconsistent template size vs. references space: $a vs. $b.") end end diff --git a/src/GraphDataInputs/expand.jl b/src/GraphDataInputs/expand.jl index 341fd9544..017714487 100644 --- a/src/GraphDataInputs/expand.jl +++ b/src/GraphDataInputs/expand.jl @@ -159,6 +159,16 @@ end export to_dense_vector +# Assuming input is a dense index. +function to_dense_refs(index::AbstractDict{Symbol,Int}) + refs = Vector{Symbol}(undef, length(index)) + for (ref, i) in index + refs[i] = ref + end + refs +end +export to_dense_refs + # Assuming all indices are valid. function to_sparse_vector(map::AbstractMap{Int64,T}, n::Int64) where {T} res = spzeros(T, n) diff --git a/src/components/body_mass.jl b/src/components/body_mass.jl index 4d7776f4e..14a12b14f 100644 --- a/src/components/body_mass.jl +++ b/src/components/body_mass.jl @@ -55,7 +55,7 @@ mutable struct Map <: Blueprint species::Brought(Species) Map(M, sp = _Species) = new(@tographdata(M, Map{Float64}), sp) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.M)) +F.implied_blueprint_for(bp::Map, ::_Species) = Species(refspace(bp.M)) @blueprint Map "[species => mass] map" export Map diff --git a/src/components/carrying_capacity.jl b/src/components/carrying_capacity.jl index 3fd085d06..ff0c98ad0 100644 --- a/src/components/carrying_capacity.jl +++ b/src/components/carrying_capacity.jl @@ -59,10 +59,8 @@ F.expand!(raw, bp::Flat) = expand!(raw, to_template(bp.K, @ref raw.producers.mas #------------------------------------------------------------------------------------------- mutable struct Map <: Blueprint K::@GraphData Map{Float64} - species::Brought(Species) - Map(K, sp = _Species) = new(@tographdata(K, Map{Float64}), sp) + Map(K) = new(@tographdata(K, Map{Float64})) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.K)) @blueprint Map "[species => carrying capacity] map" export Map diff --git a/src/components/consumption_rate.jl b/src/components/consumption_rate.jl index e48e0600d..8235daaae 100644 --- a/src/components/consumption_rate.jl +++ b/src/components/consumption_rate.jl @@ -53,10 +53,8 @@ F.expand!(raw, bp::Flat) = expand!(raw, to_template(bp.alpha, @ref raw.consumers #------------------------------------------------------------------------------------------- mutable struct Map <: Blueprint alpha::@GraphData Map{Float64} - species::Brought(Species) - Map(alpha, sp = _Species) = new(@tographdata(alpha, Map{Float64}), sp) + Map(alpha) = new(@tographdata(alpha, Map{Float64})) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.alpha)) @blueprint Map "[species => consumption rate] map" export Map diff --git a/src/components/efficiency.jl b/src/components/efficiency.jl index c3886ab47..5f1e00702 100644 --- a/src/components/efficiency.jl +++ b/src/components/efficiency.jl @@ -161,5 +161,5 @@ end # Just display range. function F.shortline(io::IO, model::Model, ::_Efficiency) print(io, "Efficiency: ") - showrange(model._e) + showrange(io, model._e) end diff --git a/src/components/foodweb.jl b/src/components/foodweb.jl index 116e1d2a8..53c16c76f 100644 --- a/src/components/foodweb.jl +++ b/src/components/foodweb.jl @@ -78,18 +78,7 @@ mutable struct Adjacency <: Blueprint end # Infer number or names of species from the lists. -function F.implied_blueprint_for(bp::Adjacency, ::_Species) - (; A) = bp - if A isa BinAdjacency{Int64} - S = refspace(A) - Species(S) - elseif A isa BinAdjacency{Symbol} - names = refs(A) - Species(names) - else - throw("unreachable: invalid adjacency list type") - end -end +F.implied_blueprint_for(bp::Adjacency, ::_Species) = Species(refspace(bp.A)) @blueprint Adjacency "adjacency list of trophic links" export Adjacency @@ -330,11 +319,14 @@ function F.shortline(io::IO, model::Model, ::_Foodweb) t = model.tops.number n(n) = n > 0 ? "$n" : "no" s(n) = n > 1 ? "s" : "" - print(io, "Foodweb: $(n(l)) link$(s(l)), \ - $(n(p)) producer$(s(p)), \ - $(n(c)) consumer$(s(c)), \ - $(n(r)) prey$(s(r)), \ - $(n(t)) top$(s(t)).") + print( + io, + "Foodweb: $(n(l)) link$(s(l)), \ + $(n(p)) producer$(s(p)), \ + $(n(c)) consumer$(s(c)), \ + $(n(r)) prey$(s(r)), \ + $(n(t)) top$(s(t)).", + ) end # ========================================================================================== diff --git a/src/components/growth_rate.jl b/src/components/growth_rate.jl index 2c5ad2847..51e861392 100644 --- a/src/components/growth_rate.jl +++ b/src/components/growth_rate.jl @@ -63,10 +63,8 @@ F.expand!(raw, bp::Flat) = expand!(raw, to_template(bp.r, @ref raw.producers.mas mutable struct Map <: Blueprint r::@GraphData Map{Float64} - species::Brought(Species) - Map(r, sp = _Species) = new(@tographdata(r, Map{Float64}), sp) + Map(r) = new(@tographdata(r, Map{Float64})) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.r)) @blueprint Map "[species => growth rate] map" export Map diff --git a/src/components/half_saturation_density.jl b/src/components/half_saturation_density.jl index 55b8c9a4e..7870db555 100644 --- a/src/components/half_saturation_density.jl +++ b/src/components/half_saturation_density.jl @@ -53,10 +53,8 @@ F.expand!(raw, bp::Flat) = expand!(raw, to_template(bp.B0, @ref raw.consumers.ma #------------------------------------------------------------------------------------------- mutable struct Map <: Blueprint B0::@GraphData Map{Float64} - species::Brought(Species) - Map(B0, sp = _Species) = new(@tographdata(B0, Map{Float64}), sp) + Map(B0) = new(@tographdata(B0, Map{Float64})) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.B0)) @blueprint Map "[species => half-saturation density] map" export Map diff --git a/src/components/intraspecific_interference.jl b/src/components/intraspecific_interference.jl index f103c75df..2c872deee 100644 --- a/src/components/intraspecific_interference.jl +++ b/src/components/intraspecific_interference.jl @@ -53,10 +53,8 @@ F.expand!(raw, bp::Flat) = expand!(raw, to_template(bp.c, @ref raw.consumers.mas #------------------------------------------------------------------------------------------- mutable struct Map <: Blueprint c::@GraphData Map{Float64} - species::Brought(Species) - Map(c, sp = _Species) = new(@tographdata(c, Map{Float64}), sp) + Map(c) = new(@tographdata(c, Map{Float64})) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.c)) @blueprint Map "[species => intra-specific interference] map" export Map diff --git a/src/components/main.jl b/src/components/main.jl index 59505a1c2..58ef4079c 100644 --- a/src/components/main.jl +++ b/src/components/main.jl @@ -81,34 +81,34 @@ include("./foodweb.jl") # # Biorates and other values parametrizing the ODE. # # (typical example 'nodes' data) -# include("./body_mass.jl") -# include("./metabolic_class.jl") +include("./body_mass.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") -# export Nutrients +# Namespace nutrients data. +include("./nutrients/main.jl") +export Nutrients include("./nontrophic_layers/main.jl") # using .NontrophicInteractions diff --git a/src/components/maximum_consumption.jl b/src/components/maximum_consumption.jl index 2e96251ce..54189bcb6 100644 --- a/src/components/maximum_consumption.jl +++ b/src/components/maximum_consumption.jl @@ -53,10 +53,8 @@ F.expand!(raw, bp::Flat) = expand!(raw, to_template(bp.y, @ref raw.consumers.mas #------------------------------------------------------------------------------------------- mutable struct Map <: Blueprint y::@GraphData Map{Float64} - species::Brought(Species) - Map(y, sp = _Species) = new(@tographdata(y, Map{Float64}), sp) + Map(y) = new(@tographdata(y, Map{Float64})) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.y)) @blueprint Map "[species => maximum consumption] map" export Map diff --git a/src/components/metabolic_class.jl b/src/components/metabolic_class.jl index 26fecb4ba..4c9627884 100644 --- a/src/components/metabolic_class.jl +++ b/src/components/metabolic_class.jl @@ -88,7 +88,7 @@ mutable struct Map <: Blueprint species::Brought(Species) Map(M, sp = _Species) = new(@tographdata(M, Map{Symbol}), sp) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.classes)) +F.implied_blueprint_for(bp::Map, ::_Species) = Species(refspace(bp.classes)) @blueprint Map "[species => class] map" depends(Foodweb) export Map diff --git a/src/components/metabolism.jl b/src/components/metabolism.jl index eb267ceaa..2d4027411 100644 --- a/src/components/metabolism.jl +++ b/src/components/metabolism.jl @@ -52,7 +52,7 @@ mutable struct Map <: Blueprint species::Brought(Species) Map(x, sp = _Species) = new(@tographdata(x, Map{Float64}), sp) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.x)) +F.implied_blueprint_for(bp::Map, ::_Species) = Species(refspace(bp.x)) @blueprint Map "[species => metabolism] map" export Map diff --git a/src/components/mortality.jl b/src/components/mortality.jl index 7cd2ac3a0..9d07ef724 100644 --- a/src/components/mortality.jl +++ b/src/components/mortality.jl @@ -52,7 +52,7 @@ mutable struct Map <: Blueprint species::Brought(Species) Map(d, sp = _Species) = new(@tographdata(d, Map{Float64}), sp) end -F.implied_blueprint_for(bp::Map, ::_Species) = Species(refs(bp.d)) +F.implied_blueprint_for(bp::Map, ::_Species) = Species(refspace(bp.d)) @blueprint Map "[species => mortality] map" export Map diff --git a/src/components/nontrophic_layers/main.jl b/src/components/nontrophic_layers/main.jl index 9319e3b8e..e4a1ce834 100644 --- a/src/components/nontrophic_layers/main.jl +++ b/src/components/nontrophic_layers/main.jl @@ -59,7 +59,7 @@ multiplex_defaults = MultiplexParametersDict(; ), ) -include("./competition.jl") +# include("./competition.jl") # include("./facilitation.jl") # include("./interference.jl") # include("./refuge.jl") diff --git a/src/components/nontrophic_layers/nti_modules.jl b/src/components/nontrophic_layers/nti_modules.jl index 68afe8c9a..041693370 100644 --- a/src/components/nontrophic_layers/nti_modules.jl +++ b/src/components/nontrophic_layers/nti_modules.jl @@ -40,6 +40,6 @@ if (false) using .KwargsHelpers using .MultiplexApi: MultiplexParametersDict, InteractionDict, interactions_names using .AliasingDicts: expand - local (competition,) + local ( competition, ) end diff --git a/src/components/nutrients/concentration.jl b/src/components/nutrients/concentration.jl index b0252218f..fc42d8bd9 100644 --- a/src/components/nutrients/concentration.jl +++ b/src/components/nutrients/concentration.jl @@ -62,15 +62,8 @@ mutable struct Adjacency <: Blueprint 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 +F.implied_blueprint_for(bp::Adjacency, ::Nutrients._Nodes) = + Nutrients.Nodes(refspace_inner(bp.c)) @blueprint Adjacency "[producer => [nutrient => concentration]] map" export Adjacency diff --git a/src/components/nutrients/half_saturation.jl b/src/components/nutrients/half_saturation.jl index b39fcfb13..fd5ab2d62 100644 --- a/src/components/nutrients/half_saturation.jl +++ b/src/components/nutrients/half_saturation.jl @@ -54,15 +54,8 @@ mutable struct Adjacency <: Blueprint nutrients::Brought(Nutrients.Nodes) Adjacency(h, nt = Nutrients._Nodes) = new(@tographdata(h, 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.h) - if space isa Integer - Nutrients.Nodes(space) - else - Nutrients.Nodes(keys(space)) - end -end +F.implied_blueprint_for(bp::Adjacency, ::Nutrients._Nodes) = + Nutrients.Nodes(refspace_inner(bp.h)) @blueprint Adjacency "[producer => [nutrient => half-saturation]] map" export Adjacency diff --git a/src/components/nutrients/nodes.jl b/src/components/nutrients/nodes.jl index 536ea518f..bde60eca9 100644 --- a/src/components/nutrients/nodes.jl +++ b/src/components/nutrients/nodes.jl @@ -19,6 +19,12 @@ mutable struct Names <: Blueprint Names(names) = new(Symbol.(names)) Names(names...) = new(Symbol.(collect(names))) + # From an index (useful when implied). + function Names(index::AbstractDict{Symbol,Int}) + check_index(index) + new(to_dense_refs(index)) + end + # Don't own data if useful to user. Names(names::Vector{Symbol}) = new(names) end diff --git a/src/components/nutrients/supply.jl b/src/components/nutrients/supply.jl index de139df58..71d0108e0 100644 --- a/src/components/nutrients/supply.jl +++ b/src/components/nutrients/supply.jl @@ -49,7 +49,7 @@ mutable struct Map <: Blueprint nutrients::Brought(Nutrients.Nodes) Map(s, nt = Nutrients._Nodes) = new(@tographdata(s, Map{Float64}), nt) end -F.implied_blueprint_for(bp::Map, ::Nutrients._Nodes) = Nutrients.Nodes(refs(bp.s)) +F.implied_blueprint_for(bp::Map, ::Nutrients._Nodes) = Nutrients.Nodes(refspace(bp.s)) @blueprint Map "[nutrient => supply] map" export Map diff --git a/src/components/nutrients/turnover.jl b/src/components/nutrients/turnover.jl index 88b48a5ee..a7d9b7da9 100644 --- a/src/components/nutrients/turnover.jl +++ b/src/components/nutrients/turnover.jl @@ -49,7 +49,7 @@ mutable struct Map <: Blueprint nutrients::Brought(Nutrients.Nodes) Map(t, nt = Nutrients._Nodes) = new(@tographdata(t, Map{Float64}), nt) end -F.implied_blueprint_for(bp::Map, ::Nutrients._Nodes) = Nutrients.Nodes(refs(bp.t)) +F.implied_blueprint_for(bp::Map, ::Nutrients._Nodes) = Nutrients.Nodes(refspace(bp.t)) @blueprint Map "[nutrient => turnover] map" export Map diff --git a/src/components/species.jl b/src/components/species.jl index c28bb60d7..b8b40436d 100644 --- a/src/components/species.jl +++ b/src/components/species.jl @@ -12,6 +12,7 @@ # Name + '_'-suffix is the module defining blueprints for this component. module Species_ include("blueprint_modules.jl") +include("blueprint_modules_identifiers.jl") #------------------------------------------------------------------------------------------- # Construct from a given set of names. @@ -23,6 +24,12 @@ mutable struct Names <: Blueprint Names(names) = new(Symbol.(names)) Names(names...) = new(Symbol.(collect(names))) + # From an index (useful when implied). + function Names(index::AbstractDict{Symbol,Int}) + check_index(index) + new(to_dense_refs(index)) + end + # Don't own data if useful to user. Names(names::Vector{Symbol}) = new(names) end diff --git a/test/graph_data_inputs/check.jl b/test/graph_data_inputs/check.jl index 9e98a101a..75ae7313e 100644 --- a/test/graph_data_inputs/check.jl +++ b/test/graph_data_inputs/check.jl @@ -323,6 +323,18 @@ v = gc((@GraphData K{Float64}), [1 => 5, 2 => 8]) @test @check_list_refs v :item part_index + # Check index. + check_index(Dict([:a => 1, :b => 2, :c => 3])) + @argfails( + check_index(Dict([:a => 1, :c => 3])), + "Invalid index: received 2 references but one of them is [3] (:c)." + ) + @argfails( + check_index(Dict([:a => 1, :c => 1])), + "Invalid index: no reference given for index [2]." + ) + + #--------------------------------------------------------------------------------------- # Adjacency lists. diff --git a/test/graph_data_inputs/expand.jl b/test/graph_data_inputs/expand.jl index 15b746380..b6a08fa00 100644 --- a/test/graph_data_inputs/expand.jl +++ b/test/graph_data_inputs/expand.jl @@ -184,6 +184,9 @@ input = gc((@GraphData K{Float64}), [:c => 8, :a => 7, :b => 9]) @test to_dense_vector(input, index) == [7.0, 9.0, 8.0] + input = Dict(:c => 3, :a => 1, :b => 2) + @test to_dense_refs(input) == [:a, :b, :c] + #--------------------------------------------------------------------------------------- # Matrices from adjacency lists. 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 2773d0b2a..cffb921c8 100644 --- a/test/user/03-components.jl +++ b/test/user/03-components.jl @@ -18,31 +18,31 @@ import .EN: WriteError # Many small similar components tests files, although they easily diverge. 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/nutrients/nodes.jl" - # "./data_components/nutrients/turnover.jl" - # "./data_components/nutrients/supply.jl" - # "./data_components/nutrients/concentration.jl" - # "./data_components/nutrients/half_saturation.jl" - "./code_components/linear_response.jl" + "./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/nutrients/nodes.jl" + "./data_components/nutrients/turnover.jl" + "./data_components/nutrients/supply.jl" + "./data_components/nutrients/concentration.jl" + "./data_components/nutrients/half_saturation.jl" + # "./code_components/linear_response.jl" ] # Only run these if specified. if isempty(only) for subfolder in ["./data_components", "./code_components"] diff --git a/test/user/data_components/attack_rate.jl b/test/user/data_components/attack_rate.jl index 5bd2708b9..dbc4b90b6 100644 --- a/test/user/data_components/attack_rate.jl +++ b/test/user/data_components/attack_rate.jl @@ -22,14 +22,21 @@ @test typeof(ar) === AttackRate.Raw # Adjacency list. - ar = AttackRate([:a => [:b => 1], :b => [:c => 3]]) - m = base + ar - @test m.attack_rate == [ - 0 1 0 - 0 0 3 - 0 0 0 - ] - @test typeof(ar) == AttackRate.Adjacency + for adj in ( + #! format: off + [:a => [:b => 1], :b => [:c => 3]], + [1 => [2 => 1], 2 => [3 => 3]], + #! format: on + ) + ar = AttackRate(adj) + m = base + ar + @test m.attack_rate == [ + 0 1 0 + 0 0 3 + 0 0 0 + ] + @test typeof(ar) == AttackRate.Adjacency + end # Scalar. ar = AttackRate(2) @@ -99,6 +106,8 @@ # Imply species names via foodweb implication. @test Model(AttackRate([:a => [:a => 1, :b => 2], :b => [:c => 3]])).species.names == [:a, :b, :c] + @test Model(AttackRate([2 => [3 => 0.3], 1 => [1 => 0.1, 2 => 0.2]])).species.names == + [:s1, :s2, :s3] # ====================================================================================== # Input guards. diff --git a/test/user/data_components/body_mass.jl b/test/user/data_components/body_mass.jl index 286aa59c1..38a075fee 100644 --- a/test/user/data_components/body_mass.jl +++ b/test/user/data_components/body_mass.jl @@ -15,6 +15,14 @@ @test typeof(bm) == BodyMass.Raw # Mapped input. + ## Integer keys. + bm = BodyMass([2 => 1, 3 => 2, 1 => 3]) + m = base + bm + @test m.richness == 3 + @test m.species.names == [:s1, :s2, :s3] + @test m.body_mass == [3, 1, 2] == m.M + @test typeof(bm) == BodyMass.Map + ## Symbol keys. bm = BodyMass([:a => 1, :b => 2, :c => 3]) m = base + bm @test m.richness == 3 diff --git a/test/user/data_components/carrying_capacity.jl b/test/user/data_components/carrying_capacity.jl index 90065725e..3d814b26d 100644 --- a/test/user/data_components/carrying_capacity.jl +++ b/test/user/data_components/carrying_capacity.jl @@ -8,10 +8,12 @@ # From raw values. # Map selected species. - cc = CarryingCapacity([:c => 3]) - m = base + cc - @test m.carrying_capacity == [0, 0, 3] == m.K - @test typeof(cc) == CarryingCapacity.Map + for map in ([:c => 3], [3 => 3]) + cc = CarryingCapacity(map) + m = base + cc + @test m.carrying_capacity == [0, 0, 3] == m.K + @test typeof(cc) == CarryingCapacity.Map + end # From a sparse vector. cc = CarryingCapacity([0, 0, 4]) diff --git a/test/user/data_components/consumers_preferences.jl b/test/user/data_components/consumers_preferences.jl index 9ca89e3ae..7ed6eab69 100644 --- a/test/user/data_components/consumers_preferences.jl +++ b/test/user/data_components/consumers_preferences.jl @@ -22,14 +22,21 @@ @test typeof(cp) === ConsumersPreferences.Raw # Adjacency list. - cp = ConsumersPreferences([:a => [:b => 1], :b => [:c => 3]]) - m = base + cp - @test m.consumers.preferences == m.w == [ - 0 1 0 - 0 0 3 - 0 0 0 - ] - @test typeof(cp) == ConsumersPreferences.Adjacency + for adj in ( + #! format: off + [:a => [:b => 1], :b => [:c => 3]], + [1 => [2 => 1], 2 => [3 => 3]], + #! format: on + ) + cp = ConsumersPreferences(adj) + m = base + cp + @test m.consumers.preferences == m.w == [ + 0 1 0 + 0 0 3 + 0 0 0 + ] + @test typeof(cp) == ConsumersPreferences.Adjacency + end # Scalar. cp = ConsumersPreferences(1) @@ -75,6 +82,9 @@ @test Model( ConsumersPreferences([:a => [:a => 0.1, :b => 0.2], :b => [:c => 0.3]]), ).species.names == [:a, :b, :c] + @test Model( + ConsumersPreferences([2 => [3 => 0.3], 1 => [1 => 0.1, 2 => 0.2]]), + ).species.names == [:s1, :s2, :s3] # ====================================================================================== # Input guards. diff --git a/test/user/data_components/consumption_rate.jl b/test/user/data_components/consumption_rate.jl index 9af9a6bf8..e82d104eb 100644 --- a/test/user/data_components/consumption_rate.jl +++ b/test/user/data_components/consumption_rate.jl @@ -8,10 +8,12 @@ # From raw values. # Map selected species. - cr = ConsumptionRate([:a => 1, :b => 2]) - m = base + cr - @test m.consumption_rate == [1, 2, 0] - @test typeof(cr) == ConsumptionRate.Map + for map in ([:a => 1, :b => 2], [1 => 1, 2 => 2]) + cr = ConsumptionRate(map) + m = base + cr + @test m.consumption_rate == [1, 2, 0] + @test typeof(cr) == ConsumptionRate.Map + end # From a sparse vector. cr = ConsumptionRate([2, 4, 0]) diff --git a/test/user/data_components/efficiency.jl b/test/user/data_components/efficiency.jl index 9fafac130..d8adadd50 100644 --- a/test/user/data_components/efficiency.jl +++ b/test/user/data_components/efficiency.jl @@ -20,14 +20,20 @@ @test typeof(ef) === Efficiency.Raw # Adjacency list. - ef = Efficiency([:a => [:b => 0.1], :b => [:c => 0.3]]) - m = base + ef - @test m.efficiency == m.e == [ - 0 1 0 - 0 0 3 - 0 0 0 - ] / 10 - @test typeof(ef) == Efficiency.Adjacency + for adj in + #! format: off + ([:a => [:b => 0.1], :b => [:c => 0.3]], + [1 => [2 => 0.1], 2 => [3 => 0.3]]) + #! format: on + ef = Efficiency(adj) + m = base + ef + @test m.efficiency == m.e == [ + 0 1 0 + 0 0 3 + 0 0 0 + ] / 10 + @test typeof(ef) == Efficiency.Adjacency + end # Scalar. ef = Efficiency(0.1) @@ -72,6 +78,8 @@ @test Model( Efficiency([:a => [:a => 0.1, :b => 0.2], :b => [:c => 0.3]]), ).species.names == [:a, :b, :c] + @test Model(Efficiency([2 => [3 => 0.3], 1 => [1 => 0.1, 2 => 0.2]])).species.names == + [:s1, :s2, :s3] # ====================================================================================== # Input guards. diff --git a/test/user/data_components/foodweb.jl b/test/user/data_components/foodweb.jl index bad08c0f5..e6535bc90 100644 --- a/test/user/data_components/foodweb.jl +++ b/test/user/data_components/foodweb.jl @@ -20,6 +20,14 @@ #--------------------------------------------------------------------------------------- # From an adjacency list. + # Integer keys. + fw = Foodweb([2 => 3, 1 => [3, 2]]) + m = base + fw + @test m.S == 3 + @test m.species.names == [:s1, :s2, :s3] + @test typeof(fw) == Foodweb.Adjacency + + # Symbol keys. fw = Foodweb([:a => [:b, :c], :b => :c]) m = base + fw @test m.S == 3 diff --git a/test/user/data_components/growth_rate.jl b/test/user/data_components/growth_rate.jl index 97003288d..905e7ff19 100644 --- a/test/user/data_components/growth_rate.jl +++ b/test/user/data_components/growth_rate.jl @@ -6,10 +6,12 @@ # From raw values. # Map selected species. - gr = GrowthRate([:c => 3]) - m = base + gr - @test m.growth_rate == [0, 0, 3] == m.r - @test typeof(gr) == GrowthRate.Map + for map in ([:c => 3], [3 => 3]) + gr = GrowthRate(map) + m = base + gr + @test m.growth_rate == [0, 0, 3] == m.r + @test typeof(gr) == GrowthRate.Map + end # From a sparse vector. gr = GrowthRate([0, 0, 4]) diff --git a/test/user/data_components/half_saturation_density.jl b/test/user/data_components/half_saturation_density.jl index de563baa5..555bfa32d 100644 --- a/test/user/data_components/half_saturation_density.jl +++ b/test/user/data_components/half_saturation_density.jl @@ -8,10 +8,12 @@ # From raw values. # Map selected species. - hd = HalfSaturationDensity([:a => 1, :b => 2]) - m = base + hd - @test m.half_saturation_density == [1, 2, 0] - @test typeof(hd) == HalfSaturationDensity.Map + for map in ([:a => 1, :b => 2], [1 => 1, 2 => 2]) + hd = HalfSaturationDensity(map) + m = base + hd + @test m.half_saturation_density == [1, 2, 0] + @test typeof(hd) == HalfSaturationDensity.Map + end # From a sparse vector. hd = HalfSaturationDensity([2, 4, 0]) diff --git a/test/user/data_components/handling_time.jl b/test/user/data_components/handling_time.jl index 38b4d4fb8..72086c622 100644 --- a/test/user/data_components/handling_time.jl +++ b/test/user/data_components/handling_time.jl @@ -22,14 +22,21 @@ @test typeof(ht) === HandlingTime.Raw # Adjacency list. - ht = HandlingTime([:a => [:b => 1], :b => [:c => 3]]) - m = base + ht - @test m.handling_time == [ - 0 1 0 - 0 0 3 - 0 0 0 - ] - @test typeof(ht) == HandlingTime.Adjacency + for adj in ( + #! format: off + [:a => [:b => 1], :b => [:c => 3]], + [1 => [2 => 1], 2 => [3 => 3]], + #! format: on + ) + ht = HandlingTime(adj) + m = base + ht + @test m.handling_time == [ + 0 1 0 + 0 0 3 + 0 0 0 + ] + @test typeof(ht) == HandlingTime.Adjacency + end # Scalar. ht = HandlingTime(2) @@ -99,6 +106,8 @@ # Imply species names via foodweb implication. @test Model(HandlingTime([:a => [:a => 1, :b => 2], :b => [:c => 3]])).species.names == [:a, :b, :c] + @test Model(HandlingTime([2 => [3 => 0.3], 1 => [1 => 0.1, 2 => 0.2]])).species.names == + [:s1, :s2, :s3] # ====================================================================================== # Input guards. diff --git a/test/user/data_components/intraspecific_interference.jl b/test/user/data_components/intraspecific_interference.jl index 4dc37e882..e34d4d8e4 100644 --- a/test/user/data_components/intraspecific_interference.jl +++ b/test/user/data_components/intraspecific_interference.jl @@ -1,6 +1,6 @@ @testset "Intra-specific interference component." begin - # Mostly duplicated from IntraspecificInterference. + # Mostly duplicated from HalfSaturationDensity. base = Model(Foodweb([:a => :b, :b => :c])) @@ -8,10 +8,12 @@ # From raw values. # Map selected species. - ii = IntraspecificInterference([:a => 1, :b => 2]) - m = base + ii - @test m.intraspecific_interference == [1, 2, 0] - @test typeof(ii) == IntraspecificInterference.Map + for map in ([:a => 1, :b => 2], [1 => 1, 2 => 2]) + ii = IntraspecificInterference(map) + m = base + ii + @test m.intraspecific_interference == [1, 2, 0] + @test typeof(ii) == IntraspecificInterference.Map + end # From a sparse vector. ii = IntraspecificInterference([2, 4, 0]) diff --git a/test/user/data_components/maximum_consumption.jl b/test/user/data_components/maximum_consumption.jl index f211e291c..2069fc302 100644 --- a/test/user/data_components/maximum_consumption.jl +++ b/test/user/data_components/maximum_consumption.jl @@ -8,10 +8,12 @@ # From raw values. # Map selected species. - mc = MaximumConsumption([:a => 1, :b => 2]) - m = base + mc - @test m.maximum_consumption == [1, 2, 0] == m.y - @test typeof(mc) == MaximumConsumption.Map + for map in ([:a => 1, :b => 2], [1 => 1, 2 => 2]) + mc = MaximumConsumption(map) + m = base + mc + @test m.maximum_consumption == [1, 2, 0] == m.y + @test typeof(mc) == MaximumConsumption.Map + end # From a sparse vector. mc = MaximumConsumption([2, 4, 0]) diff --git a/test/user/data_components/metabolic_class.jl b/test/user/data_components/metabolic_class.jl index e5039b68d..b831bfa0b 100644 --- a/test/user/data_components/metabolic_class.jl +++ b/test/user/data_components/metabolic_class.jl @@ -11,6 +11,13 @@ @test typeof(mc) == MetabolicClass.Raw # With an explicit map. + ## Integer keys. + mc = MetabolicClass([2 => :inv, 3 => :ect, 1 => :prod]) + m = base + mc + @test m.metabolic_class == [:producer, :invertebrate, :ectotherm] + @test typeof(mc) == MetabolicClass.Map + + ## Symbol keys. mc = MetabolicClass([:a => :inv, :b => :ect, :c => :prod]) m = base + mc @test m.metabolic_class == [:invertebrate, :ectotherm, :producer] diff --git a/test/user/data_components/metabolism.jl b/test/user/data_components/metabolism.jl index 17bc23525..9f9abe3ec 100644 --- a/test/user/data_components/metabolism.jl +++ b/test/user/data_components/metabolism.jl @@ -1,8 +1,8 @@ @testset "Metabolism component." begin - # Mostly duplicated from Metabolism. + # Mostly duplicated from Mortality. - base = Model(Foodweb([:a => [:b, :c], :b => :c])) + base = Model() #--------------------------------------------------------------------------------------- # From raw values. @@ -15,14 +15,23 @@ # From a single value. mb = Metabolism(2) - m = base + mb + m = base + Species(3) + mb @test m.metabolism == [2, 2, 2] == m.x @test typeof(mb) == Metabolism.Flat # Map selected species. - mb = Metabolism([:c => 3, :b => 2, :a => 1]) + ## Integer keys. + mb = Metabolism([2 => 3, 3 => 2, 1 => 1]) + m = base + mb + @test m.metabolism == [1, 3, 2] == m.x + @test m.species.names == [:s1, :s2, :s3] + @test typeof(mb) == Metabolism.Map + + ## Symbol keys. + mb = Metabolism([:a => 1, :b => 2, :c => 3]) m = base + mb @test m.metabolism == [1, 2, 3] == m.x + @test m.species.names == [:a, :b, :c] @test typeof(mb) == Metabolism.Map # Imply species component. @@ -32,7 +41,11 @@ #--------------------------------------------------------------------------------------- # From allometric rates. - base += BodyMass(1.5) + MetabolicClass(:all_invertebrates) + base = Model( + Foodweb([:a => [:b, :c], :b => :c]), + BodyMass(1.5), + MetabolicClass(:all_invertebrates), + ) mb = Metabolism(:Miele2019) @test mb.allometry[:i][:a] == 0.314 diff --git a/test/user/data_components/mortality.jl b/test/user/data_components/mortality.jl index 4fa46138d..7f0fbafb1 100644 --- a/test/user/data_components/mortality.jl +++ b/test/user/data_components/mortality.jl @@ -2,7 +2,7 @@ # Mostly duplicated from Growth. - base = Model(Foodweb([:a => [:b, :c], :b => :c])) + base = Model() #--------------------------------------------------------------------------------------- # From raw values. @@ -11,18 +11,28 @@ mr = Mortality([1, 2, 3]) m = base + mr @test m.mortality == [1, 2, 3] == m.d + @test m.species.names == [:s1, :s2, :s3] # Implied. @test typeof(mr) == Mortality.Raw # From a single value. mr = Mortality(2) - m = base + mr + m = base + Species(3) + mr @test m.mortality == [2, 2, 2] == m.d @test typeof(mr) == Mortality.Flat # Map selected species. - mr = Mortality([:c => 3, :b => 2, :a => 1]) + ## Integer keys. + mr = Mortality([2 => 3, 3 => 2, 1 => 1]) + m = base + mr + @test m.mortality == [1, 3, 2] == m.d + @test m.species.names == [:s1, :s2, :s3] + @test typeof(mr) == Mortality.Map + + ## Symbol keys. + mr = Mortality([:a => 1, :b => 2, :c => 3]) m = base + mr @test m.mortality == [1, 2, 3] == m.d + @test m.species.names == [:a, :b, :c] @test typeof(mr) == Mortality.Map # Imply species component. @@ -32,7 +42,11 @@ #--------------------------------------------------------------------------------------- # From allometric rates. - base += BodyMass(1.5) + MetabolicClass(:all_invertebrates) + base = Model( + Foodweb([:a => [:b, :c], :b => :c]), + BodyMass(1.5), + MetabolicClass(:all_invertebrates), + ) mr = Mortality(:Miele2019) @test mr.allometry[:p][:a] == 0.0138 diff --git a/test/user/data_components/nutrients/concentration.jl b/test/user/data_components/nutrients/concentration.jl index 1c8eddbab..0192304a5 100644 --- a/test/user/data_components/nutrients/concentration.jl +++ b/test/user/data_components/nutrients/concentration.jl @@ -48,13 +48,23 @@ @test has_component(m, Nutrients.Nodes) @test m.nutrients.concentration == c @test m.nutrients.names == [:n1, :n2, :n3] - @test Model( + ms = Model( fw, Nutrients.Concentration([ :b => [:x => 1, :y => 2, :z => 3], :c => [:z => 5, :x => 6, :y => 4], ]), - ).nutrients.names == [:x, :y, :z] + ) + mi = Model( + fw, + Nutrients.Concentration([ + 1 => [1 => 1, 2 => 2, 3 => 3], + 2 => [3 => 5, 1 => 6, 2 => 4], + ]), + ) + @test ms.nutrients.names == [:x, :y, :z] + @test mi.nutrients.names == [:n1, :n2, :n3] + @test ms.nutrients.concentration == mi.nutrients.concentration # ====================================================================================== # Input guards. diff --git a/test/user/data_components/nutrients/half_saturation.jl b/test/user/data_components/nutrients/half_saturation.jl index 70fec4596..6deccf86b 100644 --- a/test/user/data_components/nutrients/half_saturation.jl +++ b/test/user/data_components/nutrients/half_saturation.jl @@ -48,13 +48,24 @@ @test has_component(m, Nutrients.Nodes) @test m.nutrients.half_saturation == h @test m.nutrients.names == [:n1, :n2, :n3] - @test Model( + ms = Model( fw, Nutrients.HalfSaturation([ :b => [:x => 1, :y => 2, :z => 3], :c => [:z => 5, :x => 6, :y => 4], ]), - ).nutrients.names == [:x, :y, :z] + ) + mi = Model( + fw, + Nutrients.HalfSaturation([ + # Careful: this is *producer* dense index. + 1 => [1 => 1, 2 => 2, 3 => 3], + 2 => [3 => 5, 1 => 6, 2 => 4], + ]), + ) + @test ms.nutrients.names == [:x, :y, :z] + @test mi.nutrients.names == [:n1, :n2, :n3] + @test ms.nutrients.half_saturation == mi.nutrients.half_saturation # ====================================================================================== # Input guards. diff --git a/test/user/data_components/nutrients/nodes.jl b/test/user/data_components/nutrients/nodes.jl index cd3b068ef..e5323dc99 100644 --- a/test/user/data_components/nutrients/nodes.jl +++ b/test/user/data_components/nutrients/nodes.jl @@ -36,6 +36,16 @@ #--------------------------------------------------------------------------------------- # Guards. + @argfails( + Species(Dict([:a => 1, :c => 3])), + "Invalid index: received 2 references but one of them is [3] (:c)." + ) + + @argfails( + Species(Dict([:a => 1, :c => 1])), + "Invalid index: no reference given for index [2]." + ) + @sysfails( Model(Nutrients.Nodes([:a, :b, :a])), Check(early, [Nutrients.Nodes.Names], "Nutrients 1 and 3 are both named :a."), diff --git a/test/user/data_components/nutrients/supply.jl b/test/user/data_components/nutrients/supply.jl index 6b28fa047..9bd69555f 100644 --- a/test/user/data_components/nutrients/supply.jl +++ b/test/user/data_components/nutrients/supply.jl @@ -17,6 +17,14 @@ @test typeof(sp) == Nutrients.Supply.Raw # Mapped input. + ## Integer keys. + sp = Nutrients.Supply([2 => 1, 3 => 2, 1 => 3]) + m = base + sp + @test m.nutrients.richness == 3 + @test m.nutrients.names == [:n1, :n2, :n3] + @test m.nutrients.supply == [3, 1, 2] + @test typeof(sp) == Nutrients.Supply.Map + ## Symbol keys. sp = Nutrients.Supply([:a => 1, :b => 2, :c => 3]) m = base + sp @test m.nutrients.richness == 3 diff --git a/test/user/data_components/nutrients/turnover.jl b/test/user/data_components/nutrients/turnover.jl index 4cbfc2864..54c568295 100644 --- a/test/user/data_components/nutrients/turnover.jl +++ b/test/user/data_components/nutrients/turnover.jl @@ -17,6 +17,14 @@ @test typeof(tr) == Nutrients.Turnover.Raw # Mapped input. + ## Integer keys. + tr = Nutrients.Turnover([2 => 1, 3 => 2, 1 => 3]) + m = base + tr + @test m.nutrients.richness == 3 + @test m.nutrients.names == [:n1, :n2, :n3] + @test m.nutrients.turnover == [3, 1, 2] + @test typeof(tr) == Nutrients.Turnover.Map + ## Symbol keys. tr = Nutrients.Turnover([:a => 1, :b => 2, :c => 3]) m = base + tr @test m.nutrients.richness == 3 diff --git a/test/user/data_components/species.jl b/test/user/data_components/species.jl index 0c6ad50a3..3afbc5444 100644 --- a/test/user/data_components/species.jl +++ b/test/user/data_components/species.jl @@ -24,9 +24,30 @@ @test m.species.names == [:s1, :s2, :s3] @test typeof(sp) == Species.Number + # From an index. + sp = Species(Dict([:a => 1, :b => 2, :c => 3])) + m = base + sp + @test m.species.names == [:a, :b, :c] + @test typeof(sp) == Species.Names + #--------------------------------------------------------------------------------------- # Guards. + @argfails( + Species(Dict([:a => 1, :c => 3])), + "Invalid index: received 2 references but one of them is [3] (:c)." + ) + + @argfails( + Species(Dict([:a => 1, :c => 1])), + "Invalid index: no reference given for index [2]." + ) + + @argfails( + Species(Dict([:a => 5, :b => 2, :c => 3])), + "Invalid index: received 3 references but one of them is [5] (:a)." + ) + @sysfails( Model(Species([:a, :b, :a])), Check(early, [Species.Names], "Species 1 and 3 are both named :a."),