Skip to content

Commit b592fb1

Browse files
authored
Wrapper for numerical mechanism class [Continuation from OpenMined#372] (OpenMined#380)
* initial addition of partition selection * corrected instantiation to 'builder.Build' instead * build works, TODO: deal with passing Laplace/Gaussian mechanims builders * post-review changes, moved partition selection python related code to own submodule * fixed some formatting * Added documentation * post formatting * added tests for partition selection * moved imports for patition_selection * clarified TODO dependency * attempt to resolve linting issues * * Added exports in algorithms.partition_selection * Replaced the Create*PartitionStrategy functions with a template function and instantiantions * revert a to latest stable commit * restore the correct commit for google-dp submodule * added python bindings for numerical mechanisms * added some python files * fixed prereqs_linux.sh script * saving changes * build + tests work * added docs for numerical mechanisms
1 parent 13bf881 commit b592fb1

File tree

8 files changed

+311
-5
lines changed

8 files changed

+311
-5
lines changed

docs/pydp.rst

+12-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ Algorithms
2626
:inherited-members:
2727

2828

29+
Numerical Mechanisms
30+
####################
31+
.. currentmodule:: pydp.algorithms.numerical_mechanisms
32+
.. autoclass:: NumericalMechanism
33+
:members:
34+
.. autoclass:: LaplaceMechanism
35+
:members:
36+
:show-inheritance:
37+
.. autoclass:: GaussianMechanism
38+
:members:
39+
:show-inheritance:
2940

3041
Distributions
3142
#############
@@ -52,4 +63,4 @@ Partition Selection
5263
.. currentmodule:: pydp.algorithms.partition_selection
5364
.. autoclass:: PartitionSelectionStrategy
5465
:members:
55-
.. autofunction:: create_partition_strategy
66+
.. autofunction:: create_partition_strategy

prereqs_linux.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ git submodule update --init --recursive
6060

6161

6262
# checkout out to particular commit
63-
cd third_party/differential-privacy && \
63+
cd third_party/differential-privacy && git checkout 78d3fb8f63ea904ea6449a8276b9070254c650ec
6464
cd -
6565
# renaming workspace.bazel to workspace
6666
mv third_party/differential-privacy/cc/WORKSPACE.bazel third_party/differential-privacy/cc/WORKSPACE

src/bindings/BUILD

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ pybind_extension(
77
"PyDP/base/*.cpp",
88
"PyDP/algorithms/*.cpp",
99
"PyDP/pydp_lib/*.hpp",
10-
"PyDP/proto/*.cpp"
10+
"PyDP/proto/*.cpp",
11+
"PyDP/mechanisms/*.cpp"
1112
]),
1213

