`HE.align_mod_n_scale(this, other)` not automatically called: `encrypted1 and encrypted2 parameter mismatch`
Description
In Demo_3_Float_CKKS.py there is written that:
To ease the life of the user, Pyfhel provides
HE.align_mod_n_scale(this, other), which automatically does the rescaling and mod switching. All the 2-input overloaded operators (+, -, *, /) of PyCtxt automatically call this function. The respective HE.add, HE.sub, HE.multiply don't.
However, it looks like in some circumstances this does not happen.
Code To Reproduce Error Consider this code:
from Pyfhel import Pyfhel
HE = Pyfhel()
HE.contextGen(scheme='ckks', n=16384, scale=2**30, qi=[60]+[30]*7+[60])
HE.keyGen()
HE.relinKeyGen()
ctxt1 = HE.encrypt(42.0)
print(ctxt1)
print(ctxt1.scale)
print(ctxt1.mod_level)
ptxt1 = HE.encode(1.0)
print(ptxt1)
print(ptxt1.scale)
print(ptxt1.mod_level)
ctxt2 = ctxt1 * ptxt1
HE.rescale_to_next(ctxt2)
print(ctxt2)
print(ctxt2.scale)
print(ctxt2.mod_level)
print(f"This works -> {ctxt2 + HE.encrypt(1.0)}")
print(f"This DOES NOT work -> {ctxt2 * HE.encrypt(1.0)}")
On Pyfhel==3.1.4 this results in an error:
<Pyfhel Ciphertext at 0x7f72b35c4480, scheme=ckks, size=2/2, scale_bits=30, mod_level=0>
1073741824.0
0
<Pyfhel Plaintext at 0x7f72b35c4f00, scheme=ckks, poly=?, is_ntt=Y, mod_level=0>
1073741824.0
0
<Pyfhel Ciphertext at 0x7f72b0dc5ac0, scheme=ckks, size=2/2, scale_bits=30, mod_level=1>
1073840136.0006409
1
This works -> <Pyfhel Ciphertext at 0x7f72b0db92c0, scheme=ckks, size=2/2, scale_bits=30, mod_level=1>
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Input In [198], in <cell line: 18>()
15 print(ctxt2.mod_level)
17 print(f"This works -> {ctxt2 + HE.encrypt(1.0)}")
---> 18 print(f"This DOES NOT work -> {ctxt2 * HE.encrypt(1.0)}")
File Pyfhel/PyCtxt.pyx:437, in Pyfhel.PyCtxt.PyCtxt.__mul__()
File Pyfhel/Pyfhel.pyx:1016, in Pyfhel.Pyfhel.Pyfhel.multiply()
ValueError: encrypted1 and encrypted2 parameter mismatch
Morever, this happens also if the encryption in the problematic operation is replaced by an encoding:
print(f"This works -> {ctxt2 + HE.encode(1.0)}")
print(f"This DOES NOT work -> {ctxt2 * HE.encode(1.0)}")
In this case, the error becomes:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Input In [199], in <cell line: 18>()
15 print(ctxt2.mod_level)
17 print(f"This works -> {ctxt2 + HE.encode(1.0)}")
---> 18 print(f"This DOES NOT work -> {ctxt2 * HE.encode(1.0)}")
File Pyfhel/PyCtxt.pyx:439, in Pyfhel.PyCtxt.PyCtxt.__mul__()
File Pyfhel/Pyfhel.pyx:1043, in Pyfhel.Pyfhel.Pyfhel.multiply_plain()
ValueError: encrypted_ntt and plain_ntt parameter mismatch
Expected behavior
The automatic ciphertext maintenance should work also for multiplications, not only for additions; indeed, using manually the HE.align_mod_n_scale(this, other) before the multiplication results in a correct processing, without any problem.
Setup:
- OS: Ubuntu 20.04 LTS
- Python: [e.g. 3.8.10]
- C compiler version: GCC 9.4.0
- Pyfhel Version: 3.1.4
Thanks again for the great work! Sorry for all the issues, but Pyfhel is a valuable tool in my research and I hope to help you improve it. :)
Ok, it looks like simply the rows:
other_ = self.encode_operand(other)
self_, other_ = self._pyfhel.align_mod_n_scale(self, other_,
copy_other=(other_ is other))
were missing in Pyfhel/PyCtxt.pyx.__mul__. Adding them and fixing the variables names seems to do the trick.
Very nice inspection! You don't need to align scales to multiply (the final scale will be just the multiplication of both scales), but I forgot you need to align mod_levels. I'm adding an extra parameter in align_mod_n_scale to do only that, and then adding it to PyCtxt.__mul__.
Hurray!
ctxt1=<Pyfhel Ciphertext at 0x7fb9a9e8f840, scheme=ckks, size=2/2, scale_bits=30, mod_level=0>
ptxt1=<Pyfhel Plaintext at 0x7fb9a9e8f440, scheme=ckks, poly=?, is_ntt=Y, mod_level=0>
ctxt2=<Pyfhel Ciphertext at 0x7fb9a9e8f700, scheme=ckks, size=2/2, scale_bits=60, mod_level=1>
ctxt3=<Pyfhel Ciphertext at 0x7fb9a9e8fa40, scheme=ckks, size=2/2, scale_bits=30, mod_level=0>
This works -> <Pyfhel Ciphertext at 0x7fb9a9e8fd80, scheme=ckks, size=2/2, scale_bits=30, mod_level=1>
This DOES work -> <Pyfhel Ciphertext at 0x7fb9a9e8f240, scheme=ckks, size=3/3, scale_bits=90, mod_level=2>
I confirm it works! Thanks!
Well, actually it looks like there is still something. :)
Consider this script:
from Pyfhel import Pyfhel
HE = Pyfhel()
HE.contextGen(scheme='ckks', n=16384, scale=2**30, qi=[60]+[30]*7+[60])
HE.keyGen()
HE.relinKeyGen()
ctxt1 = HE.encrypt(42.0) * HE.encrypt(42.0)
print(ctxt1)
print(ctxt1.scale)
print(ctxt1.mod_level)
ptxt1 = HE.encode(42.0)
print(ptxt1)
print(ptxt1.scale)
print(ptxt1.mod_level)
fixed_ctxt1, fixed_ptxt1 = HE.align_mod_n_scale(ctxt1, ptxt1, only_mod=True)
print(f"This works -> {fixed_ctxt1 * fixed_ptxt1}")
print(f"This DOES NOT work -> {ctxt1 * ptxt1}")
Note that in this case I did not any prior rescaling to ctxt1.
However, if I manually do the align_mod_n_scale, it works. But it doesn't automagically work in the multiplication.
Changing PyCtxt.pyx.__mul__ to keep self does the trick:
self, other_ = self._pyfhel.align_mod_n_scale(self, other_, copy_other=(other_ is other), only_mod=True)
But I don't know why.
I added a regression test to keep track of this issue in test_ckks.py:
https://github.com/ibarrond/Pyfhel/blob/f2c476d388f652e53b7c618a0590a80be456f939/Pyfhel/test/test_ckks.py#L31
As per v3.3.0, it has been fixed and should now work correctly.