| // 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) |
| } |