Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **`read_netcdf` threw on files with a CF time coordinate.** A time axis
written by `write_netcdf(...; time=t)` (units `"seconds since ..."`, a
`calendar`) is decoded by NCDatasets to `DateTime`, which the reader tried to
store in a `Dict{String, Vector{Float64}}` — a `convert` error. The
`"coordinates"`/`"variables"` containers are now `Any`-valued, so the decoded
time is preserved (`data["coordinates"]["time"]::Vector{DateTime}`) alongside
the `Float64` spatial coordinates.

- **Unstructured RL/RLZ evaluation flipped every sine (odd-in-λ) component.**
`_eval_unstructured_rl`/`_eval_unstructured_rlz` (behind `evaluate_unstructured`,
`interpolate_to_grid`, and grid relocation) synthesized the Fourier series with
Expand Down
31 changes: 21 additions & 10 deletions src/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1092,18 +1092,26 @@ and return its contents as a plain [`Dict`].

The returned dictionary has four string-keyed entries:

| Key | Type | Contents |
|:-------------- |:----------------------------- |:--------------------------------- |
| `"dimensions"` | `Dict{String, Int}` | dimension name → length |
| `"coordinates"`| `Dict{String, Vector{Float64}}`| coordinate name → values |
| `"variables"` | `Dict{String, Array{Float64}}`| data variable name → array |
| `"attributes"` | `Dict{String, Any}` | global attribute name → value |
| Key | Type | Contents |
|:-------------- |:------------------- |:--------------------------------- |
| `"dimensions"` | `Dict{String, Int}` | dimension name → length |
| `"coordinates"`| `Dict{String, Any}` | coordinate name → values |
| `"variables"` | `Dict{String, Any}` | data variable name → array |
| `"attributes"` | `Dict{String, Any}` | global attribute name → value |

Coordinate variables are identified by sharing their name with a NetCDF
dimension. All other variables are returned under `"variables"`. This
format-independent structure can be passed directly to plotting libraries
or used for further analysis without requiring a [`SpringsteelGrid`](@ref).

Values are returned exactly as NCDatasets decodes them: spatial coordinates and
data come back as `Float64` arrays, while a CF time axis (a coordinate carrying
`units = "... since ..."` and a `calendar`) is decoded to a `DateTime` (or
`CFTime` for non-standard calendars) array. The `"coordinates"`/`"variables"`
containers are therefore `Any`-valued so the decoded time is preserved rather
than forced into `Float64` — reading a file written with `write_netcdf(...;
time=t)` returns `data["coordinates"]["time"]::Vector{DateTime}`.

# Arguments
- `filename`: Path to an existing NetCDF file.

Expand Down Expand Up @@ -1133,17 +1141,20 @@ function read_netcdf(filename::String)
end
result["dimensions"] = dims

# Coordinates (variables that share their dimension name)
coords = Dict{String, Vector{Float64}}()
# Coordinates (variables that share their dimension name). Any-valued:
# NCDatasets decodes a CF time axis to DateTime/CFTime, which must not be
# coerced to Float64 — preserving it is the whole point of a time axis.
coords = Dict{String, Any}()
for name in keys(dims)
if haskey(ds, name)
coords[name] = Array(ds[name])
end
end
result["coordinates"] = coords

# Variables (everything that's not a coordinate)
vars = Dict{String, Array{Float64}}()
# Variables (everything that's not a coordinate). Any-valued for the same
# reason as coordinates (a data variable may also carry a decoded type).
vars = Dict{String, Any}()
for name in keys(ds)
if !haskey(dims, name)
vars[name] = Array(ds[name])
Expand Down
32 changes: 32 additions & 0 deletions test/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,38 @@ using DataFrames
@test haskey(data["variables"], "u_xx")
end

@testset "read_netcdf preserves CF time coordinate" begin
# write_netcdf(...; time=t) writes a CF time axis (units
# "seconds since 1970-...", calendar gregorian), which NCDatasets
# decodes to DateTime. read_netcdf must preserve it, not coerce to
# Float64. Regression: it previously threw a convert error.
gp = SpringsteelGridParameters(geometry="R", num_cells=20,
iMin=0.0, iMax=10.0,
vars=Dict("u" => 1),
BCL=Dict("u" => CubicBSpline.PERIODIC),
BCR=Dict("u" => CubicBSpline.PERIODIC))
grid = createGrid(gp)
pts = getGridpoints(grid)
for i in eachindex(pts)
grid.physical[i, 1, 1] = sin(2π * pts[i] / 10.0)
end
spectralTransform!(grid)

tmpfile = joinpath(mktempdir(), "test_r_time.nc")
write_netcdf(tmpfile, grid; time=3600.0)

data = read_netcdf(tmpfile) # must not throw

@test haskey(data["coordinates"], "time")
t_vals = data["coordinates"]["time"]
@test eltype(t_vals) <: Dates.AbstractTime
# 3600 s since the 1970 epoch = 1970-01-01T01:00:00
@test t_vals[1] == DateTime(1970, 1, 1, 1, 0, 0)
# spatial coordinate still decodes to Float64
@test data["coordinates"]["x"] isa AbstractVector{Float64}
@test haskey(data["variables"], "u")
end

@testset "read_netcdf nonexistent file throws" begin
@test_throws Exception read_netcdf("/nonexistent/path/file.nc")
end
Expand Down
Loading