Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
1490c16
Add support for cross
Illviljan May 23, 2021
03db734
Update test_computation.py
Illviljan May 23, 2021
c824e36
Update computation.py
Illviljan May 23, 2021
7ce39c7
Update computation.py
Illviljan May 23, 2021
916e661
Update test_computation.py
Illviljan May 23, 2021
654ad60
Update test_computation.py
Illviljan May 23, 2021
a6ac578
Update test_computation.py
Illviljan May 23, 2021
e0c1fac
add more tests
Illviljan May 23, 2021
7aebae7
Update xarray/core/computation.py
Illviljan May 23, 2021
b85e236
Merge branch 'master' into Illviljan-cross
Illviljan May 23, 2021
2b54a42
Merge branch 'Illviljan-cross' of https://github.com/Illviljan/xarray…
Illviljan May 23, 2021
be7b2c2
spatial_dim to dim
Illviljan May 23, 2021
4448006
Update computation.py
Illviljan May 23, 2021
af8b09c
use pad instead of concat
Illviljan May 23, 2021
a135e05
copy paste np.cross intro
Illviljan May 23, 2021
6f17b9b
Get last dim for each array, which is more inline with np.cross
Illviljan May 23, 2021
1fadb5f
examples in docs
Illviljan May 23, 2021
57239a4
Update computation.py
Illviljan May 23, 2021
265ef82
more doc examples
Illviljan May 23, 2021
dd60562
single dim required, tranpose after apply_ufunc
Illviljan May 24, 2021
a20cb86
add dims to tests
Illviljan May 24, 2021
7ce9315
Update computation.py
Illviljan May 24, 2021
d5a0ea8
reduce code
Illviljan May 25, 2021
ef94fa4
support xr.Variable
Illviljan May 25, 2021
1a85147
Update computation.py
Illviljan May 25, 2021
2ce3dbe
Update computation.py
Illviljan May 25, 2021
53c84c2
reduce code
Illviljan May 25, 2021
dded720
docstring explanations
Illviljan May 25, 2021
7058166
Use same terms
Illviljan May 25, 2021
cb57a55
docstring formatting
Illviljan May 25, 2021
e69ca81
reduce code
Illviljan May 25, 2021
4b2fc72
add tests for dask
Illviljan May 25, 2021
afe572d
simplify check, align used variables
Illviljan May 26, 2021
e137350
trim down tests
Illviljan May 26, 2021
1a26324
Update computation.py
Illviljan May 26, 2021
531a98b
simplify code
Illviljan May 27, 2021
2146406
Add type hints
Illviljan May 28, 2021
0940472
less type hints
Illviljan May 28, 2021
a7cc565
Update computation.py
Illviljan May 28, 2021
1d1f205
undo type hints
Illviljan May 28, 2021
9af7091
Update computation.py
Illviljan May 28, 2021
14decb3
Add support for datasets
Illviljan May 30, 2021
6f73c32
determine dtype with np.result_type
Illviljan Jun 2, 2021
72330ce
test datasets, daskify the inputs not the results
Illviljan Jun 6, 2021
bce2f3e
rechunk padded values, handle 1 sized datasets
Illviljan Jun 6, 2021
1636d25
expand only unique dims, squeeze out dims in tests
Illviljan Jun 6, 2021
b5b97a0
rechunk along the dim
Illviljan Jun 6, 2021
f77780f
Merge branch 'master' into Illviljan-cross
Illviljan Jun 7, 2021
02364ca
Attempt typing again
Illviljan Jun 17, 2021
e842c75
Merge branch 'master' into Illviljan-cross
Illviljan Jun 17, 2021
ed44400
Update __init__.py
Illviljan Jun 17, 2021
4fe9737
Update computation.py
Illviljan Jun 17, 2021
ec05780
Update computation.py
Illviljan Jun 17, 2021
36c5956
test fixing type in to_stacked_array
Illviljan Jun 17, 2021
cbf289c
test fixing to_stacked_array
Illviljan Jun 17, 2021
4cfd5be
small is large
Illviljan Jun 18, 2021
658a59f
Update computation.py
Illviljan Jun 18, 2021
ab5ae20
Update xarray/core/computation.py
Illviljan Jun 18, 2021
d65ca41
obfuscate variable_dim some
Illviljan Jun 19, 2021
20eef03
Update computation.py
Illviljan Jun 19, 2021
274af32
undo to_stacked_array changes
Illviljan Jun 19, 2021
f352303
test sample_dims typing
Illviljan Jun 19, 2021
0a773cb
to_stacked_array fixes
Illviljan Jun 19, 2021
d8da29f
add reindex_like check
Illviljan Jun 19, 2021
54a76c1
Update computation.py
Illviljan Jun 20, 2021
0a2dc2e
Update computation.py
Illviljan Jun 20, 2021
b3592f3
Update computation.py
Illviljan Jun 20, 2021
06772da
test forcing int type in chunk()
Illviljan Jun 20, 2021
cfd11f7
Update computation.py
Illviljan Jun 20, 2021
8451a9e
Merge branch 'master' into Illviljan-cross
Illviljan Jun 21, 2021
90553ed
test collection in to_stacked_array
Illviljan Jun 21, 2021
6eed96e
Update computation.py
Illviljan Jun 21, 2021
d3648e5
Update computation.py
Illviljan Jun 22, 2021
c639aa3
Update computation.py
Illviljan Jun 22, 2021
4c636f5
Update computation.py
Illviljan Jun 22, 2021
3bea936
Update computation.py
Illviljan Jun 22, 2021
4fc7fcb
Merge branch 'master' into Illviljan-cross
Illviljan Jun 23, 2021
19e8f93
Merge branch 'master' into Illviljan-cross
Illviljan Jun 24, 2021
f71a6f1
Merge branch 'main' into Illviljan-cross
Illviljan Jun 24, 2021
d4070ab
Merge branch 'main' into Illviljan-cross
Illviljan Jun 24, 2021
12da913
whats new and api.rst
Illviljan Jun 24, 2021
ea062e6
Update whats-new.rst
Illviljan Jun 24, 2021
ebd89e6
Merge branch 'main' into Illviljan-cross
Illviljan Jul 2, 2021
3c7122b
Merge branch 'main' into Illviljan-cross
Illviljan Jul 5, 2021
9af1198
Merge branch 'main' into Illviljan-cross
Illviljan Jul 18, 2021
27262e6
Merge branch 'main' into Illviljan-cross
Illviljan Jul 22, 2021
cc91e7c
Merge branch 'main' into Illviljan-cross
Illviljan Jul 25, 2021
629df59
Output as dataset if any input is a dataset
Illviljan Jul 26, 2021
972c7dc
Simplify the if terms instead of using pass.
Illviljan Jul 26, 2021
3c4ace0
Merge branch 'main' into Illviljan-cross
Illviljan Aug 30, 2021
49967d4
Update computation.py
Illviljan Aug 30, 2021
6ab7d19
Remove support for datasets
Illviljan Aug 30, 2021
20a6cb6
Update computation.py
Illviljan Aug 30, 2021
ba3fa9c
Add some typing to test.
Illviljan Aug 30, 2021
8b192f2
doctest fix
Illviljan Aug 30, 2021
a27965c
lint
Illviljan Aug 30, 2021
5ec65d2
Merge branch 'main' into Illviljan-cross
Illviljan Sep 8, 2021
b058084
Update xarray/core/computation.py
Illviljan Oct 3, 2021
f007ed5
Update xarray/core/computation.py
Illviljan Oct 5, 2021
e88ae9d
Update xarray/core/computation.py
Illviljan Oct 5, 2021
9aaee2b
Update computation.py
Illviljan Oct 5, 2021
5d6ecba
Update computation.py
Illviljan Oct 5, 2021
71fc9c1
Update computation.py
Illviljan Oct 5, 2021
a98b2e3
Update computation.py
Illviljan Oct 5, 2021
c95817b
Update computation.py
Illviljan Oct 6, 2021
408eb39
Can't narrow types with old type
Illviljan Oct 7, 2021
316b935
dim now keyword only
Illviljan Oct 7, 2021
3b5b030
use all_dims in transpose
Illviljan Oct 7, 2021
f9c5404
Merge branch 'main' into Illviljan-cross
Illviljan Oct 7, 2021
34b300d
if in transpose indeed needed
Illviljan Oct 7, 2021
cf13bf9
Update xarray/core/computation.py
Illviljan Oct 10, 2021
f2167a6
Update xarray/core/computation.py
Illviljan Oct 10, 2021
570a806
Update xarray/core/computation.py
Illviljan Oct 10, 2021
6f57ed6
Update computation.py
Illviljan Oct 10, 2021
52a986b
Update computation.py
Illviljan Oct 10, 2021
fa78e74
add todo comments
Illviljan Oct 10, 2021
f2d98b6
Merge branch 'main' into Illviljan-cross
Illviljan Oct 31, 2021
7449cd7
Merge branch 'main' into Illviljan-cross
Illviljan Dec 27, 2021
70d2a4b
Update whats-new.rst
Illviljan Dec 27, 2021
e6020e3
Merge branch 'main' into Illviljan-cross
Illviljan Dec 27, 2021
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
3 changes: 2 additions & 1 deletion xarray/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .core.alignment import align, broadcast
from .core.combine import combine_by_coords, combine_nested
from .core.common import ALL_DIMS, full_like, ones_like, zeros_like
from .core.computation import apply_ufunc, corr, cov, dot, polyval, where
from .core.computation import apply_ufunc, corr, cov, cross, dot, polyval, where
from .core.concat import concat
from .core.dataarray import DataArray
from .core.dataset import Dataset
Expand Down Expand Up @@ -56,6 +56,7 @@
"dot",
"cov",
"corr",
"cross",
"full_like",
"infer_freq",
"load_dataarray",
Expand Down
126 changes: 126 additions & 0 deletions xarray/core/computation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,132 @@ def dot(*arrays, dims=None, **kwargs):
return result.transpose(*[d for d in all_dims if d in result.dims])


