// ---------------------------------------------------------------------
//
// Copyright (c) 2014 - 2022 by the IBAMR developers
// All rights reserved.
//
// This file is part of IBAMR.
//
// IBAMR is free software and is distributed under the 3-clause BSD
// license. The full text of the license can be found in the file
// COPYRIGHT at the top level directory of IBAMR.
//
// ---------------------------------------------------------------------

/////////////////////////////// INCLUDES /////////////////////////////////////

#include "ibamr/IBMethod.h"
#include "ibamr/PenaltyIBMethod.h"

#include "ibtk/IBTK_CHKERRQ.h"
#include "ibtk/IBTK_MPI.h"
#include "ibtk/LData.h"
#include "ibtk/LDataManager.h"
#include "ibtk/LInitStrategy.h"
#include "ibtk/LSiloDataWriter.h"
#include "ibtk/ibtk_utilities.h"

#include "BasePatchHierarchy.h"
#include "BasePatchLevel.h"
#include "GriddingAlgorithm.h"
#include "IntVector.h"
#include "PatchHierarchy.h"
#include "tbox/Database.h"
#include "tbox/MathUtilities.h"
#include "tbox/PIO.h"
#include "tbox/Pointer.h"
#include "tbox/RestartManager.h"
#include "tbox/Utilities.h"

#include "petscvec.h"

#include "ibamr/namespaces.h" // IWYU pragma: keep

IBTK_DISABLE_EXTRA_WARNINGS
#include <boost/multi_array.hpp>
IBTK_ENABLE_EXTRA_WARNINGS

#include <algorithm>
#include <cmath>
#include <ostream>
#include <string>
#include <utility>
#include <vector>

namespace SAMRAI
{
namespace xfer
{
template <int DIM>
class CoarsenSchedule;
template <int DIM>
class RefineSchedule;
} // namespace xfer
} // namespace SAMRAI

/////////////////////////////// NAMESPACE ////////////////////////////////////

