Repository: HypothesisWorks/hypothesis

Issue:
Numpy arrays strategy generates infinities with elements=floats(allow_infinity=False)
I have tried to explicitly as `hypothesis.extra.numpy.arrays` to not produce any infinities, by providing a strategy for both fill and elements. However, this doesn't seem to be working.

MWE:

```pycon
In [62]: import hypothesis.strategies as st                            
                                                                        
In [63]: from hypothesis.extra.numpy import arrays as st_arrays        
                                                                         
In [64]: non_inf = st.floats(allow_nan=False, allow_infinity=False)
                                                                   
In [65]: dim = st.integers(1, 10)                                  
                                      
In [66]: arr = st_arrays(shape=st.tuples(dim, dim), fill=non_inf, elements=non_inf, dtype='float32')

In [71]: arr.example()                                             
Out[71]:                              
array([[3.3333334e-01,           inf,           inf,           inf,    
        1.6367880e+16,           inf,           inf,           inf],
       [          inf,           inf,           inf, 4.9060428e+16,    
                  inf,           inf,           inf,           inf]],
      dtype=float32)  

In [72]: arr                                                                                                                                                          
Out[72]: arrays(dtype='float32', shape=tuples(integers(min_value=1, max_value=10), integers(min_value=1, max_value=10)), elements=floats(allow_nan=False, allow_infinity=False), fill=floats(allow_nan=False, allow_infinity=False))
```

Am I doing something wrong? Thanks for any help


Discussion:
Huh.

I *think* what's happening here is that the `floats` strategy generates doubles (because float in Python is actually a double because everything is terrible and naming things is hard), and when Hypothesis generates a double value that's too large to fit in a 32-bit float the conversion overflows it to infinity. That's far from ideal though.

I'm not sure what the right thing to do here is. I don't think this behaviour can/will change, but maybe this is a use case for adding a specific strategy for 32-bit floats that could be used instead?
*Strictly* speaking, this is not a bug - it's just what happens when you put large values of dtype `f64` in an array of dtype `f32`.  @ashgillman, you can avoid this by tweaking the elements strategy:

```python
f32_max = (2. - 2**-23) * 2**127
finite_f32 = st.floats(-f32_max, f32_max, allow_nan=False, allow_infinity=False)
```

--------------------------------

Nonetheless, I think Hypothesis can do better. Proposals:

- [ ] Do this in `hypothesis.extra.numpy.from_dtype(np.dtype('float32')` - currently it has a massive bias towards infinities for narrow float types.
- [ ] Check for overflow when inserting elements in a Numpy array, and IMO error we we detect it.  Precision loss must be silently allowed though or floats will be unusable.
- [ ] Consider accepting `**kwargs` to `from_dtype` and passing them to the underlying strategy so that it can be used to generate e.g. finite floats only.  Validate min and max value arguments, or maybe disallow them entirely.

Providing a native float32 strategy would be a real pain for Hypothesis in Python - on the order of duplicating [`hypothesis.internal.conjecture.floats`](https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/src/hypothesis/internal/conjecture/floats.py) :sob: - so it's fortunate that we don't need to, because it's possible to set the bounds of the `floats()` strategy and we don't mind discarding the lower mantissa bits.
> Providing a native float32 strategy would be a real pain for Hypothesis in Python - on the order of duplicating hypothesis.internal.conjecture.floats 😭 - so it's fortunate that we don't need to, because it's possible to set the bounds of the floats() strategy and we don't mind discarding the lower mantissa bits.

The solution I mostly had in mind was to implement the native 32-bit floats as something not too far off `floats(**kwargs).map(np.float32).filter(conforms_to_args)`. I definitely wasn't suggesting we do a full blown from scratch support for them!
Thanks @Zac-HD , your solution fixes. (I had tried previously to filter on `np.ininfinite` but had no luck.)

I hadn't realised that coercing floats to lower precision can result in inf!

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/extra/numpy.py b/hypothesis-python/src/hypothesis/extra/numpy.py
--- a/hypothesis-python/src/hypothesis/extra/numpy.py
+++ b/hypothesis-python/src/hypothesis/extra/numpy.py
@@ -23,7 +23,10 @@
 
 import hypothesis.strategies as st
 import hypothesis.internal.conjecture.utils as cu
+from hypothesis import Verbosity
 from hypothesis.errors import InvalidArgument
+from hypothesis._settings import note_deprecation
+from hypothesis.reporting import current_verbosity
 from hypothesis.searchstrategy import SearchStrategy
 from hypothesis.internal.compat import hrange, text_type
 from hypothesis.internal.coverage import check_function
@@ -126,10 +129,44 @@ def __init__(self, element_strategy, shape, dtype, fill, unique):
         self.element_strategy = element_strategy
         self.unique = unique
 
