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

ENH: Add cumulative integration function. #3508

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4151d1c
ENH: Add cumulative integration function.
DWesl May 21, 2024
115415d
STY: Ensure two blank lines between test functions.
DWesl May 21, 2024
9ca6384
BUG,STY: Fix NameError in test
DWesl May 21, 2024
9d9dd0d
TST: Expand cumulative_integrate tests.
DWesl May 23, 2024
8ab2549
STY: Change double quotes to single.
DWesl May 23, 2024
6c1f2a7
BUG: Fix cumulative_integrate axis handling.
DWesl May 23, 2024
27f9fc5
TST,BUG: Fix 2D XArray test for cumulative_integral
DWesl May 23, 2024
08ed4ea
BUG: Fix syntax error in units check
DWesl May 24, 2024
6ae4f66
STY: wrap lines and fix tests.
DWesl May 24, 2024
f5bf47b
TST: Fix test mismatches.
DWesl May 24, 2024
c996909
STY: Sort import ordering.
DWesl May 24, 2024
cf5951d
DOC: Add cumulative_integrate to metpy.calc Mathematical Funtions list.
DWesl May 26, 2024
4af2eb1
ENH: Switch to using scipy.integrate.cumulative_trapezoid for calcula…
DWesl May 27, 2024
5eb45cc
FIX: Pass magnitudes to unit-unaware functions.
DWesl May 27, 2024
609c44a
TST: Fix unit handling.
DWesl May 27, 2024
3b6ac91
TST: Fix unit tests.
DWesl May 27, 2024
c15f4bb
TST,FIX: Fix units in test, the right way this time.
DWesl May 27, 2024
480d5d1
BUG: Add back-up for unitless arguments.
DWesl May 27, 2024
c7e721e
Stop trying to handle unitless NumPy arrays.
DWesl May 27, 2024
25f6bf1
STY: Drop assignment to unused variable
DWesl May 27, 2024
2388547
TST,BUG: Fix pytest.raises decorator
DWesl May 27, 2024
0935278
TST,BUG: Fix type of error in test.
DWesl May 27, 2024
1b5d30b
BUG: Fix exception type to match test.
DWesl May 27, 2024
a5d95e4
DOC: Fix example for cumulative_integrate.
DWesl May 27, 2024
40a7386
DOC,FIX: Fix example output formatting.
DWesl May 27, 2024
a9de575
DOC,FIX: Import XArray for that example.
DWesl May 27, 2024
4d0379b
DOC,FIX: Fix call to cumulative_integrate.
DWesl May 27, 2024
b51682b
Use units for delta, not x, for finding final units.
DWesl May 27, 2024
9b82a80
DOC: Remove example with results differing by OS.
DWesl May 27, 2024
5a663e1
STY: Fix ruff errors.
DWesl May 28, 2024
5d72db3
STY: Fix flake8 errors.
DWesl May 28, 2024
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
1 change: 1 addition & 0 deletions docs/_templates/overrides/metpy.calc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Mathematical Functions
tangential_component
unit_vectors_from_cross_section
vector_derivative
cumulative_integrate


Apparent Temperature
Expand Down
42 changes: 42 additions & 0 deletions src/metpy/calc/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import numpy.ma as ma
from pyproj import CRS, Geod, Proj
from scipy.integrate import cumulative_trapezoid
from scipy.spatial import cKDTree
import xarray as xr

Expand Down Expand Up @@ -1942,3 +1943,44 @@ def _remove_nans(*variables):
for v in variables:
ret.append(v[~mask])
return ret


@exporter.export
@xarray_derivative_wrap
def cumulative_integrate(field, axis=None, x=None, delta=None):
"""Return cumulative integral of field along axis.

Uses trapezoidal rule for integration.

Parameters
----------
field : array-like
Array of values for which to calculate the integral
axis : int or str
The axis along which to integrate. If `field` is an
`np.ndarray` or `pint.Quantity`, must be an integer. Defaults
to zero.
x : array-like, optional
The coordinate values along which to integrate
delta : array-like, optional
Spacing between grid points in `field`.

Examples
--------
>>> cumulative_integrate(units.Quantity(np.arange(5), 'm'), delta=units('1 m'))
<Quantity([0. 0.5 2. 4.5 8. ], 'meter ** 2')>
"""
n, axis, delta = _process_deriv_args(field, axis, x, delta)
take = make_take(n, axis)
right = np.cumsum(delta, axis=axis)
left = np.zeros_like(right[take(slice(1))])
x = concatenate([left, right], axis=axis)
try:
result = cumulative_trapezoid(field.magnitude, x=x.magnitude, axis=axis, initial=0)
return units.Quantity(result, field.units * delta.units)
except AttributeError:
# NumPy arrays without units
raise TypeError(
'cumulative_integrate called with unitless arguments\n'
'Either add units or use scipy.integrate.cumulative_trapezoid'
) from None
53 changes: 52 additions & 1 deletion tests/calc/test_calc_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
import pytest
import xarray as xr