namespace IBAMR
{
/////////////////////////////// STATIC ///////////////////////////////////////

namespace
{
// Version of PenaltyIBMethod restart file data.
static const int PENALTY_IB_METHOD_VERSION = 1;
} // namespace

/////////////////////////////// PUBLIC ///////////////////////////////////////

PenaltyIBMethod::PenaltyIBMethod(std::string object_name, Pointer<Database> input_db, bool register_for_restart)
    : IBMethod(std::move(object_name), input_db, register_for_restart)
{
    // NOTE: Parent class constructor registers class with the restart manager, sets object
    // name.

    // Initialize object with data read from the input and restart databases.
    bool from_restart = RestartManager::getManager()->isFromRestart();
    if (from_restart) getFromRestart();
    if (input_db) getFromInput(input_db, from_restart);
    return;
} // PenaltyIBMethod

void
PenaltyIBMethod::preprocessIntegrateData(double current_time, double new_time, int num_cycles)
{
    IBMethod::preprocessIntegrateData(current_time, new_time, num_cycles);

    const int coarsest_ln = 0;
    const int finest_ln = d_hierarchy->getFinestLevelNumber();

    // Look-up or allocate Lagangian data.
    d_K_data.resize(finest_ln + 1);
    d_M_data.resize(finest_ln + 1);
    d_Y_current_data.resize(finest_ln + 1);
    d_Y_new_data.resize(finest_ln + 1);
    d_V_current_data.resize(finest_ln + 1);
    d_V_new_data.resize(finest_ln + 1);
    for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
    {
        if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;
        d_K_data[ln] = d_l_data_manager->getLData("K", ln);
        d_M_data[ln] = d_l_data_manager->getLData("M", ln);
        d_Y_current_data[ln] = d_l_data_manager->getLData("Y", ln);
        d_Y_new_data[ln] = d_l_data_manager->createLData("Y_new", ln, NDIM);
        d_V_current_data[ln] = d_l_data_manager->getLData("V", ln);
        d_V_new_data[ln] = d_l_data_manager->createLData("V_new", ln, NDIM);

        // Initialize Y^{n+1} and V^{n+1} to equal Y^{n} and V^{n}.
        int ierr;
        ierr = VecCopy(d_Y_current_data[ln]->getVec(), d_Y_new_data[ln]->getVec());
        IBTK_CHKERRQ(ierr);
        ierr = VecCopy(d_V_current_data[ln]->getVec(), d_V_new_data[ln]->getVec());
        IBTK_CHKERRQ(ierr);
    }
    return;
} // preprocessIntegrateData

void
PenaltyIBMethod::postprocessIntegrateData(double current_time, double new_time, int num_cycles)
{
    IBMethod::postprocessIntegrateData(current_time, new_time, num_cycles);

    const int coarsest_ln = 0;
    const int finest_ln = d_hierarchy->getFinestLevelNumber();

    // Reset time-dependent Lagrangian data.
    for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
    {
        if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;
        int ierr;
        ierr = VecSwap(d_Y_current_data[ln]->getVec(), d_Y_new_data[ln]->getVec());
        IBTK_CHKERRQ(ierr);
        ierr = VecSwap(d_V_current_data[ln]->getVec(), d_V_new_data[ln]->getVec());
        IBTK_CHKERRQ(ierr);
    }

    // Deallocate Lagrangian scratch data.
    d_K_data.clear();
    d_M_data.clear();
    d_Y_current_data.clear();
    d_Y_new_data.clear();
    d_V_current_data.clear();
    d_V_new_data.clear();
    return;
} // postprocessIntegrateData

void
PenaltyIBMethod::forwardEulerStep(const double current_time, const double new_time)
{
    IBMethod::forwardEulerStep(current_time, new_time);

    const int coarsest_ln = 0;
    const int finest_ln = d_hierarchy->getFinestLevelNumber();
    const double dt = new_time - current_time;

    // Update the values of Y^{n+1} and V^{n+1} using forward Euler.
    for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
    {
        if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;
        const double* const K = d_K_data[ln]->getLocalFormArray()->data();
        const double* const M = d_M_data[ln]->getLocalFormArray()->data();
        const double* const X = d_X_current_data[ln]->getLocalFormVecArray()->data();
        const double* const Y = d_Y_current_data[ln]->getLocalFormVecArray()->data();
        const double* const V = d_V_current_data[ln]->getLocalFormVecArray()->data();
        double* const Y_new = d_Y_new_data[ln]->getLocalFormVecArray()->data();
        double* const V_new = d_V_new_data[ln]->getLocalFormVecArray()->data();
        const unsigned int n_local = d_X_current_data[ln]->getLocalNodeCount();
        unsigned int i, d;
        for (i = 0; i < n_local; ++i)
        {
            for (d = 0; d < NDIM; ++d)
            {
                Y_new[NDIM * i + d] = Y[NDIM * i + d] + dt * V[NDIM * i + d];
                V_new[NDIM * i + d] = V[NDIM * i + d] + dt * (-K[i] * (Y[NDIM * i + d] - X[NDIM * i + d]) / M[i] +
                                                              d_gravitational_acceleration[d]);
            }
        }
    }
    return;
} // eulerStep

void
PenaltyIBMethod::midpointStep(const double current_time, const double new_time)
{
    IBMethod::midpointStep(current_time, new_time);

    const int coarsest_ln = 0;
    const int finest_ln = d_hierarchy->getFinestLevelNumber();
    const double dt = new_time - current_time;

    // Update the values of Y^{n+1} and V^{n+1} using the midpoint rule.
    for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
    {
        if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;
        const double* const K = d_K_data[ln]->getLocalFormArray()->data();
        const double* const M = d_M_data[ln]->getLocalFormArray()->data();
        const double* const X = d_X_current_data[ln]->getLocalFormVecArray()->data();
        const double* const Y = d_Y_current_data[ln]->getLocalFormVecArray()->data();
        const double* const V = d_V_current_data[ln]->getLocalFormVecArray()->data();
        const double* const X_new = d_X_new_data[ln]->getLocalFormVecArray()->data();
        double* const Y_new = d_Y_new_data[ln]->getLocalFormVecArray()->data();
        double* const V_new = d_V_new_data[ln]->getLocalFormVecArray()->data();
        const unsigned int n_local = d_X_current_data[ln]->getLocalNodeCount();
        unsigned int i, d;
        double X_half, Y_half, V_half;
        for (i = 0; i < n_local; ++i)
        {
            for (d = 0; d < NDIM; ++d)
            {
                X_half = 0.5 * (X[NDIM * i + d] + X_new[NDIM * i + d]);
                Y_half = 0.5 * (Y[NDIM * i + d] + Y_new[NDIM * i + d]);
                V_half = 0.5 * (V[NDIM * i + d] + V_new[NDIM * i + d]);
                Y_new[NDIM * i + d] = Y[NDIM * i + d] + dt * V_half;
                V_new[NDIM * i + d] =
                    V[NDIM * i + d] + dt * (-K[i] * (Y_half - X_half) / M[i] + d_gravitational_acceleration[d]);
            }
        }
    }
    return;
} // midpointStep

void
PenaltyIBMethod::trapezoidalStep(const double current_time, const double new_time)
{
    IBMethod::trapezoidalStep(current_time, new_time);

    const int coarsest_ln = 0;
    const int finest_ln = d_hierarchy->getFinestLevelNumber();
    const double dt = new_time - current_time;

    // Update the values of Y^{n+1} and V^{n+1} using the trapezoidal rule.
    for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
    {
        if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;
        const double* const K = d_K_data[ln]->getLocalFormArray()->data();
        const double* const M = d_M_data[ln]->getLocalFormArray()->data();
        const double* const X = d_X_current_data[ln]->getLocalFormVecArray()->data();
        const double* const Y = d_Y_current_data[ln]->getLocalFormVecArray()->data();
        const double* const V = d_V_current_data[ln]->getLocalFormVecArray()->data();
        const double* const X_new = d_X_new_data[ln]->getLocalFormVecArray()->data();
        double* const Y_new = d_Y_new_data[ln]->getLocalFormVecArray()->data();
        double* const V_new = d_V_new_data[ln]->getLocalFormVecArray()->data();
        const unsigned int n_local = d_X_current_data[ln]->getLocalNodeCount();
        unsigned int i, d;
        double X_half, Y_half, V_half;
        for (i = 0; i < n_local; ++i)
        {
            for (d = 0; d < NDIM; ++d)
            {
                X_half = 0.5 * (X[NDIM * i + d] + X_new[NDIM * i + d]);
                Y_half = 0.5 * (Y[NDIM * i + d] + Y_new[NDIM * i + d]);
                V_half = 0.5 * (V[NDIM * i + d] + V_new[NDIM * i + d]);
                Y_new[NDIM * i + d] = Y[NDIM * i + d] + dt * V_half;
                V_new[NDIM * i + d] =
                    V[NDIM * i + d] + dt * (-K[i] * (Y_half - X_half) / M[i] + d_gravitational_acceleration[d]);
            }
        }
    }
    return;
} // trapezoidalStep

void
PenaltyIBMethod::computeLagrangianForce(const double data_time)
{
    IBMethod::computeLagrangianForce(data_time);

    const int coarsest_ln = 0;
    const int finest_ln = d_hierarchy->getFinestLevelNumber();

    double max_displacement = 0.0;

    if (IBTK::rel_equal_eps(data_time, d_current_time))
    {
        for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
        {
            if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;
            double* const F = d_F_current_data[ln]->getLocalFormVecArray()->data();
            const double* const K = d_K_data[ln]->getLocalFormArray()->data();
            const double* const X = d_X_current_data[ln]->getLocalFormVecArray()->data();
            const double* const Y = d_Y_current_data[ln]->getLocalFormVecArray()->data();
            const unsigned int n_local = d_X_current_data[ln]->getLocalNodeCount();
            for (unsigned int i = 0; i < n_local; ++i)
            {
                double dX = 0.0;
                for (unsigned int d = 0; d < NDIM; ++d)
                {
                    F[NDIM * i + d] += K[i] * (Y[NDIM * i + d] - X[NDIM * i + d]);
                    dX += (Y[NDIM * i + d] - X[NDIM * i + d]) * (Y[NDIM * i + d] - X[NDIM * i + d]);
                }
                dX = std::sqrt(dX);
                max_displacement = std::max(max_displacement, dX);
            }
        }
        d_F_current_needs_ghost_fill = true;
    }
    else if (IBTK::rel_equal_eps(data_time, d_half_time))
    {
        for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
        {
            if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;
            double* const F = d_F_half_data[ln]->getLocalFormVecArray()->data();
            const double* const K = d_K_data[ln]->getLocalFormArray()->data();
            const double* const X = d_X_current_data[ln]->getLocalFormVecArray()->data();
            const double* const Y = d_Y_current_data[ln]->getLocalFormVecArray()->data();
            const double* const X_new = d_X_new_data[ln]->getLocalFormVecArray()->data();
            const double* const Y_new = d_Y_new_data[ln]->getLocalFormVecArray()->data();
            const unsigned int n_local = d_X_current_data[ln]->getLocalNodeCount();
            for (unsigned int i = 0; i < n_local; ++i)
            {
                double dX = 0.0;
                for (unsigned int d = 0; d < NDIM; ++d)
                {
                    const double X_half = 0.5 * (X[NDIM * i + d] + X_new[NDIM * i + d]);
                    const double Y_half = 0.5 * (Y[NDIM * i + d] + Y_new[NDIM * i + d]);
                    F[NDIM * i + d] += K[i] * (Y_half - X_half);
                    dX += (Y_half - X_half) * (Y_half - X_half);
                }
                dX = std::sqrt(dX);
                max_displacement = std::max(max_displacement, dX);
            }
        }
        d_F_half_needs_ghost_fill = true;
    }
    else if (IBTK::rel_equal_eps(data_time, d_new_time))
    {
        for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
        {
            if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;
            double* const F = d_F_new_data[ln]->getLocalFormVecArray()->data();
            const double* const K = d_K_data[ln]->getLocalFormArray()->data();
            const double* const X = d_X_new_data[ln]->getLocalFormVecArray()->data();
            const double* const Y = d_Y_new_data[ln]->getLocalFormVecArray()->data();
            const unsigned int n_local = d_X_current_data[ln]->getLocalNodeCount();
            for (unsigned int i = 0; i < n_local; ++i)
            {
                double dX = 0.0;
                for (unsigned int d = 0; d < NDIM; ++d)
                {
                    F[NDIM * i + d] += K[i] * (Y[NDIM * i + d] - X[NDIM * i + d]);
                    dX += (Y[NDIM * i + d] - X[NDIM * i + d]) * (Y[NDIM * i + d] - X[NDIM * i + d]);
                }
                dX = std::sqrt(dX);
                max_displacement = std::max(max_displacement, dX);
            }
        }
        d_F_new_needs_ghost_fill = true;
    }

    if (d_do_log)
    {
        max_displacement = IBTK_MPI::maxReduction(max_displacement);
        plog << d_object_name << "::computeLagrangianForce(): maximum point displacement: " << max_displacement << "\n";
    }
    return;
} // computeLagrangianForce

void
PenaltyIBMethod::initializePatchHierarchy(Pointer<PatchHierarchy<NDIM> > hierarchy,
                                          Pointer<GriddingAlgorithm<NDIM> > gridding_alg,
                                          int u_data_idx,
                                          const std::vector<Pointer<CoarsenSchedule<NDIM> > >& u_synch_scheds,
                                          const std::vector<Pointer<RefineSchedule<NDIM> > >& u_ghost_fill_scheds,
                                          int integrator_step,
                                          double init_data_time,
                                          bool initial_time)
{
    IBMethod::initializePatchHierarchy(hierarchy,
                                       gridding_alg,
                                       u_data_idx,
                                       u_synch_scheds,
                                       u_ghost_fill_scheds,
                                       integrator_step,
                                       init_data_time,
                                       initial_time);

    // Initialize velocity and position data objects for massive points.
    if (initial_time)
    {
        const int coarsest_ln = 0;
        const int finest_ln = d_hierarchy->getFinestLevelNumber();
        for (int ln = coarsest_ln; ln <= finest_ln; ++ln)
        {
            if (!d_l_data_manager->levelContainsLagrangianData(ln)) continue;

            Pointer<LData> X_data = d_l_data_manager->getLData(LDataManager::POSN_DATA_NAME, ln);
            Pointer<LData> U_data = d_l_data_manager->getLData(LDataManager::VEL_DATA_NAME, ln);
            Pointer<LData> Y_data = d_l_data_manager->createLData("Y", ln, NDIM, /*manage_data*/ true);
            Pointer<LData> V_data = d_l_data_manager->createLData("V", ln, NDIM, /*manage_data*/ true);

            if (d_silo_writer)
            {
                d_silo_writer->registerVariableData("Y", Y_data, ln);
            }

            // Set initial conditions.
            int ierr;
            ierr = VecCopy(X_data->getVec(), Y_data->getVec());
            IBTK_CHKERRQ(ierr);
            ierr = VecCopy(U_data->getVec(), V_data->getVec());
            IBTK_CHKERRQ(ierr);
        }
    }
    return;
} // initializePatchHierarchy

void
PenaltyIBMethod::initializeLevelData(Pointer<BasePatchHierarchy<NDIM> > hierarchy,
                                     int level_number,
                                     double init_data_time,
                                     bool can_be_refined,
                                     bool initial_time,
                                     Pointer<BasePatchLevel<NDIM> > old_level,
                                     bool allocate_data)
{
    IBMethod::initializeLevelData(
        hierarchy, level_number, init_data_time, can_be_refined, initial_time, old_level, allocate_data);

    if (initial_time && d_l_data_manager->levelContainsLagrangianData(level_number))
    {
        // Initialize Mass and Spring constant data.
        // Position and Velocity will be copied later.
        Pointer<LData> M_data = d_l_data_manager->createLData("M",
                                                              level_number,
                                                              1,
                                                              /*manage_data*/ true);
        Pointer<LData> K_data = d_l_data_manager->createLData("K",
                                                              level_number,
                                                              1,
                                                              /*manage_data*/ true);
        static const int global_index_offset = 0;
        static const int local_index_offset = 0;
        d_l_initializer->initializeMassDataOnPatchLevel(global_index_offset,
                                                        local_index_offset,
                                                        M_data,
                                                        K_data,
                                                        hierarchy,
                                                        level_number,
                                                        init_data_time,
                                                        can_be_refined,
                                                        initial_time,
                                                        d_l_data_manager);

        if (d_silo_writer)
        {
            d_silo_writer->registerVariableData("M", M_data, level_number);
        }
    }
    return;
}

void
PenaltyIBMethod::putToDatabase(Pointer<Database> db)
{
    IBMethod::putToDatabase(db);

    db->putInteger("PENALTY_IB_METHOD_VERSION", PENALTY_IB_METHOD_VERSION);
    db->putDoubleArray("d_gravitational_acceleration", &d_gravitational_acceleration[0], NDIM);
    return;
} // putToDatabase

/////////////////////////////// PROTECTED ////////////////////////////////////

/////////////////////////////// PRIVATE //////////////////////////////////////

void
PenaltyIBMethod::getFromInput(Pointer<Database> db, bool is_from_restart)
{
    if (!is_from_restart)
    {
        if (db->keyExists("gravitational_acceleration"))
        {
            db->getDoubleArray("gravitational_acceleration", &d_gravitational_acceleration[0], NDIM);
        }
        else
        {
            TBOX_WARNING(d_object_name << ":  "
                                       << "Using penalty-IB method but key data "
                                          "`gravitational_acceleration' not found in input.");
        }
    }
    return;
} // getFromInput

void
PenaltyIBMethod::getFromRestart()
{
    Pointer<Database> restart_db = RestartManager::getManager()->getRootDatabase();
    Pointer<Database> db;
    if (restart_db->isDatabase(d_object_name))
    {
        db = restart_db->getDatabase(d_object_name);
    }
    else
    {
        TBOX_ERROR(d_object_name << ":  Restart database corresponding to " << d_object_name
                                 << " not found in restart file." << std::endl);
    }
    int ver = db->getInteger("PENALTY_IB_METHOD_VERSION");
    if (ver != PENALTY_IB_METHOD_VERSION)
    {
        TBOX_ERROR(d_object_name << ":  Restart file version different than class version." << std::endl);
    }
    db->getDoubleArray("d_gravitational_acceleration", &d_gravitational_acceleration[0], NDIM);
    return;
} // getFromRestart

/////////////////////////////// NAMESPACE ////////////////////////////////////

} // namespace IBAMR

//////////////////////////////////////////////////////////////////////////////


// This file is part of the dune-gdt project:
//   http://users.dune-project.org/projects/dune-gdt
// Copyright holders: Felix Schindler
// License: BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)

#ifndef DUNE_GDT_OPERATORS_OSWALD_HH
#define DUNE_GDT_OPERATORS_OSWALD_HH

#include <vector>
#include <set>
#include <limits>

#include <boost/numeric/conversion/cast.hpp>

#include <dune/stuff/aliases.hh>
#include <dune/stuff/common/vector.hh>
#include <dune/stuff/common/float_cmp.hh>
#include <dune/stuff/common/print.hh>
#include <dune/stuff/common/ranges.hh>
#include <dune/stuff/grid/walker.hh>

#include <dune/gdt/discretefunction/default.hh>
#include <dune/gdt/playground/spaces/dg/fem.hh>
#include <dune/gdt/playground/spaces/block.hh>

#include "interfaces.hh"

