Unidata/MetPy

View on GitHub
tests/calc/test_cross_sections.py

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) 2018 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
"""Test the `cross_sections` module."""

import numpy as np
import pytest
import xarray as xr

from metpy.calc import (absolute_momentum, cross_section_components, normal_component,
                        tangential_component, unit_vectors_from_cross_section)
from metpy.calc.cross_sections import distances_from_cross_section, latitude_from_cross_section
from metpy.cbook import get_test_data
from metpy.interpolate import cross_section
from metpy.testing import assert_array_almost_equal, assert_xarray_allclose, needs_cartopy
from metpy.units import units


@pytest.fixture()
@needs_cartopy
def test_cross_lonlat():
    """Return cross section on a lon/lat grid with no time coordinate for use in tests."""
    data_u = np.linspace(-40, 40, 5 * 6 * 7).reshape((5, 6, 7)) * units.knots
    data_v = np.linspace(40, -40, 5 * 6 * 7).reshape((5, 6, 7)) * units.knots
    ds = xr.Dataset(
        {
            'u_wind': (['isobaric', 'lat', 'lon'], data_u),
            'v_wind': (['isobaric', 'lat', 'lon'], data_v)
        },
        coords={
            'isobaric': xr.DataArray(
                np.linspace(1000, 500, 5),
                name='isobaric',
                dims=['isobaric'],
                attrs={'units': 'hPa'}
            ),
            'lat': xr.DataArray(
                np.linspace(30, 45, 6),
                name='lat',
                dims=['lat'],
                attrs={'units': 'degrees_north'}
            ),
            'lon': xr.DataArray(
                np.linspace(255, 275, 7),
                name='lon',
                dims=['lon'],
                attrs={'units': 'degrees_east'}
            )
        }
    )

    start, end = (30.5, 255.5), (44.5, 274.5)
    return cross_section(ds.metpy.parse_cf(), start, end, steps=7, interp_type='nearest')


