Skip to content

Commit

Permalink
🚧 Nutrients.HalfSaturation upgraded: all data components upgraded!
Browse files Browse the repository at this point in the history
  • Loading branch information
iago-lito committed Nov 8, 2024
1 parent f25284d commit 63439f8
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 65 deletions.
2 changes: 1 addition & 1 deletion src/components/nutrients/concentration.jl
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ end
depends(Concentration)
@nutrients_index
ref(raw -> raw._scratch[:nutrients_concentration])
get(Concentations{Float64}, "nutrient")
get(Concentations{Float64}, "producer-to-nutrient link")
write!((raw, rhs::Real, i, j) -> Concentration_.check(rhs, (i, j)))
end

Expand Down
138 changes: 95 additions & 43 deletions src/components/nutrients/half_saturation.jl
Original file line number Diff line number Diff line change
@@ -1,65 +1,117 @@
# Set or generate half saturations for every producer-to-nutrient link in the model.
#
# Copied and adapted from concentrations.

# ==========================================================================================
abstract type HalfSaturation <: ModelBlueprint end
# All subtypes must require(Nutrients.Nodes,Foodweb).
# Mostly duplicated from HalfSaturation.

HalfSaturation(h) = HalfSaturationFromRawValues(h)
export HalfSaturation
# (reassure JuliaLS)
(false) && (local HalfSaturation, _HalfSaturation)

# ==========================================================================================
module HalfSaturation_
include("../blueprint_modules.jl")
include("../blueprint_modules_identifiers.jl")
import .EN: Foodweb, Nutrients

#-------------------------------------------------------------------------------------------
mutable struct HalfSaturationFromRawValues <: HalfSaturation
h::@GraphData {Scalar, Matrix}{Float64}
HalfSaturationFromRawValues(h) = new(@tographdata h SM{Float64})
mutable struct Raw <: Blueprint
h::Matrix{Float64}
nutrients::Brought(Nutrients.Nodes)
Raw(h, nt = Nutrients._Nodes) = new(Float64.(h), nt)
end
F.implied_blueprint_for(bp::Raw, ::Nutrients._Nodes) = Nutrients.Nodes(size(bp.h)[2])
@blueprint Raw "producers × nutrients half-saturation matrix"
export Raw

F.can_imply(bp::HalfSaturationFromRawValues, ::Type{Nutrients.Nodes}) = !(bp.h isa Real)
Nutrients.Nodes(bp::HalfSaturationFromRawValues) = Nutrients.Nodes(size(bp.h, 2))
F.early_check(bp::Raw) = check_edges(check, bp.h)
check(h, ref = nothing) = check_value(>=(0), h, ref, :h, "Not a positive value")

function F.check(model, bp::HalfSaturationFromRawValues)
(; n_producers, n_nutrients) = model
function F.late_check(raw, bp::Raw)
(; h) = bp
@check_size_if_matrix h (n_producers, n_nutrients)
P = @get raw.producers.number
N = @get raw.nutrients.number
@check_size h (P, N)
end

function F.expand!(model, bp::HalfSaturationFromRawValues)
(; n_producers, n_nutrients) = model
(; h) = bp
@to_size_if_scalar Real h (n_producers, n_nutrients)
model._scratch[:nutrients_half_saturation] = h
F.expand!(raw, bp::Raw) = expand!(raw, bp.h)
expand!(raw, h) = raw._scratch[:nutrients_half_saturation] = h

#-------------------------------------------------------------------------------------------
mutable struct Flat <: Blueprint
h::Float64
end
@blueprint Flat "uniform half-saturation value" depends(Foodweb, Nutrients.Nodes)
export Flat

@component HalfSaturationFromRawValues requires(Foodweb) implies(Nutrients.Nodes)
export HalfSaturationFromRawValues
F.early_check(bp::Flat) = check(bp.h)
function F.expand!(raw, bp::Flat)
P = @get raw.producers.number
N = @get raw.nutrients.number
expand!(raw, to_size(bp.h, (P, N)))
end

#-------------------------------------------------------------------------------------------
# Keep in case more alternate blueprints are added.
# @conflicts(HalfSaturationFromRawValues)
# Temporary semantic fix before framework refactoring.
F.componentof(::Type{<:HalfSaturation}) = HalfSaturation
mutable struct Adjacency <: Blueprint
h::@GraphData Adjacency{Float64}
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
@blueprint Adjacency "[producer => [nutrient => half-saturation]] map"
export Adjacency

F.early_check(bp::Adjacency) = check_edges(check, bp.h)
function F.late_check(raw, bp::Adjacency)
(; h) = bp
p_index = @ref raw.producers.sparse_index
n_index = @ref raw.nutrients.index
@check_list_refs h "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
h = to_dense_matrix(bp.h, p_index, n_index)
expand!(raw, h)
end

# ==========================================================================================
@expose_data edges begin
property(nutrients_half_saturation)
get(HalfSaturations{Float64}, "producer-to-nutrient link")
ref(m -> m._scratch[:nutrients_half_saturation])
write!((m, rhs, i, j) -> (m._nutrients_half_saturation[i, j] = rhs))
row_index(m -> m._producers_dense_index)
col_index(m -> m._nutrients_index)
depends(HalfSaturation)
end

# ==========================================================================================
display_short(bp::HalfSaturation; kwargs...) = display_short(bp, HalfSaturation; kwargs...)
display_long(bp::HalfSaturation; kwargs...) = display_long(bp, HalfSaturation; kwargs...)
function F.display(model, ::Type{<:HalfSaturation})
h = model.nutrients_half_saturation
min, max = minimum(h), maximum(h)
"Nutrients half-saturation: " * if min == max
"$min"
@component begin
HalfSaturation{Internal}
requires(Foodweb, Nutrients.Nodes)
blueprints(HalfSaturation_)
end
export HalfSaturation