def cross(a, b, spatial_dim=None):
"""
Return the cross product of two (arrays of) vectors.

Parameters
----------
a : array_like
Components of the first vector(s).
b : array_like
Components of the second vector(s).
spatial_dim : something
Copy link
Collaborator

Choose a reason for hiding this comment

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

To allow having different dimension names on a and b, we need to allow passing a 2-tuple:

Suggested change
spatial_dim : something
dim : hashable or tuple of hashable

and we could also accept a name for the output dimension (with a default, e.g. "cross")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My intuition for the cross product is lost once we go beyond cartesian dims. But what does it even mean if we do np.cross(a(cartesian), b(something_else))? Or shall I just accept it as math-magics with arrays that have the correct length? I'm used to that too...

Copy link
Collaborator

Choose a reason for hiding this comment

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

I meant that the 2 or 3 element dimension might be named differently for a and b:

a = xr.DataArray([[0, 1, 2], [3, 4, 5], [7, 5, 3]], dims=("dim_0", "dim_1"))
b = xr.DataArray([[1, 4, 2], [8, 1, 3], [5, 4, 9]], dims=("x", "y"))
xr.cross(a, b, dim=("dim_1", "y"))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But are dim_1 and y actually different things or is it a typo made by the user?

I just want to argue that since we have named dims and do all the necessary reshaping behind the scenes we don't need these extra options.
But I suppose I can think of it like this: dim_1 is a alias of y, so dim=(dim_1, y) is basically just adding aliases of the same thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another thing to think about if we want to accept different dims, what to do when they have different values?

a = xr.DataArray(
    [[0, 1, 2], [3, 4, 5]],
    dims=("dim_0", "dim_1"),
    coords=dict(dim_0=(["dim_0", [1, 2]]), dim_1=(["dim_1"], ["u", "v", "w"])),
)
b = xr.DataArray(
    [[1, 4, 2], [8, 1, 3]],
    dims=("time", "cartesian"),
    coords=dict(time=(["time", [0, 1]]), cartesian=(["cartesian"], ["x", "y", "z"])),
)
xr.cross(a, b, dim=("dim_1", "y"))

Copy link
Collaborator

Choose a reason for hiding this comment

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

as far as I can tell, this would work with:

In [2]: xr.set_options(keep_attrs=True)
   ...: 
   ...: ds = xr.tutorial.open_dataset("eraint_uvz")
   ...: 
   ...: arr = ds.to_stacked_array(variable_dim="cartesian", new_dim="variable", sample_dims=ds.dims).unstack("variable")
   ...: other_ds = arr.stack(variable=["cartesian"]).to_unstacked_dataset("variable")
   ...: xr.testing.assert_equal(ds, other_ds)

Maybe that would be a fit for ds.cross()?

Maybe, but usually top-level functions accept both DataArray and Dataset objects (not sure if this can / should be an exception?).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was it something like in 14decb3 you imagined, @keewis?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@keewis, found a dataset example here that I'm not sure how to handle, any ideas?

ds = xr.Dataset({0: ("dim_0", [1]), 1: ("dim_0", [2]), 2: ("dim_0", [3])})

arr = ds.to_stacked_array(variable_dim="cartesian", new_dim="variable", sample_dims=ds.dims).unstack("variable")
other_ds = arr.stack(variable=["cartesian"]).to_unstacked_dataset("variable")
xr.testing.assert_equal(ds, other_ds)

AssertionError: Left and right Dataset objects are not equal
Differing dimensions:
    (dim_0: 1) != ()

Differing data variables:
L   0        (dim_0) int32 1
R   0        int32 1
L   1        (dim_0) int32 2
R   1        int32 2
L   2        (dim_0) int32 3
R   2        int32 3

Copy link
Collaborator

Choose a reason for hiding this comment

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

sorry for the late reply.

Was it something like in 14decb3 you imagined, @keewis?

yes, exactly. I'm somewhat uncomfortable with the big loop over the input objects, but I'm not sure what I would suggest instead.

found a dataset example here that I'm not sure how to handle

yes, 1-sized dimensions can be tricky because they tend to lose their status as a dimension. We can try to use expand_dims to get back the original dims:

other_ds.expand_dims([dim for dim, size in ds.sizes.items() if size == 1])

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's another case I've been pondering about, should different dims be allowed?

a = xr.Dataset({0: ("dim_0", [1]), 1: ("dim_0", [2]), 2: ("dim_0", [3])})
b = xr.Dataset({0: ("dim_1", [4]), 1: ("dim_1", [5]), 2: ("dim_1", [6])})
c = xr.cross(a, b, "temp_dim")

How should c look like? Should it behave like a*b?

a*b
Out[25]: 
<xarray.Dataset>
Dimensions:  (dim_0: 1, dim_1: 1)
Dimensions without coordinates: dim_0, dim_1
Data variables:
    0        (dim_0, dim_1) int32 4
    1        (dim_0, dim_1) int32 10
    2        (dim_0, dim_1) int32 18

In bce2f3e it follows the a*b style which is also consistent when size>1.

something

Examples
--------
Vector cross-product.

>>> x = xr.DataArray(np.array([1, 2, 3]))
>>> y = xr.DataArray(np.array([4, 5, 6]))
>>> xr.cross(x, y)
array([-3, 6, -3])

One vector with dimension 2.

>>> a = xr.DataArray(
... np.array([1, 2]), dims=["x"], coords=dict(x=(["x"], np.array(["x", "z"])))
... )
>>> b = xr.DataArray(
... np.array([4, 5, 6]),
... dims=["x"],
... coords=dict(x=(["x"], np.array(["x", "y", "z"]))),
... )
>>> xr.cross(a, b)
array([12, -6, -3])



Multiple vector cross-products. Note that the direction of the
cross product vector is defined by the right-hand rule.

>>> x = xr.DataArray(np.array([[1, 2, 3], [4, 5, 6]]), dims=("a", "b"))
>>> y = xr.DataArray(np.array([[4, 5, 6], [1, 2, 3]]), dims=("a", "b"))
>>> xr.cross(x, y)
array([[-3, 6, -3],
[ 3, -6, 3]])

Change the vector definition of x and y using axisa and axisb.

>>> x = xr.DataArray(np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
>>> y = xr.DataArray(np.array([[7, 8, 9], [4, 5, 6], [1, 2, 3]]))
>>> np.cross(x, y)
array([[ -6, 12, -6],
[ 0, 0, 0],
[ 6, -12, 6]])
>>> np.cross(x, y, axisa=0, axisb=0)
array([[-24, 48, -24],
[-30, 60, -30],
[-36, 72, -36]])

See Also
--------
numpy.cross : Corresponding numpy function
"""
from .dataarray import DataArray

