blob: e963fc758f7bd861417cd36165231358bd78d97f [file] [log] [blame]
//
// Copyright 2021 Google LLC
//
// 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.privacy.differentialprivacy;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Math.max;
import static java.lang.Math.min;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.privacy.differentialprivacy.proto.SummaryOuterClass.BoundedQuantilesSummary;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Calculates differentially private quantiles for a collection of values using a quantile tree
* mechanism.
*
* <p>See https://github.com/google/differential-privacy/blob/main/common_docs/Differentially_Private_Quantile_Trees.pdf.
*
* <p>Note: the class is not thread-safe.
*
* <p>For general details and key definitions, see <a href=
* "https://github.com/google/differential-privacy/blob/main/differential_privacy.md#key-definitions">
* this</a> introduction to Differential Privacy.
*/
public class BoundedQuantiles {
public static final double NUMERICAL_TOLERANCE = 1.0e-6;
public static final int DEFAULT_TREE_HEIGHT = 4;
public static final int DEFAULT_BRANCHING_FACTOR = 16;
private static final int ROOT_INDEX = 0;
// Fraction a node needs to contribute to the total count of itself and its siblings to be
// considered during the search for a particular quantile. The idea of gamma is to filter out
// noisy empty nodes. This is a post processing parameter with no privacy implications.
private static final double GAMMA = 0.0075;
private enum ConfidenceIntervalBoundType {
LOWER,
UPPER
};
private static final ConfidenceIntervalBoundType LOWER = ConfidenceIntervalBoundType.LOWER;
private static final ConfidenceIntervalBoundType UPPER = ConfidenceIntervalBoundType.UPPER;
private final Params params;
private final Map<Integer, Long> tree;
private final Map<Integer, Double> noisedTree;
private final int numberOfLeaves;
private final int leftmostLeafIndex;
private AggregationState state = AggregationState.DEFAULT;
private BoundedQuantiles(BoundedQuantiles.Params params) {
this.params = params;
tree = new HashMap<>();
noisedTree = new HashMap<>();
int numberOfNodes =
(int)
((Math.pow(params.branchingFactor(), params.treeHeight() + 1) - 1)
/ (params.branchingFactor() - 1));
numberOfLeaves = (int) Math.pow(params.branchingFactor(), params.treeHeight());
// The following assumes that nodes are indexed in a breadth first fashion from left to right.
leftmostLeafIndex = numberOfNodes - numberOfLeaves;
}
public static BoundedQuantiles.Params.Builder builder() {
return BoundedQuantiles.Params.Builder.newBuilder();
}
/**
* Clamps the input value and adds it to the distribution.
*
* @throws IllegalStateException if this instance of {@link BoundedQuantiles} has already been
* queried or serialized.
*/
public void addEntry(double e) {
Preconditions.checkState(state.equals(AggregationState.DEFAULT), "Entry cannot be added.");
// NaN is ignored because we cannot aggregate NaNs.
if (Double.isNaN(e)) {
return;
}
// Increment all counts on the path from the leaf node where the value is inserted up to the
// first level (root not included)
int index = getIndex(clamp(e));
while (index != ROOT_INDEX) {
long count = tree.containsKey(index) ? tree.get(index) : 0;
tree.put(index, count + 1);
index = getParent(index);
}
}
/**
* Clamps the input values and adds them to the distribution.
*
* @throws IllegalStateException if this instance of {@link BoundedQuantiles} has already been
* queried or serialized.
*/
public void addEntries(Collection<Double> e) {
e.forEach(this::addEntry);
}
private double clamp(double value) {
return max(min(value, params.upper()), params.lower());
}
/**
* Returns the index of the leaf node associated with the provided value, assuming that the leaf
* nodes partition the range between lower and upper into intervals of equal size.
*/
private int getIndex(double value) {
return leftmostLeafIndex
+ (value == params.upper()
? numberOfLeaves - 1
: (int)
Math.floor(
(value - params.lower()) / (params.upper() - params.lower()) * numberOfLeaves));
}
/**
* Returns the smallest value mapped to the subtree of the provided index, assuming that the leaf
* nodes partition the range betwen lower and upper into intervals of equal size.
*/
private double getLeftValue(int index) {
// Traverse the tree towards the leaves starting at the provided index always taking the
// leftmost branch.
while (index < leftmostLeafIndex) {
index = getLeftmostChild(index);
}
return (params.upper() - params.lower())
* ((index - leftmostLeafIndex) / (double) numberOfLeaves)
+ params.lower();
}
/**
* Returns the greatest value mapped to the subtree of the provided index, assuming that the leaf
* nodes partition the range between lower and upper into intervals of equal size.
*/
private double getRightValue(int index) {
// Traverse the tree towards the leaves starting at the provided index always taking the
// rightmost branch.
while (index < leftmostLeafIndex) {
index = getRightmostChild(index);
}
// The returned value bounds the range of values for which getIndex returns the specified index.
// This bound is not itself contained in that range, i.e., getIndex will return the next index
// when called for the bound.
return (params.upper() - params.lower())
* ((index - leftmostLeafIndex + 1) / (double) numberOfLeaves)
+ params.lower();
}
/**
* Calculates and returns a differentially private quantile of the values added via {@link
* #addEntry} and {@link #addEntries}. The specified rank must be between 0.0 and 1.0.
*
* <p>This method can be called multiple times to compute different quantiles. Privacy budget is
* paid only once, on its first invocation. Calling this method repeatedly for the same rank will
* return the same result. The results of repeated calls are guaranteed to be monotonically
* increasing in the sense that r_1 < r_2 implies that computeResult(r_1) <= computeResult(r_2).
*
* <p>Note that the returned value is not an unbiased estimate of the raw bounded quantile.
*
* @throws IllegalStateException if this instance of {@link BoundedQuantiles} has already been
* serialized.
*/
public double computeResult(double rank) {
Preconditions.checkState(
state.equals(AggregationState.DEFAULT) || state.equals(AggregationState.RESULT_RETURNED),
"DP quantile cannot be computed.");
state = AggregationState.RESULT_RETURNED;
checkArgument(
rank >= 0.0 && rank <= 1.0, "rank must be >= 0 and <= 1. Provided value: %s", rank);
rank = adjustRank(rank);
int index = ROOT_INDEX;
// Search for the index of the node containing the specified quantile, starting at the root.
while (index < leftmostLeafIndex) {
int leftmostChildIndex = getLeftmostChild(index);
int rightmostChildIndex = getRightmostChild(index);
Map<Integer, Double> noisedCounts = new HashMap<>();
for (int i = leftmostChildIndex; i <= rightmostChildIndex; i++) {
noisedCounts.put(i, getNoisedCount(i));
}
IndexAndRank nextIndexAndRank =
getNextIndexAndRank(rank, leftmostChildIndex, rightmostChildIndex, noisedCounts);
if (nextIndexAndRank == null) {
// All child nodes are considred empty. No need to proceed with the search.
break;
} else {
index = nextIndexAndRank.index();
rank = nextIndexAndRank.rank();
}
}
// Linearly interpolate between the smallest and largest value associated with the node returned
// by the search.
return (1 - rank) * getLeftValue(index) + rank * getRightValue(index);
}
/**
* Computes a confidence interval that contains the quantile of the specified rank with a
* probability greater or equal to {@code 1 - alpha}. More precisely, the confidence interval
* contains the value the mechanism returns when no noise is added with the specified probability.
* Note that this value might be different from the raw quantile as a result of bounding and
* internal processing.
*
* <p>The confidence interval is exclusively based on the noised bounded quantile returned by
* {@link #computeResult}. Thus, no privacy budget is consumed by this operation.
*
* <p>Refer to <a
* href="https://github.com/google/differential-privacy/tree/main/common_docs/confidence_intervals.md">this</a> doc for
* more information.
*
* @throws IllegalStateException if this instance of {@link BoundedQuantiles} has not been queried
* yet.
*/
public ConfidenceInterval computeConfidenceInterval(double rank, double alpha) {
Preconditions.checkState(
state.equals(AggregationState.RESULT_RETURNED), "Confidence interval cannot be computed.");
checkArgument(
rank >= 0.0 && rank <= 1.0, "rank must be >= 0 and <= 1. Provided value: %s", rank);
rank = adjustRank(rank);
return ConfidenceInterval.create(
computeConfidenceIntervalBound(rank, alpha, LOWER),
computeConfidenceIntervalBound(rank, alpha, UPPER));
}
// The following computation of a lower or upper interval bound is based on the same search
// algorithm used to compute the respective quantile. The difference is that instead of using the
// noised node counts to determine the direction of the search, the algorithm uses the confidence
// intervals bounds of the node counts.
private double computeConfidenceIntervalBound(
double rank, double alpha, ConfidenceIntervalBoundType boundType) {
// Let b be the branching factor and h the height of the tree. The search for a quantile queries
// at most b * h node counts. Assigning a confidence interval with error probability
// alpha' = 1 - (1 - alpha)^(1 / (b * h))
// to each of these counts guarantees that the true counts are contained within these
// confidence intervals with error probability
// 1 - (1 - alpha')^(b * h) = 1 - (1 - (1 - (1 - alpha)^(1 / (b * h))))^(b * h) = alpha,
// which matches the specified error probability.
double alphaPerCount =
1 - Math.pow(1 - alpha, 1.0 / (params.branchingFactor() * params.treeHeight()));
// Confidence interval of a node count of 0 with error probability alpha'. All other node count
// confidence intervals are computed by shifting this interval, which is faster than calling
// computeConfidenceInterval() for each node count individually.
ConfidenceInterval zeroConfidenceInterval =
params
.noise()
.computeConfidenceInterval(
/* noisedX= */ 0.0,
/* l0Sensitivity= */ params.treeHeight() * params.maxPartitionsContributed(),
/* lInfSensitivity= */ params.maxContributionsPerPartition(),
/* epsilon= */ params.epsilon(),
/* delta= */ params.delta(),
/* alpha= */ alphaPerCount);
// Value of the bound that is being computed. The value is set to the tightest bound possible
// and loosened successively as needed.
double bound = boundType == LOWER ? params.upper() : params.lower();
int index = ROOT_INDEX;
// Search for the index of the leaf node containing the desired bound, starting at the root.
while (index < leftmostLeafIndex) {
int leftmostChildIndex = getLeftmostChild(index);
int rightmostChildIndex = getRightmostChild(index);
// Index of the node visited next in the search. The value is set to the tightest index
// possible and loosened successively as needed.
int nextIndex = boundType == LOWER ? Integer.MAX_VALUE : Integer.MIN_VALUE;
// Rank used in the next iteration of the search. The value will be set with the first update
// of nextIndex.
double nextRank = Double.NaN;
Map<Integer, ConfidenceInterval> childConfidenceIntervals = new HashMap<>();
for (int i = leftmostChildIndex; i <= rightmostChildIndex; i++) {
childConfidenceIntervals.put(
i,
ConfidenceInterval.create(
getNoisedCount(i) + zeroConfidenceInterval.lowerBound(),
getNoisedCount(i) + zeroConfidenceInterval.upperBound()));
}
// Let [l_i, u_i] denote the confidence interval of child node i. To find a lower bound b for
// the quantiles that can be reached via a particular configuration of counts c_i such that
// l_i ≤ c_i ≤ u_i, the counts to left of b should be as large as possible while the counts to
// the right of b should be as small as possible. Thus, we set
// c_i = u_i if i <= j and c_i = l_i if i > j
// for some index j. Similarly, an upper bound can be obtained by setting
// c_i = l_i if i <= j and c_i = u_i if i > j.
//
// Because we don't know the index j in advance, we go through all possible indices j and pick
// whichever yields the smallest lower bound or largest upper bound.
for (int j = leftmostChildIndex - 1; j <= rightmostChildIndex; j++) {
Map<Integer, Double> countBounds = new HashMap<>();
for (int i = leftmostChildIndex; i <= j; i++) {
countBounds.put(
i,
boundType == LOWER
? childConfidenceIntervals.get(i).upperBound()
: childConfidenceIntervals.get(i).lowerBound());
}
for (int i = j + 1; i <= rightmostChildIndex; i++) {
countBounds.put(
i,
boundType == LOWER
? childConfidenceIntervals.get(i).lowerBound()
: childConfidenceIntervals.get(i).upperBound());
}
IndexAndRank nextIndexAndRank =
getNextIndexAndRank(rank, leftmostChildIndex, rightmostChildIndex, countBounds);
if (nextIndexAndRank == null) {
// All child nodes are considred empty. Update the bound with a linear interpolation of
// the smallest and largest value associated with the current node if the result yields
// a looser bound.
if (boundType == LOWER) {
bound = min(bound, (1 - rank) * getLeftValue(index) + rank * getRightValue(index));
} else {
bound = max(bound, (1 - rank) * getLeftValue(index) + rank * getRightValue(index));
}
} else {
// Update nextIndex and nextRank if this results in a looser bound.
if ((boundType == LOWER && nextIndexAndRank.index() <= nextIndex)
|| (boundType == UPPER && nextIndexAndRank.index() >= nextIndex)) {
if (nextIndexAndRank.index() != nextIndex) {
nextIndex = nextIndexAndRank.index();
nextRank = nextIndexAndRank.rank();
} else if ((boundType == LOWER && nextIndexAndRank.rank() < nextRank)
|| (boundType == UPPER && nextIndexAndRank.rank() > nextRank)) {
nextRank = nextIndexAndRank.rank();
}
}
}
}
// Check if the current node was considered empty for all values of j (this is the case when
// nextRank has not been set). If so, the search can be stopped and the bound returned.
// Otherwise continue the search in the next node with the respective new rank.
if (Double.isNaN(nextRank)) {
return bound;
}
index = nextIndex;
rank = nextRank;
}
// The search has reached a leaf node. In this case we either return a linear interpolation
// between the smallest and the largest value associated with the leaf node, or the bound
// computed so far should it be looser.
double linearInterpolation = (1 - rank) * getLeftValue(index) + rank * getRightValue(index);
return boundType == LOWER ? min(bound, linearInterpolation) : max(bound, linearInterpolation);
}
private int getLeftmostChild(int index) {
return index * params.branchingFactor() + 1;
}
private int getRightmostChild(int index) {
return (index + 1) * params.branchingFactor();
}
private int getParent(int index) {
return (index - 1) / params.branchingFactor();
}
/*
* Retruns a pair of the index of the child node visited next in the quantile search together with
* the rank for the next iteration of the search. If all child nodes are considered empty, null is
* returned.
*/
private static IndexAndRank getNextIndexAndRank(
double rank,
int leftmostChildIndex,
int rightmostChildIndex,
Map<Integer, Double> nodeCounts) {
double totalCount = 0.0;
for (int i = leftmostChildIndex; i <= rightmostChildIndex; i++) {
totalCount += max(0.0, nodeCounts.get(i));
}
double correctedTotalCount = 0.0;
for (int i = leftmostChildIndex; i <= rightmostChildIndex; i++) {
// Treat child nodes contributing less than a gamma fraction to the total count as empty
// subtrees.
correctedTotalCount += nodeCounts.get(i) >= totalCount * GAMMA ? nodeCounts.get(i) : 0.0;
}
if (correctedTotalCount == 0.0) {
// Either all counts are 0.0 or no child node contributes more than a gamma fraction to
// the total count (the latter can only happen when gamma > 1 / branching factor, which is
// not the case for the default branching factor). This means that all child nodes are
// considered empty.
return null;
}
// Determine the child node whose subtree contains the bound.
double partialCount = 0.0;
for (int i = leftmostChildIndex; true; i++) {
double count = nodeCounts.get(i);
// Skip child nodes contributing less than gamma to the total count.
if (count >= totalCount * GAMMA) {
partialCount += count;
// Check if the bound is in the current child's subtree.
if (partialCount / correctedTotalCount >= rank - NUMERICAL_TOLERANCE) {
double nextRank =
(rank - (partialCount - count) / correctedTotalCount) / (count / correctedTotalCount);
// Clamping rank to a value between 0.0 and 1.0. Note that rank can become greater than
// 1 because of the numerical tolerance. Values less than 0.0 should not occur. The
// respective clamping is set in place to be on the safe side.
nextRank = min(max(0.0, nextRank), 1.0);
return new AutoValue_BoundedQuantiles_IndexAndRank(i, nextRank);
}
}
}
}
/**
* Clamps the rank to a value between 0.005 and 0.995. The purpose of this adjustment is to
* mitigate the inaccuracy of the quantile tree mechanism around the min and max, i.e., the 0 and
* 1 rank.
*/
@VisibleForTesting
static double adjustRank(double rank) {
return max(min(rank, 0.995), 0.005);
}
/**
* Returns a noised version of the count associated with the respective index. If the count has
* been noised before, the same value as before is returned.
*/
private double getNoisedCount(int index) {
if (noisedTree.containsKey(index)) {
return noisedTree.get(index);
}
double rawCount = tree.containsKey(index) ? tree.get(index) : 0;
// The l_1 sensitivity of a privacy unit's contribution is
// treeHeight * maxPartitionsContributed * maxContributionsPerPartition
// while the l_2 sensitivity is
// sqrt(treeHeight * maxPartitionsContributed) * maxContributionsPerPartition
// (the latter is realized if the privacy unit increments the exact same counters for each of
// their contributions to a particular partition). Setting the l_0 and l_inf sensitivity as
// follows yields the respective l_1 and l_2 values.
double noisedCount =
params
.noise()
.addNoise(
/* x= */ rawCount,
/* l0Sensitivity= */ params.treeHeight() * params.maxPartitionsContributed(),
/* lInfSensitivity= */ params.maxContributionsPerPartition(),
/* epsilon= */ params.epsilon(),
/* delta= */ params.delta());
noisedTree.put(index, noisedCount);
return noisedCount;
}
/**
* Returns a serializable summary of the current state of this {@link BoundedQuantiles} instance
* and its parameters. The summary can be used to merge this instance with another instance of
* {@link BoundedQuantiles}.
*
* <p>This method cannot be invoked if a quantile has already been queried, i.e., {@link
* computeResult(double)} has been called. Moreover, after this instance of {@link
* BoundedQuantiles} has been serialized once, further modification and queries are not possible
* anymore.
*
* @throws IllegalStateException if this instance of {@link BoundedQuantiles} has already been
* queried.
*/
public byte[] getSerializableSummary() {
Preconditions.checkState(
state != AggregationState.RESULT_RETURNED, "Distribution cannot be serialized.");
state = AggregationState.SERIALIZED;
BoundedQuantilesSummary.Builder builder =
BoundedQuantilesSummary.newBuilder()
.putAllQuantileTree(tree)
.setEpsilon(params.epsilon())
.setLower(params.lower())
.setUpper(params.upper())
.setMaxPartitionsContributed(params.maxPartitionsContributed())
.setMaxContributionsPerPartition(params.maxContributionsPerPartition())
.setMechanismType(params.noise().getMechanismType())
.setTreeHeight(params.treeHeight())
.setBranchingFactor(params.branchingFactor());
if (params.delta() != null) {
builder.setDelta(params.delta());
}
return builder.build().toByteArray();
}
/**
* Merges the output of {@link #getSerializableSummary()} from a different instance of {@link
* BoundedQuantiles} with this instance. Intended to be used in the context of distributed
* computation.
*
* @throws IllegalArgumentException if the parameters of the two instances (epsilon, delta,
* contribution bounds, etc.) do not match or if the passed serialized summary is invalid.
* @throws IllegalStateException if this instance of {@link BoundedQuantiles} has already been
* queried or serialized.
*/
public void mergeWith(byte[] otherBoundedQuantilesSummary) {
Preconditions.checkState(
state.equals(AggregationState.DEFAULT), "Distributions cannot be merged.");
BoundedQuantilesSummary otherSummaryParsed;
try {
otherSummaryParsed = BoundedQuantilesSummary.parseFrom(otherBoundedQuantilesSummary);
} catch (InvalidProtocolBufferException pbe) {
throw new IllegalArgumentException(pbe);
}
checkMergeParametersAreEqual(otherSummaryParsed);
for (int index : otherSummaryParsed.getQuantileTreeMap().keySet()) {
long oldCount = tree.containsKey(index) ? tree.get(index) : 0;
tree.put(index, oldCount + otherSummaryParsed.getQuantileTreeOrDefault(index, 0));
}
}
private void checkMergeParametersAreEqual(BoundedQuantilesSummary summary) {
DpPreconditions.checkMergeMechanismTypesAreEqual(
params.noise().getMechanismType(), summary.getMechanismType());
DpPreconditions.checkMergeEpsilonAreEqual(params.epsilon(), summary.getEpsilon());
DpPreconditions.checkMergeDeltaAreEqual(params.delta(), summary.getDelta());
DpPreconditions.checkMergeMaxPartitionsContributedAreEqual(
params.maxPartitionsContributed(), summary.getMaxPartitionsContributed());
DpPreconditions.checkMergeMaxContributionsPerPartitionAreEqual(
params.maxContributionsPerPartition(), summary.getMaxContributionsPerPartition());
DpPreconditions.checkMergeBoundsAreEqual(
params.lower(), summary.getLower(), params.upper(), summary.getUpper());
checkArgument(
params.treeHeight() == summary.getTreeHeight(),
"Failed to merge: unequal values of tree height. " + "treeHeight1 = %s, treeHeight2 = %s",
params.treeHeight(),
summary.getTreeHeight());
checkArgument(
params.branchingFactor() == summary.getBranchingFactor(),
"Failed to merge: unequal values of branchingFactor. "
+ "branchingFactor1 = %s, branchingFactor2 = %s",
params.branchingFactor(),
summary.getBranchingFactor());
}
@AutoValue
abstract static class IndexAndRank {
abstract int index();
abstract double rank();
}
@AutoValue
public abstract static class Params {
abstract Noise noise();
abstract double epsilon();
@Nullable
abstract Double delta();
abstract int maxPartitionsContributed();
abstract int maxContributionsPerPartition();
abstract double lower();
abstract double upper();
abstract int treeHeight();
abstract int branchingFactor();
@AutoValue.Builder
public abstract static class Builder {
private static BoundedQuantiles.Params.Builder newBuilder() {
BoundedQuantiles.Params.Builder builder = new AutoValue_BoundedQuantiles_Params.Builder();
// Provides LaplaceNoise as a default noise generator.
builder.noise(new LaplaceNoise());
// A default tree height of 4 and branching factor of 16 divides the domain of possible
// values into 65536 partitions.
builder.treeHeight(DEFAULT_TREE_HEIGHT);
builder.branchingFactor(DEFAULT_BRANCHING_FACTOR);
return builder;
}
/** Epsilon DP parameter. */
public abstract BoundedQuantiles.Params.Builder epsilon(double value);
/**
* Delta DP parameter.
*
* <p>Note that Laplace noise does not use delta. Hence, delta should not be set when Laplace
* noise is used.
*/
public abstract BoundedQuantiles.Params.Builder delta(@Nullable Double value);
/**
* Maximum number of partitions that a single privacy unit (e.g., an individual) is allowed to
* contribute to.
*/
public abstract BoundedQuantiles.Params.Builder maxPartitionsContributed(int value);
/** Max contributions per partition from a single privacy unit (e.g., an individual). */
public abstract BoundedQuantiles.Params.Builder maxContributionsPerPartition(int value);
/** Noise that will be used to make the quantiles differentially private. */
public abstract BoundedQuantiles.Params.Builder noise(Noise value);
/**
* Lower bound for the entries added to the distribution. Any entries smaller than this value
* will be set to this value.
*/
public abstract BoundedQuantiles.Params.Builder lower(double value);
/**
* Upper bound for the entries added to the distribution. Any entries greater than this value
* will be set to this value.
*/
public abstract BoundedQuantiles.Params.Builder upper(double value);
/**
* The height of the quantile tree used to store the entries. Should be at least 1. This
* parameter is not public, it should be used only by other aggregation functions inside the
* library.
*/
@VisibleForTesting
public abstract BoundedQuantiles.Params.Builder treeHeight(int value);
/**
* The number of children for every non-leaf node of the quantile tree used to store the
* entries. Should be at least 2. This parameters is not public, it should be used only by
* other aggregation functions inside the library.
*/
@VisibleForTesting
public abstract BoundedQuantiles.Params.Builder branchingFactor(int value);
private static void checkTreeHeight(int treeHeight) {
checkArgument(
treeHeight >= 1, "Tree height must be at least 1. Provided value: %s", treeHeight);
}
private static void checkBranchingFactor(int branchingFactor) {
checkArgument(
branchingFactor >= 2,
"Branching factor must be at least 2. Provided value: %s",
branchingFactor);
}
abstract BoundedQuantiles.Params autoBuild();
public BoundedQuantiles build() {
BoundedQuantiles.Params params = autoBuild();
// No need to check noise nullability: the noise is defaulted to Laplace noise.
DpPreconditions.checkEpsilon(params.epsilon());
DpPreconditions.checkNoiseDelta(params.delta(), params.noise());
DpPreconditions.checkMaxPartitionsContributed(params.maxPartitionsContributed());
DpPreconditions.checkMaxContributionsPerPartition(params.maxContributionsPerPartition());
DpPreconditions.checkBounds(params.lower(), params.upper());
DpPreconditions.checkBoundsNotEqual(params.lower(), params.upper());
checkTreeHeight(params.treeHeight());
checkBranchingFactor(params.branchingFactor());
return new BoundedQuantiles(params);
}
}
}
}