Skip to content

Commit

Permalink
Merge pull request HypothesisWorks#3706 from jobh/master
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Aug 5, 2023
2 parents 9e180fb + ffc4703 commit 695156f
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 11 deletions.
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

Improve shrinking of floats in narrow regions that don't cross an integer
boundary. Closes :issue:`3357`.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ def left_is_better(self, left, right):
return lex1 < lex2

def short_circuit(self):
# We check for a bunch of standard "large" floats. If we're currently
# worse than them and the shrink downwards doesn't help, abort early
# because there's not much useful we can do here.

for g in [sys.float_info.max, math.inf, math.nan]:
self.consider(g)

Expand All @@ -53,10 +57,6 @@ def short_circuit(self):
return self.current >= MAX_PRECISE_INTEGER

def run_step(self):
# We check for a bunch of standard "large" floats. If we're currently
# worse than them and the shrink downwards doesn't help, abort early
# because there's not much useful we can do here.

# Finally we get to the important bit: Each of these is a small change
# to the floating point number that corresponds to a large change in
# the lexical representation. Trying these ensures that our floating
Expand All @@ -65,18 +65,26 @@ def run_step(self):
# change that would require shifting the exponent while not changing
# the float value much.

for g in [math.floor(self.current), math.ceil(self.current)]:
self.consider(g)
# First, try dropping precision bits by rounding the scaled value. We
# try values ordered from least-precise (integer) to more precise, ie.
# approximate lexicographical order. Once we find an acceptable shrink,
# self.consider discards the remaining attempts early and skips test
# invocation. The loop count sets max fractional bits to keep, and is a
# compromise between completeness and performance.

for p in range(10):
scaled = self.current * 2**p # note: self.current may change in loop
for truncate in [math.floor, math.ceil]:
self.consider(truncate(scaled) / 2**p)

if self.consider(int(self.current)):
self.debug("Just an integer now")
self.delegate(Integer, convert_to=int, convert_from=float)
return

m, n = self.current.as_integer_ratio()
i, r = divmod(m, n)

# Now try to minimize the top part of the fraction as an integer. This
# basically splits the float as k + x with 0 <= x < 1 and minimizes
# k as an integer, but without the precision issues that would have.
self.call_shrinker(Integer, i, lambda k: self.consider((i * n + r) / n))
m, n = self.current.as_integer_ratio()
i, r = divmod(m, n)
self.call_shrinker(Integer, i, lambda k: self.consider((k * n + r) / n))
4 changes: 4 additions & 0 deletions hypothesis-python/tests/conjecture/test_float_encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ def test_shrink_down_to_half():
assert minimal_from(0.75, lambda x: 0 < x < 1) == 0.5


def test_shrink_fractional_part():
assert minimal_from(2.5, lambda x: divmod(x, 1)[1] == 0.5) == 1.5


def test_does_not_shrink_across_one():
# This is something of an odd special case. Because of our encoding we
# prefer all numbers >= 1 to all numbers in 0 < x < 1. For the most part
Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/tests/nocover/test_simple_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_minimal_float_is_zero():


def test_minimal_asymetric_bounded_float():
assert minimal(floats(min_value=1.1, max_value=1.9), lambda x: True) == 1.5
assert minimal(floats(min_value=1.1, max_value=1.6), lambda x: True) == 1.5


def test_negative_floats_simplify_to_zero():
Expand Down

0 comments on commit 695156f

Please sign in to comment.