com.github.rinde.rinsim.scenario.measure.MetricsTest.java Source code

Java tutorial

Introduction

Here is the source code for com.github.rinde.rinsim.scenario.measure.MetricsTest.java

Source

/*
 * Copyright (C) 2011-2016 Rinde van Lon, iMinds-DistriNet, KU Leuven
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.rinde.rinsim.scenario.measure;

import static com.github.rinde.rinsim.scenario.measure.Metrics.measureDynamism;
import static com.github.rinde.rinsim.scenario.measure.Metrics.measureLoad;
import static com.github.rinde.rinsim.scenario.measure.Metrics.sum;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;

import java.io.File;
import java.io.IOException;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.math3.distribution.ExponentialDistribution;
import org.apache.commons.math3.random.MersenneTwister;
import org.apache.commons.math3.random.RandomGenerator;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
import org.junit.Test;

import com.github.rinde.rinsim.core.model.pdp.Parcel;
import com.github.rinde.rinsim.core.model.pdp.ParcelDTO;
import com.github.rinde.rinsim.geom.Point;
import com.github.rinde.rinsim.pdptw.common.AddParcelEvent;
import com.github.rinde.rinsim.scenario.generator.TravelTimesUtil;
import com.github.rinde.rinsim.scenario.measure.Metrics.LoadPart;
import com.github.rinde.rinsim.util.TimeWindow;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ContiguousSet;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMultiset;
import com.google.common.collect.Range;
import com.google.common.io.Files;
import com.google.common.math.DoubleMath;

/**
 * @author Rinde van Lon
 *
 */
public class MetricsTest {
    static final double EPSILON = 0.00001;

    /**
     * Test of load metric with specific settings.
     */
    @Test
    public void testLoad1() {
        // distance is 1 km which is traveled in 2 minutes with 30km/h
        final ParcelDTO dto = Parcel.builder(new Point(0, 0), new Point(0, 1))
                .pickupTimeWindow(TimeWindow.create(0, 10)).deliveryTimeWindow(TimeWindow.create(10, 20))
                .neededCapacity(0).orderAnnounceTime(0).serviceDuration(5).buildDTO();

        final List<LoadPart> parts = measureLoad(AddParcelEvent.create(dto), TravelTimesUtil.constant(2L));
        assertEquals(3, parts.size());

        // pickup load in [0,15), duration is 5 minutes, so load is 5/15 = 1/3
        assertEquals(0, parts.get(0).begin());
        assertEquals(1 / 3d, parts.get(0).get(0), EPSILON);
        assertEquals(15, parts.get(0).length());

        // travel load in [5,20), duration is 2 minutes, so load is 2/15
        assertEquals(5, parts.get(1).begin());
        assertEquals(2 / 15d, parts.get(1).get(5), EPSILON);
        assertEquals(15, parts.get(1).length());

        // delivery load in [10,25), duration is 5 minutes, so load is 5/15 =
        // 1/3
        assertEquals(10, parts.get(2).begin());
        assertEquals(1 / 3d, parts.get(2).get(10), EPSILON);
        assertEquals(15, parts.get(2).length());

        // summing results:
        // [0,5) - 5/15
        // [5,10) - 7/15
        // [10,15) - 12/15
        // [15,20) - 7/15
        // [20,25) - 5/15

        final List<Double> load = sum(0, parts, 1);
        checkRange(load, 0, 5, 5 / 15d);
        checkRange(load, 5, 10, 7 / 15d);
        checkRange(load, 10, 15, 12 / 15d);
        checkRange(load, 15, 20, 7 / 15d);
        checkRange(load, 20, 25, 5 / 15d);
        assertEquals(25, load.size());
    }

