polyhe 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
polyhe/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """PolyHE package"""
2
+
3
+ import enum
4
+ import importlib
5
+
6
+ from polyhe.core import Options
7
+ from polyhe.core.context import Context
8
+ from polyhe.core.ciphertext import Ciphertext
9
+
10
+
11
+ __all__ = (
12
+ "new",
13
+ "Backend",
14
+ "Options",
15
+ "Context",
16
+ "Ciphertext"
17
+ )
18
+
19
+
20
+ class Backend(enum.StrEnum):
21
+ """Encryption backend"""
22
+ OPENFHE = enum.auto()
23
+ TENSEAL = enum.auto()
24
+ UARCHFHE = enum.auto()
25
+
26
+
27
+ def new(backend: Backend = Backend.OPENFHE, options: Options = Options()) -> Context:
28
+ """Generate encryption context"""
29
+ module = importlib.import_module(f"{__name__}.{backend}")
30
+ cls: type[Context] = getattr(module, "Context")
31
+ return cls(options)
@@ -0,0 +1,80 @@
1
+ """Structures package"""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+
7
+
8
+ __all__ = (
9
+ "Options",
10
+ )
11
+
12
+
13
+ # COEFF_MODULUS[security_level][poly_degree]
14
+ # source: sealapi.CoeffModulus.BFVDefault
15
+ COEFF_MODULUS = {
16
+ 128: {
17
+ 10: [27],
18
+ 11: [54],
19
+ 12: [36, 36, 37],
20
+ 13: [43, 43, 44, 44, 44],
21
+ 14: [48, 48, 48, 49, 49, 49, 49, 49, 49],
22
+ 15: [55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 56]
23
+ },
24
+ 192: {
25
+ 10: [19],
26
+ 11: [37],
27
+ 12: [25, 25, 25],
28
+ 13: [38, 38, 38, 38],
29
+ 14: [50, 50, 50, 50, 50, 50],
30
+ 15: [54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55]
31
+ },
32
+ 256: {
33
+ 10: [14],
34
+ 11: [29],
35
+ 12: [58],
36
+ 13: [39, 39, 40],
37
+ 14: [47, 47, 47, 48, 48],
38
+ 15: [52, 53, 53, 53, 53, 53, 53, 53, 53]
39
+ }
40
+ }
41
+
42
+
43
+ @dataclass(repr=False, slots=True, frozen=True)
44
+ class Descriptor:
45
+ """Data descriptor"""
46
+ type: type
47
+ shape: tuple[int, ...]
48
+
49
+ @property
50
+ def numpy(self) -> bool:
51
+ """Is numpy-like"""
52
+ return issubclass(self.type, np.number)
53
+
54
+ def __repr__(self) -> str:
55
+ """Descriptor representation"""
56
+ return f"<{self.__class__.__name__} type={self.type.__name__} shape={self.shape}>"
57
+
58
+
59
+ @dataclass(slots=True, frozen=True)
60
+ class Options:
61
+ """Encryption options"""
62
+ slots: int = 13
63
+ scale: int = 40
64
+ security: int = 128
65
+
66
+
67
+ def numpy2python(scalar: np.number) -> type:
68
+ """Transform numpy scalar to python scalar"""
69
+ return type(scalar().tolist()) # type: ignore
70
+
71
+
72
+ def materialize(obj):
73
+ """Generate description and array"""
74
+ is_np = isinstance(obj, np.ndarray)
75
+ obj = np.asarray(obj)
76
+ type = obj.dtype.type
77
+ if not is_np:
78
+ type = numpy2python(type) # type: ignore
79
+ shape = obj.shape
80
+ return Descriptor(type, shape), obj
@@ -0,0 +1,115 @@
1
+ """Ciphertext package"""
2
+ from __future__ import annotations
3
+
4
+ import typing
5
+ import operator
6
+ import itertools
7
+ from dataclasses import dataclass
8
+
9
+ import numpy as np
10
+
11
+ from polyhe.core import Descriptor
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from polyhe.core.context import Context
15
+
16
+
17
+ __all__ = (
18
+ "Ciphertext",
19
+ )
20
+
21
+
22
+ @dataclass(eq=False, order=False, slots=True, frozen=True)
23
+ class Ciphertext[T]:
24
+ """Abstract ciphertext"""
25
+ _context: Context
26
+ _descriptor: Descriptor
27
+ _chunks: tuple
28
+
29
+ def _new(self, /, *args, **kwds):
30
+ """Create new operable ciphertext"""
31
+ return self.__class__(
32
+ _context=self._context,
33
+ _descriptor=self._descriptor,
34
+ *args, **kwds
35
+ )
36
+
37
+ def _operable(self, other):
38
+ """Ensure ciphertext is operable"""
39
+ if not isinstance(other, Ciphertext):
40
+
41
+ # Try scalar to array
42
+ try:
43
+ is_np = isinstance(other, np.ndarray)
44
+ other = np.full(self._descriptor.shape, other, self._descriptor.type)
45
+ if not is_np:
46
+ other = other.tolist()
47
+ except Exception:
48
+ pass
49
+
50
+ # Try data to plaintext
51
+ try:
52
+ other = self._context._plaintext(other)
53
+ except Exception:
54
+ raise NotImplementedError(f"Incompatible type ({other.__class__})") # from None
55
+
56
+ if other._context.options != self._context.options:
57
+ raise ValueError(f"Different underlying contexts ({other._context} != {self._context})")
58
+
59
+ if other._descriptor != self._descriptor:
60
+ raise ValueError(f"Different underlying ciphertext ({other} != {self})")
61
+
62
+ return other
63
+
64
+ def _link(self, context):
65
+ """Link ciphertext to context"""
66
+
67
+ def _neg(self, a):
68
+ """Negate two ciphertext"""
69
+ return operator.neg(a)
70
+
71
+ def _add(self, a, b):
72
+ """Add two ciphertexts"""
73
+ return operator.add(a, b)
74
+
75
+ def _sub(self, a, b):
76
+ """Substract two ciphertexts"""
77
+ return operator.sub(a, b)
78
+
79
+ def _mul(self, a, b):
80
+ """Multiply two ciphertexts"""
81
+ return operator.mul(a, b)
82
+
83
+ def _op_unary(self, op):
84
+ """Execute a unary operation"""
85
+ self._link(self._context)
86
+ chunks = tuple(map(op, self._chunks))
87
+ return self._new(_chunks=chunks)
88
+
89
+ def _op_binary(self, op, other):
90
+ """Execute a binary operation"""
91
+ other = self._operable(other)
92
+ self._link(self._context)
93
+ other._link(self._context)
94
+ chunks = tuple(itertools.starmap(op, zip(self._chunks, other._chunks)))
95
+ return self._new(_chunks=chunks)
96
+
97
+ def __neg__(self):
98
+ """Negate a ciphertext"""
99
+ return self._op_unary(self._neg)
100
+
101
+ def __add__(self, other):
102
+ """Add two ciphertexts"""
103
+ return self._op_binary(self._add, other)
104
+
105
+ def __sub__(self, other):
106
+ """Substract two ciphertexts"""
107
+ return self._op_binary(self._sub, other)
108
+
109
+ def __mul__(self, other):
110
+ """Multiply two ciphertexts"""
111
+ return self._op_binary(self._mul, other)
112
+
113
+ def __repr__(self) -> str:
114
+ """Ciphertext representation"""
115
+ return f"<{self.__class__.__name__} format={self._descriptor.type.__name__} shape={self._descriptor.shape}>"
polyhe/core/context.py ADDED
@@ -0,0 +1,125 @@
1
+ """Context package"""
2
+
3
+ import copy
4
+ import math
5
+ import typing
6
+ import itertools
7
+ from abc import ABC, abstractmethod
8
+ from collections.abc import Iterable
9
+ from functools import cached_property
10
+
11
+ import numpy as np
12
+
13
+ from polyhe.core.ciphertext import Ciphertext
14
+ from polyhe.core import COEFF_MODULUS, Options, materialize
15
+
16
+
17
+ __all__ = (
18
+ "Context",
19
+ )
20
+
21
+
22
+ class Context(ABC):
23
+ """Abstract encryption context"""
24
+ _cls: type[Ciphertext]
25
+
26
+ def __init__(self, options: Options = Options()) -> None:
27
+ """Initialize context"""
28
+ self.options = options
29
+
30
+ @property
31
+ def _slots_exp(self) -> int:
32
+ """Slot exponent"""
33
+ return self.options.slots
34
+
35
+ @property
36
+ def _slots(self) -> int:
37
+ """Slot count"""
38
+ return 2 ** self._slots_exp
39
+
40
+ @property
41
+ def _scale_exp(self) -> int:
42
+ """Scale exponent"""
43
+ return self.options.scale
44
+
45
+ @property
46
+ def _security_level(self) -> int:
47
+ """Security level"""
48
+ return self.options.security
49
+
50
+ @property
51
+ def _poly_exp(self) -> int:
52
+ """Polynomial exponent"""
53
+ return self._slots_exp + 1
54
+
55
+ @property
56
+ def _poly(self) -> int:
57
+ """Polynomial size"""
58
+ return 2 ** self._poly_exp
59
+
60
+ @property
61
+ def _coeff_modulus(self) -> list[int]:
62
+ """Coefficient modulus"""
63
+ return COEFF_MODULUS[self._security_level][self._poly_exp]
64
+
65
+ def __getstate__(self) -> dict:
66
+ """Get serializable state"""
67
+ state: dict = dict(super().__getstate__()) # type: ignore
68
+ state.pop("_public", None)
69
+ return state
70
+
71
+ @cached_property
72
+ def public(self) -> typing.Self:
73
+ """Get public context"""
74
+ return copy.deepcopy(self)
75
+
76
+ def _new(self, /, *args, **kwds) -> Ciphertext:
77
+ """Create new operable ciphertext"""
78
+ return self._cls(_context=self.public, *args, **kwds)
79
+
80
+ def _partition(self, array: np.ndarray) -> Iterable[np.ndarray]:
81
+ """Transform array into flat batches"""
82
+ parts = (array.size + self._slots - 1) // self._slots
83
+ space = np.zeros(parts * self._slots, dtype=np.float64)
84
+ space[:array.size] = array.ravel()
85
+ return np.split(space, parts)
86
+
87
+ def _encode(self, chunk):
88
+ """Encode data to plaintext"""
89
+ return chunk
90
+
91
+ @abstractmethod
92
+ def _encrypt(self, chunk):
93
+ """Encrypt plaintext to ciphertext"""
94
+
95
+ @abstractmethod
96
+ def _decrypt(self, chunk):
97
+ """Decrypt cypertext to plaintext"""
98
+
99
+ def _decode(self, chunk):
100
+ """Decode plaintext to data"""
101
+ return chunk
102
+
103
+ def _plaintext[T](self, data: T) -> Ciphertext[T]:
104
+ """Encode data to plaintext"""
105
+ descriptor, obj = materialize(data)
106
+ chunks = tuple(map(self._encode, self._partition(obj)))
107
+ return Ciphertext(_context=self.public, _descriptor=descriptor, _chunks=chunks)
108
+
109
+ def encrypt[T](self, data: T) -> Ciphertext[T]:
110
+ """Encrypt data to ciphertext"""
111
+ descriptor, obj = materialize(data)
112
+ chunks = tuple(map(self._encrypt, map(self._encode, self._partition(obj))))
113
+ return self._new(_descriptor=descriptor, _chunks=chunks)
114
+
115
+ def decrypt[T](self, data: Ciphertext[T]) -> T:
116
+ """Decrypt cypertext to data"""
117
+ data._link(self)
118
+ descriptor = data._descriptor
119
+ result = itertools.chain.from_iterable(map(self._decode, map(self._decrypt, data._chunks)))
120
+ result = np.fromiter(iter=result, dtype=descriptor.type, count=math.prod(descriptor.shape)).reshape(descriptor.shape)
121
+ return result if descriptor.numpy else result.tolist() # type: ignore
122
+
123
+ def __repr__(self) -> str:
124
+ """Context representation"""
125
+ return f"{self.__class__.__name__}(options={self.options!r})"
polyhe/openfhe.py ADDED
@@ -0,0 +1,160 @@
1
+ """OpenFHE encryption"""
2
+
3
+ import sys
4
+ import typing
5
+ import copyreg
6
+ from dataclasses import dataclass
7
+ from functools import cached_property
8
+
9
+ from polyhe import core
10
+ from polyhe.core import context
11
+ from polyhe.core import ciphertext
12
+
13
+ # Make sure global package is not confused with current package
14
+ _pkg = sys.path.pop(0)
15
+ try:
16
+ import openfhe
17
+ finally:
18
+ sys.path.insert(0, _pkg)
19
+
20
+
21
+ __all__ = (
22
+ "Context",
23
+ )
24
+
25
+
26
+ SECURITY_LEVEL = {
27
+ # 0: openfhe.SecurityLevel.HEStd_NotSet,
28
+ 128: openfhe.SecurityLevel.HEStd_128_classic,
29
+ 192: openfhe.SecurityLevel.HEStd_192_classic,
30
+ 256: openfhe.SecurityLevel.HEStd_256_classic
31
+ }
32
+
33
+
34
+ @dataclass(repr=False, eq=False, order=False, slots=True, frozen=True)
35
+ class Ciphertext(ciphertext.Ciphertext):
36
+ """OpenFHE ciphertext"""
37
+ _context: "Context"
38
+
39
+ def _neg(self, a: openfhe.Ciphertext) -> openfhe.Ciphertext:
40
+ """Negate a ciphertext"""
41
+ return self._context._context.EvalNegate(a)
42
+
43
+ def _add(self, a: openfhe.Ciphertext, b: openfhe.Ciphertext) -> openfhe.Ciphertext:
44
+ """Add two ciphertexts"""
45
+ return self._context._context.EvalAdd(a, b)
46
+
47
+ def _sub(self, a: openfhe.Ciphertext, b: openfhe.Ciphertext) -> openfhe.Ciphertext:
48
+ """Substract two ciphertexts"""
49
+ return self._context._context.EvalSub(a, b)
50
+
51
+ def _mul(self, a: openfhe.Ciphertext, b: openfhe.Ciphertext) -> openfhe.Ciphertext:
52
+ """Multiply two ciphertexts"""
53
+ return self._context._context.EvalMult(a, b)
54
+
55
+
56
+ class Context(context.Context):
57
+ """OpenFHE context"""
58
+ _cls = Ciphertext
59
+
60
+ def __init__(self, options: core.Options = core.Options()) -> None:
61
+ """Initialize context"""
62
+ super().__init__(options)
63
+
64
+ # Context
65
+ level = SECURITY_LEVEL[self._security_level]
66
+ parameters = openfhe.CCParamsCKKSRNS()
67
+ parameters.SetRingDim(self._poly)
68
+ parameters.SetSecurityLevel(level)
69
+ parameters.SetScalingModSize(self._scale_exp)
70
+ self._context = openfhe.GenCryptoContext(parameters)
71
+ self._context.Enable(openfhe.PKESchemeFeature.PKE)
72
+ self._context.Enable(openfhe.PKESchemeFeature.LEVELEDSHE)
73
+
74
+ # Keys
75
+ keys = self._context.KeyGen()
76
+ self._public_key = keys.publicKey
77
+ self._private_key = keys.secretKey
78
+ self._context.EvalMultKeyGen(self._private_key)
79
+
80
+ @cached_property
81
+ def public(self) -> typing.Self:
82
+ """Get public context"""
83
+ context = super().public
84
+ try:
85
+ del context._private_key
86
+ except AttributeError:
87
+ pass
88
+ return context
89
+
90
+ def _encode(self, chunk):
91
+ """Ecnode data to plaintext"""
92
+ return self._context.MakeCKKSPackedPlaintext(chunk)
93
+
94
+ def _encrypt(self, chunk: openfhe.Plaintext) -> openfhe.Ciphertext:
95
+ """Encrypt plaintext to ciphertext"""
96
+ return self._context.Encrypt(self._public_key, chunk)
97
+
98
+ def _decrypt(self, chunk: openfhe.Ciphertext) -> openfhe.Plaintext:
99
+ """Decrypt cypertext to plaintext"""
100
+ return self._context.Decrypt(chunk, self._private_key)
101
+
102
+ def _decode(self, chunk: openfhe.Plaintext):
103
+ """Decode plaintext to data"""
104
+ return chunk.GetRealPackedValue()
105
+
106
+
107
+ # Serialization
108
+ def deserialize_context(str: bytes) -> openfhe.CryptoContext:
109
+ """OpenFHE context deserializer"""
110
+ return openfhe.DeserializeCryptoContextString(str, openfhe.BINARY)
111
+
112
+
113
+ def deserialize_ciphertext(str: bytes) -> openfhe.Ciphertext:
114
+ """OpenFHE cipher text deserializer"""
115
+ return openfhe.DeserializeCiphertextString(str, openfhe.BINARY)
116
+
117
+
118
+ def deserialize_publickey(str: bytes) -> openfhe.PublicKey:
119
+ """OpenFHE public key deserializer"""
120
+ return openfhe.DeserializePublicKeyString(str, openfhe.BINARY)
121
+
122
+
123
+ def deserialize_privatekey(str: bytes) -> openfhe.PrivateKey:
124
+ """OpenFHE private key deserializer"""
125
+ return openfhe.DeserializePrivateKeyString(str, openfhe.BINARY)
126
+
127
+
128
+ # Pickle support
129
+ def context_reducer(context: openfhe.CryptoContext):
130
+ """OpenFHE context pickle reducer"""
131
+ cls = deserialize_context
132
+ args = (openfhe.Serialize(context, openfhe.BINARY),)
133
+ return (cls, args)
134
+
135
+
136
+ def ciphertext_reducer(ciphertext: openfhe.Ciphertext):
137
+ """OpenFHE cipher text pickle reducer"""
138
+ cls = deserialize_ciphertext
139
+ args = (openfhe.Serialize(ciphertext, openfhe.BINARY),)
140
+ return (cls, args)
141
+
142
+
143
+ def public_key_reducer(public_key: openfhe.PublicKey):
144
+ """OpenFHE public key pickle reducer"""
145
+ cls = deserialize_publickey
146
+ args = (openfhe.Serialize(public_key, openfhe.BINARY),)
147
+ return (cls, args)
148
+
149
+
150
+ def private_key_reducer(private_key: openfhe.PrivateKey):
151
+ """OpenFHE private key pickle reducer"""
152
+ cls = deserialize_privatekey
153
+ args = (openfhe.Serialize(private_key, openfhe.BINARY),)
154
+ return (cls, args)
155
+
156
+
157
+ copyreg.pickle(openfhe.CryptoContext, context_reducer)
158
+ copyreg.pickle(openfhe.Ciphertext, ciphertext_reducer)
159
+ copyreg.pickle(openfhe.PublicKey, public_key_reducer)
160
+ copyreg.pickle(openfhe.PrivateKey, private_key_reducer)
polyhe/tenseal.py ADDED
@@ -0,0 +1,105 @@
1
+ """TenSEAL encryption"""
2
+
3
+ import sys
4
+ import typing
5
+ import copyreg
6
+ from dataclasses import dataclass
7
+ from functools import cached_property
8
+
9
+ from polyhe import core
10
+ from polyhe.core import context
11
+ from polyhe.core import ciphertext
12
+
13
+ # Make sure global package is not confused with current package
14
+ _pkg = sys.path.pop(0)
15
+ try:
16
+ import tenseal
17
+ from tenseal import sealapi
18
+ from tenseal.tensors import CKKSVector
19
+ from tenseal.enc_context import Context as SealContext
20
+ finally:
21
+ sys.path.insert(0, _pkg)
22
+
23
+
24
+ __all__ = (
25
+ "Context",
26
+ )
27
+
28
+
29
+ SECURITY_LEVEL = {
30
+ # 0: sealapi.SEC_LEVEL_TYPE.NONE,
31
+ 128: sealapi.SEC_LEVEL_TYPE.TC128,
32
+ 192: sealapi.SEC_LEVEL_TYPE.TC192,
33
+ 256: sealapi.SEC_LEVEL_TYPE.TC256
34
+ }
35
+
36
+
37
+ @dataclass(repr=False, eq=False, order=False, slots=True, frozen=True)
38
+ class Ciphertext(ciphertext.Ciphertext):
39
+ """TenSEAL ciphertext"""
40
+ _context: "Context"
41
+
42
+ def _link(self, context: "Context") -> None:
43
+ """Link ciphertext to context"""
44
+ for chunk in self._chunks:
45
+ chunk.link_context(context._context)
46
+
47
+
48
+ class Context(context.Context):
49
+ """TenSEAL context"""
50
+ _cls = Ciphertext
51
+
52
+ def __init__(self, options: core.Options = core.Options()) -> None:
53
+ """Initialize context"""
54
+ super().__init__(options)
55
+
56
+ # Context
57
+ self._context = tenseal.context(
58
+ scheme=tenseal.SCHEME_TYPE.CKKS,
59
+ poly_modulus_degree=self._poly,
60
+ coeff_mod_bit_sizes=self._coeff_modulus
61
+ )
62
+
63
+ # Keys
64
+ self._context.global_scale = 2 ** self._scale_exp
65
+ self._context.generate_galois_keys()
66
+ self._context.generate_relin_keys()
67
+
68
+ @cached_property
69
+ def public(self) -> typing.Self:
70
+ """Get public context"""
71
+ context = super().public
72
+ context._context.make_context_public()
73
+ return context
74
+
75
+ def _encode(self, chunk) -> tenseal.PlainTensor:
76
+ """Encrypt plaintext to ciphertext"""
77
+ return tenseal.plain_tensor(chunk)
78
+
79
+ def _encrypt(self, chunk: tenseal.PlainTensor) -> CKKSVector:
80
+ """Encrypt plaintext to ciphertext"""
81
+ return tenseal.ckks_vector(self.public._context, chunk)
82
+
83
+ def _decrypt(self, chunk: CKKSVector):
84
+ """Decrypt cypertext to list"""
85
+ return chunk.decrypt()
86
+
87
+
88
+ # Pickle support
89
+ def context_reducer(context: SealContext):
90
+ """TenSEAL context pickle reducer"""
91
+ # FIXME: Context deserialization is very slow
92
+ cls = context.load
93
+ args = (context.serialize(save_secret_key=True),)
94
+ return (cls, args)
95
+
96
+
97
+ def ckks_vector_reducer(vector: CKKSVector):
98
+ """TenSEAL CKKS vector pickle reducer"""
99
+ cls = vector.lazy_load
100
+ args = (vector.serialize(),)
101
+ return (cls, args)
102
+
103
+
104
+ copyreg.pickle(SealContext, context_reducer)
105
+ copyreg.pickle(CKKSVector, ckks_vector_reducer) # type: ignore (wrong inferred typing)
polyhe/test.py ADDED
@@ -0,0 +1,50 @@
1
+ """PolyHE test"""
2
+
3
+ from argparse import ArgumentParser, Namespace
4
+
5
+ import numpy as np
6
+
7
+ import polyhe
8
+
9
+
10
+ __all__ = ()
11
+
12
+
13
+ def main(config: Namespace) -> None:
14
+ ctx = polyhe.new(config.backend)
15
+
16
+ # Numbers
17
+ p1 = 1
18
+ p2 = -1
19
+ c1 = ctx.encrypt(p1)
20
+ c2 = ctx.encrypt(p2)
21
+ c3 = c1 * c2 + p1
22
+ p3 = ctx.decrypt(c3)
23
+ ref = 0
24
+ assert np.isclose(p3, ref).all(), f"numbers error; got {p3}, expect {ref}"
25
+
26
+ # Lists
27
+ p1 = [0.3, 0.4]
28
+ p2 = [0.4, 0.3]
29
+ c1 = ctx.encrypt(p1)
30
+ c2 = ctx.encrypt(p2)
31
+ c3 = -c1 + c2
32
+ p3 = ctx.decrypt(c3)
33
+ ref = [0.1, -0.1]
34
+ assert np.isclose(p3, ref).all(), f"lists error; got {p3}, expect {ref}"
35
+
36
+ # NumPy
37
+ p1 = np.full((2, 3, 4), 0.4, np.float64)
38
+ p2 = np.full((2, 3, 4), 0.3, np.float64)
39
+ c1 = ctx.encrypt(p1)
40
+ c2 = ctx.encrypt(p2)
41
+ c3 = c1 - c2
42
+ p3 = ctx.decrypt(c3)
43
+ ref = np.full((2, 3, 4), 0.1, np.float64)
44
+ assert np.isclose(p3, ref).all(), f"numpy error; got {p3}, expect {ref}"
45
+
46
+
47
+ if __name__ == "__main__":
48
+ parser = ArgumentParser(prog="polyhe-test", description="PolyHE test")
49
+ parser.add_argument("backend", choices=list(polyhe.Backend), help="Operational backend")
50
+ main(parser.parse_args())