from metpy.calc import (angle_to_direction, find_bounding_indices, find_intersections,
from metpy.calc import (angle_to_direction, cumulative_integrate,

Check failure on line 15 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./tests/calc/test_calc_tools.py:15:1: I001 isort found an import in the wrong position
find_bounding_indices, find_intersections,

Check failure on line 16 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./tests/calc/test_calc_tools.py:16:1: I001 isort found an import in the wrong position
first_derivative, geospatial_gradient, get_layer, get_layer_heights,

Check failure on line 17 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./tests/calc/test_calc_tools.py:17:1: I001 isort found an import in the wrong position
gradient, laplacian, lat_lon_grid_deltas, nearest_intersection_idx,

Check failure on line 18 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./tests/calc/test_calc_tools.py:18:1: I001 isort found an import in the wrong position
parse_angle, pressure_to_height_std, reduce_point_density,

Check failure on line 19 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./tests/calc/test_calc_tools.py:19:1: I001 isort found an import in the wrong position
resample_nn_1d, second_derivative, vector_derivative)

Check failure on line 20 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I001 isort found an import in the wrong position Raw Output: ./tests/calc/test_calc_tools.py:20:1: I001 isort found an import in the wrong position
from metpy.calc.tools import (_delete_masked_points, _get_bound_pressure_height,

Check failure on line 21 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I005 isort found an unexpected missing import Raw Output: ./tests/calc/test_calc_tools.py:21:1: I005 isort found an unexpected missing import

Check failure on line 21 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I005 isort found an unexpected missing import Raw Output: ./tests/calc/test_calc_tools.py:21:1: I005 isort found an unexpected missing import

Check failure on line 21 in tests/calc/test_calc_tools.py

View workflow job for this annotation

GitHub Actions / Flake8

[flake8] reported by reviewdog 🐶 I005 isort found an unexpected missing import Raw Output: ./tests/calc/test_calc_tools.py:21:1: I005 isort found an unexpected missing import
_greater_or_close, _less_or_close, _next_non_masked_element,
_remove_nans, azimuth_range_to_lat_lon, BASE_DEGREE_MULTIPLIER,
DIR_STRS, nominal_lat_lon_grid_deltas, parse_grid_arguments, UND)
Expand Down Expand Up @@ -1557,3 +1558,53 @@
u, v, longitude=lons, latitude=lats, crs=crs, return_only=return_only)

assert len(ddx) == length


def test_cumulative_integrate_numpy():
DWesl marked this conversation as resolved.
Show resolved Hide resolved
"""Test that cumulative_integrate works with numpy arrays."""
field = np.arange(5)
with pytest.raises(
TypeError, match='cumulative_integrate called with unitless arguments'
):
cumulative_integrate(field, delta=1)


def test_cumulative_integrate_pint():
DWesl marked this conversation as resolved.
Show resolved Hide resolved
"""Test that cumulative_integrate works with pint Quantities."""
field = np.arange(6) * units('kg/m^3')
delta = np.array([1, 2, 3, 2, 1]) * units('cm')
integral = cumulative_integrate(field, delta=delta)
assert integral.magnitude == pytest.approx(
np.array([0, 0.5, 3.5, 11, 18, 22.5])
)
assert units.Quantity(1, integral.units).to('dag/m^2').magnitude == 1


def test_cumulative_integrate_xarray():
DWesl marked this conversation as resolved.
Show resolved Hide resolved
"""Test that cumulative_integrate works with XArray DataArrays."""
field = xr.DataArray(
np.arange(10) / 100,
{'x': (('x',), np.arange(100, 1001, 100), {'units': 'hPa'})},
attrs={'units': 'g/kg'}
)
integral = cumulative_integrate(field, axis='x')
assert integral.metpy.magnitude == pytest.approx(
np.array([0, 0.5, 2, 4.5, 8, 12.5, 18, 24.5, 32, 40.5])
)
assert units.Quantity(1, integral.metpy.units).to('hg/(m s^2)').magnitude == 1


def test_cumulative_integrate_xr_2d():
"""Test that cumulative_integrate works with 2D DataArrays."""
arr = np.arange(5)
data_xr = xr.DataArray(
Fixed Show fixed Hide fixed
np.ones((5, 5)),
{'y': ('y', arr, {'units': 'm'}), 'x': ('x', arr, {'units': 'm'})},
('y', 'x'),
'height',
{'units': 'm'}
)
integral = cumulative_integrate(data_xr, axis='x')
assert integral.dims == data_xr.dims
assert integral.coords.keys() == data_xr.coords.keys()
assert units.Quantity(1, integral.metpy.units).to('m^2').magnitude == 1
Loading