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];
                }

                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;
        }
    }
}

@@ -4005,32 +4005,301 @@
              %7D%0A%0A
+                // cast to int here rather than making a cumulative curve so that int roundoff error is never greater than 1.%0A                // if we had a douible array and cast the differences to ints, we could accumulate roundoff error up to the number of minutes.%0A
                 

add automated test for weighting functions.