11module OceananigansNCDatasetsExt
22
33using NCDatasets
4+ import NCDatasets: defVar
45
56using Dates: AbstractTime, UTC, now
67using Printf: @sprintf
78using OrderedCollections: OrderedDict
89
910using 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
1213using Oceananigans. BuoyancyFormulations: BuoyancyForce, BuoyancyTracer, SeawaterBuoyancy, LinearEquationOfState
1314using Oceananigans. Fields
14- using Oceananigans. Fields: reduced_dimensions, reduced_location, location
15+ using Oceananigans. Fields: Reduction, reduced_dimensions, reduced_location, location
1516using 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
2021using Oceananigans. ImmersedBoundaries: ImmersedBoundaryGrid, GridFittedBottom, GFBIBG, GridFittedBoundary, PartialCellBottom, PCBIBG
2122using Oceananigans. Models: ShallowWaterModel, LagrangianParticles
@@ -41,13 +42,71 @@ using Oceananigans.OutputWriters:
4142 show_array_type
4243
4344import 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
4653const c = Center ()
4754const f = Face ()
4855const BoussinesqSeawaterBuoyancy = SeawaterBuoyancy{FT, <: BoussinesqEquationOfState , T, S} where {FT, T, S}
4956const 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
68127end
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
80137end
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
96163end
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, LΛ (), nothing , nothing , Val (:x ))
420- φ_dim_name = dim_name_generator (" φ" , grid, nothing , LΦ (), 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 , LΦ (), 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
0 commit comments