Skip to content

Commit 9ae95b0

Browse files
committed
fix: disappearing 0sec intervals in LeftDependentToggle
1 parent f54c5d2 commit 9ae95b0

File tree

4 files changed

+663
-88
lines changed

4 files changed

+663
-88
lines changed

execution_engine/task/process/rectangle_cython.pyx

Lines changed: 65 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ DEF SCHAR_MAX = 127
2222
MODULE_IMPLEMENTATION = "cython"
2323

2424
IntervalEvent = typing.Tuple[int, bool, AnyInterval]
25-
IntervalEventWithCount = typing.Tuple[int, bool, AnyInterval, int]
25+
IntervalEventOnTrack = typing.Tuple[int, bool, AnyInterval, int]
2626

2727
def intervals_to_events(
2828
intervals: list[AnyInterval],
@@ -295,10 +295,11 @@ def default_is_same_result(interval_constructor: IntervalConstructor):
295295
== interval_constructor(0, 0, active_intervals2))
296296
return is_same_result
297297
298-
def find_rectangles(all_intervals: list[list[AnyInterval]],
299-
interval_constructor: IntervalConstructor,
300-
is_same_result: SameResult | None = None) \
301-
-> list[AnyInterval]:
298+
def find_rectangles(
299+
all_intervals: list[list[AnyInterval]],
300+
interval_constructor: IntervalConstructor,
301+
is_same_result: SameResult | None = None,
302+
) -> list[AnyInterval]:
302303
"""
303304
Low-level engine for interval construction.
304305

@@ -342,41 +343,76 @@ def find_rectangles(all_intervals: list[list[AnyInterval]],
342343
(time, event, interval, j)
343344
for j, intervals in enumerate(all_intervals)
344345
for interval in intervals
345-
for (time,event) in [(interval.lower, True), (interval.upper, False)]
346+
for (time, event) in [(interval.lower, True), (interval.upper, False)]
346347
]
347-
348348
event_count = len(events)
349349
350350
if event_count == 0:
351351
return []
352352
353353
def compare_events(
354-
event1: IntervalEventWithCount, event2: IntervalEventWithCount
354+
event1: IntervalEventOnTrack, event2: IntervalEventOnTrack
355355
) -> int:
356356
"""
357357
Sorting comparator to ensure we process events in the correct order:
358358
- earlier time first
359359
- if same time and same track, close events before open events
360360
(so we don't incorrectly treat a consecutive interval on the same track
361361
as overlapping).
362+
363+
Index of event1 and event2:
364+
- [0]: time of event
365+
- [1]: opening (True) or closing (False)
366+
- [2]: the interval to which the event belongs
367+
- [3]: track index
362368
"""
363-
if event1[0] < event2[0]: # event1 is earlier
369+
if event1[0] < event2[0]: # event1 is earlier
364370
return -1
365-
elif event2[0] < event1[0]: # event2 is earlier
371+
elif event2[0] < event1[0]: # event2 is earlier
366372
return 1
367-
elif event1[3] == event2[3]: # at the same time and on same track,
368-
if event1[2] is event2[2]: # same interval
369-
return -1 if (event1[1] is True) else 1 # sort open events before open events
370-
else: # different intervals
371-
return -1 if (event1[1] is False) else 1 # sort close events before open events
372-
else: # at the same time, but different tracks => any order is fine
373+
elif event1[3] == event2[3]: # at the same time and on same track,
374+
if event1[2] == event2[2]: # same interval (we don't check for "is" because they might be different objects, but still represent the same interval)
375+
return (
376+
-1 if (event1[1] is True) else 1
377+
) # sort open events before open events
378+
else: # different intervals
379+
return (
380+
-1 if (event1[1] is False) else 1
381+
) # sort close events before open events
382+
else: # at the same time, but different tracks => any order is fine
373383
return 1
374384
375385
# Sort events chronologically according to compare_events
376386
events.sort(key=cmp_to_key(compare_events))
377387
378388
active_intervals: list[GeneralizedInterval] = [None] * track_count
379389
390+
def finalize_interval(
391+
interval_start_time: int,
392+
current_time: int,
393+
interval_start_state: List[GeneralizedInterval],
394+
) -> None:
395+
"""
396+
Appends a new time slice (interval_start_time -> current_time) to 'result_intervals',
397+
ensuring we don't create duplicate adjacency boundaries if the previous slice ends
398+
exactly where the new one starts.
399+
"""
400+
if len(result_intervals) > 0:
401+
previous_result = result_intervals[-1]
402+
if previous_result[1] == interval_start_time:
403+
# Adjust the previous slice so it doesn't overlap or duplicate
404+
result_intervals[-1] = (
405+
previous_result[0],
406+
previous_result[1] - 1,
407+
previous_result[2],
408+
)
409+
410+
# Now finalize the current slice
411+
result_intervals.append(
412+
(interval_start_time, current_time, interval_start_state)
413+
)
414+
415+
380416
def process_events_for_point_in_time(
381417
index: int, point_time: int
382418
) -> Tuple[int, int, int] | None:
@@ -405,32 +441,41 @@ def find_rectangles(all_intervals: list[list[AnyInterval]],
405441
# [START_TIME1, 10:59:59] [11:00:00, END_TIME2]
406442
# have no gap between them and can be considered a single
407443
# continuous interval [START_TIME1, END_TIME2].
408-
if (point_time == time) or (open_ and (point_time == time - 1)):
444+
445+
point_interval_closing = any_open and not open_ and interval.lower == interval.upper == point_time
446+
447+
if ((point_time == time) and not point_interval_closing) or (open_ and (point_time == time - 1)):
409448
if time > high_time:
410449
high_time = time
411450
any_open |= open_
412451
else:
413452
# As soon as we find an event that’s clearly beyond the cluster at point_time,
414453
# we break and return
415-
return i, time, high_time if any_open else high_time + 1
454+
return (
455+
i,
456+
time,
457+
high_time if any_open else high_time + 1,
458+
)
416459
417460
# Opening => set this track’s active interval to the new interval
418461
# Closing => set it to None
419462
active_intervals[track] = interval if open_ else None
463+
464+
# If we exit the loop fully, we used all events
420465
return None
421466
422-
result_intervals = []
423467
# Step through event "clusters" with a common point in time and
424468
# emit result intervals with unchanged interval "payload".
425469
index: int | None = 0
426-
time: int | None = events[0][0]
470+
time: int | None = events[index][0]
427471
interval_start_time: int = time
428472
result_intervals: list[tuple[int, int, List[GeneralizedInterval]]] = []
429473
430474
if time is None:
431475
# No events at all
432476
return []
433477
478+
# process the event at index 0 at the first timepoint
434479
res = process_events_for_point_in_time(index, time)
435480
436481
if res is None:
@@ -440,31 +485,6 @@ def find_rectangles(all_intervals: list[list[AnyInterval]],
440485
441486
interval_start_state = active_intervals.copy()
442487
443-
def finalize_interval(
444-
interval_start_time: int,
445-
current_time: int,
446-
interval_start_state: List[GeneralizedInterval],
447-
) -> None:
448-
"""
449-
Appends a new time slice (interval_start_time -> current_time) to 'result_intervals',
450-
ensuring we don't create duplicate adjacency boundaries if the previous slice ends
451-
exactly where the new one starts.
452-
"""
453-
if len(result_intervals) > 0:
454-
previous_result = result_intervals[-1]
455-
if previous_result[1] == interval_start_time:
456-
# Adjust the previous slice so it doesn't overlap or duplicate
457-
result_intervals[-1] = (
458-
previous_result[0],
459-
previous_result[1] - 1,
460-
previous_result[2],
461-
)
462-
463-
# Now finalize the current slice
464-
result_intervals.append(
465-
(interval_start_time, current_time, interval_start_state)
466-
)
467-
468488
# The main loop: step through event clusters
469489
while True:
470490
res = process_events_for_point_in_time(index, time)

execution_engine/task/process/rectangle_python.py

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
MODULE_IMPLEMENTATION = "python"
1717

1818
IntervalEvent = Tuple[int, bool, AnyInterval]
19-
IntervalEventWithCount = Tuple[int, bool, AnyInterval, int]
19+
IntervalEventOnTrack = Tuple[int, bool, AnyInterval, int]
2020

2121

2222
def intervals_to_events(
@@ -332,7 +332,7 @@ def find_rectangles(
332332
# intervals would get confused.
333333
track_count = len(all_intervals)
334334

335-
events: list[IntervalEventWithCount] = [
335+
events: list[IntervalEventOnTrack] = [
336336
(time, event, interval, j)
337337
for j, intervals in enumerate(all_intervals)
338338
for interval in intervals
@@ -344,21 +344,29 @@ def find_rectangles(
344344
return []
345345

346346
def compare_events(
347-
event1: IntervalEventWithCount, event2: IntervalEventWithCount
347+
event1: IntervalEventOnTrack, event2: IntervalEventOnTrack
348348
) -> int:
349349
"""
350350
Sorting comparator to ensure we process events in the correct order:
351351
- earlier time first
352352
- if same time and same track, close events before open events
353353
(so we don't incorrectly treat a consecutive interval on the same track
354354
as overlapping).
355+
356+
Index of event1 and event2:
357+
- [0]: time of event
358+
- [1]: opening (True) or closing (False)
359+
- [2]: the interval to which the event belongs
360+
- [3]: track index
355361
"""
356362
if event1[0] < event2[0]: # event1 is earlier
357363
return -1
358364
elif event2[0] < event1[0]: # event2 is earlier
359365
return 1
360366
elif event1[3] == event2[3]: # at the same time and on same track,
361-
if event1[2] is event2[2]: # same interval
367+
if (
368+
event1[2] == event2[2]
369+
): # same interval (we don't check for "is" because they might be different objects, but still represent the same interval)
362370
return (
363371
-1 if (event1[1] is True) else 1
364372
) # sort open events before open events
@@ -374,6 +382,31 @@ def compare_events(
374382

375383
active_intervals: list[GeneralizedInterval] = [None] * track_count
376384

385+
def finalize_interval(
386+
interval_start_time: int,
387+
current_time: int,
388+
interval_start_state: List[GeneralizedInterval],
389+
) -> None:
390+
"""
391+
Appends a new time slice (interval_start_time -> current_time) to 'result_intervals',
392+
ensuring we don't create duplicate adjacency boundaries if the previous slice ends
393+
exactly where the new one starts.
394+
"""
395+
if len(result_intervals) > 0:
396+
previous_result = result_intervals[-1]
397+
if previous_result[1] == interval_start_time:
398+
# Adjust the previous slice so it doesn't overlap or duplicate
399+
result_intervals[-1] = (
400+
previous_result[0],
401+
previous_result[1] - 1,
402+
previous_result[2],
403+
)
404+
405+
# Now finalize the current slice
406+
result_intervals.append(
407+
(interval_start_time, current_time, interval_start_state)
408+
)
409+
377410
def process_events_for_point_in_time(
378411
index: int, point_time: int
379412
) -> Tuple[int, int, int] | None:
@@ -402,7 +435,16 @@ def process_events_for_point_in_time(
402435
# [START_TIME1, 10:59:59] [11:00:00, END_TIME2]
403436
# have no gap between them and can be considered a single
404437
# continuous interval [START_TIME1, END_TIME2].
405-
if (point_time == time) or (open_ and (point_time == time - 1)):
438+
439+
point_interval_closing = (
440+
any_open
441+
and not open_
442+
and interval.lower == interval.upper == point_time
443+
)
444+
445+
if ((point_time == time) and not point_interval_closing) or (
446+
open_ and (point_time == time - 1)
447+
):
406448
if time > high_time:
407449
high_time = time
408450
any_open |= open_
@@ -425,14 +467,15 @@ def process_events_for_point_in_time(
425467
# Step through event "clusters" with a common point in time and
426468
# emit result intervals with unchanged interval "payload".
427469
index: int | None = 0
428-
time: int | None = events[0][0]
470+
time: int | None = events[index][0] # type: ignore[index]
429471
interval_start_time: int = cast(int, time)
430472
result_intervals: list[tuple[int, int, List[GeneralizedInterval]]] = []
431473

432474
if time is None:
433475
# No events at all
434476
return []
435477

478+
# process the event at index 0 at the first timepoint
436479
res = process_events_for_point_in_time(cast(int, index), cast(int, time))
437480

438481
if res is None:
@@ -442,31 +485,6 @@ def process_events_for_point_in_time(
442485

443486
interval_start_state = active_intervals.copy()
444487

445-
def finalize_interval(
446-
interval_start_time: int,
447-
current_time: int,
448-
interval_start_state: List[GeneralizedInterval],
449-
) -> None:
450-
"""
451-
Appends a new time slice (interval_start_time -> current_time) to 'result_intervals',
452-
ensuring we don't create duplicate adjacency boundaries if the previous slice ends
453-
exactly where the new one starts.
454-
"""
455-
if len(result_intervals) > 0:
456-
previous_result = result_intervals[-1]
457-
if previous_result[1] == interval_start_time:
458-
# Adjust the previous slice so it doesn't overlap or duplicate
459-
result_intervals[-1] = (
460-
previous_result[0],
461-
previous_result[1] - 1,
462-
previous_result[2],
463-
)
464-
465-
# Now finalize the current slice
466-
result_intervals.append(
467-
(interval_start_time, current_time, interval_start_state)
468-
)
469-
470488
# The main loop: step through event clusters
471489
while True:
472490
res = process_events_for_point_in_time(index, time)

0 commit comments

Comments
 (0)