Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle land mask #40

Merged
merged 7 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
88 changes: 36 additions & 52 deletions source/aerobulk/flux.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import functools

import aerobulk.aerobulk.mod_aerobulk_wrap_noskin as aeronoskin
import aerobulk.aerobulk.mod_aerobulk_wrap_skin as aeroskin
import numpy as np
import xarray as xr

VALID_ALGOS = ["coare3p0", "coare3p6", "ecmwf", "ncar", "andreas"]
Expand All @@ -13,6 +12,13 @@ def _check_algo(algo, valids):
raise ValueError(f"Algorithm {algo} not valid. Choose from {valids}.")


# Unshrink the data (i.e. put land NaN values back in their correct locations)
def unshrink_arr(shrunk_array, shape, ocean_index):
unshrunk_array = np.full(shape, np.nan)
unshrunk_array[ocean_index] = np.squeeze(shrunk_array)
return unshrunk_array


def noskin_np(
sst,
t_zt,
Expand All @@ -27,6 +33,7 @@ def noskin_np(
):
"""Python wrapper for aerobulk without skin correction.
!ATTENTION If input not provided in correct units, will crash.
!ATTENTION Missing values taken from NaN values in sst field. No input variables may have NaN values that are not reflected in the sst.

Parameters
----------
Expand Down Expand Up @@ -69,16 +76,20 @@ def noskin_np(
evap : numpy.array
evaporation [mm/s] aka [kg/m^2/s] (usually <0, as ocean loses water!)
"""
(
ql,
qh,
taux,
tauy,
evap,
) = aeronoskin.mod_aerobulk_wrapper_noskin.aerobulk_model_noskin(
algo, zt, zu, sst, t_zt, hum_zt, u_zu, v_zu, slp, niter

# Define the land mask from the native SST land mask
ocean_index = np.where(~np.isnan(sst))

# Shrink the input data (i.e. remove all land points)
args_shrunk = tuple(
np.atleast_3d(a[ocean_index]) for a in (sst, t_zt, hum_zt, u_zu, v_zu, slp)
jbusecke marked this conversation as resolved.
Show resolved Hide resolved
)

out_data = aeronoskin.mod_aerobulk_wrapper_noskin.aerobulk_model_noskin(
algo, zt, zu, *args_shrunk, niter
)
return ql, qh, taux, tauy, evap

return tuple(unshrink_arr(o, sst.shape, ocean_index) for o in out_data)


def skin_np(
Expand All @@ -97,6 +108,7 @@ def skin_np(
):
"""Python wrapper for aerobulk with skin correction.
!ATTENTION If input not provided in correct units, will crash.
!ATTENTION Missing values taken from NaN values in sst field. No input variables may have NaN values that are not reflected in the sst.

Parameters
----------
Expand Down Expand Up @@ -145,51 +157,22 @@ def skin_np(
evap : numpy.array
evaporation [mm/s] aka [kg/m^2/s] (usually <0, as ocean loses water!)
"""
(
ql,
qh,
taux,
tauy,
t_s,
evap,
) = aeroskin.mod_aerobulk_wrapper_skin.aerobulk_model_skin(
algo, zt, zu, sst, t_zt, hum_zt, u_zu, v_zu, slp, rad_sw, rad_lw, niter
)
return ql, qh, taux, tauy, t_s, evap


def input_and_output_check(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Check the input shape
test_arg = args[
0
] # assuming that all the input shapes are the same size. TODO: More thorough check
if len(test_arg.dims) < 3:
# TODO promote using expand_dims?
raise NotImplementedError(
f"Aerobulk-Python expects all input fields as 3D arrays. Found {len(test_arg.dims)} dimensions on input."
)
if len(test_arg.dims) > 4:
# TODO iterate over extra dims? Or reshape?
raise NotImplementedError(
f"Aerobulk-Python expects all input fields as 3D arrays. Found {len(test_arg.dims)} dimensions on input."
)
# Define the land mask from the native SST land mask
ocean_index = np.where(~np.isnan(sst))

out_vars = func(*args, **kwargs)

# TODO: Here we could 'un-reshape' or squeeze the output according to the logic above
# Shrink the input data (i.e. remove all land points)
args_shrunk = tuple(
np.atleast_3d(a[ocean_index])
for a in (sst, t_zt, hum_zt, u_zu, v_zu, slp, rad_sw, rad_lw)
)

if any(var.ndim != 3 for var in out_vars):
raise ValueError(
f"f2py returned result of unexpected shape. Got {[var.shape for var in out_vars]}"
)
return out_vars
out_data = aeroskin.mod_aerobulk_wrapper_skin.aerobulk_model_skin(
algo, zt, zu, *args_shrunk, niter
)

return wrapper
return tuple(unshrink_arr(o, sst.shape, ocean_index) for o in out_data)
jbusecke marked this conversation as resolved.
Show resolved Hide resolved


@input_and_output_check
def noskin(
sst, t_zt, hum_zt, u_zu, v_zu, slp=101000.0, algo="coare3p0", zt=2, zu=10, niter=6
):
Expand All @@ -198,6 +181,7 @@ def noskin(
Warnings
--------
!ATTENTION If input not provided in the units shown in [] below the code will crash.
!ATTENTION Missing values taken from NaN values in sst field. No input variables may have NaN values that are not reflected in the sst.

Parameters
----------
Expand Down Expand Up @@ -277,7 +261,6 @@ def noskin(
return out_vars


@input_and_output_check
def skin(
sst,
t_zt,
Expand All @@ -298,6 +281,7 @@ def skin(
Warnings
--------
!ATTENTION If input not provided in the units shown in [] below the code will crash.
!ATTENTION Missing values taken from NaN values in sst field. No input variables may have NaN values that are not reflected in the sst.

Parameters
----------
Expand Down
2 changes: 1 addition & 1 deletion source/fortran/mod_aerobulk_wrap_skin.pyf
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ python module mod_aerobulk_wrap_skin ! in
use mod_aerobulk, only: aerobulk_init,aerobulk_bye
use mod_aerobulk_compute
subroutine aerobulk_model_skin(ni,nj,nt,calgo,zt,zu,sst,t_zt,hum_zt,u_zu,v_zu,slp,rad_sw,rad_lw,niter,ql,qh,tau_x,tau_y,t_s,evap) ! in :mod_aerobulk_wrap_skin:mod_aerobulk_wrap_skin.f90:mod_aerobulk_wrapper_skin
threadsafe
paigem marked this conversation as resolved.
Show resolved Hide resolved

integer, optional,intent(in),check(shape(sst, 0) == ni),depend(sst) :: ni=shape(sst, 0)
integer, optional,intent(in),check(shape(sst, 1) == nj),depend(sst) :: nj=shape(sst, 1)
integer, optional,intent(in),check(shape(sst, 2) == nt),depend(sst) :: nt=shape(sst, 2)
Expand Down
51 changes: 51 additions & 0 deletions tests/create_test_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Dict, Tuple

import numpy as np
import xarray as xr
from numpy.random import default_rng


def create_data(
shape: Tuple[int, ...],
chunks: Dict[str, int] = {},
skin_correction: bool = False,
order: str = "F",
use_xr=True,
paigem marked this conversation as resolved.
Show resolved Hide resolved
land_mask=False,
):
size = shape[0] * shape[1]
shape2d = (shape[0], shape[1])
rng = default_rng()
indices = rng.choice(size, size=int(size * 0.3), replace=False)
multi_indices = np.unravel_index(indices, shape2d)

def _arr(value, chunks, order):
arr = np.full(shape, value, order=order)
if land_mask:
paigem marked this conversation as resolved.
Show resolved Hide resolved
arr[
multi_indices[0], multi_indices[1], :
] = np.nan # add NaNs to mimic land mask
if use_xr:
arr = xr.DataArray(arr)
if chunks:
arr = arr.chunk(chunks)

# adds random noise scaled by a percentage of the value
randomize_factor = 0.001
randomize_range = value * randomize_factor
noise = np.random.rand(*shape) * randomize_range
arr = arr + noise
paigem marked this conversation as resolved.
Show resolved Hide resolved
return arr

sst = _arr(290.0, chunks, order)
t_zt = _arr(280.0, chunks, order)
hum_zt = _arr(0.001, chunks, order)
u_zu = _arr(1.0, chunks, order)
v_zu = _arr(-1.0, chunks, order)
slp = _arr(101000.0, chunks, order)
rad_sw = _arr(0.000001, chunks, order)
rad_lw = _arr(350.0, chunks, order)
if skin_correction:
return sst, t_zt, hum_zt, u_zu, v_zu, rad_sw, rad_lw, slp
else:
return sst, t_zt, hum_zt, u_zu, v_zu, slp
52 changes: 52 additions & 0 deletions tests/test_flux_np.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import numpy as np
import pytest
from aerobulk.flux import noskin_np, skin_np
from create_test_data import create_data

"""Tests for the numpy land_mask wrapper"""


@pytest.mark.parametrize(
"algo, skin_correction",
[
("ecmwf", True),
("ecmwf", False),
("coare3p0", True),
("coare3p0", False),
("coare3p6", True),
("coare3p6", False),
("andreas", False),
("ncar", False),
],
)
def test_land_mask(skin_correction, algo):
shape = (2, 3, 4)
size = shape[0] * shape[1] * shape[2]

if skin_correction:
func = skin_np
else:
func = noskin_np

args = create_data(
shape,
chunks=False,
skin_correction=skin_correction,
use_xr=False,
land_mask=True,
)
out_data = func(*args, algo, 2, 10, 6)

# Check the location of all NaNs is correct
for o in out_data:
np.testing.assert_allclose(np.isnan(args[0]), np.isnan(o))

# Check that values of the unshrunk array are correct
for i in range(size):
index = np.unravel_index(i, shape)
if not np.isnan(out_data[0][index]):
single_inputs = tuple(np.atleast_3d(i[index]) for i in args)

single_outputs = func(*single_inputs, algo, 2, 10, 6)
for so, o in zip(single_outputs, out_data):
assert so == o[index]
53 changes: 19 additions & 34 deletions tests/test_flux_xr.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,11 @@
from typing import Dict, Tuple

import numpy as np
import pytest
import xarray as xr
from aerobulk import noskin, skin
from create_test_data import create_data
paigem marked this conversation as resolved.
Show resolved Hide resolved

"""Tests for the xarray wrapper"""


def create_data(
shape: Tuple[int, ...],
chunks: Dict[str, int] = {},
skin_correction: bool = False,
order: str = "F",
):
def _arr(value, chunks, order):
arr = xr.DataArray(np.full(shape, value, order=order))

# adds random noise scaled by a percentage of the value
randomize_factor = 0.001
randomize_range = value * randomize_factor
arr = arr + np.random.rand(*shape) + randomize_range
if chunks:
arr = arr.chunk(chunks)
return arr

sst = _arr(290.0, chunks, order)
t_zt = _arr(280.0, chunks, order)
hum_zt = _arr(0.001, chunks, order)
u_zu = _arr(1.0, chunks, order)
v_zu = _arr(-1.0, chunks, order)
slp = _arr(101000.0, chunks, order)
rad_sw = _arr(0.000001, chunks, order)
rad_lw = _arr(350, chunks, order)
if skin_correction:
return sst, t_zt, hum_zt, u_zu, v_zu, rad_sw, rad_lw, slp
else:
return sst, t_zt, hum_zt, u_zu, v_zu, slp


@pytest.mark.parametrize("algo", ["wrong"])
def test_algo_error_noskin(algo):
shape = (1, 1, 1)
Expand Down Expand Up @@ -99,3 +66,21 @@ def test_transpose_invariance_noskin(self, chunks, skin_correction, order):
ii.transpose("dim_0", "dim_1", "dim_2"),
iii.transpose("dim_0", "dim_1", "dim_2"),
)


@pytest.mark.parametrize("skin_correction", [True, False])
def test_all_input_array_sizes_valid(skin_correction):
shapes = (
(3, 4),
(2, 3, 4),
(2, 3, 4, 5),
) # create_data() only allows for inputs of 2 or more dimensions
data = (create_data(s, skin_correction=skin_correction) for s in shapes)
if skin_correction:
func = skin
else:
func = noskin
tuple(func(*d, "coare3p0", 2, 10, 6) for d in data)
assert (
1 == 1
) # This line is always true, but verifies that the above line doesn't crash the Fortran code
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure I get this. If the fortran code crashes, this will never be called? I would remove it. We just need to make sure that we check the CI actions carefully.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok cool, I was under the impression we needed some sort of "check" (eg an assert statement). I can remove the unnecessary assert line.

Comment on lines +72 to +86
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
def test_all_input_array_sizes_valid(skin_correction):
shapes = (
(3, 4),
(2, 3, 4),
(2, 3, 4, 5),
) # create_data() only allows for inputs of 2 or more dimensions
data = (create_data(s, skin_correction=skin_correction) for s in shapes)
if skin_correction:
func = skin
else:
func = noskin
tuple(func(*d, "coare3p0", 2, 10, 6) for d in data)
assert (
1 == 1
) # This line is always true, but verifies that the above line doesn't crash the Fortran code
@pytest.mark.parametrize('shape', [(3, 4), (2, 3, 4), (2, 3, 4, 5),])
def test_all_input_array_sizes_valid(skin_correction, shape):
# create_data() only allows for inputs of 2 or more dimensions
data = create_data(shape, skin_correction=skin_correction)
if skin_correction:
func = skin
else:
func = noskin
func(*data, "coare3p0", 2, 10, 6)

Copy link
Contributor

Choose a reason for hiding this comment

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

What I did here is factor out the shape as a parameterized input, so that each shape gets its own test. This enables a more fine grained control (e.g. if for some reason the 4d case fails, but the others pass we will see this immediately in the test report).