7 This module contains classes to work with validity intervals. There's a class
8 for a single interval, `IntervalOfValidity` and a class to manage a set of
9 validities, `IoVSet`, which can be used to manipulate iov ranges
13 from itertools
import product
18 Interval of validity class to support set operations like union and
21 An interval of validity is a set of runs for which something is valid. An
22 IntervalOfValidity consists of a `first` valid run and a `final` valid run.
25 The `final` run is inclusive so the the validity is including the final run.
27 Each run is identified by a experiment number and a run number. Accessing
28 `first` or `final` will return a tuple ``(experiment, run)`` but the
29 elements can also be accessed separately with `first_exp`, `first_exp`,
30 `final_exp` and `final_run`.
32 For `final` there's a special case where either the run or both, the run and
33 the experiment number are infinite. This means the validity extends to all
34 values. If only the run number is infinite then it's valid for all further
35 runs in this experiment. If both are infinite the validity extends to everything.
37 For simplicity ``-1`` can be passed in instead of infinity when creating objects.
40 """Create a new object.
42 It can be either instantiated by providing four values or one tuple/list
43 with four values for first_exp, first_run, final_exp, final_run
45 if len(iov) == 1
and isinstance(iov[0], (list, tuple)):
48 raise ValueError(
"A iov should have four values")
53 if math.isinf(self.
__final[0])
and not math.isinf(self.
__final[1]):
54 raise ValueError(f
"Unlimited final experiment but not unlimited run: {self}")
56 raise ValueError(f
"First exp larger than final exp: {self}")
58 raise ValueError(f
"First run larger than final run: {self}")
60 raise ValueError(f
"Negative first exp or run: {self}")
64 """Return an iov that is valid everywhere
66 >>> IntervalOfValidity.always()
73 """Return the first valid experiment,run"""
78 """Return the first valid experiment"""
83 """Return the first valid run"""
88 """Return the final valid experiment,run"""
93 """Return the final valid experiment"""
98 """Return the final valid run"""
102 """Return a printable representation"""
106 """Check for equality"""
107 if not isinstance(other, IntervalOfValidity):
108 return NotImplemented
109 return (self.
__first, self.
__final) == (other.__first, other.__final)
112 """Sort by run values"""
113 if not isinstance(other, IntervalOfValidity):
114 return NotImplemented
115 return (self.
__first, self.
__final) < (other.__first, other.__final)
118 """Intersection between iovs. Will return None if the payloads don't overlap"""
119 if not isinstance(other, IntervalOfValidity):
120 return NotImplemented
124 """Union between iovs. Will return None if the iovs don't overlap or
125 connect to each other"""
126 if not isinstance(other, IntervalOfValidity):
127 return NotImplemented
128 return self.
union(other,
False)
131 """Difference between iovs. Will return None if nothing is left over"""
132 if not isinstance(other, IntervalOfValidity):
133 return NotImplemented
137 """Make object hashable"""
141 """Return a new iov with the validity of the other removed.
142 Will return None if everything is removed.
145 If the other iov is in the middle of the validity we will return a
146 tuple of two new iovs
148 >>> iov1 = IntervalOfValidity(0,0,10,-1)
149 >>> iov2 = IntervalOfValidity(5,0,5,-1)
151 ((0, 0, 4, inf), (6, 0, 10, inf))
153 if other.first <= self.
first and other.final >= self.
final:
156 if other.first > self.
first and other.final < self.
final:
162 if other.first <= self.
final and other.final >= self.
first:
164 if self.
first < other.first:
165 end_run = other.first_run - 1
166 end_exp = other.first_exp
if end_run >= 0
else other.first_exp - 1
169 start_run = other.final_run + 1
170 start_exp = other.final_exp
171 if math.isinf(other.final_run):
179 """Intersection with another iov.
181 Will return None if the payloads don't overlap
183 >>> iov1 = IntervalOfValidity(1,0,2,5)
184 >>> iov2 = IntervalOfValidity(2,0,2,-1)
185 >>> iov3 = IntervalOfValidity(2,10,5,-1)
186 >>> iov1.intersect(iov2)
188 >>> iov2.intersect(iov3)
190 >>> iov3.intersect(iov1) is None
194 if other.first <= self.
final and other.final >= self.
first:
198 def union(self, other, allow_startone=False):
200 Return the union with another iov.
202 >>> iov1 = IntervalOfValidity(1,0,1,-1)
203 >>> iov2 = IntervalOfValidity(2,0,2,-1)
204 >>> iov3 = IntervalOfValidity(2,10,5,-1)
209 >>> iov3.union(iov1) is None
213 This method will return None if the iovs don't overlap or connect to
214 each other as no union can be formed.
217 other (IntervalOfValidity): IoV to calculate the union with
218 allow_startone (bool): If True we will consider run 0 and run 1 the
219 first run in an experiment. This means that if one of the iovs has
220 un unlimited final run it can be joined with the other iov if the
221 experiment number increases and the iov starts at run 0 and 1. If
222 this is False just run 0 is considered the next run.
224 >>> iov1 = IntervalOfValidity(0,0,0,-1)
225 >>> iov2 = IntervalOfValidity(1,1,1,-1)
226 >>> iov1.union(iov2, False) is None
228 >>> iov1.union(iov2, True)
233 if other.first <= self.
final and other.final >= self.
first:
236 for i1, i2
in (self, other), (other, self):
237 if (i1.first == (i2.final_exp, i2.final_run + 1)
or
238 (math.isinf(i2.final_run)
and (i1.first_exp == i2.final_exp + 1)
and
239 (i1.first_run == 0
or allow_startone
and i1.first_run == 1))):
245 """Check if a run is part of the validtiy"""
250 """Check whether the iov is valid until infinity"""
251 return self.
final == (math.inf, math.inf)
255 """Return the iov as a tuple with experiment/run numbers replaced with -1
257 This is mostly helpful where infinity is not supported and is how the
258 intervals are represented in the database.
260 >>> a = IntervalOfValidity.always()
272 This class allows to combine iovs into a set. New iovs can be added with
273 `add()` and will be combined with existing iovs if possible.
275 The final, minimal number of iovs can be obtained with the `iovs` property
282 {(0, 0, 0, 5), (0, 8, 0, 9)}
284 def __init__(self, iterable=None, *, allow_overlaps=False, allow_startone=False):
287 >>> a = IoVSet([IntervalOfValidity(3,6,3,-1), (0,0,3,5)])
292 iterable: if not None it should be an iterable of IntervalOfValidity
293 objects or anything that can be converted to an IntervalOfValidity.
294 allow_overlaps (bool): If False adding which overlaps with any
295 existing iov in the set will raise a ValueError.
296 allow_startone (bool): If True also join iovs if one covers the
297 whole experiment and the next one starts at run 1 in the next
298 experiment. If False they will only be joined if the next one
308 if iterable
is not None:
309 for element
in iterable:
312 def add(self, iov, allow_overlaps=None):
314 Add a new iov to the set.
316 The new iov be combined with existing iovs if possible. After the
317 operation the set will contain the minimal amount of separate iovs
318 possible to represent all added iovs
321 >>> a.add((0, 0, 0, 2))
322 >>> a.add((0, 3, 0, 5))
323 >>> a.add((0, 8, 0, 9))
325 {(0, 0, 0, 5), (0, 8, 0, 9)}
326 >>> a.add(IoVSet([(10, 0, 10, 1), (10, 2, 10, -1)]))
328 {(0, 0, 0, 5), (0, 8, 0, 9), (10, 0, 10, inf)}
330 Be aware, by default it's not possible to add overlapping iovs to the set.
331 This can be changed either on construction or per `add` call using
334 >>> a.add((0, 2, 0, 3))
335 Traceback (most recent call last):
337 ValueError: Overlap between (0, 0, 0, 5) and (0, 2, 0, 3)
338 >>> a.add((0, 2, 0, 3), allow_overlaps=True)
340 {(0, 0, 0, 5), (0, 8, 0, 9), (10, 0, 10, inf)}
343 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
344 set of IoVs to add to this set
345 allow_overlaps (bool): Can be used to override global overlap setting
346 of this set to allow/restrict overlaps for a single insertion
350 This method modifies the set in place
353 if allow_overlaps
is None:
356 if isinstance(iov, IoVSet):
358 self.
add(element, allow_overlaps)
361 if not isinstance(iov, IntervalOfValidity):
364 for existing
in list(self.
__iovs):
366 if (
not allow_overlaps)
and (existing & iov):
367 raise ValueError(f
"Overlap between {existing} and {iov}")
377 if combined
is not None:
386 """Remove an iov or a set of iovs from this set
388 After this operation the set will not be valid for the given iov or set
392 >>> a.add((0,0,10,-1))
393 >>> a.remove((1,0,1,-1))
394 >>> a.remove((5,0,8,5))
396 {(0, 0, 0, inf), (2, 0, 4, inf), (8, 6, 10, inf)}
397 >>> a.remove(IoVSet([(3,0,3,10), (3,11,3,-1)]))
399 {(0, 0, 0, inf), (2, 0, 2, inf), (4, 0, 4, inf), (8, 6, 10, inf)}
402 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
403 set of IoVs to remove from this set
406 This method modifies the set in place
409 if isinstance(iov, IoVSet):
414 if not isinstance(iov, IntervalOfValidity):
417 for existing
in list(self.
__iovs):
418 delta = existing - iov
419 if delta != existing:
421 if isinstance(delta, tuple):
425 elif delta
is not None:
429 """Intersect this set with another set and return a new set
430 which is valid exactly where both sets have been valid before
433 >>> a.add((0,0,10,-1))
434 >>> a.intersect((5,0,20,-1))
436 >>> a.intersect(IoVSet([(0,0,3,-1), (9,0,20,-1)]))
437 {(0, 0, 3, inf), (9, 0, 10, inf)}
440 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
441 set of IoVs to intersect with this set
443 if not isinstance(iov, (IoVSet, IntervalOfValidity)):
445 if isinstance(iov, IntervalOfValidity):
451 for a, b
in product(self.
iovs, iov.iovs):
459 Check if an iov is fully covered by the set
461 >>> a = IoVSet([(0,0,2,-1), (5,0,5,-1)])
462 >>> a.contains((0,0,1,-1))
464 >>> a.contains(IntervalOfValidity(0,0,3,2))
466 >>> a.contains(IoVSet([(0,1,1,23), (5,0,5,23)]))
468 >>> a.contains(IoVSet([(0,1,1,23), (5,0,6,23)]))
470 >>> a.contains((3,0,4,-1))
474 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
475 set of IoVs to be checked
478 True if the full iov or all the iovs in the given set are fully
482 if isinstance(iov, IoVSet):
483 return all(e
in self
for e
in iov)
485 if not isinstance(iov, IntervalOfValidity):
488 for existing
in self.
__iovs:
489 if iov - existing
is None:
494 """Check if the given iov overlaps with this set.
496 In contrast to `contains` this doesn't require the given iov to be fully
497 covered. It's enough if the any run covered by the iov is also covered
500 >>> a = IoVSet([(0,0,2,-1), (5,0,5,-1)])
501 >>> a.overlaps((0,0,1,-1))
503 >>> a.overlaps(IntervalOfValidity(0,0,3,2))
505 >>> a.overlaps(IoVSet([(0,1,1,23), (5,0,5,23)]))
507 >>> a.overlaps(IoVSet([(0,1,1,23), (5,0,6,23)]))
509 >>> a.overlaps((3,0,4,-1))
513 iov (Union[IoVSet, IntervalOfValidity, tuple(int)]): IoV or
514 set of IoVs to be checked
517 True if the iov or any of the iovs in the given set overlap with any
520 if not isinstance(iov, (IoVSet, IntervalOfValidity)):
522 if isinstance(iov, IntervalOfValidity):
525 for a, b
in product(self.
iovs, iov.iovs):
532 """Return a copy of this set"""
534 copy.__iovs = set(self.
__iovs)
538 """Clear all iovs from this set"""
543 """Return the set of valid iovs"""
548 """Return the first run covered by this iov set
550 >>> a = IoVSet([(3,0,3,10), (10,11,10,23), (0,0,2,-1), (5,0,5,-1)])
556 return min(self.
iovs).first
560 """Return the final run covered by this iov set
562 >>> a = IoVSet([(3,0,3,10), (10,11,10,23), (0,0,2,-1), (5,0,5,-1)])
568 return max(self.
iovs).final
572 """Return the gaps in the set: Any area not covered between the first
573 point of validity and the last
575 >>> a = IoVSet([(0,0,2,-1)])
578 >>> b = IoVSet([(0,0,2,-1), (5,0,5,-1)])
581 >>> c = IoVSet([(0,0,2,-1), (5,0,5,-1), (10,3,10,6)])
583 {(3, 0, 4, inf), (6, 0, 10, 2)}
589 return full_range - self
592 """Return True if the set is not empty
596 >>> a.add((0,0,1,-1))
605 return len(self.
__iovs) > 0
608 """Check if an iov is fully covered by the set"""
612 """Return a new set that is the intersection between two sets
614 >>> a = IoVSet([(0,0,1,-1)])
622 Return a new set that is the combination of two sets: The new set will
623 be valid everywhere any of the two sets were valid.
625 No check for overlaps will be performed but the result will inherit the
626 settings for further additions from the first set
628 >>> a = IoVSet([(0,0,1,-1)])
632 {(0, 0, 1, inf), (3, 0, 3, inf)}
635 copy.add(other, allow_overlaps=
True)
640 Return a new set which is only valid for where a is valid but not b.
642 See `remove` but this will not modify the set in place
644 >>> a = IoVSet([(0,0,-1,-1)])
646 {(0, 0, 0, inf), (3, 0, inf, inf)}
647 >>> a - (0,0,3,-1) - (10,0,-1,-1)
649 >>> IoVSet([(0,0,1,-1)]) - (2,0,2,-1)
657 """Loop over the set of iovs"""
661 """Return the number of validity intervals in this set"""
665 """Return a printable representation"""
666 return '{' +
', '.join(str(e)
for e
in sorted(self.
__iovs)) +
'}'