Skip to content

Commit 20ada8c

Browse files
committed
Add Var.apply_landmask and Var.apply_oceanmask
1 parent 4250e78 commit 20ada8c

File tree

8 files changed

+198
-1
lines changed

8 files changed

+198
-1
lines changed

docs/src/api.md

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ Var.squared_error
6868
Var.global_mse
6969
Var.global_rmse
7070
Var.shift_to_start_of_previous_month
71+
Var.apply_landmask
72+
Var.apply_oceanmask
7173
```
7274

7375
## Leaderboard

docs/src/howdoi.md

+12
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,15 @@ OrderedDict{String, Vector{Float64}} with 2 entries:
152152
"lon" => [0.0, 1.0]
153153
"latitude" => [0.0, 1.0, 2.0]
154154
```
155+
156+
## How do I apply a land or sea mask to a `OutputVar`?
157+
158+
You can use `apply_landmask` or `apply_oceanmask` to mask out the land or ocean,
159+
respectively, in a `OutputVar`. The result of `apply_landmask(var)` is data of `var`, where
160+
any coordinate corresponding to land is zero. Similarly, the result of `apply_oceanmask(var)` is
161+
data of `var`, where any coordinate corresponding to ocean is zero.
162+
163+
```@julia masks
164+
var_no_land = ClimaAnalysis.apply_landmask(var)
165+
var_no_ocean = ClimaAnalysis.apply_oceanmask(var)
166+
```

masks/land_mask16.nc

38.7 KB
Binary file not shown.

masks/ocean_mask16.nc

38.8 KB
Binary file not shown.

src/Var.jl

+76-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ export OutputVar,
5353
global_mse,
5454
global_rmse,
5555
set_units,
56-
shift_to_start_of_previous_month
56+
shift_to_start_of_previous_month,
57+
apply_landmask,
58+
apply_oceanmask
5759

5860
"""
5961
Representing an output variable
@@ -1447,6 +1449,78 @@ function shift_to_start_of_previous_month(var::OutputVar)
14471449
return OutputVar(ret_attribs, ret_dims, ret_dim_attributes, ret_data)
14481450
end
14491451

1452+
"""
1453+
apply_landmask(var::OutputVar)
1454+
1455+
Apply a land mask to `var` by zeroing any data whose coordinates are located on land.
1456+
"""
1457+
function apply_landmask(var::OutputVar)
1458+
_apply_lonlat_mask(var, LAND_MASK)
1459+
end
1460+
1461+
"""
1462+
apply_oceanmask(var::OutputVar)
1463+
1464+
Apply an ocean mask to `var` by zeroing any data whose coordinates are in the ocean.
1465+
"""
1466+
function apply_oceanmask(var::OutputVar)
1467+
_apply_lonlat_mask(var, OCEAN_MASK)
1468+
end
1469+
1470+
"""
1471+
_apply_lonlat_mask(var, mask::AbstractString)
1472+
1473+
Apply a mask using the NCDataset found at `mask`.
1474+
1475+
The dimensions of the mask should only contain longitude and latitude and are in that order.
1476+
"""
1477+
function _apply_lonlat_mask(var, mask::AbstractString)
1478+
# Check if lon and lat exist
1479+
has_longitude(var) ||
1480+
error("Longitude does not exist as a dimension in var")
1481+
has_latitude(var) || error("Latitude does not exist as a dimension in var")
1482+
1483+
# Create OutputVar using mask
1484+
mask_var = OutputVar(mask)
1485+
1486+
# Resample so that the land mask match up with the grid of var
1487+
# Round because linear resampling is done and we want the mask to be only ones and zeros
1488+
land_mask =
1489+
[
1490+
mask_var(pt) for pt in Base.product(
1491+
var.dims[longitude_name(var)],
1492+
var.dims[latitude_name(var)],
1493+
)
1494+
] .|> round
1495+
1496+
# Reshape data for broadcasting
1497+
lon_idx = var.dim2index[longitude_name(var)]
1498+
lat_idx = var.dim2index[latitude_name(var)]
1499+
lon_length = var.dims[longitude_name(mask_var)] |> length
1500+
lat_length = var.dims[latitude_name(mask_var)] |> length
1501+
if lon_idx > lat_idx
1502+
land_mask = transpose(land_mask)
1503+
end
1504+
size_to_reshape = (
1505+
if i == lon_idx
1506+
lon_length
1507+
elseif i == lat_idx
1508+
lat_length
1509+
else
1510+
1
1511+
end for i in 1:ndims(var.data)
1512+
)
1513+
1514+
# Mask data
1515+
land_mask = reshape(land_mask, size_to_reshape...)
1516+
masked_data = var.data .* land_mask
1517+
1518+
# Remake OutputVar with new data
1519+
ret_attribs = deepcopy(var.attributes)
1520+
ret_dims = deepcopy(var.dims)
1521+
ret_dim_attributes = deepcopy(var.dim_attributes)
1522+
return OutputVar(ret_attribs, ret_dims, ret_dim_attributes, masked_data)
1523+
end
14501524

14511525
"""
14521526
overload_binary_op(op)
@@ -1545,5 +1619,6 @@ end
15451619
@overload_binary_op (/)
15461620

