Repository: OpenMined/TenSEAL (language: python)

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



Discussion / hints:

Fix patch:
diff --git a/tenseal/binding.cpp b/tenseal/binding.cpp
index 66e76680..d0a3dd09 100644
--- a/tenseal/binding.cpp
+++ b/tenseal/binding.cpp
@@ -107,6 +107,8 @@ PYBIND11_MODULE(_tenseal_cpp, m) {
              })
         .def("add", &BFVVector::add)
         .def("add_", &BFVVector::add_inplace)
+        .def("add_plain", py::overload_cast<const int64_t &>(
+                              &BFVVector::add_plain, py::const_))
         .def("add_plain",
              [](shared_ptr<BFVVector> obj, const vector<int64_t> &other) {
                  return obj->add_plain(other);
@@ -115,6 +117,8 @@ PYBIND11_MODULE(_tenseal_cpp, m) {
              [](shared_ptr<BFVVector> obj, const vector<int64_t> &other) {
                  return obj->add_plain_inplace(other);
              })
+        .def("add_plain_",
+             py::overload_cast<const int64_t &>(&BFVVector::add_plain_inplace))
         .def("sub", &BFVVector::sub)
         .def("sub_", &BFVVector::sub_inplace)
         .def("sub_plain",
@@ -137,11 +141,15 @@ PYBIND11_MODULE(_tenseal_cpp, m) {
              })
         // python arithmetic
         .def("__add__", &BFVVector::add)
+        .def("__add__", py::overload_cast<const int64_t &>(
+                            &BFVVector::add_plain, py::const_))
         .def("__add__",
              [](shared_ptr<BFVVector> obj, const vector<int64_t> &other) {
                  return obj->add_plain(other);
              })
         .def("__iadd__", &BFVVector::add_inplace)
+        .def("__iadd__",
+             py::overload_cast<const int64_t &>(&BFVVector::add_plain_inplace))
         .def("__iadd__",
              [](shared_ptr<BFVVector> obj, const vector<int64_t> &other) {
                  return obj->add_plain_inplace(other);
diff --git a/tenseal/cpp/tensors/bfvvector.cpp b/tenseal/cpp/tensors/bfvvector.cpp
index 2c79552e..7b4efc49 100644
--- a/tenseal/cpp/tensors/bfvvector.cpp
+++ b/tenseal/cpp/tensors/bfvvector.cpp
@@ -244,7 +244,8 @@ shared_ptr<BFVVector> BFVVector::sum_inplace(size_t /*axis=0*/) {
 
 shared_ptr<BFVVector> BFVVector::add_plain_inplace(
     const plain_t::dtype& to_add) {
-    throw std::logic_error("not implemented");
+    for (auto& ct : this->_ciphertexts) this->_add_plain_inplace(ct, to_add);
+    return shared_from_this();
 }
 
 shared_ptr<BFVVector> BFVVector::add_plain_inplace(
@@ -257,14 +258,19 @@ shared_ptr<BFVVector> BFVVector::add_plain_inplace(
 
     for (size_t idx = 0; idx < this->_ciphertexts.size(); ++idx) {
         Plaintext plaintext;
-        this->tenseal_context()->encode<BatchEncoder>(to_add[idx].data_ref(),
-                                                      plaintext);
-        this->tenseal_context()->evaluator->add_plain_inplace(
-            this->_ciphertexts[idx], plaintext);
+        this->_add_plain_inplace(this->_ciphertexts[idx],
+                                 to_add[idx].data_ref());
     }
     return shared_from_this();
 }
 
+template <typename T>
+void BFVVector::_add_plain_inplace(Ciphertext& ct, const T& to_add) {
+    Plaintext plaintext;
+    this->tenseal_context()->encode<BatchEncoder>(to_add, plaintext);
+    this->tenseal_context()->evaluator->add_plain_inplace(ct, plaintext);
+}
+
 shared_ptr<BFVVector> BFVVector::sub_plain_inplace(
     const plain_t::dtype& to_sub) {
     throw std::logic_error("not implemented");
diff --git a/tenseal/cpp/tensors/bfvvector.h b/tenseal/cpp/tensors/bfvvector.h
index b7cafa2f..826507a3 100644
--- a/tenseal/cpp/tensors/bfvvector.h
+++ b/tenseal/cpp/tensors/bfvvector.h
@@ -122,6 +122,12 @@ class BFVVector
     double scale() const override { throw logic_error("not implemented"); }
 
    private:
+    /*
+    Private evaluation functions to process both scalar and vector arguments.
+    */
+    template <typename T>
+    void _add_plain_inplace(Ciphertext& ct, const T& to_add);
+
     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 05895b0b..2fc74921 100644
--- a/tests/cpp/tensors/bfvvector_test.cpp
+++ b/tests/cpp/tensors/bfvvector_test.cpp
@@ -102,6 +102,45 @@ TEST_P(BFVVectorTest, TestBFVMul) {
     EXPECT_THAT(decr.data(), ElementsAreArray({16, 32, 48}));
 }
 
+TEST_P(BFVVectorTest, TestBFVAddPlain) {
+    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 add = l->add_plain(2);
+    ASSERT_THAT(add->ciphertext_size(), ElementsAreArray({2}));
+
+    auto decr = add->decrypt();
+    EXPECT_THAT(decr.data(), ElementsAreArray({3, 4, 5}));
+
+    // element-wise operation
+    add = l->add_plain(r);
+    ASSERT_THAT(add->ciphertext_size(), ElementsAreArray({2}));
+
+    decr = add->decrypt();
+    EXPECT_THAT(decr.data(), ElementsAreArray({3, 5, 7}));
+
+    l->add_plain_inplace(r);
+    l->add_plain_inplace(r);
+    l->add_plain_inplace(r);
+    l->add_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({9, 14, 19}));
+}
+
 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 83dbfbff..418a67c9 100644
--- a/tests/python/tenseal/tensors/test_bfv_vector.py
+++ b/tests/python/tenseal/tensors/test_bfv_vector.py
@@ -82,6 +82,9 @@ def test_add_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], 0),
+        ([1, 2, 3, 4], -2),
         ([2 * i for i in range(10000)], [3 * i for i in range(10000)]),
         ([2 * i for i in range(100000)], [3 * i for i in range(100000)]),
     ],
@@ -92,7 +95,10 @@ def test_add_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()
@@ -112,6 +118,40 @@ def test_add_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], 0),
+        ([1, 2, 3, 4], -2),
+    ],
+)
+def test_radd_plain(context, vec1, vec2):
+    first_vec = ts.bfv_vector(context, vec1)
+    result = vec2 + first_vec
+    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, "Addition of vectors is incorrect."
+    assert first_vec.decrypt() == vec1, "Something went wrong in memory."
+
+
+@pytest.mark.parametrize(
+    "vec1, vec2",
+    [
+        ([0], [0]),
+        ([1], [0]),
+        ([-1], [0]),
+        ([-1], [-1]),
+        ([1], [1]),
+        ([-1], [1]),
+        ([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], 0),
+        ([1, 2, 3, 4], -2),
         ([2 * i for i in range(10000)], [3 * i for i in range(10000)]),
         ([2 * i for i in range(100000)], [3 * i for i in range(100000)]),
     ],
@@ -119,10 +159,12 @@ def test_add_plain(context, vec1, vec2):
 def test_add_plain_inplace(context, vec1, vec2):
     first_vec = ts.bfv_vector(context, vec1)
     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()