namespace Dune {
namespace GDT {
namespace Operators {

// forward
template< class GridViewImp, class FieldImp = double >
class OswaldInterpolation;

namespace internal {

template< class GridViewImp, class FieldImp >
class OswaldInterpolationTraits
{
public:
  typedef OswaldInterpolation< GridViewImp, FieldImp > derived_type;
  typedef GridViewImp                                  GridViewType;
  typedef FieldImp                                     FieldType;
};

} // namespace internal

template< class GridViewImp, class FieldImp >
class OswaldInterpolation
  : public OperatorInterface< internal::OswaldInterpolationTraits< GridViewImp, FieldImp > >
{
public:
  typedef internal::OswaldInterpolationTraits< GridViewImp, FieldImp > Traits;
  typedef typename Traits::GridViewType GridViewType;
  typedef typename Traits::FieldType    FieldType;
  static const size_t                   dimDomain = GridViewType::dimension;

  OswaldInterpolation(const GridViewType& grd_vw, const bool zero_boundary = true)
    : grid_view_(grd_vw)
    , zero_boundary_(zero_boundary)
  {}

  template< class SGP, class SV, class RGP, class RV >
  void apply(const ConstDiscreteFunction< Spaces::DG::FemBased< SGP, 1, FieldType, 1, 1 >, SV >&
                source,
             DiscreteFunction< Spaces::DG::FemBased< RGP, 1, FieldType, 1, 1 >, RV >&
                range) const
  {
    apply_dg_fem(source, range);
  }

  template< class SGP, class SV, class RGP, class RV >
  void apply(const ConstDiscreteFunction< Spaces::Block< Spaces::DG::FemBased< SGP, 1, FieldType, 1, 1 > >, SV >&
                source,
             DiscreteFunction< Spaces::Block< Spaces::DG::FemBased< RGP, 1, FieldType, 1, 1 > >, RV >&
                range) const
  {
    apply_dg_fem(source, range);
  }

private:
  template< class SourceType, class RangeType >
  void apply_dg_fem(const SourceType& source, RangeType& range) const
  {
    // data structures we need
    // * a map from a global vertex index to global DoF indices
    //   given a vertex, one obtains a set of all global DoF ids, which are associated with this vertex
    std::map< size_t, std::set< size_t > > global_vertex_id_to_global_DoF_id_map;
    // * a map from a global DoF index to the global index of its associated vertex
    std::vector< size_t > global_DoF_id_to_global_vertex_id_map(source.space().mapper().size());
    // * a set to hold the global id of all boundary vertices
    std::set< size_t > boundary_vertices;

    const auto entity_it_end = grid_view_.template end< 0 >();
    //walk the grid to create the maps explained above and to find the boundary vertices
    for (auto entity_it = grid_view_.template begin< 0 >(); entity_it != entity_it_end; ++entity_it) {
      const auto& entity = *entity_it;
      const size_t num_vertices = boost::numeric_cast< size_t >(entity.template count< dimDomain >());
      const auto basis = source.space().base_function_set(entity);
      if (basis.size() != num_vertices)
        DUNE_THROW(Dune::Stuff::Exceptions::internal_error, "basis.size() = " << basis.size());

      //loop over all vertices of the entitity, to find their associated global DoF indices
      for (size_t local_vertex_id = 0; local_vertex_id < num_vertices; ++local_vertex_id) {
        const auto vertex_ptr = entity.template subEntity< dimDomain >(boost::numeric_cast< int >(local_vertex_id));
        const auto global_vertex_id = grid_view_.indexSet().index(*vertex_ptr);
        const auto vertex = vertex_ptr->geometry().center();
        // find the local basis function which corresponds to this vertex
        const auto basis_values = basis.evaluate(entity.geometry().local(vertex));
        if (basis_values.size() != num_vertices)
          DUNE_THROW(Dune::Stuff::Exceptions::internal_error, "basis_values.size() = " << basis_values.size());
        size_t ones = 0;
        size_t zeros = 0;
        size_t failures = 0;
        size_t local_DoF_index = 0;
        for (size_t ii = 0; ii < basis.size(); ++ii) {
          if (std::abs(basis_values[ii][0] - 1.0) < 1e-14) {
            local_DoF_index = ii;
            ++ones;
          } else if (std::abs(basis_values[ii][0] - 0.0) < 1e-14)
            ++zeros;
          else
            ++failures;
        }
        if (ones != 1 || zeros != (basis.size() - 1) || failures > 0) {
          std::stringstream ss;
          ss << "ones = " << ones << ", zeros = " << zeros << ", failures = " << failures << ", num_vertices = "
             << num_vertices << ", entity " << grid_view_.indexSet().index(entity)
             << ", vertex " << local_vertex_id << ": [ " << vertex << "], ";
          Stuff::Common::print(basis_values, "basis_values", ss);
          DUNE_THROW(Dune::Stuff::Exceptions::internal_error, ss.str());
        }
        // now we know that the local DoF index of this vertex is ii
        const size_t global_DoF_index = source.space().mapper().mapToGlobal(entity, local_DoF_index);
        global_DoF_id_to_global_vertex_id_map[global_DoF_index] = global_vertex_id;
        global_vertex_id_to_global_DoF_id_map[global_vertex_id].insert(global_DoF_index);
      } //loop over all vertices

      if (zero_boundary_) {
        // in order to determine the boundary vertices, we need to
        // loop over all intersections
        const auto intersectionEndIt = grid_view_.iend(entity);
        for (auto intersectionIt = grid_view_.ibegin(entity); intersectionIt != intersectionEndIt; ++intersectionIt) {
          const auto& intersection = *intersectionIt;
          if (intersection.boundary() && !intersection.neighbor()) {
            const auto& intersection_geometry = intersection.geometry();
            for (auto local_intersection_corner_id : DSC::valueRange(intersection_geometry.corners())) {
              const auto global_intersection_corner = intersection_geometry.corner(local_intersection_corner_id);
              // now, we need to find the entity's vertex this intersection's corner point equals to, so we
              // loop over all vertices of the entity
              for (size_t local_vertex_id = 0; local_vertex_id < num_vertices; ++local_vertex_id) {
                const auto vertex_ptr = entity.template subEntity< dimDomain >(boost::numeric_cast< int >(local_vertex_id));
                const auto global_vertex_id = grid_view_.indexSet().index(*vertex_ptr);
                const auto vertex = vertex_ptr->geometry().center();
                if (Stuff::Common::FloatCmp::eq(global_intersection_corner, vertex))
                  boundary_vertices.insert(global_vertex_id);
              } // loop over all vertices of the entity
            } //loop over all intersection corners
          } // if (intersection.boundary() && !intersection.neighbor())
        } // loop over all intersections
      } // if(zero_boundary)
    } //walk the grid for the first time

    // walk the grid for the second time
    for (auto entity_it = grid_view_.template begin< 0 >(); entity_it != entity_it_end; ++entity_it) {
      const auto& entity = *entity_it;
      const auto num_vertices = boost::numeric_cast< size_t >(entity.template count< dimDomain >());
      // get the local functions
      const auto local_source = source.local_discrete_function(entity);
      const auto& local_source_DoF_vector = local_source->vector();

      // * loop over all local DoFs
      for (size_t local_DoF_id = 0; local_DoF_id < num_vertices; ++local_DoF_id) {
      const size_t global_DoF_index = source.space().mapper().mapToGlobal(entity, local_DoF_id);
      const size_t global_vertex_id = global_DoF_id_to_global_vertex_id_map[global_DoF_index];
        // if we are on the domain boundary
        if (zero_boundary_ && boundary_vertices.count(global_vertex_id)) {
          // set the dof to zero (we have dirichlet zero)
          range.vector().set_entry(global_DoF_index, FieldType(0));
        } else {
          // do the oswald projection
          const size_t num_DoFS_per_vertex = global_vertex_id_to_global_DoF_id_map[global_vertex_id].size();
          // * get the source DoF
          const FieldType source_DoF_value = local_source_DoF_vector.get(local_DoF_id);
          // * and add it to all target DoFs
          for (size_t target_global_DoF_id : global_vertex_id_to_global_DoF_id_map[global_vertex_id])
            range.vector().add_to_entry(target_global_DoF_id, source_DoF_value / num_DoFS_per_vertex);
        } // if (boundary_vertices.find(global_vertex_id))
      } // loop over all local DoFs
    } // walk the grid for the second time
  } // ... apply(...)

  const GridViewType& grid_view_;
  const bool zero_boundary_;
}; // class OswaldInterpolation

} // namespace Operators
} // namespace GDT
} // namespace Dune

#endif // DUNE_GDT_OPERATORS_OSWALD_HH


#ifndef GENTLE_BOOST_ENSEMBLE
#define GENTLE_BOOST_ENSEMBLE

/* Implements a Gentle Boost classification ensemble */

#include <type_traits>
#include <memory>
#include <limits>
#include <vector>
#include <exception>

#include <armadillo>

#include "types.hpp"
#include "regression_stump.hpp"

/*!
    Implements a Gentle Boost ensemble (Friedmann et al.).
    It inherits std::enable_shared_from_this because multiple RegressionStump
    objects will contain a std::shared_ptr to this object. Therefore a
    shared_from_this() shared_ptr is passed to avoid double deletion.
!*/
class GentleBoostEnsemble: public std::enable_shared_from_this<GentleBoostEnsemble> {

private:
    Matrix _x;
    Vector _y;
    Vector _x_weights;
    std::vector<std::shared_ptr<RegressionStump>> _base_learners;
    int _iteration = 0;
    double _weight = 1.0;

    std::shared_ptr<GentleBoostEnsemble> get_ptr ( );

public:
    // max number of weak learners (max number of boosting iterations)
    static int _T;
    // misc
    std::vector<double> _max;

    // constructors
    GentleBoostEnsemble ( );
    GentleBoostEnsemble ( const Matrix&, const Vector& );
    GentleBoostEnsemble ( const GentleBoostEnsemble& );
    ~GentleBoostEnsemble ( );

    // misc
    const bool create_base_learners ( );

    // setters
    void set_x ( const Matrix& x ) { _x = x; }
    void set_y ( const Vector& y ) { _y = y; }
    void set_x_weights ( const Vector& x_weights ) { _x_weights = x_weights; }
    void set_base_learners ( const std::vector<std::shared_ptr<RegressionStump>> base_learners) { _base_learners = base_learners; }
    void set_iteration ( const int& iteration ) { _iteration = iteration; }
    void set_weight ( const double& weight ) { _weight = weight; }

    //getters
    const Matrix& get_x ( ) const { return _x; }
    Matrix get_x_copy ( ) const { return _x; }
    Matrix& get_x_nonconst ( ) { return _x; }
    const Vector& get_y ( ) const  { return _y; }
    Matrix get_y_copy ( ) const { return _y; }
    Vector& get_y_nonconst ( ) { return _y; }
    const Vector& get_x_weights ( ) const { return _x_weights; }
    Vector get_x_weights_copy ( ) const { return _x_weights; }
    Vector& get_x_weights_nonconst ( ) { return _x_weights; }
    const std::vector<std::shared_ptr<RegressionStump>>& get_base_learners ( ) const { return _base_learners; }
    const int& get_iteration ( ) const { return _iteration; }
    const double& get_weight ( ) const { return _weight; }

