Repository: OpenMined/TenSEAL (language: python)

Issue:
BFVVector: implement mul_plain_inplace
## Description
Implement the mul_plain_inplace operation for BFVVector [here](https://github.com/OpenMined/TenSEAL/blob/master/tenseal/cpp/tensors/bfvvector.cpp#L293).

You can see a similar implementation in the CKKSVector class [here](https://github.com/OpenMined/TenSEAL/blob/master/tenseal/cpp/tensors/ckksvector.cpp#L321).

In order to be accepted, the PR must include:
 - [ ] The implementation of the operation.
 - [ ] C++ tests.
 - [ ] Python tests.



Discussion / hints:
Hello! If this is still open, can I have a go at it?
@philomath213 are you working on it? or can we assign @alexandrumeterez ?
> @philomath213 are you working on it? or can we assign @alexandrumeterez ?

No, I'm not.
@alexandrumeterez you can refer to #257 for the required changes (implementation, binding, tests).

Fix patch:
diff --git a/tenseal/binding.cpp b/tenseal/binding.cpp
index d9315b06..5643e7d6 100644
--- a/tenseal/binding.cpp
+++ b/tenseal/binding.cpp
@@ -208,6 +208,8 @@ void bind_bfv_vector(py::module &m) {
              })
         .def("mul", &BFVVector::mul)
         .def("mul_", &BFVVector::mul_inplace)
+        .def("mul_plain", py::overload_cast<const int64_t &>(
+                              &BFVVector::mul_plain, py::const_))
         .def("mul_plain",
              [](shared_ptr<BFVVector> obj, const vector<int64_t> &other) {
                  return obj->mul_plain(other);
@@ -216,6 +218,8 @@ void bind_bfv_vector(py::module &m) {
              [](shared_ptr<BFVVector> obj, const vector<int64_t> &other) {
                  return obj->mul_plain_inplace(other);
              })
+        .def("mul_plain_",
+             py::overload_cast<const int64_t &>(&BFVVector::mul_plain_inplace))
         // python arithmetic
         .def("__add__", &BFVVector::add)
         .def("__add__", py::overload_cast<const int64_t &>(
@@ -242,11 +246,15 @@ void bind_bfv_vector(py::module &m) {
                  return obj->sub_plain_inplace(other);
              })
         .def("__mul__", &BFVVector::mul)
+        .def("__mul__", py::overload_cast<const int64_t &>(
+                            &BFVVector::mul_plain, py::const_))
         .def("__mul__",
              [](shared_ptr<BFVVector> obj, const vector<int64_t> &other) {
                  return obj->mul_plain(other);
              })
         .def("__imul__", &BFVVector::mul_inplace)
+        .def("__imul__",
+             py::overload_cast<const int64_t &>(&BFVVector::mul_plain_inplace))
         .def("__imul__",
              [](shared_ptr<BFVVector> obj, const vector<int64_t> &other) {
                  return obj->mul_plain_inplace(other);
diff --git a/tenseal/cpp/tensors/bfvvector.cpp b/tenseal/cpp/tensors/bfvvector.cpp
index 7b4efc49..bffbcff5 100644
--- a/tenseal/cpp/tensors/bfvvector.cpp
+++ b/tenseal/cpp/tensors/bfvvector.cpp
@@ -296,9 +296,18 @@ shared_ptr<BFVVector> BFVVector::sub_plain_inplace(
     return shared_from_this();
 }
 
+template <typename T>
+void BFVVector::_mul_plain_inplace(Ciphertext& ct, const T& to_mul) {
+    Plaintext plaintext;
+    this->tenseal_context()->encode<BatchEncoder>(to_mul, plaintext);
+    this->tenseal_context()->evaluator->multiply_plain_inplace(ct, plaintext);
+}
+
 shared_ptr<BFVVector> BFVVector::mul_plain_inplace(
-    const plain_t::dtype& to_sub) {
-    throw std::logic_error("not implemented");
+    const plain_t::dtype& to_mul) {
+    for (auto& ct : this->_ciphertexts) this->_mul_plain_inplace(ct, to_mul);
+
+    return shared_from_this();
 }
 
 shared_ptr<BFVVector> BFVVector::mul_plain_inplace(
@@ -312,12 +321,9 @@ shared_ptr<BFVVector> BFVVector::mul_plain_inplace(
 
     for (size_t idx = 0; idx < this->_ciphertexts.size(); ++idx) {
         Plaintext plaintext;
-        this->tenseal_context()->encode<BatchEncoder>(to_mul[idx].data_ref(),
-                                                      plaintext);
-
         try {
-            this->tenseal_context()->evaluator->multiply_plain_inplace(
-                this->_ciphertexts[idx], plaintext);
+            this->_mul_plain_inplace(this->_ciphertexts[idx],
+                                     to_mul[idx].data_ref());
         } catch (const std::logic_error& e) {
             if (strcmp(e.what(), "result ciphertext is transparent") == 0) {
                 // replace by encryption of zero
diff --git a/tenseal/cpp/tensors/bfvvector.h b/tenseal/cpp/tensors/bfvvector.h
index 826507a3..bb305c58 100644
--- a/tenseal/cpp/tensors/bfvvector.h
+++ b/tenseal/cpp/tensors/bfvvector.h
@@ -128,6 +128,9 @@ class BFVVector
     template <typename T>
     void _add_plain_inplace(Ciphertext& ct, const T& to_add);
 
+    template <typename T>
+    void _mul_plain_inplace(Ciphertext& ct, const T& to_mul);
+
     BFVVector(const shared_ptr<TenSEALContext>& ctx, const plain_t& vec);
     BFVVector(const shared_ptr<const BFVVector>&);
     BFVVector(const shared_ptr<TenSEALContext>& ctx, const string& vec);


Test patch:
diff --git a/tests/cpp/tensors/bfvvector_test.cpp b/tests/cpp/tensors/bfvvector_test.cpp
index 2fc74921..c9e85cea 100644
--- a/tests/cpp/tensors/bfvvector_test.cpp
+++ b/tests/cpp/tensors/bfvvector_test.cpp
@@ -141,6 +141,47 @@ TEST_P(BFVVectorTest, TestBFVAddPlain) {
     EXPECT_THAT(decr.data(), ElementsAreArray({9, 14, 19}));
 }
 
+TEST_P(BFVVectorTest, TestBFVMulPlain) {
+    auto should_serialize_first = get<0>(GetParam());
+    auto enc_type = get<1>(GetParam());
+
+    auto ctx =
+        TenSEALContext::Create(scheme_type::bfv, 8192, 1032193, {}, enc_type);
+    ASSERT_TRUE(ctx != nullptr);
+
+    auto l = BFVVector::Create(ctx, vector<int64_t>({1, 2, 3}));
+    auto r = vector<int64_t>({2, 3, 4});
+
+    // scalar operation
+    auto mul = l->mul_plain(2);
+    ASSERT_THAT(mul->ciphertext_size(), ElementsAreArray({2}));
+
+    auto decr = mul->decrypt();
+    EXPECT_THAT(decr.data(), ElementsAreArray({2, 4, 6}));
+
+    // element-wise operation
+    mul = l->mul_plain(r);
+    ASSERT_THAT(mul->ciphertext_size(), ElementsAreArray({2}));
+
+    decr = mul->decrypt();
+    EXPECT_THAT(decr.data(), ElementsAreArray({2, 6, 12}));
+
+    r = vector<int64_t>({2, 2, 2});
+
+    l->mul_plain_inplace(r);
+    l->mul_plain_inplace(r);
+    l->mul_plain_inplace(r);
+    l->mul_plain_inplace(r);
+
+    if (should_serialize_first) {
+        l = duplicate(l);
+    }
+
+    ASSERT_THAT(l->ciphertext_size(), ElementsAreArray({2}));
+    decr = l->decrypt();
+    EXPECT_THAT(decr.data(), ElementsAreArray({16, 32, 48}));
+}
+
 TEST_P(BFVVectorTest, TestEmptyPlaintext) {
     auto should_serialize_first = get<0>(GetParam());
     auto enc_type = get<1>(GetParam());
diff --git a/tests/python/tenseal/tensors/test_bfv_vector.py b/tests/python/tenseal/tensors/test_bfv_vector.py
index 418a67c9..e0021a21 100644
--- a/tests/python/tenseal/tensors/test_bfv_vector.py
+++ b/tests/python/tenseal/tensors/test_bfv_vector.py
@@ -359,6 +359,9 @@ def test_mul_inplace(context, vec1, vec2):
         ([1, 2, 3, 4], [4, 3, 2, 1]),
         ([-1, -2], [-73, -10]),
         ([1, 2], [-73, -10]),
+        ([1, 2, 3, 4], 2),
+        ([1, 2, 3, 4], -2),
+        ([2 for i in range(10000)], [3 for i in range(10000)]),
     ],
 )
 def test_mul_plain(context, vec1, vec2):
@@ -367,8 +370,10 @@ def test_mul_plain(context, vec1, vec2):
     second_vec = vec2
     result = first_vec * second_vec
 
-    expected = [v1 * v2 for v1, v2 in zip(vec1, vec2)]
-
+    if isinstance(vec2, list):
+        expected = [v1 * v2 for v1, v2 in zip(vec1, vec2)]
+    elif isinstance(vec2, (float, int)):
+        expected = [v1 * vec2 for v1 in vec1]
     # Decryption
     decrypted_result = result.decrypt()
     assert decrypted_result == expected, "Multiplication of vectors is incorrect."
@@ -387,6 +392,8 @@ def test_mul_plain(context, vec1, vec2):
         ([1, 2, 3, 4], [4, 3, 2, 1]),
         ([-1, -2], [-73, -10]),
         ([1, 2], [-73, -10]),
+        ([1, 2, 3, 4], 2),
+        ([1, 2, 3, 4], -2),
     ],
 )
 def test_mul_plain_inplace(context, vec1, vec2):
@@ -395,7 +402,10 @@ def test_mul_plain_inplace(context, vec1, vec2):
     second_vec = vec2
     first_vec *= second_vec
 
-    expected = [v1 * v2 for v1, v2 in zip(vec1, vec2)]
+    if isinstance(vec2, list):
+        expected = [v1 * v2 for v1, v2 in zip(vec1, vec2)]
+    elif isinstance(vec2, (float, int)):
+        expected = [v1 * vec2 for v1 in vec1]
 
     # Decryption
     decrypted_result = first_vec.decrypt()