Error Calculator supports NumericStats report types

Added back support for UniqueDeviceNumericStats and
HourlyValueNumericStats. All Cobalt 1.1 report types are now supported.

Change-Id: I2acecff91ec5c1a5cdb61fc09439d1ad84d5489a
Reviewed-on: https://fuchsia-review.googlesource.com/c/cobalt/+/487278
Commit-Queue: Jared Weinstein <jaredweinstein@google.com>
Reviewed-by: Pasin Manurangsi <pasin@google.com>
Reviewed-by: Laura Peskin <pesk@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 4ca2458..acae241 100644
--- a/src/bin/config_parser/src/privacy/error_calculator.go
+++ b/src/bin/config_parser/src/privacy/error_calculator.go
@@ -74,18 +74,17 @@
 		contributionRange := report.MaxCount
 		population *= 24 // Hourly contribution interval: 24*population pseudo-users.
 		errorEstimate = multipleContributionRapporRMSE(population, probBitFlip, contributionRange, discretization)
-	// TODO(jaredweinstein): Enable NumericStats once the formula is resolved.
-	// case config.ReportDefinition_UNIQUE_DEVICE_NUMERIC_STATS:
-	//         if err := meanReportConfigurationError(report, minDenominatorEstimate); err != nil {
-	//                 return -1, err
-	//         }
-	//         errorEstimate = NumericStatsRapporRMSE(population, privacyEncodingParams, minDenominatorEstimate, report)
-	// case config.ReportDefinition_HOURLY_VALUE_NUMERIC_STATS:
-	//         if err := meanReportConfigurationError(report, minDenominatorEstimate); err != nil {
-	//                 return -1, err
-	//         }
-	//         population *= 24 // Hourly contribution interval: 24*population pseudo-users.
-	//         errorEstimate = NumericStatsRapporRMSE(population, privacyEncodingParams, minDenominatorEstimate, report)
+	case config.ReportDefinition_UNIQUE_DEVICE_NUMERIC_STATS:
+		if err := meanReportConfigurationError(report, minDenominatorEstimate); err != nil {
+			return -1, err
+		}
+		errorEstimate = numericStatsRapporRMSE(population, probBitFlip, minDenominatorEstimate, discretization, report)
+	case config.ReportDefinition_HOURLY_VALUE_NUMERIC_STATS:
+		if err := meanReportConfigurationError(report, minDenominatorEstimate); err != nil {
+			return -1, err
+		}
+		population *= 24 // Hourly contribution interval: 24*population pseudo-users.
+		errorEstimate = numericStatsRapporRMSE(population, probBitFlip, minDenominatorEstimate, discretization, report)
 	case config.ReportDefinition_FLEETWIDE_MEANS:
 		if err := meanReportConfigurationError(report, minDenominatorEstimate); err != nil {
 			return -1, err
@@ -126,23 +125,18 @@
 	return estimate
 }
 
-// TODO(jaredweinstein): Add back NumericStats once the formula is resolved.
 // Compute 2D-RAPPOR RMSE for NumericStats reports.