    // training functionalities
    friend void RegressionStump::train ( );
    friend void RegressionStump::retrain_on_last ( double, int );
    void train_single ( );
    void train_single_no_update ( );
    void train ( const int& );
    void retrain_current ( int );
    void train_full ( Matrix&, Vector&, Matrix&, Vector& );

    inline void update_weights ( const Vector& );
    void update_and_normalize_weights();
    inline void reset_weights_uniform ( );
    inline void normalize_weights ( );
    void update_max ( );

    //prediction functionalities
    friend inline Vector RegressionStump::predict ( ) const;
    friend inline Vector RegressionStump::predict ( const Matrix& ) const;
    friend inline Vector RegressionStump::compute_margins ( ) const;
    friend inline Vector RegressionStump::compute_margins ( const Matrix&, const Vector& ) const;
    Vector predict_real ( ) const;
    Vector predict_real ( const Matrix& ) const;
    Vector predict_discrete ( ) const;
    Vector predict_discrete ( const Matrix& ) const;
    Vector compute_margins ( ) const;
    Vector compute_margins ( const Matrix&, const Vector& ) const;
    double compute_error_rate ( const Matrix&, const Vector& ) const;
    void report_errors ( Matrix&, Vector&, Matrix&, Vector& ) const;

};

/*!
    Normalize the weights such that they all sum up to one
!*/
inline void GentleBoostEnsemble::normalize_weights ( ) {
    _x_weights /= sum(_x_weights);
}

/*!
    Updates the weights by the negative of the margins
!*/
inline void GentleBoostEnsemble::update_weights ( const Vector& arg ) {
    Vector exponents = arma::exp(-arg);
    _x_weights = _x_weights % exponents;
}

/*!
    Resets the weights so that they form a uniform probability distribution
!*/
inline void GentleBoostEnsemble::reset_weights_uniform ( ) {
    _x_weights = 1.0/ (double)_x_weights.n_elem * arma::ones<Vector>(_x_weights.n_elem);
}

#endif //GENTLE_BOOST_ENSEMBLE


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <set>
#include <gsl/gsl_rng.h>
#include <boost/tuple/tuple.hpp>
#include <boost/graph/graphviz.hpp>
#include <boost/graph/dijkstra_shortest_paths.hpp>
#include <boost/lexical_cast.hpp>

#include "types.hpp"
#include "randvars2.hpp"
#include "bgl_graph_info.hpp"

using namespace std;
using namespace boost;

static const size_t npos = -1; //defined in string::npos, for find() not found

/*
 * Generate the graph to run simulation on.
 * Make a preference file that itm can then use to generate the graph
 * Prefernece file has special values, see comments below.
 *
*/

void gen_graph(user_input *i,
	       unsigned	   seed){

  int syscheck = 0; //return value for system calls

  /* Makes gt-itm not crash.*/
  fprintf(stderr, "Exporting MALLOC_CHECK_=0\n");
  syscheck = system("export MALLOC_CHECK_=0"); //makes itm not crash
  if (syscheck != 0){
    fprintf(stderr, "Export Command Failed!");
    exit(1);
  }

  /* create a file to use for prefrences for gt-itm*/
  ofstream pref;
  pref.open("pref");

  /*
   * geo: flat random graph
   * hier: N-level hierarchial graph
   * ts: transit-stub graph
   *
   * Fromat:
   * <method> [above] <# of graphs> <seed> [optional]
  */

  pref << "geo 1 " << seed << endl; // if needed can change this so it isnt flat

  /*
   * Graph Parameters:
   * n: number of nodes in graph
   * scale: one-sided dimension of space in which nodes are distributed
   * edgemethod:
   *   1) Waxman 1
   *   2) Waxman 2
   *   3) Pure Random
   *   4) Door-Leslie
   *   5) Exponential
   *   6) Locality
   * alpha: random graph parameter (0 =< alpha =< 1.0)
   * beta: " " (0 <= beta)
   * gamma: " " (0 <= gamma)
   * geo: <n> <scale> <edgemethod> <alpha> [<beta><gamma>]
  */
  char preferences[255];
  DEBUGE("verts: %d : edge: %f\n", i->get_vertices(), i->get_edgechance());
  sprintf(preferences, "%d %d %d %f\n",
  i->get_vertices(), 10, 3, i->get_edgechance());

  pref << preferences;
  pref.close();

  /* Run itm on pref file created above. */
  fprintf(stderr,"Calling ../bin/itm pref\n");
  syscheck = system("../bin/itm pref");
  if (syscheck != 0){
    fprintf(stderr, "itm not found or failed to run!\n");
    exit(1);
  }

  /*convert gb file to readable alternate format*/
  fprintf(stderr,"Calling ../bin/sgb2alt pref-0.gb pref.alt\n");
  syscheck = system("../bin/sgb2alt pref-0.gb pref.alt");
  if (syscheck == 1){
    fprintf(stderr, "sgb2alt failed to run, not found, incompatable file!\n");
    exit(1);
  }

}

/*
 * Convert the graph from the alternative format to a dot format
 * that can easily be loaded into BGL as a graph.
 * This function will have some hard coding variables that may need to be
 * changed at a later date.
*/

Graph con_graph(UI in){
  ifstream alt;
  alt.open("pref.alt");
  if (!alt){
    fprintf(stderr, "Error: pref.alt does not Exist\n");
  }

  gsl_rng_env_setup(); //set up our random
  r = gsl_rng_alloc(gsl_rng_mt19937);
  gsl_rng_set(r, in->get_seed()); //use seed

  string line, v1, v2, weight, whitespace;
  int V1, V2;
  int num_verts = 1; //max of V1, V2.
  vector <pair< pair <int,int>, pair <int, long> > > edges;
  while(getline(alt,line) != NULL){
    if (line.find("EDGES") != npos){
      while(getline(alt,line) != NULL){
        istringstream stream(line);
        stream >> v1;
        stream >> v2;
        stream >> weight; //throw away itm weight, generate our own.
        V1 = atoi(v1.c_str());
        V2 = atoi(v2.c_str());
        /*
         * This code will determine the number of vertices that are used
         * in the graph, it will do this by checking if either number found
         * is larger than a previous one, this will find the number of
         * vertices under the assumption that gt-itm will generate fully
         * connected graphs, and therefore, the largest value will be the
         * the highest value seen.  It will also be offset by 1, because
         * gt-itm starts nodes at 0. This is compensated later in the code.
         */
        if (V1 > num_verts || V2 > num_verts){
          if (V1 > V2){
            num_verts = V1;
          } else {
            num_verts = V2;
          }
        }
        /* generate weights and capacities for this link*/
        long weight;
        long minw = in->get_minw();
        long maxw = in->get_maxw();
        long maxc = in->get_maxcap(); // Use maxcap() for core links.
        if (minw == maxw){
          weight = minw; //if they are set to same value, all links have
        } else {
          weight = UniformD(minw,maxw);  //otherwise, distributed
        }
        /* make them into pairs, then through int a vector.*/
        pair<int,int> edge (V1,V2);
        pair <int, long> w_c (weight,maxc);
        pair < pair <int, int> , pair <int, long> > edge_prop (edge,w_c);
        edges.push_back(edge_prop);
      }
    }
  }

  num_verts += 1;  //above code doesnt take into account gt-itm starts at 0
  Graph g(num_verts); //generate graph with num_verts nodes.
  //convert from alternative format to dot format for graphviz.
  for (int itor = 0; itor < num_verts; ++itor){
    //set the name for each node to its index
    g[itor].name = lexical_cast<string>(itor);
  }

  //add the edges to the graph now.
  for(vector< pair< pair < int, int>, pair <int, long> > >::iterator
    it = edges.begin(); it != edges.end(); ++it) {
    //let ele = be ((u,v),(w,c)) for both edge and properties
    pair< pair < int, int>, pair <int, long> > ele = (*it);
    edge_info prop; //set the properties to add to the graph with the edge.
    prop.weight = ele.second.first;
    prop.capacity = ele.second.second;
    pair<edge_desc, bool> res; // make sure that it added the edge.
    res = add_edge(ele.first.first,ele.first.second, prop, g);
    if (res.second == false){
      DEBUGE("Failed to add edge to graph!\n");
      DEBUGE("Edge in question: %d-%ld\n",
              ele.first.first,ele.second.second);
      exit(3);
    }
  }

  /* now open up the dot file and write the graph to it */
  ofstream dot("graph.dot");
  write_graphviz(dot, g,
    boost::make_label_writer(boost::get(&vert_info::name, g)),
    make_edge_writer(boost::get(&edge_info::weight,g),
    boost::get(&edge_info::capacity,g)));

  DEBUGE("Converting dot file to pdf format -- RUN WAS A:  ");
  errno = system("dot -Tpdf graph.dot -o graph.pdf &> /dev/null");
  if (errno == 0){
    DEBUGE("SUCCESS!\n");
  } else {
    DEBUGE("FAILURE!\n");
  }

  return g;
}


/* Copyright (C) 2012-2017 IBM Corp.
 * This program is Licensed under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *   http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. See accompanying LICENSE file.
 */
#include <NTL/BasicThreadPool.h>

#include "recryption.h"
#include "EncryptedArray.h"
#include "EvalMap.h"
#include "powerful.h"
#include "CtPtrs.h"
#include "intraSlot.h"
#include "norms.h"
#include "sample.h"
#include "debugging.h"
#include "fhe_stats.h"

NTL_CLIENT

#ifdef DEBUG_PRINTOUT
#include "debugging.h"
long printFlag = FLAG_PRINT_VEC;
#endif

/************************ Some local functions ***********************/
/*********************************************************************/

static void
checkCriticalValue(const vector<ZZX>& zzParts, const DoubleCRT& sKey,
                   const RecryptData& rcData, long q);

static void
checkRecryptBounds(const vector<ZZX>& zzParts, const DoubleCRT& sKey,
                   const FHEcontext& context, long q);

static void
checkRecryptBounds_v(const vector<ZZX>& v, const DoubleCRT& sKey,
                     const FHEcontext& context, long q);

// Return in poly a polynomial with X^i encoded in all the slots
static void x2iInSlots(ZZX& poly, long i,
		       vector<ZZX>& xVec, const EncryptedArray& ea)
{
  xVec.resize(ea.size());
  ZZX x2i = ZZX(i,1);
  for (long j=0; j<(long)xVec.size(); j++) xVec[j] = x2i;
  ea.encode(poly, xVec);
}

// Make every entry of vec divisible by p2e by adding/subtracting q, while
// keeping the added multiples small.  Specifically, for q = 1 mod p2e and any
// integer z can be made divisible by p2e via z' = z + v*q, with |v| <= p2e/2.