    /**
     * Test of load metric with specific settings.
     */
    @Test
    public void testLoad2() {
        // distance is 10km which is traveled in 20 minutes with 30km/h
        final ParcelDTO dto = Parcel.builder(new Point(0, 0), new Point(0, 10))
                .pickupTimeWindow(TimeWindow.create(15, 15)).deliveryTimeWindow(TimeWindow.create(15, 15))
                .neededCapacity(0).orderAnnounceTime(0).serviceDuration(5).buildDTO();

        final List<LoadPart> parts = measureLoad(AddParcelEvent.create(dto), TravelTimesUtil.constant(20L));
        assertEquals(3, parts.size());

        // pickup load in [15,20), duration is 5 minutes, so load is 5/5 = 1
        assertEquals(15, parts.get(0).begin());
        assertEquals(1d, parts.get(0).get(15), EPSILON);
        assertEquals(0d, parts.get(0).get(20), EPSILON);
        assertEquals(5, parts.get(0).length());

        // travel load in [20,40), duration is 20 minutes, so load is 20/20 = 1
        assertEquals(20, parts.get(1).begin());
        assertEquals(1, parts.get(1).get(20), EPSILON);
        assertEquals(20, parts.get(1).length());

        // delivery load in [40,45), duration is 5 minutes, so load is 5/5 = 1
        assertEquals(40, parts.get(2).begin());
        assertEquals(1, parts.get(2).get(40), EPSILON);
        assertEquals(5, parts.get(2).length());

        // summing results:
        // [0,15) - 0
        // [15,45) - 1

        final List<Double> load = sum(0, parts, 1);
        checkRange(load, 0, 15, 0);
        checkRange(load, 15, 45, 1);
        assertEquals(45, load.size());
    }

    /**
     * Test of load metric with specific settings.
     */
    @Test
    public void testLoad3() {
        // distance is 3 km which is traveled in 6 minutes with 30km/h
        final ParcelDTO dto = Parcel.builder(new Point(0, 0), new Point(0, 3))
                .pickupTimeWindow(TimeWindow.create(10, 30)).deliveryTimeWindow(TimeWindow.create(50, 75))
                .neededCapacity(0).orderAnnounceTime(0L).pickupDuration(5L).deliveryDuration(5L).buildDTO();

        final List<LoadPart> parts = measureLoad(AddParcelEvent.create(dto), TravelTimesUtil.constant(6L));
        assertEquals(3, parts.size());

        // pickup load in [10,35), duration is 5 minutes, so load is 5/25 = 6/30
        assertEquals(10, parts.get(0).begin());
        assertEquals(6 / 30d, parts.get(0).get(10), EPSILON);
        assertEquals(25, parts.get(0).length());

        // travel load in [15,75), duration is 6 minutes, so load is 6/60 = 3/30
        assertEquals(15, parts.get(1).begin());
        assertEquals(3 / 30d, parts.get(1).get(15), EPSILON);
        assertEquals(60, parts.get(1).length());

        // delivery load in [50,80), duration is 5 minutes, so load is 5/30
        assertEquals(50, parts.get(2).begin());
        assertEquals(5 / 30d, parts.get(2).get(50), EPSILON);
        assertEquals(30, parts.get(2).length());

        // summing results:
        // [00,10) - 0/30
        // [10,15) - 6/30
        // [15,35) - 9/30
        // [35,50) - 3/30
        // [50,75) - 8/30
        // [75,80) - 5/30

        final List<Double> load = sum(0, parts, 1);
        checkRange(load, 0, 10, 0d);
        checkRange(load, 10, 15, 6 / 30d);
        checkRange(load, 15, 35, 9 / 30d);
        checkRange(load, 35, 50, 3 / 30d);
        checkRange(load, 50, 75, 8 / 30d);
        checkRange(load, 75, 80, 5 / 30d);
        assertEquals(80, load.size());
    }

    // checks whether the range [from,to) in list contains value val
    static void checkRange(List<Double> list, int from, int to, double val) {
        for (int i = from; i < to; i++) {
            assertEquals(val, list.get(i), EPSILON);
        }
    }

    static Times generateTimes(RandomGenerator rng, double intensity) {
        final ExponentialDistribution ed = new ExponentialDistribution(1000d / intensity);
        ed.reseedRandomGenerator(rng.nextLong());
        final List<Long> times = newArrayList();

        long sum = 0;
        while (sum < 1000) {
            sum += DoubleMath.roundToLong(ed.sample(), RoundingMode.HALF_DOWN);
            if (sum < 1000) {
                times.add(sum);
            }
        }
        return asTimes(1000, times);
    }

    static class Times {
        public final int length;
        public final ImmutableList<Double> list;

