Program Listing for File cryptocontext.h

Return to documentation for file (pke/include/cryptocontext.h)

//==================================================================================
// BSD 2-Clause License
//
// Copyright (c) 2014-2025, NJIT, Duality Technologies Inc. and other contributors
//
// All rights reserved.
//
// Author TPOC: contact@openfhe.org
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//==================================================================================

/*
  Control for encryption operations
 */

#ifndef __CRYPTOCONTEXT_H__
#define __CRYPTOCONTEXT_H__

#include "binfhecontext.h"
#include "ciphertext.h"
#include "cryptocontextfactory.h"
#include "cryptocontext-fwd.h"
#include "encoding/plaintextfactory.h"
#include "key/evalkey.h"
#include "key/keypair.h"
#include "scheme/scheme-swch-params.h"
#include "schemebase/base-pke.h"
#include "schemebase/base-scheme.h"
#include "schemerns/rns-cryptoparameters.h"
#include "utils/caller_info.h"
#include "utils/type_name.h"

#include <algorithm>
#include <complex>
#include <functional>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>

#ifdef DEBUG_KEY
    #include <iostream>
#endif

namespace lbcrypto {

template <typename Element>
class CryptoContextImpl : public Serializable {
    using IntType  = typename Element::Integer;
    using ParmType = typename Element::Params;

    inline void VerifyCKKSScheme(const std::string& functionName) const {
        if (!isCKKS(m_schemeId)) {
            std::string errMsg = std::string(functionName) +
                                 "() is available for the CKKS scheme only."
                                 " The current scheme is " +
                                 convertToString(m_schemeId);
            OPENFHE_THROW(errMsg);
        }
    }

    inline void VerifyCKKSRealDataType(const std::string& functionName) const {
        if (GetCKKSDataType() != REAL) {
            std::string errMsg =
                "Function " + std::string(functionName) + " is available for the real CKKS data types only.";
            OPENFHE_THROW(errMsg);
        }
    }

    void SetKSTechniqueInScheme();

    const CryptoContext<Element> GetContextForPointer(const CryptoContextImpl<Element>* cc) const {
        const auto& contexts = CryptoContextFactory<Element>::GetAllContexts();
        for (const auto& ctx : contexts) {
            if (cc == ctx.get())
                return ctx;
        }
        OPENFHE_THROW("Cannot find context for the given pointer to CryptoContextImpl");
    }

    Plaintext MakePlaintext(const PlaintextEncodings encoding, const std::vector<int64_t>& value, size_t depth,
                            uint32_t level) const {
        const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersRNS>(GetCryptoParameters());

        if (level > 0) {
            size_t numModuli = cryptoParams->GetElementParams()->GetParams().size();
            if (!isBFVRNS(m_schemeId)) {
                // we throw an exception if level >= numModuli. However, we use multiplicativeDepth in the error message,
                // so the user can understand the error more easily.
                if (level >= numModuli) {
                    uint32_t multiplicativeDepth =
                        (cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT) ? (numModuli - 2) : (numModuli - 1);
                    std::string errorMsg{"The level value should be less than or equal to "};
                    errorMsg +=
                        ((cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT) ? "(multiplicativeDepth + 1)." :
                                                                                    "multiplicativeDepth.");
                    errorMsg += " Currently: level is [" + std::to_string(level) + "] and multiplicativeDepth is [" +
                                std::to_string(multiplicativeDepth) + "]";
                    OPENFHE_THROW(errorMsg);
                }
            }
            else {
                if ((cryptoParams->GetMultiplicationTechnique() == BEHZ) ||
                    (cryptoParams->GetMultiplicationTechnique() == HPS)) {
                    OPENFHE_THROW(
                        "BFV: Encoding at level > 0 is not currently supported for BEHZ or HPS. Use one of the HPSPOVERQ* methods instead.");
                }

                if ((cryptoParams->GetEncryptionTechnique() == EXTENDED)) {
                    OPENFHE_THROW(
                        "BFV: Encoding at level > 0 is not currently supported for the EXTENDED encryption method. Use the STANDARD encryption method instead.");
                }
                if (level >= numModuli) {
                    std::string errorMsg =
                        "The level value should be less the current number of RNS limbs in the cryptocontext.";
                    errorMsg += " Currently: level is [" + std::to_string(level) + "] and number of RNS limbs is [" +
                                std::to_string(numModuli) + "]";
                    OPENFHE_THROW(errorMsg);
                }
            }
        }

        // uses a parameter set with a reduced number of RNS limbs corresponding to the level
        std::shared_ptr<ILDCRTParams<DCRTPoly::Integer>> elemParamsPtr;
        if (level != 0) {
            ILDCRTParams<DCRTPoly::Integer> elemParams = *(cryptoParams->GetElementParams());
            for (uint32_t i = 0; i < level; i++) {
                elemParams.PopLastParam();
            }
            elemParamsPtr = std::make_shared<ILDCRTParams<DCRTPoly::Integer>>(elemParams);
        }
        else {
            elemParamsPtr = cryptoParams->GetElementParams();
        }

        NativeInteger scf{1};
        bool setNoiseScaleDeg = false;
        auto scaleTech        = cryptoParams->GetScalingTechnique();
        if (isBGVRNS(m_schemeId) && (scaleTech == FLEXIBLEAUTO || scaleTech == FLEXIBLEAUTOEXT)) {
            if (scaleTech == FLEXIBLEAUTOEXT && level == 0) {
                scf              = cryptoParams->GetScalingFactorIntBig(level);
                depth            = 1;
                setNoiseScaleDeg = true;
            }
            else
                scf = cryptoParams->GetScalingFactorInt(level);
        }

        Plaintext p = PlaintextFactory::MakePlaintext(value, encoding, elemParamsPtr, this->GetEncodingParams(),
                                                      getSchemeId(), depth, level, scf);
        if (setNoiseScaleDeg)
            p->SetNoiseScaleDeg(2);

        return p;
    }

    template <typename Value1>
    static Plaintext MakePlaintext(PlaintextEncodings encoding, CryptoContext<Element> cc, const Value1& value) {
        return PlaintextFactory::MakePlaintext(value, encoding, cc->GetElementParams(), cc->GetEncodingParams());
    }

    template <typename Value1, typename Value2>
    static Plaintext MakePlaintext(PlaintextEncodings encoding, CryptoContext<Element> cc, const Value1& value,
                                   const Value2& value2) {
        return PlaintextFactory::MakePlaintext(encoding, cc->GetElementParams(), cc->GetEncodingParams(), value,
                                               value2);
    }

    static std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> GetPartialEvalAutomorphismKeyMapPtr(
        const std::string& keyTag, const std::vector<uint32_t>& indexList);

    // cached evalmult keys, by secret key UID
    static std::map<std::string, std::vector<EvalKey<Element>>> s_evalMultKeyMap;
    // cached evalautomorphism keys, by secret key UID
    static std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>> s_evalAutomorphismKeyMap;

protected:
    // crypto parameters
    std::shared_ptr<CryptoParametersBase<Element>> m_params{nullptr};
    // algorithm used; accesses all crypto methods
    std::shared_ptr<SchemeBase<Element>> m_scheme{nullptr};

    SCHEME m_schemeId{SCHEME::INVALID_SCHEME};

    uint32_t m_keyGenLevel{0};

