EFH2RunoffTable.java [src/java/m/efh2] Revision:   Date:
package m.efh2;

import java.util.ArrayList;
import java.util.List;

/**
 * lookup table for runoff Q, based on precip depth P and runoff curve number
 */
public class EFH2RunoffTable {
    // NOTE: This is an implementation of the runoff lookup table, Table 2.2, in the
    // Engineering Field Handbook, chapter 2. (EFH2)

    private static final List<RunoffRecord> runoffTable = new ArrayList<RunoffRecord>();
    private static double minP,  maxP;


    static {
        minP = 1.0;
        runoffTable.add(new RunoffRecord(1.0, new double[]{0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.03, 0.08, 0.17, 0.32, 0.56}));
        runoffTable.add(new RunoffRecord(1.2, new double[]{0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.03, 0.07, 0.15, 0.27, 0.46, 0.74}));
        runoffTable.add(new RunoffRecord(1.4, new double[]{0.00, 0.00, 0.00, 0.00, 0.00, 0.02, 0.06, 0.13, 0.24, 0.39, 0.61, 0.92}));
        runoffTable.add(new RunoffRecord(1.6, new double[]{0.00, 0.00, 0.00, 0.00, 0.01, 0.05, 0.11, 0.20, 0.34, 0.52, 0.76, 1.11}));
        runoffTable.add(new RunoffRecord(1.8, new double[]{0.00, 0.00, 0.00, 0.00, 0.03, 0.09, 0.17, 0.29, 0.44, 0.65, 0.93, 1.29}));
        runoffTable.add(new RunoffRecord(2.0, new double[]{0.00, 0.00, 0.00, 0.02, 0.06, 0.14, 0.24, 0.38, 0.56, 0.80, 1.09, 1.48}));
        runoffTable.add(new RunoffRecord(2.5, new double[]{0.00, 0.00, 0.02, 0.08, 0.17, 0.30, 0.46, 0.65, 0.89, 1.18, 1.53, 1.96}));
        runoffTable.add(new RunoffRecord(3.0, new double[]{0.00, 0.02, 0.09, 0.19, 0.33, 0.51, 0.71, 0.96, 1.25, 1.59, 1.98, 2.45}));
        runoffTable.add(new RunoffRecord(3.5, new double[]{0.02, 0.08, 0.20, 0.35, 0.53, 0.75, 1.01, 1.30, 1.64, 2.02, 2.45, 2.94}));
        runoffTable.add(new RunoffRecord(4.0, new double[]{0.06, 0.18, 0.33, 0.53, 0.76, 1.03, 1.33, 1.67, 2.04, 2.46, 2.92, 3.43}));
        runoffTable.add(new RunoffRecord(4.5, new double[]{0.14, 0.30, 0.50, 0.74, 1.02, 1.33, 1.67, 2.05, 2.46, 2.91, 3.40, 3.92}));
        runoffTable.add(new RunoffRecord(5.0, new double[]{0.24, 0.44, 0.69, 0.98, 1.30, 1.65, 2.04, 2.45, 2.89, 3.37, 3.88, 4.42}));
        runoffTable.add(new RunoffRecord(6.0, new double[]{0.50, 0.80, 1.14, 1.52, 1.92, 2.35, 2.81, 3.28, 3.78, 4.30, 4.85, 5.41}));
        runoffTable.add(new RunoffRecord(7.0, new double[]{0.84, 1.24, 1.68, 2.12, 2.60, 3.10, 3.62, 4.15, 4.69, 5.25, 5.82, 6.41}));
        runoffTable.add(new RunoffRecord(8.0, new double[]{1.25, 1.74, 2.25, 2.78, 3.33, 3.89, 4.46, 5.04, 5.63, 6.21, 6.81, 7.40}));
        runoffTable.add(new RunoffRecord(9.0, new double[]{1.71, 2.29, 2.88, 3.49, 4.10, 4.72, 5.33, 5.95, 6.57, 7.18, 7.79, 8.40}));
        runoffTable.add(new RunoffRecord(10.0, new double[]{2.23, 2.89, 3.56, 4.23, 4.90, 5.56, 6.22, 6.88, 7.52, 8.16, 8.78, 9.40}));
        runoffTable.add(new RunoffRecord(11.0, new double[]{2.78, 3.52, 4.26, 5.00, 5.72, 6.43, 7.13, 7.81, 8.48, 9.13, 9.77, 10.39}));
        runoffTable.add(new RunoffRecord(12.0, new double[]{3.38, 4.19, 5.00, 5.79, 6.56, 7.32, 8.05, 8.76, 9.45, 10.11, 10.76, 11.39}));
        runoffTable.add(new RunoffRecord(13.0, new double[]{4.00, 4.89, 5.76, 6.61, 7.42, 8.21, 8.98, 9.71, 10.42, 11.10, 11.76, 12.39}));
        runoffTable.add(new RunoffRecord(14.0, new double[]{4.65, 5.62, 6.55, 7.44, 8.30, 9.12, 9.91, 10.67, 11.39, 12.08, 12.75, 13.39}));
        runoffTable.add(new RunoffRecord(15.0, new double[]{5.33, 6.36, 7.35, 8.29, 9.19, 10.04, 10.85, 11.63, 12.37, 13.07, 13.74, 14.39}));
        maxP = 15.0;
    }