        public Times(int l, List<Double> li) {
            length = l;
            list = ImmutableList.copyOf(li);
        }
    }

    static Times asTimesDouble(int length, List<Double> li) {
        return new Times(length, li);
    }

    static Times asTimes(int length, List<Long> li) {
        final List<Double> newLi = newArrayList();
        for (final long l : li) {
            newLi.add((double) l);
        }
        return new Times(length, newLi);
    }

    static Times asTimes(int length, Long... longs) {
        return asTimes(length, asList(longs));
    }

    @Test
    public void dynamismTest2() {

        final double[] ordersPerHour = { 15d };// , 20d, 50d, 100d, 1000d };

        final StandardDeviation sd = new StandardDeviation();

        final RandomGenerator rng = new MersenneTwister(123L);

        final List<Times> times = newArrayList();
        // for (int i = 0; i < 10; i++) {
        // times.add(generateTimes(rng));
        // }
        times.add(asTimes(1000, 250L, 500L, 750L));
        times.add(asTimes(1000, 100L, 500L, 750L));
        times.add(asTimes(1000, 100L, 200L, 300L, 400L, 500L, 600L, 700L, 800L, 900L));
        times.add(asTimes(1000, 100L, 200L, 300L, 399L, 500L, 600L, 700L, 800L, 900L));
        times.add(asTimes(1000, 10L, 150L, 250L, 350L, 450L, 550L, 650L, 750L, 850L, 950L));
        times.add(asTimes(1000, 50L, 150L, 250L, 350L, 450L, 551L, 650L, 750L, 850L, 950L));
        times.add(asTimes(1000, 250L, 500L, 750L));
        times.add(asTimes(1000, 0L, 50L, 55L, 57L, 59L, 60L, 100L, 150L, 750L));
        times.add(asTimes(1000, 5L, 5L, 5L, 5L));
        times.add(asTimes(1000, 4L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L,
                5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L));
        times.add(asTimes(1000, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L,
                5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L));
        times.add(asTimes(1000, 0L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L,
                5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 999L));
        times.add(asTimes(1000, 500L, 500L, 500L, 500L));
        times.add(asTimes(1000, 5L, 5L, 5L, 5L, 400L, 410L, 430L, 440L, 800L, 810L, 820L, 830L));
        times.add(asTimes(1000, 0L, 0L, 0L));
        times.add(asTimes(1000, 1L, 1L, 1L));
        times.add(asTimes(1000, 999L, 999L, 999L));
        times.add(asTimes(1000, 0L, 0L, 500L, 500L, 999L, 999L));
        times.add(asTimes(1000, 250L, 250L, 500L, 500L, 750L, 750L));
        times.add(asTimes(1000, 250L, 250L, 250L, 500L, 500L, 500L, 750L, 750L, 750L));

        for (int i = 0; i < 10; i++) {
            times.add(generateTimes(rng, 10d));
        }

        for (int i = 0; i < 10; i++) {
            times.add(generateTimes(rng, 30d));
        }
        for (int i = 0; i < 5; i++) {

            final List<Double> ts = generateTimes(rng, 50d).list;

            final List<Double> newTs = newArrayList();
            for (final double l : ts) {
                newTs.add(l);
                newTs.add(Math.min(999, Math.max(0,
                        l + DoubleMath.roundToLong(rng.nextDouble() * 6d - 3d, RoundingMode.HALF_EVEN))));
            }
            times.add(asTimesDouble(1000, newTs));
        }

        for (int i = 0; i < 5; i++) {

            final List<Double> ts = generateTimes(rng, 100d).list;

            final List<Double> newTs = newArrayList();
            System.out.println("num events: " + ts.size());
            for (final double l : ts) {
                newTs.add(l);
                newTs.add(Math.min(999, Math.max(0,
                        l + DoubleMath.roundToLong(rng.nextDouble() * 2d - 1d, RoundingMode.HALF_EVEN))));
                newTs.add(Math.min(999, Math.max(0,
                        l + DoubleMath.roundToLong(rng.nextDouble() * 2d - 1d, RoundingMode.HALF_EVEN))));
            }
            times.add(asTimesDouble(1000, newTs));
        }

        final List<Long> t2 = asList(10L, 30L, 50L, 70L, 90L);
        for (int i = 0; i < 5; i++) {
            final List<Long> c = newArrayList();
            for (int j = 0; j < i + 1; j++) {
                c.addAll(t2);
            }
            Collections.sort(c);
            times.add(asTimes(100, c));
        }

        final List<Long> t = asList(100L, 300L, 500L, 700L, 900L);

        for (int i = 0; i < 15; i++) {
            final List<Long> c = newArrayList();
            for (int j = 0; j < i + 1; j++) {
                c.addAll(t);
            }
            Collections.sort(c);
            times.add(asTimes(1000, c));
        }

        final List<Long> variant = newArrayList();
        variant.addAll(t);
        for (int i = 0; i < 70; i++) {
            variant.add(100L);
        }
        Collections.sort(variant);
        times.add(asTimes(1000, variant));
        checkState(variant.size() == 75);
        checkState(times.get(times.size() - 2).list.size() == 75, "", times.get(times.size() - 2).list.size());

        for (int i = 0; i < 10; i++) {
            times.add(generateTimes(rng, (i + 1) * 100d));
        }

        final ImmutableList<Long> all = ContiguousSet.create(Range.closedOpen(0L, 1000L), DiscreteDomain.longs())
                .asList();

        times.add(asTimes(1000, all));

        final List<Long> more = newArrayList(all);
        for (final long l : all) {
            if (l % 2 == 0) {
                more.add(l);
            }
        }
        Collections.sort(more);
        times.add(asTimes(1000, more));

        final List<Long> more2 = newArrayList(all);
        for (int i = 0; i < 200; i++) {
            more2.add(100L);
        }
        for (int i = 0; i < 100; i++) {
            more2.add(200L);
        }

        Collections.sort(more2);
        final List<Long> newMore = newArrayList();
        for (int i = 0; i < more2.size(); i++) {
            newMore.add(more2.get(i) * 10L);
        }
        times.add(asTimes(1000, more2));
        times.add(asTimes(10000, newMore));

        for (int k = 0; k < 5; k++) {
            final List<Double> ts = newArrayList(generateTimes(rng, 20).list);
            final List<Double> additions = newArrayList();
            for (int i = 0; i < ts.size(); i++) {

                if (i % 3 == 0) {
                    for (int j = 0; j < k; j++) {
                        additions.add(ts.get(i) + rng.nextDouble() * 10);
                    }
                }
            }
            ts.addAll(additions);
            Collections.sort(ts);
            times.add(asTimesDouble(1000, ts));
        }

        final Times regular = asTimes(10, 0L, 1L, 2L, 5L, 6L, 7L, 8L, 9L);

        for (int i = 1; i < 4; i++) {
            final List<Double> newList = newArrayList();
            for (final double l : regular.list) {
                newList.add(l * Math.pow(10, i));
            }
            times.add(asTimesDouble((int) (regular.length * Math.pow(10, i)), newList));
        }

        times.add(asTimes(1000, 250L, 250L, 250L, 500L, 500L, 500L, 750L, 750L, 750L));

        times.add(asTimes(1000, 250L, 500L, 500L, 500L, 500L, 500L, 500L, 500L, 750L));

        times.add(asTimes(1000, 100L, 100L, 200L, 200L, 300L, 300L, 400L, 400L, 500L, 500L, 600L, 600L, 700L, 700L,
                800L, 800L, 900L, 900L));
        times.add(asTimes(1000, 100L, 200L, 200L, 200L, 200L, 200L, 200L, 200L, 200L, 200L, 200L, 300L, 400L, 500L,
                600L, 700L, 800L, 900L));
        times.add(asTimes(1000, 100L, 200L, 300L, 400L, 500L, 600L, 700L, 800L, 800L, 800L, 800L, 800L, 800L, 800L,
                800L, 800L, 800L, 900L));

        // times.subList(1, times.size()).clear();

        for (int j = 0; j < ordersPerHour.length; j++) {
            System.out.println("=========" + ordersPerHour[j] + "=========");
            for (int i = 0; i < times.size(); i++) {

                System.out.println("----- " + i + " -----");
                // System.out.println(times.get(i).length + " " + times.get(i).list);
                // final double dod2 = measureDynamismDistr(times.get(i).list,
                // times.get(i).length);

                final double dod8 = measureDynamism(times.get(i).list, times.get(i).length);

                // final double dod5 = measureDynamism2ndDerivative(times.get(i).list,
                // times.get(i).length);
                // final double dod6 = measureDynDeviationCount(times.get(i).list,
                // times.get(i).length);
                // final double dod7 = chi(times.get(i).list,
                // times.get(i).length);
                // System.out.println(dod);
                // System.out.printf("%1.3f%%\n", dod2 * 100d);
                System.out.printf("%1.3f%%\n", dod8 * 100d);
                // System.out.printf("%1.3f%%\n", dod5 * 100d);
                // System.out.printf("%1.3f%%\n", dod6 * 100d);
                // System.out.printf("%1.3f%%\n", dod7);

                final double name = Math.round(dod8 * 100000d) / 1000d;

                try {
                    final File dest = new File("files/generator/times/orders"
                            + Strings.padStart(Integer.toString(i), 3, '0') + "-" + name + ".times");
                    Files.createParentDirs(dest);
                    Files.write(times.get(i).length + "\n" + Joiner.on("\n").join(times.get(i).list) + "\n", dest,
                            Charsets.UTF_8);
                } catch (final IOException e) {
                    throw new IllegalArgumentException();
                }
            }
        }
    }

