Skip to content

Commit e42b996

Browse files
committed
Add Var._make_interpolant and boundary conditions
This commit refactors the code making interpolats for OutputVars into the function `Var._make_interpolant`. A periodic boundary condition is added for the longitude dimension and a flat boundary condition is added for the latitude dimension when the array is equispaced and spans the entire range. Lastly, if the element type of any dimension array is DateTime, then no interpolant will be created.
1 parent 264f09a commit e42b996

File tree

4 files changed

+174
-29
lines changed

4 files changed

+174
-29
lines changed

NEWS.md

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ the units or units are missing.
1414
new_var = ClimaAnalysis.set_units(var, "kg m s^-1")
1515
```
1616

17+
### Extrapolating `OutputVar` on longitude and latitude
18+
Extrapolation is now possible for the longitude and latitude dimensions. If the dimension
19+
arrays are equispaced and span the entire range, then a periodic boundary condition is added
20+
for the longitude dimension and a flat boundary condition is added for the latitude
21+
dimension.
22+
23+
## Bug fixes
24+
25+
- Interpolation is not possible with dates. When dates are detected in any dimension, an
26+
interpolat will not be made.
1727

1828
v0.5.9
1929
------

docs/src/var.md

+17
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ new_var = ClimaAnalysis.set_units(var, "kg m s^-1")
6363
!!! warning "Override existing units"
6464
If units already exist, this will override the units for data in `var`.
6565

66+
## Interpolations and extrapolations
67+
68+
Interpolating a `OutputVar` onto coordinates can be done by doing the following:
69+
```julia
70+
var((0.0, 0.0)) # var is a two-dimensional OutputVar
71+
```
72+
73+
A multilinear interpolation is used to determine the value at the coordinate (0, 0).
74+
!!! warning "Interpolate on dates"
75+
If any of the dimensions contains `Dates.DateTime` elements, interpolation is not
76+
possible. `Interpolations.jl` does not support interpolating on dates.
77+
78+
Extrapolating is supported only on the longitude and latitude dimensions. For the longitude
79+
and latitude dimensions, a periodic boundary condition and a flat boundary condition are
80+
added, respectively, when the dimension array is equispaced and spans the entire range. For
81+
all other cases, extrapolating beyond the domain of the dimension will throw an error.
82+
6683
## Integration
6784

6885
`OutputVar`s can be integrated with respect to longitude, latitude, or both using

src/Var.jl

+90-28
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import ..Utils:
1616
split_by_season,
1717
time_to_date,
1818
date_to_time,
19-
_data_at_dim_vals
19+
_data_at_dim_vals,
20+
_isequispaced
2021

2122
export OutputVar,
2223
read_var,
@@ -80,37 +81,91 @@ struct OutputVar{T <: AbstractArray, A <: AbstractArray, B, C, ITP}
8081
interpolant::ITP
8182
end
8283

83-
function OutputVar(attribs, dims, dim_attribs, data)
84-
index2dim = keys(dims) |> collect
85-
dim2index =
86-
Dict([dim_name => index for (index, dim_name) in enumerate(keys(dims))])
84+
"""
85+
_make_interpolant(dims, data)
86+
87+
Make a linear interpolant from `dims`, a dictionary mapping dimension name to array and
88+
`data`, an array containing data. Used in constructing a `OutputVar`.
89+
90+
If any element of the arrays in `dims` is a Dates.DateTime, then no interpolant is returned.
91+
Interpolations.jl does not support interpolating on dates. If the longitudes span the entire
92+
range and are equispaced, then a periodic boundary condition is added for the longitude
93+
dimension. If the latitudes span the entire range and are equispaced, then a flat boundary
94+
condition is added for the latitude dimension. In all other cases, an error is thrown when
95+
extrapolating outside of `dim_array`.
96+
"""
97+
function _make_interpolant(dims, data)
98+
# If any element is DateTime, then return nothing for the interpolant because
99+
# Interpolations.jl do not support DateTimes
100+
for dim_array in values(dims)
101+
eltype(dim_array) <: Dates.DateTime && return nothing
102+
end
87103

88104
# We can only create interpolants when we have 1D dimensions
89-
if isempty(index2dim) ||
90-
any(d -> ndims(d) != 1 || length(d) == 1, values(dims))
91-
itp = nothing
92-
else
93-
# Dimensions are all 1D, check that they are compatible with data
94-
size_data = size(data)
95-
for (dim_index, (dim_name, dim_array)) in enumerate(dims)
96-
dim_length = length(dim_array)
97-
data_length = size_data[dim_index]
98-
if dim_length != data_length
99-
error(
100-
"Dimension $dim_name has inconsistent size with provided data ($dim_length != $data_length)",
101-
)
102-
end
105+
if isempty(dims) || any(d -> ndims(d) != 1 || length(d) == 1, values(dims))
106+
return nothing
107+
end
108+
109+
# Dimensions are all 1D, check that they are compatible with data
110+
size_data = size(data)
111+
for (dim_index, (dim_name, dim_array)) in enumerate(dims)
112+
dim_length = length(dim_array)
113+
data_length = size_data[dim_index]
114+
if dim_length != data_length
115+
error(
116+
"Dimension $dim_name has inconsistent size with provided data ($dim_length != $data_length)",
117+
)
103118
end
119+
end
104120

105-
dims_tuple = tuple(values(dims)...)
121+
# Find boundary conditions for extrapolation
122+
extp_bound_conds = (
123+
_find_extp_bound_cond(dim_name, dim_array) for
124+
(dim_name, dim_array) in dims
125+
)
106126

107-
# TODO: Make this lazy: we should compute the spline the first time we use
108-
# it, not when we create the object
109-
itp = Intp.extrapolate(
110-
Intp.interpolate(dims_tuple, data, Intp.Gridded(Intp.Linear())),
111-
Intp.Throw(),
112-
)
113-
end
127+
dims_tuple = tuple(values(dims)...)
128+
extp_bound_conds_tuple = tuple(extp_bound_conds...)
129+
return Intp.extrapolate(
130+
Intp.interpolate(dims_tuple, data, Intp.Gridded(Intp.Linear())),
131+
extp_bound_conds_tuple,
132+
)
133+
end
134+
135+
"""
136+
_find_extp_bound_cond(dim_name, dim_array)
137+
138+
Find the appropriate boundary condition for the `dim_name` dimension.
139+
"""
140+
function _find_extp_bound_cond(dim_name, dim_array)
141+
min_of_dim, max_of_dim = extrema(dim_array)
142+
dim_size = max_of_dim - min_of_dim
143+
dsize = dim_array[begin + 1] - dim_array[begin]
144+
145+
# If the dimension array span the entire range and is equispaced, then add the
146+
# appropriate boundary condition
147+
# We do not handle the cases when the array is not equispaced
148+
(
149+
conventional_dim_name(dim_name) == "longitude" &&
150+
_isequispaced(dim_array) &&
151+
isapprox(dim_size + dsize, 360.0)
152+
) && return Intp.Periodic()
153+
(
154+
conventional_dim_name(dim_name) == "latitude" &&
155+
_isequispaced(dim_array) &&
156+
isapprox(dim_size + dsize, 180.0)
157+
) && return Intp.Flat()
158+
return Intp.Throw()
159+
end
160+
161+
function OutputVar(attribs, dims, dim_attribs, data)
162+
index2dim = keys(dims) |> collect
163+
dim2index =
164+
Dict([dim_name => index for (index, dim_name) in enumerate(keys(dims))])
165+
166+
# TODO: Make this lazy: we should compute the spline the first time we use
167+
# it, not when we create the object
168+
itp = _make_interpolant(dims, data)
114169

115170
function _maybe_process_key_value(k, v)
116171
k != "units" && return k => v
@@ -710,7 +765,14 @@ end
710765
Interpolate variable `x` onto the given `target_coord` coordinate using
711766
multilinear interpolation.
712767
713-
Extrapolation is now allowed and will throw a `BoundsError`.
768+
Extrapolation is now allowed and will throw a `BoundsError` in most cases.
769+
770+
If any element of the arrays of the dimensions is a Dates.DateTime, then interpolation is
771+
not possible. Interpolations.jl do not support making interpolations for dates. If the
772+
longitudes span the entire range and are equispaced, then a periodic boundary condition is
773+
added for the longitude dimension. If the latitudes span the entire range and are
774+
equispaced, then a flat boundary condition is added for the latitude dimension. In all other
775+
cases, an error is thrown when extrapolating outside of the array of the dimension.
714776
715777
Example
716778
=======

test/test_Var.jl

+57-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import NaNStatistics: nanmean
66
import NCDatasets: NCDataset
77
import OrderedCollections: OrderedDict
88
import Unitful: @u_str
9+
import Dates
910

1011
@testset "General" begin
1112
# Add test for short constructor
@@ -85,6 +86,61 @@ import Unitful: @u_str
8586
)
8687
end
8788

89+
@testset "Interpolant boundary conditions" begin
90+
# Check boundary condtions for lon (equispaced and span), lat (equispaced and span), and
91+
# time
92+
lon = 0.5:1.0:359.5 |> collect
93+
lat = -89.5:1.0:89.5 |> collect
94+
time = 1.0:100 |> collect
95+
data = ones(length(lon), length(lat), length(time))
96+
dims = OrderedDict(["lon" => lon, "lat" => lat, "time" => time])
97+
attribs = Dict("long_name" => "hi")
98+
dim_attribs = OrderedDict([
99+
"long" => Dict("units" => "test_units1"),
100+
"lat" => Dict("units" => "test_units2"),
101+
"time" => Dict("units" => "test_units3"),
102+
])
103+
var = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
104+
@test var.interpolant.et == (Intp.Periodic(), Intp.Flat(), Intp.Throw())
105+
106+
# Not equispaced for lon and lat
107+
lon = 0.5:1.0:359.5 |> collect |> x -> push!(x, 42.0) |> sort
108+
lat = -89.5:1.0:89.5 |> collect |> x -> push!(x, 42.0) |> sort
109+
time = 1.0:100 |> collect
110+
data = ones(length(lon), length(lat), length(time))
111+
dims = OrderedDict(["lon" => lon, "lat" => lat, "time" => time])
112+
var = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
113+
@test var.interpolant.et == (Intp.Throw(), Intp.Throw(), Intp.Throw())
114+
115+
# Does not span entire range for and lat
116+
lon = 0.5:1.0:350.5 |> collect
117+
lat = -89.5:1.0:80.5 |> collect
118+
time = 1.0:100 |> collect
119+
data = ones(length(lon), length(lat), length(time))
120+
dims = OrderedDict(["lon" => lon, "lat" => lat, "time" => time])
121+
var = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
122+
@test var.interpolant.et == (Intp.Throw(), Intp.Throw(), Intp.Throw())
123+
124+
# Dates for the time dimension
125+
lon = 0.5:1.0:359.5 |> collect
126+
lat = -89.5:1.0:89.5 |> collect
127+
time = [
128+
Dates.DateTime(2020, 3, 1, 1, 1),
129+
Dates.DateTime(2020, 3, 1, 1, 2),
130+
Dates.DateTime(2020, 3, 1, 1, 3),
131+
]
132+
data = ones(length(lon), length(lat), length(time))
133+
dims = OrderedDict(["lon" => lon, "lat" => lat, "time" => time])
134+
attribs = Dict("long_name" => "hi")
135+
dim_attribs = OrderedDict([
136+
"long" => Dict("units" => "test_units1"),
137+
"lat" => Dict("units" => "test_units2"),
138+
"time" => Dict("units" => "test_units3"),
139+
])
140+
var = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
141+
@test isnothing(var.interpolant)
142+
end
143+
88144
@testset "empty" begin
89145
dims = OrderedDict{String, Vector{Float64}}()
90146
data = Float64[]
@@ -423,7 +479,7 @@ end
423479

424480
@testset "Interpolation" begin
425481
# 1D interpolation with linear data, should yield correct results
426-
long = -180.0:180.0 |> collect
482+
long = -175.0:175.0 |> collect
427483
data = copy(long)
428484

429485
longvar = ClimaAnalysis.OutputVar(Dict("long" => long), data)

0 commit comments

Comments
 (0)