@pytest.fixture()
@needs_cartopy
def test_cross_xy():
    """Return cross section on a x/y grid with a time coordinate for use in tests."""
    data_u = np.linspace(-25, 25, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units('m/s')
    data_v = np.linspace(25, -25, 5 * 6 * 7).reshape((1, 5, 6, 7)) * units('m/s')
    ds = xr.Dataset(
        {
            'u_wind': (['time', 'isobaric', 'y', 'x'], data_u),
            'v_wind': (['time', 'isobaric', 'y', 'x'], data_v),
            'lambert_conformal': ([], '')
        },
        coords={
            'time': xr.DataArray(
                np.array(['2018-07-01T00:00'], dtype='datetime64[ns]'),
                name='time',
                dims=['time']
            ),
            'isobaric': xr.DataArray(
                np.linspace(1000, 500, 5),
                name='isobaric',
                dims=['isobaric'],
                attrs={'units': 'hPa'}
            ),
            'y': xr.DataArray(
                np.linspace(-1500, 0, 6),
                name='y',
                dims=['y'],
                attrs={'units': 'km'}
            ),
            'x': xr.DataArray(
                np.linspace(-500, 3000, 7),
                name='x',
                dims=['x'],
                attrs={'units': 'km'}
            )
        }
    )
    ds['u_wind'].attrs = ds['v_wind'].attrs = {
        'grid_mapping': 'lambert_conformal'
    }
    ds['lambert_conformal'].attrs = {
        'grid_mapping_name': 'lambert_conformal_conic',
        'standard_parallel': 50.0,
        'longitude_of_central_meridian': -107.0,
        'latitude_of_projection_origin': 50.0,
        'earth_shape': 'spherical',
        'earth_radius': 6367470.21484375
    }

    start, end = ((36.46, -112.45), (42.95, -68.74))
    return cross_section(ds.metpy.parse_cf(), start, end, steps=7)


def test_distances_from_cross_section_given_lonlat(test_cross_lonlat):
    """Test distances from cross section with lat/lon grid."""
    x, y = distances_from_cross_section(test_cross_lonlat['u_wind'])

    true_x_values = np.array([-0., 252585.3108187, 505170.6216374, 757755.93245611,
                              1010341.24327481, 1262926.55409352, 1515511.86491222])
    true_y_values = np.array([-0., 283412.80349716, 566825.60699432, 850238.41049148,
                              1133651.21398864, 1417064.0174858, 1700476.82098296])
    index = xr.DataArray(range(7), name='index', dims=['index'])
    true_x = xr.DataArray(
        true_x_values * units.meters,
        coords={
            'metpy_crs': test_cross_lonlat['metpy_crs'],
            'lat': test_cross_lonlat['lat'],
            'lon': test_cross_lonlat['lon'],
            'index': index,
        },
        dims=['index']
    )
    true_y = xr.DataArray(
        true_y_values * units.meters,
        coords={
            'metpy_crs': test_cross_lonlat['metpy_crs'],
            'lat': test_cross_lonlat['lat'],
            'lon': test_cross_lonlat['lon'],
            'index': index,
        },
        dims=['index']
    )
    assert_xarray_allclose(x, true_x)
    assert_xarray_allclose(y, true_y)


def test_distances_from_cross_section_given_xy(test_cross_xy):
    """Test distances from cross section with x/y grid."""
    x, y = distances_from_cross_section(test_cross_xy['u_wind'])
    xr.testing.assert_identical(test_cross_xy['x'], x)
    xr.testing.assert_identical(test_cross_xy['y'], y)


def test_distances_from_cross_section_given_bad_coords(test_cross_xy):
    """Ensure an AttributeError is raised when the cross section lacks need coordinates."""
    with pytest.raises(AttributeError):
        distances_from_cross_section(test_cross_xy['u_wind'].drop_vars('x'))


def test_latitude_from_cross_section_given_lat(test_cross_lonlat):
    """Test latitude from cross section with latitude given."""
    latitude = latitude_from_cross_section(test_cross_lonlat['v_wind'])
    xr.testing.assert_identical(test_cross_lonlat['lat'], latitude)


def test_latitude_from_cross_section_given_y(test_cross_xy):
    """Test latitude from cross section with y given."""
    latitude = latitude_from_cross_section(test_cross_xy['v_wind'])
    true_latitude_values = np.array([36.46, 38.64829115, 40.44833152, 41.81277354, 42.7011178,
                                     43.0845549, 42.95])
    index = xr.DataArray(range(7), name='index', dims=['index'])
    true_latitude = xr.DataArray(
        true_latitude_values * units.degrees_north,
        coords={
            'metpy_crs': test_cross_xy['metpy_crs'],
            'y': test_cross_xy['y'],
            'x': test_cross_xy['x'],
            'index': index,
        },
        dims=['index']
    )
    assert_xarray_allclose(latitude, true_latitude)


def test_unit_vectors_from_cross_section_given_lonlat(test_cross_lonlat):
    """Test unit vector calculation from cross section with lat/lon grid."""
    unit_tangent, unit_normal = unit_vectors_from_cross_section(test_cross_lonlat['u_wind'])
    true_unit_tangent = np.array([[0.66533859, 0.66533859, 0.66533859, 0.66533859, 0.66533859,
                                   0.66533859, 0.66533859],
                                  [0.74654173, 0.74654173, 0.74654173, 0.74654173, 0.74654173,
                                   0.74654173, 0.74654173]])
    true_unit_normal = np.array([[-0.74654173, -0.74654173, -0.74654173, -0.74654173,
                                  -0.74654173, -0.74654173, -0.74654173],
                                 [0.66533859, 0.66533859, 0.66533859, 0.66533859,
                                  0.66533859, 0.66533859, 0.66533859]])
    assert_array_almost_equal(true_unit_tangent, unit_tangent, 7)
    assert_array_almost_equal(true_unit_normal, unit_normal, 7)


def test_unit_vectors_from_cross_section_given_xy(test_cross_xy):
    """Test unit vector calculation from cross section with x/y grid."""
    unit_tangent, unit_normal = unit_vectors_from_cross_section(test_cross_xy['u_wind'])
    true_unit_tangent = np.array([[0.93567585, 0.929688, 0.92380315, 0.91844706, 0.91349795,
                                   0.90875771, 0.90400673],
                                 [0.35286074, 0.36834796, 0.3828678, 0.39554392, 0.40684333,
                                  0.41732413, 0.42751822]])
    true_unit_normal = np.array([[-0.35286074, -0.36834796, -0.3828678, -0.39554392,
                                  -0.40684333, -0.41732413, -0.42751822],
                                 [0.93567585, 0.929688, 0.92380315, 0.91844706, 0.91349795,
                                  0.90875771, 0.90400673]])
    assert_array_almost_equal(true_unit_tangent, unit_tangent, 7)
    assert_array_almost_equal(true_unit_normal, unit_normal, 7)


def test_cross_section_components(test_cross_lonlat):
    """Test getting cross section components of a 2D vector field."""
    tang_wind, norm_wind = cross_section_components(test_cross_lonlat['u_wind'],
                                                    test_cross_lonlat['v_wind'])
    true_tang_wind_values = np.array([[3.24812563, 2.9994653, 2.75080496, 2.50214463,
                                       2.47106208, 2.22240175, 1.97374141],
                                      [1.94265887, 1.69399854, 1.4453382, 1.19667786,
                                       1.16559532, 0.91693499, 0.66827465],
                                      [0.63719211, 0.38853177, 0.13987144, -0.1087889,
                                       -0.13987144, -0.38853177, -0.63719211],
                                      [-0.66827465, -0.91693499, -1.16559532, -1.41425566,
                                       -1.4453382, -1.69399854, -1.94265887],
                                      [-1.97374141, -2.22240175, -2.47106208, -2.71972242,
                                       -2.75080496, -2.9994653, -3.24812563]])
    true_norm_wind_values = np.array([[56.47521297, 52.15175169, 47.82829041, 43.50482913,
                                       42.96439647, 38.64093519, 34.31747391],
                                      [33.77704125, 29.45357997, 25.13011869, 20.80665741,
                                       20.26622475, 15.94276347, 11.61930219],
                                      [11.07886953, 6.75540825, 2.43194697, -1.89151431,
                                       -2.43194697, -6.75540825, -11.07886953],
                                      [-11.61930219, -15.94276347, -20.26622475, -24.58968603,
                                       -25.13011869, -29.45357997, -33.77704125],
                                      [-34.31747391, -38.64093519, -42.96439647, -47.28785775,
                                       -47.82829041, -52.15175169, -56.47521297]])
    true_tang_wind = xr.DataArray(true_tang_wind_values * units.knots,
                                  coords=test_cross_lonlat['u_wind'].coords,
                                  dims=test_cross_lonlat['u_wind'].dims,
                                  attrs=test_cross_lonlat['u_wind'].attrs)
    true_norm_wind = xr.DataArray(true_norm_wind_values * units.knots,
                                  coords=test_cross_lonlat['u_wind'].coords,
                                  dims=test_cross_lonlat['u_wind'].dims,
                                  attrs=test_cross_lonlat['u_wind'].attrs)
    assert_xarray_allclose(tang_wind, true_tang_wind)
    assert_xarray_allclose(norm_wind, true_norm_wind)


def test_tangential_component(test_cross_xy):
    """Test getting cross section tangential component of a 2D vector field."""
    tang_wind = tangential_component(test_cross_xy['u_wind'], test_cross_xy['v_wind'])
    true_tang_wind_values = np.array([[[-14.56982141, -13.17102075, -11.83790134,
                                        -10.59675064, -9.42888813, -8.31533355, -7.2410326],
                                       [-8.71378435, -7.53076196, -6.40266576, -5.34269988,
                                        -4.33810002, -3.37748418, -2.45334901],
                                       [-2.85774728, -1.89050316, -0.96743019, -0.08864912,
                                        0.7526881, 1.5603652, 2.33433459],
                                       [2.99828978, 3.74975563, 4.46780539, 5.16540164,
                                        5.84347621, 6.49821458, 7.12201819],
                                       [8.85432685, 9.39001443, 9.90304096, 10.41945241,
                                        10.93426433, 11.43606396, 11.90970179]]])
    true_tang_wind = xr.DataArray(true_tang_wind_values * units('m/s'),
                                  coords=test_cross_xy['u_wind'].coords,
                                  dims=test_cross_xy['u_wind'].dims,
                                  attrs=test_cross_xy['u_wind'].attrs)
    assert_xarray_allclose(tang_wind, true_tang_wind)


def test_normal_component(test_cross_xy):
    """Test getting cross section normal component of a 2D vector field."""
    norm_wind = normal_component(test_cross_xy['u_wind'], test_cross_xy['v_wind'])
    true_norm_wind_values = np.array([[[32.21218429, 30.45650997, 28.59536112, 26.62832466,
                                        24.57166983, 22.43805311, 20.2347284],
                                       [19.26516594, 17.41404337, 15.46613157, 13.42554447,
                                        11.30508283, 9.11378585, 6.85576955],
                                       [6.3181476, 4.37157677, 2.33690202, 0.22276428,
                                        -1.96150417, -4.2104814, -6.52318931],
                                       [-6.62887075, -8.67088982, -10.79232752, -12.98001592,
                                        -15.22809117, -17.53474865, -19.90214816],
                                       [-19.5758891, -21.71335642, -23.92155707, -26.18279611,
                                        -28.49467817, -30.8590159, -33.28110701]]])
    true_norm_wind = xr.DataArray(true_norm_wind_values * units('m/s'),
                                  coords=test_cross_xy['u_wind'].coords,
                                  dims=test_cross_xy['u_wind'].dims,
                                  attrs=test_cross_xy['u_wind'].attrs)
    assert_xarray_allclose(norm_wind, true_norm_wind)


def test_absolute_momentum_given_lonlat(test_cross_lonlat):
    """Test absolute momentum calculation."""
    momentum = absolute_momentum(test_cross_lonlat['u_wind'], test_cross_lonlat['v_wind'])
    true_momentum_values = np.array([[29.05335956, 57.00676169, 88.89733786, 124.37969813,
                                      165.02883664, 206.55775948, 250.49678829],
                                     [17.37641122, 45.32981335, 77.22038952, 112.70274979,
                                      153.3518883, 194.88081114, 238.81983995],
                                     [5.69946288, 33.65286501, 65.54344118, 101.02580145,
                                      141.67493996, 183.2038628, 227.14289161],
                                     [-5.97748546, 21.97591667, 53.86649284, 89.34885311,
                                      129.99799162, 171.52691446, 215.46594327],
                                     [-17.6544338, 10.29896833, 42.1895445, 77.67190477,
                                      118.32104328, 159.84996612, 203.78899492]])

    true_momentum = xr.DataArray(true_momentum_values * units('m/s'),
                                 coords=test_cross_lonlat['u_wind'].coords,
                                 dims=test_cross_lonlat['u_wind'].dims)
    assert_xarray_allclose(momentum, true_momentum)


def test_absolute_momentum_given_xy(test_cross_xy):
    """Test absolute momentum calculation."""
    momentum = absolute_momentum(test_cross_xy['u_wind'], test_cross_xy['v_wind'])
    true_momentum_values = np.array([[[169.22222693, 146.36354006, 145.75559124, 171.8710635,
                                       215.04876817, 265.73797007, 318.34138347],
                                      [156.27520858, 133.32107346, 132.62636169, 158.66828331,
                                       201.78218117, 252.41370282, 304.96242462],
                                      [143.32819023, 120.27860686, 119.49713214, 145.46550311,
                                       188.51559418, 239.08943557, 291.58346576],
                                      [130.38117188, 107.23614026, 106.36790259, 132.26272292,
                                       175.24900718, 225.76516831, 278.20450691],
                                      [117.43415353, 94.19367366, 93.23867305, 119.05994273,
                                       161.98242018, 212.44090106, 264.82554806]]])
    true_momentum = xr.DataArray(true_momentum_values * units('m/s'),
                                 coords=test_cross_xy['u_wind'].coords,
                                 dims=test_cross_xy['u_wind'].dims)
    assert_xarray_allclose(momentum, true_momentum)


def test_absolute_momentum_xarray_units_attr():
    """Test absolute momentum when `u` and `v` are DataArrays with a `units` attribute."""
    data = xr.open_dataset(get_test_data('narr_example.nc', False))
    data = data.metpy.parse_cf().squeeze()

    start = (37.0, -105.0)
    end = (35.5, -65.0)
    cross = cross_section(data, start, end)

    u = cross['u_wind'][0].sel(index=slice(0, 2))
    v = cross['v_wind'][0].sel(index=slice(0, 2))

    momentum = absolute_momentum(u, v)
    true_momentum_values = np.array([137.46164031, 134.11450232, 133.85196023])
    true_momentum = xr.DataArray(units.Quantity(true_momentum_values, 'm/s'),
                                 coords=u.coords)

    assert_xarray_allclose(momentum, true_momentum)