[ckks] polynomial evaluation return an error when ciphertext level doesn't enable full evaluation (closes #71)

In addition to the partially evaluated result, the homomorphic polynomial evaluation methods return an error if the ciphertext level does not enable full evaluation.
This commit is contained in:
Jean-Philippe Bossuat
2020-11-06 16:48:47 +01:00
committed by GitHub
parent 6202936e00
commit f023ff529d
7 changed files with 148 additions and 46 deletions

View File

@@ -502,7 +502,7 @@ func (btp *Bootstrapper) evaluateCheby(ct *Ciphertext) (res *Ciphertext) {
eval.AddConst(C[1], -0.5/(scfac*(cheby.b-cheby.a)), C[1])
}
res = eval.evalCheby(cheby, C, btp.relinkey)
res, _ = eval.evalCheby(cheby, C, btp.relinkey)
sqrt2pi := math.Pow(0.15915494309189535, 1.0/float64(int(1<<btp.SinRescal)))

View File

@@ -75,7 +75,9 @@ func TestBootstrapp(t *testing.T) {
//fmt.Println(ciphertext.Level() - 1)
//start := time.Now()
ciphertext = testContext.evaluator.EvaluateCheby(ciphertext, cheby, testContext.rlk)
if ciphertext, err = testContext.evaluator.EvaluateCheby(ciphertext, cheby, testContext.rlk); err != nil {
t.Error(err)
}
//fmt.Printf("Elapsed : %s \n", time.Since(start))
//fmt.Println(ciphertext.Level())
@@ -132,7 +134,9 @@ func TestBootstrapp(t *testing.T) {
//fmt.Println(ciphertext.Level(), ciphertext.Scale())
//start := time.Now()
ciphertext = testContext.evaluator.EvaluateChebySpecial(ciphertext, scFac, cheby, testContext.rlk)
if ciphertext, err = testContext.evaluator.EvaluateChebySpecial(ciphertext, scFac, cheby, testContext.rlk); err != nil {
t.Error(err)
}
//fmt.Println(ciphertext.Level(), ciphertext.Scale())
for i := 0; i < scNum; i++ {
@@ -193,7 +197,9 @@ func TestBootstrapp(t *testing.T) {
//fmt.Println(ciphertext.Level(), ciphertext.Scale())
//start := time.Now()
ciphertext = testContext.evaluator.EvaluateChebySpecial(ciphertext, scFac, cheby, testContext.rlk)
if ciphertext, err = testContext.evaluator.EvaluateChebySpecial(ciphertext, scFac, cheby, testContext.rlk); err != nil {
t.Error(err)
}
//fmt.Println(ciphertext.Level(), ciphertext.Scale())
for i := 0; i < scNum; i++ {

View File

@@ -733,6 +733,8 @@ func testFunctions(testContext *testParams, t *testing.T) {
func testEvaluatePoly(testContext *testParams, t *testing.T) {
var err error
t.Run(testString(testContext, "EvaluatePoly/Exp/"), func(t *testing.T) {
if testContext.params.MaxLevel() < 3 {
@@ -758,7 +760,9 @@ func testEvaluatePoly(testContext *testParams, t *testing.T) {
values[i] = cmplx.Exp(values[i])
}
ciphertext = testContext.evaluator.EvaluatePoly(ciphertext, poly, testContext.rlk)
if ciphertext, err = testContext.evaluator.EvaluatePoly(ciphertext, poly, testContext.rlk); err != nil {
t.Error(err)
}
verifyTestVectors(testContext, testContext.decryptor, values, ciphertext, t)
})
@@ -766,6 +770,8 @@ func testEvaluatePoly(testContext *testParams, t *testing.T) {
func testChebyshevInterpolator(testContext *testParams, t *testing.T) {
var err error
t.Run(testString(testContext, "ChebyshevInterpolator/Sin/"), func(t *testing.T) {
if testContext.params.MaxLevel() < 5 {
@@ -780,7 +786,9 @@ func testChebyshevInterpolator(testContext *testParams, t *testing.T) {
values[i] = cmplx.Sin(values[i])
}
ciphertext = testContext.evaluator.EvaluateCheby(ciphertext, cheby, testContext.rlk)
if ciphertext, err = testContext.evaluator.EvaluateCheby(ciphertext, cheby, testContext.rlk); err != nil {
t.Error(err)
}
verifyTestVectors(testContext, testContext.decryptor, values, ciphertext, t)
})

View File

@@ -56,9 +56,9 @@ type Evaluator interface {
PowerNew(op *Ciphertext, degree uint64, evakey *EvaluationKey) (opOut *Ciphertext)
Power(ct0 *Ciphertext, degree uint64, evakey *EvaluationKey, res *Ciphertext)
InverseNew(ct0 *Ciphertext, steps uint64, evakey *EvaluationKey) (res *Ciphertext)
EvaluatePoly(ct *Ciphertext, coeffs *Poly, evakey *EvaluationKey) (res *Ciphertext)
EvaluateCheby(ct *Ciphertext, cheby *ChebyshevInterpolation, evakey *EvaluationKey) (res *Ciphertext)
EvaluateChebySpecial(ct *Ciphertext, n complex128, cheby *ChebyshevInterpolation, evakey *EvaluationKey) (res *Ciphertext)
EvaluatePoly(ct *Ciphertext, coeffs *Poly, evakey *EvaluationKey) (res *Ciphertext, err error)
EvaluateCheby(ct *Ciphertext, cheby *ChebyshevInterpolation, evakey *EvaluationKey) (res *Ciphertext, err error)
EvaluateChebySpecial(ct *Ciphertext, n complex128, cheby *ChebyshevInterpolation, evakey *EvaluationKey) (res *Ciphertext, err error)
}
// evaluator is a struct that holds the necessary elements to execute the homomorphic operations between Ciphertexts and/or Plaintexts.
@@ -1243,10 +1243,19 @@ func (eval *evaluator) RescaleNew(ct0 *Ciphertext, threshold float64) (ctOut *Ci
// in ctOut. Since all the moduli in the moduli chain are generated to be close to the
// original scale, this procedure is equivalent to dividing the input element by the scale and adding
// some error.
// Returns an error if "threshold <= 0", ct.Scale() = 0, ct.Level() = 0, ct.IsNTT() != true or if ct.Leve() != ctOut.Level()
func (eval *evaluator) Rescale(ct0 *Ciphertext, threshold float64, ctOut *Ciphertext) (err error) {
ringQ := eval.ringQ
if threshold <= 0 {
return errors.New("cannot Rescale: threshold is 0")
}
if ct0.Scale() == 0 {
return errors.New("cannot Rescale: ciphertext scale is 0")
}
if ct0.Level() == 0 {
return errors.New("cannot Rescale: input Ciphertext already at level 0")
}

View File

@@ -1,6 +1,7 @@
package ckks
import (
"fmt"
"math"
"math/bits"
)
@@ -25,6 +26,21 @@ func NewPoly(coeffs []complex128) (p *Poly) {
return
}
func checkEnoughLevels(levels uint64, pol *Poly, c complex128) (err error) {
logDegree := uint64(math.Log2(float64(len(pol.coeffs))) + 0.5)
if real(c) != float64(int64(real(c))) || imag(c) != float64(int64(imag(c))) {
logDegree++
}
if levels < logDegree {
return fmt.Errorf("%d levels < %d log(d) -> cannot evaluate", levels, logDegree)
}
return nil
}
// Degree returns the degree of the polynomial
func (p *Poly) Degree() uint64 {
return uint64(len(p.coeffs) - 1)
@@ -61,7 +77,13 @@ func computeSmallPoly(split uint64, coeffs *Poly) (polyList []*Poly) {
}
// EvaluatePoly evaluates a polynomial in standard basis on the input Ciphertext in ceil(log2(deg+1)) levels.
func (eval *evaluator) EvaluatePoly(ct0 *Ciphertext, pol *Poly, evakey *EvaluationKey) (res *Ciphertext) {
// Returns an error if the input ciphertext does not have enough level to carry out the full polynomial evaluation.
// Returns an error if something is wrong with the scale.
func (eval *evaluator) EvaluatePoly(ct0 *Ciphertext, pol *Poly, evakey *EvaluationKey) (opOut *Ciphertext, err error) {
if err := checkEnoughLevels(ct0.Level(), pol, 1); err != nil {
return ct0, err
}
C := make(map[uint64]*Ciphertext)
@@ -78,15 +100,19 @@ func (eval *evaluator) EvaluatePoly(ct0 *Ciphertext, pol *Poly, evakey *Evaluati
computePowerBasis(1<<i, C, eval, evakey)
}
res = recurse(eval.scale, logSplit, logDegree, pol, C, eval, evakey)
opOut, err = recurse(eval.scale, logSplit, logDegree, pol, C, eval, evakey)
C = nil
return
return opOut, nil
}
// EvaluateCheby evaluates a polynomial in Chebyshev basis on the input Ciphertext in ceil(log2(deg+1))+1 levels.
func (eval *evaluator) EvaluateCheby(op *Ciphertext, cheby *ChebyshevInterpolation, evakey *EvaluationKey) (opOut *Ciphertext) {
// Returns an error if the input ciphertext does not have enough level to carry out the full polynomial evaluation.
// Returns an error if something is wrong with the scale.
func (eval *evaluator) EvaluateCheby(op *Ciphertext, cheby *ChebyshevInterpolation, evakey *EvaluationKey) (opOut *Ciphertext, err error) {
if err := checkEnoughLevels(op.Level(), &cheby.Poly, 2/(cheby.b-cheby.a)); err != nil {
return op, err
}
C := make(map[uint64]*Ciphertext)
@@ -101,11 +127,17 @@ func (eval *evaluator) EvaluateCheby(op *Ciphertext, cheby *ChebyshevInterpolati
// EvaluateChebyFastSpecial evaluates the input Chebyshev polynomial with the input ciphertext.
// Slower than EvaluateChebyFast but consumes ceil(log(deg)) + 1 levels.
func (eval *evaluator) EvaluateChebySpecial(ct *Ciphertext, n complex128, cheby *ChebyshevInterpolation, evakey *EvaluationKey) (res *Ciphertext) {
// Returns an error if the input ciphertext does not have enough level to carry out the full polynomial evaluation.
// Returns an error if something is wrong with the scale.
func (eval *evaluator) EvaluateChebySpecial(op *Ciphertext, n complex128, cheby *ChebyshevInterpolation, evakey *EvaluationKey) (opOut *Ciphertext, err error) {
if err := checkEnoughLevels(op.Level(), &cheby.Poly, 2/((cheby.b-cheby.a)*n)); err != nil {
return op, err
}
C := make(map[uint64]*Ciphertext)
C[1] = ct.CopyNew().Ciphertext()
C[1] = op.CopyNew().Ciphertext()
eval.MultByConst(C[1], 2/((cheby.b-cheby.a)*n), C[1])
eval.AddConst(C[1], (-cheby.a-cheby.b)/(cheby.b-cheby.a), C[1])
@@ -114,27 +146,31 @@ func (eval *evaluator) EvaluateChebySpecial(ct *Ciphertext, n complex128, cheby
return eval.evalCheby(cheby, C, evakey)
}
func (eval *evaluator) evalCheby(cheby *ChebyshevInterpolation, C map[uint64]*Ciphertext, evakey *EvaluationKey) (res *Ciphertext) {
func (eval *evaluator) evalCheby(cheby *ChebyshevInterpolation, C map[uint64]*Ciphertext, evakey *EvaluationKey) (opOut *Ciphertext, err error) {
logDegree := uint64(bits.Len64(cheby.Degree()))
logSplit := (logDegree >> 1) //optimalSplit(logDegree) //
for i := uint64(2); i < (1 << logSplit); i++ {
computePowerBasisCheby(i, C, eval, evakey)
if err = computePowerBasisCheby(i, C, eval, evakey); err != nil {
return nil, err
}
}
for i := logSplit; i < logDegree; i++ {
computePowerBasisCheby(1<<i, C, eval, evakey)
if err = computePowerBasisCheby(1<<i, C, eval, evakey); err != nil {
return nil, err
}
}
res = recurseCheby(eval.scale, logSplit, logDegree, &cheby.Poly, C, eval, evakey)
opOut, err = recurseCheby(eval.scale, logSplit, logDegree, &cheby.Poly, C, eval, evakey)
C = nil
return
return opOut, err
}
func computePowerBasis(n uint64, C map[uint64]*Ciphertext, evaluator *evaluator, evakey *EvaluationKey) {
func computePowerBasis(n uint64, C map[uint64]*Ciphertext, evaluator *evaluator, evakey *EvaluationKey) (err error) {
if C[n] == nil {
@@ -143,17 +179,25 @@ func computePowerBasis(n uint64, C map[uint64]*Ciphertext, evaluator *evaluator,
b := n >> 1
// Recurses on the given indexes
computePowerBasis(a, C, evaluator, evakey)
computePowerBasis(b, C, evaluator, evakey)
if err = computePowerBasis(a, C, evaluator, evakey); err != nil {
return err
}
if err = computePowerBasis(b, C, evaluator, evakey); err != nil {
return err
}
// Computes C[n] = C[a]*C[b]
C[n] = evaluator.MulRelinNew(C[a], C[b], evakey)
evaluator.Rescale(C[n], evaluator.scale, C[n])
if err = evaluator.Rescale(C[n], evaluator.scale, C[n]); err != nil {
return err
}
}
return nil
}
func computePowerBasisCheby(n uint64, C map[uint64]*Ciphertext, evaluator *evaluator, evakey *EvaluationKey) {
func computePowerBasisCheby(n uint64, C map[uint64]*Ciphertext, evaluator *evaluator, evakey *EvaluationKey) (err error) {
// Given a hash table with the first three evaluations of the Chebyshev ring at x in the interval a, b:
// C0 = 1 (actually not stored in the hash table)
@@ -171,18 +215,26 @@ func computePowerBasisCheby(n uint64, C map[uint64]*Ciphertext, evaluator *evalu
c := uint64(math.Abs(float64(a) - float64(b)))
// Recurses on the given indexes
computePowerBasisCheby(a, C, evaluator, evakey)
computePowerBasisCheby(b, C, evaluator, evakey)
if err = computePowerBasisCheby(a, C, evaluator, evakey); err != nil {
return err
}
if err = computePowerBasisCheby(b, C, evaluator, evakey); err != nil {
return err
}
// Since C[0] is not stored (but rather seen as the constant 1), only recurses on c if c!= 0
if c != 0 {
computePowerBasisCheby(c, C, evaluator, evakey)
if err = computePowerBasisCheby(c, C, evaluator, evakey); err != nil {
return err
}
}
// Computes C[n] = C[a]*C[b]
//fmt.Println("Mul", C[a].Level(), C[b].Level())
C[n] = evaluator.MulRelinNew(C[a], C[b], evakey)
evaluator.Rescale(C[n], evaluator.scale, C[n])
if err = evaluator.Rescale(C[n], evaluator.scale, C[n]); err != nil {
return err
}
// Computes C[n] = 2*C[a]*C[b]
evaluator.Add(C[n], C[n], C[n])
@@ -195,6 +247,8 @@ func computePowerBasisCheby(n uint64, C map[uint64]*Ciphertext, evaluator *evalu
}
}
return nil
}
func splitCoeffs(coeffs *Poly, split uint64) (coeffsq, coeffsr *Poly) {
@@ -261,7 +315,7 @@ func splitCoeffsCheby(coeffs *Poly, split uint64) (coeffsq, coeffsr *Poly) {
return coeffsq, coeffsr
}
func recurse(targetScale float64, logSplit, logDegree uint64, coeffs *Poly, C map[uint64]*Ciphertext, evaluator *evaluator, evakey *EvaluationKey) (res *Ciphertext) {
func recurse(targetScale float64, logSplit, logDegree uint64, coeffs *Poly, C map[uint64]*Ciphertext, evaluator *evaluator, evakey *EvaluationKey) (res *Ciphertext, err error) {
// Recursively computes the evalution of the Chebyshev polynomial using a baby-set giant-step algorithm.
if coeffs.Degree() < (1 << logSplit) {
@@ -295,10 +349,14 @@ func recurse(targetScale float64, logSplit, logDegree uint64, coeffs *Poly, C ma
//fmt.Printf("X^%2d: %f %f\n", nextPower, targetScale, targetScale* currentQi / C[nextPower].Scale())
//fmt.Printf("X^%2d : qi %d %t %d %d\n", nextPower, level, coeffsq.lead, coeffsq.maxDeg, 1<<(logDegree-1))
//fmt.Println()
var tmp *Ciphertext
if res, err = recurse(targetScale*currentQi/C[nextPower].Scale(), logSplit, logDegree, coeffsq, C, evaluator, evakey); err != nil {
return nil, err
}
res = recurse(targetScale*currentQi/C[nextPower].Scale(), logSplit, logDegree, coeffsq, C, evaluator, evakey)
tmp := recurse(targetScale, logSplit, logDegree, coeffsr, C, evaluator, evakey)
if tmp, err = recurse(targetScale, logSplit, logDegree, coeffsr, C, evaluator, evakey); err != nil {
return nil, err
}
if res.Level() > tmp.Level() {
for res.Level() != tmp.Level()+1 {
@@ -310,13 +368,18 @@ func recurse(targetScale float64, logSplit, logDegree uint64, coeffs *Poly, C ma
evaluator.MulRelin(res, C[nextPower], evakey, res)
if res.Level() > tmp.Level() {
evaluator.Rescale(res, evaluator.scale, res)
if err = evaluator.Rescale(res, evaluator.scale, res); err != nil {
return nil, err
}
//fmt.Printf("%f = %d) + (%d %f) = ", res.Scale(), res.Level(), tmp.Level(), tmp.Scale())
evaluator.Add(res, tmp, res)
//fmt.Printf("(%d %f) %f\n", res.Level(), res.Scale(), res.Scale()-tmp.Scale())
} else {
evaluator.Add(res, tmp, res)
evaluator.Rescale(res, evaluator.scale, res)
if err = evaluator.Rescale(res, evaluator.scale, res); err != nil {
return nil, err
}
}
tmp = nil
@@ -324,7 +387,7 @@ func recurse(targetScale float64, logSplit, logDegree uint64, coeffs *Poly, C ma
return
}
func recurseCheby(targetScale float64, logSplit, logDegree uint64, coeffs *Poly, C map[uint64]*Ciphertext, evaluator *evaluator, evakey *EvaluationKey) (res *Ciphertext) {
func recurseCheby(targetScale float64, logSplit, logDegree uint64, coeffs *Poly, C map[uint64]*Ciphertext, evaluator *evaluator, evakey *EvaluationKey) (res *Ciphertext, err error) {
// Recursively computes the evalution of the Chebyshev polynomial using a baby-set giant-step algorithm.
if coeffs.Degree() < (1 << logSplit) {
@@ -359,10 +422,14 @@ func recurseCheby(targetScale float64, logSplit, logDegree uint64, coeffs *Poly,
//fmt.Printf("X^%2d : qi %d %t %d %d\n", nextPower, level, coeffsq.lead, coeffsq.maxDeg, 1<<(logDegree-1))
//fmt.Println()
res = recurseCheby(targetScale*currentQi/C[nextPower].Scale(), logSplit, logDegree, coeffsq, C, evaluator, evakey)
if res, err = recurseCheby(targetScale*currentQi/C[nextPower].Scale(), logSplit, logDegree, coeffsq, C, evaluator, evakey); err != nil {
return nil, err
}
var tmp *Ciphertext
tmp = recurseCheby(targetScale, logSplit, logDegree, coeffsr, C, evaluator, evakey)
if tmp, err = recurseCheby(targetScale, logSplit, logDegree, coeffsr, C, evaluator, evakey); err != nil {
return nil, err
}
if res.Level() > tmp.Level() {
for res.Level() != tmp.Level()+1 {
@@ -374,13 +441,17 @@ func recurseCheby(targetScale float64, logSplit, logDegree uint64, coeffs *Poly,
evaluator.MulRelin(res, C[nextPower], evakey, res)
if res.Level() > tmp.Level() {
evaluator.Rescale(res, evaluator.scale, res)
if err = evaluator.Rescale(res, evaluator.scale, res); err != nil {
return nil, err
}
//fmt.Printf("%f = %d) + (%d %f) = ", res.Scale(), res.Level(), tmp.Level(), tmp.Scale())
evaluator.Add(res, tmp, res)
//fmt.Printf("(%d %f) %f\n", res.Level(), res.Scale(), res.Scale()-tmp.Scale())
} else {
evaluator.Add(res, tmp, res)
evaluator.Rescale(res, evaluator.scale, res)
if err = evaluator.Rescale(res, evaluator.scale, res); err != nil {
return nil, err
}
}
tmp = nil
@@ -389,7 +460,7 @@ func recurseCheby(targetScale float64, logSplit, logDegree uint64, coeffs *Poly,
}
func evaluatePolyFromPowerBasis(targetScale float64, coeffs *Poly, C map[uint64]*Ciphertext, evaluator *evaluator) (res *Ciphertext) {
func evaluatePolyFromPowerBasis(targetScale float64, coeffs *Poly, C map[uint64]*Ciphertext, evaluator *evaluator) (res *Ciphertext, err error) {
if coeffs.Degree() == 0 {
@@ -430,7 +501,9 @@ func evaluatePolyFromPowerBasis(targetScale float64, coeffs *Poly, C map[uint64]
}
}
evaluator.Rescale(res, evaluator.scale, res)
if err = evaluator.Rescale(res, evaluator.scale, res); err != nil {
return nil, err
}
return
}

View File

@@ -153,7 +153,9 @@ func example() {
poly := ckks.NewPoly(coeffs)
ciphertext = evaluator.EvaluatePoly(ciphertext, poly, rlk)
if ciphertext, err = evaluator.EvaluatePoly(ciphertext, poly, rlk); err != nil {
panic(err)
}
fmt.Printf("Done in %s \n", time.Since(start))

View File

@@ -20,6 +20,8 @@ func randomComplex(min, max float64) complex128 {
func chebyshevinterpolation() {
var err error
// This example packs random 8192 float64 values in the range [-8, 8]
// and approximates the function 1/(exp(-x) + 1) over the range [-8, 8].
// The result is then parsed and compared to the expected result.
@@ -79,7 +81,9 @@ func chebyshevinterpolation() {
chebyapproximation := ckks.Approximate(f, -8, 8, 33)
// We evaluate the interpolated Chebyshev interpolant on the ciphertext
ciphertext = evaluator.EvaluateCheby(ciphertext, chebyapproximation, rlk)
if ciphertext, err = evaluator.EvaluateCheby(ciphertext, chebyapproximation, rlk); err != nil {
panic(err)
}
fmt.Println("Done... Consumed levels:", params.MaxLevel()-ciphertext.Level())