    @Test
    public void testDynamismScaleInvariant0() {
        final double expectedDynamism = 0d;
        final List<Integer> numEvents = asList(7, 30, 50, 70, 100, 157, 234, 748, 998, 10000, 100000);
        final int scenarioLength = 1000;
        for (final int num : numEvents) {
            final List<Double> scenario = newArrayList();
            for (int i = 0; i < num; i++) {
                scenario.add(300d);
            }
            final double dyn = measureDynamism(scenario, scenarioLength);
            assertEquals(expectedDynamism, dyn, 0.0001);
        }
    }

    @Test
    public void testDynamismScaleInvariant50() {
        final double expectedDynamism = 0.5d;
        final List<Integer> numEvents = asList(30, 50, 70, 100, 158, 234, 426, 748, 998, 10000, 100000);
        final int scenarioLength = 1000;
        for (final int num : numEvents) {
            final List<Double> scenario = newArrayList();
            final int unique = num / 2;
            final double dist = scenarioLength / (double) unique;
            for (int i = 0; i < num / 2; i++) {
                scenario.add(dist / 2d + i * dist);
                scenario.add(dist / 2d + i * dist);
            }
            final double dyn = measureDynamism(scenario, scenarioLength);
            assertEquals(expectedDynamism, dyn, 0.02);
        }
    }