static void newMakeDivisible(ZZX& poly, long p2e, long q,
                          const FHEcontext& context, ZZX& vpoly)
{
  if (p2e == 1) {
    vpoly = 0;
    return;
  }

  helib::assertTrue<helib::InvalidArgument>(q > 0l, "q must be positive");
  helib::assertTrue<helib::InvalidArgument>(p2e > 0l, "p2e must be positive");

  helib::assertEq<helib::InvalidArgument>(q % p2e, 1l, "q must equal 1 modulo p2e");

  long p = context.zMStar.getP();

  const RecryptData& rcData = context.rcData;
  const PowerfulDCRT& p2d_conv = *rcData.p2dConv;

  Vec<ZZ> pwrfl;
  p2d_conv.ZZXtoPowerful(pwrfl, poly);

#ifdef DEBUG_PRINTOUT
  Vec<ZZ> vvec(INIT_SIZE, pwrfl.length());
#endif

  for (long i: range(pwrfl.length())) {
    ZZ& z = pwrfl[i];
    long u, v;

    // What to add to z to make it divisible by p2e?
    long zMod = rem(z, p2e); // zMod is in [0,p2e-1]
    // NOTE: this makes sure we get a truly balanced remainder
    if (zMod > p2e/2 || (p==2 && zMod == p2e/2 && RandomBnd(2))) {
      // randomize so that v has expected value 0
      zMod = p2e - zMod;
    }
    else {
      // need to add a negative number
      zMod = -zMod;
    }
    v = zMod;
    z += to_ZZ(q)*v; // make z divisible by p2e

    if (rem(z,p2e) != 0) { // sanity check
      cerr << "**error: original z["<<i<<"]=" << (z-(to_ZZ(q)*v))
	   << std::dec << ", p^e="<<p2e << endl;
      cerr << "z' = z + "<<v<<"*q = "<<z<<endl;
      exit(1);
    }

#ifdef DEBUG_PRINTOUT
    vvec[i] = v;
#endif
  }

  p2d_conv.powerfulToZZX(poly, pwrfl);

#ifdef DEBUG_PRINTOUT
  p2d_conv.powerfulToZZX(vpoly, vvec);
#endif

}

/*********************************************************************/
/*********************************************************************/

RecryptData::~RecryptData()
{
  if (alMod!=NULL)     delete alMod;
  if (ea!=NULL)        delete ea;
  if (firstMap!=NULL)  delete firstMap;
  if (secondMap!=NULL) delete secondMap;
  if (p2dConv!=NULL)   delete p2dConv;
}

/**
 * Summary of Appendix A from https://ia.cr/2014/873 (version from 2019):
 * Assume that we already chosen e, e' and t.
 *
 * Based in this analysis, we need
 *    (1) (f*p^{e'} + 2*p^r+2))*B <= p^e/2
 * where B is a certain high-probability bound and f is a certain
 * fudge factor.
 *
 **/

// the routine compute_fudge is used to correct for the fact that
// the v-coeffs are not quite uniform

static
double compute_fudge(long p2ePrime, long p2e)
{
  double eps = 0;

  if (p2ePrime > 1) {

      if (p2ePrime%2 == 0) {
         eps = 1/fsquare(p2ePrime);

	 // The exact variance in this case is at most the variance
         // of a random variable that is distributed over
         //    -N..+N
         // where N = 2^{e'}/2.
         // Each endpoint occurs with probability 1/(4*N),
         // and the remaining values each occur with the same probability
         // 1/(2*N)

         // This variance is exactly computed as
	 //    (N^2)/3 + 1/6 = ((N^2)/3)*(1 + 1/(2*N^2)), where N = 2^{e'}/2
	 // So the std dev is at most
	 //    N/sqrt(3)*(1 + 1/(4*N^2))

      }
      else{
         eps = 1/double(p2e);

         // We are computing X + Y mod p^{e'}, where
         // X and Y are independent.
         // Y is uniformly distributed over
         //    -floor(p^{r}/2)..floor(p^{r}/2)
         // X is distributed over
         //    -floor(p^e/2)-1..floor(p^e/2)+1,
         // where each endpoint occurs with probability 1 / (2*(p^e+1)),
         // and the remaining p^e values are equally likely

         // The variance in this case is bounded by
         //   (N^2)/3*(1-eps) + (N^2)*eps = (N^2)/3*(1+2*eps),
         //       where N = p^{e'}/2 and eps < 1/p^e
         // So the std dev is bounded by
         //    N/sqrt(3)*sqrt(1+2*eps) <= N/sqrt(3)*(1+eps)

      }

  }

  return 1 + eps;
}

long RecryptData::setAE(long& e, long& ePrime,
                    const FHEcontext& context, long targetWeight)
{
  bool default_target=false;
  if (targetWeight<=0) {
    targetWeight = RecryptData::defSkHwt;
    default_target=true;
  }

  double coeff_bound = context.boundForRecryption(targetWeight);
  // coeff_bound is ultimately a high prob bound on |w0+w1*s|,
  // the coeffs of w0, w1 are chosen uniformly on [-1/2,1/2]

  long p = context.zMStar.getP();
  long p2r = context.alMod.getPPowR();
  long r = context.alMod.getR();
  long frstTerm = 2*p2r+2;

  long e_bnd = 0;
  long p2e_bnd = 1;
  while (p2e_bnd <= ((1L << 30)-2)/p) { // NOTE: this avoids overflow
    e_bnd++;
    p2e_bnd *= p;
  }
  // e_bnd is largest e such that p^e+1 < 2^30

  // Start with the smallest e s.t. p^e/2 >= frstTerm*coeff_bound
  ePrime = 0;
  e = r+1;
  while (e <= e_bnd && power_long(p, e) < frstTerm*coeff_bound*2)
    e++;

  if (e > e_bnd) Error("setAE: cannot find suitable e");

  //long ePrimeTry = r+1;
  long ePrimeTry = 1;

  while (ePrimeTry <= e_bnd) {
    long p2ePrimeTry = power_long(p, ePrimeTry);
    //long eTry = ePrimeTry+1;
    long eTry = max(r+1, ePrimeTry+1);
    while (eTry <= e_bnd && eTry-ePrimeTry < e-ePrime) {
      long p2eTry = power_long(p, eTry);
      double fudge = compute_fudge(p2ePrimeTry, p2eTry);
      if (p2eTry >= (p2ePrimeTry*fudge+frstTerm)*coeff_bound*2) break;

      eTry++;
    }

    if (eTry <= e_bnd && eTry-ePrimeTry < e-ePrime) {
      e = eTry;
      ePrime = ePrimeTry;
    }

    ePrimeTry++;
  }

#ifdef DEBUG_PRINTOUT
  cerr << "RecryptData::setAE(): e="<<e<<", e'="<<ePrime
       << endl;
#endif
  return targetWeight;
}

bool RecryptData::operator==(const RecryptData& other) const
{
  if (mvec != other.mvec) return false;
  if (skHwt != other.skHwt) return false;

  return true;
}

// The main method
void RecryptData::init(const FHEcontext& context, const Vec<long>& mvec_,
                  bool enableThick, long t, bool build_cache_, bool minimal)
{
  if (alMod != NULL) { // were we called for a second time?
    cerr << "@Warning: multiple calls to RecryptData::init\n";
    return;
  }
  helib::assertEq(computeProd(mvec_), (long)context.zMStar.getM(), "Cyclotomic polynomial mismatch"); // sanity check

  // Record the arguments to this function
  mvec = mvec_;
  build_cache = build_cache_;

  bool mvec_ok = true;
  for (long i: range(mvec.length())) {
    Vec<Pair<long,long>> factors;
    factorize(factors, mvec[i]);
    if (factors.length() > 1) mvec_ok = false;
  }

  if (!mvec_ok) {
    Warning("prime power factorization recommended for bootstrapping");
  }

  skHwt = setAE(e, ePrime, context, t);
  long p = context.zMStar.getP();
  long r = context.alMod.getR();

  // First part of Bootstrapping works wrt plaintext space p^{r'}
  alMod = new PAlgebraMod(context.zMStar, e-ePrime+r);
  ea = new EncryptedArray(context, *alMod);
         // Polynomial defaults to F0, PAlgebraMod explicitly given

  p2dConv = new PowerfulDCRT(context, mvec);

  if (!enableThick) return;

  // Initialize the linear polynomial for unpacking the slots
  zz_pBak bak; bak.save(); ea->getAlMod().restoreContext();
  long nslots = ea->size();
  long d = ea->getDegree();

  const Mat<zz_p>& CBi=ea->getDerived(PA_zz_p()).getNormalBasisMatrixInverse();

  vector<ZZX> LM;
  LM.resize(d);
  for (long i = 0; i < d; i++) // prepare the linear polynomial
    LM[i] = rep(CBi[i][0]);

  vector<ZZX> C;
  ea->buildLinPolyCoeffs(C, LM); // "build" the linear polynomial

  unpackSlotEncoding.resize(d);  // encode the coefficients

  for (long j = 0; j < d; j++) {
    vector<ZZX> v(nslots);
    for (long k = 0; k < nslots; k++) v[k] = C[j];
    ea->encode(unpackSlotEncoding[j], v);
  }
  firstMap = new EvalMap(*ea, minimal, mvec, true, build_cache);
  secondMap = new EvalMap(*context.ea, minimal, mvec, false, build_cache);
}

/********************************************************************/
/********************************************************************/

// Extract digits from fully packed slots
void extractDigitsPacked(Ctxt& ctxt, long botHigh, long r, long ePrime,
			 const vector<ZZX>& unpackSlotEncoding);

// Extract digits from unpacked slots
void extractDigitsThin(Ctxt& ctxt, long botHigh, long r, long ePrime);

