//
//  bpp.cpp
//  PhyloAcc
//
//  Created by hzr on 3/8/16.
//  Copyright © 2016 hzr. All rights reserved.
//

#include "bpp.hpp"
#include <armadillo>
#include <sys/types.h>
#include <dirent.h>
#include<queue>

#include <cmath>
#include <cassert>

#include <gsl/gsl_rng.h>
#include <gsl/gsl_randist.h>
#include <gsl/gsl_linalg.h>
#include <gsl/gsl_cdf.h>

#include <fstream>
#include <sstream>
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <ctype.h>

#include "newick.h"
#include "utils.h"
#include "bpp_c.hpp"

using namespace std;
using namespace arma;

// load the phylogenetic tree
void BPP::InitPhyloTree(PhyloTree & tree) //, double indel_pi), double indel, double indel2
{

//    cx_mat bvec;
//    cx_vec aval;
//
//    if(num_base <= 4)  // no indel
//    {
//
//    }else{
//        tree.subs_rate *= (1-indel);
//        mat B = ones<mat>(4,1) * indel;
//        mat C = ones<mat>(1,5) * indel2;
//        //tree.subs_rate.diag() -= indel;
//        tree.subs_rate.insert_cols(4, B);
//        tree.subs_rate.insert_rows(4, C);
//        //tree.subs_rate(4,4) = -0.04;
//        colvec c = sum(tree.subs_rate,1);
//        tree.subs_rate.diag() -=c;
//
////        tree.pi.insert_rows(4,1);
////        tree.pi.head(4) *= 1 - indel_pi;
////        tree.pi[4] = indel_pi;
//
//    }
//
//    eig_gen(aval, bvec, tree.subs_rate);
//    eigenval = conv_to<mat>::from(aval);
//    eigenvec = conv_to<mat>::from(bvec).t();
//    eigeninv = inv(eigenvec);
//    submat = tree.subs_rate;
//
//    //cout <<eigenval;
//    //cout <<"eigenvec: " << eigenvec;
//    //cout <<tree.subs_rate;
//
//    mat a = null(tree.subs_rate.t());
//
//
//    pi = a/accu(a); //tree.pi;
//    //cout <<"pi: " <<  pi.t();
//
//    log_pi = log(pi);

    submat = tree.subs_rate;
    children    = new int[N][2];
    parent      = new int[N];
    distances   = new double[N];

    for(int s=0; s<N; s++)
    {
        distances[s] = tree.distances[s];

    }

    for(int i=0; i<N; i++)
    {
        children[i][0] = -1;
        children[i][1] = -1;
        parent[i] = -1;
    }

    for(int i=0; i<N; i++)
    {
        int p = -1;
        for(int j=0; j<N; j++)
        {
            if (tree.dag[i][j])
            {
                p++;
                children[i][p] = j;
                parent[j] = i;
            }
        }
    }

//    distances[children[N-1][1]] += distances[children[N-1][0]];
//    distances[children[N-1][0]] = 0;
//    moveroot = children[N-1][0];

    //distances[83] += distances[42];  // modify distance for root!
    //distances[42] =0 ;

}

// try to match the phylogenetic profile and tree
void BPP::MatchProfAndTree(PhyloProf & _prof, PhyloTree & _tree)
{

    // try to match the species
    bool success_match = true;
    int S = _tree.S;
    int S2 = _prof.S;
    vector<int> reorder(S);  //each species in the tree where is in prof
    for(int s1=0; s1<S; s1++)
    {
        bool has_same_species = false;
        string sname1 = _tree.species_names[s1];
        for(int s2=0; s2<S2; s2++)
        {
            string sname2 = _prof.species_names[s2];
            //            cout << sname1 << " ? " << sname2 << endl;
            if (sname1 == sname2)
            {
                has_same_species = true;
                reorder[s1] = s2;
                break;
            }
        }
        if (!has_same_species)
        {
            cout << "No matrix species " << _prof.species_names[s1] << " found in tree." << endl;
            success_match = false;
            break;
        }
    }

    if (!success_match) // if cannot match literally
    {
        cout << endl << "The species in phylogenetic profile and tree cannot be matched literally:" << endl;
        cout << "The program will use the default mapping in data:" << endl;
        for(int s=0; s<S; s++)
            cout << "(" << _prof.species_names[s] << "\t=  " << _tree.species_names[s] << ")" << endl;
        cout << endl;
    }
    else                // if successully matched
    {
        cout << "The species in profile and tree match perfectly. Reorder the species in profile matrix by the tree." << endl << endl;
        vector<string> old_X = _prof.X;
        for(int s=0; s<S; s++)
        {
            int reorder_s = reorder[s];
            _prof.X[s] = old_X[reorder_s];
        }
        _prof.species_names = _tree.species_names;
    }

}

void BPP::InitMCMC(int _num_burn, int _num_mcmc, int _num_thin)
{
    // init parameters
    num_burn = _num_burn;

    num_mcmc = _num_mcmc;
    num_thin = _num_thin;

    last_time = time(NULL);

    // init MCMC sampling storage
    Max_Z = vector<vector < vector <int > >> (3,vector < vector <int >>  (C,vector <int > (N,0)));
    Max_B = vector<vector < vector <bool > >> (3,vector < vector <bool >>  (C,vector <bool > (N,0)));
    cur_Z = vector<vector < vector <int > >> (3,vector < vector <int >>  (C,vector <int > (N,0)));
    //cur_B = vector < vector <double > > (3, vector <double >  (C, 2.0));

    log_liks_null = vector <double>(C,0);
    log_liks_Z = vector<vector <double>>(3,vector <double>(C,0));
    log_liks_sgl = vector <double>(C,0);
    log_liks_resZ = vector <double>(C,0);

    log_liks_curZ = vector <double>(C,0);
    log_liks_propZ = vector <double>(C,0);
    MH_ratio_gain = vector <double>(C,0);
    MH_ratio_loss = vector <double>(C,0);

    cur_crate = vector <double>(C,ratio0);
    cur_nrate = vector <double>(C,ratio1);
    cur_nGB = vector <double>(C,3);
    //cur_cGB = vector <double>(C,0.01);

}

void BPP::sample_proposal(int iter, double & lrate_prop, double & grate_prop, ofstream & output)
{
    //indel_prop =gsl_ran_gamma(RNG,20 ,indel/20);
    //indel2_prop =gsl_ran_gamma(RNG,10 ,indel2/10);
    lrate_prop =gsl_ran_beta(RNG, ind_lrate * vlr, (1 - ind_lrate) *vlr);
    grate_prop =gsl_ran_beta(RNG, ind_grate * vgr, (1 - ind_grate) *vgr); //gsl_ran_gamma(RNG, vgr, ind_grate/vgr);

//    submat.submat(0,0,3,3) *= (1 - indel_prop)/(1 - indel);
//    submat.col(4) = ones<mat>(5,1) * indel_prop;
//    submat.row(4) = ones<mat>(1,5) * indel2_prop;
//    colvec c = sum(submat,1);
//    submat.diag() -=c;
//
//    cx_mat bvec;
//    cx_vec aval;
//    eig_gen(aval, bvec, submat);
//    eigenvalprop = conv_to<mat>::from(aval);
//    eigenvecprop = conv_to<mat>::from(bvec).t();
//    eigeninvprop = inv(eigenvecprop);
//
//    mat a = null(submat.t());
//    piprop = a/accu(a);
//    //log_piprop = log(a/accu(a));
//
//    //cout <<"proposed subs matrix: " << submat << endl;
//    //cout <<"piprop: " <<  piprop.t();

    output << iter << "\t"<< nprior_a<< "\t"<< nprior_b <<"\t"<< cprior_a << "\t"<< cprior_b << "\t"<< indel << "\t"<< indel2 << "\t"<<ind_grate<< "\t"<< ind_lrate <<endl;
}

double BPP::log_lik(vector< vector<vec> > & lambda, double _indel, double _indel2, int start1, int end1, vector<unsigned int> & v, double p)
{
    // compute loglik
    double result =0;
    mat x(2,2);

    int rr = *subtree.rbegin();
    // 1. sending the lambda msg from leaves bottom up through the network
    for(vector<int>::iterator it = subtree.begin(); it!=subtree.end(); it++) //int s=S; s<N; s++)
    {
        int s = *it;
        if(s<S) continue;
        int* p = children[s];
        for(int it = start1; it < end1; it++)  lambda[v[it]][s].fill(0);

        for(int cc=0;cc<2;cc++)
        {
            int chi = p[cc];
            assert(chi != -1);
            if(distances[chi]>0 )
            {
                double tt = (1 - exp(-(_indel + _indel2) * distances[chi]))/(_indel + _indel2);
                x.at(1,0) = _indel * tt;
                x.at(0,0) = 1 - x.at(1,0);

                x.at(0,1) = _indel2 * tt;
                x.at(1,1) = 1 - x.at(0,1);

                //cout << x;
                x = log(x);

            }
            else{
                x.fill(-INFINITY); //83, root
                x.diag().fill(0);
            }

            #pragma omp parallel for schedule (guided)
            for(int it = start1; it < end1; it++)  lambda[v[it]][s] +=  BPP::log_multi(x,lambda[v[it]][chi]);
        }

    }

    // 2. processing the distribution of root species

    for(int it = start1; it < end1; it++)
    {

//        if(lambda[v[it]][N-1][1] < -1e3)
//        {
//            cout <<v[it]<<": "<< lambda[v[it]][N-1].t();
//            cout <<children[N-1][0]<<": " << lambda[v[it]][children[N-1][0]].t();
//            cout <<children[N-1][1]<<": " << lambda[v[it]][children[N-1][1]].t();
//
//        }
        lambda[v[it]][rr][0] += log(1-p); //N-1
        lambda[v[it]][rr][1] += log(p) ;
        result += BPP::log_exp_sum(lambda[v[it]][rr]);
    }

    return(result);
}

