15 This module contains classes to work with validity intervals. There's a class
16 for a single interval, `IntervalOfValidity` and a class to manage a set of
17 validities, `IoVSet`, which can be used to manipulate iov ranges
21 from itertools
import product
26 Interval of validity class to support set operations like union and
29 An interval of validity is a set of runs for which something is valid. An
30 IntervalOfValidity consists of a `first` valid run and a `final` valid run.
33 The `final` run is inclusive so the the validity is including the final run.
35 Each run is identified by a experiment number and a run number. Accessing
36 `first` or `final` will return a tuple ``(experiment, run)`` but the
37 elements can also be accessed separately with `first_exp`, `first_exp`,
38 `final_exp` and `final_run`.
40 For `final` there's a special case where either the run or both, the run and
41 the experiment number are infinite. This means the validity extends to all
42 values. If only the run number is infinite then it's valid for all further
43 runs in this experiment. If both are infinite the validity extends to everything.
45 For simplicity ``-1`` can be passed in instead of infinity when creating objects.
48 """Create a new object.
50 It can be either instantiated by providing four values or one tuple/list
51 with four values for first_exp, first_run, final_exp, final_run
53 if len(iov) == 1
and isinstance(iov[0], (list, tuple)):
56 raise ValueError(
"A iov should have four values")
60 self.
__final__final =
tuple(math.inf
if x == -1
else x
for x
in iov[2:])
61 if math.isinf(self.
__final__final[0])
and not math.isinf(self.
__final__final[1]):
62 raise ValueError(f
"Unlimited final experiment but not unlimited run: {self}")
64 raise ValueError(f
"First exp larger than final exp: {self}")
66 raise ValueError(f
"First run larger than final run: {self}")
68 raise ValueError(f
"Negative first exp or run: {self}")
72 """Return an iov that is valid everywhere
74 >>> IntervalOfValidity.always()
81 """Return the first valid experiment,run"""
86 """Return the first valid experiment"""
91 """Return the first valid run"""
96 """Return the final valid experiment,run"""
101 """Return the final valid experiment"""
106 """Return the final valid run"""
110 """Return a printable representation"""
114 """Check for equality"""
115 if not isinstance(other, IntervalOfValidity):
116 return NotImplemented
117 return (self.
__first__first, self.
__final__final) == (other.__first, other.__final)
120 """Sort by run values"""
121 if not isinstance(other, IntervalOfValidity):
122 return NotImplemented
123 return (self.
__first__first, self.
__final__final) < (other.__first, other.__final)
126 """Intersection between iovs. Will return None if the payloads don't overlap"""
127 if not isinstance(other, IntervalOfValidity):
128 return NotImplemented
132 """Union between iovs. Will return None if the iovs don't overlap or
133 connect to each other"""
134 if not isinstance(other, IntervalOfValidity):
135 return NotImplemented
136 return self.
unionunion(other,
False)
139 """Difference between iovs. Will return None if nothing is left over"""
140 if not isinstance(other, IntervalOfValidity):
141 return NotImplemented
145 """Make object hashable"""
149 """Return a new iov with the validity of the other removed.
150 Will return None if everything is removed.
153 If the other iov is in the middle of the validity we will return a
154 tuple of two new iovs
156 >>> iov1 = IntervalOfValidity(0,0,10,-1)
157 >>> iov2 = IntervalOfValidity(5,0,5,-1)
159 ((0, 0, 4, inf), (6, 0, 10, inf))
161 if other.first <= self.
firstfirst
and other.final >= self.
finalfinalfinal:
164 if other.first > self.
firstfirst
and other.final < self.
finalfinalfinal:
170 if other.first <= self.
finalfinalfinal
and other.final >= self.
firstfirst:
172 if self.
firstfirst < other.first:
173 end_run = other.first_run - 1
174 end_exp = other.first_exp
if end_run >= 0
else other.first_exp - 1
177 start_run = other.final_run + 1
178 start_exp = other.final_exp
179 if math.isinf(other.final_run):
187 """Intersection with another iov.
189 Will return None if the payloads don't overlap
191 >>> iov1 = IntervalOfValidity(1,0,2,5)
192 >>> iov2 = IntervalOfValidity(2,0,2,-1)
193 >>> iov3 = IntervalOfValidity(2,10,5,-1)
194 >>> iov1.intersect(iov2)
196 >>> iov2.intersect(iov3)
198 >>> iov3.intersect(iov1) is None
202 if other.first <= self.
finalfinalfinal
and other.final >= self.
firstfirst:
206 def union(self, other, allow_startone=False):
208 Return the union with another iov.
210 >>> iov1 = IntervalOfValidity(1,0,1,-1)
211 >>> iov2 = IntervalOfValidity(2,0,2,-1)
212 >>> iov3 = IntervalOfValidity(2,10,5,-1)
217 >>> iov3.union(iov1) is None
221 This method will return None if the iovs don't overlap or connect to
222 each other as no union can be formed.
225 other (IntervalOfValidity): IoV to calculate the union with
226 allow_startone (bool): If True we will consider run 0 and run 1 the
227 first run in an experiment. This means that if one of the iovs has
228 un unlimited final run it can be joined with the other iov if the
229 experiment number increases and the iov starts at run 0 and 1. If
230 this is False just run 0 is considered the next run.
232 >>> iov1 = IntervalOfValidity(0,0,0,-1)
233 >>> iov2 = IntervalOfValidity(1,1,1,-1)
234 >>> iov1.union(iov2, False) is None
236 >>> iov1.union(iov2, True)
241 if other.first <= self.
finalfinalfinal
and other.final >= self.
firstfirst:
244 for i1, i2
in (self, other), (other, self):
245 if (i1.first == (i2.final_exp, i2.final_run + 1)
or
246 (math.isinf(i2.final_run)
and (i1.first_exp == i2.final_exp + 1)
and
247 (i1.first_run == 0
or allow_startone
and i1.first_run == 1))):
253 """Check if a run is part of the validtiy"""
258 """Check whether the iov is valid until infinity"""
264 """Return the iov as a tuple with experiment/run numbers replaced with -1
266 This is mostly helpful where infinity is not supported and is how the
267 intervals are represented in the database.
269 >>> a = IntervalOfValidity.always()
275 return self.
__first__first +
tuple(-1
if math.isinf(x)
else x
for x
in self.
__final__final)
281 This class allows to combine iovs into a set. New iovs can be added with
282 `add()` and will be combined with existing iovs if possible.
284 The final, minimal number of iovs can be obtained with the `iovs` property
291 {(0, 0, 0, 5), (0, 8, 0, 9)}
293 def __init__(self, iterable=None, *, allow_overlaps=False, allow_startone=False):
296 >>> a = IoVSet([IntervalOfValidity(3,6,3,-1), (0,0,3,5)])
301 iterable: if not None it should be an iterable of IntervalOfValidity
302 objects or anything that can be converted to an IntervalOfValidity.
303 allow_overlaps (bool): If False adding which overlaps with any
304 existing iov in the set will raise a ValueError.
305 allow_startone (bool): If True also join iovs if one covers the
306 whole experiment and the next one starts at run 1 in the next
307 experiment. If False they will only be joined if the next one
317 if iterable
is not None:
318 for element
in iterable:
321 def add(self, iov, allow_overlaps=None):
323 Add a new iov to the set.
325 The new iov be combined with existing iovs if possible. After the
326 operation the set will contain the minimal amount of separate iovs
327 possible to represent all added iovs
330 >>> a.add((0, 0, 0, 2))
331 >>> a.add((0, 3, 0, 5))
332 >>> a.add((0, 8, 0, 9))
334 {(0, 0, 0, 5), (0, 8, 0, 9)}
335 >>> a.add(IoVSet([(10, 0, 10, 1), (10, 2, 10, -1)]))
337 {(0, 0, 0, 5), (0, 8, 0, 9), (10, 0, 10, inf)}
339 Be aware, by default it's not possible to add overlapping iovs to the set.
340 This can be changed either on construction or per `add` call using
343 >>> a.add((0, 2, 0, 3))
344 Traceback (most recent call last):
346 ValueError: Overlap between (0, 0, 0, 5) and (0, 2, 0, 3)
347 >>> a.add((0, 2, 0, 3), allow_overlaps=True)
349 {(0, 0, 0, 5), (0, 8, 0, 9), (10, 0, 10, inf)}
352 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
353 set of IoVs to add to this set
354 allow_overlaps (bool): Can be used to override global overlap setting
355 of this set to allow/restrict overlaps for a single insertion
359 This method modifies the set in place
362 if allow_overlaps
is None:
365 if isinstance(iov, IoVSet):
367 self.
addadd(element, allow_overlaps)
370 if not isinstance(iov, IntervalOfValidity):
373 for existing
in list(self.
__iovs__iovs):
375 if (
not allow_overlaps)
and (existing & iov):
376 raise ValueError(f
"Overlap between {existing} and {iov}")
386 if combined
is not None:
395 """Remove an iov or a set of iovs from this set
397 After this operation the set will not be valid for the given iov or set
401 >>> a.add((0,0,10,-1))
402 >>> a.remove((1,0,1,-1))
403 >>> a.remove((5,0,8,5))
405 {(0, 0, 0, inf), (2, 0, 4, inf), (8, 6, 10, inf)}
406 >>> a.remove(IoVSet([(3,0,3,10), (3,11,3,-1)]))
408 {(0, 0, 0, inf), (2, 0, 2, inf), (4, 0, 4, inf), (8, 6, 10, inf)}
411 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
412 set of IoVs to remove from this set
415 This method modifies the set in place
418 if isinstance(iov, IoVSet):
420 self.
removeremove(element)
423 if not isinstance(iov, IntervalOfValidity):
426 for existing
in list(self.
__iovs__iovs):
427 delta = existing - iov
428 if delta != existing:
430 if isinstance(delta, tuple):
434 elif delta
is not None:
438 """Intersect this set with another set and return a new set
439 which is valid exactly where both sets have been valid before
442 >>> a.add((0,0,10,-1))
443 >>> a.intersect((5,0,20,-1))
445 >>> a.intersect(IoVSet([(0,0,3,-1), (9,0,20,-1)]))
446 {(0, 0, 3, inf), (9, 0, 10, inf)}
449 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
450 set of IoVs to intersect with this set
452 if not isinstance(iov, (IoVSet, IntervalOfValidity)):
454 if isinstance(iov, IntervalOfValidity):
460 for a, b
in product(self.
iovsiovs, iov.iovs):
468 Check if an iov is fully covered by the set
470 >>> a = IoVSet([(0,0,2,-1), (5,0,5,-1)])
471 >>> a.contains((0,0,1,-1))
473 >>> a.contains(IntervalOfValidity(0,0,3,2))
475 >>> a.contains(IoVSet([(0,1,1,23), (5,0,5,23)]))
477 >>> a.contains(IoVSet([(0,1,1,23), (5,0,6,23)]))
479 >>> a.contains((3,0,4,-1))
483 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
484 set of IoVs to be checked
487 True if the full iov or all the iovs in the given set are fully
491 if isinstance(iov, IoVSet):
492 return all(e
in self
for e
in iov)
494 if not isinstance(iov, IntervalOfValidity):
497 for existing
in self.
__iovs__iovs:
498 if iov - existing
is None:
503 """Check if the given iov overlaps with this set.
505 In contrast to `contains` this doesn't require the given iov to be fully
506 covered. It's enough if the any run covered by the iov is also covered
509 >>> a = IoVSet([(0,0,2,-1), (5,0,5,-1)])
510 >>> a.overlaps((0,0,1,-1))
512 >>> a.overlaps(IntervalOfValidity(0,0,3,2))
514 >>> a.overlaps(IoVSet([(0,1,1,23), (5,0,5,23)]))
516 >>> a.overlaps(IoVSet([(0,1,1,23), (5,0,6,23)]))
518 >>> a.overlaps((3,0,4,-1))
522 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
523 set of IoVs to be checked
526 True if the iov or any of the iovs in the given set overlap with any
529 if not isinstance(iov, (IoVSet, IntervalOfValidity)):
531 if isinstance(iov, IntervalOfValidity):
534 for a, b
in product(self.
iovsiovs, iov.iovs):
541 """Return a copy of this set"""
543 copy.__iovs = set(self.
__iovs__iovs)
547 """Clear all iovs from this set"""
552 """Return the set of valid iovs"""
557 """Return the first run covered by this iov set
559 >>> a = IoVSet([(3,0,3,10), (10,11,10,23), (0,0,2,-1), (5,0,5,-1)])
565 return min(self.
iovsiovs).first
569 """Return the final run covered by this iov set
571 >>> a = IoVSet([(3,0,3,10), (10,11,10,23), (0,0,2,-1), (5,0,5,-1)])
577 return max(self.
iovsiovs).final
581 """Return the gaps in the set: Any area not covered between the first
582 point of validity and the last
584 >>> a = IoVSet([(0,0,2,-1)])
587 >>> b = IoVSet([(0,0,2,-1), (5,0,5,-1)])
590 >>> c = IoVSet([(0,0,2,-1), (5,0,5,-1), (10,3,10,6)])
592 {(3, 0, 4, inf), (6, 0, 10, 2)}
594 if len(self.
__iovs__iovs) < 2:
598 return full_range - self
601 """Return True if the set is not empty
605 >>> a.add((0,0,1,-1))
614 return len(self.
__iovs__iovs) > 0
617 """Check if an iov is fully covered by the set"""
621 """Return a new set that is the intersection between two sets
623 >>> a = IoVSet([(0,0,1,-1)])
631 Return a new set that is the combination of two sets: The new set will
632 be valid everywhere any of the two sets were valid.
634 No check for overlaps will be performed but the result will inherit the
635 settings for further additions from the first set
637 >>> a = IoVSet([(0,0,1,-1)])
641 {(0, 0, 1, inf), (3, 0, 3, inf)}
643 copy = self.
copycopy()
644 copy.add(other, allow_overlaps=
True)
649 Return a new set which is only valid for where a is valid but not b.
651 See `remove` but this will not modify the set in place
653 >>> a = IoVSet([(0,0,-1,-1)])
655 {(0, 0, 0, inf), (3, 0, inf, inf)}
656 >>> a - (0,0,3,-1) - (10,0,-1,-1)
658 >>> IoVSet([(0,0,1,-1)]) - (2,0,2,-1)
661 copy = self.
copycopy()
666 """Loop over the set of iovs"""
667 return iter(self.
__iovs__iovs)
670 """Return the number of validity intervals in this set"""
671 return len(self.
__iovs__iovs)
674 """Return a printable representation"""
675 return '{' +
', '.join(str(e)
for e
in sorted(self.
__iovs__iovs)) +
'}'
def contains(self, exp, run)
def subtract(self, other)
def intersect(self, other)
final
Doxygen complains without this string.
def union(self, other, allow_startone=False)
__first
tuple with the first valid exp, run
__final
tuple with the final valid exp, run
def __contains__(self, iov)
def __init__(self, iterable=None, *allow_overlaps=False, allow_startone=False)
def add(self, iov, allow_overlaps=None)
__allow_startone
Whether or not run 1 will be also considered the first run when combining iovs between experiments.
__allow_overlaps
Whether or not we raise an error on overlaps.