-// func NumericStatsRapporRMSE(population uint64, params PrivacyEncodingParams, minDenominatorEstimate uint64, report *config.ReportDefinition) float64 {
-//         sigma := SingleContributionRapporRMSE(population, params)
-//         a := params.ProbBitFlip / (1 - 2*params.ProbBitFlip)
-//         M := uint64(math.Max(math.Abs(float64(report.MinValue)), math.Abs(float64(report.MaxValue))))
-//
-//         // Numerator is aggregated like FleetwideOccurrenceCounts.
-//         rmseNumerator := MultipleContributionRapporRMSE(population, params, M)
-//         mseNumerator := math.Pow(rmseNumerator, 2)
-//
-//         // Denominator is aggregated like UniqueDeviceCounts.
-//         rmseDenominator := SingleContributionRapporRMSE(population, params)
-//         mseDenominator := math.Pow(rmseDenominator, 2)
-//
-//         return meanRapporRMSE(mseNumerator, mseDenominator, sigma, a, float64(M), float64(minDenominatorEstimate))
-// }
+func numericStatsRapporRMSE(population uint64, probBitFlip float64, minDenominatorEstimate uint64, discretization uint64, report *config.ReportDefinition) float64 {
+	T := float64(math.Max(float64(-report.MinValue), float64(report.MaxValue)))
+	M := uint64(report.MaxValue - report.MinValue)
+
+	sigmaNum := multipleContributionRapporRMSE(population, probBitFlip, M, discretization)
+	alphaNum := probBitFlip * float64(M) / (1 - 2*probBitFlip)
+	sigmaDenom := singleContributionRapporRMSE(population, probBitFlip)
+	alphaDenom := probBitFlip / (1 - 2*probBitFlip)
+
+	return eThree(T, float64(minDenominatorEstimate), sigmaNum, alphaNum, sigmaDenom, alphaDenom)
+}
 
 // Compute 2D-RAPPOR RMSE for the FleetwideMeans report.
 func fleetwideMeansRapporRMSE(population uint64, probBitFlip float64, minDenominatorEstimate uint64, discretization uint64, report *config.ReportDefinition) float64 {
@@ -190,6 +184,21 @@
 	return -quad.Fixed(f, l-B, 0, 10000, nil, 0)
 }
 
+func eThree(T float64, B float64, sigmaNum float64, alphaNum float64, sigmaDenom float64, alphaDenom float64) float64 {
+	f := func(y float64) float64 {
+		tOne := math.Sqrt(y) / (T + 1)
+		tTwo := math.Sqrt(y) * B / (math.Sqrt(y) + T + 1)
+		t := math.Max(tOne, tTwo)
+		return 2*math.Exp(-math.Pow(sigmaNum, 2)/math.Pow(alphaNum, 2)*
+			h(alphaNum*t/math.Pow(sigmaNum, 2))) +
+			2*math.Exp(-math.Pow(sigmaDenom, 2)/math.Pow(alphaDenom, 2)*
+				h(alphaDenom*t/math.Pow(sigmaDenom, 2)))
+
+	}
+	return quad.Fixed(f, 0, math.Inf(1), 10000, nil, 0)
+
+}
+
 func h(u float64) float64 {
 	return (1+u)*math.Log(1+u) - u
 }
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 b2a85f8..a73c4da 100644
--- a/src/bin/config_parser/src/privacy/error_calculator_test.go
+++ b/src/bin/config_parser/src/privacy/error_calculator_test.go
@@ -77,20 +77,22 @@
 		LocalAggregationProcedure: config.ReportDefinition_SELECT_FIRST,
 		MaxCount:                  20,
 	}