    /**
     * The metric should be insensitive to the number of events.
     */
    @Test
    public void testDynamismScaleInvariant100() {
        final double expectedDynamism = 1d;
        final List<Integer> numEvents = asList(7, 30, 50, 70, 100, 157, 234, 748, 998, 10000, 100000);
        final int scenarioLength = 1000;
        for (final int num : numEvents) {
            final double dist = scenarioLength / (double) num;
            final List<Double> scenario = newArrayList();
            for (int i = 0; i < num; i++) {
                scenario.add(dist / 2d + i * dist);
            }
            final double dyn = measureDynamism(scenario, scenarioLength);
            assertEquals(expectedDynamism, dyn, 0.0001);
        }
    }

    List<Double> generateUniformScenario(RandomGenerator rng, int numEvents, double scenarioLength) {
        final List<Double> events = newArrayList();
        for (int i = 0; i < numEvents; i++) {
            events.add(rng.nextDouble() * scenarioLength);
        }
        Collections.sort(events);
        return events;
    }

    /**
     * The metric should be time scale invariant, meaning that when the length of
     * the day is scaled together with all event times the metric should give the
     * same value. For example:
     * <ul>
     * <li>10 - 2 3 6 7 9</li>
     * <li>100 - 20 30 60 70 90</li>
     * </ul>
     * <br>
     * should give the same values.
     */
    @Test
    public void testDynamismTimeScaleInvariant() {
        final RandomGenerator rng = new MersenneTwister(123);
        final double startLengthOfDay = 1000;
        final int repetitions = 3;
        final List<Integer> numEvents = asList(200, 300, 400, 500, 600, 700, 800);
        final List<Double> lengthsOfDay = asList(startLengthOfDay, 1300d, 2000d, 4500d, 7600d, 15000d, 60000d,
                10000d, 100000d);
        for (final int events : numEvents) {
            // repetitions
            for (int j = 0; j < repetitions; j++) {
                final List<Double> scenario = newArrayList();
                for (int i = 0; i < events; i++) {
                    scenario.add(rng.nextDouble() * startLengthOfDay);
                }
                Collections.sort(scenario);
                double dod = -1;
                for (final double dayLength : lengthsOfDay) {
                    final List<Double> cur = newArrayList();
                    for (final double i : scenario) {
                        cur.add(i * (dayLength / startLengthOfDay));
                    }
                    final double curDod = measureDynamism(cur, dayLength);
                    if (dod >= 0d) {
                        assertEquals(dod, curDod, 0.001);
                    }
                    dod = curDod;
                }
            }
        }
    }

