Repository: cupy/cupy

Issue:
`random.choice`: 0 samples from 0 candidates
Inconsistency in
```
>>> numpy.random.choice(0, size=(3, 0, 7))
array([], shape=(3, 0, 7), dtype=int64)
>>> numpy.random.choice([], size=(3, 0, 7))
array([], shape=(3, 0, 7), dtype=float64)
```

vs

```
>>> cupy.random.choice(0, size=(3, 0, 7))
ValueError: a must be greater than 0
>>> cupy.random.choice([], size=(3, 0, 7))
ValueError: a must be non-empty
```

Note that
```
>>> numpy.random.choice(0)
ValueError: a must be greater than 0 unless no samples are taken
```


Discussion:

Resolving patch:
diff --git a/cupy/random/_generator.py b/cupy/random/_generator.py
--- a/cupy/random/_generator.py
+++ b/cupy/random/_generator.py
@@ -1021,16 +1021,13 @@ def choice(self, a, size=None, replace=True, p=None):
             raise NotImplementedError
         if isinstance(a, int):
             a_size = a
-            if a_size <= 0:
-                raise ValueError('a must be greater than 0')
+            if a_size < 0:
+                raise ValueError('a must be greater than or equal to 0')
         else:
             a = cupy.array(a, copy=False)
             if a.ndim != 1:
                 raise ValueError('a must be 1-dimensional or an integer')
-            else:
-                a_size = len(a)
-                if a_size == 0:
-                    raise ValueError('a must be non-empty')
+            a_size = len(a)
 
         if p is not None:
             p = cupy.array(p)
@@ -1049,6 +1046,9 @@ def choice(self, a, size=None, replace=True, p=None):
         shape = size
         size = numpy.prod(shape)
 
+        if a_size == 0 and size > 0:
+            raise ValueError('a cannot be empty unless no samples are taken')
+
         if not replace and p is None:
             if a_size < size:
                 raise ValueError(
@@ -1072,6 +1072,8 @@ def choice(self, a, size=None, replace=True, p=None):
             if not isinstance(shape, int):
                 index = cupy.reshape(index, shape)
         else:
+            if a_size == 0:  # TODO: (#4511) Fix `randint` instead
+                a_size = 1
             index = self.randint(0, a_size, size=shape)
             # Align the dtype with NumPy
             index = index.astype(cupy.int64, copy=False)


Test patch:
diff --git a/tests/cupy_tests/random_tests/test_generator.py b/tests/cupy_tests/random_tests/test_generator.py
--- a/tests/cupy_tests/random_tests/test_generator.py
+++ b/tests/cupy_tests/random_tests/test_generator.py
@@ -1006,6 +1006,8 @@ def test_tomaxint_tuple(self):
     {'a': 3, 'size': (5, 5), 'p': numpy.array([0.3, 0.3, 0.4])},
     {'a': 3, 'size': (), 'p': None},
     {'a': numpy.array([0.0, 1.0, 2.0]), 'size': 2, 'p': [0.3, 0.3, 0.4]},
+    {'a': 0, 'size': 0, 'p': None},
+    {'a': numpy.array([]), 'size': 0, 'p': None},
 )
 @testing.fix_random()
 @testing.gpu
@@ -1032,6 +1034,8 @@ def test_bound(self):
             a=self.a, size=self.size, p=self.p, _count=20)
         vals = [val.get() for val in vals]
         size_ = self.size if isinstance(self.size, tuple) else (self.size,)
+        if size_ == (0, ):
+            self.skipTest('no bound check for empty `random.choice`')
         for val in vals:
             assert val.shape == size_
         assert min(val.min() for val in vals) == 0