15471621
include("outvar_dimensions.jl")
1622+
include("constants.jl")
15481623

15491624
end

src/constants.jl

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
The variable LAND_MASK is a filepath to a NCDataset whose dimensions are latitude and
3+
longitude and data are ones and zeros. The zeros indicate land and ones indicate everything
4+
else.
5+
6+
The land mask is generated from the ETOPO2022 dataset at 60 arc-second resolutions
7+
(https://www.ncei.noaa.gov/products/etopo-global-relief-model). The dimensions latitude and
8+
longitude are sampled every 16th element to thin out the dimensions. Any value for z greater
9+
than 0 is assigned 0.0 and all other values for z is assigned 1.0.
10+
"""
11+
LAND_MASK = joinpath(@__DIR__, "..", "masks", "land_mask16.nc")
12+
13+
"""
14+
The variable OCEAN_MASK is a filepath to a NCDataset whose dimensions are latitude and
15+
longitude and data are 1s and 0s. The zeros indicate ocean and ones indicate everything
16+
else.
17+
18+
The land mask is generated from the ETOPO2022 dataset at 60 arc-second resolutions
19+
(https://www.ncei.noaa.gov/products/etopo-global-relief-model). The dimensions latitude and
20+
longitude are sampled every 16th element to thin out the dimensions. Any value for z less
21+
than or equal to 0 is assigned 0.0 and all other values for z is assigned 1.0.
22+
"""
23+
OCEAN_MASK = joinpath(@__DIR__, "..", "masks", "ocean_mask16.nc")

test/test_GeoMakieExt.jl

+25
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,29 @@ using OrderedCollections
136136
)
137137
output_name = joinpath(tmp_dir, "plot_bias_kwargs.png")
138138
Makie.save(output_name, fig7)
139+
140+
# Test plots with apply_landmask and apply_oceanmask
141+
fig8 = Makie.Figure()
142+
143+
lon = collect(range(-179.5, 179.5, 360))
144+
lat = collect(range(-89.5, 89.5, 180))
145+
data = ones(length(lon), length(lat))
146+
dims = OrderedDict(["lon" => lon, "lat" => lat])
147+
attribs =
148+
Dict("long_name" => "idk", "short_name" => "short", "units" => "kg")
149+
dim_attribs = OrderedDict([
150+
"lon" => Dict("units" => "deg"),
151+
"lat" => Dict("units" => "deg"),
152+
])
153+
var = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
154+
land_var = var |> ClimaAnalysis.apply_landmask
155+
ClimaAnalysis.Visualize.heatmap2D_on_globe!(fig8, land_var)
156+
output_name = joinpath(tmp_dir, "plot_apply_land_mask.png")
157+
Makie.save(output_name, fig8)
158+
159+
fig9 = Makie.Figure()
160+
ocean_var = var |> ClimaAnalysis.apply_oceanmask
161+
ClimaAnalysis.Visualize.heatmap2D_on_globe!(fig9, ocean_var)
162+
output_name = joinpath(tmp_dir, "plot_apply_ocean_mask.png")
163+
Makie.save(output_name, fig9)
139164
end

test/test_Var.jl

+60
Original file line numberDiff line numberDiff line change
@@ -1506,3 +1506,63 @@ end
15061506
var_min,
15071507
)
15081508
end
1509+
1510+
@testset "Land and ocean masks" begin
1511+
# Order of dimensions should not matter
1512+
lat = collect(range(-89.5, 89.5, 180))
1513+
lon = collect(range(-179.5, 179.5, 360))
1514+
data = ones(length(lat), length(lon))
1515+
dims = OrderedDict(["lat" => lat, "lon" => lon])
1516+
attribs = Dict("long_name" => "hi")
1517+
dim_attribs = OrderedDict([
1518+
"lat" => Dict("units" => "deg"),
1519+
"lon" => Dict("units" => "deg"),
1520+
])
1521+
var_latlon = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
1522+
1523+
lon = collect(range(-179.5, 179.5, 360))
1524+
lat = collect(range(-89.5, 89.5, 180))
1525+
data = ones(length(lon), length(lat))
1526+
dims = OrderedDict(["lon" => lon, "lat" => lat])
1527+
attribs = Dict("long_name" => "hi")
1528+
dim_attribs = OrderedDict([
1529+
"lon" => Dict("units" => "deg"),
1530+
"lat" => Dict("units" => "deg"),
1531+
])
1532+
var_lonlat = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
1533+
1534+
land_var_lonlat = ClimaAnalysis.apply_landmask(var_lonlat)
1535+
ocean_var_lonlat = ClimaAnalysis.apply_oceanmask(var_lonlat)
1536+
land_var_latlon = ClimaAnalysis.apply_landmask(var_latlon)
1537+
ocean_var_latlon = ClimaAnalysis.apply_oceanmask(var_latlon)
1538+
@test land_var_lonlat.data == land_var_latlon.data'
1539+
@test ocean_var_lonlat.data == ocean_var_latlon.data'
1540+
1541+
# Testing with another dimension
1542+
lat = collect(range(-89.5, 89.5, 180))
1543+
times = collect(range(0.0, 100, 2 * 180))
1544+
lon = collect(range(-179.5, 179.5, 360))
1545+
data = ones(length(lat), length(times), length(lon))
1546+
dims = OrderedDict(["lat" => lat, "time" => times, "lon" => lon])
1547+
attribs = Dict("long_name" => "hi")
1548+
dim_attribs = OrderedDict([
1549+
"lat" => Dict("units" => "deg"),
1550+
"time" => Dict("units" => "s"),
1551+
"lon" => Dict("units" => "deg"),
1552+
])
1553+
var = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
1554+
land_var = ClimaAnalysis.apply_landmask(var) |> ClimaAnalysis.average_time
1555+
ocean_var = ClimaAnalysis.apply_oceanmask(var) |> ClimaAnalysis.average_time
1556+
@test land_var.data |> transpose == land_var_lonlat.data
1557+
@test ocean_var.data |> transpose == ocean_var_lonlat.data
1558+
1559+
# Test error handling
1560+
times = collect(range(0.0, 100, 2 * 180))
1561+
data = ones(length(times))
1562+
dims = OrderedDict(["time" => times])
1563+
attribs = Dict("long_name" => "hi")
1564+
dim_attribs = OrderedDict(["time" => Dict("units" => "s")])
1565+
var = ClimaAnalysis.OutputVar(attribs, dims, dim_attribs, data)
1566+
@test_throws ErrorException ClimaAnalysis.apply_landmask(var)
1567+
@test_throws ErrorException ClimaAnalysis.apply_oceanmask(var)
1568+
end

0 commit comments

Comments
 (0)