// bootstrap a ciphertext to reduce noise
void FHEPubKey::reCrypt(Ctxt &ctxt)
{
  FHE_TIMER_START;

  // Some sanity checks for dummy ciphertext
  long ptxtSpace = ctxt.getPtxtSpace();
  if (ctxt.isEmpty()) return;
  if (ctxt.parts.size()==1 && ctxt.parts[0].skHandle.isOne()) {
    // Dummy encryption, just ensure that it is reduced mod p
    ZZX poly = to_ZZX(ctxt.parts[0]);
    for (long i=0; i<poly.rep.length(); i++)
      poly[i] = to_ZZ( rem(poly[i],ptxtSpace) );
    poly.normalize();
    ctxt.DummyEncrypt(poly);
    return;
  }

  //OLD: assert(recryptKeyID>=0); // check that we have bootstrapping data
  helib::assertTrue(recryptKeyID>=0l, "No bootstrapping data");

  long p = getContext().zMStar.getP();
  long r = getContext().alMod.getR();
  long p2r = getContext().alMod.getPPowR();

  long intFactor = ctxt.intFactor;

  // the bootstrapping key is encrypted relative to plaintext space p^{e-e'+r}.
  const RecryptData& rcData = getContext().rcData;
  long e = rcData.e;
  long ePrime = rcData.ePrime;
  long p2ePrime = power_long(p,ePrime);
  long q = power_long(p,e)+1;
  //OLD: assert(e>=r);
  helib::assertTrue(e>=r, "rcData.e must be at least alMod.r");

#ifdef DEBUG_PRINTOUT
  cerr << "reCrypt: p="<<p<<", r="<<r<<", e="<<e<<" ePrime="<<ePrime
       << ", q="<<q<<endl;
  CheckCtxt(ctxt, "init");
#endif

  // can only bootstrap ciphertext with plaintext-space dividing p^r
  //OLD: assert(p2r % ptxtSpace == 0);
  helib::assertEq(p2r % ptxtSpace, 0l, "ptxtSpace must divide p^r when bootstrapping");

  ctxt.dropSmallAndSpecialPrimes();

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after mod down");
#endif

  FHE_NTIMER_START(AAA_preProcess);

  // Make sure that this ciphertxt is in canonical form
  if (!ctxt.inCanonicalForm()) ctxt.reLinearize();

  // Mod-switch down if needed
  IndexSet s = ctxt.getPrimeSet() / context.specialPrimes;
  //OLD: assert(s <= context.ctxtPrimes);
  helib::assertTrue(s <= context.ctxtPrimes,  "prime set is messed up");
  if (s.card()>3) { // leave only first three ciphertext primes
    long first = s.first();
    IndexSet s3(first, first+2);
    s.retain(s3);
  }
  ctxt.modDownToSet(s);

  // key-switch to the bootstrapping key
  ctxt.reLinearize(recryptKeyID);

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after key switching");
#endif

  // "raw mod-switch" to the bootstrapping mosulus q=p^e+1.
  vector<ZZX> zzParts; // the mod-switched parts, in ZZX format

  double mfac = ctxt.getContext().zMStar.getNormBnd();
  double noise_est = ctxt.rawModSwitch(zzParts, q) * mfac;
  // noise_est is an upper bound on the L-infty norm of the scaled noise
  // in the pwrfl basis
  double noise_bnd = 0.66*p2r*ctxt.getContext().boundForRecryption();
  // noise_bnd is the bound assumed in selecting the parameters
  double noise_rat = noise_est/noise_bnd;

  FHE_STATS_UPDATE("raw-mod-switch-noise", noise_rat);

  if (noise_rat > 1) {
    Warning("rawModSwitch scaled noise exceeds bound: " + std::to_string(noise_rat));
  }

  //OLD: assert(zzParts.size() == 2);
  helib::assertEq(zzParts.size(), (std::size_t)2, "Exactly 2 parts required for mod-switching in thin bootstrapping");

#ifdef DEBUG_PRINTOUT
  if (dbgKey) {
    checkRecryptBounds(zzParts, dbgKey->sKeys[recryptKeyID],
                       ctxt.getContext(), q);
  }
#endif

  vector<ZZX> v;
  v.resize(2);

  // Add multiples of q to make the zzParts divisible by p^{e'}
  for (long i: range(2)) {
    // make divisible by p^{e'}

    newMakeDivisible(zzParts[i], p2ePrime, q, ctxt.getContext(), v[i]);

  }

#ifdef DEBUG_PRINTOUT
  if (dbgKey) {
    checkRecryptBounds_v(v, dbgKey->sKeys[recryptKeyID],
		       ctxt.getContext(), q);
    checkCriticalValue(zzParts, dbgKey->sKeys[recryptKeyID],
                       ctxt.getContext().rcData, q);
  }
#endif

  for (long i: range(zzParts.size())) {
    zzParts[i] /= p2ePrime;   // divide by p^{e'}
  }

  // NOTE: here we lose the intFactor associated with ctxt.
  // We will restore it below.
  ctxt = recryptEkey;

  ctxt.multByConstant(zzParts[1]);
  ctxt.addConstant(zzParts[0]);

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after preProcess");
#endif
  FHE_NTIMER_STOP(AAA_preProcess);

  // Move the powerful-basis coefficients to the plaintext slots
  FHE_NTIMER_START(AAA_LinearTransform1);
  ctxt.getContext().rcData.firstMap->apply(ctxt);
  FHE_NTIMER_STOP(AAA_LinearTransform1);

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after LinearTransform1");
#endif

  // Extract the digits e-e'+r-1,...,e-e' (from fully packed slots)
  FHE_NTIMER_START(AAA_extractDigitsPacked);
  extractDigitsPacked(ctxt, e-ePrime, r, ePrime,
		      context.rcData.unpackSlotEncoding);
  FHE_NTIMER_STOP(AAA_extractDigitsPacked);

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after extractDigitsPacked");
#endif

  // Move the slots back to powerful-basis coefficients
  FHE_NTIMER_START(AAA_LinearTransform2);
  ctxt.getContext().rcData.secondMap->apply(ctxt);
  FHE_NTIMER_STOP(AAA_LinearTransform2);

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after linearTransform2");
#endif

  // restore intFactor
  if (intFactor != 1)
    ctxt.intFactor = MulMod(ctxt.intFactor, intFactor, ptxtSpace);
}

#ifdef FHE_BOOT_THREADS

// Extract digits from fully packed slots, multithreaded version
void extractDigitsPacked(Ctxt& ctxt, long botHigh, long r, long ePrime,
			 const vector<ZZX>& unpackSlotEncoding)
{
  FHE_TIMER_START;

  // Step 1: unpack the slots of ctxt
  FHE_NTIMER_START(unpack);
  ctxt.cleanUp();

  // Apply the d automorphisms and store them in scratch area
  long d = ctxt.getContext().zMStar.getOrdP();

  vector<Ctxt> unpacked(d, Ctxt(ZeroCtxtLike, ctxt));
  { // explicit scope to force all temporaries to be released
    vector< shared_ptr<DoubleCRT> > coeff_vector;
    vector<double> coeff_vector_sz;
    coeff_vector.resize(d);
    coeff_vector_sz.resize(d);

    FHE_NTIMER_START(unpack1);
    for (long i = 0; i < d; i++) {
      coeff_vector[i] = shared_ptr<DoubleCRT>(new
        DoubleCRT(unpackSlotEncoding[i], ctxt.getContext(), ctxt.getPrimeSet()) );
      coeff_vector_sz[i] =
        conv<double>( embeddingLargestCoeff(unpackSlotEncoding[i],
                                            ctxt.getContext().zMStar) );
    }
    FHE_NTIMER_STOP(unpack1);

    FHE_NTIMER_START(unpack2);
    vector<Ctxt> frob(d, Ctxt(ZeroCtxtLike, ctxt));

    NTL_EXEC_RANGE(d, first, last)
    // FIXME: implement using hoisting!
        for (long j = first; j < last; j++) { // process jth Frobenius
          frob[j] = ctxt;
          frob[j].frobeniusAutomorph(j);
          frob[j].cleanUp();
          // FIXME: not clear if we should call cleanUp here
        }
    NTL_EXEC_RANGE_END

    FHE_NTIMER_STOP(unpack2);

    FHE_NTIMER_START(unpack3);
    Ctxt tmp1(ZeroCtxtLike, ctxt);
    for (long i = 0; i < d; i++) {
      for (long j = 0; j < d; j++) {
        tmp1 = frob[j];
        tmp1.multByConstant(*coeff_vector[mcMod(i+j, d)],
                            coeff_vector_sz[mcMod(i+j, d)]);
        unpacked[i] += tmp1;
      }
    }
    FHE_NTIMER_STOP(unpack3);
  }
  FHE_NTIMER_STOP(unpack);

  //#ifdef DEBUG_PRINTOUT
  //  CheckCtxt(unpacked[0], "after unpack");
  //#endif

  NTL_EXEC_RANGE(d, first, last)
  for (long i = first; i < last; i++) {
    extractDigitsThin(unpacked[i], botHigh, r, ePrime);
  }
  NTL_EXEC_RANGE_END

  //#ifdef DEBUG_PRINTOUT
  //CheckCtxt(unpacked[0], "before repack");
  //#endif

  // Step 3: re-pack the slots
  FHE_NTIMER_START(repack);
  const EncryptedArray& ea2 = *ctxt.getContext().ea;
  ZZX xInSlots;
  vector<ZZX> xVec(ea2.size());
  ctxt = unpacked[0];
  for (long i=1; i<d; i++) {
    x2iInSlots(xInSlots, i, xVec, ea2);
    unpacked[i].multByConstant(xInSlots);
    ctxt += unpacked[i];
  }
  FHE_NTIMER_STOP(repack);
  //#ifdef DEBUG_PRINTOUT
  //CheckCtxt(ctxt, "after repack");
  //#endif
}

#else

// Extract digits from fully packed slots
void extractDigitsPacked(Ctxt& ctxt, long botHigh, long r, long ePrime,
			 const vector<ZZX>& unpackSlotEncoding)
{
  FHE_TIMER_START;

  // Step 1: unpack the slots of ctxt
  FHE_NTIMER_START(unpack);
  ctxt.cleanUp();

  // Apply the d automorphisms and store them in scratch area
  long d = ctxt.getContext().zMStar.getOrdP();

  vector<Ctxt> unpacked(d, Ctxt(ZeroCtxtLike, ctxt));
  { // explicit scope to force all temporaries to be released
    vector< shared_ptr<DoubleCRT> > coeff_vector;
    vector<double> coeff_vector_sz;
    coeff_vector.resize(d);
    coeff_vector_sz.resize(d);
    for (long i = 0; i < d; i++) {
      coeff_vector[i] = shared_ptr<DoubleCRT>(new
        DoubleCRT(unpackSlotEncoding[i], ctxt.getContext(), ctxt.getPrimeSet()) );
      coeff_vector_sz[i] =
        conv<double>( embeddingLargestCoeff(unpackSlotEncoding[i],
                                            ctxt.getContext().zMStar) );
    }

    Ctxt tmp1(ZeroCtxtLike, ctxt);
    Ctxt tmp2(ZeroCtxtLike, ctxt);

    // FIXME: implement using hoisting!
    for (long j = 0; j < d; j++) { // process jth Frobenius
      tmp1 = ctxt;
      tmp1.frobeniusAutomorph(j);
      tmp1.cleanUp();
      // FIXME: not clear if we should call cleanUp here

      for (long i = 0; i < d; i++) {
        tmp2 = tmp1;
        tmp2.multByConstant(*coeff_vector[mcMod(i+j, d)],
                            coeff_vector_sz[mcMod(i+j, d)]);
        unpacked[i] += tmp2;
      }
    }
  }
  FHE_NTIMER_STOP(unpack);

  //#ifdef DEBUG_PRINTOUT
  //  CheckCtxt(unpacked[0], "after unpack");
  //#endif

  for (long i=0; i<(long)unpacked.size(); i++) {
    extractDigitsThin(unpacked[i], botHigh, r, ePrime);
  }

  //#ifdef DEBUG_PRINTOUT
  //  CheckCtxt(unpacked[0], "before repack");
  //#endif

  // Step 3: re-pack the slots
  FHE_NTIMER_START(repack);
  const EncryptedArray& ea2 = *ctxt.getContext().ea;
  ZZX xInSlots;
  vector<ZZX> xVec(ea2.size());
  ctxt = unpacked[0];
  for (long i=1; i<d; i++) {
    x2iInSlots(xInSlots, i, xVec, ea2);
    unpacked[i].multByConstant(xInSlots);
    ctxt += unpacked[i];
  }
  FHE_NTIMER_STOP(repack);
}

