package org.opentripplanner.analyst.core; import org.apache.commons.math3.util.FastMath; /** * A weighting function, expressing how influential something is according to its distance from you. * * Perhaps this could be a FunctionalInterface so implementations can be defined using Java 8 shorthand, but it * currently has two functions. The separation into two functions was to allow optimizations for simple hard-cutoff * functions, where you can avoid doing a multiplication to determine the final result. I'm not sure eliminating * multiplications by 0 and 1 actually has much of an effect on execution speed, so maybe the functional abstraction * is more important. But then again, maybe we'll always just use the same few functions, in which case the abstraction * is purely to aid comprehension by OTP developers. */ public abstract class WeightingFunction { /** * Weight the counts (binned by second, so countsPerSecond[i] is destinations reachable in i - i + seconds) * and return the output of this weighting function as a _cumulative distribution_. */ public abstract int[] apply (int[] countsPerSecond); /** * The logistic function, a commonly used sigmoid (s-shaped function). * Basically a step function with a smooth rolloff. * Positive values of k cause the function to increase from zero to one, negative values will be inverted. * <br> * k=-1 / 60 gives a rolloff over an interval of ~10 minutes<br> * k=-0.5 / 60 gives a rolloff over an interval of ~20 minutes<br> * k=-2 / 60 contracts the rolloff interval to ~5 minutes<br> */ public static final class Logistic extends WeightingFunction { double x0; // cutoff point double k; // steepness of the curve. negative // the seconds at which the rolloff starts and ends. // (where the weight is equal to 0.999 and 0.001, respectively) int rolloffMin, rolloffMax; // lookup table for weights double[] weights; public Logistic (double steepness) { this.k = steepness; // ln(1/epsilon - 1) / -k is the value at which the weight takes on the value epsilon this.rolloffMin = (int) (Math.log(1 / 0.999 - 1) / -k); this.rolloffMax = (int) (Math.log(1 / 0.001 - 1) / -k); // make a lookup table for the weights, once everything has been normalized by the cutoff weights = new double[rolloffMax - rolloffMin + 1]; for (int i = rolloffMin; i <= rolloffMax; i++) { weights[i - rolloffMin] = 1 / (1 + Math.exp( -k * (i - x0))); } } /** * Apply the logistic weighting function. Uses some slightly clever optimizations. * We have precomputed the weights for the sigmoid centered on 0 and stored them in the weights array, * offset by -rolloffMin. We loop over the values to output and first find the largest * second where the weight is effectively 1. We store a cumulative sum up to that point. * We then run through the next */ @Override public int[] apply(int[] countsPerSecond) { int len = countsPerSecond.length / 60; // the frontier is the highest index for which the weight is effectively 1 // (in seconds) int frontier = 0; // cumulative sum up to the frontier int valueAtFrontier = 0; int[] ret = new int[len]; for (int i = 0; i < len; i++) { int newFrontier = Math.max(0, (i * 60) + rolloffMin); for (int j = frontier; j < newFrontier; j++) { valueAtFrontier += countsPerSecond[j]; } double sum = valueAtFrontier; for (int k = newFrontier; k < i * 60 + rolloffMax + 1 && k < countsPerSecond.length; k++) { sum += weights[k - (i * 60) - rolloffMin] * countsPerSecond[k]; } // cast to int here rather than making a cumulative curve so that int roundoff error is never greater than 1. // if we had a douible array and cast the differences to ints, we could accumulate roundoff error up to the number of minutes. ret[i] = (int) sum; frontier = newFrontier; } // make cumulative curve for (int i = ret.length - 1; i > 0; i--) { ret[i] -= ret[i - 1]; } return ret; } } /** * The typical hard cutoff used to select all objects within an isochrone curve. * WARNING: This can have nefarious effects on accessibility indicators when the opportunity density surface * contains steep gradients (e.g. Manhattan). * Any misalignment in the before/after travel time surfaces near these gradients (for example due to street length * roundoff errors) will create a sharp bidirectional fringe x0 minutes away in the resulting * accessibility indicator surface, frequently in outlying low-accessibility areas where the echo will overwhelm * the local indicator value, showing impossible decreases in accessibility in a clearly superior network. */ public static class SharpCutoff extends WeightingFunction { @Override public int[] apply(int[] countsPerSecond) { int len = countsPerSecond.length / 60; int[] ret = new int[len]; int cumulative = 0; for (int i = 0; i < countsPerSecond.length; i++) { cumulative += countsPerSecond[i]; if (i % 60 == 59) { ret[(int) FastMath.floor(i / 60)] = cumulative; } } // make cumulative curve for (int i = ret.length - 1; i > 0; i--) { ret[i] -= ret[i - 1]; } return ret; } } }