function (::_HalfSaturation)(h)
h = @tographdata h {Scalar, Matrix, Adjacency}{Float64}
if h isa Real
HalfSaturation.Flat(h)
elseif h isa AbstractMatrix
HalfSaturation.Raw(h)
else
"ranging from $min to $max."
HalfSaturation.Adjacency(h)
end
end

@expose_data edges begin
property(nutrients.half_saturation)
depends(HalfSaturation)
@nutrients_index
ref(raw -> raw._scratch[:nutrients_half_saturation])
get(HalfSaturations{Float64}, "producer-to-nutrient link")
write!((raw, rhs::Real, i, j) -> HalfSaturation_.check(rhs, (i, j)))
end

function F.shortline(io::IO, model::Model, ::_HalfSaturation)
print(io, "Nutrients half-saturation: ")
showrange(io, model.nutrients._half_saturation)
end
2 changes: 1 addition & 1 deletion src/components/nutrients/main.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ include("./nodes.jl")
include("./turnover.jl")
include("./supply.jl")
include("./concentration.jl")
# include("./half_saturation.jl")
include("./half_saturation.jl")

end
1 change: 1 addition & 0 deletions test/user/03-components.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ only = [
"./data_components/nutrients/turnover.jl"
"./data_components/nutrients/supply.jl"
"./data_components/nutrients/concentration.jl"
"./data_components/nutrients/half_saturation.jl"
] # Only run these if specified.
if isempty(only)
for subfolder in ["./data_components", "./code_components"]
Expand Down
134 changes: 114 additions & 20 deletions test/user/data_components/nutrients/half_saturation.jl
Original file line number Diff line number Diff line change
@@ -1,45 +1,139 @@
FR = EN.Nutrients.HalfSaturationFromRawValues

@testset "Nutrients half-saturation component." begin

# Adapted from concentration.
# Mostly duplicated from Concentration.

fw = Foodweb([:a => [:b, :c]])
base = Model(fw, Nutrients.Nodes(3))

hs = Nutrients.HalfSaturation([
cn = Nutrients.HalfSaturation([
1 2 3
4 5 6
])
m = base + hs
@test m.nutrients_half_saturation == [
m = base + cn
@test m.nutrients.half_saturation == [
1 2 3
4 5 6
]
@test typeof(hs) === FR
@test typeof(cn) === Nutrients.HalfSaturation.Raw

# Adjacency list.
cn = Nutrients.HalfSaturation([
:b => [:n1 => 1, :n2 => 2, :n3 => 3],
:c => [:n2 => 5, :n3 => 6, :n1 => 4],
])
m = base + cn
@test m.nutrients.half_saturation == [
1 2 3
4 5 6
]
@test typeof(cn) === Nutrients.HalfSaturation.Adjacency

# Scalar.
cn = Nutrients.HalfSaturation(2)
m = base + cn
@test m.nutrients.half_saturation == [
2 2 2
2 2 2
]
@test typeof(cn) === Nutrients.HalfSaturation.Flat

#---------------------------------------------------------------------------------------
# Imply Nutrients.

h = [
1 2 3
4 5 6
]
m = Model(fw, Nutrients.HalfSaturation(h))
@test has_component(m, Nutrients.Nodes)
@test m.nutrients.half_saturation == h
@test m.nutrients.names == [:n1, :n2, :n3]
@test Model(
fw,
Nutrients.HalfSaturation([
: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.HalfSaturation([
0 1 -2
3 0 4
]),
Check(
early,
[Nutrients.HalfSaturation.Raw],
"Not a positive value: h[1, 3] = -2.0.",
)
)

@sysfails(
base + Nutrients.HalfSaturation([
:b => [:n1 => 1, :n2 => 2, :n3 => 3],
:c => [:n2 => 5, :n3 => -6, :n1 => 4],
]),
Check(
early,
[Nutrients.HalfSaturation.Adjacency],
"Not a positive value: h[:c, :n3] = -6.0.",
)
)

@sysfails(
base + Nutrients.HalfSaturation(-5),
Check(early, [Nutrients.HalfSaturation.Flat], "Not a positive value: h = -5.0.")
)

# Invalid size.
@sysfails(
base + Nutrients.HalfSaturation([
0 1
3 0
]),
Check(FR),
"Invalid size for parameter 'h': expected (2, 3), got (2, 2).",
Check(
late,
[Nutrients.HalfSaturation.Raw],
"Invalid size for parameter 'h': expected (2, 3), got (2, 2).",
)
)

# Implies nutrients component.
base = Model(fw)
m = base + Nutrients.HalfSaturation([
1 2 3
4 5 6
])
@test m.nutrients_names == [:n1, :n2, :n3]
# Non-dense input.
@sysfails(
Model(
fw,
Nutrients.HalfSaturation([
:b => [:x => 1, :y => 2],
:c => [:z => 5, :x => 6, :y => 4],
]),
),
Check(
late,
[Nutrients.HalfSaturation.Adjacency],
"Missing 'producer trophic' edge label in 'h': \
no value specified for [:b, :z].",
)
)

# Unless we can't infer it.
# Respect template.
@sysfails(
base + Nutrients.HalfSaturation(5),
Check(FR),
"missing a required component '$(Nutrients.Nodes)': implied."
Model(
fw,
Nutrients.HalfSaturation([
:b => [:x => 1, :y => 2, :z => 3],
:a => [:z => 5, :x => 6, :y => 4],
]),
),
Check(
late,
[Nutrients.HalfSaturation.Adjacency],
"Invalid 'producer trophic' edge label in 'h'. \
Expected either :b or :c, got instead: [:a] (5.0).",
)
)

end

0 comments on commit 63439f8

Please sign in to comment.