#endif

// Use packed bootstrapping, so we can bootstrap all in just one go.
void packedRecrypt(const CtPtrs& cPtrs,
                   const std::vector<zzX>& unpackConsts,
                   const EncryptedArray& ea)
{
  FHEPubKey& pKey = (FHEPubKey&)cPtrs[0]->getPubKey();

  // Allocate temporary ciphertexts for the recryption
  int nPacked = divc(cPtrs.size(), ea.getDegree()); // ceil(totoalNum/d)
  std::vector<Ctxt> cts(nPacked, Ctxt(pKey));

  repack(CtPtrs_vectorCt(cts), cPtrs, ea);  // pack ciphertexts
  //  cout << "@"<< lsize(cts)<<std::flush;
  for (Ctxt& c: cts) {     // then recrypt them
    c.reducePtxtSpace(2);  // we only have recryption data for binary ctxt
    pKey.reCrypt(c);
  }
  unpack(cPtrs, CtPtrs_vectorCt(cts), ea, unpackConsts);
}

// recrypt all ctxt at level < belowLvl
void packedRecrypt(const CtPtrs& array,
                   const std::vector<zzX>& unpackConsts,
                   const EncryptedArray& ea, long belowLvl)
{
  std::vector<Ctxt*> v;
  for (long i=0; i<array.size(); i++)
    if ( array.isSet(i) && !array[i]->isEmpty()
         && array[i]->bitCapacity()<belowLvl*(array[i]->getContext().BPL()) )
      v.push_back(array[i]);
  packedRecrypt(CtPtrs_vectorPt(v), unpackConsts, ea);
}
void packedRecrypt(const CtPtrMat& m,
                   const std::vector<zzX>& unpackConsts,
                   const EncryptedArray& ea, long belowLvl)
{
  std::vector<Ctxt*> v;
  for (long i=0; i<m.size(); i++)
    for (long j=0; j<m[i].size(); j++)
      if ( m[i].isSet(j) && !m[i][j]->isEmpty()
           && m[i][j]->bitCapacity()<belowLvl*(m[i][j]->getContext().BPL()) )
        v.push_back(m[i][j]);
  packedRecrypt(CtPtrs_vectorPt(v), unpackConsts, ea);
}

//===================== Thin Bootstrapping stuff ==================

ThinRecryptData::~ThinRecryptData()
{
  if (coeffToSlot!=NULL)  delete coeffToSlot;
  if (slotToCoeff!=NULL) delete slotToCoeff;
}

// This code was copied from RecryptData::init, and is mostly
// the same, except for the linear-map-related stuff.
// FIXME: There is really too much code (and data!) duplication here.
void ThinRecryptData::init(const FHEcontext& context, const Vec<long>& mvec_,
                      bool alsoThick, long t, bool build_cache_, bool minimal)
{
  RecryptData::init(context, mvec_, alsoThick, t, build_cache_, minimal);
  coeffToSlot = new ThinEvalMap(*ea, minimal, mvec, true, build_cache);
  slotToCoeff = new ThinEvalMap(*context.ea, minimal, mvec, false, build_cache);
}

// Extract digits from thinly packed slots

long fhe_force_chen_han = 0;

void extractDigitsThin(Ctxt& ctxt, long botHigh, long r, long ePrime)
{
  FHE_TIMER_START;

  Ctxt unpacked(ctxt);
  unpacked.cleanUp();

  vector<Ctxt> scratch;

  long p = ctxt.getContext().zMStar.getP();
  long p2r = power_long(p,r);
  long topHigh = botHigh + r-1;

  // degree Chen/Han technique is p^{bot-1}(p-1)r
  // degree of basic technique is p^{bot-1}p^r,
  //     or p^{bot-1}p^{r-1} if p==2, r > 1, and bot+r > 2

  bool use_chen_han = false;
  if (r > 1) {
    double chen_han_cost = log(p-1) + log(r);
    double basic_cost;
    if (p == 2 && botHigh + r > 2)
       basic_cost = (r-1)*log(p);
    else
       basic_cost = r*log(p);

    //cerr << "*** basic: " << basic_cost << "\n";
    //cerr << "*** chen/han: " << chen_han_cost << "\n";

    double thresh = 1.5;
    if (p == 2) thresh = 1.75;
    // increasing thresh makes chen_han less likely to be chosen.
    // For p == 2, the basic algorithm is just squaring,
    // and so is a bit cheaper, so we raise thresh a bit.
    // This is all a bit heuristic.

    if (basic_cost > thresh*chen_han_cost)
      use_chen_han = true;
  }

  if (fhe_force_chen_han > 0)
    use_chen_han = true;
  else if (fhe_force_chen_han < 0)
    use_chen_han = false;

  if (use_chen_han) {
    // use Chen and Han technique

    extendExtractDigits(scratch, unpacked, botHigh, r);

#if 0
    for (long i: range(scratch.size())) {
      CheckCtxt(scratch[i], "**");
    }
#endif

    for (long j = 0; j < botHigh; j++) {
      unpacked -= scratch[j];
      unpacked.divideByP();
    }

    if (p==2 && botHigh>0)   // For p==2, subtract also the previous bit
      unpacked += scratch[botHigh-1];
    unpacked.negate();

    if (r>ePrime) {          // Add in digits from the bottom part, if any
      long topLow = r-1 - ePrime;
      Ctxt tmp = scratch[topLow];
      for (long j=topLow-1; j>=0; --j) {
	tmp.multByP();
	tmp += scratch[j];
      }
      if (ePrime>0)
	tmp.multByP(ePrime); // multiply by p^e'
      unpacked += tmp;
    }
    unpacked.reducePtxtSpace(p2r); // Our plaintext space is now mod p^r

    ctxt = unpacked;
  }
  else {

    if (p==2 && r>1 && topHigh+1 > 2)
      topHigh--; // For p==2 we sometime get a bit for free

    extractDigits(scratch, unpacked, topHigh+1);

    // set upacked = -\sum_{j=botHigh}^{topHigh} scratch[j] * p^{j-botHigh}
    if (topHigh >= LONG(scratch.size())) {
      topHigh = scratch.size() -1;
      cerr << " @ suspect: not enough digits in extractDigitsPacked\n";
    }

    unpacked = scratch[topHigh];
    for (long j=topHigh-1; j>=botHigh; --j) {
      unpacked.multByP();
      unpacked += scratch[j];
    }
    if (p==2 && botHigh>0)   // For p==2, subtract also the previous bit
      unpacked += scratch[botHigh-1];
    unpacked.negate();

    if (r>ePrime) {          // Add in digits from the bottom part, if any
      long topLow = r-1 - ePrime;
      Ctxt tmp = scratch[topLow];
      for (long j=topLow-1; j>=0; --j) {
	tmp.multByP();
	tmp += scratch[j];
      }
      if (ePrime>0)
	tmp.multByP(ePrime); // multiply by p^e'
      unpacked += tmp;
    }
    unpacked.reducePtxtSpace(p2r); // Our plaintext space is now mod p^r
    ctxt = unpacked;
  }

}

// Hack to get at private fields of public key
struct FHEPubKeyHack { // The public key
  const FHEcontext& context; // The context

  //! @var Ctxt pubEncrKey
  //! The public encryption key is an encryption of 0,
  //! relative to the first secret key
  Ctxt pubEncrKey;

  std::vector<long> skHwts; // The Hamming weight of the secret keys
  std::vector<KeySwitch> keySwitching; // The key-switching matrices

  // The keySwitchMap structure contains pointers to key-switching matrices
  // for re-linearizing automorphisms. The entry keySwitchMap[i][n] contains
  // the index j such that keySwitching[j] is the first matrix one needs to
  // use when re-linearizing s_i(X^n).
  std::vector< std::vector<long> > keySwitchMap;

  NTL::Vec<int> KS_strategy; // NTL Vec's support I/O, which is
                             // more convenient

  // bootstrapping data

  long recryptKeyID; // index of the bootstrapping key
  Ctxt recryptEkey;  // the key itself, encrypted under key #0

};

// bootstrap a ciphertext to reduce noise
void FHEPubKey::thinReCrypt(Ctxt &ctxt)
{
  FHE_TIMER_START;

  // Some sanity checks for dummy ciphertext
  long ptxtSpace = ctxt.getPtxtSpace();
  if (ctxt.isEmpty()) return;

  if (ctxt.parts.size()==1 && ctxt.parts[0].skHandle.isOne()) {
    // Dummy encryption, just ensure that it is reduced mod p
    ZZX poly = to_ZZX(ctxt.parts[0]);
    for (long i=0; i<poly.rep.length(); i++)
      poly[i] = to_ZZ( rem(poly[i],ptxtSpace) );
    poly.normalize();
    ctxt.DummyEncrypt(poly);
    return;
  }

  //OLD: assert(recryptKeyID>=0); // check that we have bootstrapping data
  helib::assertTrue(recryptKeyID>=0l, "Bootstrapping data not present");

  long p = ctxt.getContext().zMStar.getP();
  long r = ctxt.getContext().alMod.getR();
  long p2r = ctxt.getContext().alMod.getPPowR();

  long intFactor = ctxt.intFactor;

  const ThinRecryptData& trcData = ctxt.getContext().rcData;

  // the bootstrapping key is encrypted relative to plaintext space p^{e-e'+r}.
  long e = trcData.e;
  long ePrime = trcData.ePrime;
  long p2ePrime = power_long(p,ePrime);
  long q = power_long(p,e)+1;
  //OLD: assert(e>=r);
  helib::assertTrue(e>=r, "trcData.e must be at least alMod.r");

  // can only bootstrap ciphertext with plaintext-space dividing p^r
  //OLD: assert(p2r % ptxtSpace == 0);
  helib::assertEq(p2r % ptxtSpace, 0l, "ptxtSpace must divide p^r when thin bootstrapping");

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "init");
#endif

  ctxt.dropSmallAndSpecialPrimes();

#define DROP_BEFORE_THIN_RECRYPT
#define THIN_RECRYPT_NLEVELS (3)
#ifdef DROP_BEFORE_THIN_RECRYPT
  // experimental code...we should drop down to a reasonably low level
  // before doing the first linear map.
  long first = context.ctxtPrimes.first();
  long last = min(context.ctxtPrimes.last(),
                  first + THIN_RECRYPT_NLEVELS - 1);
  ctxt.bringToSet(IndexSet(first, last));