-	// uniqueDeviceNumericStats := config.ReportDefinition{
-	//         ReportName: "UniqueDeviceNumericStats",
-	//         ReportType: config.ReportDefinition_UNIQUE_DEVICE_NUMERIC_STATS,
-	//         MaxValue:   1,
-	// }
-	// hourlyValueNumericStats := config.ReportDefinition{
-	//         ReportName: "HourlyDeviceNumericStats",
-	//         ReportType: config.ReportDefinition_HOURLY_VALUE_NUMERIC_STATS,
-	//         MaxValue:   1,
-	// }
-	// uniqueDeviceNumericStatsMissingMaxValue := config.ReportDefinition{
-	//         ReportName: "UniqueDeviceNumericStatsMissingMaxValue",
-	//         ReportType: config.ReportDefinition_UNIQUE_DEVICE_NUMERIC_STATS,
-	// }
+	uniqueDeviceNumericStats := config.ReportDefinition{
+		ReportName: "UniqueDeviceNumericStats",
+		ReportType: config.ReportDefinition_UNIQUE_DEVICE_NUMERIC_STATS,
+		MinValue:   0,
+		MaxValue:   1,
+	}
+	hourlyValueNumericStats := config.ReportDefinition{
+		ReportName: "HourlyValueNumericStats",
+		ReportType: config.ReportDefinition_HOURLY_VALUE_NUMERIC_STATS,
+		MinValue:   0,
+		MaxValue:   1,
+	}
+	uniqueDeviceNumericStatsMissingMaxValue := config.ReportDefinition{
+		ReportName: "UniqueDeviceNumericStatsMissingMaxValue",
+		ReportType: config.ReportDefinition_UNIQUE_DEVICE_NUMERIC_STATS,
+	}
 	fleetwideMeans := config.ReportDefinition{
 		ReportName: "FleetwideMeans",
 		ReportType: config.ReportDefinition_FLEETWIDE_MEANS,
@@ -148,14 +150,14 @@
 		{args{&testMetric, &fleetwideHistogramHighMax, 1, 10000, 0}, true, 1057.87447719305},
 
 		// Mean reports
-		// {args{&testMetric, &uniqueDeviceNumericStats, 1, 10000, 500}, true, 0.027891803930152177},
-		// {args{&testMetric, &uniqueDeviceNumericStats, 10, 10000, 500}, true, 0.018650077285590205},
-		// {args{&testMetric, &uniqueDeviceNumericStats, 1, 20000, 500}, true, 0.039654432133364455},
-		// {args{&testMetric, &uniqueDeviceNumericStats, 1, 10000, 2000}, true, 0.006907368191507632},
-		// {args{&testMetric, &hourlyValueNumericStats, 1, 10000, 500}, true, 0.14403990551821316},
-		// {args{&testMetric, &hourlyValueNumericStats, 10, 10000, 500}, true, 0.09424308127635954},
-		// {args{&testMetric, &hourlyValueNumericStats, 1, 20000, 500}, true, 0.20996453488325503},
-		// {args{&testMetric, &hourlyValueNumericStats, 1, 10000, 2000}, true, 0.03425744055252169},
+		{args{&testMetric, &uniqueDeviceNumericStats, 1, 10000, 500}, true, 0.05322079602809264},
+		{args{&testMetric, &uniqueDeviceNumericStats, 10, 10000, 500}, true, 0.05052783827668571},
+		{args{&testMetric, &uniqueDeviceNumericStats, 1, 20000, 500}, true, 0.11687754910096394},
+		{args{&testMetric, &uniqueDeviceNumericStats, 1, 10000, 2000}, true, 0.0028519041732824315},
+		{args{&testMetric, &hourlyValueNumericStats, 1, 10000, 2000}, true, 0.08391065449924393},
+		{args{&testMetric, &hourlyValueNumericStats, 10, 10000, 2000}, true, 0.07965661465979317},
+		{args{&testMetric, &hourlyValueNumericStats, 1, 20000, 2000}, true, 0.18931407076500353},
+		{args{&testMetric, &hourlyValueNumericStats, 1, 10000, 5000}, true, 0.011480997122055539},
 
 		{args{&testMetric, &fleetwideMeans, 1, 10000, 500}, true, 1140.9529271415656},
 		{args{&testMetric, &fleetwideMeans, 10, 10000, 500}, true, 819.2029589502301},
@@ -169,9 +171,9 @@
 		{args{&testMetric, &uniqueDeviceCount, 0, 100000, 0}, true, 0},
 
 		// Invalid input
-		// {args{&testMetric, &uniqueDeviceNumericStats, 1, 10000, 0}, false, 0},
-		// {args{&testMetric, &hourlyValueNumericStats, 1, 10000, 0}, false, 0},
-		// {args{&testMetric, &uniqueDeviceNumericStatsMissingMaxValue, 1, 10000, 500}, false, 0},
+		{args{&testMetric, &uniqueDeviceNumericStats, 1, 10000, 0}, false, 0},
+		{args{&testMetric, &hourlyValueNumericStats, 1, 10000, 0}, false, 0},
+		{args{&testMetric, &uniqueDeviceNumericStatsMissingMaxValue, 1, 10000, 500}, false, 0},
 		{args{&testMetric, &fleetwideMeansMissingMaxValue, 1, 10000, 500}, false, 0},
 
 		// This report type is not currently supported.