arrays = [a, b]
for arr in arrays:
if not isinstance(arr, (DataArray)):
raise TypeError(
f"Only xr.DataArray and xr.Variable are supported, got {type(arr)}."
)

if spatial_dim is None:
# TODO: Find spatial dim default by looking for unique
# (3 or 2)-valued dim?
spatial_dim = arr.dims[-1]
elif spatial_dim not in arr.dims:
raise ValueError(f"Dimension {spatial_dim} not in {arr}.")

s = arr.sizes[spatial_dim]
if s < 1 or s > 3:
raise ValueError(
"incompatible dimensions for cross product\n"
"(dimension with coords must be 1, 2 or 3)"
)

if a.sizes[spatial_dim] == b.sizes[spatial_dim]:
# Arrays have the same size, no need to do anything:
pass
else:
# Arrays have different sizes. Append zeros where the smaller
# array is missing a value, zeros will not affect np.cross:
ind = 1 if a.sizes[spatial_dim] > b.sizes[spatial_dim] else 0
if a.coords:
# If the array has coords we know which indexes to fill
# with zeros:
arrays[ind] = arrays[ind].reindex_like(arrays[1 - ind], fill_value=0)
elif arrays[ind].sizes[spatial_dim] > 1:
# If it doesn't have coords we can can only that infer that
# it is composite values if the size is 2.
from .concat import concat