#endif

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after mod down");
#endif

  // Move the slots to powerful-basis coefficients
  FHE_NTIMER_START(AAA_slotToCoeff);
  trcData.slotToCoeff->apply(ctxt);
  FHE_NTIMER_STOP(AAA_slotToCoeff);

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after slotToCoeff");
#endif

  FHE_NTIMER_START(AAA_bootKeySwitch);

  // Make sure that this ciphertxt is in canonical form
  if (!ctxt.inCanonicalForm()) ctxt.reLinearize();

  // Mod-switch down if needed
  IndexSet s = ctxt.getPrimeSet() / context.specialPrimes;
  //OLD: assert(s <= context.ctxtPrimes);
  helib::assertTrue(s <= context.ctxtPrimes,  "prime set is messed up");
  if (s.card()>3) { // leave only first three ciphertext primes
    long first = s.first();
    IndexSet s3(first, first+2);
    s.retain(s3);
  }
  ctxt.modDownToSet(s);

  // key-switch to the bootstrapping key
  ctxt.reLinearize(recryptKeyID);

#ifdef DEBUG_PRINTOUT
  CheckCtxt(ctxt, "after key switching");
#endif

  // "raw mod-switch" to the bootstrapping mosulus q=p^e+1.
  vector<ZZX> zzParts; // the mod-switched parts, in ZZX format

  double mfac = ctxt.getContext().zMStar.getNormBnd();
  double noise_est = ctxt.rawModSwitch(zzParts, q) * mfac;
  // noise_est is an upper bound on the L-infty norm of the scaled noise
  // in the pwrfl basis
  double noise_bnd = 0.66*p2r*ctxt.getContext().boundForRecryption();
  // noise_bnd is the bound assumed in selecting the parameters
  double noise_rat = noise_est/noise_bnd;

  FHE_STATS_UPDATE("raw-mod-switch-noise", noise_rat);

  if (noise_rat > 1) {
    Warning("rawModSwitch scaled noise exceeds bound: " + std::to_string(noise_rat));
  }

  //OLD: assert(zzParts.size() == 2);
  helib::assertEq(zzParts.size(), (std::size_t)2, "Exactly 2 parts required for mod-switching in thin bootstrapping");

#ifdef DEBUG_PRINTOUT
  if (dbgKey) {
    checkRecryptBounds(zzParts, dbgKey->sKeys[recryptKeyID],
                       ctxt.getContext(), q);
  }
#endif

  vector<ZZX> v;
  v.resize(2);

  // Add multiples of q to make the zzParts divisible by p^{e'}
  for (long i: range(2)) {
    // make divisible by p^{e'}

    newMakeDivisible(zzParts[i], p2ePrime, q, ctxt.getContext(), v[i]);

  }

#ifdef DEBUG_PRINTOUT
  if (dbgKey) {
    checkRecryptBounds_v(v, dbgKey->sKeys[recryptKeyID],
		       ctxt.getContext(), q);
    checkCriticalValue(zzParts, dbgKey->sKeys[recryptKeyID],
                       ctxt.getContext().rcData, q);
  }
#endif

  for (long i: range(zzParts.size())) {
    zzParts[i] /= p2ePrime;   // divide by p^{e'}
  }

  // NOTE: here we lose the intFactor associated with ctxt.
  // We will restore it below.
  ctxt = recryptEkey;

  ctxt.multByConstant(zzParts[1]);
  ctxt.addConstant(zzParts[0]);

#ifdef DEBUG_PRINTOUT
   CheckCtxt(ctxt, "after bootKeySwitch");
#endif

  FHE_NTIMER_STOP(AAA_bootKeySwitch);

  // Move the powerful-basis coefficients to the plaintext slots
  FHE_NTIMER_START(AAA_coeffToSlot);
  trcData.coeffToSlot->apply(ctxt);
  FHE_NTIMER_STOP(AAA_coeffToSlot);

#ifdef DEBUG_PRINTOUT
   CheckCtxt(ctxt, "after coeffToSlot");
#endif

  // Extract the digits e-e'+r-1,...,e-e' (from fully packed slots)
  FHE_NTIMER_START(AAA_extractDigitsThin);
  extractDigitsThin(ctxt, e-ePrime, r, ePrime);
  FHE_NTIMER_STOP(AAA_extractDigitsThin);

#ifdef DEBUG_PRINTOUT
   CheckCtxt(ctxt, "after extractDigitsThin");
#endif

  // restore intFactor
  if (intFactor != 1)
    ctxt.intFactor = MulMod(ctxt.intFactor, intFactor, ptxtSpace);
}

static void
checkCriticalValue(const vector<ZZX>& zzParts, const DoubleCRT& sKey,
                   const RecryptData& rcData, long q)
{
  ZZX ptxt;
  rawDecrypt(ptxt, zzParts, sKey); // no mod q

  Vec<ZZ> powerful;
  rcData.p2dConv->ZZXtoPowerful(powerful, ptxt);
  xdouble max_pwrfl = conv<xdouble>(largestCoeff(powerful));
  double critical_value = conv<double>((max_pwrfl/q)/q);

  vecRed(powerful, powerful, q, false);
  max_pwrfl = conv<xdouble>(largestCoeff(powerful));
  critical_value += conv<double>(max_pwrfl/q);

  FHE_STATS_UPDATE("critical-value", critical_value);

  cerr << "=== critical_value=" << critical_value;
  if (critical_value > 0.5) cerr << " BAD-BOUND";

  cerr << "\n";
}

static void
checkRecryptBounds(const vector<ZZX>& zzParts, const DoubleCRT& sKey,
                   const FHEcontext& context, long q)
{
  const RecryptData& rcData = context.rcData;
  double coeff_bound = context.boundForRecryption();
  long p2r = context.alMod.getPPowR();

  ZZX ptxt;
  rawDecrypt(ptxt, zzParts, sKey); // no mod q

  Vec<ZZ> powerful;
  rcData.p2dConv->ZZXtoPowerful(powerful, ptxt);
  double max_pwrfl = conv<double>(largestCoeff(powerful));
  double ratio = max_pwrfl/(2*q*coeff_bound);

  FHE_STATS_UPDATE("|x|/bound", ratio);

  cerr << "=== |x|/bound=" << ratio;
  if (ratio > 1.0) cerr << " BAD-BOUND";

  vecRed(powerful, powerful, q, false);
  max_pwrfl = conv<double>(largestCoeff(powerful));
  ratio = max_pwrfl/(2*p2r*coeff_bound);

  FHE_STATS_UPDATE("|x%q|/bound", ratio);

  cerr << ", (|x%q|)/bound=" << ratio;
  if (ratio > 1.0) cerr << " BAD-BOUND";

  cerr << "\n";
}

static void
checkRecryptBounds_v(const vector<ZZX>& v, const DoubleCRT& sKey,
                     const FHEcontext& context, long q)
{
  const RecryptData& rcData = context.rcData;

  long p = context.zMStar.getP();
  long e = rcData.e;
  long p2e = power_long(p, e);
  long ePrime = rcData.ePrime;
  long p2ePrime = power_long(p, ePrime);
  long phim = context.zMStar.getPhiM();
  long k = context.zMStar.getNFactors();
  long skHwt = rcData.skHwt;

  double fudge = compute_fudge(p2ePrime, p2e);

  double coeff_bound = context.boundForRecryption() * fudge;

  double sigma = context.stdDevForRecryption() * fudge;

  ZZX ptxt;
  rawDecrypt(ptxt, v, sKey); // no mod q

  Vec<ZZ> powerful;
  rcData.p2dConv->ZZXtoPowerful(powerful, ptxt);
  double max_pwrfl = conv<double>(largestCoeff(powerful));

  double denom = p2ePrime*coeff_bound;
  double ratio = max_pwrfl/denom;

  FHE_STATS_UPDATE("|v|/bound", ratio);

  cerr << "=== |v|/bound=" << ratio;
  if (ratio > 1.0) cerr << " BAD-BOUND";
  cerr << "\n";

  ptxt -= v[0];  // so now ptxt is just sKey * v[1]
  rcData.p2dConv->ZZXtoPowerful(powerful, ptxt);

  helib::assertEq(powerful.length(), phim, "length should be phim");

  double ran_pwrfl = conv<double>(powerful[RandomBnd(phim)]);
  // pick a random coefficient in the poweful basis

  double std_devs = fabs(ran_pwrfl)/(p2ePrime*sigma);
  // number of standard deviations away from mean

  // update various indicator variables
  FHE_STATS_UPDATE("sigma_0_5", double(std_devs <= 0.5)); // 0.383
  FHE_STATS_UPDATE("sigma_1_0", double(std_devs <= 1.0)); // 0.683
  FHE_STATS_UPDATE("sigma_1_5", double(std_devs <= 1.5)); // 0.866
  FHE_STATS_UPDATE("sigma_2_0", double(std_devs <= 2.0)); // 0.954
  FHE_STATS_UPDATE("sigma_2_5", double(std_devs <= 2.5)); // 0.988
  FHE_STATS_UPDATE("sigma_3_0", double(std_devs <= 3.0)); // 0.997, 1 in 370
  FHE_STATS_UPDATE("sigma_3_5", double(std_devs <= 3.5)); // 0.999535, 1 in 2149
  FHE_STATS_UPDATE("sigma_4_0", double(std_devs <= 4.0)); // 0.999937, 1 in 15787

  // compute sample variance, and scale by the variance we expect
  FHE_STATS_UPDATE("sigma_calc", fsquare(ran_pwrfl)/fsquare(p2ePrime*sigma));

  // save the scaled value for application of other tests
  FHE_STATS_SAVE("v_values", ran_pwrfl/(p2ePrime*sigma));
}

#if 0
void fhe_stats_print(long iter, const FHEcontext& context)
{
   long phim = context.zMStar.getPhiM();

   cerr << "||||| recryption stats ||||\n";
   cerr << "**** averages ****\n";
   cerr << "=== critical_value=" << (fhe_stats_cv_sum/iter) << "\n";
   cerr << "=== |x|/bound=" << (fhe_stats_x_sum/iter) << "\n";
   cerr << "=== |x%q|/bound=" << (fhe_stats_xmod_sum/iter) << "\n";
   cerr << "=== |u|/bound=" << (fhe_stats_u_sum/iter) << "\n";
   cerr << "=== |v|/bound=" << (fhe_stats_v_sum/iter) << "\n";
   cerr << "**** maxima ****\n";
   cerr << "=== critical_value=" << (fhe_stats_cv_max) << "\n";
   cerr << "=== |x|/bound=" << (fhe_stats_x_max) << "\n";
   cerr << "=== |x%q|/bound=" << (fhe_stats_xmod_max) << "\n";
   cerr << "=== |u|/bound=" << (fhe_stats_u_max) << "\n";
   cerr << "=== |v|/bound=" << (fhe_stats_v_max) << "\n";
   cerr << "**** theoretical bounds ***\n";
   cerr << "=== single-max=" << (sqrt(2.0*log(phim))/context.scale) << "\n";
   cerr << "=== global-max=" << (sqrt(2.0*(log(iter)+log(phim)))/context.scale) << "\n";

}
#endif