Skip to content

Add methods to write a field directly to NetCDF#4730

Merged
tomchor merged 29 commits intomainfrom
tc/field-to-netcdf
Sep 30, 2025
Merged

Add methods to write a field directly to NetCDF#4730
tomchor merged 29 commits intomainfrom
tc/field-to-netcdf

Conversation

@tomchor
Copy link
Copy Markdown
Member

@tomchor tomchor commented Aug 27, 2025

This PR does a bit of reorganizing but mainly it makes it possible to write a Field directly into a NetCDF file. The motivations are:

  • Historically Oceananigans' NetCDF capabilities have been tied to simulations, but that doesn't need to be the case and this is a first step towards something more general
  • More practically, it makes it easier for us to support immersed grid reconstruction from NetCDF (something I'll tackle after (0.98.0) Modify interface for OpenBoundaryConditions with schemes and use Tuples in boundary_mass_fluxes #4687 is merged, which supports non-immersed grid reconstruction)
  • This feature also allows for fields with different underlying grids, or windowed different, and even fields from two completely different simulations, to live in the same NetCDF file. All that is needed is to pass a different dimension_name_generator for each.

Here's a quick example:

julia> grid = RectilinearGrid(size=(4, 4, 4), extent=(1, 1, 1));

julia> c = CenterField(grid);

julia> ds = NCDataset("test.nc", "c")
Dataset: test.nc
Group: /



julia> defVar(ds, "c", c, attrib=Dict("long_name" => "Center field", "units" => "kg/m³"));

julia> ds
Dataset: test.nc
Group: /

Dimensions
   x_caa = 4
   y_aca = 4
   z_aac = 4

Variables
  x_caa   (4)
    Datatype:    Float64 (Float64)
    Dimensions:  x_caa
    Attributes:
     units                = m
     long_name            = Cell center locations in the x-direction.

  y_aca   (4)
    Datatype:    Float64 (Float64)
    Dimensions:  y_aca
    Attributes:
     units                = m
     long_name            = Cell center locations in the y-direction.

  z_aac   (4)
    Datatype:    Float64 (Float64)
    Dimensions:  z_aac
    Attributes:
     units                = m
     long_name            = Cell center locations in the z-direction.

  c   (4 × 4 × 4)
    Datatype:    Float64 (Float64)
    Dimensions:  x_caa × y_aca × z_aac
    Attributes:
     units                = kg/m³
     long_name            = Center field



julia>= Average(c, dims=3);

julia> defVar(ds, "", c̄, attrib=Dict("long_name" => "Average center field", "units" => "kg/m²"));

julia> ds
Dataset: test.nc
Group: /

Dimensions
   x_caa = 4
   y_aca = 4
   z_aac = 4

Variables
  x_caa   (4)
    Datatype:    Float64 (Float64)
    Dimensions:  x_caa
    Attributes:
     units                = m
     long_name            = Cell center locations in the x-direction.

  y_aca   (4)
    Datatype:    Float64 (Float64)
    Dimensions:  y_aca
    Attributes:
     units                = m
     long_name            = Cell center locations in the y-direction.

  z_aac   (4)
    Datatype:    Float64 (Float64)
    Dimensions:  z_aac
    Attributes:
     units                = m
     long_name            = Cell center locations in the z-direction.

  c   (4 × 4 × 4)
    Datatype:    Float64 (Float64)
    Dimensions:  x_caa × y_aca × z_aac
    Attributes:
     units                = kg/m³
     long_name            = Center field

  c̄   (4 × 4)
    Datatype:    Float64 (Float64)
    Dimensions:  x_caa × y_aca
    Attributes:
     units                = kg/m²
     long_name            = Average center field

For now I decided to extend the existing defVar in order to avoid creating new functions, but we can definitely also create a new function specifically for that (to_netcdf(field::AbstractField)?).

@glwagner
Copy link
Copy Markdown
Member

Excited for this!

@tomchor tomchor marked this pull request as ready for review September 20, 2025 21:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds the capability to write Oceananigans Field objects directly to NetCDF files by extending the defVar function. This decouples NetCDF writing from simulation contexts and enables writing fields from different grids to the same file.

Key changes:

  • Extended defVar to handle AbstractField, AbstractOperation, and Reduction objects
  • Moved dimension name generation utilities from extension to main module
  • Added validation and creation of field dimensions in NetCDF datasets

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/OutputWriters/netcdf_writer.jl Moved dimension name generation functions from extension module to main module
ext/OceananigansNCDatasetsExt.jl Extended defVar for fields and added dimension validation/creation functions
test/test_netcdf_writer.jl Added comprehensive tests for single field writing, dimension validation, and multiple grid support

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread test/test_netcdf_writer.jl Outdated
Comment thread ext/OceananigansNCDatasetsExt.jl Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@tomchor
Copy link
Copy Markdown
Member Author

tomchor commented Sep 24, 2025

Not sure what's going on with the distributed tests, but this should be ready to review/merge. @navidcy @glwagner can I get a review when you get the chance?

Copy link
Copy Markdown
Member

@ali-ramadhan ali-ramadhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! I think the PR could just do with some more testing:

  1. I would run the newly added tests on both CPU and GPU, especially to ensure no scalar indexing occurs.
  2. I would also run the newly added tests on LatitudeLongitudeGrid and immersed grids (both immersed rectilinear and immersed lat-lon).

This feature also allows for fields with different underlying grids, or windowed different, and even fields from two completely different simulations, to live in the same NetCDF file. All that is needed is to pass a different dimension_name_generator for each.

That's awesome! Just to confirm: for a user to utilize this feature, I assume it's the job of each dimension_name_generator to generate unique dimension names? Actually reading through the tests has answered my question (yes)!

@tomchor
Copy link
Copy Markdown
Member Author

tomchor commented Sep 30, 2025

Looks good to me! I think the PR could just do with some more testing:

1. I would run the newly added tests on both CPU and GPU, especially to ensure no scalar indexing occurs.

2. I would also run the newly added tests on `LatitudeLongitudeGrid` and immersed grids (both immersed rectilinear and immersed lat-lon).

Good points! I actually was supposed to add GPU from the start (since those tests are on an archs loop) but I forgot, so thanks for catching that.

This feature also allows for fields with different underlying grids, or windowed different, and even fields from two completely different simulations, to live in the same NetCDF file. All that is needed is to pass a different dimension_name_generator for each.

That's awesome! Just to confirm: for a user to utilize this feature, I assume it's the job of each dimension_name_generator to generate unique dimension names? Actually reading through the tests has answered my question (yes)!

Yeah, I think this is a limitation of NetCDF unfortunately. Variables that use the same dimension name must have the same shape. So different shapes require different dimension names :(

@tomchor tomchor merged commit bed34ae into main Sep 30, 2025
69 checks passed
@tomchor tomchor deleted the tc/field-to-netcdf branch September 30, 2025 21:27
minimal_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:x}) = loc2letter(LX, false)
minimal_location_string(::RectilinearGrid, LX, LY, LZ, ::Val{:y}) = loc2letter(LY, false)

minimal_dim_name(var_name, grid, LX, LY, LZ, dim) =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was this line supposed to do? It's defining a function which returns a function, I doubt this is what you meant. Also, this is untested: https://app.codecov.io/gh/CliMA/Oceananigans.jl/blob/main/src%2FOutputWriters%2Fnetcdf_writer.jl#L47

@giordano giordano mentioned this pull request Jan 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants