All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.api.control.model.Distributions Maven / Gradle / Ivy

There is a newer version: 1.0.14
Show newest version
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.api.control.model;

import com.google.api.servicecontrol.v1.Distribution;
import com.google.api.servicecontrol.v1.Distribution.BucketOptionCase;
import com.google.api.servicecontrol.v1.Distribution.Builder;
import com.google.api.servicecontrol.v1.Distribution.ExplicitBuckets;
import com.google.api.servicecontrol.v1.Distribution.ExponentialBuckets;
import com.google.api.servicecontrol.v1.Distribution.LinearBuckets;
import com.google.common.collect.Sets;
import com.google.common.math.DoubleMath;
import com.google.common.primitives.Doubles;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Utility methods for working with {@link Distribution} instances.
 */
public final class Distributions {
  private static final String MSG_BUCKET_COUNTS_MISMATCH = "Bucket counts do not match";
  private static final String MSG_BUCKET_OPTIONS_MISMATCH = "Bucket options do not match";
  private static final String MSG_UNKNOWN_BUCKET_OPTION_TYPE = "Unknown bucket option type";
  private static final String MSG_SOME_BOUNDS_ARE_THE_SAME =
      "Illegal bounds, at least two bounds are the same!";
  private static final String MSG_BAD_DIST_LOW_BUCKET_COUNT =
      "cannot update a distribution with a low bucket count";
  private static final String MSG_DOUBLE_TOO_LOW = "%s should be > %f";
  private static final String MSG_BAD_NUM_FINITE_BUCKETS = "number of finite buckets should be > 0";
  private static final double TOLERANCE = 1e-5;
  private static final Logger log = Logger.getLogger(Distributions.class.getName());

  private Distributions() {}

  /**
   * Creates an {@code Distribution} with {@code ExponentialBuckets}.
   *
   * @param numFiniteBuckets initializes the number of finite buckets
   * @param growthFactor initializes the growth factor
   * @param scale initializes the scale
   * @return a {@code Distribution} with {@code ExponentialBuckets}
   * @throws IllegalArgumentException if a bad input prevents creation.
   */
  public static Distribution createExponential(int numFiniteBuckets, double growthFactor,
      double scale) {
    if (numFiniteBuckets <= 0) {
      throw new IllegalArgumentException(MSG_BAD_NUM_FINITE_BUCKETS);
    }
    if (growthFactor <= 1.0) {
      throw new IllegalArgumentException(String.format(MSG_DOUBLE_TOO_LOW, "growth factor", 1.0));
    }
    if (scale <= 0.0) {
      throw new IllegalArgumentException(String.format(MSG_DOUBLE_TOO_LOW, "scale", 0.0));
    }
    ExponentialBuckets buckets = ExponentialBuckets.newBuilder().setGrowthFactor(growthFactor)
        .setNumFiniteBuckets(numFiniteBuckets).setScale(scale).build();
    Builder builder = Distribution.newBuilder().setExponentialBuckets(buckets);
    for (int i = 0; i < numFiniteBuckets + 2; i++) {
      builder.addBucketCounts(0L);
    }
    return builder.build();
  }

  /**
   * Creates a {@code Distribution} with {@code LinearBuckets}.
   *
   * @param numFiniteBuckets initializes the number of finite buckets
   * @param width initializes the width of each bucket
   * @param offset initializes the offset of the start bucket
   * @return a {@code Distribution} with {@code LinearBuckets}
   * @throws IllegalArgumentException if a bad input prevents creation.
   */
  public static Distribution createLinear(int numFiniteBuckets, double width, double offset) {
    if (numFiniteBuckets <= 0) {
      throw new IllegalArgumentException(MSG_BAD_NUM_FINITE_BUCKETS);
    }
    if (width <= 0.0) {
      throw new IllegalArgumentException(String.format(MSG_DOUBLE_TOO_LOW, "width", 0.0));
    }
    LinearBuckets buckets = LinearBuckets.newBuilder().setOffset(offset).setWidth(width)
        .setNumFiniteBuckets(numFiniteBuckets).build();
    Builder builder = Distribution.newBuilder().setLinearBuckets(buckets);
    for (int i = 0; i < numFiniteBuckets + 2; i++) {
      builder.addBucketCounts(0L);
    }
    return builder.build();
  }

  /**
   * Creates a {@code Distribution} with {@code ExplicitBuckets}.
   *
   * @param bounds initializes the bounds used to define the explicit buckets
   *
   * @return a {@code Distribution} with {@code ExplicitBuckets}
   * @throws IllegalArgumentException if a bad input prevents creation.
   */
  public static Distribution createExplicit(double[] bounds) {
    List allBounds = Doubles.asList(bounds);
    Set uniqueBounds = Sets.newHashSet(allBounds);
    if (allBounds.size() != uniqueBounds.size()) {
      throw new IllegalArgumentException(MSG_SOME_BOUNDS_ARE_THE_SAME);
    }
    Collections.sort(allBounds);
    ExplicitBuckets buckets = ExplicitBuckets.newBuilder().addAllBounds(allBounds).build();
    Builder builder = Distribution.newBuilder().setExplicitBuckets(buckets);
    for (int i = 0; i < allBounds.size() + 1; i++) {
      builder.addBucketCounts(0L);
    }
    return builder.build();
  }

  /**
   * Updates as new distribution that contains value added to an existing one.
   *
   * @param value the sample value
   * @param distribution a {@code Distribution}
   * @return the updated distribution
   */
  public static Distribution addSample(double value, Distribution distribution) {
    Builder builder = distribution.toBuilder();
    switch (distribution.getBucketOptionCase()) {
      case EXPLICIT_BUCKETS:
        updateStatistics(value, builder);
        updateExplicitBuckets(value, builder);
        return builder.build();
      case EXPONENTIAL_BUCKETS:
        updateStatistics(value, builder);
        updateExponentialBuckets(value, builder);
        return builder.build();
      case LINEAR_BUCKETS:
        updateStatistics(value, builder);
        updateLinearBuckets(value, builder);
        return builder.build();
      default:
        throw new IllegalArgumentException(MSG_UNKNOWN_BUCKET_OPTION_TYPE);
    }
  }

  /**
   * Merge {@code prior} with {@code latest}.
   *
   * @param prior a {@code Distribution} instance
   * @param latest a {@code Distribution}, expected to be a later version of {@code prior}
   *
   * @return a new {@code Distribution} that combines the statistics and buckets of the earlier two
   * @throws IllegalArgumentException if the bucket options of {@code prior} and {@code latest}
   *         don't match
   * @throws IllegalArgumentException if the bucket counts of {@code prior} and {@code latest} dont'
   *         match
   */
  public static Distribution merge(Distribution prior, Distribution latest) {
    if (!bucketsNearlyEquals(prior, latest)) {
      throw new IllegalArgumentException(MSG_BUCKET_OPTIONS_MISMATCH);
    }
    if (prior.getBucketCountsCount() != latest.getBucketCountsCount()) {
      throw new IllegalArgumentException(MSG_BUCKET_COUNTS_MISMATCH);
    }
    if (prior.getCount() == 0) {
      return latest;
    }

    // Merge the distribution statistics
    Builder builder = latest.toBuilder();
    long oldCount = latest.getCount();
    double oldMean = latest.getMean();
    builder.setCount(prior.getCount() + oldCount);
    builder.setMaximum(Math.max(prior.getMaximum(), latest.getMaximum()));
    builder.setMinimum(Math.min(prior.getMinimum(), latest.getMinimum()));
    double newMean = (oldCount * oldMean + prior.getCount() * prior.getMean()) / builder.getCount();
    builder.setMean(newMean);
    double oldSumOfSquaredDeviation = latest.getSumOfSquaredDeviation();
    double newSumOfSquaredDeviation = oldSumOfSquaredDeviation + prior.getSumOfSquaredDeviation()
        + (oldCount * Math.pow((builder.getMean() - oldMean), 2))
        + (prior.getCount() * Math.pow((builder.getMean() - prior.getMean()), 2));
    builder.setSumOfSquaredDeviation(newSumOfSquaredDeviation);

    // Merge the bucket counts
    for (int i = 0; i < latest.getBucketCountsCount(); i++) {
      builder.setBucketCounts(i, prior.getBucketCounts(i) + latest.getBucketCounts(i));
    }
    return builder.build();
  }

  private static boolean bucketsNearlyEquals(Distribution a, Distribution b) {
    BucketOptionCase caseA = a.getBucketOptionCase();
    BucketOptionCase caseB = b.getBucketOptionCase();
    if (caseA != caseB) {
      return false;
    }
    switch (caseA) {
      case EXPLICIT_BUCKETS:
        return bucketsNearlyEquals(a.getExplicitBuckets(), b.getExplicitBuckets());
      case EXPONENTIAL_BUCKETS:
        return bucketsNearlyEquals(a.getExponentialBuckets(), b.getExponentialBuckets());
      case LINEAR_BUCKETS:
        return bucketsNearlyEquals(a.getLinearBuckets(), b.getLinearBuckets());
      default:
        return false;
    }
  }

  private static boolean bucketsNearlyEquals(ExplicitBuckets a, ExplicitBuckets b) {
    if (a.getBoundsCount() != b.getBoundsCount()) {
      return false;
    }
    for (int i = 0; i < b.getBoundsCount(); i++) {
      if (!DoubleMath.fuzzyEquals(a.getBounds(i), b.getBounds(i), TOLERANCE)) {
        return false;
      }
    }
    return true;
  }

  private static boolean bucketsNearlyEquals(ExponentialBuckets a, ExponentialBuckets b) {
    return ((a.getNumFiniteBuckets() == b.getNumFiniteBuckets())
        && (DoubleMath.fuzzyEquals(a.getGrowthFactor(), b.getGrowthFactor(), TOLERANCE)
            && (DoubleMath.fuzzyEquals(a.getScale(), b.getScale(), TOLERANCE))));
  }

  private static boolean bucketsNearlyEquals(LinearBuckets a, LinearBuckets b) {
    return ((a.getNumFiniteBuckets() == b.getNumFiniteBuckets())
        && (DoubleMath.fuzzyEquals(a.getWidth(), b.getWidth(), TOLERANCE)
            && (DoubleMath.fuzzyEquals(a.getOffset(), b.getOffset(), TOLERANCE))));
  }

  private static void updateStatistics(double value, Builder distribution) {
    long oldCount = distribution.getCount();
    if (oldCount == 0) {
      distribution.setCount(1);
      distribution.setMaximum(value);
      distribution.setMinimum(value);
      distribution.setMean(value);
      distribution.setSumOfSquaredDeviation(0);
    } else {
      double oldMean = distribution.getMean();
      double newMean = ((oldCount * oldMean) + value) / (oldCount + 1);
      double deltaSumOfSquares = (value - oldMean) * (value - newMean);
      distribution.setCount(oldCount + 1);
      distribution.setMean(newMean);
      distribution.setMaximum(Math.max(value, distribution.getMaximum()));
      distribution.setMinimum(Math.min(value, distribution.getMinimum()));
      distribution
          .setSumOfSquaredDeviation(deltaSumOfSquares + distribution.getSumOfSquaredDeviation());
    }
  }

  private static void updateLinearBuckets(double value, Builder distribution) {
    LinearBuckets buckets = distribution.getLinearBuckets();
    if (distribution.getBucketCountsCount() != buckets.getNumFiniteBuckets() + 2) {
      throw new IllegalArgumentException(MSG_BAD_DIST_LOW_BUCKET_COUNT);
    }

    // Determine the offset the value fits into
    double upper = buckets.getWidth() * buckets.getNumFiniteBuckets() + buckets.getOffset();
    int index = 0;
    if (value >= upper) {
      index = buckets.getNumFiniteBuckets() + 1;
    } else if (value > buckets.getOffset()) {
      index = 1 + ((int) Math.round((value - buckets.getOffset()) / buckets.getWidth()));
    }
    long newCount = distribution.getBucketCounts(index) + 1;
    log.log(Level.FINE, "Updating explicit bucket {0} to {1} for {2}",
        new Object[] {index, newCount, value});
    distribution.setBucketCounts(index, newCount);
  }

  private static void updateExponentialBuckets(double value, Builder distribution) {
    ExponentialBuckets buckets = distribution.getExponentialBuckets();
    if (distribution.getBucketCountsCount() != buckets.getNumFiniteBuckets() + 2) {
      throw new IllegalArgumentException(MSG_BAD_DIST_LOW_BUCKET_COUNT);
    }

    // Determine the offset the value fits into
    int index = 0;
    if (value > buckets.getScale()) {
      index =
          1 + (int) (Math.log(value / buckets.getScale()) / Math.log(buckets.getGrowthFactor()));
      index = Math.min(buckets.getNumFiniteBuckets() + 1, index);
    }
    long newCount = distribution.getBucketCounts(index) + 1;
    if (log.isLoggable(Level.FINE)) {
      log.log(Level.FINE, "Updating explicit bucket {0} to {1} for {2}",
          new Object[] {index, newCount, value});
    }
    distribution.setBucketCounts(index, newCount);
  }

  private static void updateExplicitBuckets(double value, Builder distribution) {
    ExplicitBuckets buckets = distribution.getExplicitBuckets();
    if (distribution.getBucketCountsCount() != buckets.getBoundsCount() + 1) {
      throw new IllegalArgumentException(MSG_BAD_DIST_LOW_BUCKET_COUNT);
    }

    // Determine the offset for the value using Collections.binarySearch.
    //
    // Note that when the value is not in the list the result of
    // Collections.binarySearch is -ve: - (insertion point) - 1.
    int index = Collections.binarySearch(buckets.getBoundsList(), value);
    if (index < 0) {
      index = -index - 1;
    } else {
      index += 1;
    }
    long newCount = distribution.getBucketCounts(index) + 1;
    if (log.isLoggable(Level.FINE)) {
      log.log(Level.FINE, "Updating explicit bucket {0} to {1} for {2}",
          new Object[] {index, newCount, value});
    }
    distribution.setBucketCounts(index, newCount);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy