blob: 31908da58f0f6402074debfceddcd165ba489f9b [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file reads a Cobalt Configuration in YAML format and uses the
// error_estimation package to output an error estimate for each relevant
// privacy level and corresponding epsilon value.
package main
import (
"config"
"flag"
"fmt"
"os"
"privacy"
"registry_util"
"strconv"
"text/tabwriter"
"github.com/golang/glog"
)
var (
registryProto = flag.String("registry_proto", "", "File path of the serialized Cobalt registry.")
privacyParams = flag.String("privacy_params", "", "File containing privacy param records.")
epsilonFlag = flag.Float64("epsilon", 0, "If set, estimates the error for the specified epsilon value.")
populationFlag = flag.Int("population", 0, "If set, estimates the error given the specified population estimate.")
minDenominatorEstimateFlag = flag.Int("min_denominator", 0, "Estimated minimum number of unique contributing devices per day.")
minValueFlag = flag.Int("min_value", 0, "Optionally overrides the report's minValue field.")
maxValueFlag = flag.Int("max_value", 0, "Optionally overrides the report's maxValue field.")
maxCountFlag = flag.Int("max_count", 0, "Optionally overrides the report's maxCount field.")
simpleFlag = flag.Bool("simple", false, "Output a single error estimate.")
)
var (
privacyLevels = []config.ReportDefinition_PrivacyLevel{
config.ReportDefinition_LOW_PRIVACY,
config.ReportDefinition_MEDIUM_PRIVACY,
config.ReportDefinition_HIGH_PRIVACY,
}
)
// Input for each error estimation row.
type errorEstimationConfig struct {
label string
epsilon float64
population uint64
minDemonimator uint64
}
// Data for a single error estimate row.
type errorEstimate struct {
config errorEstimationConfig
absoluteError float64
relativeError float64
}
// Prints a list of estimates to the console structured as a table.
func prettyPrint(estimates []errorEstimate, report *config.ReportDefinition) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent)
fmt.Printf("Report: %v\n", report.ReportName)
fmt.Printf("Population Estimate: %v\n", estimates[0].config.population)
fmt.Printf("Current Privacy Level: %v\n", report.PrivacyLevel)
if *minDenominatorEstimateFlag != 0 {
fmt.Printf("Estimated Denominator Lower Bound: %v\n", *minDenominatorEstimateFlag)
}
fmt.Println("")
fmt.Fprintln(w, "Privacy Level\t| Epsilon\t| Absolute Error*\t| Relative Error*\t|")
fmt.Fprintln(w, "-------------\t+ -------\t+ ---------------\t+ ---------------\t+")
for _, e := range estimates {
fmt.Fprintf(w, "%v\t| %v\t| ±%.5v\t| ±%.5v\t|\n", e.config.label, e.config.epsilon, e.absoluteError, e.relativeError)
}
fmt.Fprintln(w, "\n* Estimated root mean square error for the 'active_count' field for each report row.")
w.Flush()
}
// Confirms that required flags are provided and all flag values are valid.
func validateFlags() (err error) {
flag.Parse()
if *registryProto == "" {
return fmt.Errorf("--registry_proto flag is required.")
}
if *privacyParams == "" {
return fmt.Errorf("--privacy_params flag is required.")
}
if *populationFlag == 0 {
return fmt.Errorf("--population flag is required.")
}
if flag.NArg() != 4 {
return fmt.Errorf("You must specify customer, project, metric, and report id.")
}
if *epsilonFlag < 0 {
return fmt.Errorf("--epsilon must be positive.")
}
if *populationFlag < 0 {
return fmt.Errorf("--population must be positive.")
}
return nil
}
func overrideReport(report *config.ReportDefinition) *config.ReportDefinition {
if *minValueFlag != 0 {
report.MinValue = int64(*minValueFlag)
}
if *maxValueFlag != 0 {
report.MaxValue = int64(*maxValueFlag)
}
if *maxCountFlag != 0 {
report.MaxCount = uint64(*maxCountFlag)
}
return report
}
func generateSingleEstimate(report *config.ReportDefinition, metric *config.MetricDefinition, paramsCalc *privacy.PrivacyEncodingParamsCalculator, errorCalc *privacy.ErrorCalculator) (float64, error) {
epsilon := paramsCalc.Constants.EpsilonForPrivacyLevel[report.PrivacyLevel]
if *epsilonFlag != 0 {
epsilon = *epsilonFlag
}
value, err := errorCalc.Estimate(metric, report, epsilon, uint64(*populationFlag), uint64(*minDenominatorEstimateFlag))
if err != nil {
return 0, err
}
return value, nil
}
func generateAllEstimates(report *config.ReportDefinition, metric *config.MetricDefinition, paramsCalc *privacy.PrivacyEncodingParamsCalculator, errorCalc *privacy.ErrorCalculator) ([]errorEstimate, error) {
configs := []errorEstimationConfig{}
for _, l := range privacyLevels {
configs = append(configs, errorEstimationConfig{
label: l.String(),
epsilon: paramsCalc.Constants.EpsilonForPrivacyLevel[l],
population: uint64(*populationFlag),
minDemonimator: uint64(*minDenominatorEstimateFlag),
})
}
if *epsilonFlag != 0 {
configs = append(configs, errorEstimationConfig{
label: "CUSTOM",
epsilon: *epsilonFlag,
population: uint64(*populationFlag),
minDemonimator: uint64(*minDenominatorEstimateFlag),
})
}
values := []errorEstimate{}
for _, c := range configs {
absoluteError, err := errorCalc.Estimate(metric, report, c.epsilon, c.population, c.minDemonimator)
relativeError := absoluteError / float64(c.population)
if err != nil {
return nil, err
}
values = append(values, errorEstimate{config: c, absoluteError: absoluteError, relativeError: relativeError})
}
return values, nil
}
func main() {
err := validateFlags()
if err != nil {
glog.Exit(err)
}
ids := []uint64{}
for i := 0; i < 4; i++ {
id, err := strconv.ParseUint(flag.Arg(i), 10, 64)
if err != nil {
glog.Exitf("Failure converting id '%v' to integer.", flag.Arg(i))
}
ids = append(ids, id)
}
customerId := uint32(ids[0])
projectId := uint32(ids[1])
metricId := uint32(ids[2])
reportId := uint32(ids[3])
registryUtil, err := registry_util.NewRegistryUtil(*registryProto)
if err != nil {
glog.Exit(err)
}
metric, err := registryUtil.FindMetric(customerId, projectId, metricId)
if err != nil {
glog.Exit(err)
}
report, err := registryUtil.FindReport(customerId, projectId, metricId, reportId)
if err != nil {
glog.Exit(err)
}
report = overrideReport(report)
paramsCalc, err := privacy.NewPrivacyEncodingParamsCalculator(*privacyParams)
errorCalc := privacy.NewErrorCalculator(*paramsCalc)
if err != nil {
glog.Exit(err)
}
if *simpleFlag {
estimate, err := generateSingleEstimate(report, metric, paramsCalc, errorCalc)
if err != nil {
glog.Exit(err)
}
fmt.Println(estimate)
} else {
estimates, err := generateAllEstimates(report, metric, paramsCalc, errorCalc)
if err != nil {
glog.Exit(err)
}
prettyPrint(estimates, report)
}
os.Exit(0)
}