295 def __init__(self, iterable=None, *, allow_overlaps=False, allow_startone=False):
298 >>> a = IoVSet([IntervalOfValidity(3,6,3,-1), (0,0,3,5)])
303 iterable: if not None it should be an iterable of IntervalOfValidity
304 objects or anything that can be converted to an IntervalOfValidity.
305 allow_overlaps (bool): If False adding which overlaps with any
306 existing iov in the set will raise a ValueError.
307 allow_startone (bool): If True also join iovs if one covers the
308 whole experiment and the next one starts at run 1 in the next
309 experiment. If False they will only be joined if the next one
314 ## Whether or not we raise an error on overlaps
315 self.__allow_overlaps = allow_overlaps
316 ## Whether or not run 1 will be also considered the first run when
317 # combining iovs between experiments
318 self.__allow_startone = allow_startone
319 if iterable is not None:
320 for element in iterable:
323 def add(self, iov, allow_overlaps=None):
325 Add a new iov to the set.
327 The new iov be combined with existing iovs if possible. After the
328 operation the set will contain the minimal amount of separate iovs
329 possible to represent all added iovs
332 >>> a.add((0, 0, 0, 2))
333 >>> a.add((0, 3, 0, 5))
334 >>> a.add((0, 8, 0, 9))
336 {(0, 0, 0, 5), (0, 8, 0, 9)}
337 >>> a.add(IoVSet([(10, 0, 10, 1), (10, 2, 10, -1)]))
339 {(0, 0, 0, 5), (0, 8, 0, 9), (10, 0, 10, inf)}
341 Be aware, by default it's not possible to add overlapping iovs to the set.
342 This can be changed either on construction or per `add` call using
345 >>> a.add((0, 2, 0, 3))
346 Traceback (most recent call last):
348 ValueError: Overlap between (0, 0, 0, 5) and (0, 2, 0, 3)
349 >>> a.add((0, 2, 0, 3), allow_overlaps=True)
351 {(0, 0, 0, 5), (0, 8, 0, 9), (10, 0, 10, inf)}
354 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
355 set of IoVs to add to this set
356 allow_overlaps (bool): Can be used to override global overlap setting
357 of this set to allow/restrict overlaps for a single insertion
361 This method modifies the set in place
363 # check whether we override overlap settings
364 if allow_overlaps is None:
365 allow_overlaps = self.__allow_overlaps
366 # we can add a set to a set :D
367 if isinstance(iov, IoVSet):
369 self.add(element, allow_overlaps)
371 # make sure it's actually an IoV, this will raise an error on failure
372 if not isinstance(iov, IntervalOfValidity):
373 iov = IntervalOfValidity(iov)
374 # and now check for all existing iovs ... (but use a copy since we modify the set)
375 for existing in list(self.__iovs):
376 # if there's an overlap to the new iov
377 if (not allow_overlaps) and (existing & iov):
378 raise ValueError(f"Overlap between {existing} and {iov}")
379 # and if they can be combined to a bigger iov
380 combined = existing.union(iov, self.__allow_startone)
381 # if we now have a combined iov, remove the one that we were able to
382 # combine it with from the existing iovs because we now check
383 # against the combined one. Since the only way to add a new iov is
384 # this loop we know all previous existing iovs we checked before
385 # didn't have a union with this new iov or any other existing iovs
386 # so if the just check the remaining iovs against the new combined
387 # one we can cascade combine all iovs in one go.
388 if combined is not None:
389 self.__iovs.remove(existing)
391 # done, we now have a new iov which combines all existing iovs it had an
392 # overlap with and we removed the existing iovs so nothing else to do
393 # but add the iov back in the list
396 def remove(self, iov):
397 """Remove an iov or a set of iovs from this set
399 After this operation the set will not be valid for the given iov or set
403 >>> a.add((0,0,10,-1))
404 >>> a.remove((1,0,1,-1))
405 >>> a.remove((5,0,8,5))
407 {(0, 0, 0, inf), (2, 0, 4, inf), (8, 6, 10, inf)}
408 >>> a.remove(IoVSet([(3,0,3,10), (3,11,3,-1)]))
410 {(0, 0, 0, inf), (2, 0, 2, inf), (4, 0, 4, inf), (8, 6, 10, inf)}
413 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
414 set of IoVs to remove from this set
417 This method modifies the set in place
419 # we can remove a set from a set :D
420 if isinstance(iov, IoVSet):
424 # make sure it's actually an IoV, this will raise an error on failure
425 if not isinstance(iov, IntervalOfValidity):
426 iov = IntervalOfValidity(iov)
427 # and subtract the iov from all existing iovs
428 for existing in list(self.__iovs):
429 delta = existing - iov
430 if delta != existing:
431 self.__iovs.remove(existing)
432 if isinstance(delta, tuple):
433 # got two new iovs, apparently we split the old one
436 elif delta is not None:
437 self.__iovs.add(delta)
439 def intersect(self, iov):
440 """Intersect this set with another set and return a new set
441 which is valid exactly where both sets have been valid before
444 >>> a.add((0,0,10,-1))
445 >>> a.intersect((5,0,20,-1))
447 >>> a.intersect(IoVSet([(0,0,3,-1), (9,0,20,-1)]))
448 {(0, 0, 3, inf), (9, 0, 10, inf)}
451 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
452 set of IoVs to intersect with this set
454 if not isinstance(iov, (IoVSet, IntervalOfValidity)):
455 iov = IntervalOfValidity(iov)
456 if isinstance(iov, IntervalOfValidity):
459 # ok for all combinations a,b from set1 and set2 check the intersection
460 # and if not empty add to the result
462 for a, b in product(self.iovs, iov.iovs):
468 def contains(self, iov):
470 Check if an iov is fully covered by the set
472 >>> a = IoVSet([(0,0,2,-1), (5,0,5,-1)])
473 >>> a.contains((0,0,1,-1))
475 >>> a.contains(IntervalOfValidity(0,0,3,2))
477 >>> a.contains(IoVSet([(0,1,1,23), (5,0,5,23)]))
479 >>> a.contains(IoVSet([(0,1,1,23), (5,0,6,23)]))
481 >>> a.contains((3,0,4,-1))
485 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
486 set of IoVs to be checked
489 True if the full iov or all the iovs in the given set are fully
492 # check if the whole set is in this set: all iovs need to be in here
493 if isinstance(iov, IoVSet):
494 return all(e in self for e in iov)
495 # make sure it's actually an IoV, this will raise an error on failure
496 if not isinstance(iov, IntervalOfValidity):
497 iov = IntervalOfValidity(iov)
498 # and then check all iovs in the set if they cover it
499 for existing in self.__iovs:
500 if iov - existing is None:
504 def overlaps(self, iov):
505 """Check if the given iov overlaps with this set.
507 In contrast to `contains` this doesn't require the given iov to be fully
508 covered. It's enough if the any run covered by the iov is also covered
511 >>> a = IoVSet([(0,0,2,-1), (5,0,5,-1)])
512 >>> a.overlaps((0,0,1,-1))
514 >>> a.overlaps(IntervalOfValidity(0,0,3,2))
516 >>> a.overlaps(IoVSet([(0,1,1,23), (5,0,5,23)]))
518 >>> a.overlaps(IoVSet([(0,1,1,23), (5,0,6,23)]))
520 >>> a.overlaps((3,0,4,-1))
524 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
525 set of IoVs to be checked
528 True if the iov or any of the iovs in the given set overlap with any
531 if not isinstance(iov, (IoVSet, IntervalOfValidity)):
532 iov = IntervalOfValidity(iov)
533 if isinstance(iov, IntervalOfValidity):
536 for a, b in product(self.iovs, iov.iovs):
583 """Return the gaps in the set. Any area not covered between the first
584 point of validity and the last
586 >>> a = IoVSet([(0,0,2,-1)])
589 >>> b = IoVSet([(0,0,2,-1), (5,0,5,-1)])
592 >>> c = IoVSet([(0,0,2,-1), (5,0,5,-1), (10,3,10,6)])
594 {(3, 0, 4, inf), (6, 0, 10, 2)}
596 if len(self.__iovs) < 2:
599 full_range = IoVSet([self.first + self.final])
600 return full_range - self
631 def __or__(self, other):
633 Return a new set that is the combination of two sets: The new set will
634 be valid everywhere any of the two sets were valid.
636 No check for overlaps will be performed but the result will inherit the
637 settings for further additions from the first set
639 >>> a = IoVSet([(0,0,1,-1)])
643 {(0, 0, 1, inf), (3, 0, 3, inf)}
646 copy.add(other, allow_overlaps=True)
649 def __sub__(self, other):
651 Return a new set which is only valid for where a is valid but not b.
653 See `remove` but this will not modify the set in place
655 >>> a = IoVSet([(0,0,-1,-1)])
657 {(0, 0, 0, inf), (3, 0, inf, inf)}
658 >>> a - (0,0,3,-1) - (10,0,-1,-1)
660 >>> IoVSet([(0,0,1,-1)]) - (2,0,2,-1)