Skip to content

Inconsistent identification of regular coordinates by read_ncdf() #746

@dschlaep

Description

@dschlaep

read_ncdf() appears to recognize regular coordinates correctly ("offset" and "delta") only if the netCDF file does not contain "axis" attributes (or if ignore_bounds is requested).

It seems that this is because ncmeta::nc_coord_var() does not find coordinate bounds if there are no "axis" attributes (or if the coordinate bounds are ignored because of ignore_bounds)

If coordinate bounds have been identified (that is, if an "axis" attribute is present), then stars:::.get_nc_dimensions() finds try_bounds to be TRUE

stars/R/ncdf.R

Line 734 in 6300710

length(bounds <- coord_var[coord_var$variable == names(coords)[i], ]$bounds) > 0 &&

and continues to correctly assign offset and delta (the coordinate values are regular, is_reg is TRUE, and abs(v - u) < eps is TRUE)

stars/R/ncdf.R

Line 758 in 6300710

dimensions[[i]]$offset = bounds[1,1]

In a next step, however, offset and delta are then erased because regular[i] & !try_bounds is FALSE.

stars/R/ncdf.R

Line 786 in 6300710

dimensions[[i]]$offset[1L] = NA_real_

read_mdim() works correctly whether or not the file contains "axis" attributes.

Many thanks!

Example code to illustrate the varying behavior:

getNamespaceVersion("stars")
#> version 
#> "0.6-8"
getNamespaceVersion("ncmeta")
#> version 
#> "0.4.0"

# Example netcd files
nLat <- 3
nLon <- 2
nVertical <- 9

tsl <- array(
  data = rowSums(
    expand.grid(
      seq_len(nVertical),
      100 * (seq_len(nLat) - 1)),
    10 * (seq_len(nLon) - 1)
  ),
  dim = c(nVertical, nLat, nLon)
)

dlat <- 5
crdsLat <- 40 + dlat * seq_len(nLat)
crdsLatBounds <- rbind(crdsLat - dlat / 2, crdsLat + dlat / 2)

dlon <- 5
crdsLon <- -100 + dlon * seq_len(nLon)
crdsLonBounds <- rbind(crdsLon - dlon / 2, crdsLon + dlon / 2)

tmp <- 10 * (1:nVertical) ^ 2
crdsVerticalBounds <- rbind(c(0, tmp[-length(tmp)]), tmp)
crdsVertical <- colMeans(crdsVerticalBounds) # not regular

# Create filename1
filename1 <- tempfile("tsl_example_", fileext = ".nc")
nc <- RNetCDF::create.nc(filename1)

id_bnds <- RNetCDF::dim.def.nc(nc, "bnds", 2)

id_lon <- RNetCDF::dim.def.nc(nc, "lon", nLon)
iv_lon <- RNetCDF::var.def.nc(nc, "lon", "NC_FLOAT", id_lon)
RNetCDF::var.put.nc(nc, "lon", crdsLon)

RNetCDF::att.put.nc(nc, "lon", "bounds", "NC_CHAR", "lon_bnds")
iv_blon <- RNetCDF::var.def.nc(nc, "lon_bnds", "NC_FLOAT", c(id_bnds, id_lon))
RNetCDF::var.put.nc(nc, "lon_bnds", crdsLonBounds)

id_lat <- RNetCDF::dim.def.nc(nc, "lat", nLat)
iv_lat <- RNetCDF::var.def.nc(nc, "lat", "NC_FLOAT", id_lat)
RNetCDF::var.put.nc(nc, "lat", crdsLat)

RNetCDF::att.put.nc(nc, "lat", "bounds", "NC_CHAR", "lat_bnds")
iv_blat <- RNetCDF::var.def.nc(nc, "lat_bnds", "NC_FLOAT", c(id_bnds, id_lat))
RNetCDF::var.put.nc(nc, "lat_bnds", crdsLatBounds)

id_vertical <- RNetCDF::dim.def.nc(nc, "vertical", nVertical)
iv_vertical <- RNetCDF::var.def.nc(nc, "vertical", "NC_INT", id_vertical)
RNetCDF::att.put.nc(nc, "vertical", "positive", "NC_CHAR", "down")
RNetCDF::var.put.nc(nc, "vertical", crdsVertical)

iv_tsl <- RNetCDF::var.def.nc(nc, "soil_variable", "NC_FLOAT", c(id_vertical, id_lat, id_lon))
RNetCDF::var.put.nc(nc, "soil_variable", tsl)

RNetCDF::close.nc(nc)


# filename2 is a copy of filename1 but it has X, Y, Z axis attributes
filename2 <- tempfile("tsl_example_", fileext = ".nc")
file.copy(from = filename1, to = filename2)
#> [1] TRUE
nc <- RNetCDF::open.nc(filename2, write = TRUE)

RNetCDF::att.put.nc(nc, "lon", "axis", "NC_CHAR", "X")
RNetCDF::att.put.nc(nc, "lat", "axis", "NC_CHAR", "Y")
RNetCDF::att.put.nc(nc, "vertical", "axis", "NC_CHAR", "Z")

