def add(self, a_range): """ Adds a range to the range set. If this range is not connected to any current ranges, it will place the new range on its own. If there is a connection, any connected ranges will be merged into a single range Parameters ---------- a_range : A Range object The Range to add to the RangeSet Raises ------ ValueError If adding range of type not compatible with previously added ranges TypeError: If not adding a Range """ if not isinstance(a_range, Range): raise TypeError("a_range is not a Range") elif a_range.is_empty(): # Skip if this is an empty range return # Check for compatibility of types if necessary if len(self) > 0: if not (issubclass(a_range.lower_cut.the_type, self.ranges[0].lower_cut.the_type) or \ issubclass(self.ranges[0].lower_cut.the_type, a_range.lower_cut.the_type)): raise ValueError("Range not compatible with previously added ranges") # Get the insertion point (where the lower bound should go), should # this range be added on its own lower_ind = bisect_left(self.lower_cuts, a_range.lower_cut) if len(self) == 0: # Add on its own if there is nothing in the list self.ranges.append(a_range) self.lower_cuts.append(a_range.lower_cut) self.upper_cuts.append(a_range.upper_cut) elif len(self) == lower_ind: if not a_range.is_connected(self.ranges[max(lower_ind - 1, 0)]): # Add on its own if not connected to previous and last self.ranges.insert(lower_ind, a_range) self.lower_cuts.insert(lower_ind, a_range.lower_cut) self.upper_cuts.insert(lower_ind, a_range.upper_cut) else: # If connected with the range below, replace with new range new_lower_cut = min(a_range.lower_cut, self.lower_cuts[max(lower_ind - 1, 0)]) new_upper_cut = max(a_range.upper_cut, self.upper_cuts[max(lower_ind - 1, 0)]) new_range = Range(new_lower_cut, new_upper_cut) self.ranges[-1] = new_range self.lower_cuts[-1] = new_lower_cut self.upper_cuts[-1] = new_upper_cut elif not any((a_range.is_connected(self.ranges[max(lower_ind - 1, 0)]), a_range.is_connected(self.ranges[lower_ind]))): # Add on its own if not connected self.ranges.insert(lower_ind, a_range) self.lower_cuts.insert(lower_ind, a_range.lower_cut) self.upper_cuts.insert(lower_ind, a_range.upper_cut) elif a_range.is_connected(self.ranges[max(lower_ind - 1, 0)]): # If connected with range below new_lower_cut = min(self.lower_cuts[max(lower_ind - 1, 0)], a_range.lower_cut) new_upper_cut = max(a_range.upper_cut, self.upper_cuts[max(lower_ind - 1, 0)]) remove_count = 1 if len(self) == (lower_ind): # If hitting the last range, take the maximum uppercut new_upper_cut = max(new_upper_cut, self.upper_cuts[max(lower_ind - 1, 0)]) else: # If not hitting the last range, go find the upper cut for i in range(max(1, lower_ind), len(self)): if a_range.is_connected(self.ranges[i]): new_upper_cut = max(new_upper_cut, self.upper_cuts[i]) remove_count += 1 else: break # Make the new range new_range = Range(new_lower_cut, new_upper_cut) # Get rid of all overlapping ranges for i in range(remove_count): self.ranges.pop(max(lower_ind - 1, 0)) self.lower_cuts.pop(max(lower_ind - 1, 0)) self.upper_cuts.pop(max(lower_ind - 1, 0)) # Add the new range self.ranges.insert(max(lower_ind - 1, 0), new_range) self.lower_cuts.insert(max(lower_ind - 1, 0), new_range.lower_cut) self.upper_cuts.insert(max(lower_ind - 1, 0), new_range.upper_cut) elif a_range.is_connected(self.ranges[lower_ind]): # If connected with the range above new_lower_cut = min(a_range.lower_cut, self.lower_cuts[lower_ind]) new_upper_cut = max(a_range.upper_cut, self.upper_cuts[lower_ind]) remove_count = 0 if len(self) == (lower_ind + 1): # If hitting the last range, you're done remove_count += 1 else: # Go find the upper cut for i in range(lower_ind, len(self)): if a_range.is_connected(self.ranges[i]): new_upper_cut = max(new_upper_cut, self.upper_cuts[i]) remove_count += 1 else: break # Make the new range new_range = Range(new_lower_cut, new_upper_cut) # Remove the overlapping ranges for i in range(remove_count): self.ranges.pop(lower_ind) self.lower_cuts.pop(lower_ind) self.upper_cuts.pop(lower_ind) # Add the new range self.ranges.insert(lower_ind, new_range) self.lower_cuts.insert(lower_ind, new_range.lower_cut) self.upper_cuts.insert(lower_ind, new_range.upper_cut)
def remove(self, a_range): """ Removes a range from the range set. Parameters ---------- a_range : A Range object The Range to remove from the RangeSet Raises ------ ValueError If removing range of type not compatible with previously added ranges TypeError If not a Range """ if not isinstance(a_range, Range): raise TypeError("a_range is not a Range") elif a_range.is_empty(): # Skip if this is an empty range return # Check for compatibility of types if necessary if len(self) > 0: if not (issubclass(a_range.lower_cut.the_type, self.ranges[0].lower_cut.the_type) or issubclass(self.ranges[0].lower_cut.the_type, a_range.lower_cut.the_type)): raise ValueError("Range not compatible with previously added ranges") # Check if the range actually overlaps with this set if not self.overlaps(a_range): return else: # There's some overlap, so deal with that # Determine where overlap occurs ovlap_lower_ind = max(bisect_left(self.lower_cuts, a_range.lower_cut) - 1, 0) ovlap_upper_ind = bisect_left(self.lower_cuts, a_range.upper_cut) # Create queue of indices marked for removal remove_ranges = deque() # Create queue of ranges to add add_ranges = deque() for i in range(ovlap_lower_ind, ovlap_upper_ind): try: # Get intersection of the ranges intersect = a_range.intersection(self.ranges[i]) if not intersect.is_empty(): if intersect == self.ranges[i]: # Mark range for removal remove_ranges.append(i) elif self.lower_cuts[i] == intersect.lower_cut: # If equal on the left cutpoint, subtract out left # part self.lower_cuts[i] = intersect.upper_cut self.ranges[i] = Range(intersect.upper_cut, self.upper_cuts[i]) elif self.upper_cuts[i] == intersect.upper_cut: # If equal on right cutpoint, subtract out right # part self.upper_cuts[i] = intersect.lower_cut self.ranges[i] = Range(self.lower_cuts[i], intersect.lower_cut) else: # If in the middle, split into two parts, putting both into # add queue and placing the old range index into the removal # queue add_ranges.append(Range(self.lower_cuts[i], intersect.lower_cut)) add_ranges.append(Range(intersect.upper_cut, self.upper_cuts[i])) remove_ranges.append(i) except ValueError: # Continue if no overlap with this range continue # Remove any ranges that are marked for removal while len(remove_ranges) > 0: remove_ind = remove_ranges.pop() self.ranges.pop(remove_ind) self.lower_cuts.pop(remove_ind) self.upper_cuts.pop(remove_ind) # Add any ranges that need to be added while len(add_ranges) > 0: self.add(add_ranges.pop())
def difference(self, otherSet): """ Creates a new RangeSet in which all elements in another RangeSet are taken out of this RangeSet Parameters ---------- otherSet : RangeSet object The RangeSet used for this difference Raises ------ TypeError If the object passed in is not a RangeSet ValueError If the value type of the ranges in the other set not compatible with the range's values Returns ------- RangeSet consisting of the difference of the two sets """ if not isinstance(otherSet, RangeSet): raise TypeError("other_set is not a RangeSet") new_set = RangeSet() for add_range in self.ranges: if otherSet.overlaps(add_range): # Determine where overlap occurs other_lower_ind = max(bisect_left(otherSet.lower_cuts, add_range.lower_cut) - 1, 0) other_upper_ind = bisect_left(otherSet.lower_cuts, add_range.upper_cut) new_lower_cut = add_range.lower_cut new_upper_cut = add_range.upper_cut add = True for i in range(other_lower_ind, other_upper_ind): try: # Get the intersection of the ranges intersect = add_range.intersection(otherSet.ranges[i]) if not intersect.is_empty(): if add_range == intersect: add = False break elif add_range.lower_cut == intersect.lower_cut: # If equal on the left cutpoint, subtract out left # part new_lower_cut = intersect.upper_cut add_range = Range(new_lower_cut, new_upper_cut) elif add_range.upper_cut == intersect.upper_cut: # If equal on right cutpoint, subtract out right # part new_upper_cut = intersect.lower_cut add_range = Range(add_range.lower_cut, new_upper_cut) else: # If in the middle, split into two parts and # add the lower one immediately new_set.add(Range(new_lower_cut, intersect.lower_cut)) new_lower_cut = intersect.upper_cut new_upper_cut = add_range.upper_cut add_range = Range(new_lower_cut, new_upper_cut) except ValueError: continue if add: new_set.add(Range(new_lower_cut, new_upper_cut)) else: new_set.add(add_range) return new_set
def put(self, key, val): """ Creates a mapping from a Range to a value. Note that if the key Range overlaps any existing ranges, it will replace those Range(s) over the intersection Parameters ---------- key : Range object A Range to serve as a key val : value Some value that the Range should map to Raises ------ TypeError If the key is not a Range object """ if not isinstance(key, Range): raise TypeError("key is not a Range") elif key.is_empty(): # Skip if this is an empty range return # Figure out where to the key/value if not self.overlaps(key): # If this range is completely on its own, just insert insert_ind = bisect_left(self.lower_cuts, key.lower_cut) self.ranges.insert(insert_ind, key) self.lower_cuts.insert(insert_ind, key.lower_cut) self.upper_cuts.insert(insert_ind, key.upper_cut) self.items.insert(insert_ind, val) return else: # If this range has some overlap with existing ranges ovlap_lower_ind = max( bisect_left(self.lower_cuts, key.lower_cut) - 1, 0) ovlap_upper_ind = bisect_left(self.lower_cuts, key.upper_cut) # Create queue or indices marked for removal remove_ranges = deque() # Create queue ranges to add add_ranges = deque() # Create queue of items to add add_items = deque() for i in range(ovlap_lower_ind, ovlap_upper_ind): try: # Get intersection of the ranges intersect = key.intersection(self.ranges[i]) if not intersect.is_empty(): if intersect == self.ranges[i]: # Mark range for removal remove_ranges.append(i) elif self.lower_cuts[i] == intersect.lower_cut: # If equal on left cutpoint, subtract out left # part self.lower_cuts[i] = intersect.upper_cut self.ranges[i] = Range(intersect.upper_cut, self.upper_cuts[i]) elif self.upper_cuts[i] == intersect.upper_cut: # If equal on right cutpoint, subtract out # right part self.upper_cuts[i] = intersect.lower_cut self.ranges[i] = Range(self.lower_cuts[i], intersect.lower_cut) else: # If in the middle, split into two parts, putting # both in add queue and placing the old range index # in the remove queue add_ranges.append( Range(self.lower_cuts[i], intersect.lower_cut)) add_ranges.append( Range(intersect.upper_cut, self.upper_cuts[i])) add_items.append(self.items[i]) add_items.append(self.items[i]) remove_ranges.append(i) except ValueError: # Continue if no overlap with this range continue # Remove any ranges that are marked for removal while len(remove_ranges) > 0: remove_ind = remove_ranges.pop() self.ranges.pop(remove_ind) self.lower_cuts.pop(remove_ind) self.upper_cuts.pop(remove_ind) self.items.pop(remove_ind) add_items.append(val) add_ranges.append(key) # Use recursive call to place the pairs, which now # should not overlap with any other ranges while len(add_ranges) > 0: self.put(add_ranges.pop(), add_items.pop())
def iteritems(self, start=None, end=None): """ Iterates over pairs of (Range, value) Parameters ---------- start : comparable, optional The starting point for iterating, inclusive end : comparable, optional The ending point for iterating, inclusive Returns ------- Generator of (Range intersecting [start,end], value), ordered by start point """ if start is None: start = self.lower_cuts[0] else: start = Cut.below_value(start) if end is None: end = self.upper_cuts[-1] else: end = Cut.above_value(end) bounding_range = Range(start, end) # Get the bounding indices ovlapLowerInd = max(bisect_left(self.lower_cuts, start) - 1, 0) ovlapUpperInd = bisect_left(self.lower_cuts, end) # Create queue of values that need to be generated yield_vals = deque() # Create dictionary of values to be generated -> indices containing them vals_inds_dict = {} for i in range(ovlapLowerInd, ovlapUpperInd): # Check if anything can be released from the queue while len(yield_vals) > 0: if vals_inds_dict[yield_vals[0]][-1] < i - 1: # Yield the full range, value. Remove value from queue val = yield_vals.popleft() yield Range( max(self.lower_cuts[vals_inds_dict[val][0]], start), min(self.upper_cuts[vals_inds_dict[val][-1]], end)), val # Remove value from dict del vals_inds_dict[val] else: break try: # Get intersection of the ranges intersect = bounding_range.intersection(self.ranges[i]) if not intersect.is_empty(): # If overlapping with this range, put into queue for val in self.items[i]: if val not in vals_inds_dict: yield_vals.append(val) vals_inds_dict[val] = deque() vals_inds_dict[val].append(i) except ValueError: # Continue if no overlap with this range continue ## Yield remaining values while len(yield_vals) > 0: # Yield the full range, value. Remove value from queue val = yield_vals.popleft() yield Range(max(self.lower_cuts[vals_inds_dict[val][0]], start), min(self.upper_cuts[vals_inds_dict[val][-1]], end)), val # Remove value from dict del vals_inds_dict[val]
def put(self, key, val): """ Creates a mapping from a Range to a value, adding to any existing values over that Range Parameters ---------- key : Range object A Range to serve as a key val : value, hashable Some value that the Range should map to Raises ------ TypeError If the key is not a Range object or value is not hashable """ if not isinstance(key, Range): raise TypeError("key is not a Range") elif not any((isinstance(val, Hashable), self.recurse_add)): raise TypeError("value not hashable") elif key.is_empty(): # Skip if this is an empty range return # Figure out where to the key/value if not self.overlaps(key): # If this range is completely on its own, just insert insert_ind = bisect_left(self.lower_cuts, key.lower_cut) self.ranges.insert(insert_ind, key) self.lower_cuts.insert(insert_ind, key.lower_cut) self.upper_cuts.insert(insert_ind, key.upper_cut) if not isinstance(val, set): self.items.insert(insert_ind, set([val])) else: self.items.insert(insert_ind, val) return else: # If this range has some overlap with existing ranges ovlap_lower_ind = max( bisect_left(self.lower_cuts, key.lower_cut) - 1, 0) ovlap_upper_ind = bisect_left(self.lower_cuts, key.upper_cut) # Create queue ranges to add add_ranges = deque() # Create queue of items to add add_items = deque() # Keep track of next lower cutpoint to add next_lower_cut = key.lower_cut for i in range(ovlap_lower_ind, ovlap_upper_ind): try: # Get intersection of the ranges intersect = key.intersection(self.ranges[i]) if not intersect.is_empty(): # Add in a Range between the next LowerCut and # the beginning of this intersection if necessary if next_lower_cut < intersect.lower_cut: add_ranges.append( Range(next_lower_cut, intersect.lower_cut)) add_items.append(val) next_lower_cut = intersect.lower_cut if intersect == self.ranges[i]: ## If key encompassing existing Range ## # Add item to this range self.items[i].add(val) # Change the next lower cut next_lower_cut = intersect.upper_cut elif self.lower_cuts[i] == intersect.lower_cut: ## If key upper cutpoint enclosed by existing Range ## # Add in the rest of the original Range if self.upper_cuts[i] > intersect.upper_cut: add_ranges.append( Range(intersect.upper_cut, self.upper_cuts[i])) add_items.append(set(self.items[i])) # Define original part to be shorter self.upper_cuts[i] = intersect.upper_cut self.ranges[i] = Range(self.lower_cuts[i], intersect.upper_cut) self.items[i].add(val) # Change the next lower cut next_lower_cut = intersect.upper_cut elif self.upper_cuts[i] == intersect.upper_cut: ## If key lower cutpoint enclosed by existing Range ## # Add in the rest of the original Range if intersect.lower_cut > self.lower_cuts[i]: add_ranges.append( Range(self.lower_cuts[i], intersect.lower_cut)) add_items.append(set(self.items[i])) # Define original part to be shorter self.lower_cuts[i] = intersect.lower_cut self.ranges[i] = Range(self.lower_cuts[i], intersect.upper_cut) self.items[i].add(val) # Change the next lower cut next_lower_cut = intersect.upper_cut else: # If entire key enclosed by existing Range # Add in lower part of original Range add_ranges.append( Range(self.lower_cuts[i], intersect.lower_cut)) add_items.append(set(self.items[i])) # Add in upper part of original Range add_ranges.append( Range(intersect.upper_cut, self.upper_cuts[i])) add_items.append(set(self.items[i])) # Define original part to be middle self.lower_cuts[i] = intersect.lower_cut self.upper_cuts[i] = intersect.upper_cut self.ranges[i] = Range(intersect.lower_cut, intersect.upper_cut) self.items[i].add(val) # Change the next lower cut next_lower_cut = intersect.upper_cut except ValueError: # Continue if no overlap with this range continue # Put in a last range if necessary if next_lower_cut < key.upper_cut: add_ranges.append(Range(next_lower_cut, key.upper_cut)) add_items.append(val) # Use recursive call to place the pairs, which now # should not overlap with any other ranges self.recurse_add = True while len(add_ranges) > 0: self.put(add_ranges.pop(), add_items.pop()) self.recurse_add = False