void BPP::sample_hyperparam(double lrate_prop, double grate_prop) // recompute log_TM, double indel_prop, double indel2_prop,
{

    //indepent MH to sample hyperparam of rates
    double p=1,r = 1; //hyperparam for shape
    double q=0.1,s = 0.1; // hyperparam for scale

    double vna = 100, vnb = 100, vca = 100, vcb = 100;
    double nprior_a_prop =gsl_ran_gamma(RNG, vna, nprior_a/vna);
    double cprior_a_prop =gsl_ran_gamma(RNG, vca, cprior_a/vca);

    double nprior_b_prop =gsl_ran_gamma(RNG, vnb, nprior_b/vnb);
    double cprior_b_prop =gsl_ran_gamma(RNG, vcb, cprior_b/vcb);

    //MH proposal
    double sum_r = 0;
    double log_prod_r = 0;
    //double var_r = 0;
    for(vector<double>::iterator it = cur_nrate.begin(); it< cur_nrate.end(); it++)
    {
        sum_r += *it;
        //var_r += pow(*it, 2);
        log_prod_r += log(*it);
    }

    double M_ratio = (nprior_a_prop - 1) * (log(p) + log_prod_r) - (q + sum_r)/nprior_b_prop - (C + r)*lgamma(nprior_a_prop) - log(nprior_b_prop) * nprior_a_prop * (s + C);
    M_ratio -= (nprior_a - 1) * (log(p) + log_prod_r) - (q + sum_r)/nprior_b - (C + r)*lgamma(nprior_a) - log(nprior_b) * nprior_a * (s + C);

    double H_ratio = log(gsl_ran_gamma_pdf(nprior_a,vna,nprior_a_prop/vna)) - log(gsl_ran_gamma_pdf(nprior_a_prop,vna,nprior_a/vna)) + log(gsl_ran_gamma_pdf(nprior_b,vnb,nprior_b_prop/vnb)) - log(gsl_ran_gamma_pdf(nprior_b_prop,vnb,nprior_b/vnb));

    cout << "nrate_MH_ratio: " << M_ratio <<", " << H_ratio << ", " << nprior_a << ", " << nprior_a_prop << ", " << nprior_b << ", " << nprior_b_prop << endl;

    if(log(gsl_rng_uniform(RNG)) < M_ratio + H_ratio)
    {
        nprior_a = nprior_a_prop;
        nprior_b = nprior_b_prop;
    }

    sum_r = 0;
    log_prod_r = 0;
    for(vector<double>::iterator it = cur_crate.begin(); it< cur_crate.end(); it++)
    {
        sum_r += *it;
        //var_r += pow(*it, 2);
        log_prod_r += log(*it);
    }

    M_ratio = (cprior_a_prop - 1) * (log(p) + log_prod_r) - (q + sum_r)/cprior_b_prop - (C + r)*lgamma(cprior_a_prop) - log(cprior_b_prop) * cprior_a_prop * (s + C) - ((cprior_a - 1) * (log(p) + log_prod_r) - (q + sum_r)/cprior_b - (C + r)*lgamma(cprior_a) - log(cprior_b) * cprior_a * (s + C));

    H_ratio = log(gsl_ran_gamma_pdf(cprior_a,vca,cprior_a_prop/vca)) - log(gsl_ran_gamma_pdf(cprior_a_prop,vca,cprior_a/vca)) + log(gsl_ran_gamma_pdf(cprior_b,vcb,cprior_b_prop/vcb)) - log(gsl_ran_gamma_pdf(cprior_b_prop,vcb,cprior_b/vcb));

    cout << "crate_MH_ratio: " << M_ratio + H_ratio << ", " << cprior_a << ", " << cprior_a_prop <<", " << cprior_b << ", " << cprior_b_prop << endl;

    if(log(gsl_rng_uniform(RNG)) < M_ratio + H_ratio)
    {
        cprior_a = cprior_a_prop;
        cprior_b = cprior_b_prop;
    }

    //MH to sample lrate, grate
    //lrate
    double MH_ratio =0;
    for(int c =0 ; c<C; c++)
    {
        MH_ratio += MH_ratio_gain[c];
    }

    MH_ratio += log(gsl_ran_beta_pdf(ind_grate, grate_prop*vgr, (1-grate_prop)*vgr)) - log(gsl_ran_beta_pdf(grate_prop, ind_grate*vgr, (1-ind_grate)*vgr));

    cout << "grate: " << MH_ratio << ", " << ind_grate << ", " << grate_prop << endl;

    if(log(gsl_rng_uniform(RNG)) < MH_ratio)
    {
        ind_grate = grate_prop;
        for(int s=0; s<N; s++)
        {

            //double x = exp(-ind_grate *distances[s]);
            double x = 1 - ind_grate;
            TM_Int[s](0,0) = x;
            TM_Int[s](1,0) = 1-x;

        }
    }

    MH_ratio = 0;
    for(int c =0 ; c<C; c++)
    {
        MH_ratio += MH_ratio_loss[c];
    }
    MH_ratio += log(gsl_ran_beta_pdf(ind_lrate, lrate_prop*vlr, (1-lrate_prop)*vlr)) - log(gsl_ran_beta_pdf(lrate_prop, ind_lrate*vlr, (1-ind_lrate)*vlr));

    cout << "lrate: " << MH_ratio << ", " << ind_lrate << ", " << lrate_prop << endl << endl;

    if(log(gsl_rng_uniform(RNG)) < MH_ratio)
    {
        ind_lrate = lrate_prop;
        for(vector<int>::iterator it = subtree.begin(); it<subtree.end(); it++)
        {
            int s = *it;
            //double y = exp(-ind_lrate *distances[s]);
            double y = 1 - ind_lrate;
            TM_Int[s](1,1) = y;
            TM_Int[s](2,1) = 1-y;

        }
    }

    for(int s=0; s<N; s++){
        log_TM_Int[s] = log(TM_Int[s]);
    }

}

void BPP::getUppertree(int root, vector<int>& child, set<int> & visited_init)  // include root!
{
    for(vector<int>::iterator it = child.begin(); it!=child.end(); it++)
    {
        int p = *it;
       while(p!=root)
       {
           visited_init.insert(p);
           p = parent[p];
       }

    }

    visited_init.insert(root);

}

void BPP::getSubtree(int root, vector<int> & visited_init)  // traverse from root to children, include root
{

    int j = root;

    //cout << nodes_names[j]<<"\t";

    if(children[j][0]!=-1)
    {

        for(int chi=0;chi<2;chi++)
        {
            getSubtree(children[j][chi], visited_init);

        }

    }

    visited_init.push_back(j);

}

void BPP::getSubtree(int root, set<int>& child, vector<int> & visited_init)  // traverse from root, stop at children, 74 & 64; do include 1-S!
{

    int j = root;

    //cout << nodes_names[j]<<"\t";
    if(child.find(j) != child.end())
    {

        visited_init.push_back(j);

        return;
    }

    if(children[j][0]!=-1)
    {

        for(int chi=0;chi<2;chi++)
        {
            getSubtree(children[j][chi], child, visited_init);

        }

      //  visited_init.push_back(j);

    }

     visited_init.push_back(j);

}

void BPP::Output_init(PhyloProf & prof, string output_path){

        string outpath_elem = output_path+ "_elem_lik.txt";
        ofstream out_lik(outpath_elem.c_str());
        out_lik.precision(8);
        out_lik << "No.\tID\tloglik_NUll\tloglik_RES\tloglik_all\tlog_ratio\tloglik_Max1\tloglik_Max2\tloglik_Max3"<<endl;
        for(int cc=0; cc<C;cc++)
        {
            out_lik <<cc << "\t" << prof.element_names[cc] << "\t" << log_liks_null[cc] <<"\t"  <<log_liks_resZ[cc] <<"\t"  <<log_liks_sgl[cc]<< "\t"<< log_liks_resZ[cc] -  log_liks_null[cc];
            for(int r=0;r<3;r++) out_lik <<"\t" <<log_liks_Z[r][cc];
            out_lik << endl;
        }

        out_lik.close();

    ofstream out_z;
    for(int r =1;r<2;r++)
    {
            outpath_elem = output_path+"_" +to_string(r) + "_elem_Z.txt";
            out_z.open(outpath_elem.c_str());

                for(int s =0 ;s<N;s++){  // header: species name
                    out_z<<nodes_names[s] << "_B\t";
                }
        for(int s =0 ;s<N;s++){  // header: species name
            out_z<<nodes_names[s] << "\t";
        }
                out_z <<endl;
            for(int c=0;c<C;c++)
            {
                    for(int s=0; s<N;s++)
                            out_z<<Max_Z[r][c][s]<<"\t";
                    out_z <<endl;
            }
        out_z.close();
    }

}

void BPP::Output_init0(PhyloProf & prof, ofstream& out_lik){

    for(int cc=0; cc<C;cc++)
    {
        out_lik <<cc << "\t" << prof.element_names[cc] << "\t"  <<log_liks_sgl[cc]<< "\t"<<log_liks_Z[1][cc];
        out_lik << endl;
    }

    //out_lik.close();

}


/*
 * The Conv integrators will implicitly determine the structure of the previous
 * layer, as they have to be structured in a specific way. The Net and reservoir
 * however, specify the connections either within or between layers.
 * Within a layer, a neighbor graph is used, where sources of the connections
 * correspond with nodes in the same graph.
 * Between layers, the sources of the connections correspond to the nodes
 * of the previous layer, while the destination corresponds with nodes in the
 * current layer.
 *
 * Integrators don't actually have to know the real size/shape of the previous
 * layer. Any size/shape can be given so long as the # elements is less than
 * then number in the previous layer so that non-existent elements are not
 * accessed. The integrator will create its own view into the previous layer
 * with the shape provided it, regardless of whether that layer has that shape.
 */

#ifndef NN_INTEGRATOR_H_
#define NN_INTEGRATOR_H_

#include <cstddef>
#include <vector>
#include <stdexcept>
#include <iostream>
#include <numeric>
#include <functional>
#include <utility>
#include <Eigen/Core>
#include <Eigen/Sparse>
#include "../common/multi_array.hpp"
#include "../common/graphs.hpp"
#include "parameter_types.hpp"
#include "../common/utilities.hpp"

namespace nervous_system {

enum INTEGRATOR_TYPE {
  BASE_INTEGRATOR,
  NONE_INTEGRATOR,
  ALL2ALL_INTEGRATOR,
  CONV_INTEGRATOR,
  RECURRENT_INTEGRATOR,
  RESERVOIR_INTEGRATOR,
  RESERVOIR_HYBRID,
  TRUNCATED_RECURRENT_INTEGRATOR,
  CONV_EIGEN_INTEGRATOR,
  ALL2ALL_EIGEN_INTEGRATOR,
  RECURRENT_EIGEN_INTEGRATOR,
  RESERVOIR_EIGEN_INTEGRATOR,
  REWARD_MODULATED_INTEGRATOR,
  REWARD_MODULATED_ALL2ALL_INTEGRATOR,
  REWARD_MODULATED_RECURRENT_INTEGRATOR,
  REWARD_MODULATED_CONV_INTEGRATOR,
  REWARD_MODULATED
};

// Abstract base class
template<typename TReal>
class Integrator {
  public:
    typedef std::size_t Index;

    Integrator() {
      integrator_type_ = BASE_INTEGRATOR;
      parameter_count_ = 0;
    }
    virtual ~Integrator()=default;
    virtual void operator()(const multi_array::Tensor<TReal>& src_state,
                            multi_array::Tensor<TReal>& tar_state)=0;
    virtual void Configure(const multi_array::ConstArraySlice<TReal>& parameters)=0;

    std::size_t GetParameterCount() const {
      return parameter_count_;
    }

    INTEGRATOR_TYPE GetIntegratorType() const {
      return integrator_type_;
    }

    virtual std::vector<PARAMETER_TYPE> GetParameterLayout() const=0;

    /*
     * Returns a pair representing the start and stop indices for the parameters
     * associated with link weights. These should always be contiguous.
     */
    virtual std::pair<Index, Index> GetWeightIndexRange() const=0;

  protected:
    INTEGRATOR_TYPE integrator_type_;
    std::size_t parameter_count_;
};

template <typename TReal>
class RewardModulatedIntegrator : public virtual Integrator<TReal>
{
  public:
    typedef std::size_t Index;
    typedef Integrator<TReal> super_type;

