15This module contains classes to work with validity intervals. There's a class
16for a single interval, `IntervalOfValidity` and a class to manage a set of
17validities, `IoVSet`, which can be used to manipulate iov ranges
21from 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.
49 """Create a new object.
51 It can be either instantiated by providing four values or one tuple/list
52 with four values
for first_exp, first_run, final_exp, final_run
54 if len(iov) == 1
and isinstance(iov[0], (list, tuple)):
57 raise ValueError(
"A iov should have four values")
61 self.
__final = tuple(math.inf
if x == -1
else x
for x
in iov[2:])
62 if math.isinf(self.
__final[0])
and not math.isinf(self.
__final[1]):
63 raise ValueError(f
"Unlimited final experiment but not unlimited run: {self}")
65 raise ValueError(f
"First exp larger than final exp: {self}")
67 raise ValueError(f
"First run larger than final run: {self}")
69 raise ValueError(f
"Negative first exp or run: {self}")
73 """Return an iov that is valid everywhere
75 >>> IntervalOfValidity.always()
82 """Return the first valid experiment,run"""
87 """Return the first valid experiment"""
92 """Return the first valid run"""
97 """Return the final valid experiment,run"""
102 """Return the final valid experiment"""
107 """Return the final valid run"""
111 """Return a printable representation"""
115 """Check for equality"""
116 if not isinstance(other, IntervalOfValidity):
117 return NotImplemented
118 return (self.
__first, self.
__final) == (other.__first, other.__final)
121 """Sort by run values"""
122 if not isinstance(other, IntervalOfValidity):
123 return NotImplemented
124 return (self.
__first, self.
__final) < (other.__first, other.__final)
127 """Intersection between iovs. Will return None if the payloads don't overlap"""
128 if not isinstance(other, IntervalOfValidity):
129 return NotImplemented
133 """Union between iovs. Will return None if the iovs don't overlap or
134 connect to each other"""
135 if not isinstance(other, IntervalOfValidity):
136 return NotImplemented
137 return self.
union(other,
False)
140 """Difference between iovs. Will return None if nothing is left over"""
141 if not isinstance(other, IntervalOfValidity):
142 return NotImplemented
146 """Make object hashable"""
150 """Return a new iov with the validity of the other removed.
151 Will return None if everything
is removed.
154 If the other iov
is in the middle of the validity we will
return a
155 tuple of two new iovs
160 ((0, 0, 4, inf), (6, 0, 10, inf))
173 if self.
first < other.first:
174 end_run = other.first_run - 1
175 end_exp = other.first_exp
if end_run >= 0
else other.first_exp - 1
178 start_run = other.final_run + 1
179 start_exp = other.final_exp
180 if math.isinf(other.final_run):
188 """Intersection with another iov.
190 Will return None if the payloads don
't overlap
195 >>> iov1.intersect(iov2)
197 >>> iov2.intersect(iov3)
199 >>> iov3.intersect(iov1) is None
207 def union(self, other, allow_startone=False):
209 Return the union with another iov.
218 >>> iov3.union(iov1)
is None
222 This method will
return None if the iovs don
't overlap or connect to
223 each other as no union can be formed.
226 other (IntervalOfValidity): IoV to calculate the union
with
227 allow_startone (bool): If
True we will consider run 0
and run 1 the
228 first run
in an experiment. This means that
if one of the iovs has
229 un unlimited final run it can be joined
with the other iov
if the
230 experiment number increases
and the iov starts at run 0
and 1. If
231 this
is False just run 0
is considered the next run.
235 >>> iov1.union(iov2,
False)
is None
237 >>> iov1.union(iov2,
True)
245 for i1, i2
in (self, other), (other, self):
246 if (i1.first == (i2.final_exp, i2.final_run + 1)
or
247 (math.isinf(i2.final_run)
and (i1.first_exp == i2.final_exp + 1)
and
248 (i1.first_run == 0
or allow_startone
and i1.first_run == 1))):
254 """Check if a run is part of the validity"""
259 """Check whether the iov is valid until infinity"""
265 """Return the iov as a tuple with experiment/run numbers replaced with -1
267 This is mostly helpful where infinity
is not supported
and is how the
268 intervals are represented
in the database.
270 >>> a = IntervalOfValidity.always()
276 return self.
__first + tuple(-1
if math.isinf(x)
else x
for x
in self.
__final)
282 This class allows to combine
iovs into a set. New iovs can be added
with
283 `
add()`
and will be combined
with existing iovs
if possible.
285 The final, minimal number of iovs can be obtained
with the `iovs` property
292 {(0, 0, 0, 5), (0, 8, 0, 9)}
295 def __init__(self, iterable=None, *, allow_overlaps=False, allow_startone=False):
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
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
364 if allow_overlaps
is None:
367 if isinstance(iov, IoVSet):
369 self.
add(element, allow_overlaps)
372 if not isinstance(iov, IntervalOfValidity):
375 for existing
in list(self.
__iovs):
377 if (
not allow_overlaps)
and (existing & iov):
378 raise ValueError(f
"Overlap between {existing} and {iov}")
388 if combined
is not None:
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
420 if isinstance(iov, IoVSet):
425 if not isinstance(iov, IntervalOfValidity):
428 for existing
in list(self.
__iovs):
429 delta = existing - iov
430 if delta != existing:
432 if isinstance(delta, tuple):
436 elif delta
is not None:
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)):
456 if isinstance(iov, IntervalOfValidity):
462 for a, b
in product(self.
iovs, iov.iovs):
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))
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
493 if isinstance(iov, IoVSet):
494 return all(e
in self
for e
in iov)
496 if not isinstance(iov, IntervalOfValidity):
499 for existing
in self.
__iovs:
500 if iov - existing
is None:
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))
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)):
533 if isinstance(iov, IntervalOfValidity):
536 for a, b
in product(self.
iovs, iov.iovs):
543 """Return a copy of this set"""
545 copy.__iovs = set(self.
__iovs)
549 """Clear all iovs from this set"""
554 """Return the set of valid iovs"""
559 """Return the first run covered by this iov set
561 >>> a = IoVSet([(3,0,3,10), (10,11,10,23), (0,0,2,-1), (5,0,5,-1)])
567 return min(self.
iovs).first
571 """Return the final run covered by this iov set
573 >>> a = IoVSet([(3,0,3,10), (10,11,10,23), (0,0,2,-1), (5,0,5,-1)])
579 return max(self.
iovs).final
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)}
600 return full_range - self
603 """Return True if the set is not empty
607 >>> a.add((0,0,1,-1))
616 return len(self.
__iovs) > 0
619 """Check if an iov is fully covered by the set"""
623 """Return a new set that is the intersection between two sets
625 >>> a = IoVSet([(0,0,1,-1)])
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)
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)
668 """Loop over the set of iovs"""
672 """Return the number of validity intervals in this set"""
676 """Return a printable representation"""
677 return '{' +
', '.join(str(e)
for e
in sorted(self.
__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.