class _OdometerShape(object): # A 0.1% cone - Coefficient to defavor points further away on a shape # when doing stop snapping to shape. The summit angle of the offset cone # is 2 * arctan(K). K = 0.001 def __init__(self, shape): self._shape = shape self._cache = _CacheEntry(None) self._cache_hit = 0 self._cache_miss = 0 if all(pt.shape_dist_traveled != -999999 for pt in shape.points): self._xdist = ContinousPiecewiseLinearFunc() else: self._xdist = None # Normalize the shape here: # 1) dist_traveled to meters # 2) pt_seq to contiguous numbering from 0 ptseq = 0 distance_meters = 0.0 last_pt = None shape.points.sort(key=lambda p: p.shape_pt_sequence) for pt in shape.points: if last_pt is not None: # Note: we do not use distance cache, as most probably # many of the points will be different from each other. distance_meters += orthodromic_distance(last_pt, pt) last_pt = pt pt.shape_pt_sequence = ptseq old_distance = pt.shape_dist_traveled pt.shape_dist_traveled = distance_meters # Remember the distance mapping for stop times if self._xdist: self._xdist.append(old_distance, pt.shape_dist_traveled) ptseq += 1 def reset(self): self._distance = 0 self._istart = 0 self._cache_cursor = self._cache def dist_traveled(self, stop, old_dist_traveled): if old_dist_traveled and self._xdist: # Case 1: we have in the original data shape_dist_traveled # We need to remap from the old scale to the new meter scale return self._xdist.interpolate(old_dist_traveled) else: # Case 2: we do not have original shape_dist_traveled # We need to determine ourselves where in the shape we lie # TODO Implement a cache, this can be slow for lots of trips # and the result is the same for the same pattern # Check the cache first cache_entry = self._cache_cursor.next_entry(stop) if cache_entry is not None: self._cache_cursor = cache_entry self._cache_hit += 1 return cache_entry.distance() min_dist = 1e20 best_i = self._istart best_dist = 0 for i in range(self._istart, len(self._shape.points) - 1): a = self._shape.points[i] b = self._shape.points[i + 1] dist, pdist = orthodromic_seg_distance(stop, a, b) newdist = a.shape_dist_traveled + pdist howfar = newdist - self._distance # Add a slight "cone" offset. There are pathological # cases with backtracking shapes where the best distance # is slightly better way further (for eg 0.01m) than at # the starting point (for eg 0.02m). In that case we should # obviously keep the first point instead of moving too fast # to the shape end. That offset should help for some cases. dist += howfar * self.K if dist < min_dist: min_dist = dist best_i = i best_dist = newdist if best_dist > self._distance: self._distance = best_dist else: delta = self._distance - best_dist if delta > 10: # This is harmless if the backtracking distance is small. # We have lots of false positive (<<1m) due to rounding errors. logger.warn( "Backtracking of %f m detected in shape %s for stop %s (%s) (%f,%f) at distance %f < %f m on segment #[%d-%d]" % (delta, self._shape.shape_id, stop.stop_id, stop.stop_name, stop.stop_lat, stop.stop_lon, best_dist, self._distance, best_i, best_i + 1)) self._istart = best_i self._cache_miss += 1 self._cache_cursor = self._cache_cursor.insert( stop, self._distance) return self._distance def _debug_cache(self): logger.debug("Shape %s: Cache hit: %d, misses: %d" % (self._shape.shape_id, self._cache_hit, self._cache_miss))
class _OdometerShape(object): # A 0.1% cone - Coefficient to defavor points further away on a shape # when doing stop snapping to shape. The summit angle of the offset cone # is 2 * arctan(K). K = 0.001 def __init__(self, shape): self._shape = shape self._cache = _CacheEntry(None) self._cache_hit = 0 self._cache_miss = 0 if all(pt.shape_dist_traveled != -999999 for pt in shape.points): self._xdist = ContinousPiecewiseLinearFunc() else: self._xdist = None # Normalize the shape here: # 1) dist_traveled to meters # 2) pt_seq to contiguous numbering from 0 ptseq = 0 distance_meters = 0.0 last_pt = None shape.points.sort(key=lambda p: p.shape_pt_sequence) for pt in shape.points: if last_pt is not None: # Note: we do not use distance cache, as most probably # many of the points will be different from each other. distance_meters += orthodromic_distance(last_pt, pt) last_pt = pt pt.shape_pt_sequence = ptseq old_distance = pt.shape_dist_traveled pt.shape_dist_traveled = distance_meters # Remember the distance mapping for stop times if self._xdist: self._xdist.append(old_distance, pt.shape_dist_traveled) ptseq += 1 def reset(self): self._distance = 0 self._istart = 0 self._cache_cursor = self._cache def dist_traveled(self, stop, old_dist_traveled): if old_dist_traveled and self._xdist: # Case 1: we have in the original data shape_dist_traveled # We need to remap from the old scale to the new meter scale return self._xdist.interpolate(old_dist_traveled) else: # Case 2: we do not have original shape_dist_traveled # We need to determine ourselves where in the shape we lie # TODO Implement a cache, this can be slow for lots of trips # and the result is the same for the same pattern # Check the cache first cache_entry = self._cache_cursor.next_entry(stop) if cache_entry is not None: self._cache_cursor = cache_entry self._cache_hit += 1 return cache_entry.distance() min_dist = 1e20 best_i = self._istart best_dist = 0 for i in range(self._istart, len(self._shape.points) - 1): a = self._shape.points[i] b = self._shape.points[i+1] dist, pdist = orthodromic_seg_distance(stop, a, b) newdist = a.shape_dist_traveled + pdist howfar = newdist - self._distance # Add a slight "cone" offset. There are pathological # cases with backtracking shapes where the best distance # is slightly better way further (for eg 0.01m) than at # the starting point (for eg 0.02m). In that case we should # obviously keep the first point instead of moving too fast # to the shape end. That offset should help for some cases. dist += howfar * self.K if dist < min_dist: min_dist = dist best_i = i best_dist = newdist if best_dist > self._distance: self._distance = best_dist else: delta = self._distance - best_dist if delta > 10: # This is harmless if the backtracking distance is small. # We have lots of false positive (<<1m) due to rounding errors. logger.warn("Backtracking of %f m detected in shape %s for stop %s (%s) (%f,%f) at distance %f < %f m on segment #[%d-%d]" % ( delta, self._shape.shape_id, stop.stop_id, stop.stop_name, stop.stop_lat, stop.stop_lon, best_dist, self._distance, best_i, best_i+1)) self._istart = best_i self._cache_miss += 1 self._cache_cursor = self._cache_cursor.insert(stop, self._distance) return self._distance def _debug_cache(self): logger.debug("Shape %s: Cache hit: %d, misses: %d" % (self._shape.shape_id, self._cache_hit, self._cache_miss))
def test_piecewise_linear_interpolation(self): # Empty function f = ContinousPiecewiseLinearFunc() catched = False try: self.assertAlmostEqual(f.interpolate(0), 0, 3) except: catched = True self.assertTrue(catched) # Single point function f = ContinousPiecewiseLinearFunc() f.append(0, 0) self.assertAlmostEqual(f.interpolate(0), 0, 6) self.assertAlmostEqual(f.interpolate(-1), 0, 6) self.assertAlmostEqual(f.interpolate(1), 0, 6) # A simple well-behaved function f = ContinousPiecewiseLinearFunc() f.append(0, 0) f.append(1, 100) self.assertAlmostEqual(f.interpolate(-0.000000001), 0, 6) self.assertAlmostEqual(f.interpolate(0), 0, 6) self.assertAlmostEqual(f.interpolate(0.000000001), 0, 6) self.assertAlmostEqual(f.interpolate(0.5), 50.0, 6) self.assertAlmostEqual(f.interpolate(0.9999999999), 100, 6) self.assertAlmostEqual(f.interpolate(1.0), 100, 6) self.assertAlmostEqual(f.interpolate(1.0000000001), 100, 6) # Function with double-point (infinite derivative) f = ContinousPiecewiseLinearFunc() f.append(0, 0) f.append(1, 0) f.append(1, 100) f.append(2, 100) self.assertAlmostEqual(f.interpolate(0), 0, 6) self.assertAlmostEqual(f.interpolate(1), 100, 6) self.assertAlmostEqual(f.interpolate(-1), 0, 6) # A more complex one f = ContinousPiecewiseLinearFunc() f.append(10, 9000) f.append(20, 10000) f.append(1000, 10980) self.assertAlmostEqual(f.interpolate(5), 9000.0, 6) self.assertAlmostEqual(f.interpolate(15), 9500.0, 6) self.assertAlmostEqual(f.interpolate(40), 10020.0, 3) self.assertAlmostEqual(f.interpolate(1010), 10980.0, 3)