1314
visibility = ["//src/python:__pkg__"],
@@ -23,6 +24,7 @@ pybind_extension(
2324
"@google_dp//algorithms:bounded-sum",
2425
"@google_dp//algorithms:bounded-standard-deviation",
2526
"@google_dp//algorithms:partition-selection",
27+
"@google_dp//algorithms:numerical-mechanisms",
2628
"@google_dp//algorithms:count",
2729
"@google_dp//algorithms:order-statistics",
2830
"@google_dp//proto:util-lib"

src/bindings/PyDP/bindings.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ void init_algorithms_rand(py::module &);
2929
// proto
3030
void init_proto(py::module &);
3131

32+
// numerical mechanisms
33+
void init_mechanisms_mechanism(py::module &);
34+
3235
PYBIND11_MODULE(_pydp, m) {
3336
m.doc() = "Google Differential Privacy python extension";
3437

@@ -52,6 +55,9 @@ PYBIND11_MODULE(_pydp, m) {
5255
init_algorithms_rand(mutil);
5356
init_algorithms_util(mutil);
5457

58+
auto mnumericalmechanisms = m.def_submodule("_mechanisms", "Numerical Mechanisms.");
59+
init_mechanisms_mechanism(mnumericalmechanisms);
60+
5561
// Proto
5662
// TODO: Delete if it is not necessary (we no longer return StatusOr to the user)
5763
init_proto(m);
+184-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,184 @@
1-
#include "mechanism.h"
1+
#include <pybind11/pybind11.h>
2+
#include "pybind11/complex.h"
3+
#include "pybind11/functional.h"
4+
#include "pybind11/stl.h"
5+
6+
#include "algorithms/distributions.h"
7+
#include "algorithms/numerical-mechanisms.h"
8+
9+
#include "../pydp_lib/algorithm_builder.hpp"
10+
#include "../pydp_lib/casting.hpp"
11+
12+
using namespace std;
13+
14+
namespace py = pybind11;
15+
namespace dp = differential_privacy;
16+
17+
class ConfidenceIntervalBinder {
18+
public:
19+
static void DeclareIn(py::module& m) {
20+
py::class_<dp::ConfidenceInterval> confidence_interval(m, "ConfidenceInterval");
21+
confidence_interval.attr("__module__") = "pydp";
22+
confidence_interval
23+
.def_property("lower_bound", &dp::ConfidenceInterval::lower_bound,
24+
&dp::ConfidenceInterval::set_lower_bound)
25+
.def_property("upper_bound", &dp::ConfidenceInterval::upper_bound,
26+
&dp::ConfidenceInterval::set_upper_bound)
27+
.def_property("confidence_level", &dp::ConfidenceInterval::confidence_level,
28+
&dp::ConfidenceInterval::set_confidence_level);
29+
}
30+
};
31+
32+
template <typename T>
33+
py::class_<dp::NumericalMechanism>& DefPyAddNoise(
34+
py::class_<dp::NumericalMechanism>& pyclass) {
35+
using FunctorType = T (dp::NumericalMechanism::*)(T);
36+
return pyclass.def("add_noise",
37+
static_cast<FunctorType>(&dp::NumericalMechanism::AddNoise),
38+
py::arg("result"));
39+
}
40+
41+
template <typename T, typename U>
42+
py::class_<dp::NumericalMechanism>& DefPyAddNoise(
43+
py::class_<dp::NumericalMechanism>& pyclass) {
44+
using FunctorType = T (dp::NumericalMechanism::*)(T, U);
45+
return pyclass.def("add_noise",
46+
static_cast<FunctorType>(&dp::NumericalMechanism::AddNoise),
47+
py::arg("result"), py::arg("privacy_budget"));
48+
}
49+
50+
template <typename T, typename U>
51+
std::unique_ptr<T> downcast_unique_ptr(std::unique_ptr<U> u_ptr) {
52+
static_assert(std::is_base_of<U, T>::value, "Illegal downcast.");
53+
T* ptr = dynamic_cast<T*>(u_ptr.release());
54+
return std::unique_ptr<T>(ptr);
55+
}
56+
57+
class NumericalMechanismBinder {
58+
public:
59+
static void DeclareIn(py::module& m) {
60+
py::class_<dp::NumericalMechanism> numerical_mech(m, "NumericalMechanism",
61+
R"pbdoc(
62+
Base class for all (Ɛ, 𝛿)-differenially private additive noise numerical mechanisms.
63+
)pbdoc");
64+
numerical_mech.attr("__module__") = "pydp";
65+
DefPyAddNoise<int, double>(numerical_mech);
66+
DefPyAddNoise<int64_t, double>(numerical_mech);
67+
DefPyAddNoise<double, double>(numerical_mech);
68+
DefPyAddNoise<int>(numerical_mech);
69+
DefPyAddNoise<int64_t>(numerical_mech);
70+
DefPyAddNoise<double>(numerical_mech);
71+
numerical_mech
72+
.def("noised_value_above_threshold",
73+
&dp::NumericalMechanism::NoisedValueAboveThreshold,
74+
R"pbdoc(
75+
Quickly determines if `result` with added noise is above certain `threshold`.
76+
)pbdoc")
77+
.def("memory_used", &dp::NumericalMechanism::MemoryUsed)
78+
.def(
79+
"noise_confidence_interval",
80+
[](dp::NumericalMechanism& self, double cl, double pb,
81+
double nr) -> dp::ConfidenceInterval {
82+
auto result = self.NoiseConfidenceInterval(cl, pb, nr);
83+
return result.ValueOrDie();
84+
},
85+
py::arg("confidence_level"), py::arg("privacy_budget"),
86+
py::arg("noised_result"),
87+
R"pbdoc(
88+
Returns the confidence interval of the specified confidence level of the
89+
noise that AddNoise() would add with the specified privacy budget.
90+
If the returned value is <x,y>, then the noise added has a confidence_level
91+
chance of being in the domain [x,y]
92+
)pbdoc")
93+
.def_property_readonly("epsilon", &dp::NumericalMechanism::GetEpsilon,
94+
"The Ɛ of the numerical mechanism");
95+
}
96+
};
97+
98+
class LaplaceMechanismBinder {
99+
public:
100+
static std::unique_ptr<dp::LaplaceMechanism> build(double epsilon,
101+
double l1_sensitivity) {
102+
dp::LaplaceMechanism::Builder builder;
103+
builder.SetEpsilon(epsilon);
104+
builder.SetSensitivity(l1_sensitivity);
105+
builder.SetL1Sensitivity(l1_sensitivity);
106+
return downcast_unique_ptr<dp::LaplaceMechanism, dp::NumericalMechanism>(
107+
builder.Build().value());
108+
}
109+
110+
static std::unique_ptr<dp::NumericalMechanismBuilder> clone() {
111+
dp::LaplaceMechanism::Builder cloner;
112+
return std::move(cloner.Clone());
113+
}
114+
115+
static void DeclareIn(py::module& m) {
116+
py::class_<dp::LaplaceMechanism, dp::NumericalMechanism> lap_mech(
117+
m, "LaplaceMechanism");
118+
lap_mech.attr("__module__") = "pydp";
119+
lap_mech
120+
.def(py::init([](double epsilon, double sensitivity) {
121+
return build(epsilon, sensitivity);
122+
}),
123+
py::arg("epsilon"), py::arg("sensitivity") = 1.0)
124+
.def("get_uniform_double", &dp::LaplaceMechanism::GetUniformDouble)
125+
// .def("deserialize", &dp::LaplaceMechanism::Deserialize)
126+
// .def("serialize", &dp::LaplaceMechanism::Serialize)
127+
.def_property_readonly("sensitivity", &dp::LaplaceMechanism::GetSensitivity,
128+
"The L1 sensitivity of the query.")
129+
.def_property_readonly("diversity", &dp::LaplaceMechanism::GetDiversity,
130+
"The diversity of the Laplace mechanism.");
131+
}
132+
};
133+
134+
class GaussianMechanismBinder {
135+
public:
136+
static std::unique_ptr<dp::GaussianMechanism> build(double epsilon, double delta,
137+
double l2_sensitivity) {
138+
dp::GaussianMechanism::Builder builder;
139+
builder.SetEpsilon(epsilon);
140+
builder.SetDelta(delta);
141+
builder.SetL2Sensitivity(l2_sensitivity);
142+
return downcast_unique_ptr<dp::GaussianMechanism, dp::NumericalMechanism>(
143+
builder.Build().value());
144+
};
145+
146+
static std::unique_ptr<dp::NumericalMechanismBuilder> clone() {
147+
dp::GaussianMechanism::Builder cloner;
148+
return std::move(cloner.Clone());
149+
};
150+
151+
static void DeclareIn(py::module& m) {
152+
py::class_<dp::GaussianMechanism, dp::NumericalMechanism> gaus_mech(
153+
m, "GaussianMechanism");
154+
gaus_mech.attr("__module__") = "pydp";
155+
gaus_mech
156+
.def(py::init([](double epsilon, double delta, double l2_sensitivity) {
157+
return build(epsilon, delta, l2_sensitivity);
158+
}))
159+
// .def("deserialize", &dp::GaussianMechanism::Deserialize)
160+
// .def("serialize", &dp::GaussianMechanism::Serialize)
161+
.def_property_readonly("delta", &dp::GaussianMechanism::GetDelta,
162+
"The 𝛿 of the Gaussian mechanism.")
163+
.def_property_readonly(
164+
"std",
165+
[](const dp::GaussianMechanism& self) {
166+
return dp::GaussianMechanism::CalculateStddev(
167+
self.GetEpsilon(), self.GetDelta(), self.GetL2Sensitivity());
168+
},
169+
R"pbdoc(
170+
The standard deviation parameter of the
171+
Gaussian mechanism underlying distribution.
172+
)pbdoc")
173+
.def_property_readonly("l2_sensitivity",
174+
&dp::GaussianMechanism::GetL2Sensitivity,
175+
"The L2 sensitivity of the query.");
176+
}
177+
};
178+
179+
void init_mechanisms_mechanism(py::module& m) {
180+
ConfidenceIntervalBinder::DeclareIn(m);
181+
NumericalMechanismBinder::DeclareIn(m);
182+
LaplaceMechanismBinder::DeclareIn(m);
183+
GaussianMechanismBinder::DeclareIn(m);
184+
}

src/pydp/algorithms/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# pydp relative
22
from . import laplacian
33
from . import partition_selection
4+
from . import numerical_mechanisms
45

5-
__all__ = ["laplacian", "partition_selection"]
6+
__all__ = ["laplacian", "partition_selection", "numerical_mechanisms"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .._pydp._mechanisms import (
2+
NumericalMechanism, # type: ignore
3+
GaussianMechanism, # type: ignore
4+
LaplaceMechanism, # type: ignore
5+
ConfidenceInterval, # type: ignore
6+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import numpy as np
2+
import pytest
3+
import pydp.algorithms.numerical_mechanisms as num_mech
4+
from scipy.special import erfinv
5+
6+
7+
REL_ERR_TOL = 1e-5
8+
9+
10+
def assert_almost_eq(val_true, val_pred):
11+
return np.abs((val_true - val_pred) / val_true) < REL_ERR_TOL
12+
13+
14+
def test_basic():
15+
num_mech_methods = {
16+
"add_noise",
17+
"noised_value_above_threshold",
18+
"memory_used",
19+
"noise_confidence_interval",
20+
"epsilon",
21+
}
22+
assert num_mech_methods.issubset(set(dir(num_mech.NumericalMechanism)))
23+
epsilon, delta, sensitivity = 1, 1e-7, 5.0
24+
with pytest.raises(TypeError):
25+
# This is a abstract class, it cannot be instantiated!
26+
obj = num_mech.NumericalMechanism(epsilon, delta)
27+
obj = num_mech.LaplaceMechanism(epsilon, sensitivity)
28+
assert num_mech_methods.issubset(set(dir(obj)))
29+
assert {
30+
# "deserialize",
31+
# "serialize",
32+
"get_uniform_double",
33+
"memory_used",
34+
"sensitivity",
35+
"diversity",
36+
}.issubset(set(dir(obj)))
37+
obj = num_mech.GaussianMechanism(epsilon, delta, sensitivity)
38+
assert num_mech_methods.issubset(set(dir(obj)))
39+
assert {
40+
# "deserialize",
41+
# "serialize",
42+
"memory_used",
43+
"l2_sensitivity",
44+
"std",
45+
"delta",
46+
}.issubset(set(dir(obj)))
47+
48+
49+
def test_laplace_mechanism():
50+
epsilon, sensitivity = 1, 3.0
51+
laplace = num_mech.LaplaceMechanism(epsilon, sensitivity)
52+
value = 0
53+
value = laplace.add_noise(value)
54+
assert type(value) is int
55+
value = laplace.add_noise(value, 0.1)
56+
assert type(value) is int
57+
value = 0.0
58+
value = laplace.add_noise(value)
59+
assert type(value) is float
60+
value = laplace.add_noise(value, 0.1)
61+
assert type(value) is float
62+
conf_level = 0.5
63+
priv_budg = 0.1
64+
interval = laplace.noise_confidence_interval(0.5, 0.1, value)
65+
assert type(interval) is num_mech.ConfidenceInterval
66+
bound = laplace.diversity * np.log(1 - conf_level) / priv_budg
67+
lower_bound, upper_bound = value - bound, value + bound
68+
assert_almost_eq(lower_bound, interval.lower_bound)
69+
assert_almost_eq(upper_bound, interval.upper_bound)
70+
assert conf_level == interval.confidence_level
71+
72+
73+
def test_gaussian_mechanism():
74+
epsilon, delta, l2_sensitivity = 1, 1e-5, 3.0
75+
gaussian = num_mech.GaussianMechanism(epsilon, delta, l2_sensitivity)
76+
value = 0
77+
value = gaussian.add_noise(value)
78+
assert type(value) is int
79+
value = gaussian.add_noise(value, 0.1)
80+
assert type(value) is int
81+
value = 0.0
82+
value = gaussian.add_noise(value)
83+
assert type(value) is float
84+
value = gaussian.add_noise(value, 0.1)
85+
assert type(value) is float
86+
conf_level = 0.5
87+
priv_budg = 0.1
88+
interval = gaussian.noise_confidence_interval(0.5, 0.1, value)
89+
local_gaussian = num_mech.GaussianMechanism(
90+
priv_budg * epsilon, priv_budg * delta, l2_sensitivity
91+
)
92+
assert type(interval) is num_mech.ConfidenceInterval
93+
bound = erfinv(-conf_level) * local_gaussian.std * (2 ** 0.5)
94+
lower_bound, upper_bound = value - bound, value + bound
95+
assert_almost_eq(lower_bound, interval.lower_bound)
96+
assert_almost_eq(upper_bound, interval.upper_bound)
97+
assert conf_level == interval.confidence_level

0 commit comments

Comments
 (0)