RNetCDF::close.nc(nc)



# read_mdim() sets offset and delta correctly
(x1m <- stars::read_mdim(filename1)) # correct
#> stars object with 3 dimensions and 1 attribute
#> attribute(s):
#>                Min. 1st Qu. Median Mean 3rd Qu. Max.
#> soil_variable     1    7.25    105  105  202.75  209
#> dimension(s):
#>          from to offset delta               values x/y
#> vertical    1  9     NA    NA [5,25),...,[725,885)    
#> lat         1  3   42.5     5                 NULL [y]
#> lon         1  2  -97.5     5                 NULL [x]

(x2m <- stars::read_mdim(filename2)) # correct
#> stars object with 3 dimensions and 1 attribute
#> attribute(s):
#>                Min. 1st Qu. Median Mean 3rd Qu. Max.
#> soil_variable     1    7.25    105  105  202.75  209
#> dimension(s):
#>          from to offset delta               values x/y
#> vertical    1  9     NA    NA [5,25),...,[725,885)    
#> lat         1  3   42.5     5                 NULL [y]
#> lon         1  2  -97.5     5                 NULL [x]

# read_ncdf() sets offset and delta correctly only if there are no "axis" attributes
(x1n <- stars::read_ncdf(filename1, var = "soil_variable")) # correct
#> Will return stars object with 54 cells.
#> Warning in .get_nc_projection(meta$attribute, rep_var, cv): No projection
#> information found in nc file.
#> stars object with 3 dimensions and 1 attribute
#> attribute(s):
#>                Min. 1st Qu. Median Mean 3rd Qu. Max.
#> soil_variable     1    7.25    105  105  202.75  209
#> dimension(s):
#>          from to offset delta        values x/y
#> vertical    1  9     NA    NA [9] 5,...,725 [x]
#> lat         1  3   42.5     5          NULL [y]
#> lon         1  2  -97.5     5          NULL

(x2n <- stars::read_ncdf(filename2, var = "soil_variable")) # no offset/delta
#> Warning in FUN(X[[i]], ...): Non-canonical axis order found, attempting to
#> correct.
#> Will return stars object with 54 cells.
#> Warning in .get_nc_projection(meta$attribute, rep_var, cv): No projection
#> information found in nc file.
#> stars object with 3 dimensions and 1 attribute
#> attribute(s):
#>                Min. 1st Qu. Median Mean 3rd Qu. Max.
#> soil_variable     1    7.25    105  105  202.75  209
#> dimension(s):
#>          from to        values x/y
#> lon         1  2      -95, -90 [x]
#> lat         1  3    45, 50, 55 [y]
#> vertical    1  9 [9] 5,...,725

(x2ni <- stars::read_ncdf(filename2, var = "soil_variable", ignore_bounds = TRUE)) # correct
#> Warning in FUN(X[[i]], ...): Non-canonical axis order found, attempting to
#> correct.
#> Will return stars object with 54 cells.
#> Warning in .get_nc_projection(meta$attribute, rep_var, cv): No projection
#> information found in nc file.
#> stars object with 3 dimensions and 1 attribute
#> attribute(s):
#>                Min. 1st Qu. Median Mean 3rd Qu. Max.
#> soil_variable     1    7.25    105  105  202.75  209
#> dimension(s):
#>          from to offset delta        values x/y
#> lon         1  2  -97.5     5          NULL [x]
#> lat         1  3   42.5     5          NULL [y]
#> vertical    1  9     NA    NA [9] 5,...,725


# `ncmeta::nc_coord_var()` does not find coordinate bounds
# if there are no "axis" attributes
ncmeta::nc_coord_var(filename1) # bounds are not found
#> # A tibble: 2 × 6
#>   variable      X     Y     Z        T     bounds
#>   <chr>         <chr> <chr> <chr>    <chr> <chr> 
#> 1 vertical      <NA>  <NA>  vertical <NA>  <NA>  
#> 2 soil_variable <NA>  <NA>  vertical <NA>  <NA>

ncmeta::nc_coord_var(filename2) # bounds are found
#> # A tibble: 6 × 6
#>   variable      X     Y     Z        T     bounds  
#>   <chr>         <chr> <chr> <chr>    <chr> <chr>   
#> 1 lon           lon   <NA>  <NA>     <NA>  lon_bnds
#> 2 lon_bnds      lon   <NA>  <NA>     <NA>  <NA>    
#> 3 lat           <NA>  lat   <NA>     <NA>  lat_bnds
#> 4 lat_bnds      <NA>  lat   <NA>     <NA>  <NA>    
#> 5 vertical      <NA>  <NA>  vertical <NA>  <NA>    
#> 6 soil_variable lon   lat   vertical <NA>  <NA>


# Clean up
unlink(filename1)
unlink(filename2)

Created on 2025-04-22 with reprex v2.1.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions