|
1 | 1 | package lineartransform |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "fmt" |
5 | | - "github.com/tuneinsight/lattigo/v6/circuits/ckks/lintrans" |
6 | | - "github.com/tuneinsight/lattigo/v6/core/rlwe" |
7 | | - "github.com/tuneinsight/lattigo/v6/ring" |
8 | | - "github.com/tuneinsight/lattigo/v6/schemes/ckks" |
9 | 4 | "math" |
10 | 5 | "testing" |
| 6 | + |
| 7 | + "github.com/tuneinsight/lattigo/v6/schemes/ckks" |
11 | 8 | ) |
12 | 9 |
|
13 | 10 | func TestLinearTransform(t *testing.T) { |
14 | | - // This test is a bit weird because we're skipping most of the pipeline |
15 | | - // to just test the emitter for linear_transform which is needed in the |
16 | | - // context of comparisons with the orion compiler. This requires us to |
17 | | - // manually set up the crypto parameters and encode/encrypt stuff. |
| 11 | + evaluator, params, encoder, encryptor, decryptor := linear_transform__configure() |
| 12 | + numSlots := params.MaxSlots() |
18 | 13 |
|
19 | | - // Input vector of all 1s |
20 | | - numSlots := 4096 |
21 | 14 | inputClear := make([]float64, numSlots) |
22 | 15 | for i := range inputClear { |
23 | 16 | inputClear[i] = 1.0 |
24 | 17 | } |
25 | 18 |
|
26 | | - // Matrix of weights. This represents two nonzero diagonals on an |
27 | | - // otherwise all-zero matrix: |
28 | | - // |
29 | | - // 0 4096 |
30 | | - // 1 4097 |
31 | | - // 2 4098 |
32 | | - // ... |
33 | | - // 4094 8190 |
34 | | - // 8191 4095 |
35 | | - // |
36 | | - // In this way, the nonzero diagonals become |
37 | | - // diagonal 0: range(0, 4096) |
38 | | - // diagonal 1: range(4096, 8192) |
| 19 | + // Matrix of weights: two nonzero diagonals on an otherwise all-zero matrix. |
39 | 20 | // |
40 | | - // and the expected values are (noting lattigo does |
41 | | - // left-multiplication of the cleartext matrix and the vector is all |
42 | | - // 1s): |
| 21 | + // diagonal 0: [0, 1, 2, ..., 4095] |
| 22 | + // diagonal 1: [4096, 4097, ..., 8191] |
43 | 23 | // |
44 | | - // [4096 + 0, 4097 + 1, ..., 8191 + 4095] |
| 24 | + // With an all-1s input vector, expected output = diagonal0 + diagonal1: |
| 25 | + // [4096+0, 4097+1, ..., 8191+4095] |
45 | 26 | diagonals := 2 |
46 | | - cols := numSlots |
47 | | - // Matrix is flattened 2 x numSlots |
48 | | - matrix := make([]float64, diagonals*cols) |
| 27 | + matrix := make([]float64, diagonals*numSlots) |
49 | 28 | value := 0.0 |
50 | 29 | for r := 0; r < diagonals; r++ { |
51 | | - for c := 0; c < cols; c++ { |
52 | | - matrix[r*cols+c] = value |
| 30 | + for c := 0; c < numSlots; c++ { |
| 31 | + matrix[r*numSlots+c] = value |
53 | 32 | value++ |
54 | 33 | } |
55 | 34 | } |
56 | 35 |
|
57 | 36 | expectedClear := make([]float64, numSlots) |
58 | 37 | for i := range expectedClear { |
59 | | - expectedClear[i] = float64(4096 + 2*i) |
| 38 | + expectedClear[i] = float64(numSlots + 2*i) |
60 | 39 | } |
61 | 40 |
|
62 | | - // These parameters should match linear_transform.mlir, though due to |
63 | | - // the weird nature of this test, this is the source of truth for what |
64 | | - // is used, not the mlir file. |
65 | | - param, err := ckks.NewParametersFromLiteral(ckks.ParametersLiteral{ |
66 | | - LogN: 13, |
67 | | - Q: []uint64{536903681, 67043329, 66994177, 67239937, 66961409, 66813953}, |
68 | | - P: []uint64{536952833, 536690689}, |
69 | | - LogDefaultScale: 26, |
70 | | - }) |
71 | | - if err != nil { |
72 | | - panic(err) |
73 | | - } |
74 | | - |
75 | | - encoder := ckks.NewEncoder(param) |
76 | | - kgen := rlwe.NewKeyGenerator(param) |
77 | | - sk, pk := kgen.GenKeyPairNew() |
78 | | - encryptor := rlwe.NewEncryptor(param, pk) |
79 | | - decryptor := rlwe.NewDecryptor(param, sk) |
80 | | - |
81 | | - // This is copied from the generated code so we can get access to the |
82 | | - // Lattigo-produced Galois key set to generate... ideally this is moved |
83 | | - // to a shared client helper generated by HEIR, but the client |
84 | | - // interface generation is before lowering to scheme, and Orion enters |
85 | | - // post-lowering-to-scheme. For this test it's OK and, if nothing else, |
86 | | - // a good reference. |
87 | | - ct1diags := make(lintrans.Diagonals[float64]) |
88 | | - for i := 0; i < 2; i++ { |
89 | | - ct1diags[i] = matrix[i*numSlots : (i+1)*numSlots] |
| 41 | + pt := ckks.NewPlaintext(params, params.MaxLevel()) |
| 42 | + pt.Scale = params.DefaultScale() |
| 43 | + if err := encoder.Encode(inputClear, pt); err != nil { |
| 44 | + t.Fatal(err) |
90 | 45 | } |
91 | | - ct1params := lintrans.Parameters{ |
92 | | - DiagonalsIndexList: ct1diags.DiagonalsIndexList(), |
93 | | - LevelQ: 5, |
94 | | - LevelP: param.MaxLevelP(), |
95 | | - Scale: rlwe.NewScale(param.Q()[5]), |
96 | | - LogDimensions: ring.Dimensions{Rows: 0, Cols: 12}, // 1x4096 |
97 | | - LogBabyStepGiantStepRatio: 2, |
98 | | - } |
99 | | - ct1lt := lintrans.NewTransformation(param, ct1params) |
100 | | - galEls := ct1lt.GaloisElements(param) |
101 | | - |
102 | | - // Manually add Galois key for rotation index 2048 |
103 | | - rotIndex := 2048 |
104 | | - logN := 13 |
105 | | - galoisElement := uint64(1) |
106 | | - for i := 0; i < rotIndex; i++ { |
107 | | - galoisElement = (galoisElement * 5) % (1 << (logN + 1)) |
108 | | - } |
109 | | - galEls = append(galEls, galoisElement) |
110 | | - fmt.Printf("Final galEls: %v\n", galEls) |
111 | | - |
112 | | - evk := rlwe.NewMemEvaluationKeySet(nil, kgen.GenGaloisKeysNew(galEls, sk)...) |
113 | | - evaluator := ckks.NewEvaluator(param, evk) |
114 | | - |
115 | | - pt := ckks.NewPlaintext(param, param.MaxLevel()) |
116 | | - pt.LogDimensions = ring.Dimensions{Rows: 0, Cols: 12} // 2^(0+12) = 4096 slots |
117 | | - encoder.Encode(inputClear, pt) |
118 | | - ctInput, err25 := encryptor.EncryptNew(pt) |
119 | | - if err25 != nil { |
120 | | - panic(err25) |
| 46 | + ct, err := encryptor.EncryptNew(pt) |
| 47 | + if err != nil { |
| 48 | + t.Fatal(err) |
121 | 49 | } |
122 | 50 |
|
123 | | - resultCt := linear_transform(evaluator, param, encoder, ctInput, matrix) |
| 51 | + resultCt := linear_transform(evaluator, params, encoder, ct, matrix) |
124 | 52 | resultPt := decryptor.DecryptNew(resultCt) |
125 | | - resultFloat64 := make([]float64, 4096) |
126 | | - encoder.Decode(resultPt, resultFloat64) |
| 53 | + resultFloat64 := make([]float64, numSlots) |
| 54 | + if err := encoder.Decode(resultPt, resultFloat64); err != nil { |
| 55 | + t.Fatal(err) |
| 56 | + } |
127 | 57 |
|
128 | | - // We need such a large epsilon because scale 26 is not very precise, |
129 | | - // increasing scale to 40 produces errors of about 1e-04. |
| 58 | + // Scale 26 is not very precise; epsilon of 1.5 is sufficient here. |
130 | 59 | epsilon := 1.5 |
131 | 60 | for i := 0; i < numSlots; i++ { |
132 | 61 | diff := math.Abs(resultFloat64[i] - expectedClear[i]) |
133 | 62 | if diff > epsilon { |
134 | 63 | t.Errorf("Mismatch at index %d: got %f, expected %f (diff: %e)", |
135 | 64 | i, resultFloat64[i], expectedClear[i], diff) |
136 | | - |
137 | | - // Fail fast to avoid spamming 4096 errors |
138 | 65 | if i > 10 { |
139 | 66 | t.Fatal("Too many errors, stopping verification.") |
140 | 67 | } |
|
0 commit comments