You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

158 lines
5.4 KiB

  1. """Interval. Like nilmdb.server.interval, but re-implemented here
  2. in plain Python so clients have easier access to it, and with a few
  3. helper functions.
  4. Intervals are half-open, ie. they include data points with timestamps
  5. [start, end)
  6. """
  7. import nilmdb.utils.time
  8. import nilmdb.utils.iterator
  9. class IntervalError(Exception):
  10. """Error due to interval overlap, etc"""
  11. pass
  12. # Interval
  13. class Interval:
  14. """Represents an interval of time."""
  15. def __init__(self, start, end):
  16. """
  17. 'start' and 'end' are arbitrary numbers that represent time
  18. """
  19. if start >= end:
  20. # Explicitly disallow zero-width intervals (since they're half-open)
  21. raise IntervalError("start %s must precede end %s" % (start, end))
  22. self.start = start
  23. self.end = end
  24. def __repr__(self):
  25. s = repr(self.start) + ", " + repr(self.end)
  26. return self.__class__.__name__ + "(" + s + ")"
  27. def __str__(self):
  28. return ("[" + nilmdb.utils.time.timestamp_to_string(self.start) +
  29. " -> " + nilmdb.utils.time.timestamp_to_string(self.end) + ")")
  30. def human_string(self):
  31. return ("[ " + nilmdb.utils.time.timestamp_to_human(self.start) +
  32. " -> " + nilmdb.utils.time.timestamp_to_human(self.end) + " ]")
  33. # Compare two intervals. If non-equal, order by start then end
  34. def __lt__(self, other):
  35. return (self.start, self.end) < (other.start, other.end)
  36. def __gt__(self, other):
  37. return (self.start, self.end) > (other.start, other.end)
  38. def __le__(self, other):
  39. return (self.start, self.end) <= (other.start, other.end)
  40. def __le__(self, other):
  41. return (self.start, self.end) >= (other.start, other.end)
  42. def __eq__(self, other):
  43. return (self.start, self.end) == (other.start, other.end)
  44. def __ne__(self, other):
  45. return (self.start, self.end) != (other.start, other.end)
  46. def intersects(self, other):
  47. """Return True if two Interval objects intersect"""
  48. if not isinstance(other, Interval):
  49. raise TypeError("need an Interval")
  50. if self.end <= other.start or self.start >= other.end:
  51. return False
  52. return True
  53. def subset(self, start, end):
  54. """Return a new Interval that is a subset of this one"""
  55. # A subclass that tracks additional data might override this.
  56. if start < self.start or end > self.end:
  57. raise IntervalError("not a subset")
  58. return Interval(start, end)
  59. def _interval_math_helper(a, b, op, subset = True):
  60. """Helper for set_difference, intersection functions,
  61. to compute interval subsets based on a math operator on ranges
  62. present in A and B. Subsets are computed from A, or new intervals
  63. are generated if subset = False."""
  64. # Iterate through all starts and ends in sorted order. Add a
  65. # tag to the iterator so that we can figure out which one they
  66. # were, after sorting.
  67. def decorate(it, key_start, key_end):
  68. for i in it:
  69. yield i.start, key_start, i
  70. yield i.end, key_end, i
  71. a_iter = decorate(iter(a), 0, 2)
  72. b_iter = decorate(iter(b), 1, 3)
  73. # Now iterate over the timestamps of each start and end.
  74. # At each point, evaluate which type of end it is, to determine
  75. # how to build up the output intervals.
  76. a_interval = None
  77. in_a = False
  78. in_b = False
  79. out_start = None
  80. for (ts, k, i) in nilmdb.utils.iterator.imerge(a_iter, b_iter):
  81. if k == 0:
  82. a_interval = i
  83. in_a = True
  84. elif k == 1:
  85. in_b = True
  86. elif k == 2:
  87. in_a = False
  88. elif k == 3:
  89. in_b = False
  90. include = op(in_a, in_b)
  91. if include and out_start is None:
  92. out_start = ts
  93. elif not include:
  94. if out_start is not None and out_start != ts:
  95. if subset:
  96. yield a_interval.subset(out_start, ts)
  97. else:
  98. yield Interval(out_start, ts)
  99. out_start = None
  100. def set_difference(a, b):
  101. """
  102. Compute the difference (a \\ b) between the intervals in 'a' and
  103. the intervals in 'b'; i.e., the ranges that are present in 'self'
  104. but not 'other'.
  105. 'a' and 'b' must both be iterables.
  106. Returns a generator that yields each interval in turn.
  107. Output intervals are built as subsets of the intervals in the
  108. first argument (a).
  109. """
  110. return _interval_math_helper(a, b, (lambda a, b: a and not b))
  111. def intersection(a, b):
  112. """
  113. Compute the intersection between the intervals in 'a' and the
  114. intervals in 'b'; i.e., the ranges that are present in both 'a'
  115. and 'b'.
  116. 'a' and 'b' must both be iterables.
  117. Returns a generator that yields each interval in turn.
  118. Output intervals are built as subsets of the intervals in the
  119. first argument (a).
  120. """
  121. return _interval_math_helper(a, b, (lambda a, b: a and b))
  122. def optimize(it):
  123. """
  124. Given an iterable 'it' with intervals, optimize them by joining
  125. together intervals that are adjacent in time, and return a generator
  126. that yields the new intervals.
  127. """
  128. saved_int = None
  129. for interval in it:
  130. if saved_int is not None:
  131. if saved_int.end == interval.start:
  132. interval.start = saved_int.start
  133. else:
  134. yield saved_int
  135. saved_int = interval
  136. if saved_int is not None:
  137. yield saved_int