Fix FleetwideMeans minDenominatorEstimate usage.

FleetwideMeans now returns the maximum for all denominator values >
minDenominatorEstimate. For speed, this value is estimated by doing a
linear search for the highest error through a geometric series of
denominators using r = 1.5.

Unit tests have beeen updated.

BEFORE: https://screenshot.googleplex.com/8nQFhgWtpHawjEd
AFTER: https://screenshot.googleplex.com/CnA2puxWo2nks7y

Change-Id: Iaace7ca4fb6f048716375b8ada056a07ace444cc
Reviewed-on: https://fuchsia-review.googlesource.com/c/cobalt/+/511286
Reviewed-by: Pasin Manurangsi <pasin@google.com>
Commit-Queue: Jared Weinstein <jaredweinstein@google.com>
diff --git a/src/bin/config_parser/src/privacy/error_calculator.go b/src/bin/config_parser/src/privacy/error_calculator.go
index 5d66ffe..31113d8 100644
--- a/src/bin/config_parser/src/privacy/error_calculator.go
+++ b/src/bin/config_parser/src/privacy/error_calculator.go
@@ -68,14 +68,14 @@
 	case config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS:
 		contributionRange := uint64(report.MaxValue - report.MinValue)
 		if contributionRange <= 0 {
-			return -1, fmt.Errorf("contribution range must be positive to estimate error.")
+			return -1, fmt.Errorf("contribution range must be positive to estimate error")
 		}
 		population *= 24 // Hourly contribution interval: 24*population pseudo-users.
 		errorEstimate = multipleContributionRapporRMSE(population, probBitFlip, contributionRange, discretization)
 	case config.ReportDefinition_FLEETWIDE_HISTOGRAMS:
 		contributionRange := report.MaxCount
 		if contributionRange <= 0 {
-			return -1, fmt.Errorf("contribution range must be posititve to estimate error.")
+			return -1, fmt.Errorf("contribution range must be positive to estimate error")
 		}
 		population *= 24 // Hourly contribution interval: 24*population pseudo-users.
 		errorEstimate = multipleContributionRapporRMSE(population, probBitFlip, contributionRange, discretization)
@@ -95,7 +95,10 @@
 			return -1, err
 		}
 		population *= 24 // Hourly contribution interval: 24*population pseudo-users.
-		errorEstimate = fleetwideMeansRapporRMSE(population, probBitFlip, minDenominatorEstimate, discretization, report)
+		errorEstimate, err = fleetwideMeansRapporRMSE(population, probBitFlip, minDenominatorEstimate, discretization, report)
+		if err != nil {
+			return -1, err
+		}
 	default:
 		reportType := config.ReportDefinition_ReportType_name[int32(report.GetReportType())]
 		return -1, fmt.Errorf("error estimation is not supported for reports of type %s", reportType)
@@ -147,7 +150,7 @@
 }
 
 // Compute 2D-RAPPOR RMSE for the FleetwideMeans report.
-func fleetwideMeansRapporRMSE(population uint64, probBitFlip float64, minDenominatorEstimate uint64, discretization uint64, report *config.ReportDefinition) float64 {
+func fleetwideMeansRapporRMSE(population uint64, probBitFlip float64, minDenominatorEstimate uint64, discretization uint64, report *config.ReportDefinition) (float64, error) {
 	t := report.MaxCount
 	a := probBitFlip * float64(t) / (1 - 2*probBitFlip)
 	T := float64(math.Max(float64(-report.MinValue), float64(report.MaxValue)))
@@ -160,7 +163,22 @@
 	// Denominator RMSE aggregated like FleetwideHistograms.
 	sigma := multipleContributionRapporRMSE(population, probBitFlip, t, discretization)
 
-	return meanRapporRMSE(mseNumerator, sigma, a, T, float64(minDenominatorEstimate))
+	// Find the max over a geometric sequence of minDenominator > minDenominatorEstimate.
+	ratio := 1.5
+	maxSequence := 20
+	previousError := 0.0
+	for n := 0; n < maxSequence; n++ {
+		minDenominator := float64(minDenominatorEstimate) * math.Pow(ratio, float64(n))
+		currentError := meanRapporRMSE(mseNumerator, sigma, a, T, minDenominator)
+		// Assume meanRapporRMSE is concave. This is not proven but is observed for
+		// significant values of minDenominatorEstimate (roughly estimate > 500)
+		if currentError <= previousError {
+			return previousError, nil
+		}
+		previousError = currentError
+	}
+	// No maximum found
+	return -1, fmt.Errorf("maximum not found after %v iterations", maxSequence)
 }
 
 func meanRapporRMSE(mseNum float64, sigma float64, a float64, T float64, B float64) float64 {
diff --git a/src/bin/config_parser/src/privacy/error_calculator_test.go b/src/bin/config_parser/src/privacy/error_calculator_test.go
index 49174a9..3de38cb 100644
--- a/src/bin/config_parser/src/privacy/error_calculator_test.go
+++ b/src/bin/config_parser/src/privacy/error_calculator_test.go
@@ -166,9 +166,9 @@
 		{args{&testMetric, &fleetwideMeans, 10, 10000, 500}, true, 28.078743913512465},
 		{args{&testMetric, &fleetwideMeans, 1, 20000, 500}, true, 551.1417346736127},
 		{args{&testMetric, &fleetwideMeans, 1, 10000, 2000}, true, 0.9722845785198999},
-		{args{&testMetric, &fleetwideMeansHighMaxCount, 1, 10000, 500}, true, 2370.2707572142904},
-		{args{&testMetric, &fleetwideMeansHighMaxCount, 10, 10000, 500}, true, 2238.0931858912145},
-		{args{&testMetric, &fleetwideMeansHighMaxCount, 1, 20000, 500}, true, 2404.5027616065695},
+		{args{&testMetric, &fleetwideMeansHighMaxCount, 1, 10000, 500}, true, 4471.951966988096},
+		{args{&testMetric, &fleetwideMeansHighMaxCount, 10, 10000, 500}, true, 3203.1776450207326},
+		{args{&testMetric, &fleetwideMeansHighMaxCount, 1, 20000, 500}, true, 5217.923499327469},
 		{args{&testMetric, &fleetwideMeansHighMaxCount, 1, 10000, 2000}, true, 4098.32133059633},
 
 		{args{&testMetric, &uniqueDeviceCount, 0, 100000, 0}, true, 0},
@@ -177,7 +177,7 @@
 		{args{&testMetricWithBufferMax, &uniqueDeviceCount, 1, 10000, 0}, true, 4.935511431266572},
 		{args{&testMetricWithBufferMax, &fleetwideOccurrenceCount, 1, 10000, 0}, true, 386.4795472694349},
 		{args{&testMetricWithBufferMax, &uniqueDeviceNumericStats, 1, 10000, 500}, true, 0.03372677729859206},
-		{args{&testMetricWithBufferMax, &fleetwideMeans, 1, 10000, 500}, true, 2117.7764297912554},
+		{args{&testMetricWithBufferMax, &fleetwideMeans, 1, 10000, 500}, true, 2470.4301734178152},
 
 		// Invalid input
 		{args{&testMetric, &uniqueDeviceNumericStats, 1, 10000, 0}, false, 0},