    RewardModulatedIntegrator(const TReal learning_rate)
        : super_type(), learning_rate_(learning_rate) {
    }
    virtual ~RewardModulatedIntegrator()= default;
    virtual void UpdateWeights(const TReal reward,
                               const TReal reward_average,
                               const multi_array::Tensor<TReal>& src_state,
                               const multi_array::Tensor<TReal>& tar_state,
                               const multi_array::Tensor<TReal>& tar_state_averages)=0;
    virtual const multi_array::Tensor<TReal>& GetWeights() const=0;

  protected:
    const TReal learning_rate_;
};

// None integrator - does nothing
template<typename TReal>
class NoneIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;

    NoneIntegrator() : super_type() { super_type::integrator_type_ = NONE_INTEGRATOR; }
    virtual ~NoneIntegrator()=default;

    virtual void operator()(const multi_array::Tensor<TReal>& src_state, multi_array::Tensor<TReal>& tar_state) {}
    virtual void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {}

    virtual std::vector<PARAMETER_TYPE> GetParameterLayout() const {
      return std::vector<PARAMETER_TYPE>(super_type::parameter_count_);
    }

    virtual std::pair<Index, Index> GetWeightIndexRange() const {
      return std::make_pair(0, 0);
    }
};

// Integrator that has All2All connectivity with previous layer
template<typename TReal>
class All2AllIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;

    All2AllIntegrator(Index num_states, Index num_prev_states)
        : num_states_(num_states), num_prev_states_(num_prev_states) {
      super_type::parameter_count_ = num_states_ * num_prev_states_;
      super_type::integrator_type_ = ALL2ALL_INTEGRATOR;
    }

    virtual void operator()(const multi_array::Tensor<TReal>& src_state, multi_array::Tensor<TReal>& tar_state) {
      if (!((src_state.size() == num_prev_states_) && (tar_state.size() == num_states_))) {
        std::cerr << "src state size: " << src_state.size() << std::endl;
        std::cerr << "prev state size: " << num_prev_states_ << std::endl;
        std::cerr << "tar state size: " << tar_state.size() << std::endl;
        std::cerr << "state size: " << num_states_ << std::endl;
        throw std::invalid_argument("src state size and prev state size "
                                    "must be equal. tar state size and state"
                                    " size must be equal");
      }
      Index weight_id = 0;
      for (Index iii = 0; iii < tar_state.size(); ++iii) {
        TReal cumulative_sum = 0.0;
        for (Index jjj = 0; jjj < src_state.size(); ++jjj) {
          cumulative_sum += src_state[jjj] * weights_[weight_id];
          ++weight_id;
        }
        tar_state[iii] = utilities::BoundState(cumulative_sum);
      }
    }

    virtual void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {
      if (parameters.size() != super_type::parameter_count_) {
        std::cerr << "parameter size: " << parameters.size() << std::endl;
        std::cerr << "parameter count: " << super_type::parameter_count_ << std::endl;
        throw std::invalid_argument("Wrong number of parameters");
      }
      weights_ = multi_array::ConstArraySlice<TReal>(parameters);
    }

    virtual std::vector<PARAMETER_TYPE> GetParameterLayout() const {
      std::vector<PARAMETER_TYPE> layout(super_type::parameter_count_);
      for (Index iii = 0; iii < super_type::parameter_count_; ++iii) {
        layout[iii] = WEIGHT;
      }
      return layout;
    }

    virtual std::pair<Index, Index> GetWeightIndexRange() const {
      return std::make_pair(0, super_type::parameter_count_);
    }

  protected:
    Index num_states_;
    Index num_prev_states_;
    multi_array::ConstArraySlice<TReal> weights_;
};

/*
 * Conv integrator - uses implicit structure
 * Uses separable filters, so # params for a KxHxW kernel is K*(H+W)
 *
 * F = layer.shape[0]
 * H/W = filter.shape[1/2]
 * D = filter.shape[0]
 * filter_shape and layer_shape implicitly contain most the information for
 * the shape of the previous layer. Because this is a separable filter
 * the # of parameters for each filter is (W * D) + (H * D).
 * This convolution uses spatially and depthwise separable convolutions.
 * First F HxW convolutions are applied to each input dimension D.
 * Then F Dx1x1 filters are applied to the results.
 * Principle behind DxHxW + DxFx1x1 is that the same filter is used on all
 * layers, and then a 1x1 filter is used to do a weighted sum. This is
 * opposed to having a separate filer for each input channel
 * (each 1x1 needs D parameters, with a total of F 1x1 filters)
 */
template<typename TReal>
class Conv2DIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;

    Conv2DIntegrator(const multi_array::Array<Index,3>& filter_shape,
        const multi_array::Array<Index,3>& layer_shape,
        const multi_array::Array<Index,3>& prev_layer_shape, Index stride)
        : num_filters_(layer_shape[0]), layer_shape_(layer_shape),
        prev_layer_shape_(prev_layer_shape),
        filter_shape_(filter_shape), stride_(stride),
        filter_parameters_major_({num_filters_}),
        filter_parameters_minor_({num_filters_}),
        channel_weights_({num_filters_}) {

      min_src_size_ = std::accumulate(prev_layer_shape_.begin(),
        prev_layer_shape_.end(), 1, std::multiplies<TReal>());
      min_tar_size_ = std::accumulate(layer_shape_.begin(), layer_shape_.end(),
        1, std::multiplies<TReal>());
      // NumDim should be 3 for stride calculation
      multi_array::CalculateStrides(layer_shape_.data(), layer_strides_.data(), 3);
      multi_array::CalculateStrides(prev_layer_shape_.data(), prev_layer_strides_.data(), 3);
      if (filter_shape[0] != prev_layer_shape[0]) {
        std::cerr << "first filter shape: " << filter_shape[0] << std::endl;
        std::cerr << "first prev layer shape: " << prev_layer_shape[0] << std::endl;
        throw std::invalid_argument("First dimensions must be equal.");
      }

      super_type::parameter_count_ = num_filters_
        * (filter_shape_[2] + filter_shape_[1] + filter_shape_[0]);
      super_type::integrator_type_ = CONV_INTEGRATOR;
      firstpass_buffer_ = multi_array::Tensor<TReal>(
                            {prev_layer_shape[1], layer_shape[2]});
      secondpass_buffer_ = multi_array::Tensor<TReal>(
                            {layer_shape[1], layer_shape[2]});
    }
    virtual ~Conv2DIntegrator()=default;

    virtual void operator()(const multi_array::Tensor<TReal>& src_state,
                            multi_array::Tensor<TReal>& tar_state) {
      /*
       * tar_state needs to have AT LEAST as many elements as required by
       * layer_shape, but it doesn't not need to have the same shape.
       * tar_state will be re-interpreted as the same shape as layer_shape
       * src_state will be reinterpreted as the same shape as prev_layer_shape
       */
      if (src_state.size() < min_src_size_) {
        throw std::invalid_argument("Src state too small for integrator");
      }
      const multi_array::TensorView<TReal> src_view(src_state.data(),
        prev_layer_strides_.data(), prev_layer_shape_.data());

      if (tar_state.size() < min_tar_size_) {
        throw std::invalid_argument("tar state too small for integrator");
      }
      multi_array::TensorView<TReal> tar_view(tar_state.data(),
        layer_strides_.data(), layer_shape_.data());

      multi_array::ArrayView<multi_array::ConstArraySlice<TReal>, 1> major_view(filter_parameters_major_);
      multi_array::ArrayView<multi_array::ConstArraySlice<TReal>, 1> minor_view(filter_parameters_minor_);
      multi_array::ArrayView<multi_array::ConstArraySlice<TReal>, 1> channel_view(channel_weights_);

      // for each filter
      for (Index iii = 0; iii < num_filters_; iii++) {
        multi_array::TensorView<TReal> tar_image = tar_view[iii];
        // for each input channel
        for (Index jjj = 0; jjj < src_view.extent(0); jjj++) {
          // Carry out separable conv on layer of inputs
          const multi_array::TensorView<TReal> src_image = src_view[jjj];
          multi_array::TensorView<TReal> secondpass_image = secondpass_buffer_.accessor();
          multi_array::TensorView<TReal> firstpass_image = firstpass_buffer_.accessor();
          Convolve2D(src_image, secondpass_image, firstpass_image,
                     major_view[iii], minor_view[iii], stride_);

          for (Index kkk = 0; kkk < tar_view.extent(1); ++kkk) {
            for (Index lll = 0; lll < tar_view.extent(2); ++lll) {
              tar_image[kkk][lll] += secondpass_image[kkk][lll] * channel_weights_[iii][jjj];
              tar_image[kkk][lll] = utilities::BoundState<TReal>(tar_image[kkk][lll]);
            }
          }
        }
      }
    }

    void Convolve2D(const multi_array::TensorView<TReal>& src,
        multi_array::TensorView<TReal>& tar,
        multi_array::TensorView<TReal>& buffer,
        const multi_array::ConstArraySlice<TReal> &kernel_minor,
        const multi_array::ConstArraySlice<TReal> &kernel_major,
        Index stride) {

      // If stride is larger than the kernel, cumulative sum is reset
      if (stride < kernel_major.size()) {
        // Accumulate through major axis
        Index half_kernel = kernel_major.size() / 2;
        // Minor axis loop
        TReal cumulative_sum = 0.0;
        Index output_index = 0;
        for (Index iii = 0; iii < src.extent(0); ++iii) {
          cumulative_sum = 0.0;
          output_index = 0;
          // Initiate sum
          // Add out-of-bounds components
          for (Index kkk = 0; kkk < half_kernel; kkk++) {
            cumulative_sum += kernel_major[kkk] * src[iii][0];
          }
          // Add in-bounds components
          for (Index kkk = half_kernel; kkk < kernel_major.size(); kkk++) {
            cumulative_sum += kernel_major[kkk] * src[iii][kkk - half_kernel];
          }
          buffer[iii][output_index++] = cumulative_sum;

          // Roll sum (out-of-bounds components)
          Index jjj = stride;
          for (; jjj < half_kernel+stride; jjj+=stride) {
            for (Index kkk = 0; kkk < stride; ++kkk) {
              cumulative_sum += kernel_major[kernel_major.size() - kkk - 1] * src[iii][jjj + half_kernel - kkk]
                              - kernel_major[kkk] * src[iii][0];
            }
            buffer[iii][output_index++] = cumulative_sum;
          }

          //  Roll sum (in-bounds components)
          for (; jjj < src.extent(1)-half_kernel; jjj+=stride) {
            for (Index kkk = 0; kkk < stride; ++kkk) {
              cumulative_sum += kernel_major[kernel_major.size() - kkk - 1] * src[iii][jjj + half_kernel - kkk]
                              - kernel_major[kkk] * src[iii][jjj - half_kernel - 1 - kkk];
            }
            buffer[iii][output_index++] = cumulative_sum;
          }

          // Roll sum (out-of-bounds end components)
          for (; jjj < src.extent(1); jjj+=stride) {
            for (Index kkk = 0; kkk < stride; ++kkk) {
              cumulative_sum += kernel_major[kernel_major.size() - kkk - 1] * src[iii][src.extent(1)-1]
                              - kernel_major[kkk] * src[iii][jjj - half_kernel - 1 - kkk];
            }
            buffer[iii][output_index++] = cumulative_sum;
          }
        }
      }
      else {
        // Accumulate through major axis
        Index half_kernel = kernel_major.size() / 2;
        // Minor axis loop
        TReal cumulative_sum = 0.0;
        Index output_index = 0;
        for (Index iii = 0; iii < src.extent(0); ++iii) {
          cumulative_sum = 0.0;
          output_index = 0;
          // Initiate sum
          // Add out-of-bounds components
          for (Index kkk = 0; kkk < half_kernel; kkk++) {
            cumulative_sum += kernel_major[kkk] * src[iii][0];
          }
          // Add in-bounds components
          for (Index kkk = half_kernel; kkk < kernel_major.size(); kkk++) {
            cumulative_sum += kernel_major[kkk] * src[iii][kkk - half_kernel];
          }
          buffer[iii][output_index++] = cumulative_sum;

          //  Roll sum (in-bounds components)
          Index jjj = stride;
          for (; jjj < src.extent(1)-half_kernel; jjj+=stride) {
            cumulative_sum = 0.0;
            for (Index kkk = 0; kkk < kernel_major.size(); ++kkk) {
              cumulative_sum += kernel_major[kkk] * src[iii][jjj - half_kernel + kkk];
            }
            buffer[iii][output_index++] = cumulative_sum;
          }

          // Roll sum (out-of-bounds end components)
          for (; jjj < src.extent(1); jjj+=stride) {
            cumulative_sum = 0.0;
            // Add in-bounds components
            for (Index kkk = 0; kkk < half_kernel+1; kkk++) {
              cumulative_sum += kernel_major[kkk] * src[iii][jjj - half_kernel + kkk];
            }
            // Add out-of-bounds components
            for (Index kkk = half_kernel+1; kkk < kernel_major.size(); kkk++) {
              cumulative_sum += kernel_major[kkk] * src[iii][src.extent(1)-1];
            }
            buffer[iii][output_index++] = cumulative_sum;
          }
        }
      }
      if (stride < kernel_minor.size()) {
        // Accumulate through minor axis
        TReal cumulative_sum = 0.0;
        Index output_index = 0;
        Index half_kernel = kernel_minor.size() / 2;
        for (Index iii = 0; iii < buffer.extent(1); iii++) {
          cumulative_sum = 0.0;
          output_index = 0;
          // Initiate sum
          // Add out-of-bounds components
          for (Index kkk = 0; kkk < half_kernel; kkk++) {
            cumulative_sum += kernel_minor[kkk] * buffer[0][iii];
          }
          // Add in-bounds components
          for (Index kkk = half_kernel; kkk < kernel_minor.size(); kkk++) {
            cumulative_sum += kernel_minor[kkk] * buffer[kkk - half_kernel][iii];
          }
          tar[output_index++][iii] = cumulative_sum;

          // Roll sum (out-of-bounds components)
          Index jjj = stride;
          for (; jjj < half_kernel+stride; jjj+=stride) {
            for (Index kkk = 0; kkk < stride; ++kkk) {
              cumulative_sum += kernel_major[kernel_minor.size() - kkk - 1] * buffer[jjj + half_kernel - kkk][iii]
                              - kernel_minor[kkk] * buffer[0][iii];
            }
            tar[output_index++][iii] = cumulative_sum;
          }

          //  Roll sum (in-bounds components)
          for (; jjj < buffer.extent(0)-half_kernel; jjj+=stride) {
            for (Index kkk = 0; kkk < stride; ++kkk) {
              cumulative_sum += kernel_major[kernel_minor.size() - kkk - 1] * buffer[jjj + half_kernel - kkk][iii]
                              - kernel_minor[kkk] * buffer[jjj - half_kernel - 1 - kkk][iii];
            }
            tar[output_index++][iii] = cumulative_sum;
          }

          // Roll sum (out-of-bounds end components)
          for (; jjj < buffer.extent(0); jjj+=stride) {
            for (Index kkk = 0; kkk < stride; ++kkk) {
              cumulative_sum += kernel_major[kernel_minor.size() - kkk - 1] * buffer[src.extent(0)-1][iii]
                              - kernel_minor[kkk] * buffer[jjj - half_kernel - 1 - kkk][iii];
            }
            tar[output_index++][iii] = cumulative_sum;
          }
        }
      }
      else {

        // Accumulate through major axis
        Index half_kernel = kernel_minor.size() / 2;
        TReal cumulative_sum = 0.0;
        Index output_index = 0;
        // Minor axis loop
        for (Index iii = 0; iii < buffer.extent(1); ++iii) {
          cumulative_sum = 0.0;
          output_index = 0;
          // Initiate sum
          // Add out-of-bounds components
          for (Index kkk = 0; kkk < half_kernel; kkk++) {
            cumulative_sum += kernel_minor[kkk] * buffer[0][iii];
          }
          // Add in-bounds components
          for (Index kkk = half_kernel; kkk < kernel_minor.size(); kkk++) {
            cumulative_sum += kernel_minor[kkk] * buffer[kkk - half_kernel][iii];
          }
          tar[output_index++][iii] = cumulative_sum;

          //  Roll sum (in-bounds components)
          Index jjj = stride;
          for (; jjj < buffer.extent(0)-half_kernel; jjj+=stride) {
            cumulative_sum = 0.0;
            for (Index kkk = 0; kkk < kernel_minor.size(); ++kkk) {
              cumulative_sum += kernel_minor[kkk] * buffer[jjj - half_kernel + kkk][iii];
            }
            tar[output_index++][iii] = cumulative_sum;
          }

          // Roll sum (out-of-bounds end components)
          for (; jjj < buffer.extent(0); jjj+=stride) {
            cumulative_sum = 0.0;
            // Add in-bounds components
            for (Index kkk = 0; kkk < half_kernel+1; kkk++) {
              cumulative_sum += kernel_minor[kkk] * buffer[jjj - half_kernel + kkk][iii];
            }
            // Add out-of-bounds components
            for (Index kkk = half_kernel+1; kkk < kernel_minor.size(); kkk++) {
              cumulative_sum += kernel_minor[kkk] * buffer[buffer.extent(0)-1][iii];
            }
            tar[output_index++][iii] = cumulative_sum;
          }
        }
      }
    }

    virtual void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {

      if (parameters.size() != super_type::parameter_count_) {
        std::cerr << "parameter size: " << parameters.size() << std::endl;
        std::cerr << "parameter count: " << super_type::parameter_count_ << std::endl;
        throw std::invalid_argument("Wrong number of parameters");
      }
      multi_array::ArrayView< multi_array::ConstArraySlice<TReal>, 1> major_view(filter_parameters_major_);
      multi_array::ArrayView< multi_array::ConstArraySlice<TReal>, 1> minor_view(filter_parameters_minor_);
      multi_array::ArrayView< multi_array::ConstArraySlice<TReal>, 1> channel_view(channel_weights_);

      Index start(0);
      for (Index filter = 0; filter < num_filters_; filter++) {
        major_view[filter] = parameters.slice(start, filter_shape_[2]);
        start += parameters.stride() * filter_shape_[2];

        minor_view[filter] = parameters.slice(start, filter_shape_[1]);
        start += parameters.stride() * filter_shape_[1];

        channel_view[filter] = parameters.slice(start, filter_shape_[0]);
        start += parameters.stride() * filter_shape_[0];
      }
    }

    virtual std::vector<PARAMETER_TYPE> GetParameterLayout() const {
      std::vector<PARAMETER_TYPE> layout(super_type::parameter_count_);
      for (Index iii = 0; iii < super_type::parameter_count_; ++iii) {
        layout[iii] = WEIGHT;
      }
      return layout;
    }

    virtual std::pair<Index, Index> GetWeightIndexRange() const {
      return std::make_pair(0, super_type::parameter_count_);
    }

    const multi_array::Array<Index, 3>& GetFilterShape() const {
      return filter_shape_;
    };

    Index GetMinTarSize() const {
      return min_tar_size_;
    }

  protected:
    Index num_filters_;
    multi_array::Array<Index, 3> layer_shape_;
    multi_array::Array<Index, 3> prev_layer_shape_;
    multi_array::Array<Index, 3> filter_shape_;
    Index stride_;
    Index min_src_size_;
    Index min_tar_size_;
    multi_array::Array<Index, 3> prev_layer_strides_;
    multi_array::Array<Index, 3> layer_strides_;
    multi_array::MultiArray<multi_array::ConstArraySlice<TReal>, 1> filter_parameters_major_;
    multi_array::MultiArray<multi_array::ConstArraySlice<TReal>, 1> filter_parameters_minor_;
    multi_array::MultiArray<multi_array::ConstArraySlice<TReal>, 1> channel_weights_;
    multi_array::Tensor<TReal> secondpass_buffer_;
    multi_array::Tensor<TReal> firstpass_buffer_;
};

// Network integrator -- uses explicit unweighted structure
template<typename TReal>
class RecurrentIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;

    RecurrentIntegrator(const graphs::PredecessorGraph<>& network)
        : network_(network) {
      super_type::integrator_type_ = RECURRENT_INTEGRATOR;
      super_type::parameter_count_ = network_.NumEdges();
    }

    virtual ~RecurrentIntegrator()=default;

    virtual void operator()(const multi_array::Tensor<TReal>& src_state,
                    multi_array::Tensor<TReal>& tar_state) {

      /*
       * Graph maybe a connector graph or an internal graph.
       * In case of connector, the num nodes doesn't need to match tar_state,
       * because predecessors should be empty for nodes not in tar_state.
       * However, src state does have to be checked using (at) when tar_state
       * is larger than src_state, to ensure nothing invalid is accessed.
       */
      Index edge_id = 0;
      for (Index node = 0; node < network_.NumNodes(); ++node) {
        for (Index iii = 0; iii < network_.Predecessors(node).size(); ++iii) {
          tar_state.at(node) += src_state.at(network_.Predecessors(node)[iii].source) * weights_[edge_id];
          tar_state[node] = utilities::BoundState(tar_state[node]);
          ++edge_id;
        }
      }
      if (edge_id != network_.NumEdges()) {
        throw std::runtime_error("Miss match between number of edges and the"
                                 " number integrated");
      }
    }

    virtual void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {
      if (parameters.size() != super_type::parameter_count_) {
        std::cerr << "parameter size: " << parameters.size() << std::endl;
        std::cerr << "parameter count: " << super_type::parameter_count_ << std::endl;
        throw std::invalid_argument("Wrong number of parameters");
      }
      weights_ = parameters.slice(0, super_type::parameter_count_);
    }

    virtual std::vector<PARAMETER_TYPE> GetParameterLayout() const {
      std::vector<PARAMETER_TYPE> layout(super_type::parameter_count_);
      for (Index iii = 0; iii < super_type::parameter_count_; ++iii) {
        layout[iii] = WEIGHT;
      }
      return layout;
    }

    const multi_array::ConstArraySlice<TReal>& GetWeights() const {
      return weights_;
    }

    virtual std::pair<Index, Index> GetWeightIndexRange() const {
      return std::make_pair(0, super_type::parameter_count_);
    }

    const graphs::PredecessorGraph<>& GetGraph() const {
      return network_;
    }

  protected:
    graphs::PredecessorGraph<> network_;
    multi_array::ConstArraySlice<TReal> weights_;
};

// Truncated Recurrent integrator sets weights to 0 during calculations if
// they are below the magnitude of the threshold.
template<typename TReal>
class TruncatedRecurrentIntegrator : public virtual RecurrentIntegrator<TReal> {
  public:
    typedef RecurrentIntegrator<TReal> super_type;
    typedef typename super_type::Index Index;

    TruncatedRecurrentIntegrator(const graphs::PredecessorGraph<>& network,
                                 TReal weight_threshold)
        : super_type(network), weight_threshold_(weight_threshold) {

      super_type::integrator_type_ = TRUNCATED_RECURRENT_INTEGRATOR;
    }

    virtual ~TruncatedRecurrentIntegrator()= default;

    virtual void operator()(const multi_array::Tensor<TReal>& src_state,
                    multi_array::Tensor<TReal>& tar_state) {

      Index edge_id = 0;
      for (Index node = 0; node < super_type::network_.NumNodes(); ++node) {
        for (Index iii = 0; iii < super_type::network_.Predecessors(node).size(); ++iii) {
          // Only carry out calculation if it exceeds magnitude of the threshold
          if ((super_type::weights_[edge_id] > weight_threshold_
               && super_type::weights_[edge_id] >= 0) ||
              (super_type::weights_[edge_id] < -weight_threshold_
               && super_type::weights_[edge_id] <= 0)) {
            tar_state.at(node) += src_state.at(super_type::network_.Predecessors(node)[iii].source)
                               * super_type::weights_[edge_id];
            tar_state[node] = utilities::BoundState(tar_state[node]);
          }
          ++edge_id;
        }
      }
      if (edge_id != super_type::network_.NumEdges()) {
        throw std::runtime_error("Miss match between number of edges and the"
                                 " number integrated");
      }
    }

    TReal GetWeightThreshold() const {
      return weight_threshold_;
    }

  protected:
    TReal weight_threshold_;
};

// Reservoir -- uses explicit weighted structure
template<typename TReal>
class ReservoirIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;

    ReservoirIntegrator(const graphs::PredecessorGraph<TReal>& network)
        : network_(network) {
      super_type::integrator_type_ = RESERVOIR_INTEGRATOR;
      super_type::parameter_count_ = 0;
    }

    ~ReservoirIntegrator()=default;

    void operator()(const multi_array::Tensor<TReal>& src_state,
                    multi_array::Tensor<TReal>& tar_state) {

      for (Index node = 0; node < network_.NumNodes(); ++node) {
        for (Index iii = 0; iii < network_.Predecessors(node).size(); ++iii) {
          tar_state.at(node) += src_state.at(network_.Predecessors(node)[iii].source)
                              * network_.Predecessors(node)[iii].weight;
          tar_state[node] = utilities::BoundState(tar_state[node]);
        }
      }
    }

    void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {}

    std::vector<PARAMETER_TYPE> GetParameterLayout() const {
      return std::vector<PARAMETER_TYPE>(super_type::parameter_count_);
    }

    std::pair<Index, Index> GetWeightIndexRange() const {
      return std::make_pair(0, 0);
    }

    const graphs::PredecessorGraph<TReal>& GetGraph() const {
      return network_;
    }

  protected:
    graphs::PredecessorGraph<TReal> network_;
};

/*
 * Implements non-separable convolution using im2col and gemm through Eigen
 * Assumes NCHW memory layout
 * TODO: Support even filters
 */
template<typename TReal>
class ConvEigenIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;
    typedef Eigen::Matrix<TReal, Eigen::Dynamic, Eigen::Dynamic> Matrix;
    typedef const Eigen::Matrix<TReal, Eigen::Dynamic, Eigen::Dynamic> ConstMatrix;
    typedef Eigen::Map<Matrix> MatrixView;
    typedef const Eigen::Map<ConstMatrix> ConstMatrixView;

    /*
     * Filter shape order: {depth, height, width}
     * Layer shape order: {# channels, height, width}
     * Prev Layer order: {# channels, height, width}
     * Note: This is a bit confusing because my tensors us C++ memory order
     * and Eigen uses Fortran. So I list shapes here with last element of shape
     * being the major axis. This is reversed in Eigen, where first is the major.
     */
    ConvEigenIntegrator(const multi_array::Array<Index,3>& filter_shape,
                        const multi_array::Array<Index,3>& layer_shape,
                        const multi_array::Array<Index,3>& prev_layer_shape,
                        Index stride)
                      : num_filters_(layer_shape[0]), layer_shape_(layer_shape),
                        prev_layer_shape_(prev_layer_shape),
                        filter_shape_(filter_shape), stride_(stride),
                        channels_(prev_layer_shape[0]),
                        height_(prev_layer_shape[1]),
                        width_(prev_layer_shape[2]),
                        kernel_h_(filter_shape[1]),
                        kernel_w_(filter_shape[2]),
                        pad_h_(kernel_h_/2),
                        pad_w_(kernel_w_/2),
                        channel_size_(((height_ + 2 * pad_h_ - kernel_h_)
                                       / stride_ + 1)
                                      * ((width_ + 2 * pad_w_ - kernel_w_)
                                         / stride_ + 1)),
                        buffer_state_(channel_size_,
                                      kernel_h_ * kernel_w_ * channels_) {
      super_type::parameter_count_ = kernel_w_ * kernel_h_ * channels_ * num_filters_;
      super_type::integrator_type_ = CONV_EIGEN_INTEGRATOR;
    }

    virtual ~ConvEigenIntegrator()=default;

    /*
     * Matrix: (# rows, # cols) -> column major
     * src shape: {height * width, channels}
     * tar shape: {(new height * new width), num_filters}
     * Note: If you check tensor shape it will be the reverse, since I use C's
     * order, while Eigen uses Fortran's. Sorry for the confusion, but Eigen
     * got added in latter and I am not sure why they choose Fortran's way for a
     * C++ API.
     */
    virtual void operator()(const multi_array::Tensor<TReal>& src_state,
                    multi_array::Tensor<TReal>& tar_state) override {

      utilities::Im2Col(src_state.data(), channels_, height_, width_,
                        kernel_h_, kernel_w_, pad_h_, pad_w_, stride_, stride_,
                        1, 1, buffer_state_.data());
      MatrixView output(tar_state.data(), channel_size_, num_filters_);
      ConstMatrixView params(weight_view_.data() + weight_view_.start(),
                             kernel_w_ * kernel_h_ * channels_,
                             num_filters_);
      output.noalias() = buffer_state_ * params;
    }

    virtual void Configure(const multi_array::ConstArraySlice<TReal>& parameters) override {
      weight_view_ = parameters;
    }

    virtual std::vector<PARAMETER_TYPE> GetParameterLayout() const override {
      std::vector<PARAMETER_TYPE> layout(super_type::parameter_count_);
      for (Index iii = 0; iii < super_type::parameter_count_; ++iii) {
        layout[iii] = WEIGHT;
      }
      return layout;
    }

    virtual std::pair<Index, Index> GetWeightIndexRange() const override {
      return std::make_pair(0, super_type::parameter_count_);
    };

  protected:
    const utilities::Integer num_filters_;
    const multi_array::Array<Index, 3> layer_shape_;
    const multi_array::Array<Index, 3> prev_layer_shape_;
    const multi_array::Array<Index, 3> filter_shape_;
    const utilities::Integer stride_;
    const utilities::Integer channels_;
    const utilities::Integer height_;
    const utilities::Integer width_;
    const utilities::Integer kernel_h_;
    const utilities::Integer kernel_w_;
    const utilities::Integer pad_h_;
    const utilities::Integer pad_w_;
    const utilities::Integer channel_size_;
    Matrix buffer_state_;
    multi_array::ConstArraySlice<TReal> weight_view_;
};

template <typename TReal>
class RewardModulatedConvIntegrator : public ConvEigenIntegrator<TReal>,
                                      public RewardModulatedIntegrator<TReal> {
  public:
    typedef RewardModulatedIntegrator<TReal> reward_modulator_type;
    typedef ConvEigenIntegrator<TReal> conv_type;
    typedef typename conv_type::Index Index;
    typedef typename conv_type::Matrix Matrix;
    typedef typename conv_type::ConstMatrix ConstMatrix;
    typedef typename conv_type::MatrixView MatrixView;
    typedef typename conv_type::ConstMatrixView ConstMatrixView;

    RewardModulatedConvIntegrator(const multi_array::Array<Index,3>& filter_shape,
                                  const multi_array::Array<Index,3>& layer_shape,
                                  const multi_array::Array<Index,3>& prev_layer_shape,
                                  Index stride, const TReal learning_rate)
        : conv_type(filter_shape, layer_shape, prev_layer_shape, stride),
          reward_modulator_type(learning_rate),
          weights_({conv_type::parameter_count_}) {
      conv_type::integrator_type_ = REWARD_MODULATED;
    }

    void operator()(const multi_array::Tensor<TReal>& src_state,
                    multi_array::Tensor<TReal>& tar_state) override {

      utilities::Im2Col(src_state.data(), conv_type::channels_,
                        conv_type::height_, conv_type::width_,
                        conv_type::kernel_h_, conv_type::kernel_w_,
                        conv_type::pad_h_, conv_type::pad_w_,
                        conv_type::stride_, conv_type::stride_,
                        1, 1, conv_type::buffer_state_.data());
      MatrixView output(tar_state.data(), conv_type::channel_size_,
                        conv_type::num_filters_);
      ConstMatrixView params(weights_.data(),
                             conv_type::kernel_w_
                             * conv_type::kernel_h_
                             * conv_type::channels_,
                             conv_type::num_filters_);
      output.noalias() = conv_type::buffer_state_ * params;
    }

    void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {
      if (parameters.size() != conv_type::parameter_count_) {
        std::cerr << "parameter size: " << parameters.size() << std::endl;
        std::cerr << "parameter count: " << conv_type::parameter_count_ << std::endl;
        throw std::invalid_argument("Wrong number of parameters");
      }

      for (Index iii = 0; iii < parameters.size(); ++iii) {
        weights_[iii] = parameters[iii];
      }
      conv_type::weight_view_ = multi_array::ConstArraySlice<TReal>(weights_.data(), 0,
                                                         weights_.size());
    }

    void UpdateWeights(const TReal reward,
                       const TReal reward_average,
                       const multi_array::Tensor<TReal>& src_state,
                       const multi_array::Tensor<TReal>& tar_state,
                       const multi_array::Tensor<TReal>& tar_state_averages) {

      MatrixView weights(weights_.data(),
                         conv_type::kernel_w_
                         * conv_type::kernel_h_
                         * conv_type::channels_,
                         conv_type::num_filters_);
      ConstMatrixView tar_view(tar_state.data(), conv_type::channel_size_,
                               conv_type::num_filters_);
      ConstMatrixView tar_avg_view(tar_state_averages.data(),
                                   conv_type::channel_size_,
                                   conv_type::num_filters_);

      std::cout << "weights dim: cols " << weights.cols() << " rows " << weights.rows() << std::endl;/////////////
      std::cout << "tar view dim: cols " << tar_view.cols() << " rows " << tar_view.rows() << std::endl;/////////////
      std::cout << "tar avg view dim: cols " << tar_avg_view.cols() << " rows " << tar_avg_view.rows() << std::endl;/////////////
      std::cout << "buff dim: cols " << conv_type::buffer_state_.cols() << " rows " << conv_type::buffer_state_.rows() << std::endl;/////////////

      // find max index tar state, updates single filter
      const Index max_neuron_index = utilities::IndexOfMaxElement(tar_state);
      // Get the filter index for the max neuron
      const Index max_filter = max_neuron_index / conv_type::channel_size_;
      // The buffer_state im2col matrix has a window of states from the previous
      // layer that will correspond to the states that need to be integrated
      // for a given position in the current layer (same for each channel)
      // This position is modulo the size of the channel:
      const Index max_neuron_window = max_neuron_index % conv_type::channel_size_;
      // Alternatively find max index tar state for each filter, updates each filter

      // Loops through src neuron states
      const TReal reward_modulated_learning_factor = (reward - reward_average)
                                                     * reward_modulator_type::learning_rate_;
      for (auto src_neuron = 0; src_neuron < conv_type::buffer_state_.cols(); ++src_neuron) {
        weights(src_neuron, max_filter) += conv_type::buffer_state_(max_neuron_window,
                                                                    src_neuron)
                                 * (tar_state[max_neuron_index]
                                    - tar_state_averages[max_neuron_index])
                                 * reward_modulated_learning_factor;
      }
    }

    const multi_array::Tensor<TReal>& GetWeights() const
    {
      return weights_;
    }

  protected:
    multi_array::Tensor<TReal> weights_;
};

/*
 * Implements an All2All integrator with an Eigen backend
 */
template<typename TReal>
class All2AllEigenIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;
    typedef Eigen::Matrix<TReal, Eigen::Dynamic, 1> ColVector;
    typedef Eigen::Map<ColVector> ColVectorView;
    typedef const Eigen::Matrix<TReal, Eigen::Dynamic, 1> ConstColVector;
    typedef const Eigen::Map<ConstColVector> ConstColVectorView;
    typedef const Eigen::Matrix<TReal, Eigen::Dynamic, Eigen::Dynamic> ConstMatrix;
    typedef const Eigen::Map<ConstMatrix> ConstMatrixView;

    All2AllEigenIntegrator(Index num_states, Index num_prev_states)
    : num_states_(num_states), num_prev_states_(num_prev_states) {
      super_type::parameter_count_ = num_states_ * num_prev_states_;
      super_type::integrator_type_ = ALL2ALL_EIGEN_INTEGRATOR;
    }

    virtual void operator()(const multi_array::Tensor<TReal>& src_state,
                            multi_array::Tensor<TReal>& tar_state) {
      if (!((src_state.size() == num_prev_states_) && (tar_state.size() == num_states_))) {
        std::cerr << "src state size: " << src_state.size() << std::endl;
        std::cerr << "prev state size: " << num_prev_states_ << std::endl;
        std::cerr << "tar state size: " << tar_state.size() << std::endl;
        std::cerr << "state size: " << num_states_ << std::endl;
        throw std::invalid_argument("src state size and prev state size "
                                    "must be equal. tar state size and state"
                                    " size must be equal");
      }

      ColVectorView output(tar_state.data(), tar_state.size());
      output.noalias() = ConstMatrixView(weight_view_.data() + weight_view_.start(),
                                         tar_state.size(),
                                         src_state.size())
                         * ConstColVectorView(src_state.data(), src_state.size());
    }

    virtual void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {
      if (parameters.size() != super_type::parameter_count_) {
        std::cerr << "parameter size: " << parameters.size() << std::endl;
        std::cerr << "parameter count: " << super_type::parameter_count_ << std::endl;
        throw std::invalid_argument("Wrong number of parameters");
      }
      weight_view_ = parameters;
    }

    virtual std::vector<PARAMETER_TYPE> GetParameterLayout() const {
      std::vector<PARAMETER_TYPE> layout(super_type::parameter_count_);
      for (Index iii = 0; iii < super_type::parameter_count_; ++iii) {
        layout[iii] = WEIGHT;
      }
      return layout;
    }

    virtual std::pair<Index, Index> GetWeightIndexRange() const {
      return std::make_pair(0, super_type::parameter_count_);
    }

  protected:
    Index num_states_;
    Index num_prev_states_;
    multi_array::ConstArraySlice<TReal> weight_view_;
};

/*
 * Implements a recurrent integrator with Eigen
 */
template<typename TReal>
class RecurrentEigenIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;
    typedef Eigen::Matrix<TReal, Eigen::Dynamic, 1> ColVector;
    typedef Eigen::Map<ColVector> ColVectorView;
    typedef const Eigen::Matrix<TReal, Eigen::Dynamic, 1> ConstColVector;
    typedef const Eigen::Map<ConstColVector> ConstColVectorView;
    typedef Eigen::SparseMatrix<TReal> SparseMatrix;
    typedef const Eigen::SparseMatrix<TReal> ConstSparseMatrix;
    typedef const Eigen::Map<ConstSparseMatrix> ConstSparseMatrixView;

    RecurrentEigenIntegrator(SparseMatrix network) : network_(std::move(network)) {
      network_.makeCompressed();
      super_type::integrator_type_ = RECURRENT_EIGEN_INTEGRATOR;
      super_type::parameter_count_ = network_.nonZeros();
    }

    virtual ~RecurrentEigenIntegrator()=default;

    virtual void operator()(const multi_array::Tensor<TReal>& src_state,
                            multi_array::Tensor<TReal>& tar_state) {

      if ((network_.cols() != src_state.size())
          && (network_.rows() != tar_state.size())) {
        throw std::invalid_argument("src state size and tar state size "
                                    "incompatible with network");
      }

      ConstSparseMatrixView weight_matrix(network_.rows(), network_.cols(),
                                          weight_view_.size(), network_.outerIndexPtr(),
                                          network_.innerIndexPtr(),
                                          weight_view_.data() + weight_view_.start(),
                                          network_.innerNonZeroPtr());
      ColVectorView output_vector(tar_state.data(), tar_state.size());
      ConstColVectorView src_vector(src_state.data(), src_state.size());

      output_vector.noalias() = weight_matrix * src_vector;
    }

    virtual void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {
      if (parameters.size() != super_type::parameter_count_) {
        std::cerr << "parameter size: " << parameters.size() << std::endl;
        std::cerr << "parameter count: " << super_type::parameter_count_ << std::endl;
        throw std::invalid_argument("Wrong number of parameters");
      }
      weight_view_ = parameters.slice(0, super_type::parameter_count_);
    }

    virtual std::vector<PARAMETER_TYPE> GetParameterLayout() const {
      std::vector<PARAMETER_TYPE> layout(super_type::parameter_count_);
      for (Index iii = 0; iii < super_type::parameter_count_; ++iii) {
        layout[iii] = WEIGHT;
      }
      return layout;
    }

    const multi_array::ConstArraySlice<TReal>& GetWeights() const {
      return weight_view_;
    }

    virtual std::pair<Index, Index> GetWeightIndexRange() const {
      return std::make_pair(0, super_type::parameter_count_);
    }

  protected:
    SparseMatrix network_;
    multi_array::ConstArraySlice<TReal> weight_view_;
};

/*
 * Implements a reservior layer using Eigen
 */
template<typename TReal>
class ReservoirEigenIntegrator : public virtual Integrator<TReal> {
  public:
    typedef Integrator<TReal> super_type;
    typedef typename super_type::Index Index;
    typedef Eigen::Matrix<TReal, Eigen::Dynamic, 1> ColVector;
    typedef Eigen::Map <ColVector> ColVectorView;
    typedef const Eigen::Matrix<TReal, Eigen::Dynamic, 1> ConstColVector;
    typedef const Eigen::Map <ConstColVector> ConstColVectorView;
    typedef Eigen::SparseMatrix <TReal> SparseMatrix;

    ReservoirEigenIntegrator(SparseMatrix network)
    : network_(std::move(network)) {
      network_.makeCompressed();
      super_type::integrator_type_ = RESERVOIR_EIGEN_INTEGRATOR;
      super_type::parameter_count_ = 0;
    }

    ~ReservoirEigenIntegrator()=default;

    void operator()(const multi_array::Tensor<TReal>& src_state,
                    multi_array::Tensor<TReal>& tar_state) {

      if ((network_.cols() != src_state.size())
          && (network_.rows() != tar_state.size())) {
        throw std::invalid_argument("src state size and tar state size "
                                    "incompatible with network");
      }

      ColVectorView output_vector(tar_state.data(), tar_state.size());
      ConstColVectorView src_vector(src_state.data(), src_state.size());
      output_vector.noalias() = network_ * src_vector;
    }

    void Configure(const multi_array::ConstArraySlice<TReal>& parameters) {}

    std::vector<PARAMETER_TYPE> GetParameterLayout() const {
      return std::vector<PARAMETER_TYPE>(super_type::parameter_count_);
    }

    std::pair<Index, Index> GetWeightIndexRange() const {
      return std::make_pair(0, 0);
    }

  protected:
    SparseMatrix network_;
};

template <typename TReal>
class RewardModulatedAll2AllIntegrator : public All2AllEigenIntegrator<TReal>,
                                         public RewardModulatedIntegrator<TReal> {
  public:
    typedef All2AllEigenIntegrator<TReal> all2all_type;
    typedef RewardModulatedIntegrator<TReal> reward_modulator_type;
    typedef typename all2all_type::Index Index;
    typedef typename all2all_type::ColVector ColVector;
    typedef typename all2all_type::ColVectorView ColVectorView;
    typedef typename all2all_type::ConstColVector ConstColVector;
    typedef typename all2all_type::ConstColVectorView ConstColVectorView;
    typedef typename all2all_type::ConstMatrix ConstMatrix;
    typedef typename all2all_type::ConstMatrixView ConstMatrixView;
    typedef Eigen::Matrix<TReal, Eigen::Dynamic, Eigen::Dynamic> Matrix;
    typedef Eigen::Map<Matrix> MatrixView;

    RewardModulatedAll2AllIntegrator(const Index num_states,
                                     const Index num_prev_states,
                                     const TReal learning_rate)
      : all2all_type(num_states, num_prev_states),
        reward_modulator_type(learning_rate),
        weights_({all2all_type::parameter_count_}) {
      all2all_type::integrator_type_ = REWARD_MODULATED;
    }

    void operator()(const multi_array::Tensor<TReal>& src_state,
                    multi_array::Tensor<TReal>& tar_state) override
    {
      if (!((src_state.size() == all2all_type::num_prev_states_)
            && (tar_state.size() == all2all_type::num_states_))) {
        std::cerr << "src state size: " << src_state.size() << std::endl;
        std::cerr << "prev state size: " << all2all_type::num_prev_states_ << std::endl;
        std::cerr << "tar state size: " << tar_state.size() << std::endl;
        std::cerr << "state size: " << all2all_type::num_states_ << std::endl;
        throw std::invalid_argument("src state size and prev state size "
                                    "must be equal. tar state size and state"
                                    " size must be equal");
      }

      ColVectorView output(tar_state.data(), tar_state.size());
      output.noalias() = ConstMatrixView(weights_.data(),
                                         tar_state.size(),
                                         src_state.size())
                         * ConstColVectorView(src_state.data(), src_state.size());
    }

    void Configure(const multi_array::ConstArraySlice<TReal>& parameters) override {
      if (parameters.size() != all2all_type::parameter_count_) {
        std::cerr << "parameter size: " << parameters.size() << std::endl;
        std::cerr << "parameter count: " << all2all_type::parameter_count_ << std::endl;
        throw std::invalid_argument("Wrong number of parameters");
      }
      for (Index iii = 0; iii < parameters.size(); ++iii) {
        weights_[iii] = parameters[iii];
      }
      all2all_type::weight_view_ = multi_array::ConstArraySlice<TReal>(weights_.data(), 0,
                                                         weights_.size());
    }

    void UpdateWeights(const TReal reward,
                       const TReal reward_average,
                       const multi_array::Tensor<TReal>& src_state,
                       const multi_array::Tensor<TReal>& tar_state,
                       const multi_array::Tensor<TReal>& tar_state_averages) override {

      ConstColVectorView src_view(src_state.data(), src_state.size());
      ConstColVectorView tar_view(tar_state.data(), tar_state.size());
      ConstColVectorView tar_avg_view(tar_state_averages.data(), tar_state_averages.size());
      MatrixView weight_view(weights_.data(), tar_state.size(), src_state.size());
      const TReal reward_modulated_learning_factor = (reward - reward_average)
                                                   * reward_modulator_type::learning_rate_;
      for (Index s = 0; s < src_state.size(); ++s) {
        for (Index t = 0; t < tar_state.size(); ++t) {
          weight_view(t, s) += src_view(s)
                               * (tar_view(t) - tar_avg_view(t))
                               * reward_modulated_learning_factor;
        }
      }
    }

    const multi_array::Tensor<TReal>& GetWeights() const
    {
      return weights_;
    }

  protected:
    multi_array::Tensor<TReal> weights_;
};

template <typename TReal>
class RewardModulatedRecurrentIntegrator : public RecurrentEigenIntegrator<TReal>,
                                           public RewardModulatedIntegrator<TReal> {
  public:
    typedef RecurrentEigenIntegrator<TReal> recurrent_type;
    typedef RewardModulatedIntegrator<TReal> reward_modulator_type;
    typedef typename recurrent_type::Index Index;
    typedef typename recurrent_type::ColVector ColVector;
    typedef typename recurrent_type::ColVectorView ColVectorView;
    typedef typename recurrent_type::ConstColVector ConstColVector;
    typedef typename recurrent_type::ConstColVectorView ConstColVectorView;
    typedef typename recurrent_type::SparseMatrix SparseMatrix;
    typedef typename Eigen::Map<SparseMatrix> SparseMatrixView;
    typedef typename recurrent_type::ConstSparseMatrix ConstSparseMatrix;
    typedef typename recurrent_type::ConstSparseMatrixView ConstSparseMatrixView;

    RewardModulatedRecurrentIntegrator(SparseMatrix network, const TReal learning_rate)
        : recurrent_type(network), reward_modulator_type(learning_rate),
          weights_({recurrent_type::parameter_count_}) {
      recurrent_type::integrator_type_ = REWARD_MODULATED;
    }

    void operator()(const multi_array::Tensor<TReal>& src_state,
                    multi_array::Tensor<TReal>& tar_state) override {

      if ((recurrent_type::network_.cols() != src_state.size())
          && (recurrent_type::network_.rows() != tar_state.size())) {
        throw std::invalid_argument("src state size and tar state size "
                                    "incompatible with network");
      }

      ConstSparseMatrixView weight_matrix(recurrent_type::network_.rows(),
                                          recurrent_type::network_.cols(),
                                          weights_.size(),
                                          recurrent_type::network_.outerIndexPtr(),
                                          recurrent_type::network_.innerIndexPtr(),
                                          weights_.data(),
                                          recurrent_type::network_.innerNonZeroPtr());
      ColVectorView output_vector(tar_state.data(), tar_state.size());
      ConstColVectorView src_vector(src_state.data(), src_state.size());

      output_vector.noalias() = weight_matrix * src_vector;
    }

    void Configure(const multi_array::ConstArraySlice<TReal>& parameters) override {
      if (parameters.size() != recurrent_type::parameter_count_) {
        std::cerr << "parameter size: " << parameters.size() << std::endl;
        std::cerr << "parameter count: " << recurrent_type::parameter_count_ << std::endl;
        throw std::invalid_argument("Wrong number of parameters");
      }

      for (Index iii = 0; iii < parameters.size(); ++iii) {
        weights_[iii] = parameters[iii];
      }
      recurrent_type::weight_view_ = multi_array::ConstArraySlice<TReal>(weights_.data(), 0,
                                                         weights_.size());
    }

    void UpdateWeights(const TReal reward,
                       const TReal reward_average,
                       const multi_array::Tensor<TReal>& src_state,
                       const multi_array::Tensor<TReal>& tar_state,
                       const multi_array::Tensor<TReal>& tar_state_averages) override {

      SparseMatrixView weight_matrix(recurrent_type::network_.rows(),
                                     recurrent_type::network_.cols(),
                                     weights_.size(),
                                     recurrent_type::network_.outerIndexPtr(),
                                     recurrent_type::network_.innerIndexPtr(),
                                     weights_.data(),
                                     recurrent_type::network_.innerNonZeroPtr());
      ConstColVectorView src_view(src_state.data(), src_state.size());
      ConstColVectorView tar_view(tar_state.data(), tar_state.size());
      ConstColVectorView tar_avg_view(tar_state_averages.data(), tar_state_averages.size());
      const TReal reward_modulated_learning_factor = (reward - reward_average)
                                                     * reward_modulator_type::learning_rate_;
      for (Index s = 0; s < weight_matrix.outerSize(); ++s) {
        for (typename SparseMatrixView::InnerIterator it(weight_matrix, s); it; ++it) {
          it.valueRef() += src_view(s)
                           * (tar_view(it.row()) - tar_avg_view(it.row()))
                           * reward_modulated_learning_factor;
        }
      }
    }

    const multi_array::Tensor<TReal>& GetWeights() const override
    {
      return weights_;
    }

  protected:
    multi_array::Tensor<TReal> weights_;
};

} // End nervous_system namespace

#endif /* NN_INTEGRATOR_H_ */


#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

#include <boost/dynamic_bitset.hpp>
#include <cassert>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

#include "type.hpp"
#include "utility.hpp"
#ifdef _USE_GPU
#include <gpusim/stat_ops.h>
#endif

#include <csim/stat_ops.hpp>
#include <csim/stat_ops_dm.hpp>

#include "gate_factory.hpp"
#include "pauli_operator.hpp"
#include "state.hpp"

PauliOperator::PauliOperator(std::string strings, CPPCTYPE coef) : _coef(coef) {
    std::stringstream ss(strings);
    std::string pauli_str;
    UINT index, pauli_type = 0;
    while (!ss.eof()) {
        ss >> pauli_str >> index;
        if (pauli_str.length() == 0) break;
        if (pauli_str == "I" || pauli_str == "i")
            pauli_type = 0;
        else if (pauli_str == "X" || pauli_str == "x")
            pauli_type = 1;
        else if (pauli_str == "Y" || pauli_str == "y")
            pauli_type = 2;
        else if (pauli_str == "Z" || pauli_str == "z")
            pauli_type = 3;
        else {
            fprintf(stderr, "invalid Pauli string is given : %s\n ",
                pauli_str.c_str());
            assert(false);
        }
        if (pauli_type != 0) this->add_single_Pauli(index, pauli_type);
    }
}

PauliOperator::PauliOperator(const std::vector<UINT>& target_qubit_list,
    std::string Pauli_operator_type_list, CPPCTYPE coef)
    : _coef(coef) {
    UINT term_count = (UINT)(strlen(Pauli_operator_type_list.c_str()));
    UINT pauli_type = 0;
    for (UINT term_index = 0; term_index < term_count; ++term_index) {
        if (Pauli_operator_type_list[term_index] == 'i' ||
            Pauli_operator_type_list[term_index] == 'I') {
            pauli_type = 0;
        } else if (Pauli_operator_type_list[term_index] == 'x' ||
                   Pauli_operator_type_list[term_index] == 'X') {
            pauli_type = 1;
        } else if (Pauli_operator_type_list[term_index] == 'y' ||
                   Pauli_operator_type_list[term_index] == 'Y') {
            pauli_type = 2;
        } else if (Pauli_operator_type_list[term_index] == 'z' ||
                   Pauli_operator_type_list[term_index] == 'Z') {
            pauli_type = 3;
        } else {
            fprintf(stderr, "invalid Pauli string is given\n");
            assert(false);
        }

        if (pauli_type != 0)
            this->add_single_Pauli(target_qubit_list[term_index], pauli_type);
    }
}

PauliOperator::PauliOperator(const std::vector<UINT>& pauli_list, CPPCTYPE coef)
    : _coef(coef) {
    for (UINT term_index = 0; term_index < pauli_list.size(); ++term_index) {
        if (pauli_list[term_index] != 0)
            this->add_single_Pauli(term_index, pauli_list[term_index]);
    }
}

PauliOperator::PauliOperator(const std::vector<UINT>& target_qubit_index_list,
    const std::vector<UINT>& target_qubit_pauli_list, CPPCTYPE coef)
    : _coef(coef) {
    assert(target_qubit_index_list.size() == target_qubit_pauli_list.size());
    for (UINT term_index = 0; term_index < target_qubit_index_list.size();
         ++term_index) {
        this->add_single_Pauli(target_qubit_index_list[term_index],
            target_qubit_pauli_list[term_index]);
    }
}

PauliOperator::PauliOperator(const boost::dynamic_bitset<>& x,
    const boost::dynamic_bitset<>& z, CPPCTYPE coef) {
    _coef = coef;
    for (UINT i = 0; i < x.size(); i++) {
        UINT pauli_type = 0;
        if (x[i] && !z[i]) {
            pauli_type = 1;
        } else if (x[i] && z[i]) {
            pauli_type = 2;
        } else if (!x[i] && z[i]) {
            pauli_type = 3;
        }
        if (pauli_type != 0) {
            this->add_single_Pauli(i, pauli_type);
        }
    }
}

void PauliOperator::add_single_Pauli(UINT qubit_index, UINT pauli_type) {
    this->_pauli_list.push_back(SinglePauliOperator(qubit_index, pauli_type));
    while (_x.size() <= qubit_index) {
        _x.resize(_x.size() * 2 + 1);
        _z.resize(_z.size() * 2 + 1);
    }
    if (pauli_type == 1) {
        _x.set(qubit_index);
    } else if (pauli_type == 2) {
        _x.set(qubit_index);
        _z.set(qubit_index);
    } else if (pauli_type == 3) {
        _z.set(qubit_index);
    }
}

CPPCTYPE PauliOperator::get_expectation_value(
    const QuantumStateBase* state) const {
    if (state->is_state_vector()) {
#ifdef _USE_GPU
        if (state->get_device_name() == "gpu") {
            return _coef *
                   expectation_value_multi_qubit_Pauli_operator_partial_list_host(
                       this->get_index_list().data(),
                       this->get_pauli_id_list().data(),
                       (UINT)this->get_index_list().size(), state->data(),
                       state->dim, state->get_cuda_stream(),
                       state->device_number);
        } else {
            return _coef *
                   expectation_value_multi_qubit_Pauli_operator_partial_list(
                       this->get_index_list().data(),
                       this->get_pauli_id_list().data(),
                       (UINT)this->get_index_list().size(), state->data_c(),
                       state->dim);
        }
#else
        return _coef *
               expectation_value_multi_qubit_Pauli_operator_partial_list(
                   this->get_index_list().data(),
                   this->get_pauli_id_list().data(),
                   (UINT)this->get_index_list().size(), state->data_c(),
                   state->dim);
#endif
    } else {
        return _coef *
               dm_expectation_value_multi_qubit_Pauli_operator_partial_list(
                   this->get_index_list().data(),
                   this->get_pauli_id_list().data(),
                   (UINT)this->get_index_list().size(), state->data_c(),
                   state->dim);
    }
}

CPPCTYPE PauliOperator::get_transition_amplitude(
    const QuantumStateBase* state_bra,
    const QuantumStateBase* state_ket) const {
    if ((!state_bra->is_state_vector()) || (!state_ket->is_state_vector())) {
        std::cerr
            << "get_transition_amplitude for density matrix is not implemented"
            << std::endl;
    }
#ifdef _USE_GPU
    if (state_ket->get_device_name() == "gpu" &&
        state_bra->get_device_name() == "gpu") {
        return _coef *
               (CPPCTYPE)
                   transition_amplitude_multi_qubit_Pauli_operator_partial_list_host(
                       this->get_index_list().data(),
                       this->get_pauli_id_list().data(),
                       (UINT)this->get_index_list().size(), state_bra->data(),
                       state_ket->data(), state_bra->dim,
                       state_ket->get_cuda_stream(), state_ket->device_number);
    } else {
        return _coef *
               (CPPCTYPE)
                   transition_amplitude_multi_qubit_Pauli_operator_partial_list(
                       this->get_index_list().data(),
                       this->get_pauli_id_list().data(),
                       (UINT)this->get_index_list().size(), state_bra->data_c(),
                       state_ket->data_c(), state_bra->dim);
    }
#else
    return _coef *
           (CPPCTYPE)
               transition_amplitude_multi_qubit_Pauli_operator_partial_list(
                   this->get_index_list().data(),
                   this->get_pauli_id_list().data(),
                   (UINT)this->get_index_list().size(), state_bra->data_c(),
                   state_ket->data_c(), state_bra->dim);
#endif
}

PauliOperator* PauliOperator::copy() const {
    auto pauli = new PauliOperator(this->_coef);
    for (auto val : this->_pauli_list) {
        pauli->add_single_Pauli(val.index(), val.pauli_id());
    }
    return pauli;
}

std::string PauliOperator::get_pauli_string() const {
    std::string res = "";
    UINT size = _pauli_list.size();
    UINT target_index, pauli_id;
    if (size == 0) {
        return "I";
    }
    for (UINT index = 0; index < size; index++) {
        target_index = _pauli_list[index].index();
        pauli_id = _pauli_list[index].pauli_id();
        if (pauli_id == 0)
            continue;
        else if (pauli_id == 1)
            res += "X";
        else if (pauli_id == 2)
            res += "Y";
        else if (pauli_id == 3)
            res += "Z";
        res += " " + std::to_string(target_index) + " ";
    }
    res.pop_back();
    return res;
}

void PauliOperator::change_coef(CPPCTYPE new_coef) { _coef = new_coef; }

PauliOperator PauliOperator::operator*(const PauliOperator& target) const {
    CPPCTYPE bits_coef = 1.0;
    CPPCTYPE I = 1.0i;
    auto x = _x;
    auto z = _z;
    auto target_x = target.get_x_bits();
    auto target_z = target.get_x_bits();
    if (target_x.size() != _x.size()) {
        ITYPE max_size = std::max(_x.size(), target_x.size());
        x.resize(max_size);
        z.resize(max_size);
        target_x.resize(max_size);
        target_z.resize(max_size);
    }
    ITYPE i;
#pragma omp parallel for
    for (i = 0; i < x.size(); i++) {
        if (x[i] && !z[i]) {  // X
            if (!target_x[i] && target_z[i]) {
                bits_coef = bits_coef * -I;
            } else if (target_x[i] && target_z[i]) {
                bits_coef = bits_coef * I;
            }
        } else if (!x[i] && z[i]) {             // Z
            if (target_x[i] && !target_z[i]) {  // X
                bits_coef = bits_coef * -I;
            } else if (target_x[i] && target_z[i]) {  // Y
                bits_coef = bits_coef * I;
            }
        } else if (x[i] && z[i]) {              // Y
            if (target_x[i] && !target_z[i]) {  // X
                bits_coef = bits_coef * I;
            } else if (!target_x[i] && target_z[i]) {  // Z
                bits_coef = bits_coef * I;
            }
        }
    }
    PauliOperator res(
        x ^ target_x, z ^ target_z, _coef * target.get_coef() * bits_coef);
    return res;
}

PauliOperator PauliOperator::operator*(CPPCTYPE target) const {
    PauliOperator res(_x, _z, _coef * target);
    return res;
}

PauliOperator& PauliOperator::operator*=(const PauliOperator& target) {
    _coef *= target.get_coef();
    CPPCTYPE I = 1.0i;
    auto target_x = target.get_x_bits();
    auto target_z = target.get_z_bits();
    ITYPE max_size = std::max(_x.size(), target_x.size());
    if (target_x.size() != _x.size()) {
        _x.resize(max_size);
        _z.resize(max_size);
        target_x.resize(max_size);
        target_z.resize(max_size);
    }
    ITYPE i;
#pragma omp parallel for
    for (i = 0; i < _x.size(); i++) {
        if (_x[i] && !_z[i]) {  // X
            if (!target_x[i] && target_z[i]) {
                _coef *= -I;
            } else if (target_x[i] && target_z[i]) {
                _coef *= I;
            }
        } else if (!_x[i] && _z[i]) {           // Z
            if (target_x[i] && !target_z[i]) {  // X
                _coef *= -I;
            } else if (target_x[i] && target_z[i]) {  // Y
                _coef *= I;
            }
        } else if (_x[i] && _z[i]) {            // Y
            if (target_x[i] && !target_z[i]) {  // X
                _coef *= I;
            } else if (!target_x[i] && target_z[i]) {  // Z
                _coef *= I;
            }
        }
    }
    auto x_bit = _x ^ target_x;
    auto z_bit = _z ^ target_z;
    _x.clear();
    _z.clear();
    _pauli_list.clear();
    _x.resize(max_size);
    _z.resize(max_size);
#pragma omp parallel for
    for (i = 0; i < x_bit.size(); i++) {
        ITYPE pauli_type = 0;
        if (x_bit[i] && !z_bit[i]) {
            pauli_type = 1;
        } else if (x_bit[i] && z_bit[i]) {
            pauli_type = 2;
        } else if (!x_bit[i] && z_bit[i]) {
            pauli_type = 3;
        }
        if (pauli_type != 0) {
            this->add_single_Pauli(i, pauli_type);
        }
    }
    return *this;
}

PauliOperator& PauliOperator::operator*=(CPPCTYPE target) {
    _coef *= target;
    return *this;
}

// made by watle
void PauliOperator::update_quantum_state(QuantumStateBase* instate) {
    // PauliOperator　wo gate tosite kanngaeru
    std::vector<UINT> index_list = this->get_index_list();
    std::vector<UINT> pauli_list = this->get_pauli_id_list();
    for (size_t ii = 0; ii < index_list.size(); ii++) {
        if (pauli_list[ii] == 1) {
            auto x_gate = gate::X(index_list[ii]);
            x_gate->update_quantum_state(instate);
            delete x_gate;
        } else if (pauli_list[ii] == 2) {
            auto y_gate = gate::Y(index_list[ii]);
            y_gate->update_quantum_state(instate);
            delete y_gate;
        } else if (pauli_list[ii] == 3) {
            auto z_gate = gate::Z(index_list[ii]);
            z_gate->update_quantum_state(instate);
            delete z_gate;
        }
    }
    instate->multiply_coef(this->get_coef());
    return;
}


//==============================================================================
//          Copyright 2015 J.T.  Lapreste
//
//          Distributed under the Boost Software License, Version 1.0.
//                 See accompanying file LICENSE.txt or copy at
//                     http://www.boost.org/LICENSE_1_0.txt
//==============================================================================
#ifndef BOOST_SIMD_BITWISE_FUNCTIONS_GENERIC_SWAPBYTES_HPP_INCLUDED
#define BOOST_SIMD_BITWISE_FUNCTIONS_GENERIC_SWAPBYTES_HPP_INCLUDED

#include <boost/simd/bitwise/functions/swapbytes.hpp>
#include <boost/simd/include/functions/simd/shift_left.hpp>
#include <boost/simd/include/functions/simd/shr.hpp>
#include <boost/dispatch/attributes.hpp>

namespace boost { namespace simd { namespace ext
{
  BOOST_DISPATCH_IMPLEMENT( swapbytes_, tag::cpu_
                          , (A0)
                          , (generic_< ints8_<A0> >)
                          )
  {
    typedef A0 result_type;

    BOOST_FORCEINLINE result_type operator()(A0 const& a0) const
    {
      return a0;
    }
  };
  BOOST_DISPATCH_IMPLEMENT( swapbytes_, tag::cpu_
                          , (A0)
                          , (generic_< ints16_<A0> >)
                          )
  {
    typedef A0 result_type;

    BOOST_FORCEINLINE result_type operator()(A0 const& a0) const
    {
     return shl(a0, 8)|shr(a0, 8);
    }
  };
  BOOST_DISPATCH_IMPLEMENT( swapbytes_, tag::cpu_
                          , (A0)
                          , (generic_< type32_<A0> >)
                          )
  {
    typedef A0 result_type;

    BOOST_FORCEINLINE result_type operator()(A0 const& a0) const
    {
      result_type val = ((shl(a0, 8) & 0xFF00FF00 ) | (shr(a0, 8) & 0xFF00FF ));
      return shl(val, 16) | shr(val,16);
    }
  };
  BOOST_DISPATCH_IMPLEMENT( swapbytes_, tag::cpu_
                          , (A0)
                          , (generic_< type64_<A0> >)
                          )
  {
    typedef A0 result_type;

    BOOST_FORCEINLINE result_type operator()(A0 const& a0) const
    {
      result_type val = (shl(a0, 8) & 0xFF00FF00FF00FF00ULL ) | (shr(a0, 8) & 0x00FF00FF00FF00FFULL );
      val = (shl(val, 16) & 0xFFFF0000FFFF0000ULL ) | (shr(val, 16) & 0x0000FFFF0000FFFFULL );
      return shl(val, 32) | shr(val,32);
    }
  };
} } }

#endif