Repository: dask/dask

Issue:
`dask.array.tensordot` bug: negative values in argument `axes`  give wrong results.
**Describe the issue**:
While `numpy.tensordot` accepts negative axes, negative axes in `dask.array.tensordot`  give wrong results.

**Minimal Complete Verifiable Example**:

```python
import numpy as np
import dask.array as da

rng = np.random.default_rng()
a = rng.normal(size=(30, 10))
b = rng.normal(size=(10, 20))

kwargs = dict(axes=[[-1], [0]])
np_z = np.tensordot(a, b, **kwargs)
da_z = da.tensordot(da.array(a), da.array(b), **kwargs)
print(np_z.shape) 
# (30, 20) 
print(da_z.shape)
# (30, 1)
```


Discussion:
GPUtester: Can one of the admins verify this patch?

Admins can comment `ok to test` to allow this one PR to run or `add to allowlist` to allow all future PRs from the same author to run.
github-actions[bot]: ## Unit Test Results
_See [test report](https://dask.github.io/dask/test_report.html) for an extended history of previous test failures. This is useful for diagnosing flaky tests._

     15 files  ±0       15 suites  ±0   3h 52m 34s :stopwatch: + 5m 41s
 12 941 tests ±0   11 608 :white_check_mark: +11     929 :zzz: ±0  404 :x:  - 10 
160 329 runs  ±0  143 037 :white_check_mark: + 7  16 888 :zzz: +4  404 :x:  - 10 

For more details on these failures, see [this check](https://github.com/dask/dask/runs/20565105206).

Results for commit aad3ee20. ± Comparison against base commit cdcddb68.

[test-results]:data:application/gzip;base64,H4sIAJKfp2UC/1XNSw7CIBSF4a00jB3w7sXNNJRCQuzDUBgZ9+4tplSH57vh50VCnP1O7h1Tt47sJeZrTSXZHLf12MIoiYTXXO/cSHbuYS/OHcg0hQsf8YlouGkUbJyRJG2pwae0JTSKkspa25qK+ujYrS0FFX3Db5tpADjtL17lt+22ZYkZB7F2Et5zqp1XdjTMgvZTb0wAJWQPo+A8AP5F3h+V7mvCHQEAAA==

joanrue: Hi! I'm reaching out to see if there's anything more I can do to facilitate the review process.
joanrue: Hi @hendrikmakait ! The only failing test is not related to these changes. Can we go ahead with the PR?

Patch:
diff --git a/dask/array/routines.py b/dask/array/routines.py
index 4780f71d7..a56e9179b 100644
--- a/dask/array/routines.py
+++ b/dask/array/routines.py
@@ -345,6 +345,7 @@ def tensordot(lhs, rhs, axes=2):
     if concatenate:
         return intermediate
     else:
+        left_axes = [ax if ax >= 0 else lhs.ndim + ax for ax in left_axes]
         return intermediate.sum(axis=left_axes)
 
 


Test patch:
diff --git a/dask/array/tests/test_routines.py b/dask/array/tests/test_routines.py
index 4b1ca1e47..0b07ccc14 100644
--- a/dask/array/tests/test_routines.py
+++ b/dask/array/tests/test_routines.py
@@ -334,7 +334,7 @@ def test_tensordot():
     y = np.arange(200).reshape((20, 10))
     b = da.from_array(y, chunks=(4, 5))
 
-    for axes in [1, (1, 0)]:
+    for axes in [1, (1, 0), (-1, 0)]:
         assert_eq(da.tensordot(a, b, axes=axes), np.tensordot(x, y, axes=axes))
         assert_eq(da.tensordot(x, b, axes=axes), np.tensordot(x, y, axes=axes))
         assert_eq(da.tensordot(a, y, axes=axes), np.tensordot(x, y, axes=axes))