Skip to content

Commit bed34ae

Browse files
tomchorCopilot
andauthored
Add methods to write a field directly to NetCDF (#4730)
Co-authored-by: Copilot <[email protected]>
1 parent cbfb6f2 commit bed34ae

File tree

3 files changed

+336
-82
lines changed

3 files changed

+336
-82
lines changed

ext/OceananigansNCDatasetsExt.jl

Lines changed: 97 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
module OceananigansNCDatasetsExt
22

33
using NCDatasets
4+
import NCDatasets: defVar
45

56
using Dates: AbstractTime, UTC, now
67
using Printf: @sprintf
78
using OrderedCollections: OrderedDict
89

910
using Oceananigans: initialize!, prettytime, pretty_filesize, AbstractModel
10-
using Oceananigans.Architectures: CPU, GPU
11-
using Oceananigans.AbstractOperations: KernelFunctionOperation
11+
using Oceananigans.Architectures: CPU, GPU, on_architecture
12+
using Oceananigans.AbstractOperations: KernelFunctionOperation, AbstractOperation
1213
using Oceananigans.BuoyancyFormulations: BuoyancyForce, BuoyancyTracer, SeawaterBuoyancy, LinearEquationOfState
1314
using Oceananigans.Fields
14-
using Oceananigans.Fields: reduced_dimensions, reduced_location, location
15+
using Oceananigans.Fields: Reduction, reduced_dimensions, reduced_location, location
1516
using Oceananigans.Grids: Center, Face, Flat, Periodic, Bounded,
1617
AbstractGrid, RectilinearGrid, LatitudeLongitudeGrid, StaticVerticalDiscretization,
1718
topology, halo_size, xspacings, yspacings, zspacings, λspacings, φspacings,
18-
parent_index_range, ξnodes, ηnodes, rnodes, validate_index, peripheral_node,
19+
parent_index_range, nodes, ξnodes, ηnodes, rnodes, validate_index, peripheral_node,
1920
constructor_arguments
2021
using Oceananigans.ImmersedBoundaries: ImmersedBoundaryGrid, GridFittedBottom, GFBIBG, GridFittedBoundary, PartialCellBottom, PCBIBG
2122
using Oceananigans.Models: ShallowWaterModel, LagrangianParticles
@@ -41,13 +42,71 @@ using Oceananigans.OutputWriters:
4142
show_array_type
4243

4344
import Oceananigans: write_output!
44-
import Oceananigans.OutputWriters: NetCDFWriter, write_grid_reconstruction_data!, convert_for_netcdf, materialize_from_netcdf, reconstruct_grid
45+
import Oceananigans.OutputWriters:
46+
NetCDFWriter,
47+
write_grid_reconstruction_data!,
48+
convert_for_netcdf,
49+
materialize_from_netcdf,
50+
reconstruct_grid,
51+
trilocation_dim_name
4552

4653
const c = Center()
4754
const f = Face()
4855
const BoussinesqSeawaterBuoyancy = SeawaterBuoyancy{FT, <:BoussinesqEquationOfState, T, S} where {FT, T, S}
4956
const BuoyancyBoussinesqEOSModel = BuoyancyForce{<:BoussinesqSeawaterBuoyancy, g} where {g}
5057

58+
#####
59+
##### Extend defVar to be able to write fields to NetCDF directly
60+
#####
61+
62+
defVar(ds, name, op::AbstractOperation; kwargs...) = defVar(ds, name, Field(op); kwargs...)
63+
defVar(ds, name, op::Reduction; kwargs...) = defVar(ds, name, Field(op); kwargs...)
64+
65+
function defVar(ds, name, field::AbstractField;
66+
time_dependent=false,
67+
dimension_name_generator = trilocation_dim_name,
68+
kwargs...)
69+
field_cpu = on_architecture(CPU(), field) # Need to bring field to CPU in order to write it to NetCDF
70+
field_data = Array{eltype(field)}(field_cpu)
71+
dims = field_dimensions(field, dimension_name_generator)
72+
all_dims = time_dependent ? (dims..., "time") : dims
73+
74+
# Validate that all dimensions exist and match the field
75+
create_field_dimensions!(ds, field, all_dims, dimension_name_generator)
76+
defVar(ds, name, field_data, all_dims; kwargs...)
77+
end
78+
79+
#####
80+
##### Dimension validation
81+
#####
82+
83+
"""
84+
create_field_dimensions!(ds, field::AbstractField, all_dims, dimension_name_generator)
85+
86+
Creates all dimensions for the given `field` in the NetCDF dataset `ds`. If the dimensions
87+
already exist, they are validated to match the expected dimensions for the given `field`.
88+
89+
Arguments:
90+
- `ds`: NetCDF dataset
91+
- `field`: AbstractField being written
92+
- `all_dims`: Tuple of dimension names to create/validate
93+
- `dimension_name_generator`: Function to generate dimension names
94+
"""
95+
function create_field_dimensions!(ds, field::AbstractField, all_dims, dimension_name_generator)
96+
dimension_attributes = default_dimension_attributes(field.grid, dimension_name_generator)
97+
spatial_dims = all_dims[1:end-(("time" in all_dims) ? 1 : 0)]
98+
99+
spatial_dims_dict = Dict(dim_name => dim_data for (dim_name, dim_data) in zip(spatial_dims, nodes(field)))
100+
create_spatial_dimensions!(ds, spatial_dims_dict, dimension_attributes; array_type=Array{eltype(field)})
101+
102+
# Create time dimension if needed
103+
if "time" in all_dims && "time" keys(ds.dim)
104+
create_time_dimension!(ds)
105+
end
106+
107+
return nothing
108+
end
109+
51110
#####
52111
##### Utils
53112
#####
@@ -67,73 +126,42 @@ function collect_dim(ξ, ℓ, T, N, H, inds, with_halos)
67126
end
68127
end
69128

70-
#####
71-
##### Dimension name generators
72-
#####
73-
74-
function suffixed_dim_name_generator(var_name, grid::AbstractGrid{FT, TX}, LX, LY, LZ, dim::Val{:x}; connector="_", location_letters) where {FT, TX}
75-
if TX == Flat || isnothing(LX)
76-
return ""
77-
else
78-
return "$(var_name)" * connector * location_letters
129+
function create_time_dimension!(dataset; attrib=nothing)
130+
if "time" keys(dataset.dim)
131+
# Create an unlimited dimension "time"
132+
# Time should always be Float64 to be extra safe from rounding errors.
133+
# See: https://github.com/CliMA/Oceananigans.jl/issues/3056
134+
defDim(dataset, "time", Inf)
135+
defVar(dataset, "time", Float64, ("time",), attrib=attrib)
79136
end
80137
end
81138

82-
function suffixed_dim_name_generator(var_name, grid::AbstractGrid{FT, TX, TY}, LX, LY, LZ, dim::Val{:y}; connector="_", location_letters) where {FT, TX, TY}
83-
if TY == Flat || isnothing(LY)
84-
return ""
85-
else
86-
return "$(var_name)" * connector * location_letters
87-
end
88-
end
89139

90-
function suffixed_dim_name_generator(var_name, grid::AbstractGrid{FT, TX, TY, TZ}, LX, LY, LZ, dim::Val{:z}; connector="_", location_letters) where {FT, TX, TY, TZ}
91-
if TZ == Flat || isnothing(LZ)
92-
return ""
93-
else
94-
return "$(var_name)" * connector * location_letters
140+
"""
141+
create_spatial_dimensions!(dataset, dims, attributes_dict; array_type=Array{Float32}, kwargs...)
142+
143+
Create spatial dimensions in the NetCDF dataset and define corresponding variables to store
144+
their coordinate values. Each dimension variable has itself as its sole dimension (e.g., the
145+
`x` variable has dimension `x`). The dimensions are created if they don't exist, and validated
146+
against provided arrays if they do exist. An error is thrown if the dimension already exists
147+
but is different from the provided array.
148+
"""
149+
function create_spatial_dimensions!(dataset, dims, attributes_dict; array_type=Array{Float32}, kwargs...)
150+
for (i, (dim_name, dim_array)) in enumerate(dims)
151+
if dim_name keys(dataset.dim)
152+
# Create missing dimension
153+
defVar(dataset, dim_name, array_type(dim_array), (dim_name,), attrib=attributes_dict[dim_name]; kwargs...)
154+
else
155+
# Validate existing dimension
156+
if dataset[dim_name] != dim_array
157+
throw(ArgumentError("Dimension '$dim_name' already exists in dataset but is different from expected.\n" *
158+
" Actual: $(dataset[dim_name]) (length=$(length(dataset[dim_name])))\n" *
159+
" Expected: $(dim_array) (length=$(length(dim_array)))"))
160+
end
161+
end
95162
end
96163
end
97164

98-
suffixed_dim_name_generator(var_name, ::StaticVerticalDiscretization, LX, LY, LZ, dim::Val{:z}; connector="_", location_letters) = var_name * connector * location_letters
99-
100-
loc2letter(::Face, full=true) = "f"
101-
loc2letter(::Center, full=true) = "c"
102-
loc2letter(::Nothing, full=true) = full ? "a" : ""
103-
104-
minimal_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:x}) = loc2letter(LX, false)
105-
minimal_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:y}) = loc2letter(LY, false)
106-
107-
minimal_location_string(::LatitudeLongitudeGrid, LX, LY, LZ, ::Val{:x}) = loc2letter(LX, false) * loc2letter(LY, false)
108-
minimal_location_string(::LatitudeLongitudeGrid, LX, LY, LZ, ::Val{:y}) = loc2letter(LX, false) * loc2letter(LY, false)
109-
110-
minimal_location_string(grid::AbstractGrid, LX, LY, LZ, dim::Val{:z}) = minimal_location_string(grid.z, LX, LY, LZ, dim)
111-
minimal_location_string(::StaticVerticalDiscretization, LX, LY, LZ, dim::Val{:z}) = loc2letter(LZ, false)
112-
minimal_location_string(grid, LX, LY, LZ, dim) = loc2letter(LX, false) * loc2letter(LY, false) * loc2letter(LZ, false)
113-
114-
minimal_dim_name(var_name, grid, LX, LY, LZ, dim) =
115-
suffixed_dim_name_generator(var_name, grid, LX, LY, LZ, dim, connector="_", location_letters=minimal_location_string(grid, LX, LY, LZ, dim))
116-
117-
minimal_dim_name(var_name, grid::ImmersedBoundaryGrid, args...) = minimal_dim_name(var_name, grid.underlying_grid, args...)
118-
119-
120-
121-
trilocation_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:x}) = loc2letter(LX) * "aa"
122-
trilocation_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:y}) = "a" * loc2letter(LY) * "a"
123-
124-
trilocation_location_string(::LatitudeLongitudeGrid, LX, LY, LZ, ::Val{:x}) = loc2letter(LX) * loc2letter(LY) * "a"
125-
trilocation_location_string(::LatitudeLongitudeGrid, LX, LY, LZ, ::Val{:y}) = loc2letter(LX) * loc2letter(LY) * "a"
126-
127-
trilocation_location_string(grid::AbstractGrid, LX, LY, LZ, dim::Val{:z}) = trilocation_location_string(grid.z, LX, LY, LZ, dim)
128-
trilocation_location_string(::StaticVerticalDiscretization, LX, LY, LZ, dim::Val{:z}) = "aa" * loc2letter(LZ)
129-
trilocation_location_string(grid, LX, LY, LZ, dim) = loc2letter(LX) * loc2letter(LY) * loc2letter(LZ)
130-
131-
trilocation_dim_name(var_name, grid, LX, LY, LZ, dim) =
132-
suffixed_dim_name_generator(var_name, grid, LX, LY, LZ, dim, connector="_", location_letters=trilocation_location_string(grid, LX, LY, LZ, dim))
133-
134-
trilocation_dim_name(var_name, grid::ImmersedBoundaryGrid, args...) = trilocation_dim_name(var_name, grid.underlying_grid, args...)
135-
136-
137165
#####
138166
##### Gathering of grid dimensions
139167
#####
@@ -417,8 +445,8 @@ function field_dimensions(field::AbstractField, grid::LatitudeLongitudeGrid, dim
417445
LΛ, LΦ, LZ = location(field)
418446

419447
λ_dim_name = dim_name_generator("λ", grid, (), nothing, nothing, Val(:x))
420-
φ_dim_name = dim_name_generator("φ", grid, nothing, (), nothing, Val(:y))
421-
z_dim_name = dim_name_generator("z", grid, nothing, nothing, LZ(), Val(:z))
448+
φ_dim_name = dim_name_generator("φ", grid, nothing, (), nothing, Val(:y))
449+
z_dim_name = dim_name_generator("z", grid, nothing, nothing, LZ(), Val(:z))
422450

423451
λ_dim_name = isempty(λ_dim_name) ? tuple() : tuple(λ_dim_name)
424452
φ_dim_name = isempty(φ_dim_name) ? tuple() : tuple(φ_dim_name)
@@ -1146,18 +1174,8 @@ function initialize_nc_file(model,
11461174
Dict("long_name" => "Time", "units" => "seconds since 2000-01-01 00:00:00") :
11471175
Dict("long_name" => "Time", "units" => "seconds")
11481176

1149-
# Create an unlimited dimension "time"
1150-
# Time should always be Float64 to be extra safe from rounding errors.
1151-
# See: https://github.com/CliMA/Oceananigans.jl/issues/3056
1152-
defDim(dataset, "time", Inf)
1153-
defVar(dataset, "time", Float64, ("time",), attrib=time_attrib)
1154-
1155-
# Create spatial dimensions as variables whose dimensions are themselves.
1156-
# Each should already have a default attribute.
1157-
for (dim_name, dim_array) in dims
1158-
defVar(dataset, dim_name, array_type(dim_array), (dim_name,),
1159-
deflatelevel=deflatelevel, attrib=output_attributes[dim_name])
1160-
end
1177+
create_time_dimension!(dataset, attrib=time_attrib)
1178+
create_spatial_dimensions!(dataset, dims, output_attributes; deflatelevel=1, array_type)
11611179

11621180
time_independent_vars = Dict()
11631181

src/OutputWriters/netcdf_writer.jl

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,66 @@
44
##### NetCDFWriter functionality is implemented in ext/OceananigansNCDatasetsExt
55
#####
66

7+
using Oceananigans.Grids: topology, Flat, StaticVerticalDiscretization
8+
using Oceananigans.ImmersedBoundaries: ImmersedBoundaryGrid
9+
10+
#####
11+
##### Dimension name generators
12+
#####
13+
14+
function suffixed_dim_name_generator(var_name, grid::AbstractGrid{FT, TX}, LX, LY, LZ, dim::Val{:x}; connector="_", location_letters) where {FT, TX}
15+
if TX == Flat || isnothing(LX)
16+
return ""
17+
else
18+
return "$(var_name)" * connector * location_letters
19+
end
20+
end
21+
22+
function suffixed_dim_name_generator(var_name, grid::AbstractGrid{FT, TX, TY}, LX, LY, LZ, dim::Val{:y}; connector="_", location_letters) where {FT, TX, TY}
23+
if TY == Flat || isnothing(LY)
24+
return ""
25+
else
26+
return "$(var_name)" * connector * location_letters
27+
end
28+
end
29+
30+
function suffixed_dim_name_generator(var_name, grid::AbstractGrid{FT, TX, TY, TZ}, LX, LY, LZ, dim::Val{:z}; connector="_", location_letters) where {FT, TX, TY, TZ}
31+
if TZ == Flat || isnothing(LZ)
32+
return ""
33+
else
34+
return "$(var_name)" * connector * location_letters
35+
end
36+
end
37+
38+
suffixed_dim_name_generator(var_name, ::StaticVerticalDiscretization, LX, LY, LZ, dim::Val{:z}; connector="_", location_letters) = var_name * connector * location_letters
39+
40+
loc2letter(::Face, full=true) = "f"
41+
loc2letter(::Center, full=true) = "c"
42+
loc2letter(::Nothing, full=true) = full ? "a" : ""
43+
44+
minimal_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:x}) = loc2letter(LX, false)
45+
minimal_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:y}) = loc2letter(LY, false)
46+
47+
minimal_dim_name(var_name, grid, LX, LY, LZ, dim) =
48+
minimal_dim_name(var_name, grid::ImmersedBoundaryGrid, args...) = minimal_dim_name(var_name, grid.underlying_grid, args...)
49+
50+
trilocation_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:x}) = loc2letter(LX) * "aa"
51+
trilocation_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:y}) = "a" * loc2letter(LY) * "a"
52+
53+
trilocation_location_string(::LatitudeLongitudeGrid, LX, LY, LZ, ::Val{:x}) = loc2letter(LX) * loc2letter(LY) * "a"
54+
trilocation_location_string(::LatitudeLongitudeGrid, LX, LY, LZ, ::Val{:y}) = loc2letter(LX) * loc2letter(LY) * "a"
55+
56+
trilocation_location_string(grid::AbstractGrid, LX, LY, LZ, dim::Val{:z}) = trilocation_location_string(grid.z, LX, LY, LZ, dim)
57+
trilocation_location_string(::StaticVerticalDiscretization, LX, LY, LZ, dim::Val{:z}) = "aa" * loc2letter(LZ)
58+
trilocation_location_string(grid, LX, LY, LZ, dim) = loc2letter(LX) * loc2letter(LY) * loc2letter(LZ)
59+
60+
trilocation_dim_name(var_name, grid, LX, LY, LZ, dim) =
61+
suffixed_dim_name_generator(var_name, grid, LX, LY, LZ, dim, connector="_", location_letters=trilocation_location_string(grid, LX, LY, LZ, dim))
62+
63+
trilocation_dim_name(var_name, grid::ImmersedBoundaryGrid, args...) = trilocation_dim_name(var_name, grid.underlying_grid, args...)
64+
65+
66+
767
mutable struct NetCDFWriter{G, D, O, T, A, FS, DN} <: AbstractOutputWriter
868
grid :: G
969
filepath :: String

0 commit comments

Comments
 (0)