    /**
     * constructor is private - use the static methods
     */
    private EFH2RunoffTable() {
    }

    public static double lookupQ(double rainfallInches, int runoffCurveNumber) {

        // validate inputs
        double P = rainfallInches;
        if (P < minP) {
            throw new IllegalArgumentException("EFH2 minimum rainfall is " + minP);
        }
        if (P > maxP) {
            throw new IllegalArgumentException("EFH2 maximum rainfall is " + maxP);
        }

        int CN = runoffCurveNumber;
        if (CN < 40) {
            throw new IllegalArgumentException("Runoff Curve Number cannot be < 40");
        }
        if (CN > 95) {
            throw new IllegalArgumentException("Runoff Curve Number cannot be > 95");
        }

        // scan runoff table for equal or bounding P values
        //
        for (int i = 0; i < runoffTable.size(); i++) {
            RunoffRecord curr = runoffTable.get(i);
            if (Math.abs(P - curr.getP()) < 0.01) {
                // approximately equal - use single row
                return curr.getQ(CN);
            }
            if (P < curr.getP()) {
                continue;
            }
            if (P > curr.getP()) {
                if (i + 1 < runoffTable.size()) {
                    RunoffRecord next = runoffTable.get(i + 1);
                    if (P < next.getP()) {
                        // interpolate between rows
                        double currQ = curr.getQ(CN);
                        double nextQ = next.getQ(CN);
                        double Q = currQ + (nextQ - currQ) * (P - curr.getP()) / (next.getP() - curr.getP());
                        return Q;
                    }
                }
            }
        }
        // should not be possible to get here
        return Double.NaN;
    }

    /**
     * inner class defining single row in runoff table
     */
    private static class RunoffRecord {

        /** rainfall depth, inches */
        private double P;
        /** runoff depth, inches, for various values of CN */
        private double[] Q;

        public RunoffRecord(double p, double[] q) {
            P = p;
            Q = q;
        }

        public double getP() {
            return P;
        }

        public double getQ(int CN) {
            if (CN < 40) {
                throw new IllegalArgumentException("Runoff Curve Number cannot be < 40");
            }
            if (CN > 95) {
                throw new IllegalArgumentException("Runoff Curve Number cannot be > 95");
            }
            if (CN % 5 == 0) {
                // do direct lookup in array q
                return lookupQ(CN);
            } else {
                // do 2 lookups, interpolate
                int lowCN = (CN / 5) * 5;
                double lower = lookupQ(lowCN);
                double upper = lookupQ(lowCN + 5);
                double Q = lower + (upper - lower) * (CN - lowCN) / 5.0;
                return Q;
            }
        }

        private double lookupQ(int CN) {
            int index = (CN - 40) / 5;
            return Q[index];
        }
    }
}