Repository navigation
Fix IntervalDict.combine dropping pieces of the domain intersection - #115
Conversation
The sweep introduced in 8db341e iterates d.items() (non-atomic keys) and can advance past a later key that fills an interior gap of an earlier non-atomic key (e.g. a singleton inside a union), silently dropping those points from the combined result. Iterate globally sorted atomic pieces on both sides instead.
Added a test for combining non-atomic intervals with interior pieces in the IntervalDict class.
| items.sort(key=lambda item: (item[0].lower, item[0].left is Bound.OPEN)) | ||
| return iter(items) | ||
|
|
||
| i_iter, o_iter = _atomic_items(d1), _atomic_items(d2) |
|
Hello, Thanks for reporting this issue. I'm a bit afraid that, with this fix, the code for combine is becoming even more complex and will be hard to maintain in the future. I'm not sure it's worth the speed-up :-/ |
|
Thanks for the feedback. I can simplify this by restoring the previous pairwise intersection loop and keeping the regression tests. That version passes all 233 tests locally, but gives up the sweep's speed-up. Would you prefer that approach? |
|
Let's do it step by step :-) I definitely want the tests to be included in our test suite (we never have enough tests anyway ;-) and then I'll have a deeper look at why we get this bug and identify our options :-) |
For now, I would say yes, let's go back to the previous approach (pairwise intersection loop) to stay on the safe side. Then, we can discuss on how to speed-up |
|
Updated as requested: restored the previous pairwise intersection loop and kept all three regression tests. All 233 tests pass locally. Thanks for the guidance. |
|
Thanks a lot! |

Summary
IntervalDict.combineon master (after 8db341e, the unreleased 2.7.0-pre1 performance rewrite) can silently drop pieces of the intersection of the two domains.When a non-atomic key's interior gap is filled by a later key, the new two-pointer sweep iterates
d.items()(non-atomic keys) and advances one side based on upper bounds, so it can exhaust one iterator before visiting the remaining keys of the other side. The affected points simply vanish from the combined result.Reproducer
The same state is reachable through plain assignments: setting
[3,4)and(4,5]to the same value merges them into a single non-atomic key, and assigning[4]afterwards fills the gap.This is a regression: at 5f91bf5 the same code returns
("middle", "right")correctly.Fix
Per maintainer preference, this restores the pairwise intersection loop used before 8db341e (the same implementation as 5f91bf5): the stored keys of both restricted domains are iterated directly, and
howis invoked exactly once for each pair of intersecting stored keys, with the full (possibly non-atomic) intersection. This preserves the documented behavior ("f(x, y) for x in d1, y in d2 for intersecting keys") and the pre-rewrite semantics forpass_interval=True: interval-dependent callbacks receive the full intersection of a key pair rather than individual atomic pieces.This gives up the sweep optimization in favor of the previous O(n*m) pairwise approach, as discussed in review.
Tests
Three regression tests:
test_combine_non_atomic_interval_with_interior_piece(both directions): fails on master, passes with the fix.test_combine_pass_interval_non_atomic_keysandtest_combine_pass_interval_non_atomic_spanning_keys: with non-atomic keys,howis invoked exactly once per intersecting key pair and receives the full union of the intersection. These pass on master and with the fix.The full test suite passes: 233 passed, 11 skipped.