+        # Used by self.insert_element to check that the value can be stored
+        # in the array without e.g. overflowing.  See issue #1385.
+        if dtype.kind in (u'i', u'u'):
+            self.check_cast = lambda x: np.can_cast(x, self.dtype, 'safe')
+        elif dtype.kind == u'f' and dtype.itemsize == 2:
+            max_f2 = (2. - 2 ** -10) * 2 ** 15
+            self.check_cast = lambda x: \
+                (not np.isfinite(x)) or (-max_f2 <= x <= max_f2)
+        elif dtype.kind == u'f' and dtype.itemsize == 4:
+            max_f4 = (2. - 2 ** -23) * 2 ** 127
+            self.check_cast = lambda x: \
+                (not np.isfinite(x)) or (-max_f4 <= x <= max_f4)
+        else:
+            self.check_cast = lambda x: True
+
+    def set_element(self, data, result, idx, strategy=None):
+        strategy = strategy or self.element_strategy
+        val = data.draw(strategy)
+        if self._report_overflow and not self.check_cast(val):
+            note_deprecation(
+                'Generated array element %r from %r cannot be represented as '
+                'dtype %r without overflow or underflow.  Consider using a '
+                'more precise strategy, as this will be an error in a future '
+                'version of Hypothesis.' % (val, strategy, self.dtype)
+            )
+            # Because the message includes the value of the generated element,
+            # it would be easy to spam users with thousands of warnings.
+            # We therefore only warn once per draw, unless in verbose mode.
+            self._report_overflow = current_verbosity() >= Verbosity.verbose
+        result[idx] = val
+
     def do_draw(self, data):
         if 0 in self.shape:
             return np.zeros(dtype=self.dtype, shape=self.shape)
 
+        # Reset this flag for each test case to emit warnings from set_element
+        self._report_overflow = True
+
         # This could legitimately be a np.empty, but the performance gains for
         # that would be so marginal that there's really not much point risking
         # undefined behaviour shenanigans.
@@ -154,7 +191,7 @@ def do_draw(self, data):
                     # uniqueness after numpy has converted it to the relevant
                     # type for us. Because we don't increment the counter on
                     # a duplicate we will overwrite it on the next draw.
-                    result[i] = data.draw(self.element_strategy)
+                    self.set_element(data, result, i)
                     if result[i] not in seen:
                         seen.add(result[i])
                         i += 1
@@ -162,7 +199,7 @@ def do_draw(self, data):
                         elements.reject()
             else:
                 for i in hrange(len(result)):
-                    result[i] = data.draw(self.element_strategy)
+                    self.set_element(data, result, i)
         else:
             # We draw numpy arrays as "sparse with an offset". We draw a
             # collection of index assignments within the array and assign
@@ -188,7 +225,7 @@ def do_draw(self, data):
                 if not needs_fill[i]:
                     elements.reject()
                     continue
-                result[i] = data.draw(self.element_strategy)
+                self.set_element(data, result, i)
                 if self.unique:
                     if result[i] in seen:
                         elements.reject()
@@ -209,7 +246,7 @@ def do_draw(self, data):
                 # it and putmask will do the right thing by repeating the
                 # values of the array across the mask.
                 one_element = np.zeros(shape=1, dtype=self.dtype)
-                one_element[0] = data.draw(self.fill)
+                self.set_element(data, one_element, 0, self.fill)
                 fill_value = one_element[0]
                 if self.unique:
                     try:


Test patch:
diff --git a/hypothesis-python/tests/numpy/test_gen_data.py b/hypothesis-python/tests/numpy/test_gen_data.py
--- a/hypothesis-python/tests/numpy/test_gen_data.py
+++ b/hypothesis-python/tests/numpy/test_gen_data.py
@@ -28,13 +28,15 @@
 from hypothesis import given, assume, settings
 from hypothesis.errors import InvalidArgument
 from tests.common.debug import minimal, find_any
+from tests.common.utils import checks_deprecated_behaviour
 from hypothesis.searchstrategy import SearchStrategy
 from hypothesis.internal.compat import text_type, binary_type
 
 STANDARD_TYPES = list(map(np.dtype, [
-    u'int8', u'int32', u'int64',
-    u'float', u'float32', u'float64',
-    complex,
+    u'int8', u'int16', u'int32', u'int64',
+    u'uint8', u'uint16', u'uint32', u'uint64',
+    u'float', u'float16', u'float32', u'float64',
+    u'complex64', u'complex128',
     u'datetime64', u'timedelta64',
     bool, text_type, binary_type
 ]))
@@ -300,3 +302,30 @@ def test_all_inferred_scalar_strategies_roundtrip(data, dtype):
     assume(ex == ex)  # If not, the roundtrip test *should* fail!  (eg NaN)
     arr[0] = ex
     assert arr[0] == ex
+
+
+@pytest.mark.parametrize('fill', [False, True])
+@checks_deprecated_behaviour
+@given(st.data())
+def test_overflowing_integers_are_deprecated(fill, data):
+    kw = dict(elements=st.just(300))
+    if fill:
+        kw = dict(elements=st.nothing(), fill=kw['elements'])
+    arr = data.draw(nps.arrays(dtype='int8', shape=(1,), **kw))
+    assert arr[0] == (300 % 256)
+
+
+@pytest.mark.parametrize('fill', [False, True])
+@checks_deprecated_behaviour
+@given(st.data())
+def test_overflowing_floats_are_deprecated(fill, data):
+    kw = dict(elements=st.floats(min_value=65520, allow_infinity=False))
+    if fill:
+        kw = dict(elements=st.nothing(), fill=kw['elements'])
+    arr = data.draw(nps.arrays(dtype='float16', shape=(1,), **kw))
+    assert np.isinf(arr[0])
+
+
+@given(nps.arrays(dtype='float16', shape=(1,)))
+def test_inferred_floats_do_not_overflow(arr):
+    pass