    /**
     * The metric should be insensitive to shifting all events left or right.
     */
    @Test
    public void testDynamismShiftInvariant() {
        final RandomGenerator rng = new MersenneTwister(123);
        final double lengthOfDay = 1000;
        final int repetitions = 10;
        final List<Integer> numEvents = asList(20, 50, 120, 200, 300, 500, 670, 800);

        for (final int num : numEvents) {
            for (int j = 0; j < repetitions; j++) {
                final List<Double> scenario = newArrayList();
                for (int i = 0; i < num; i++) {
                    scenario.add(rng.nextDouble() * lengthOfDay);
                }
                Collections.sort(scenario);

                final double curDod = measureDynamism(scenario, lengthOfDay);

                // shift left
                final List<Double> leftShiftedScenario = newArrayList();
                final double leftMost = scenario.get(0);
                for (int i = 0; i < num; i++) {
                    leftShiftedScenario.add(scenario.get(i) - leftMost);
                }
                final double leftDod = measureDynamism(leftShiftedScenario, lengthOfDay);

                // shift right
                final List<Double> rightShiftedScenario = newArrayList();
                final double rightMost = lengthOfDay - scenario.get(scenario.size() - 1) - 0.00000001;
                for (int i = 0; i < num; i++) {
                    rightShiftedScenario.add(scenario.get(i) + rightMost);
                }
                final double rightDod = measureDynamism(rightShiftedScenario, lengthOfDay);

                assertEquals(curDod, leftDod, 0.0001);
                assertEquals(curDod, rightDod, 0.0001);
            }
        }
    }

    /**
     * Tests whether histogram is computed correctly.
     */
    @Test
    public void testHistogram() {
        assertEquals(ImmutableSortedMultiset.of(), Metrics.computeHistogram(Arrays.<Double>asList(), .2));

        final List<Double> list = Arrays.asList(1d, 2d, 2d, 1.99999, 3d, 4d);
        assertEquals(ImmutableSortedMultiset.of(0d, 0d, 2d, 2d, 2d, 4d), Metrics.computeHistogram(list, 2));

        final List<Double> list2 = Arrays.asList(1d, 2d, 2d, 1.99999, 3d, 4d);
        assertEquals(ImmutableSortedMultiset.of(1d, 1.5d, 2d, 2d, 3d, 4d), Metrics.computeHistogram(list2, .5));
    }

    /**
     * NaN is not accepted.
     */
    @Test(expected = IllegalArgumentException.class)
    public void testHistogramInvalidInput1() {
        Metrics.computeHistogram(asList(0d, Double.NaN), 3);
    }

    /**
     * Infinity is not accepted.
     */
    @Test(expected = IllegalArgumentException.class)
    public void testHistogramInvalidInput2() {
        Metrics.computeHistogram(asList(0d, Double.POSITIVE_INFINITY), 3);
    }

    static <T> boolean areAllValuesTheSame(List<T> list) {
        if (list.isEmpty()) {
            return true;
        }
        return Collections.frequency(list, list.get(0)) == list.size();
    }
}