    void TypeCheck(ConstCiphertext<Element>& a, ConstCiphertext<Element>& b, CALLER_INFO_ARGS_HDR) const {
        if (a == nullptr || b == nullptr) {
            std::string errorMsg(std::string("Null Ciphertext") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (a->GetCryptoContext().get() != this) {
            std::string errorMsg(std::string("Ciphertext was not created in this CryptoContext") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (a->GetCryptoContext() != b->GetCryptoContext()) {
            std::string errorMsg(std::string("Ciphertexts were not created in the same CryptoContext") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (a->GetKeyTag() != b->GetKeyTag()) {
            std::string errorMsg(std::string("Ciphertexts were not encrypted with same keys") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (a->GetEncodingType() != b->GetEncodingType()) {
            std::stringstream ss;
            ss << "Ciphertext encoding types " << a->GetEncodingType();
            ss << " and " << b->GetEncodingType();
            ss << " do not match";
            ss << CALLER_INFO;
            OPENFHE_THROW(ss.str());
        }
    }

    void TypeCheck(ConstCiphertext<Element>& a, ConstPlaintext& b, CALLER_INFO_ARGS_HDR) const {
        if (a == nullptr) {
            std::string errorMsg(std::string("Null Ciphertext") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (b == nullptr) {
            std::string errorMsg(std::string("Null Plaintext") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (a->GetCryptoContext().get() != this) {
            std::string errorMsg(std::string("Ciphertext was not created in this CryptoContext") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (a->GetEncodingType() != b->GetEncodingType()) {
            std::stringstream ss;
            ss << "Ciphertext encoding type " << a->GetEncodingType();
            ss << " and Plaintext encoding type " << b->GetEncodingType();
            ss << " do not match";
            ss << CALLER_INFO;
            OPENFHE_THROW(ss.str());
        }
    }

    bool Mismatched(const CryptoContext<Element> a) const {
        return a.get() != this;
    }

    template <typename T>
    void ValidateKey(const T& key, CALLER_INFO_ARGS_HDR) const {
        if (key == nullptr) {
            std::string errorMsg(std::string("Key is nullptr") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (Mismatched(key->GetCryptoContext())) {
            std::string errorMsg(std::string("Key was not generated with the same crypto context") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
    }

    void ValidateCiphertext(ConstCiphertext<Element>& ciphertext, CALLER_INFO_ARGS_HDR) const {
        if (ciphertext == nullptr) {
            std::string errorMsg(std::string("Ciphertext is nullptr") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (Mismatched(ciphertext->GetCryptoContext())) {
            std::string errorMsg(std::string("Ciphertext was not generated with the same crypto context") +
                                 CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
    }

    void ValidateSeriesPowers(std::shared_ptr<seriesPowers<Element>> powers, CALLER_INFO_ARGS_HDR) const {
        if (powers == nullptr) {
            std::string errorMsg(std::string("The object for powers is nullptr") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (powers->powersRe.size() == 0) {
            std::string errorMsg(std::string("The powersRe member is empty") + CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
        if (Mismatched(powers->powersRe[0]->GetCryptoContext())) {
            std::string errorMsg(std::string("Power ciphertext was not generated with the same crypto context") +
                                 CALLER_INFO);
            OPENFHE_THROW(errorMsg);
        }
    }

    virtual Plaintext MakeCKKSPackedPlaintextInternal(const std::vector<std::complex<double>>& value,
                                                      size_t noiseScaleDeg, uint32_t level,
                                                      const std::shared_ptr<ParmType> params, uint32_t slots) const {
        VerifyCKKSScheme(__func__);
        const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersRNS>(GetCryptoParameters());
        if (level > 0) {
            // validation of level: We need to compare it to multiplicativeDepth, but multiplicativeDepth is not
            // readily available. so, what we get is numModuli and use it for calculations
            size_t numModuli = cryptoParams->GetElementParams()->GetParams().size();
            uint32_t multiplicativeDepth =
                (cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT) ? (numModuli - 2) : (numModuli - 1);
            // we throw an exception if level >= numModuli. however, we use multiplicativeDepth in the error message,
            // so the user can understand the error more easily.
            if (level >= numModuli) {
                std::string errorMsg;
                if (cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT)
                    errorMsg = "The level value should be less than or equal to (multiplicativeDepth + 1).";
                else
                    errorMsg = "The level value should be less than or equal to multiplicativeDepth.";

                errorMsg += " Currently: level is [" + std::to_string(level) + "] and multiplicativeDepth is [" +
                            std::to_string(multiplicativeDepth) + "]";
                OPENFHE_THROW(errorMsg);
            }
        }

        double scFact = 0;
        if (cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT && level == 0) {
            scFact = cryptoParams->GetScalingFactorRealBig(level);
            // In FLEXIBLEAUTOEXT mode at level 0, we don't use the noiseScaleDeg in our encoding function,
            // so we set it to 1 to make sure it has no effect on the encoding.
            noiseScaleDeg = 1;
        }
        else {
            scFact = cryptoParams->GetScalingFactorReal(level);
        }

        Plaintext p;
        if (params == nullptr) {
            std::shared_ptr<ILDCRTParams<DCRTPoly::Integer>> elemParamsPtr;
            if (level != 0) {
                ILDCRTParams<DCRTPoly::Integer> elemParams = *(cryptoParams->GetElementParams());
                for (uint32_t i = 0; i < level; i++) {
                    elemParams.PopLastParam();
                }
                elemParamsPtr = std::make_shared<ILDCRTParams<DCRTPoly::Integer>>(elemParams);
            }
            else {
                elemParamsPtr = cryptoParams->GetElementParams();
            }
            // Check if plaintext has got enough slots for data (value)
            uint32_t ringDim = elemParamsPtr->GetRingDimension();
            size_t valueSize = value.size();
            if (valueSize > ringDim / 2) {
                OPENFHE_THROW("The size [" + std::to_string(valueSize) +
                              "] of the vector with values should not be greater than ringDim/2 [" +
                              std::to_string(ringDim / 2) + "] if the scheme is CKKS");
            }
            // TODO (dsuponit): we should call a version of MakePlaintext instead of calling Plaintext() directly here
            p = Plaintext(std::make_shared<CKKSPackedEncoding>(elemParamsPtr, this->GetEncodingParams(), value,
                                                               noiseScaleDeg, level, scFact, slots,
                                                               this->GetCKKSDataType()));
        }
        else {
            // Check if plaintext has got enough slots for data (value)
            uint32_t ringDim = params->GetRingDimension();
            size_t valueSize = value.size();
            if (valueSize > ringDim / 2) {
                OPENFHE_THROW("The size [" + std::to_string(valueSize) +
                              "] of the vector with values should not be greater than ringDim/2 [" +
                              std::to_string(ringDim / 2) + "] if the scheme is CKKS");
            }
            // TODO (dsuponit): we should call a version of MakePlaintext instead of calling Plaintext() directly here
            p = Plaintext(std::make_shared<CKKSPackedEncoding>(params, this->GetEncodingParams(), value, noiseScaleDeg,
                                                               level, scFact, slots, this->GetCKKSDataType()));
        }
        p->Encode();

        // In FLEXIBLEAUTOEXT mode, a fresh plaintext at level 0 always has noiseScaleDeg 2.
        if (cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT && level == 0) {
            p->SetNoiseScaleDeg(2);
        }
        return p;
    }

    uint32_t GetCompositeDegreeFromCtxt() const {
        const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersRNS>(m_params);
        if (!cryptoParams) {
            std::string errorMsg(std::string("std::dynamic_pointer_cast<CryptoParametersRNS>() failed"));
            OPENFHE_THROW(errorMsg);
        }

        return cryptoParams->GetCompositeDegree();
    }

#ifdef DEBUG_KEY
    PrivateKey<Element> privateKey;
#endif

public:
#ifdef DEBUG_KEY
    void SetPrivateKey(const PrivateKey<Element> privateKey) {
        std::cerr << "Warning - SetPrivateKey is only intended to be used for debugging "
                     "purposes - not for production systems."
                  << std::endl;
        this->privateKey = privateKey;
    }

    const PrivateKey<Element>& GetPrivateKey() const {
        return this->privateKey;
    }
#endif

    void setSchemeId(SCHEME schemeTag) {
        this->m_schemeId = schemeTag;
    }

    SCHEME getSchemeId() const {
        return this->m_schemeId;
    }

    // TODO (dsuponit): investigate if we really need 2 constructors for CryptoContextImpl as one of them take regular pointer
    // and the other one takes shared_ptr
    CryptoContextImpl(CryptoParametersBase<Element>* params = nullptr, SchemeBase<Element>* scheme = nullptr,
                      SCHEME schemeId = SCHEME::INVALID_SCHEME) {
        this->m_params.reset(params);
        this->m_scheme.reset(scheme);
        this->m_keyGenLevel = 0;
        this->m_schemeId    = schemeId;
    }

    CryptoContextImpl(std::shared_ptr<CryptoParametersBase<Element>> params,
                      std::shared_ptr<SchemeBase<Element>> scheme, SCHEME schemeId = SCHEME::INVALID_SCHEME) {
        this->m_params      = params;
        this->m_scheme      = scheme;
        this->m_keyGenLevel = 0;
        this->m_schemeId    = schemeId;
    }

    CryptoContextImpl(const CryptoContextImpl<Element>& other) {
        m_params      = other.m_params;
        m_scheme      = other.m_scheme;
        m_keyGenLevel = 0;
        m_schemeId    = other.m_schemeId;
    }

    CryptoContextImpl<Element>& operator=(const CryptoContextImpl<Element>& rhs) {
        m_params      = rhs.m_params;
        m_scheme      = rhs.m_scheme;
        m_keyGenLevel = rhs.m_keyGenLevel;
        m_schemeId    = rhs.m_schemeId;
        return *this;
    }

    operator bool() const {
        return m_params && m_scheme;
    }

    friend bool operator==(const CryptoContextImpl<Element>& a, const CryptoContextImpl<Element>& b) {
        // Identical if the parameters and the schemes are identical... the exact
        // same object, OR the same type and the same values
        if (a.m_params.get() == b.m_params.get()) {
            return true;
        }
        else {
            if (typeid(*a.m_params.get()) != typeid(*b.m_params.get())) {
                return false;
            }
            if (*a.m_params.get() != *b.m_params.get())
                return false;
        }

        if (a.m_scheme.get() == b.m_scheme.get()) {
            return true;
        }
        else {
            if (typeid(*a.m_scheme.get()) != typeid(*b.m_scheme.get())) {
                return false;
            }
            if (*a.m_scheme.get() != *b.m_scheme.get())
                return false;
        }

        return true;
    }

    friend bool operator!=(const CryptoContextImpl<Element>& a, const CryptoContextImpl<Element>& b) {
        return !(a == b);
    }

    static void ClearStaticMapsAndVectors();

    template <typename ST>
    static bool SerializeEvalMultKey(std::ostream& ser, const ST& sertype, const std::string& keyTag = "") {
        const auto& evalMultKeys = CryptoContextImpl<Element>::GetAllEvalMultKeys();
        if (keyTag.length() == 0) {
            Serial::Serialize(evalMultKeys, ser, sertype);
        }
        else {
            const auto it = evalMultKeys.find(keyTag);
            if (it == evalMultKeys.end())
                return false;  // no such keyTag

            std::map<std::string, std::vector<EvalKey<Element>>> omap{{it->first, it->second}};

            Serial::Serialize(omap, ser, sertype);
        }
        return true;
    }

    template <typename ST>
    static bool SerializeEvalMultKey(std::ostream& ser, const ST& sertype, const CryptoContext<Element> cc) {
        std::map<std::string, std::vector<EvalKey<Element>>> omap;
        for (const auto& [key, vec] : CryptoContextImpl<Element>::GetAllEvalMultKeys()) {
            if (vec[0]->GetCryptoContext() == cc) {
                omap[key] = vec;
            }
        }

        if (omap.empty())
            return false;

        Serial::Serialize(omap, ser, sertype);
        return true;
    }

    template <typename ST>
    static bool DeserializeEvalMultKey(std::istream& ser, const ST& sertype) {
        std::map<std::string, std::vector<EvalKey<Element>>> omap;

        Serial::Deserialize(omap, ser, sertype);

        // The deserialize call creates all contexts that need to be created...
        // so, all we need to do is to insert the keys into the maps for their context(s)
        for (auto& [tag, vec] : omap) {
            CryptoContextImpl<Element>::InsertEvalMultKey(vec, tag);
        }
        return true;
    }

    static void ClearEvalMultKeys();

    static void ClearEvalMultKeys(const std::string& keyTag);

    static void ClearEvalMultKeys(const CryptoContext<Element>& cc);

    static void InsertEvalMultKey(const std::vector<EvalKey<Element>>& evalKeyVec, const std::string& keyTag = "");

    template <typename ST>
    static bool SerializeEvalSumKey(std::ostream& ser, const ST& sertype, const std::string& keyTag = "") {
        return CryptoContextImpl<Element>::SerializeEvalAutomorphismKey(ser, sertype, keyTag);
    }

    template <typename ST>
    static bool SerializeEvalSumKey(std::ostream& ser, const ST& sertype, const CryptoContext<Element> cc) {
        return CryptoContextImpl<Element>::SerializeEvalAutomorphismKey(ser, sertype, cc);
    }

    template <typename ST>
    static bool DeserializeEvalSumKey(std::istream& ser, const ST& sertype) {
        return CryptoContextImpl<Element>::DeserializeEvalAutomorphismKey(ser, sertype);
    }

    static void ClearEvalSumKeys();

    static void ClearEvalSumKeys(const std::string& keyTag);

    static void ClearEvalSumKeys(const CryptoContext<Element> cc);

    static void InsertEvalSumKey(const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> mapToInsert,
                                 std::string keyTag = "") {
        CryptoContextImpl<Element>::InsertEvalAutomorphismKey(mapToInsert, keyTag);
    }

    template <typename ST>
    static bool SerializeEvalAutomorphismKey(std::ostream& ser, const ST& sertype, const std::string& keyTag = "") {
        // TODO (dsuponit): do we need Serailize/Deserialized to return bool?
        std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>>* smap;
        std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>> omap;
        if (keyTag.length() == 0) {
            smap = &CryptoContextImpl<Element>::GetAllEvalAutomorphismKeys();
        }
        else {
            const auto keys = CryptoContextImpl<Element>::GetEvalAutomorphismKeyMapPtr(keyTag);
            omap[keyTag]    = keys;
            smap            = &omap;
        }
        Serial::Serialize(*smap, ser, sertype);
        return true;
    }

    template <typename ST>
    static bool SerializeEvalAutomorphismKey(std::ostream& ser, const ST& sertype, const CryptoContext<Element> cc) {
        std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>> omap;
        for (const auto& k : CryptoContextImpl<Element>::GetAllEvalAutomorphismKeys()) {
            if (k.second->begin()->second->GetCryptoContext() == cc) {
                omap[k.first] = k.second;
            }
        }

        if (omap.empty())
            return false;

        Serial::Serialize(omap, ser, sertype);
        return true;
    }

    template <typename ST>
    static bool SerializeEvalAutomorphismKey(std::ostream& ser, const ST& sertype, const std::string& keyTag,
                                             const std::vector<uint32_t>& indexList) {
        std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>> keyMap = {
            {keyTag, CryptoContextImpl<Element>::GetPartialEvalAutomorphismKeyMapPtr(keyTag, indexList)}};

        Serial::Serialize(keyMap, ser, sertype);
        return true;
    }

    template <typename ST>
    static bool DeserializeEvalAutomorphismKey(std::ostream& ser, const ST& sertype, const std::string& keyTag,
                                               const std::vector<uint32_t>& indexList) {
        if (indexList.empty())
            OPENFHE_THROW("indexList may not be empty");
        if (keyTag.empty())
            OPENFHE_THROW("keyTag may not be empty");

        std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>> allDeserKeys;
        Serial::Deserialize(allDeserKeys, ser, sertype);

        const auto& keyMapIt = allDeserKeys.find(keyTag);
        if (keyMapIt == allDeserKeys.end()) {
            OPENFHE_THROW("Deserialized automorphism keys are not generated for ID [" + keyTag + "].");
        }

        // create a new map with evalkeys for the specified indices
        std::map<uint32_t, EvalKey<Element>> newMap;
        for (const uint32_t indx : indexList) {
            const auto& key = keyMapIt->find(indx);
            if (key == keyMapIt->end()) {
                OPENFHE_THROW("No automorphism key generated for index [" + std::to_string(indx) + "] within keyTag [" +
                              keyTag + "].");
            }
            newMap[indx] = key->second;
        }

        CryptoContextImpl<Element>::InsertEvalAutomorphismKey(
            std::make_shared<std::map<uint32_t, EvalKey<Element>>>(newMap), keyTag);

        return true;
    }

    template <typename ST>
    static bool DeserializeEvalAutomorphismKey(std::istream& ser, const ST& sertype) {
        std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>> keyMap;

        Serial::Deserialize(keyMap, ser, sertype);

        // The deserialize call created any contexts that needed to be created....
        // so all we need to do is put the keys into the maps for their context
        for (auto& k : keyMap) {
            CryptoContextImpl<Element>::InsertEvalAutomorphismKey(k.second, k.first);
        }
        return true;
    }

    static void ClearEvalAutomorphismKeys();

    static void ClearEvalAutomorphismKeys(const std::string& keyTag);

    static void ClearEvalAutomorphismKeys(const CryptoContext<Element> cc);

    // TODO (dsuponit): move InsertEvalAutomorphismKey() to the private section of the class
    static void InsertEvalAutomorphismKey(const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> mapToInsert,
                                          const std::string& keyTag = "");
    //------------------------------------------------------------------------------
    // TURN FEATURES ON
    //------------------------------------------------------------------------------

    void Enable(PKESchemeFeature feature) {
        m_scheme->Enable(feature);
    }

    void Enable(uint32_t featureMask) {
        m_scheme->Enable(featureMask);
    }

    // GETTERS
    const std::shared_ptr<SchemeBase<Element>> GetScheme() const {
        return m_scheme;
    }

    const std::shared_ptr<CryptoParametersBase<Element>> GetCryptoParameters() const {
        return m_params;
    }

    size_t GetKeyGenLevel() const {
        return m_keyGenLevel;
    }

    void SetKeyGenLevel(size_t level) {
        m_keyGenLevel = level;
    }

    const std::shared_ptr<ParmType> GetElementParams() const {
        return m_params->GetElementParams();
    }

    const EncodingParams GetEncodingParams() const {
        return m_params->GetEncodingParams();
    }

    uint32_t GetCyclotomicOrder() const {
        return m_params->GetElementParams()->GetCyclotomicOrder();
    }

    uint32_t GetRingDimension() const {
        return m_params->GetElementParams()->GetRingDimension();
    }

    const IntType& GetModulus() const {
        return m_params->GetElementParams()->GetModulus();
    }

    const IntType& GetRootOfUnity() const {
        return m_params->GetElementParams()->GetRootOfUnity();
    }

    CKKSDataType GetCKKSDataType() const {
        const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersRNS>(m_params);
        if (!cryptoParams) {
            std::string errorMsg(std::string("std::dynamic_pointer_cast<CryptoParametersRNS>() failed"));
            OPENFHE_THROW(errorMsg);
        }

        return cryptoParams->GetCKKSDataType();
    }

    //------------------------------------------------------------------------------
    // KEYS GETTERS
    //------------------------------------------------------------------------------

    static std::map<std::string, std::vector<EvalKey<Element>>>& GetAllEvalMultKeys();

    static const std::vector<EvalKey<Element>>& GetEvalMultKeyVector(const std::string& keyTag);

    static std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>>& GetAllEvalAutomorphismKeys();

    static std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> GetEvalAutomorphismKeyMapPtr(
        const std::string& keyTag);

    static std::map<uint32_t, EvalKey<Element>>& GetEvalAutomorphismKeyMap(const std::string& keyTag) {
        return *(CryptoContextImpl<Element>::GetEvalAutomorphismKeyMapPtr(keyTag));
    }

    static std::map<std::string, std::shared_ptr<std::map<uint32_t, EvalKey<Element>>>>& GetAllEvalSumKeys();

    static const std::map<uint32_t, EvalKey<Element>>& GetEvalSumKeyMap(const std::string& keyTag);

    //------------------------------------------------------------------------------
    // PLAINTEXT FACTORY METHODS
    //------------------------------------------------------------------------------

    // TODO to be deprecated in 2.0
    Plaintext MakeStringPlaintext(const std::string& str) const {
        return PlaintextFactory::MakePlaintext(str, STRING_ENCODING, this->GetElementParams(),
                                               this->GetEncodingParams());
    }

    Plaintext MakeCoefPackedPlaintext(const std::vector<int64_t>& value, size_t noiseScaleDeg = 1,
                                      uint32_t level = 0) const {
        if (value.empty())
            OPENFHE_THROW("Cannot encode an empty value vector");

        return MakePlaintext(COEF_PACKED_ENCODING, value, noiseScaleDeg, level);
    }

    Plaintext MakePackedPlaintext(const std::vector<int64_t>& value, size_t noiseScaleDeg = 1,
                                  uint32_t level = 0) const {
        if (value.empty())
            OPENFHE_THROW("Cannot encode an empty value vector");

        return MakePlaintext(PACKED_ENCODING, value, noiseScaleDeg, level);
    }

    Plaintext MakeCKKSPackedPlaintext(const std::vector<std::complex<double>>& value, size_t noiseScaleDeg = 1,
                                      uint32_t level = 0, const std::shared_ptr<ParmType> params = nullptr,
                                      uint32_t slots = 0) const {
        VerifyCKKSScheme(__func__);
        if (value.empty())
            OPENFHE_THROW("Cannot encode an empty value vector");

        return MakeCKKSPackedPlaintextInternal(value, noiseScaleDeg, level, params, slots);
    }

    Plaintext MakeCKKSPackedPlaintext(const std::vector<double>& value, size_t noiseScaleDeg = 1, uint32_t level = 0,
                                      const std::shared_ptr<ParmType> params = nullptr, uint32_t slots = 0) const {
        VerifyCKKSScheme(__func__);
        if (value.empty())
            OPENFHE_THROW("Cannot encode an empty value vector");

        std::vector<std::complex<double>> complexValue(value.size());
        std::transform(value.begin(), value.end(), complexValue.begin(),
                       [](double da) { return std::complex<double>(da); });

        return MakeCKKSPackedPlaintextInternal(complexValue, noiseScaleDeg, level, params, slots);
    }

    static Plaintext GetPlaintextForDecrypt(PlaintextEncodings pte, std::shared_ptr<ParmType> evp, EncodingParams ep,
                                            CKKSDataType cdt = REAL);

    //------------------------------------------------------------------------------
    // PKE Wrapper
    //------------------------------------------------------------------------------

    KeyPair<Element> KeyGen() const {
        return GetScheme()->KeyGen(GetContextForPointer(this), false);
    }

    KeyPair<Element> SparseKeyGen() const {
        return GetScheme()->KeyGen(GetContextForPointer(this), true);
    }

    Ciphertext<Element> Encrypt(ConstPlaintext& plaintext, const PublicKey<Element>& publicKey) const {
        if (plaintext == nullptr)
            OPENFHE_THROW("Input plaintext is nullptr");
        ValidateKey(publicKey);

        Ciphertext<Element> ciphertext = GetScheme()->Encrypt(plaintext->GetElement<Element>(), publicKey);

        if (ciphertext) {
            ciphertext->SetSlots(plaintext->GetSlots());
            ciphertext->SetLevel(plaintext->GetLevel());
            ciphertext->SetNoiseScaleDeg(plaintext->GetNoiseScaleDeg());
            ciphertext->SetScalingFactor(plaintext->GetScalingFactor());
            ciphertext->SetScalingFactorInt(plaintext->GetScalingFactorInt());
            ciphertext->SetEncodingType(plaintext->GetEncodingType());
        }

        return ciphertext;
    }

    Ciphertext<Element> Encrypt(const PublicKey<Element>& publicKey, ConstPlaintext& plaintext) const {
        return Encrypt(plaintext, publicKey);
    }

    Ciphertext<Element> Encrypt(ConstPlaintext& plaintext, const PrivateKey<Element>& privateKey) const {
        //    if (plaintext == nullptr)
        //      OPENFHE_THROW( "Input plaintext is nullptr");
        ValidateKey(privateKey);

        Ciphertext<Element> ciphertext = GetScheme()->Encrypt(plaintext->GetElement<Element>(), privateKey);

        if (ciphertext) {
            ciphertext->SetSlots(plaintext->GetSlots());
            ciphertext->SetLevel(plaintext->GetLevel());
            ciphertext->SetNoiseScaleDeg(plaintext->GetNoiseScaleDeg());
            ciphertext->SetScalingFactor(plaintext->GetScalingFactor());
            ciphertext->SetScalingFactorInt(plaintext->GetScalingFactorInt());
            ciphertext->SetEncodingType(plaintext->GetEncodingType());
        }

        return ciphertext;
    }

    Ciphertext<Element> Encrypt(const PrivateKey<Element>& privateKey, ConstPlaintext& plaintext) const {
        return Encrypt(plaintext, privateKey);
    }

    DecryptResult Decrypt(ConstCiphertext<Element>& ciphertext, const PrivateKey<Element>& privateKey,
                          Plaintext* plaintext);

    inline DecryptResult Decrypt(const PrivateKey<Element>& privateKey, ConstCiphertext<Element>& ciphertext,
                                 Plaintext* plaintext) {
        return Decrypt(ciphertext, privateKey, plaintext);
    }

    //------------------------------------------------------------------------------
    // KeySwitch Wrapper
    //------------------------------------------------------------------------------

    EvalKey<Element> KeySwitchGen(const PrivateKey<Element>& oldPrivateKey,
                                  const PrivateKey<Element>& newPrivateKey) const {
        ValidateKey(oldPrivateKey);
        ValidateKey(newPrivateKey);
        return GetScheme()->KeySwitchGen(oldPrivateKey, newPrivateKey);
    }

    Ciphertext<Element> KeySwitch(ConstCiphertext<Element>& ciphertext, const EvalKey<Element>& evalKey) const {
        ValidateCiphertext(ciphertext);
        ValidateKey(evalKey);
        return GetScheme()->KeySwitch(ciphertext, evalKey);
    }

    void KeySwitchInPlace(Ciphertext<Element>& ciphertext, const EvalKey<Element>& evalKey) const {
        ValidateCiphertext(ciphertext);
        ValidateKey(evalKey);
        GetScheme()->KeySwitchInPlace(ciphertext, evalKey);
    }

    //------------------------------------------------------------------------------
    // SHE NEGATION Wrapper
    //------------------------------------------------------------------------------

    Ciphertext<Element> EvalNegate(ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalNegate(ciphertext);
    }

    void EvalNegateInPlace(Ciphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        GetScheme()->EvalNegateInPlace(ciphertext);
    }

    //------------------------------------------------------------------------------
    // SHE ADDITION Wrapper
    //------------------------------------------------------------------------------

    Ciphertext<Element> EvalAdd(ConstCiphertext<Element>& ciphertext1, ConstCiphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        return GetScheme()->EvalAdd(ciphertext1, ciphertext2);
    }

    void EvalAddInPlace(Ciphertext<Element>& ciphertext1, ConstCiphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        GetScheme()->EvalAddInPlace(ciphertext1, ciphertext2);
    }

    void EvalAddInPlaceNoCheck(Ciphertext<Element>& ctxt1, ConstCiphertext<Element>& ctxt2) const {
        auto& cv1  = ctxt1->GetElements();
        auto& cv2  = ctxt2->GetElements();
        uint32_t n = cv1.size();
        for (uint32_t i = 0; i < n; ++i)
            cv1[i] += cv2[i];
    }

    Ciphertext<Element> EvalAddMutable(Ciphertext<Element>& ciphertext1, Ciphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        return GetScheme()->EvalAddMutable(ciphertext1, ciphertext2);
    }

    void EvalAddMutableInPlace(Ciphertext<Element>& ciphertext1, Ciphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        GetScheme()->EvalAddMutableInPlace(ciphertext1, ciphertext2);
    }

    Ciphertext<Element> EvalAdd(ConstCiphertext<Element>& ciphertext, Plaintext& plaintext) const {
        TypeCheck(ciphertext, plaintext);
        plaintext->SetFormat(EVALUATION);
        return GetScheme()->EvalAdd(ciphertext, plaintext);
    }

    inline Ciphertext<Element> EvalAdd(Plaintext& plaintext, ConstCiphertext<Element>& ciphertext) const {
        return EvalAdd(ciphertext, plaintext);
    }

    void EvalAddInPlace(Ciphertext<Element>& ciphertext, Plaintext& plaintext) const {
        TypeCheck(ciphertext, plaintext);
        plaintext->SetFormat(EVALUATION);
        GetScheme()->EvalAddInPlace(ciphertext, plaintext);
    }

    void EvalAddInPlace(Plaintext& plaintext, Ciphertext<Element>& ciphertext) const {
        EvalAddInPlace(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalAddMutable(Ciphertext<Element>& ciphertext, Plaintext& plaintext) const {
        TypeCheck(ciphertext, plaintext);
        plaintext->SetFormat(EVALUATION);
        return GetScheme()->EvalAddMutable(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalAddMutable(Plaintext& plaintext, Ciphertext<Element>& ciphertext) const {
        return EvalAddMutable(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalAdd(ConstCiphertext<Element>& ciphertext, double scalar) const {
        return scalar >= 0. ? GetScheme()->EvalAdd(ciphertext, scalar) : GetScheme()->EvalSub(ciphertext, -scalar);
    }

    Ciphertext<Element> EvalAdd(double scalar, ConstCiphertext<Element>& ciphertext) const {
        return EvalAdd(ciphertext, scalar);
    }

    void EvalAddInPlace(Ciphertext<Element>& ciphertext, double scalar) const {
        if (scalar == 0.)
            return;
        if (scalar > 0.)
            GetScheme()->EvalAddInPlace(ciphertext, scalar);
        else
            GetScheme()->EvalSubInPlace(ciphertext, -scalar);
    }

    void EvalAddInPlace(double scalar, Ciphertext<Element>& ciphertext) const {
        EvalAddInPlace(ciphertext, scalar);
    }

    Ciphertext<Element> EvalAdd(ConstCiphertext<Element>& ciphertext, std::complex<double> scalar) const {
        return GetScheme()->EvalAdd(ciphertext, scalar);
    }

    Ciphertext<Element> EvalAdd(std::complex<double> scalar, ConstCiphertext<Element>& ciphertext) const {
        return EvalAdd(ciphertext, scalar);
    }

    void EvalAddInPlace(Ciphertext<Element>& ciphertext, std::complex<double> scalar) const {
        if (scalar == std::complex<double>(0.0, 0.0))
            return;
        GetScheme()->EvalAddInPlace(ciphertext, scalar);
    }

    void EvalAddInPlace(std::complex<double> scalar, Ciphertext<Element>& ciphertext) const {
        EvalAddInPlace(ciphertext, scalar);
    }

    //------------------------------------------------------------------------------
    // SHE SUBTRACTION Wrapper
    //------------------------------------------------------------------------------

    Ciphertext<Element> EvalSub(ConstCiphertext<Element>& ciphertext1, ConstCiphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        return GetScheme()->EvalSub(ciphertext1, ciphertext2);
    }

    void EvalSubInPlace(Ciphertext<Element>& ciphertext1, ConstCiphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        GetScheme()->EvalSubInPlace(ciphertext1, ciphertext2);
    }

    Ciphertext<Element> EvalSubMutable(Ciphertext<Element>& ciphertext1, Ciphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        return GetScheme()->EvalSubMutable(ciphertext1, ciphertext2);
    }

    void EvalSubMutableInPlace(Ciphertext<Element>& ciphertext1, Ciphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        GetScheme()->EvalSubMutableInPlace(ciphertext1, ciphertext2);
    }

    Ciphertext<Element> EvalSub(ConstCiphertext<Element>& ciphertext, Plaintext& plaintext) const {
        TypeCheck(ciphertext, plaintext);
        return GetScheme()->EvalSub(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalSub(Plaintext& plaintext, ConstCiphertext<Element>& ciphertext) const {
        return EvalAdd(EvalNegate(ciphertext), plaintext);
    }

    Ciphertext<Element> EvalSubMutable(Ciphertext<Element>& ciphertext, Plaintext& plaintext) const {
        TypeCheck(ciphertext, plaintext);
        return GetScheme()->EvalSubMutable(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalSubMutable(Plaintext& plaintext, Ciphertext<Element>& ciphertext) const {
        Ciphertext<Element> negated = EvalNegate(ciphertext);
        Ciphertext<Element> result  = EvalAddMutable(negated, plaintext);
        ciphertext                  = EvalNegate(negated);
        return result;
    }

    void EvalSubInPlace(Ciphertext<Element>& ciphertext, ConstPlaintext& plaintext) const {
        TypeCheck(ciphertext, plaintext);
        GetScheme()->EvalSubInPlace(ciphertext, plaintext);
    }

    void EvalSubInPlace(Plaintext& plaintext, Ciphertext<Element>& ciphertext) const {
        EvalNegateInPlace(ciphertext);
        EvalAddInPlace(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalSub(ConstCiphertext<Element>& ciphertext, double scalar) const {
        return scalar >= 0 ? GetScheme()->EvalSub(ciphertext, scalar) : GetScheme()->EvalAdd(ciphertext, -scalar);
    }

    Ciphertext<Element> EvalSub(double scalar, ConstCiphertext<Element>& ciphertext) const {
        return EvalAdd(EvalNegate(ciphertext), scalar);
    }

    void EvalSubInPlace(Ciphertext<Element>& ciphertext, double scalar) const {
        if (scalar == 0.)
            return;
        if (scalar > 0.)
            GetScheme()->EvalSubInPlace(ciphertext, scalar);
        else
            GetScheme()->EvalAddInPlace(ciphertext, -scalar);
    }

    void EvalSubInPlace(double scalar, Ciphertext<Element>& ciphertext) const {
        EvalNegateInPlace(ciphertext);
        EvalAddInPlace(ciphertext, scalar);
    }

    Ciphertext<Element> EvalSub(ConstCiphertext<Element>& ciphertext, std::complex<double> scalar) const {
        return GetScheme()->EvalAdd(ciphertext, -scalar);
    }

    Ciphertext<Element> EvalSub(std::complex<double> scalar, ConstCiphertext<Element>& ciphertext) const {
        return EvalAdd(EvalNegate(ciphertext), scalar);
    }

    void EvalSubInPlace(Ciphertext<Element>& ciphertext, std::complex<double> scalar) const {
        if (scalar == std::complex<double>(0.0, 0.0))
            return;
        GetScheme()->EvalAddInPlace(ciphertext, -scalar);
    }

    void EvalSubInPlace(std::complex<double> scalar, Ciphertext<Element>& ciphertext) const {
        EvalNegateInPlace(ciphertext);
        EvalAddInPlace(ciphertext, scalar);
    }

    //------------------------------------------------------------------------------
    // SHE MULTIPLICATION Wrapper
    //------------------------------------------------------------------------------

    void EvalMultKeyGen(const PrivateKey<Element>& key);

    void EvalMultKeysGen(const PrivateKey<Element>& key);

    Ciphertext<Element> EvalMult(ConstCiphertext<Element>& ciphertext1, ConstCiphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext1->GetKeyTag());
        if (evalKeyVec.empty())
            OPENFHE_THROW("Evaluation key has not been generated for EvalMult");

        return GetScheme()->EvalMult(ciphertext1, ciphertext2, evalKeyVec[0]);
    }

    Ciphertext<Element> EvalMultMutable(Ciphertext<Element>& ciphertext1, Ciphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext1->GetKeyTag());
        if (evalKeyVec.empty())
            OPENFHE_THROW("Evaluation key has not been generated for EvalMultMutable");

        return GetScheme()->EvalMultMutable(ciphertext1, ciphertext2, evalKeyVec[0]);
    }

    void EvalMultMutableInPlace(Ciphertext<Element>& ciphertext1, Ciphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext1->GetKeyTag());
        if (evalKeyVec.empty())
            OPENFHE_THROW("Evaluation key has not been generated for EvalMultMutableInPlace");

        GetScheme()->EvalMultMutableInPlace(ciphertext1, ciphertext2, evalKeyVec[0]);
    }

    Ciphertext<Element> EvalSquare(ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext->GetKeyTag());
        if (evalKeyVec.empty())
            OPENFHE_THROW("Evaluation key has not been generated for EvalSquare");

        return GetScheme()->EvalSquare(ciphertext, evalKeyVec[0]);
    }

    Ciphertext<Element> EvalSquareMutable(Ciphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext->GetKeyTag());
        if (evalKeyVec.empty())
            OPENFHE_THROW("Evaluation key has not been generated for EvalSquareMutable");

        return GetScheme()->EvalSquareMutable(ciphertext, evalKeyVec[0]);
    }

    void EvalSquareInPlace(Ciphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext->GetKeyTag());
        if (evalKeyVec.empty())
            OPENFHE_THROW("Evaluation key has not been generated for EvalSquareInPlace");

        GetScheme()->EvalSquareInPlace(ciphertext, evalKeyVec[0]);
    }

    Ciphertext<Element> EvalMultNoRelin(ConstCiphertext<Element>& ciphertext1,
                                        ConstCiphertext<Element>& ciphertext2) const {
        TypeCheck(ciphertext1, ciphertext2);
        return GetScheme()->EvalMult(ciphertext1, ciphertext2);
    }

    Ciphertext<Element> EvalMultNoRelinNoCheck(ConstCiphertext<Element>& ctxt1, ConstCiphertext<Element>& ctxt2) const {
        auto& cv1 = ctxt1->GetElements();
        auto& cv2 = ctxt2->GetElements();

        uint32_t n1 = cv1.size();
        uint32_t n2 = cv2.size();
        uint32_t nr = n1 + n2 - 1;

        std::vector<DCRTPoly> cvr;
        cvr.reserve(nr);

        if (n1 == 2 && n2 == 2) {
            cvr.emplace_back(cv1[0] * cv2[0]);
            cvr.emplace_back((cv1[0] * cv2[1]) += (cv1[1] * cv2[0]));
            cvr.emplace_back(cv1[1] * cv2[1]);
        }
        else {
            uint32_t m = 0;
            for (uint32_t i = 0; i < n1; ++i) {
                auto& cv1i = cv1[i];
                for (uint32_t j = 0, k = i; j < n2; ++j, ++k) {
                    if (k == m) {
                        cvr.emplace_back(cv1i * cv2[j]);
                        ++m;
                    }
                    else {
                        cvr[k] += (cv1i * cv2[j]);
                    }
                }
            }
        }

        auto result = ctxt1->CloneEmpty();
        result->SetElements(std::move(cvr));
        result->SetNoiseScaleDeg(ctxt1->GetNoiseScaleDeg() + ctxt2->GetNoiseScaleDeg());
        result->SetScalingFactor(ctxt1->GetScalingFactor() * ctxt2->GetScalingFactor());
        result->SetScalingFactorInt(ctxt1->GetScalingFactorInt().ModMul(
            ctxt2->GetScalingFactorInt(), ctxt1->GetCryptoParameters()->GetPlaintextModulus()));
        return result;
    }

    Ciphertext<Element> Relinearize(ConstCiphertext<Element>& ciphertext) const {
        // input parameter check
        if (!ciphertext)
            OPENFHE_THROW("Input ciphertext is nullptr");

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext->GetKeyTag());

        if (evalKeyVec.size() < (ciphertext->NumberCiphertextElements() - 2))
            OPENFHE_THROW("Insufficient value was used for maxRelinSkDeg to generate keys for Relinearize");

        return GetScheme()->Relinearize(ciphertext, evalKeyVec);
    }

    void RelinearizeInPlace(Ciphertext<Element>& ciphertext) const {
        // input parameter check
        if (!ciphertext)
            OPENFHE_THROW("Input ciphertext is nullptr");

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext->GetKeyTag());
        if (evalKeyVec.size() < (ciphertext->NumberCiphertextElements() - 2))
            OPENFHE_THROW("Insufficient value was used for maxRelinSkDeg to generate keys for RelinearizeInPlace");

        GetScheme()->RelinearizeInPlace(ciphertext, evalKeyVec);
    }

    Ciphertext<Element> EvalMultAndRelinearize(ConstCiphertext<Element>& ciphertext1,
                                               ConstCiphertext<Element>& ciphertext2) const {
        if (!ciphertext1 || !ciphertext2)
            OPENFHE_THROW("Input ciphertext is nullptr");

        const auto& evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext1->GetKeyTag());

        if (evalKeyVec.size() <
            (ciphertext1->NumberCiphertextElements() + ciphertext2->NumberCiphertextElements() - 3)) {
            OPENFHE_THROW("Insufficient value was used for maxRelinSkDeg to generate keys for EvalMultAndRelinearize");
        }

        return GetScheme()->EvalMultAndRelinearize(ciphertext1, ciphertext2, evalKeyVec);
    }

    Ciphertext<Element> EvalMultNoCheck(ConstCiphertext<Element>& ctxt, NativeInteger k) const {
        auto result = ctxt->Clone();
        auto& cv    = result->GetElements();
        uint32_t n  = cv.size();
        for (uint32_t i = 0; i < n; ++i)
            cv[i] *= k;
        return result;
    }

    Ciphertext<Element> EvalMult(ConstCiphertext<Element>& ciphertext, ConstPlaintext& plaintext) const {
        TypeCheck(ciphertext, plaintext);
        return GetScheme()->EvalMult(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalMult(ConstPlaintext& plaintext, ConstCiphertext<Element>& ciphertext) const {
        return EvalMult(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalMultMutable(Ciphertext<Element>& ciphertext, Plaintext& plaintext) const {
        TypeCheck(ciphertext, plaintext);
        return GetScheme()->EvalMultMutable(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalMultMutable(Plaintext& plaintext, Ciphertext<Element>& ciphertext) const {
        return EvalMultMutable(ciphertext, plaintext);
    }

    Ciphertext<Element> EvalMult(ConstCiphertext<Element>& ciphertext, double scalar) const {
        if (!ciphertext)
            OPENFHE_THROW("Input ciphertext is nullptr");
        return GetScheme()->EvalMult(ciphertext, scalar);
    }

    inline Ciphertext<Element> EvalMult(double scalar, ConstCiphertext<Element>& ciphertext) const {
        return EvalMult(ciphertext, scalar);
    }

    void EvalMultInPlace(Ciphertext<Element>& ciphertext, double scalar) const {
        if (!ciphertext)
            OPENFHE_THROW("Input ciphertext is nullptr");
        GetScheme()->EvalMultInPlace(ciphertext, scalar);
    }

    inline void EvalMultInPlace(double scalar, Ciphertext<Element>& ciphertext) const {
        EvalMultInPlace(ciphertext, scalar);
    }

    Ciphertext<Element> EvalMult(ConstCiphertext<Element>& ciphertext, std::complex<double> scalar) const {
        if (!ciphertext)
            OPENFHE_THROW("Input ciphertext is nullptr");
        return GetScheme()->EvalMult(ciphertext, scalar);
    }

    inline Ciphertext<Element> EvalMult(std::complex<double> scalar, ConstCiphertext<Element>& ciphertext) const {
        return EvalMult(ciphertext, scalar);
    }

    void EvalMultInPlace(Ciphertext<Element>& ciphertext, std::complex<double> scalar) const {
        if (!ciphertext)
            OPENFHE_THROW("Input ciphertext is nullptr");
        GetScheme()->EvalMultInPlace(ciphertext, scalar);
    }

    inline void EvalMultInPlace(std::complex<double> scalar, Ciphertext<Element>& ciphertext) const {
        EvalMultInPlace(ciphertext, scalar);
    }

    //------------------------------------------------------------------------------
    // SHE AUTOMORPHISM Wrapper
    //------------------------------------------------------------------------------

    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> EvalAutomorphismKeyGen(
        const PrivateKey<Element> privateKey, const std::vector<uint32_t>& indexList) const {
        ValidateKey(privateKey);
        if (indexList.empty())
            OPENFHE_THROW("Input index vector is empty");
        auto evalKeys = GetScheme()->EvalAutomorphismKeyGen(privateKey, indexList);
        CryptoContextImpl<Element>::InsertEvalAutomorphismKey(evalKeys, privateKey->GetKeyTag());
        return evalKeys;
    }

    Ciphertext<Element> EvalAutomorphism(ConstCiphertext<Element>& ciphertext, uint32_t i,
                                         const std::map<uint32_t, EvalKey<Element>>& evalKeyMap,
                                         CALLER_INFO_ARGS_HDR) const {
        ValidateCiphertext(ciphertext);

        if (evalKeyMap.empty())
            OPENFHE_THROW(std::string("Empty input key map") + CALLER_INFO);

        auto key = evalKeyMap.find(i);
        if (key == evalKeyMap.end())
            OPENFHE_THROW(std::string("Could not find an EvalKey for index ") + std::to_string(i) + CALLER_INFO);

        auto evalKey = key->second;
        ValidateKey(evalKey);

        return GetScheme()->EvalAutomorphism(ciphertext, i, evalKeyMap);
    }

    uint32_t FindAutomorphismIndex(const uint32_t idx) const {
        const auto cryptoParams  = GetCryptoParameters();
        const auto elementParams = cryptoParams->GetElementParams();
        uint32_t m               = elementParams->GetCyclotomicOrder();
        return GetScheme()->FindAutomorphismIndex(idx, m);
    }

    std::vector<uint32_t> FindAutomorphismIndices(const std::vector<uint32_t>& idxList) const {
        std::vector<uint32_t> newIndices;
        newIndices.reserve(idxList.size());
        for (const auto idx : idxList) {
            newIndices.emplace_back(FindAutomorphismIndex(idx));
        }
        return newIndices;
    }

    Ciphertext<Element> EvalRotate(ConstCiphertext<Element>& ciphertext, int32_t index) const {
        ValidateCiphertext(ciphertext);

        auto evalKeyMap = CryptoContextImpl<Element>::GetEvalAutomorphismKeyMap(ciphertext->GetKeyTag());
        return GetScheme()->EvalAtIndex(ciphertext, index, evalKeyMap);
    }

    std::shared_ptr<std::vector<Element>> EvalFastRotationPrecompute(ConstCiphertext<Element>& ciphertext) const {
        return GetScheme()->EvalFastRotationPrecompute(ciphertext);
    }

    Ciphertext<Element> EvalFastRotation(ConstCiphertext<Element>& ciphertext, const uint32_t index, const uint32_t m,
                                         const std::shared_ptr<std::vector<Element>> digits) const {
        return GetScheme()->EvalFastRotation(ciphertext, index, m, digits);
    }

    Ciphertext<Element> EvalFastRotation(ConstCiphertext<Element>& ciphertext, const uint32_t index,
                                         const std::shared_ptr<std::vector<Element>> digits) const {
        return EvalFastRotation(ciphertext, index, GetRingDimension() * 2, digits);
    }

    Ciphertext<Element> EvalFastRotationExt(ConstCiphertext<Element>& ciphertext, uint32_t index,
                                            const std::shared_ptr<std::vector<Element>> digits, bool addFirst) const {
        auto evalKeyMap = CryptoContextImpl<Element>::GetEvalAutomorphismKeyMap(ciphertext->GetKeyTag());
        return GetScheme()->EvalFastRotationExt(ciphertext, index, digits, addFirst, evalKeyMap);
    }

    Ciphertext<Element> KeySwitchDown(ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->KeySwitchDown(ciphertext);
    }

    Element KeySwitchDownFirstElement(ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->KeySwitchDownFirstElement(ciphertext);
    }

    Ciphertext<Element> KeySwitchExt(ConstCiphertext<Element>& ciphertext, bool addFirst) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->KeySwitchExt(ciphertext, addFirst);
    }

    void EvalAtIndexKeyGen(const PrivateKey<Element> privateKey, const std::vector<int32_t>& indexList);

    void EvalRotateKeyGen(const PrivateKey<Element> privateKey, const std::vector<int32_t>& indexList) {
        EvalAtIndexKeyGen(privateKey, indexList);
    };

    Ciphertext<Element> EvalAtIndex(ConstCiphertext<Element>& ciphertext, int32_t index) const;

    //------------------------------------------------------------------------------
    // SHE Leveled Methods Wrapper
    //------------------------------------------------------------------------------

    Ciphertext<Element> ComposedEvalMult(ConstCiphertext<Element>& ciphertext1,
                                         ConstCiphertext<Element>& ciphertext2) const {
        ValidateCiphertext(ciphertext1);
        ValidateCiphertext(ciphertext2);

        auto evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertext1->GetKeyTag());
        if (evalKeyVec.empty())
            OPENFHE_THROW("Evaluation key has not been generated for EvalMult");

        return GetScheme()->ComposedEvalMult(ciphertext1, ciphertext2, evalKeyVec[0]);
    }

    Ciphertext<Element> Rescale(ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->ModReduce(ciphertext, GetCompositeDegreeFromCtxt());
    }

    void RescaleInPlace(Ciphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        GetScheme()->ModReduceInPlace(ciphertext, GetCompositeDegreeFromCtxt());
    }

    Ciphertext<Element> ModReduce(ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->ModReduce(ciphertext, GetCompositeDegreeFromCtxt());
    }

    void ModReduceInPlace(Ciphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        GetScheme()->ModReduceInPlace(ciphertext, GetCompositeDegreeFromCtxt());
    }

    Ciphertext<Element> LevelReduce(ConstCiphertext<Element>& ciphertext, const EvalKey<Element> evalKey,
                                    size_t levels = 1) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->LevelReduce(ciphertext, evalKey, levels * GetCompositeDegreeFromCtxt());
    }

    void LevelReduceInPlace(Ciphertext<Element>& ciphertext, const EvalKey<Element> evalKey, size_t levels = 1) const {
        ValidateCiphertext(ciphertext);
        if (levels > 0)
            GetScheme()->LevelReduceInPlace(ciphertext, evalKey, levels * GetCompositeDegreeFromCtxt());
    }

    Ciphertext<Element> Compress(ConstCiphertext<Element>& ciphertext, uint32_t towersLeft = 1,
                                 size_t noiseScaleDeg = 1) const {
        if (ciphertext == nullptr)
            OPENFHE_THROW("input ciphertext is invalid (has no data)");
        if (towersLeft < noiseScaleDeg) {
            std::string errorMsg{"Input towersLeft["};
            errorMsg += std::to_string(towersLeft) + "] may not be less than noiseScaleDeg[";
            errorMsg += std::to_string(noiseScaleDeg) + "]";
            OPENFHE_THROW(errorMsg);
        }
        return GetScheme()->Compress(ciphertext, towersLeft, noiseScaleDeg);
    }

    //------------------------------------------------------------------------------
    // Advanced SHE Wrapper
    //------------------------------------------------------------------------------

    Ciphertext<Element> EvalAddMany(const std::vector<Ciphertext<Element>>& ciphertextVec) const {
        if (ciphertextVec.empty())
            OPENFHE_THROW("Empty input ciphertext vector");
        if (ciphertextVec.size() == 1)
            return ciphertextVec[0];
        return GetScheme()->EvalAddMany(ciphertextVec);
    }

    Ciphertext<Element> EvalAddManyInPlace(std::vector<Ciphertext<Element>>& ciphertextVec) const {
        if (ciphertextVec.empty())
            OPENFHE_THROW("Empty input ciphertext vector");
        return GetScheme()->EvalAddManyInPlace(ciphertextVec);
    }

    Ciphertext<Element> EvalMultMany(const std::vector<Ciphertext<Element>>& ciphertextVec) const {
        if (ciphertextVec.empty())
            OPENFHE_THROW("Empty input ciphertext vector");
        if (ciphertextVec.size() == 1)
            return ciphertextVec[0];
        const auto evalKeyVec = CryptoContextImpl<Element>::GetEvalMultKeyVector(ciphertextVec[0]->GetKeyTag());
        if (evalKeyVec.size() < (ciphertextVec[0]->NumberCiphertextElements() - 2))
            OPENFHE_THROW("Insufficient value was used for maxRelinSkDeg to generate keys");
        return GetScheme()->EvalMultMany(ciphertextVec, evalKeyVec);
    }

    //------------------------------------------------------------------------------
    // Advanced SHE LINEAR WEIGHTED SUM
    //------------------------------------------------------------------------------

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalLinearWSum(std::vector<ReadOnlyCiphertext<Element>>& ciphertextVec,
                                       const std::vector<VectorDataType>& constantVec) const {
        return GetScheme()->EvalLinearWSum(ciphertextVec, constantVec);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalLinearWSum(const std::vector<VectorDataType>& constantsVec,
                                       std::vector<ReadOnlyCiphertext<Element>>& ciphertextVec) const {
        return EvalLinearWSum(ciphertextVec, constantsVec);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalLinearWSumMutable(std::vector<Ciphertext<Element>>& ciphertextVec,
                                              const std::vector<VectorDataType>& constantsVec) const {
        return GetScheme()->EvalLinearWSumMutable(ciphertextVec, constantsVec);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalLinearWSumMutable(const std::vector<VectorDataType>& constantsVec,
                                              std::vector<Ciphertext<Element>>& ciphertextVec) const {
        return EvalLinearWSumMutable(ciphertextVec, constantsVec);
    }

    //------------------------------------------------------------------------------
    // Advanced SHE EVAL POLYNOMIAL
    //------------------------------------------------------------------------------

    template <typename VectorDataType = double>
    std::shared_ptr<seriesPowers<Element>> EvalPowers(ConstCiphertext<Element>& ciphertext,
                                                      const std::vector<VectorDataType>& coefficients) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalPowers(ciphertext, coefficients);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalPoly(ConstCiphertext<Element>& ciphertext,
                                 const std::vector<VectorDataType>& coefficients) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalPoly(ciphertext, coefficients);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalPolyWithPrecomp(std::shared_ptr<seriesPowers<Element>> powers,
                                            const std::vector<VectorDataType>& coefficients) const {
        ValidateSeriesPowers(powers);
        return GetScheme()->EvalPolyWithPrecomp(powers, coefficients);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalPolyLinear(ConstCiphertext<Element>& ciphertext,
                                       const std::vector<VectorDataType>& coefficients) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalPolyLinear(ciphertext, coefficients);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalPolyPS(ConstCiphertext<Element>& ciphertext,
                                   const std::vector<VectorDataType>& coefficients) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalPolyPS(ciphertext, coefficients);
    }

    //------------------------------------------------------------------------------
    // Advanced SHE EVAL CHEBYSHEV SERIES
    //------------------------------------------------------------------------------

    template <typename VectorDataType = double>
    std::shared_ptr<seriesPowers<Element>> EvalChebyPolys(ConstCiphertext<Element>& ciphertext,
                                                          const std::vector<VectorDataType>& coefficients, double a,
                                                          double b) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalChebyPolys(ciphertext, coefficients, a, b);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalChebyshevSeries(ConstCiphertext<Element>& ciphertext,
                                            const std::vector<VectorDataType>& coefficients, double a, double b) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalChebyshevSeries(ciphertext, coefficients, a, b);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalChebyshevSeriesWithPrecomp(std::shared_ptr<seriesPowers<Element>> polys,
                                                       const std::vector<VectorDataType>& coefficients) const {
        ValidateSeriesPowers(polys);
        return GetScheme()->EvalChebyshevSeriesWithPrecomp(polys, coefficients);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalChebyshevSeriesLinear(ConstCiphertext<Element>& ciphertext,
                                                  const std::vector<VectorDataType>& coefficients, double a,
                                                  double b) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalChebyshevSeriesLinear(ciphertext, coefficients, a, b);
    }

    template <typename VectorDataType = double>
    Ciphertext<Element> EvalChebyshevSeriesPS(ConstCiphertext<Element>& ciphertext,
                                              const std::vector<VectorDataType>& coefficients, double a,
                                              double b) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalChebyshevSeriesPS(ciphertext, coefficients, a, b);
    }

    Ciphertext<Element> EvalChebyshevFunction(std::function<double(double)> func, ConstCiphertext<Element>& ciphertext,
                                              double a, double b, uint32_t degree) const;

    Ciphertext<Element> EvalSin(ConstCiphertext<Element>& ciphertext, double a, double b, uint32_t degree) const;

    Ciphertext<Element> EvalCos(ConstCiphertext<Element>& ciphertext, double a, double b, uint32_t degree) const;

    Ciphertext<Element> EvalLogistic(ConstCiphertext<Element>& ciphertext, double a, double b, uint32_t degree) const;

    Ciphertext<Element> EvalDivide(ConstCiphertext<Element>& ciphertext, double a, double b, uint32_t degree) const;

    //------------------------------------------------------------------------------
    // Advanced SHE EVAL SUM
    //------------------------------------------------------------------------------

    void EvalSumKeyGen(const PrivateKey<Element> privateKey);

    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> EvalSumRowsKeyGen(const PrivateKey<Element> privateKey,
                                                                            uint32_t rowSize    = 0,
                                                                            uint32_t subringDim = 0);

    // TODO: this is here for backwards compatibility; should remove in v2.0
    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> EvalSumRowsKeyGen(const PrivateKey<Element> privateKey,
                                                                            const PublicKey<Element> publicKey,
                                                                            uint32_t rowSize    = 0,
                                                                            uint32_t subringDim = 0);

    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> EvalSumColsKeyGen(const PrivateKey<Element> privateKey);

    Ciphertext<Element> EvalSum(ConstCiphertext<Element>& ciphertext, uint32_t batchSize) const;

    Ciphertext<Element> EvalSumRows(ConstCiphertext<Element>& ciphertext, uint32_t numRows,
                                    const std::map<uint32_t, EvalKey<Element>>& evalSumKeyMap,
                                    uint32_t subringDim = 0) const;

    Ciphertext<Element> EvalSumCols(ConstCiphertext<Element>& ciphertext, uint32_t numCols,
                                    const std::map<uint32_t, EvalKey<Element>>& evalSumKeyMap) const;

    //------------------------------------------------------------------------------
    // Advanced SHE EVAL INNER PRODUCT
    //------------------------------------------------------------------------------

    Ciphertext<Element> EvalInnerProduct(ConstCiphertext<Element>& ciphertext1, ConstCiphertext<Element>& ciphertext2,
                                         uint32_t batchSize) const;

    Ciphertext<Element> EvalInnerProduct(ConstCiphertext<Element>& ciphertext, ConstPlaintext& plaintext,
                                         uint32_t batchSize) const;

    Ciphertext<Element> EvalMerge(const std::vector<Ciphertext<Element>>& ciphertextVec) const;

    //------------------------------------------------------------------------------
    // PRE Wrapper
    //------------------------------------------------------------------------------

    EvalKey<Element> ReKeyGen(const PrivateKey<Element> oldPrivateKey, const PublicKey<Element> newPublicKey) const {
        ValidateKey(oldPrivateKey);
        ValidateKey(newPublicKey);
        return GetScheme()->ReKeyGen(oldPrivateKey, newPublicKey);
    }

    EvalKey<Element> ReKeyGen(const PrivateKey<Element> originalPrivateKey,
                              const PrivateKey<Element> newPrivateKey) const
        __attribute__((deprecated("functionality removed from OpenFHE")));

    Ciphertext<Element> ReEncrypt(ConstCiphertext<Element>& ciphertext, EvalKey<Element> evalKey,
                                  const PublicKey<Element> publicKey = nullptr) const {
        ValidateCiphertext(ciphertext);
        ValidateKey(evalKey);
        return GetScheme()->ReEncrypt(ciphertext, evalKey, publicKey);
    }

    //------------------------------------------------------------------------------
    // Multiparty Wrapper
    //------------------------------------------------------------------------------

    KeyPair<Element> MultipartyKeyGen(const std::vector<PrivateKey<Element>>& privateKeyVec) {
        if (privateKeyVec.empty())
            OPENFHE_THROW("Input private key vector is empty");
        return GetScheme()->MultipartyKeyGen(GetContextForPointer(this), privateKeyVec, false);
    }

    KeyPair<Element> MultipartyKeyGen(const PublicKey<Element> publicKey, bool makeSparse = false, bool fresh = false) {
        if (!publicKey)
            OPENFHE_THROW("Input public key is empty");
        return GetScheme()->MultipartyKeyGen(GetContextForPointer(this), publicKey, makeSparse, fresh);
    }

    std::vector<Ciphertext<Element>> MultipartyDecryptLead(const std::vector<Ciphertext<Element>>& ciphertextVec,
                                                           const PrivateKey<Element> privateKey) const {
        ValidateKey(privateKey);
        std::vector<Ciphertext<Element>> newCiphertextVec;
        for (const auto& ciphertext : ciphertextVec) {
            ValidateCiphertext(ciphertext);
            newCiphertextVec.push_back(GetScheme()->MultipartyDecryptLead(ciphertext, privateKey));
        }
        return newCiphertextVec;
    }

    std::vector<Ciphertext<Element>> MultipartyDecryptMain(const std::vector<Ciphertext<Element>>& ciphertextVec,
                                                           const PrivateKey<Element> privateKey) const {
        ValidateKey(privateKey);
        std::vector<Ciphertext<Element>> newCiphertextVec;
        for (const auto& ciphertext : ciphertextVec) {
            ValidateCiphertext(ciphertext);
            newCiphertextVec.push_back(GetScheme()->MultipartyDecryptMain(ciphertext, privateKey));
        }
        return newCiphertextVec;
    }

    DecryptResult MultipartyDecryptFusion(const std::vector<Ciphertext<Element>>& partialCiphertextVec,
                                          Plaintext* plaintext) const {
        std::string datatype = demangle(typeid(Element).name());
        OPENFHE_THROW("Not implemented for " + datatype);
    }

    EvalKey<Element> MultiKeySwitchGen(const PrivateKey<Element> originalPrivateKey,
                                       const PrivateKey<Element> newPrivateKey, const EvalKey<Element> evalKey) const {
        if (!originalPrivateKey)
            OPENFHE_THROW("Input first private key is nullptr");
        if (!newPrivateKey)
            OPENFHE_THROW("Input second private key is nullptr");
        if (!evalKey)
            OPENFHE_THROW("Input evaluation key is nullptr");
        return GetScheme()->MultiKeySwitchGen(originalPrivateKey, newPrivateKey, evalKey);
    }

    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> MultiEvalAutomorphismKeyGen(
        const PrivateKey<Element> privateKey, const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> evalKeyMap,
        const std::vector<uint32_t>& indexList, const std::string& keyTag = "") {
        if (!privateKey)
            OPENFHE_THROW("Input private key is nullptr");
        if (!evalKeyMap)
            OPENFHE_THROW("Input evaluation key map is nullptr");
        if (indexList.empty())
            OPENFHE_THROW("Input index vector is empty");
        return GetScheme()->MultiEvalAutomorphismKeyGen(privateKey, evalKeyMap, indexList, keyTag);
    }

    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> MultiEvalAtIndexKeyGen(
        const PrivateKey<Element> privateKey, const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> evalKeyMap,
        const std::vector<int32_t>& indexList, const std::string& keyTag = "") {
        if (!privateKey)
            OPENFHE_THROW("Input private key is nullptr");
        if (!evalKeyMap)
            OPENFHE_THROW("Input evaluation key map is nullptr");
        if (indexList.empty())
            OPENFHE_THROW("Input index vector is empty");
        return GetScheme()->MultiEvalAtIndexKeyGen(privateKey, evalKeyMap, indexList, keyTag);
    }

    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> MultiEvalSumKeyGen(
        const PrivateKey<Element> privateKey, const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> evalKeyMap,
        const std::string& keyTag = "") {
        if (!privateKey)
            OPENFHE_THROW("Input private key is nullptr");
        if (!evalKeyMap)
            OPENFHE_THROW("Input evaluation key map is nullptr");
        return GetScheme()->MultiEvalSumKeyGen(privateKey, evalKeyMap, keyTag);
    }

    EvalKey<Element> MultiAddEvalKeys(EvalKey<Element> evalKey1, EvalKey<Element> evalKey2,
                                      const std::string& keyTag = "") {
        if (!evalKey1)
            OPENFHE_THROW("Input first evaluation key is nullptr");
        if (!evalKey2)
            OPENFHE_THROW("Input second evaluation key is nullptr");
        return GetScheme()->MultiAddEvalKeys(evalKey1, evalKey2, keyTag);
    }

    EvalKey<Element> MultiMultEvalKey(PrivateKey<Element> privateKey, EvalKey<Element> evalKey,
                                      const std::string& keyTag = "") {
        if (!privateKey)
            OPENFHE_THROW("Input private key is nullptr");
        if (!evalKey)
            OPENFHE_THROW("Input evaluation key is nullptr");
        return GetScheme()->MultiMultEvalKey(privateKey, evalKey, keyTag);
    }

    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> MultiAddEvalSumKeys(
        const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> evalKeyMap1,
        const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> evalKeyMap2, const std::string& keyTag = "") {
        if (!evalKeyMap1)
            OPENFHE_THROW("Input first evaluation key map is nullptr");
        if (!evalKeyMap2)
            OPENFHE_THROW("Input second evaluation key map is nullptr");
        return GetScheme()->MultiAddEvalSumKeys(evalKeyMap1, evalKeyMap2, keyTag);
    }

    std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> MultiAddEvalAutomorphismKeys(
        const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> evalKeyMap1,
        const std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> evalKeyMap2, const std::string& keyTag = "") {
        if (!evalKeyMap1)
            OPENFHE_THROW("Input first evaluation key map is nullptr");
        if (!evalKeyMap2)
            OPENFHE_THROW("Input second evaluation key map is nullptr");
        return GetScheme()->MultiAddEvalAutomorphismKeys(evalKeyMap1, evalKeyMap2, keyTag);
    }

    PublicKey<Element> MultiAddPubKeys(PublicKey<Element> publicKey1, PublicKey<Element> publicKey2,
                                       const std::string& keyTag = "") {
        if (!publicKey1)
            OPENFHE_THROW("Input first public key is nullptr");
        if (!publicKey2)
            OPENFHE_THROW("Input second public key is nullptr");
        return GetScheme()->MultiAddPubKeys(publicKey1, publicKey2, keyTag);
    }

    EvalKey<Element> MultiAddEvalMultKeys(EvalKey<Element> evalKey1, EvalKey<Element> evalKey2,
                                          const std::string& keyTag = "") {
        if (!evalKey1)
            OPENFHE_THROW("Input first evaluation key is nullptr");
        if (!evalKey2)
            OPENFHE_THROW("Input second evaluation key is nullptr");
        return GetScheme()->MultiAddEvalMultKeys(evalKey1, evalKey2, keyTag);
    }

    Ciphertext<Element> IntBootDecrypt(const PrivateKey<Element> privateKey,
                                       ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        ValidateKey(privateKey);
        return GetScheme()->IntBootDecrypt(privateKey, ciphertext);
    }

    Ciphertext<Element> IntBootEncrypt(const PublicKey<Element> publicKey, ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        ValidateKey(publicKey);
        return GetScheme()->IntBootEncrypt(publicKey, ciphertext);
    }

    Ciphertext<Element> IntBootAdd(ConstCiphertext<Element>& ciphertext1, ConstCiphertext<Element>& ciphertext2) const {
        ValidateCiphertext(ciphertext1);
        ValidateCiphertext(ciphertext2);
        return GetScheme()->IntBootAdd(ciphertext1, ciphertext2);
    }

    Ciphertext<Element> IntBootAdjustScale(ConstCiphertext<Element>& ciphertext) const {
        ValidateCiphertext(ciphertext);
        return GetScheme()->IntBootAdjustScale(ciphertext);
    }

    Ciphertext<Element> IntMPBootAdjustScale(ConstCiphertext<Element>& ciphertext) const;

    Ciphertext<Element> IntMPBootRandomElementGen(const PublicKey<Element> publicKey) const;
    Ciphertext<Element> IntMPBootRandomElementGen(ConstCiphertext<Element>& ciphertext) const;

    std::vector<Ciphertext<Element>> IntMPBootDecrypt(const PrivateKey<Element> privateKey,
                                                      ConstCiphertext<Element>& ciphertext,
                                                      ConstCiphertext<Element>& a) const;

    std::vector<Ciphertext<Element>> IntMPBootAdd(std::vector<std::vector<Ciphertext<Element>>>& sharesPairVec) const;

    Ciphertext<Element> IntMPBootEncrypt(const PublicKey<Element> publicKey,
                                         const std::vector<Ciphertext<Element>>& sharesPair,
                                         ConstCiphertext<Element>& a, ConstCiphertext<Element>& ciphertext) const;

    std::unordered_map<uint32_t, Element> ShareKeys(const PrivateKey<Element>& sk, uint32_t N, uint32_t threshold,
                                                    uint32_t index, const std::string& shareType) const {
        std::string datatype = demangle(typeid(Element).name());
        OPENFHE_THROW("Not implemented for " + datatype);
    }

    void RecoverSharedKey(PrivateKey<Element>& sk, std::unordered_map<uint32_t, Element>& sk_shares, uint32_t N,
                          uint32_t threshold, const std::string& shareType) const;

    //------------------------------------------------------------------------------
    // FHE Bootstrap Methods
    //------------------------------------------------------------------------------

    void EvalBootstrapSetup(std::vector<uint32_t> levelBudget = {5, 4}, std::vector<uint32_t> dim1 = {0, 0},
                            uint32_t slots = 0, uint32_t correctionFactor = 0, bool precompute = true,
                            bool BTSlotsEncoding = false) {
        GetScheme()->EvalBootstrapSetup(*this, levelBudget, dim1, slots, correctionFactor, precompute, BTSlotsEncoding);
    }

    void EvalBootstrapKeyGen(const PrivateKey<Element> privateKey, uint32_t slots) {
        ValidateKey(privateKey);
        auto evalKeys = GetScheme()->EvalBootstrapKeyGen(privateKey, slots);
        CryptoContextImpl<Element>::InsertEvalAutomorphismKey(evalKeys, privateKey->GetKeyTag());
    }

    void EvalBootstrapPrecompute(uint32_t slots = 0) {
        GetScheme()->EvalBootstrapPrecompute(*this, slots);
    }

    Ciphertext<Element> EvalBootstrap(ConstCiphertext<Element>& ciphertext, uint32_t numIterations = 1,
                                      uint32_t precision = 0) const {
        return GetScheme()->EvalBootstrap(ciphertext, numIterations, precision);
    }

    Ciphertext<Element> EvalBootstrapStCFirst(ConstCiphertext<Element>& ciphertext, uint32_t numIterations = 1,
                                              uint32_t precision = 0) const {
        return GetScheme()->EvalBootstrapStCFirst(ciphertext, numIterations, precision);
    }

    template <typename VectorDataType>
    void EvalFBTSetup(const std::vector<VectorDataType>& coeffs, uint32_t numSlots, const BigInteger& PIn,
                      const BigInteger& POut, const BigInteger& Bigq, const PublicKey<DCRTPoly>& pubKey,
                      const std::vector<uint32_t>& dim1, const std::vector<uint32_t>& levelBudget,
                      uint32_t lvlsAfterBoot = 0, uint32_t depthLeveledComputation = 0, size_t order = 1) {
        GetScheme()->EvalFBTSetup(*this, coeffs, numSlots, PIn, POut, Bigq, pubKey, dim1, levelBudget, lvlsAfterBoot,
                                  depthLeveledComputation, order);
    }

    template <typename VectorDataType>
    Ciphertext<Element> EvalFBT(ConstCiphertext<Element>& ciphertext, const std::vector<VectorDataType>& coeffs,
                                uint32_t digitBitSize, const BigInteger& initialScaling, uint64_t postScaling,
                                uint32_t levelToReduce = 0, size_t order = 1) {
        return GetScheme()->EvalFBT(ciphertext, coeffs, digitBitSize, initialScaling, postScaling, levelToReduce,
                                    order);
    }

    template <typename VectorDataType>
    Ciphertext<Element> EvalFBTNoDecoding(ConstCiphertext<Element>& ciphertext,
                                          const std::vector<VectorDataType>& coeffs, uint32_t digitBitSize,
                                          const BigInteger& initialScaling, size_t order = 1) {
        return GetScheme()->EvalFBTNoDecoding(ciphertext, coeffs, digitBitSize, initialScaling, order);
    }

    Ciphertext<Element> EvalHomDecoding(ConstCiphertext<Element>& ciphertext, uint64_t postScaling,
                                        uint32_t levelToReduce = 0) {
        return GetScheme()->EvalHomDecoding(ciphertext, postScaling, levelToReduce);
    }

    template <typename VectorDataType>
    std::shared_ptr<seriesPowers<Element>> EvalMVBPrecompute(ConstCiphertext<Element>& ciphertext,
                                                             const std::vector<VectorDataType>& coeffs,
                                                             uint32_t digitBitSize, const BigInteger& initialScaling,
                                                             size_t order = 1) {
        return GetScheme()->EvalMVBPrecompute(ciphertext, coeffs, digitBitSize, initialScaling, order);
    }

    template <typename VectorDataType>
    Ciphertext<Element> EvalMVB(const std::shared_ptr<seriesPowers<Element>> ciphertexts,
                                const std::vector<VectorDataType>& coeffs, uint32_t digitBitSize,
                                const uint64_t postScaling, uint32_t levelToReduce = 0, size_t order = 1) {
        return GetScheme()->EvalMVB(ciphertexts, coeffs, digitBitSize, postScaling, levelToReduce, order);
    }

    template <typename VectorDataType>
    Ciphertext<Element> EvalMVBNoDecoding(const std::shared_ptr<seriesPowers<Element>> ciphertexts,
                                          const std::vector<VectorDataType>& coeffs, uint32_t digitBitSize,
                                          size_t order = 1) {
        return GetScheme()->EvalMVBNoDecoding(ciphertexts, coeffs, digitBitSize, order);
    }

    template <typename VectorDataType>
    Ciphertext<Element> EvalHermiteTrigSeries(ConstCiphertext<Element>& ciphertext,
                                              const std::vector<std::complex<double>>& coefficientsCheb, double a,
                                              double b, const std::vector<VectorDataType>& coefficientsHerm,
                                              size_t precomp = 0) {
        return GetScheme()->EvalHermiteTrigSeries(ciphertext, coefficientsCheb, a, b, coefficientsHerm, precomp);
    }

    uint32_t GetCKKSBootCorrectionFactor() {
        return GetScheme()->GetCKKSBootCorrectionFactor();
    }

    void SetCKKSBootCorrectionFactor(uint32_t cf) {
        return GetScheme()->SetCKKSBootCorrectionFactor(cf);
    }

    //------------------------------------------------------------------------------
    // Scheme switching Methods
    //------------------------------------------------------------------------------

    LWEPrivateKey EvalCKKStoFHEWSetup(SchSwchParams params) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        SetParamsFromCKKSCryptocontext(params);
        return GetScheme()->EvalCKKStoFHEWSetup(params);
    }

    void EvalCKKStoFHEWKeyGen(const KeyPair<Element>& keyPair, ConstLWEPrivateKey& lwesk) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        ValidateKey(keyPair.secretKey);
        if (!lwesk)
            OPENFHE_THROW("FHEW private key passed to EvalCKKStoFHEWKeyGen is null");

        auto evalKeys = GetScheme()->EvalCKKStoFHEWKeyGen(keyPair, lwesk);
        CryptoContextImpl<Element>::InsertEvalAutomorphismKey(evalKeys, keyPair.secretKey->GetKeyTag());
    }

    void EvalCKKStoFHEWPrecompute(double scale = 1.0) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        GetScheme()->EvalCKKStoFHEWPrecompute(*this, scale);
    }

    std::vector<std::shared_ptr<LWECiphertextImpl>> EvalCKKStoFHEW(ConstCiphertext<Element>& ciphertext,
                                                                   uint32_t numCtxts = 0) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        if (ciphertext == nullptr)
            OPENFHE_THROW("ciphertext passed to EvalCKKStoFHEW is empty");

        return GetScheme()->EvalCKKStoFHEW(ciphertext, numCtxts);
    }

    void EvalFHEWtoCKKSSetup(const std::shared_ptr<BinFHEContext>& ccLWE, uint32_t numSlotsCKKS = 0,
                             uint32_t logQ = 25) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        GetScheme()->EvalFHEWtoCKKSSetup(*this, ccLWE, numSlotsCKKS, logQ);
    }

    void EvalFHEWtoCKKSKeyGen(const KeyPair<Element>& keyPair, ConstLWEPrivateKey& lwesk, uint32_t numSlots = 0,
                              uint32_t numCtxts = 0, uint32_t dim1 = 0, uint32_t L = 0) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        ValidateKey(keyPair.secretKey);

        auto evalKeys = GetScheme()->EvalFHEWtoCKKSKeyGen(keyPair, lwesk, numSlots, numCtxts, dim1, L);
        CryptoContextImpl<Element>::InsertEvalAutomorphismKey(evalKeys, keyPair.secretKey->GetKeyTag());
    }

    Ciphertext<Element> EvalFHEWtoCKKS(std::vector<std::shared_ptr<LWECiphertextImpl>>& LWECiphertexts,
                                       uint32_t numCtxts = 0, uint32_t numSlots = 0, uint32_t p = 4, double pmin = 0.0,
                                       double pmax = 2.0, uint32_t dim1 = 0) const {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        return GetScheme()->EvalFHEWtoCKKS(LWECiphertexts, numCtxts, numSlots, p, pmin, pmax, dim1);
    }

    void SetParamsFromCKKSCryptocontext(SchSwchParams& params) {
        const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersCKKSRNS>(GetCryptoParameters());
        if (!cryptoParams)
            OPENFHE_THROW("std::dynamic_pointer_cast<CryptoParametersCKKSRNS>() failed");
        params.SetInitialCKKSModulus(cryptoParams->GetElementParams()->GetParams()[0]->GetModulus());
        params.SetRingDimension(GetRingDimension());
        // TODO (dsuponit): is this correct - PlaintextModulus used as scalingModSize?
        params.SetScalingModSize(GetEncodingParams()->GetPlaintextModulus());
        params.SetBatchSize(GetEncodingParams()->GetBatchSize());
        params.SetParamsFromCKKSCryptocontextCalled();
    }

    LWEPrivateKey EvalSchemeSwitchingSetup(SchSwchParams& params) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        SetParamsFromCKKSCryptocontext(params);
        return GetScheme()->EvalSchemeSwitchingSetup(params);
    }

    void EvalSchemeSwitchingKeyGen(const KeyPair<Element>& keyPair, ConstLWEPrivateKey& lwesk) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        ValidateKey(keyPair.secretKey);

        auto evalKeys = GetScheme()->EvalSchemeSwitchingKeyGen(keyPair, lwesk);
        CryptoContextImpl<Element>::InsertEvalAutomorphismKey(evalKeys, keyPair.secretKey->GetKeyTag());
    }

    void EvalCompareSwitchPrecompute(uint32_t pLWE = 0, double scaleSign = 1.0, bool unit = false) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        GetScheme()->EvalCompareSwitchPrecompute(*this, pLWE, scaleSign, unit);
    }

    Ciphertext<Element> EvalCompareSchemeSwitching(ConstCiphertext<Element>& ciphertext1,
                                                   ConstCiphertext<Element>& ciphertext2, uint32_t numCtxts = 0,
                                                   uint32_t numSlots = 0, uint32_t pLWE = 0, double scaleSign = 1.0,
                                                   bool unit = false) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        ValidateCiphertext(ciphertext1);
        ValidateCiphertext(ciphertext2);
        return GetScheme()->EvalCompareSchemeSwitching(ciphertext1, ciphertext2, numCtxts, numSlots, pLWE, scaleSign,
                                                       unit);
    }

    std::vector<Ciphertext<Element>> EvalMinSchemeSwitching(ConstCiphertext<Element>& ciphertext,
                                                            PublicKey<Element>& publicKey, uint32_t numValues = 0,
                                                            uint32_t numSlots = 0, uint32_t pLWE = 0,
                                                            double scaleSign = 1.0) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalMinSchemeSwitching(ciphertext, publicKey, numValues, numSlots, pLWE, scaleSign);
    }

    std::vector<Ciphertext<Element>> EvalMinSchemeSwitchingAlt(ConstCiphertext<Element>& ciphertext,
                                                               PublicKey<Element>& publicKey, uint32_t numValues = 0,
                                                               uint32_t numSlots = 0, uint32_t pLWE = 0,
                                                               double scaleSign = 1.0) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalMinSchemeSwitchingAlt(ciphertext, publicKey, numValues, numSlots, pLWE, scaleSign);
    }

    std::vector<Ciphertext<Element>> EvalMaxSchemeSwitching(ConstCiphertext<Element>& ciphertext,
                                                            PublicKey<Element>& publicKey, uint32_t numValues = 0,
                                                            uint32_t numSlots = 0, uint32_t pLWE = 0,
                                                            double scaleSign = 1.0) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalMaxSchemeSwitching(ciphertext, publicKey, numValues, numSlots, pLWE, scaleSign);
    }

    std::vector<Ciphertext<Element>> EvalMaxSchemeSwitchingAlt(ConstCiphertext<Element>& ciphertext,
                                                               PublicKey<Element>& publicKey, uint32_t numValues = 0,
                                                               uint32_t numSlots = 0, uint32_t pLWE = 0,
                                                               double scaleSign = 1.0) {
        VerifyCKKSScheme(__func__);
        VerifyCKKSRealDataType(__func__);
        ValidateCiphertext(ciphertext);
        return GetScheme()->EvalMaxSchemeSwitchingAlt(ciphertext, publicKey, numValues, numSlots, pLWE, scaleSign);
    }

    std::shared_ptr<lbcrypto::BinFHEContext> GetBinCCForSchemeSwitch() const {
        return GetScheme()->GetBinCCForSchemeSwitch();
    }

    void SetBinCCForSchemeSwitch(std::shared_ptr<lbcrypto::BinFHEContext> ccLWE) {
        GetScheme()->SetBinCCForSchemeSwitch(ccLWE);
    }

    Ciphertext<Element> GetSwkFC() const {
        return GetScheme()->GetSwkFC();
    }

    void SetSwkFC(Ciphertext<Element> FHEWtoCKKSswk) {
        GetScheme()->SetSwkFC(FHEWtoCKKSswk);
    }

    static std::set<uint32_t> GetEvalAutomorphismNoKeyIndices(const std::string& keyTag,
                                                              const std::set<uint32_t>& indices) {
        std::set<uint32_t> existingIndices{CryptoContextImpl<Element>::GetExistingEvalAutomorphismKeyIndices(keyTag)};
        // if no index found for the given keyTag, then the entire set "indices" is returned
        return (existingIndices.empty()) ? indices :
                                           CryptoContextImpl<Element>::GetUniqueValues(existingIndices, indices);
    }

    static std::set<uint32_t> GetExistingEvalAutomorphismKeyIndices(const std::string& keyTag);

    static std::set<uint32_t> GetUniqueValues(const std::set<uint32_t>& oldValues, const std::set<uint32_t>& newValues);

    template <class Archive>
    void save(Archive& ar, std::uint32_t const version) const {
        ar(cereal::make_nvp("cc", m_params));
        ar(cereal::make_nvp("kt", m_scheme));
        ar(cereal::make_nvp("si", m_schemeId));
    }

    template <class Archive>
    void load(Archive& ar, std::uint32_t const version) {
        if (version > CryptoContextImpl<Element>::SerializedVersion()) {
            OPENFHE_THROW("serialized object version " + std::to_string(version) +
                          " is from a later version of the library");
        }
        ar(cereal::make_nvp("cc", m_params));
        ar(cereal::make_nvp("kt", m_scheme));
        ar(cereal::make_nvp("si", m_schemeId));
        SetKSTechniqueInScheme();

        // NOTE: a pointer to this object will be wrapped in a shared_ptr, and is a
        // "CryptoContext". OpenFHE relies on the notion that identical
        // CryptoContextImpls are not duplicated in memory Once we deserialize this
        // object, we must check to see if there is a matching object for this
        // object that's already existing in memory if it DOES exist, use it. If it
        // does NOT exist, add this to the cache of all contexts
    }

    std::string SerializedObjectName() const override {
        return "CryptoContext";
    }

    static uint32_t SerializedVersion() {
        return 1;
    }
};

// Member function specializations. Their implementations are in cryptocontext.cpp
template <>
DecryptResult CryptoContextImpl<DCRTPoly>::MultipartyDecryptFusion(
    const std::vector<Ciphertext<DCRTPoly>>& partialCiphertextVec, Plaintext* plaintext) const;
template <>
std::unordered_map<uint32_t, DCRTPoly> CryptoContextImpl<DCRTPoly>::ShareKeys(const PrivateKey<DCRTPoly>& sk,
                                                                              uint32_t N, uint32_t threshold,
                                                                              uint32_t index,
                                                                              const std::string& shareType) const;
}  // namespace lbcrypto

#endif  // __CRYPTOCONTEXT_H__