diff --git a/src/DataWrangling/metadata.jl b/src/DataWrangling/metadata.jl index 645c27f8..c77ef783 100644 --- a/src/DataWrangling/metadata.jl +++ b/src/DataWrangling/metadata.jl @@ -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, @@ -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...) diff --git a/test/test_metadata_set.jl b/test/test_metadata_set.jl index 9e5d62c5..32c085d5 100644 --- a/test/test_metadata_set.jl +++ b/test/test_metadata_set.jl @@ -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(),