Repository: astropy/astropy

Issue:
Cannot store output from Quantity comparison in array
Not a super-big deal, but the following should not error:
```
import astropy.units as u, numpy as np
q = np.arange(3.) * u.m
comp = np.empty(q.shape, bool)
np.less(q, 0, out=comp)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-28-66669b62f886> in <module>()
      1 q = np.arange(3.) * u.m
      2 comp = np.empty(q.shape, bool)
----> 3 np.less(q, 0, out=comp)

/usr/lib/python3/dist-packages/astropy/units/quantity.py in __array_ufunc__(self, function, method, *inputs, **kwargs)
    451             if function.nout == 1:
    452                 out = out[0]
--> 453             out_array = check_output(out, unit, inputs, function=function)
    454             # Ensure output argument remains a tuple.
    455             kwargs['out'] = (out_array,) if function.nout == 1 else out_array

/usr/lib/python3/dist-packages/astropy/units/quantity_helper/converters.py in check_output(output, unit, inputs, function)
    337                        casting='same_kind'):
    338         raise TypeError("Arguments cannot be cast safely to inplace "
--> 339                         "output with dtype={0}".format(output.dtype))
    340     return output

TypeError: Arguments cannot be cast safely to inplace output with dtype=bool
```

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 3bf050a8f5..8c822c4d18 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -559,6 +559,9 @@ astropy.uncertainty
 astropy.units
 ^^^^^^^^^^^^^
 
+- Ensure that output from test functions of and comparisons between quantities
+  can be stored into pre-allocated output arrays (using ``out=array``) [#9273]
+
 astropy.utils
 ^^^^^^^^^^^^^
 
diff --git a/astropy/units/quantity_helper/converters.py b/astropy/units/quantity_helper/converters.py
index 2b676782d4..ea76104482 100644
--- a/astropy/units/quantity_helper/converters.py
+++ b/astropy/units/quantity_helper/converters.py
@@ -326,9 +326,17 @@ def check_output(output, unit, inputs, function=None):
                                if function is not None else ""), type(output),
                         output.__quantity_subclass__(unit)[0]))
 
+        # check we can handle the dtype (e.g., that we are not int
+        # when float is required).  Note that we only do this for Quantity
+        # output; for array output, we defer to numpy's default handling.
+        if not np.can_cast(np.result_type(*inputs), output.dtype,
+                           casting='same_kind'):
+            raise TypeError("Arguments cannot be cast safely to inplace "
+                            "output with dtype={}".format(output.dtype))
         # Turn into ndarray, so we do not loop into array_wrap/array_ufunc
         # if the output is used to store results of a function.
-        output = output.view(np.ndarray)
+        return output.view(np.ndarray)
+
     else:
         # output is not a Quantity, so cannot obtain a unit.
         if not (unit is None or unit is dimensionless_unscaled):
@@ -338,10 +346,4 @@ def check_output(output, unit, inputs, function=None):
                                         "resulting from {} function "
                                         .format(function.__name__)))
 
-    # check we can handle the dtype (e.g., that we are not int
-    # when float is required).
-    if not np.can_cast(np.result_type(*inputs), output.dtype,
-                       casting='same_kind'):
-        raise TypeError("Arguments cannot be cast safely to inplace "
-                        "output with dtype={}".format(output.dtype))
-    return output
+        return output
diff --git a/astropy/units/quantity_helper/helpers.py b/astropy/units/quantity_helper/helpers.py
index d5e0a4adfd..89ef7d9cf1 100644
--- a/astropy/units/quantity_helper/helpers.py
+++ b/astropy/units/quantity_helper/helpers.py
@@ -344,7 +344,8 @@ for name in 'isnat', 'gcd', 'lcm':
 
 # SINGLE ARGUMENT UFUNCS
 