arrays[ind] = concat([a, DataArray([0])], dim=spatial_dim)
else:
# Size is 1, then we do not know if it is a constant or
# composite value:
raise ValueError(
"incompatible dimensions for cross product\n"
"(dimension without coords must be 2 or 3)"
)

# Figure out the output dtype:
# output_dtype = np.cross(
# np.empty((2, 2), dtype=arrays[0].dtype), np.empty((2, 2), dtype=arrays[1].dtype)
# ).dtype

return apply_ufunc(
np.cross,
*arrays,
# input_core_dims=[[spatial_dim], [spatial_dim]],
# output_core_dims=[[spatial_dim]],
dask="parallelized",
# output_dtypes=[output_dtype],
)


def where(cond, x, y):
"""Return elements from `x` or `y` depending on `cond`.

Expand Down
37 changes: 37 additions & 0 deletions xarray/tests/test_computation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1898,3 +1898,40 @@ def test_polyval(use_dask, use_datetime):
da_pv = xr.polyval(da.x, coeffs)

xr.testing.assert_allclose(da, da_pv.T)


@pytest.mark.parametrize(
"a, b, ae, be",
[
[
xr.DataArray(np.array([1, 2, 3])),
xr.DataArray(np.array([4, 5, 6])),
np.array([1, 2, 3]),
np.array([4, 5, 6]),
],
[
xr.DataArray(np.array([1, 2])),
xr.DataArray(np.array([4, 5, 6])),
np.array([1, 2]),
np.array([4, 5, 6]),
],
[
xr.DataArray(
np.array([1, 2]),
dims=["ax"],
coords=dict(x=(["ax"], np.array(["x", "z"]))),
),
xr.DataArray(
np.array([4, 5, 6]),
dims=["ax"],
coords=dict(x=(["ax"], np.array(["x", "y", "z"]))),
),
np.array([1, 0, 2]),
np.array([4, 5, 6]),
],
],
)
def test_cross(a, b, ae, be):
expected = np.cross(ae, be)
actual = xr.cross(a, b)
xr.testing.assert_allclose(expected, actual)