Skip to content

Commit

Permalink
change bounded ordering to match semibounded
Browse files Browse the repository at this point in the history
  • Loading branch information
tybug committed Dec 22, 2024
1 parent b6456b1 commit e289629
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 30 deletions.
63 changes: 33 additions & 30 deletions hypothesis-python/src/hypothesis/internal/conjecture/choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,11 @@ def choice_to_index(choice, kwargs):
# Let a = shrink_towards.
# * Unbounded: Ordered by (|a - x|, sgn(a - x)). Think of a zigzag.
# [a, a + 1, a - 1, a + 2, a - 2, ...]
# * Semi-bounded: Same as unbounded except stop on one side when you hit
# * Semi-bounded: Same as unbounded, except stop on one side when you hit
# {min, max}_value. so min_value=-1 a=0 has order
# [0, 1, -1, 2, 3, 4, ...]
# * Bounded: Ordered by (sgn(a - x), |a - x|). Count upwards until max_value,
# then count downards.
# [a, a + 1, a + 2, ..., max_value, a - 1, a - 2, ..., min_value]
# * Bounded: Same as unbounded and semibounded, except stop on each side
# when you hit {min, max}_value.
#
# To simplify and gain intuition about this ordering, you can think about
# the most common case where 0 is first (a = 0). We deviate from this only
Expand Down Expand Up @@ -185,17 +184,28 @@ def choice_to_index(choice, kwargs):
# range = [-2, 5]
# shrink_towards = 2
# index | 0 1 2 3 4 5 6 7
# v | 2 3 4 5 1 0 -1 -2
# v | 2 3 1 4 0 5 -1 -2
#
# ^ with zero weights at index = [0, 2, 6]
# index | 0 1 2 3 4
# v | 3 5 1 0 -2
# v | 3 4 0 5 -2
assert kwargs["weights"] is None or all(
w > 0 for w in kwargs["weights"].values()
), "technically possible but really annoying to support zero weights"
if choice >= shrink_towards:
return choice - shrink_towards
return max_value - shrink_towards + abs(choice - shrink_towards)

# check which side gets exhausted first
if (shrink_towards - min_value) < (max_value - shrink_towards):
# Below shrink_towards gets exhausted first. Equivalent to
# semibounded below
if abs(choice - shrink_towards) <= (shrink_towards - min_value):
return zigzag_index(choice, shrink_towards=shrink_towards)
return choice - min_value
else:
# Above shrink_towards gets exhausted first. Equivalent to semibounded
# above
if abs(choice - shrink_towards) <= (max_value - shrink_towards):
return zigzag_index(choice, shrink_towards=shrink_towards)
return max_value - choice
elif isinstance(choice, bool):
# Ordered by [False, True].
p = kwargs["p"]
Expand Down Expand Up @@ -241,37 +251,30 @@ def choice_from_index(index, ir_type, kwargs):
return zigzag_value(index, shrink_towards=shrink_towards)
elif min_value is not None and max_value is None:
# case: semibounded below

# min_value = -2
# index | 0 1 2 3 4 5 6 7
# v | 0 1 -1 2 -2 3 4 5
if index <= zigzag_index(min_value, shrink_towards=shrink_towards):
return zigzag_value(index, shrink_towards=shrink_towards)
return index + min_value

elif max_value is not None and min_value is None:
# case: semibounded above
if index <= zigzag_index(max_value, shrink_towards=shrink_towards):
return zigzag_value(index, shrink_towards=shrink_towards)
return max_value - index
else:
# case: bounded

# range = [-2, 5]
# shrink_towards = 2
# index | 0 1 2 3 4 5 6 7
# v | 2 3 4 5 1 0 -1 -2
#
# ^ with zero weights at index = [0, 2, 6]
# index | 0 1 2 3 4
# v | 3 5 1 0 -2
if kwargs["weights"] is not None:
assert all(
w > 0 for w in kwargs["weights"].values()
), "possible but really annoying to support zero weightss"
if index <= max_value - shrink_towards:
return shrink_towards + index
return shrink_towards - (index - (max_value - shrink_towards))
assert kwargs["weights"] is None or all(
w > 0 for w in kwargs["weights"].values()
), "possible but really annoying to support zero weights"

if (shrink_towards - min_value) < (max_value - shrink_towards):
# equivalent to semibounded below case
if index <= zigzag_index(min_value, shrink_towards=shrink_towards):
return zigzag_value(index, shrink_towards=shrink_towards)
return index + min_value
else:
# equivalent to semibounded above case
if index <= zigzag_index(max_value, shrink_towards=shrink_towards):
return zigzag_value(index, shrink_towards=shrink_towards)
return max_value - index
elif ir_type == "boolean":
# Ordered by [False, True].
p = kwargs["p"]
Expand Down
30 changes: 30 additions & 0 deletions hypothesis-python/tests/conjecture/test_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,3 +817,33 @@ def test_choice_index_and_value_are_inverses_explicit(ir_type, kwargs, choices):
assert ir_value_equal(
ir_type, choice_from_index(index, ir_type, kwargs), choice
)


@pytest.mark.parametrize(
"kwargs, choices",
[
# unbounded
(integer_kw(), (0, 1, -1, 2, -2, 3, -3)),
(integer_kw(shrink_towards=2), (2, 3, 1, 4, 0, 5, -1, 6, -2)),
# semibounded (below)
(integer_kw(min_value=3), (3, 4, 5, 6, 7)),
(integer_kw(min_value=3, shrink_towards=5), (5, 6, 4, 7, 3, 8, 9)),
(integer_kw(min_value=-3), (0, 1, -1, 2, -2, 3, -3, 4, 5, 6)),
(integer_kw(min_value=-3, shrink_towards=-1), (-1, 0, -2, 1, -3, 2, 3, 4)),
# semibounded (above)
(integer_kw(max_value=3), (0, 1, -1, 2, -2, 3, -3, -4, -5, -6)),
(integer_kw(max_value=3, shrink_towards=1), (1, 2, 0, 3, -1, -2, -3, -4)),
(integer_kw(max_value=-3), (-3, -4, -5, -6, -7)),
(integer_kw(max_value=-3, shrink_towards=-5), (-5, -4, -6, -3, -7, -8, -9)),
# bounded
(integer_kw(-3, 3), (0, 1, -1, 2, -2, 3, -3)),
(integer_kw(-3, 3, shrink_towards=1), (1, 2, 0, 3, -1, -2, -3)),
(integer_kw(-3, 3, shrink_towards=-1), (-1, 0, -2, 1, -3, 2, 3)),
],
ids=repr,
)
def test_integer_choice_index(kwargs, choices):
# explicit test which checks that the order of `choices` matches the index
# order.
for i, choice in enumerate(choices):
assert choice_to_index(choice, kwargs) == i

0 comments on commit e289629

Please sign in to comment.