-# ufuncs that return a boolean and do not care about the unit
+# ufuncs that do not care about the unit and do not return a Quantity
+# (but rather a boolean, or -1, 0, or +1 for np.sign).
 onearg_test_ufuncs = (np.isfinite, np.isinf, np.isnan, np.sign, np.signbit)
 for ufunc in onearg_test_ufuncs:
     UFUNC_HELPERS[ufunc] = helper_onearg_test


Test patch:
diff --git a/astropy/units/tests/test_quantity_ufuncs.py b/astropy/units/tests/test_quantity_ufuncs.py
index f303324f66..a05e9836af 100644
--- a/astropy/units/tests/test_quantity_ufuncs.py
+++ b/astropy/units/tests/test_quantity_ufuncs.py
@@ -702,6 +702,23 @@ class TestComparisonUfuncs:
             ufunc(q_i1, q_i2)
         assert "compatible dimensions" in exc.value.args[0]
 
+    @pytest.mark.parametrize('ufunc', (np.isfinite, np.isinf, np.isnan,
+                                       np.signbit))
+    def test_onearg_test_ufuncs(self, ufunc):
+        q = [1., np.inf, -np.inf, np.nan, -1., 0.] * u.m
+        out = ufunc(q)
+        assert not isinstance(out, u.Quantity)
+        assert out.dtype == bool
+        assert np.all(out == ufunc(q.value))
+
+    def test_sign(self):
+        q = [1., np.inf, -np.inf, np.nan, -1., 0.] * u.m
+        out = np.sign(q)
+        assert not isinstance(out, u.Quantity)
+        assert out.dtype == q.dtype
+        assert np.all((out == np.sign(q.value)) |
+                      (np.isnan(out) & np.isnan(q.value)))
+
 
 class TestInplaceUfuncs:
 
@@ -868,6 +885,46 @@ class TestInplaceUfuncs:
         with pytest.raises(TypeError):
             a4 += u.Quantity(10, u.mm, dtype=np.int64)
 
+    @pytest.mark.parametrize('ufunc', (np.equal, np.greater))
+    def test_comparison_ufuncs_inplace(self, ufunc):
+        q_i1 = np.array([-3.3, 2.1, 10.2]) * u.kg / u.s
+        q_i2 = np.array([10., -5., 1.e6]) * u.g / u.Ms
+        check = np.empty(q_i1.shape, bool)
+        ufunc(q_i1.value, q_i2.to_value(q_i1.unit), out=check)
+
+        result = np.empty(q_i1.shape, bool)
+        q_o = ufunc(q_i1, q_i2, out=result)
+        assert q_o is result
+        assert type(q_o) is np.ndarray
+        assert q_o.dtype == bool
+        assert np.all(q_o == check)
+
+    @pytest.mark.parametrize('ufunc', (np.isfinite, np.signbit))
+    def test_onearg_test_ufuncs_inplace(self, ufunc):
+        q = [1., np.inf, -np.inf, np.nan, -1., 0.] * u.m
+        check = np.empty(q.shape, bool)
+        ufunc(q.value, out=check)
+
+        result = np.empty(q.shape, bool)
+        out = ufunc(q, out=result)
+        assert out is result
+        assert type(out) is np.ndarray
+        assert out.dtype == bool
+        assert np.all(out == ufunc(q.value))
+
+    def test_sign_inplace(self):
+        q = [1., np.inf, -np.inf, np.nan, -1., 0.] * u.m
+        check = np.empty(q.shape, q.dtype)
+        np.sign(q.value, out=check)
+
+        result = np.empty(q.shape, q.dtype)
+        out = np.sign(q, out=result)
+        assert out is result
+        assert type(out) is np.ndarray
+        assert out.dtype == q.dtype
+        assert np.all((out == np.sign(q.value)) |
+                      (np.isnan(out) & np.isnan(q.value)))
+
 
 @pytest.mark.skipif(not hasattr(np.core.umath, 'clip'),
                     reason='no clip ufunc available')