Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions src/DataWrangling/metadata.jl
Original file line number Diff line number Diff line change
Expand Up @@ -497,16 +497,16 @@ function Fields.set!(fields::NamedTuple, mset::MetadataSet)
end

"""
set!(model, mset::MetadataSet)
set!(model, mset::MetadataSet, names=keys(variable_glossary))

Set fields of `model` from the variables in `mset`, auto-routing verbose
dataset variable names to short model field-names via the global
[`variable_glossary`](@ref) registry.
Route variables from `mset` to `set!(model; kwargs...)`, translating verbose
dataset names to short model field-names via [`variable_glossary`](@ref).
Only the intersection of `names` and `mset.names` is forwarded.

Variables in `mset` that have no entry in `variable_glossary` are silently
skipped — this lets partial application across coupled-model components work
naturally. For example, a single 4-variable set can drive both an ocean and a
sea-ice model:
The default `names` is every glossary key — fine for permissive models. Models
that throw on unknown kwargs (`HydrostaticFreeSurfaceModel`, `SeaIceModel`)
override the 2-argument form to pass a narrower `names`, letting a single
multi-component MetadataSet drive both an ocean and a sea-ice model:

```julia
mset = MetadataSet(:temperature, :salinity,
Expand All @@ -516,13 +516,31 @@ set!(ocean.model, mset) # consumes :temperature, :salinity → T, S
set!(sea_ice.model, mset) # consumes :sea_ice_thickness, :sea_ice_concentration → h, ℵ
```
"""
function Fields.set!(model, mset::MetadataSet)
names = getfield(mset, :names)
known = filter(n -> haskey(variable_glossary, n), names)
kwargs = NamedTuple{Tuple(variable_glossary[n] for n in known)}(Tuple(mset[n] for n in known))
function Fields.set!(model, mset::MetadataSet, names=keys(variable_glossary))
routed = filter(in(names), getfield(mset, :names))
isempty(routed) && return model
kwargs = NamedTuple{Tuple(variable_glossary[n] for n in routed)}(Tuple(mset[n] for n in routed))
return set!(model; kwargs...)
end

# Ocean: route only variables whose short name appears in velocities,
# tracers, or free_surface — the three places HydrostaticFreeSurfaceModel's
# `set!` looks up kwargs.
using Oceananigans.Models.HydrostaticFreeSurfaceModels: HydrostaticFreeSurfaceModel
function Fields.set!(model::HydrostaticFreeSurfaceModel, mset::MetadataSet)
short = (propertynames(model.velocities)...,
propertynames(model.tracers)...,
propertynames(model.free_surface)...)
names = Tuple(n for (n, s) in variable_glossary if s in short)
return set!(model, mset, names)
end

# Sea ice: ClimaSeaIce's `set!(::SeaIceModel; h, ℵ)` only accepts these two.
using ClimaSeaIce: SeaIceModel
function Fields.set!(model::SeaIceModel, mset::MetadataSet)
return set!(model, mset, (:sea_ice_thickness, :sea_ice_concentration))
end

"""
download(mset::MetadataSet; kwargs...)

Expand Down
63 changes: 63 additions & 0 deletions test/test_metadata_set.jl
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,69 @@ end
@test issubset(received_keys, values(NumericalEarth.DataWrangling.variable_glossary))
end

#####
##### Per-model overrides (regression for issue with shared multi-component sets)
#####
##### Real Oceananigans/ClimaSeaIce models throw ArgumentError on unknown
##### kwargs, so they override `set!(model, ::MetadataSet)` to filter down to
##### the variables they consume. This RestrictiveStubModel mimics that
##### strictness and demonstrates the override pattern used by the real
##### dispatches in src/DataWrangling/metadata.jl.

mutable struct RestrictiveStubModel
accepts :: Tuple{Vararg{Symbol}} # short field names this model accepts
received :: Vector{Pair{Symbol, Any}}
RestrictiveStubModel(accepts...) = new(accepts, Pair{Symbol, Any}[])
end

function NumericalEarth.DataWrangling.set!(m::RestrictiveStubModel; kw...)
for (k, v) in kw
k in m.accepts || throw(ArgumentError("name $k not accepted by RestrictiveStubModel"))
push!(m.received, k => v)
end
return m
end

# Override the 2-arg dispatch to pass a narrowed `names` to the generic
# 3-arg form — same shape as the real HydrostaticFreeSurfaceModel/SeaIceModel
# methods.
function Oceananigans.Fields.set!(m::RestrictiveStubModel, mset::MetadataSet)
names = Tuple(n for (n, s) in variable_glossary if s in m.accepts)
return set!(m, mset, names)
end

@testset "set!(model, mset) — per-model overrides filter a shared MetadataSet" begin
mset = MetadataSet(:temperature, :salinity,
:sea_ice_thickness, :sea_ice_concentration;
dataset = ECCO4Monthly(),
date = snapshot_date)

# Ocean-like model: override filters to (:T, :S).
ocean_like = RestrictiveStubModel(:T, :S)
set!(ocean_like, mset)
ocean_keys = first.(ocean_like.received)
@test :T in ocean_keys
@test :S in ocean_keys
@test !(:h in ocean_keys)
@test !(:ℵ in ocean_keys)
@test length(ocean_keys) == 2

# Sea-ice-like model: override filters to (:h, :ℵ).
sea_ice_like = RestrictiveStubModel(:h, :ℵ)
set!(sea_ice_like, mset)
ice_keys = first.(sea_ice_like.received)
@test :h in ice_keys
@test :ℵ in ice_keys
@test !(:T in ice_keys)
@test !(:S in ice_keys)
@test length(ice_keys) == 2

# No matching variables → no-op, no error.
empty_model = RestrictiveStubModel(:foo)
set!(empty_model, mset)
@test isempty(empty_model.received)
end

@testset "set!(::NamedTuple, mset) — explicit per-variable" begin
mset = MetadataSet(:temperature, :salinity;
dataset = ECCO4Monthly(),
Expand Down
Loading