#include "modules/neuralNetwork/layer/deconvolution/deconvolution.hpp"
#include "modules/neuralNetwork/neuralNetwork.hpp"

#ifdef _KORALI_USE_CUDNN
  #include "auxiliar/cudaUtils.hpp"
#endif

#ifdef _KORALI_USE_ONEDNN
  #include "auxiliar/dnnUtils.hpp"
using namespace dnnl;
#endif

#include <Eigen/Dense>
using namespace Eigen;

namespace korali
{
namespace neuralNetwork
{
namespace layer
{
;

void Deconvolution::initialize()
{
  // Checking Layer size
  if (_outputChannels == 0) KORALI_LOG_ERROR("Node count for layer (%lu) should be larger than zero.\n", _index);

  // Checking position
  if (_index == 0) KORALI_LOG_ERROR("Deconvolutional layers cannot be the starting layer of the NN\n");
  if (_index == _nn->_layers.size() - 1) KORALI_LOG_ERROR("Deconvolutional layers cannot be the last layer of the NN\n");

  // Precalculating values for the deconvolution operation
  N = _batchSize;
  OH = _imageHeight;
  OW = _imageWidth;
  KH = _kernelHeight;
  KW = _kernelWidth;

  SV = _verticalStride;
  SH = _horizontalStride;
  PT = _paddingTop;
  PL = _paddingLeft;
  PB = _paddingBottom;
  PR = _paddingRight;

  // Check for non zeros
  if (OH <= 0) KORALI_LOG_ERROR("Image height must be larger than zero for deconvolutional layer.\n");
  if (OW <= 0) KORALI_LOG_ERROR("Image width must be larger than zero for deconvolutional layer.\n");
  if (KH <= 0) KORALI_LOG_ERROR("Kernel height must be larger than zero for deconvolutional layer.\n");
  if (KW <= 0) KORALI_LOG_ERROR("Kernel width must be larger than zero for deconvolutional layer.\n");
  if (SV <= 0) KORALI_LOG_ERROR("Vertical stride must be larger than zero for deconvolutional layer.\n");
  if (SH <= 0) KORALI_LOG_ERROR("Horizontal stride must be larger than zero for deconvolutional layer.\n");

  // Several sanity checks
  if (KH > OH) KORALI_LOG_ERROR("Kernel height cannot be larger than output image height.\n");
  if (KW > OW) KORALI_LOG_ERROR("Kernel height cannot be larger than output image height.\n");
  if (PR + PL > OW) KORALI_LOG_ERROR("L+R Paddings cannot exceed the width of the output image.\n");
  if (PT + PB > OH) KORALI_LOG_ERROR("T+B Paddings cannot exceed the height of the output image.\n");

  // Check whether the output channels of the previous layer is divided by the height and width
  if (_outputChannels % (OH * OW) > 0) KORALI_LOG_ERROR("Deconvolutional layer contains a number of channels (%lu) not divisible by the 2D HxW setup (%lux%lu).\n", _outputChannels, OH, OW);
  OC = _outputChannels / (OH * OW);

  // Deriving output height and width
  IH = (OH - KH + PT + PB) / SV + 1;
  IW = (OW - KW + PR + PL) / SH + 1;

  // Check whether the output channels of the previous layer is divided by the height and width
  if (_prevLayer->_outputChannels % (IH * IW) > 0) KORALI_LOG_ERROR("Previous layer to the convolutional layer contains a number of output channels (%lu) not divisible by the image size (%lux%lu) given kernel (%lux%lu) size and padding/stride configuration.\n", _prevLayer->_outputChannels, IH, IW, KH, KW);
  IC = _prevLayer->_outputChannels / (IH * IW);
}

std::vector<float> Deconvolution::generateInitialHyperparameters()
{
  std::vector<float> hyperparameters;
  size_t weightCount = OC * IC * KH * KW;
  size_t biasCount = OC;

  // If this is not the initial layer, calculate hyperparameters for weight and bias operation
  if (_prevLayer != nullptr)
  {
    // Setting value for this layer's xavier constant
    float xavierConstant = std::sqrt(6.0f) / std::sqrt(_outputChannels + _prevLayer->_outputChannels);

    // Adding layer's weights hyperparameter values
    for (size_t i = 0; i < weightCount; i++)
      hyperparameters.push_back(_weightScaling * xavierConstant * _nn->_uniformGenerator->getRandomNumber());

    // Adding layer's bias hyperparameter values
    for (size_t i = 0; i < biasCount; i++)
      hyperparameters.push_back(0.0f);
  }

  return hyperparameters;
}

void Deconvolution::createHyperparameterMemory()
{
  // Setting hyperparameter count
  size_t weightCount = OC * IC * KH * KW;
  size_t biasCount = OC;

  _hyperparameterCount = weightCount + biasCount;

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    memory::dims weightDims = {OC, IC, KH, KW};
    auto weightMemDesc = memory::desc(weightDims, memory::data_type::f32, memory::format_tag::oihw);
    _weightsMem = memory(weightMemDesc, _nn->_dnnlEngine);

    auto biasMemDesc = memory::desc({OC}, memory::data_type::f32, memory::format_tag::a);
    _biasMem = memory(biasMemDesc, _nn->_dnnlEngine);
  }
#endif
}

void Deconvolution::copyHyperparameterPointers(Layer *dstLayer)
{
  Deconvolution *dstPtr = dynamic_cast<Deconvolution *>(dstLayer);
  dstPtr->_hyperparameterCount = _hyperparameterCount;

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    dstPtr->_weightsMem = _weightsMem;
    dstPtr->_biasMem = _biasMem;
  }
#endif
}

void Deconvolution::createForwardPipeline()
{
  // Calling base layer function
  Layer::createForwardPipeline();

  if (_nn->_engine == "Korali") KORALI_LOG_ERROR("Deconvolutional Layers still not supported in Korali's NN backend. Use OneDNN.\n");
  if (_nn->_engine == "CuDNN") KORALI_LOG_ERROR("Deconvolutional Layers still not supported in CuDNNbackend. Use OneDNN.\n");

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    // Creating memory descriptor mappings for input memory
    _srcMemDesc = memory::desc({N, IC, IH, IW}, memory::data_type::f32, memory::format_tag::nchw);
    _dstMemDesc = memory::desc({N, OC, OH, OW}, memory::data_type::f32, memory::format_tag::nchw);

    // Creating padding dims
    memory::dims ST = {SV, SH};  // Horizontal Vertical
    memory::dims PTL = {PT, PL}; // Top Left
    memory::dims PBR = {PB, PR}; // Bottom Right

    // We create the deconvolution operation
    auto deconvolution_d = deconvolution_forward::desc(_propKind, algorithm::deconvolution_direct, _srcMemDesc, _weightsMem.get_desc(), _biasMem.get_desc(), _dstMemDesc, ST, PTL, PBR);

    // Create inner product primitive descriptor.
    dnnl::primitive_attr deconvolutionPrimitiveAttributes;
    _forwardDeconvolutionPrimitiveDesc = deconvolution_forward::primitive_desc(deconvolution_d, deconvolutionPrimitiveAttributes, _nn->_dnnlEngine);

    // Create the weights+bias primitive.
    _forwardDeconvolutionPrimitive = deconvolution_forward(_forwardDeconvolutionPrimitiveDesc);
  }
#endif
}

void Deconvolution::createBackwardPipeline()
{
  //  Initializing memory objects and primitives for BACKWARD propagation

  // Calling base layer function
  Layer::createBackwardPipeline();

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    // Creating memory descriptor mappings for input memory
    _srcMemDesc = memory::desc({N, IC, IH, IW}, memory::data_type::f32, memory::format_tag::nchw);
    _dstMemDesc = memory::desc({N, OC, OH, OW}, memory::data_type::f32, memory::format_tag::nchw);

    // Creating padding dims
    memory::dims ST = {SV, SH};  // Horizontal Vertical
    memory::dims PTL = {PT, PL}; // Top Left
    memory::dims PBR = {PB, PR}; // Bottom Right

    // Setting strides and padding configuration
    _weightsGradientMem = memory(_weightsMem.get_desc(), _nn->_dnnlEngine);
    _biasGradientMem = memory(_biasMem.get_desc(), _nn->_dnnlEngine);

    auto backwardDataDesc = deconvolution_backward_data::desc(
      algorithm::deconvolution_direct,
      _srcMemDesc,
      _weightsMem.get_desc(),
      _dstMemDesc,
      ST,
      PTL,
      PBR);

    // Create the primitive.
    auto backwardDataPrimitiveDesc = deconvolution_backward_data::primitive_desc(backwardDataDesc, _nn->_dnnlEngine, _forwardDeconvolutionPrimitiveDesc);
    _backwardDataPrimitive = deconvolution_backward_data(backwardDataPrimitiveDesc);

    auto backwardWeightsDesc = deconvolution_backward_weights::desc(
      algorithm::deconvolution_direct,
      _srcMemDesc,
      _weightsMem.get_desc(),
      _biasMem.get_desc(),
      _dstMemDesc,
      ST,
      PTL,
      PBR);

    // Create the primitive.
    auto backwardWeightsPrimitiveDesc = deconvolution_backward_weights::primitive_desc(backwardWeightsDesc, _nn->_dnnlEngine, _forwardDeconvolutionPrimitiveDesc);
    _backwardWeightsPrimitive = deconvolution_backward_weights(backwardWeightsPrimitiveDesc);
  }
#endif
}

void Deconvolution::forwardData(const size_t t)
{
#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    // Arguments to the inner product operation
    std::unordered_map<int, dnnl::memory> forwardDeconvolutionArgs;
    forwardDeconvolutionArgs[DNNL_ARG_SRC] = _prevLayer->_outputMem[t];
    forwardDeconvolutionArgs[DNNL_ARG_WEIGHTS] = _weightsMem;
    forwardDeconvolutionArgs[DNNL_ARG_BIAS] = _biasMem;
    forwardDeconvolutionArgs[DNNL_ARG_DST] = _outputMem[t];

    _forwardDeconvolutionPrimitive.execute(_nn->_dnnlStream, forwardDeconvolutionArgs);
  }
#endif
}

void Deconvolution::backwardData(const size_t t)
{
  if (_nn->_mode == "Inference")
    KORALI_LOG_ERROR("Requesting Layer backward data propagation but NN was configured for inference only.\n");

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    _backwardDataArgs[DNNL_ARG_DIFF_DST] = _outputGradientMem[t];             // Input
    _backwardDataArgs[DNNL_ARG_WEIGHTS] = _weightsMem;                        // Input
    _backwardDataArgs[DNNL_ARG_DIFF_SRC] = _prevLayer->_outputGradientMem[t]; // Output

    _backwardDataPrimitive.execute(_nn->_dnnlStream, _backwardDataArgs);
  }
#endif
}

void Deconvolution::backwardHyperparameters(size_t t)
{
  if (_nn->_mode == "Inference")
    KORALI_LOG_ERROR("Requesting Layer hyperparameter gradient propagation but NN was configured for inference only.\n");

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    // Arguments for the backward propagation of the gradient wrt Weights and Biases
    std::unordered_map<int, dnnl::memory> backwardWeightsArgs;
    backwardWeightsArgs[DNNL_ARG_SRC] = _prevLayer->_outputMem[t];    // Input
    backwardWeightsArgs[DNNL_ARG_DIFF_DST] = _outputGradientMem[t];   // Input
    backwardWeightsArgs[DNNL_ARG_DIFF_WEIGHTS] = _weightsGradientMem; // Output
    backwardWeightsArgs[DNNL_ARG_DIFF_BIAS] = _biasGradientMem;       // Output

    _backwardWeightsPrimitive.execute(_nn->_dnnlStream, backwardWeightsArgs);
  }
#endif
}

void Deconvolution::setHyperparameters(float *hyperparameters)
{
#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    write_to_dnnl_memory(&hyperparameters[0], _weightsMem);
    write_to_dnnl_memory(&hyperparameters[IC * OC], _biasMem);
  }
#endif
}

void Deconvolution::getHyperparameters(float *hyperparameters)
{
  size_t IC = _prevLayer->_outputChannels;
  size_t OC = _outputChannels;

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    read_from_dnnl_memory(&hyperparameters[0], _weightsMem);
    read_from_dnnl_memory(&hyperparameters[IC * OC], _biasMem);
  }
#endif
}

void Deconvolution::getHyperparameterGradients(float *gradient)
{
#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    read_from_dnnl_memory(&gradient[0], _weightsGradientMem);
    read_from_dnnl_memory(&gradient[IC * OC], _biasGradientMem);
  }
#endif
}

void Deconvolution::setConfiguration(knlohmann::json& js)
{
 if (isDefined(js, "Results"))  eraseValue(js, "Results");

 if (isDefined(js, "Image Height"))
 {
 try { _imageHeight = js["Image Height"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Image Height']\n%s", e.what()); }
   eraseValue(js, "Image Height");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Image Height'] required by deconvolution.\n");

 if (isDefined(js, "Image Width"))
 {
 try { _imageWidth = js["Image Width"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Image Width']\n%s", e.what()); }
   eraseValue(js, "Image Width");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Image Width'] required by deconvolution.\n");

 if (isDefined(js, "Kernel Height"))
 {
 try { _kernelHeight = js["Kernel Height"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Kernel Height']\n%s", e.what()); }
   eraseValue(js, "Kernel Height");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Kernel Height'] required by deconvolution.\n");

 if (isDefined(js, "Kernel Width"))
 {
 try { _kernelWidth = js["Kernel Width"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Kernel Width']\n%s", e.what()); }
   eraseValue(js, "Kernel Width");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Kernel Width'] required by deconvolution.\n");

 if (isDefined(js, "Vertical Stride"))
 {
 try { _verticalStride = js["Vertical Stride"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Vertical Stride']\n%s", e.what()); }
   eraseValue(js, "Vertical Stride");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Vertical Stride'] required by deconvolution.\n");

 if (isDefined(js, "Horizontal Stride"))
 {
 try { _horizontalStride = js["Horizontal Stride"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Horizontal Stride']\n%s", e.what()); }
   eraseValue(js, "Horizontal Stride");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Horizontal Stride'] required by deconvolution.\n");

 if (isDefined(js, "Padding Left"))
 {
 try { _paddingLeft = js["Padding Left"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Padding Left']\n%s", e.what()); }
   eraseValue(js, "Padding Left");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Padding Left'] required by deconvolution.\n");

 if (isDefined(js, "Padding Right"))
 {
 try { _paddingRight = js["Padding Right"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Padding Right']\n%s", e.what()); }
   eraseValue(js, "Padding Right");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Padding Right'] required by deconvolution.\n");

 if (isDefined(js, "Padding Top"))
 {
 try { _paddingTop = js["Padding Top"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Padding Top']\n%s", e.what()); }
   eraseValue(js, "Padding Top");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Padding Top'] required by deconvolution.\n");

 if (isDefined(js, "Padding Bottom"))
 {
 try { _paddingBottom = js["Padding Bottom"].get<ssize_t>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ deconvolution ] \n + Key:    ['Padding Bottom']\n%s", e.what()); }
   eraseValue(js, "Padding Bottom");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Padding Bottom'] required by deconvolution.\n");

 Layer::setConfiguration(js);
 _type = "layer/deconvolution";
 if(isDefined(js, "Type")) eraseValue(js, "Type");
 if(isEmpty(js) == false) KORALI_LOG_ERROR(" + Unrecognized settings for Korali module: deconvolution: \n%s\n", js.dump(2).c_str());
}

void Deconvolution::getConfiguration(knlohmann::json& js)
{

 js["Type"] = _type;
   js["Image Height"] = _imageHeight;
   js["Image Width"] = _imageWidth;
   js["Kernel Height"] = _kernelHeight;
   js["Kernel Width"] = _kernelWidth;
   js["Vertical Stride"] = _verticalStride;
   js["Horizontal Stride"] = _horizontalStride;
   js["Padding Left"] = _paddingLeft;
   js["Padding Right"] = _paddingRight;
   js["Padding Top"] = _paddingTop;
   js["Padding Bottom"] = _paddingBottom;
 Layer::getConfiguration(js);
}

void Deconvolution::applyModuleDefaults(knlohmann::json& js)
{

 std::string defaultString = "{}";
 knlohmann::json defaultJs = knlohmann::json::parse(defaultString);
 mergeJson(js, defaultJs);
 Layer::applyModuleDefaults(js);
}

void Deconvolution::applyVariableDefaults()
{

 Layer::applyVariableDefaults();
}

;

} //layer
} //neuralNetwork
} //korali
;


/* Copyright 2020 Gopinath Chennupati, Raviteja Vangara, Namita Kharat, Erik Skau and Boian Alexandrov,
Triad National Security, LLC. All rights reserved
This program was produced under U.S. Government contract 89233218CNA000001 for Los Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC for the U.S. Department of Energy/National Nuclear Security Administration. All rights in the program are reserved by Triad National Security, LLC, and the U.S. Department of Energy/National Nuclear Security Administration. The Government is granted for itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license in this material to reproduce, prepare derivative works, distribute copies to the public, perform publicly and display publicly, and to permit others to do so.

This file includes distributed custom clustering and stability criterion based on distributed silhouette statistics.
*/

#ifndef DISTNMF_DISTCLUST_HPP_
#define DISTNMF_DISTCLUST_HPP_

#include <unistd.h>
#include <armadillo>
#include <string>
#include "../planc-master/common/utils.hpp"
#include "../planc-master/common/distutils.hpp"
#include "../planc-master/distnmf/mpicomm.hpp"
#include "../planc-master/distnmf/distnmftime.hpp"

#include <ctime>
#define CUBE arma::cube
#define FIELD arma::field

/**
 * File name formats
 */

namespace planc {

template <class MATTYPE>
class DistClust {
 private:
  const MPICommunicator& m_mpicomm;
  DistNMFTime time_stats;
  int m_ownedm;
  int m_ownedn;
  int m_slices;

  int m_pr;
  int m_pc;
  int m_k;

  MAT median_W, median_H;       //Both the median low rank factors
  MAT tempWc, tempHc;           //temporary matrices to hold all columns of W and H

  CUBE Wcube, Hcube;            //Both the cubes for low rank matrices
  CUBE Wcube_new;

  MAT mad_W;                    //Mean absolute deviations from the median_W

  CUBE localWcubenorm, Wcubenorm; //column-wise L2-norms of the Wcube
  CUBE finalWcubenorm, fWcnorm;   //cross check the normalization
  CUBE localaiDotProd;            //m_slices x m_slices x m_k
  CUBE globalaiDotProd;           //m_slices x m_slices x m_k
  MAT aiAvgDist;                        // avg dist of i^th data-point to the rest in Cluster_i
  CUBE localbiDotProd;
  CUBE globalbiDotProd;
  FIELD<CUBE> partDotProd;
  MAT biAvgDist;
  MAT si;                         //Final Silhouette widths

  MAT Centroids;
  CUBE localCosDist, globalCosDist;

  /**
   * Allocates matrices and vectors
   */
  void allocateData() {
    // Init matrices
    median_W.zeros(this->m_ownedm, this->m_k);
    median_H.zeros(this->m_ownedn, this->m_k);

    // low-rank factor matrices
    tempWc.zeros(this->m_ownedm, this->m_slices);
    tempHc.zeros(this->m_ownedn, this->m_slices);

    mad_W.zeros(this->m_ownedm, this->m_k);

    Wcube_new.zeros(this->m_ownedm, this->m_k, this->m_slices);

    //clustStability (Silhouette) data
    localWcubenorm.zeros(1, this->m_k, this->m_slices);
    Wcubenorm.zeros(1, this->m_k, this->m_slices);

    finalWcubenorm.zeros(1, this->m_k, this->m_slices);
    fWcnorm.zeros(1, this->m_k, this->m_slices);

    localaiDotProd.zeros(this->m_slices, this->m_slices, this->m_k);
    globalaiDotProd.zeros(this->m_slices, this->m_slices, this->m_k);
    aiAvgDist.zeros(this->m_slices, this->m_k); //m_k clusters and each has m_slices (data points)
    localbiDotProd.zeros(this->m_slices, this->m_slices, (this->m_k-1) );
    //partDotProd.zeros(this->m_k, 1);
    globalbiDotProd.zeros( this->m_slices, this->m_slices, (this->m_k-1) );
    biAvgDist.zeros(this->m_slices, this->m_k);
    si.zeros(this->m_slices, this->m_k);

    Centroids.zeros(this->m_k, this->m_ownedm);
    localCosDist.zeros(this->m_k, this->m_k, this->m_slices);
    globalCosDist.zeros(this->m_k, this->m_k, this->m_slices);
  }

  void freeMatrices() {
    median_W.clear();
    median_H.clear();

    tempWc.clear();
    tempHc.clear();

    Wcube.clear();
    Hcube.clear();
    Wcube_new.clear();

    mad_W.clear();

    localWcubenorm.clear();
    Wcubenorm.clear();

    finalWcubenorm.clear();
    fWcnorm.clear();

    localaiDotProd.clear();
    globalaiDotProd.clear();

    aiAvgDist.clear();
    localbiDotProd.clear();
    partDotProd.clear();
    globalbiDotProd.clear();
    biAvgDist.clear();
    si.clear();

    Centroids.clear();
    localCosDist.clear();
  }

 public:
    DistClust<MATTYPE>(const CUBE &Wall, const CUBE &Hall,
            const MPICommunicator& communicator, const int k)
        : m_mpicomm(communicator),
        time_stats(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)   {
        assert(Wall.n_cols == Hall.n_cols);
        this->m_k = k;
        this->m_pr = NUMROWPROCS;
        this->m_pc = NUMCOLPROCS;
        //Assign the low-rank factors
        this->Wcube = Wall;
        this->Hcube = Hall;
        this->m_ownedm  = Wall.n_rows;
        this->m_ownedn = Hall.n_rows;
        this->m_slices = Wall.n_slices;
        allocateData();
        PRINTROOT("distclust()::constructor succesful");

        // Column-wise normalize so that the cosine-distance is just a dot product
        normalizeWcube();
        // if(MPI_RANK == 0)   {
        //   this->Wcube.print("Wcube after = ");
        // }
        //MPI_Barrier(MPI_COMM_WORLD);
        //Error check to see the columns are unit-norm vectors
        // finalWcubenorm = sum(this->Wcube % this->Wcube, 0);
        /* check the unit-norm vecs of normalized W, Looks normalization works ...! */
        // TO DO! Need to report time later
        // mpitic();
        // MPI_Allreduce(finalWcubenorm.memptr(), fWcnorm.memptr(), this->m_k * this->m_slices,
        //         MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
        // double temp = mpitoc();
        // if(MPI_RANK == 0) { fWcnorm.print("fWcnorm = "); }
    }

    ~DistClust() {
      //freeMatrices();
    }

    /* Distributed mean absolute deviations (MAD) */
    VEC distMAD(int median_c)  {
      VEC mad;
      mad.zeros(this->m_ownedm);
      for(int s = 0; s < this->m_slices; s++)    {
        mad += arma::abs(this->tempWc.col(s) - this->median_W.col(median_c));
      }
      mad = mad / this->m_slices;
      return mad;
    }

    void distMedian()  {
        //Find median W and H for one column at a time across all procs
        for(int ki = 0; ki < this->m_k; ki++)   { // For each column in rank 'k'
            //tempWc/Hc contains 'm_slices' points (a column is a point in same cluster)
            this->tempWc.zeros();
            this->tempHc.zeros();
            for(int s = 0; s < this->m_slices; s++)    {   // For each slice
                this->tempWc.col(s) = this->Wcube.slice(s).col(ki);
                this->tempHc.col(s) = this->Hcube.slice(s).col(ki);
            }
            // The median's are calculated on each row of the W, H
            //Therefore, the median W and H are of size (m/p x k) and (k x n/p)
            this->median_W.col(ki) = arma::median(this->tempWc, 1); //per proc median W
            this->median_H.col(ki) = arma::median(this->tempHc, 1); //per proc median H
            mad_W.col(ki) = distMAD(ki); //mean absolute deviations
        }

        // if(MPI_RANK == 0)   {
        //     median_W.print("median_W = ");
        //     median_H.print("median_H = ");
        //     mad_W.print("mad_W = ");
        // }
    }

    /*
      Distributed reordering of the columns in all Ws using a centroid
      and K-means kind of an algorithm
    */
   void distReorder() {
     // Only for the testing purpose will be removed later
    //  CUBE WcubeBefore = Wcube;
    //  WcubeBefore.reshape( this->m_ownedm, this->m_k * this->m_slices, 1 );
    //  MAT reshaped_WcubeBefore = WcubeBefore.slice(0);
    //  std::stringstream sw;
    //  sw << "/lustre/scratch3/turquoise/gchennupati/factors/Wall_before_k" <<  this->m_k << "_" << MPI_SIZE << "_" << MPI_RANK;
    //  reshaped_WcubeBefore.save(sw.str(), arma::raw_ascii);
     // Remove until here after testing the clustering

     Centroids = Wcube.slice(0).t();
     Wcube_new.slice(0) = Centroids.t();

     for(int it = 0; it < 3; it++)  {
       localCosDist.zeros();
      // Calc local Cosine-distances from Centroid to W_i
      for(int s = 0; s < this->m_slices; s++) {
        // Find the dot product first
        localCosDist.slice(s) = Centroids * Wcube.slice(s);
        //  if(MPI_RANK == 0)  { Wcube.slice(s).print("Wcube.slice(s) = "); }
      }
      globalCosDist.zeros();
      // Calc global Cosine-distances or angular distances from Centroid to Ws in Wall
      MPI_Allreduce(localCosDist.memptr(), globalCosDist.memptr(), this->m_k * this->m_k * this->m_slices,
                MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
      // Angular similarity or Cosine Similarity
      globalCosDist = 1 - globalCosDist;

      // if(MPI_RANK == 0)  {
      //   globalCosDist.print("globalCosDist = ");
      // }
      // Calc the orders of the clusters in Ws of Wall
      for(int s = 0; s < this->m_slices; s++)  {
        for(UWORD ki = 0; ki < this->m_k; ki++)  {
          //Find the global min distance in each m_k x m_k matrix
          UWORD idx = globalCosDist.slice(s).index_min();
          UVEC cart_idx = arma::ind2sub(size(globalCosDist.slice(s)), idx);
          ROWVEC row_sum = sum(globalCosDist.slice(s), 0);
          VEC col_sum = sum(globalCosDist.slice(s), 1);
          // Update the corresponding centroid row and W column to find next global min dist
          globalCosDist.slice(s).row(cart_idx(0)) = row_sum;
          globalCosDist.slice(s).col(cart_idx(1)) = col_sum;
          //Reorder the clusters into a new Wcube
          Wcube_new.slice(s).col(cart_idx(0)) = Wcube.slice(s).col(cart_idx(1));
        }
      }
      this->Wcube = Wcube_new;
      distMedian(); // Calculate the median W
      if(it != 0)  {
        Centroids = median_W.t();
      }
     }
     //if(MPI_RANK == 0)  { Wcube.print("New Wcube is = "); }
   }

    /* Calculate the stability of the NMFk clusters
       We calculate the Silhouettes for now
       Mean Absolute Deviation (MAD) is calculated in distMean()
     */
    void distClustStability() {
      clock_t begin = clock();
      MPITIC;
      try {
        distCalcAi();
        distCalcBi();

        // Wcube.reshape( this->m_ownedm, this->m_k * this->m_slices, 1 );
        // MAT reshaped_W = Wcube.slice(0);

        // if(MPI_RANK == 0) { cout<<"Reshaped W = \n"<<reshaped_W<<endl; }

        // std::stringstream sw;
        // sw << "/lustre/scratch3/turquoise/gchennupati/5760x3840/factors/Wall_after_k" <<  this->m_k << "_" << MPI_SIZE << "_" << MPI_RANK;
        // reshaped_W.save(sw.str(), arma::raw_ascii);

      } catch (const std::exception& e) {
          std::cout<<"Exception "<<e.what()<<std::endl;
      }
      double temp = MPITOC;
      this->time_stats.compute_duration(temp);
      clock_t end = clock();
      double elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;
      PRINTROOT("completed clustering for k=" << this->m_k << "::taken::" << this->time_stats.duration());
      PRINTROOT("clustering elpased time = "<< elapsed_secs);
    }

    /* Calculate a(i) = cosine_dist(i, j)*/
    void distCalcAi() {
      for(int ki = 0; ki < this->m_k; ki++) {
        CUBE tempCluster = Wcube( arma::span(), arma::span(ki, ki), arma::span() );
        tempCluster.reshape( this->m_ownedm, this->m_slices, tempCluster.n_cols );
        MAT tempClust = tempCluster.slice(0);
        //Calculate the dot product of a cluster data points
        localaiDotProd.slice(ki) = tempClust.t() * tempClust;
        //cout<<"Shapes are "<<size(localaiDotProd)<<" and "<<size(tempClust)<<endl;

      }
      MPI_Allreduce(localaiDotProd.memptr(), globalaiDotProd.memptr(), this->m_slices * this->m_slices * this->m_k,
              MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
      // Following line is the most important, because we calc cosine similarity not cosine distance
      globalaiDotProd = 1 - globalaiDotProd;
      //cout<<"Shapes are "<<size(globalaiDotProd)<<" and "<<size((sum(this->globalaiDotProd.slice(0), 1) - 1) / (this->m_slices - 1))<<endl;
      // Similarity of a cluster smaller the a_i better the cluster assignment of a point
      for(int ki = 0; ki < this->m_k; ki++) {
        this->aiAvgDist.col(ki) = sum(this->globalaiDotProd.slice(ki), 1)  / (this->m_slices - 1);
        //cout<<"cluster "<<ki<<" sum is "<<(sum(this->globalaiDotProd.slice(ki), 1) - 1) / (this->m_slices - 1)<<endl;
      }
      // if(MPI_RANK == 0)   {
      //   //this->globalaiDotProd.print("globalaiDotProd = ");
      //   this->aiAvgDist.print("Avg ai distances = ");
      // }

      // Clear temporary data

    }

    /* Calculate b(i) = minimum cosine_dist(i, j)
       from point i in cluster i to all other points in cluster j (averaged)
       Repeat it for all the cluster where i != j and the minimum avg distance
       is the distance for that point.
    */
    void distCalcBi() {
      for(int ki = 0; ki < this->m_k; ki++)  {
       localbiDotProd.zeros();
       CUBE tempCluster = Wcube( arma::span(), arma::span(ki, ki), arma::span() );
       tempCluster.reshape( this->m_ownedm, this->m_slices, tempCluster.n_cols );
       MAT tempClust = tempCluster.slice(0);
       int count = 0;
       for(int kj = 0; kj < this->m_k; kj++)  {
         if(ki != kj) {
          CUBE tempCluster2 = Wcube( arma::span(), arma::span(kj, kj), arma::span() );
          tempCluster2.reshape( this->m_ownedm, this->m_slices, tempCluster.n_cols );
          MAT tempClust2 = tempCluster2.slice(0);
          localbiDotProd.slice(count) = tempClust.t() * tempClust2;
          count++;
          //cout<<"count increment "<<count<<endl;
         }
        }
        globalbiDotProd.zeros();
        //partDotProd(ki, 0) = localbiDotProd;
        MPI_Allreduce(localbiDotProd.memptr(), globalbiDotProd.memptr(), this->m_slices * this->m_slices * (this->m_k-1),
              MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
        MPI_Barrier(MPI_COMM_WORLD);
        // Following is the most important, because we calc cosine similarity not cosine distance
        globalbiDotProd = arma::abs(1 - globalbiDotProd);

        CUBE tempMean = arma::mean(globalbiDotProd, 1);
        tempMean.reshape( tempMean.n_rows, tempMean.n_slices, 1 );
        MAT allMeans = tempMean.slice(0);
        biAvgDist.col(ki) = arma::min( allMeans, 1 );
        // if(MPI_RANK == 0)   {
        //   //this->globalbiDotProd.print("globalbiDotProd = ");
        //   cout<<"means = \n"<< tempMean.slice(0)<<endl;
        // }

        // Clear the temporary data
        allMeans.clear();
      }

      // Following line is the most important, because we calc cosine similarity not cosine distance
      // for(UWORD ki = 0; ki < this->m_k; ki++) {
      //   globalbiDotProd(ki, 0) = 1 - globalbiDotProd(ki, 0);
      //   CUBE tempMean = arma::mean(globalbiDotProd(ki, 0), 1);
      //   tempMean.reshape( this->m_slices, this->m_k-1, 1 );
      //   biAvgDist.col(ki) = arma::min(tempMean.slice(0), 1);
      // }

      // for(int ki = 0; ki < this->m_k; ki++) {
      //   biAvgDist.col(ki) = min(globalbiDotProd.slice(ki), 1);
      // }
      /* Final Silhouette for all data points */
      this->si = (biAvgDist - aiAvgDist) / arma::max(biAvgDist, aiAvgDist);

      /*
      if(MPI_RANK == 0)   {
        //this->globalbiDotProd.print("globalbiDotProd = ");
        // this->biAvgDist.print("Avg bi distances = ");
        // this->si.print("Final Silhouettes = ");
        std::stringstream si;
        si << "/lustre/scratch3/turquoise/gchennupati/5760x3840/silhouettes/Si_at_k" << this->m_k;
        this->si.save(si.str(), arma::raw_ascii);
        //cout<<"bi-ai "<<(biAvgDist-aiAvgDist)<<endl;
        //cout<<"Max(bi, ai) "<<max(biAvgDist, aiAvgDist)<<endl;
      }
      */
    }

    /* Returns the final Silhouettes at a given rank 'k' */
    MAT getSilhouettes()  { return si; }

    /*
      Normalize columns of Wcube with the respective L2-norm
     */
    void normalizeWcube()  {
      localWcubenorm = sum(this->Wcube % this->Wcube, 0);
      mpitic();
      MPI_Allreduce(localWcubenorm.memptr(), Wcubenorm.memptr(), this->m_k * this->m_slices,
                MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
      double temp = mpitoc();
      // if(MPI_RANK == 0) {
      //  Wcubenorm.print("global Wcube_sq_norm ");
      // }
      //this->time_stats.allgather_duration(temp);
      for(int s = 0; s < this->m_slices; s++) {
        for (int c = 0; c < this->m_k; c++) {
          if ( Wcubenorm.slice(s)(c) > 1 ) {
            double norm_const = sqrt( Wcubenorm.slice(s)(c) );
            this->Wcube.slice(s).col(c) = this->Wcube.slice(s).col(c) / norm_const;
            // this->Hcube.slice(s).col(i) = this->Hcube.slice(s).col(i) * norm_const;
          }
        }
      }
    }

    /// Returns the left low rank (median) factor matrix W
    MAT getLeftLowRankFactor() { return median_W; }

    /// Returns the right low rank (median) factor matrix H
    MAT getRightLowRankFactor() { return median_H; }

  }; //class DistReOrder

}  // namespace planc

#endif  // DISTNMF_DISTCLUST_HPP_


#include <iostream>
#include <fstream>
#include <ctime>
#include <string>
#include <boost/iostreams/filtering_stream.hpp>

#include <iomanip>
#include <math.h>
using namespace std;

#define QUALITY_OFFSET 33

// https://stackoverflow.com/questions/29028137/how-to-multithread-reading-a-file-in-c11

int main(int argc, char* argv[])
{

    cout.setf(ios::fixed);
    string line, second, output;

    int qual;
    long read_size=0;
    long count_reads=0;
    long count_q30_bases=0;
    long count_q30_reads=0;
    long sum_read_scores=0;
    long total_sum_read_scores=0;
    double average_read_scores=0;
    long sum_scores=0;
    long total_bases=0;

    std::ifstream fastq_f(argv[1], std::ios_base::in | std::ios_base::binary);

    output = argv[1];
    unsigned found = output.find_last_of("/\\");
    output = argv[2] + output.substr(found + 1) + ".metrics.csv";

    std::ofstream results_f(output.c_str(), std::ios_base::out);

    boost::iostreams::filtering_istream in;

    in.push(fastq_f);

    clock_t begin = clock();

    //get reads size
    getline(in, line);
    getline(in, second);

    read_size = second.size();

    cout << "Read Size: " << read_size << endl;
    //read_size = 150;

    int n_content[read_size];
    for (int i = 0; i < read_size; i++) n_content[i] = 0;

    getline(in, line);
    getline(in, line);

    //do it for the first read first cause we can't go back to begining of the loop
    count_reads += 1;
    sum_read_scores=0;

    for(int i=0; i < read_size; ++i){

            char base = second[i];

            if (base == 'N' || base == 'n'){
                n_content[i] += 1;
            }

            qual = int(line[i]) - QUALITY_OFFSET;
            sum_scores += qual;
            //per read Q30
            sum_read_scores +=qual;
            //per base Q30
            if (qual >= 30){
                count_q30_bases += 1;
            }
        }
        average_read_scores = sum_read_scores/float(read_size);

        total_sum_read_scores += average_read_scores;

        if (average_read_scores >= 30){
                count_q30_reads += 1;
        }

    while(getline(in, line) && getline(in, second) && getline(in, line) && getline(in, line)) {

        count_reads += 1;
        sum_read_scores=0;
        read_size = second.size();

        for(int i=0; i <read_size; ++i){

            char base = second[i];

            if (base == 'N' || base == 'n'){
                n_content[i] += 1;
            }
            qual = int(line[i]) - QUALITY_OFFSET;
            // cout << "Qual: " << qual << endl;
            //per read Q30
            sum_read_scores +=qual;
            //per base Q30
            sum_scores += qual;
            if (qual >= 30){
                count_q30_bases += 1;
            }
        }
        average_read_scores = sum_read_scores/float(read_size);
        // cout << "Read Qual: " << qual << endl;
        total_sum_read_scores += average_read_scores;

        if (average_read_scores >= 30){
                count_q30_reads += 1;
        }
    }

    total_bases = read_size*count_reads;

    clock_t end = clock();

    double elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;

    cout << "Total Sum of Phred Scores: " << sum_scores << endl;
    cout << "Total number of Q30 Bases with phred quality score higher or equal than 30: " << count_q30_bases << endl;
    cout << "Total number of Bases: " << total_bases << endl;
    cout << "Total number of Reads with phred quality score higher or equal than 30: " << count_q30_reads << endl;
    cout << "Total number of Reads: " << count_reads << endl;
    cout << "Max per base N content: ";
    for (int i = 0; i < read_size; i++) {
        cout << (float(n_content[i]) / float(count_reads)) * 100;
        if (i < read_size-1) {
            cout << ',';
        }
    }
    cout << endl;
    cout << "------------------ Summary ------------------" << endl;
    cout << "Percentage of bases with phred quality score higher or equal than 30: " << (count_q30_bases/float(total_bases)) * 100 << endl;
    cout << "Percentage of reads with phred quality score higher or equal than 30: " << (count_q30_reads/float(count_reads)) * 100 << endl;
    cout << "Average per base quality Score: " << sum_scores/float(total_bases) << endl;
    cout << "Average per read quality Score: " << total_sum_read_scores/float(count_reads) << endl;

    cout << endl;
    cout << "Time Taken: " << elapsed_secs << endl;

    results_f << "sum_phred_scores," << sum_scores << endl;
    results_f << "count_q30_bases," << count_q30_bases << endl;
    results_f << "n_bases," << total_bases << endl;
    results_f << "count_q30_reads," << count_q30_reads << endl;
    results_f << "n_reads," << count_reads << endl;
    results_f << "max_percentage_per_base_n_content,";

    for (int i = 0; i < read_size; i++) {
        results_f << (float(n_content[i]) / float(count_reads)) * 100;
        if (i < read_size-1) {
            results_f << ',';
        }
    }
    results_f << endl;

    results_f << "percentage_q30_bases," << (count_q30_bases/float(total_bases)) * 100 << endl;
    results_f << "percentage_q30_reads," << (count_q30_reads/float(count_reads)) * 100 << endl;
    results_f << "average_bases_quality_score," << sum_scores/float(total_bases) << endl;
    results_f << "average_reads_quality_score," << total_sum_read_scores/float(count_reads) << endl;

    results_f << "time_taken," << elapsed_secs << endl;
    results_f.close();

}


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

#include <boost/numeric/odeint.hpp>
#include <vle/dsde/qss1.hpp>
#include <vle/dsde/qss2.hpp>
#include <vle/vle.hpp>
#include <chrono>

#define CATCH_CONFIG_MAIN
#include "catch.hpp"

typedef vle::DoubleTime MyTime;
typedef vle::PortList <double> MyPort;
typedef vle::dsde::Engine <MyTime> MyDSDE;

constexpr double X_init = 1.0;
constexpr double Y_init = 1.0;
constexpr double Alpha = 5.2;
constexpr double Beta = 3.4;
constexpr double Gamma = 2.1;
constexpr double Delta = 1.4;
constexpr double Finish = 10.0;

//
// helper class.
//

class ShowDuration
{
    std::chrono::time_point<std::chrono::steady_clock> m_begin;

public:
    ShowDuration(const std::string &str)
        : m_begin(std::chrono::steady_clock::now())
    {
        std::cout << str << ": " << std::flush;
    }

    ~ShowDuration()
    {
        try {
            auto end = std::chrono::steady_clock::now();
            auto diff = end - m_begin;
            std::cout << std::chrono::duration <double, std::milli> (diff).count()
                      << " ms\n";
        } catch (...) {
        }
    }
};

//
// function defintion.
//

struct prey_fct {
    double m_a, m_b;

    prey_fct(const double a = Alpha, const double b = Beta)
        : m_a(a)
        , m_b(b)
    {}

    double operator()(const std::array<double, 2> &x, const double) const
    {
        return m_a * x[0] - m_b * x[0] * x[1];
    }
};

struct predator_fct {
    double m_c, m_d;

    predator_fct(const double c = Gamma, const double d = Delta)
        : m_c(c)
        , m_d(d)
    {}

    double operator()(const std::array <double, 2> &x, const double) const
    {
        return - m_c * x[1] + m_d * x[0] * x[1];
    }
};

struct prey_predator_fct {
    double m_a, m_b, m_c, m_d;

    prey_predator_fct(const double a = Alpha, const double b = Beta,
                      const double c = Gamma, const double d = Delta)
        : m_a(a)
        , m_b(b)
        , m_c(c)
        , m_d(d)
    {}

    void operator()(const std::array <double, 2> &x,
                    std::array <double, 2> &dxdt,
                    const double) const
    {
        dxdt[0] = m_a * x[0] - m_b * x[0] * x[1];
        dxdt[1] = - m_c * x[1] + m_d * x[0] * x[1];
    }
};

struct harm_osc {
    double m_gam;

    harm_osc(double gam)
        : m_gam(gam)
    {}

    void operator()(const std::array<double, 2> &x,
        std::array <double, 2> &dxdt, const double)
    {
        dxdt[0] = x[1];
        dxdt[1] = -x[0] - m_gam * x[1];
    }
};

class SingleEvent : public vle::dsde::AtomicModel <MyTime,
    vle::no_port,
    vle::dsde::qss1::doubleport>
{
public:
    using parent_type = vle::dsde::AtomicModel <MyTime,
          vle::no_port,
          vle::dsde::qss1::doubleport>;

    SingleEvent(const vle::Context &ctx)
        : parent_type(ctx)
    {}

    virtual double init(const vle::Common &, const double &) override final
    {
        return 57;
    }

    virtual double delta(const double &, const double &,
                         const double &) override final
    {
        return MyTime::infinity();
    }

    virtual void lambda() const override final
    {
        y[0] = .37;
    }
};

class MySystem1 : public vle::dsde::CoupledModel <
    MyTime, vle::no_port, vle::no_port,
    vle::dsde::qss1::inputport <2>,
    vle::dsde::qss1::doubleport,
    vle::dsde::TransitionPolicyDefault <MyTime >>
{
public:
    using parent_type = vle::dsde::CoupledModel <
                        MyTime, vle::no_port, vle::no_port,
                        vle::dsde::qss1::inputport <2>,
                        vle::dsde::qss1::doubleport,
                        vle::dsde::TransitionPolicyDefault <MyTime >>;
    using children_t = parent_type::children_t;

    vle::dsde::qss1::EquationBlock <MyTime, 2u> prey;
    vle::dsde::qss1::EquationBlock <MyTime, 2u> predator;

    MySystem1(const vle::Context &ctx, double dq, double epsilon)
        : parent_type(ctx)
        , prey(ctx, dq, epsilon, X_init, 0u, prey_fct())
        , predator(ctx, dq, epsilon, Y_init, 1u, predator_fct())
    {}

    virtual children_t children(const vle::Common &) override final
    {
        return { &prey, &predator };
    }

    virtual void post(const UpdatedPort &out,
                      UpdatedPort &in) const override final
    {
        (void)out;

        if (not prey.y.empty()) {
            predator.x[0] = prey.y[0];
            in.emplace(&predator);
        }

        if (not predator.y.empty()) {
            prey.x[1] = predator.y[0];
            in.emplace(&prey);
        }
    }
};

class MySystem2 : public vle::dsde::CoupledModel <
    MyTime, vle::no_port, vle::no_port,
    vle::dsde::qss2::inputport <2>,
    vle::dsde::qss2::doubleport,
    vle::dsde::TransitionPolicyDefault <MyTime>>
{
public:
    using parent_type = vle::dsde::CoupledModel <
    MyTime, vle::no_port, vle::no_port,
    vle::dsde::qss2::inputport <2>,
    vle::dsde::qss2::doubleport,
    vle::dsde::TransitionPolicyDefault <MyTime>>;
    using children_t = parent_type::children_t;

    vle::dsde::qss2::EquationBlock <MyTime, 2u> prey;
    vle::dsde::qss2::EquationBlock <MyTime, 2u> predator;

    MySystem2(const vle::Context &ctx, double dq)
        : parent_type(ctx)
        , prey(ctx, dq, X_init, 0u, prey_fct())
        , predator(ctx, dq, Y_init, 1u, predator_fct())
    {}

    virtual children_t children(const vle::Common &) override final
    {
        return { &prey, &predator };
    }

    virtual void post(const UpdatedPort &out,
                      UpdatedPort &in) const override final
    {
        (void)out;

        if (not prey.y.empty()) {
            predator.x[0] = prey.y.m_value;
            in.emplace(&predator);
        }

        if (not predator.y.empty()) {
            prey.x[1] = predator.y.m_value;
            in.emplace(&prey);
        }
    }
};

class MySystem11 : public vle::dsde::CoupledModel <
    MyTime, vle::no_port, vle::no_port,
    vle::dsde::qss1::inputport <2>,
    vle::dsde::qss1::doubleport,
    vle::dsde::TransitionPolicyDefault <MyTime>>
{
public:
    using parent_type = vle::dsde::CoupledModel <
    MyTime, vle::no_port, vle::no_port,
    vle::dsde::qss1::inputport <2>,
    vle::dsde::qss1::doubleport,
    vle::dsde::TransitionPolicyDefault <MyTime>>;
    using children_t = parent_type::children_t;

    vle::dsde::qss1::Equation <MyTime, 2> prey;
    vle::dsde::qss1::Equation <MyTime, 2> predator;

    MySystem11(const vle::Context &ctx, double dq, double epsilon)
        : parent_type(ctx)
        , prey(ctx, dq, epsilon, X_init, 0u, prey_fct())
        , predator(ctx, dq, epsilon, Y_init, 1u, predator_fct())
    {}

    virtual children_t children(const vle::Common &) override final
    {
        return { &prey, &predator };
    }

    virtual void post(const UpdatedPort &out,
                      UpdatedPort &in) const override final
    {
        (void)out;

        if (not prey.y.empty()) {
            predator.x[0] = prey.y[0];
            in.emplace(&predator);
        }

        if (not predator.y.empty()) {
            prey.x[1] = predator.y[0];
            in.emplace(&prey);
        }
    }
};

class MySystem22 : public vle::dsde::CoupledModel <
    MyTime, vle::no_port, vle::no_port,
    vle::dsde::qss2::inputport <2>,
    vle::dsde::qss2::doubleport,
    vle::dsde::TransitionPolicyDefault <MyTime>>
{
public:
    using parent_type = vle::dsde::CoupledModel <
    MyTime, vle::no_port, vle::no_port,
    vle::dsde::qss2::inputport <2>,
    vle::dsde::qss2::doubleport,
    vle::dsde::TransitionPolicyDefault <MyTime>>;
    using children_t = parent_type::children_t;

    vle::dsde::qss2::Equation <MyTime, 2> prey;
    vle::dsde::qss2::Equation <MyTime, 2> predator;

    MySystem22(const vle::Context &ctx, double dq)
        : parent_type(ctx)
        , prey(ctx, dq, X_init, 0u, prey_fct())
        , predator(ctx, dq, Y_init, 1u, predator_fct())
    {}

    virtual children_t children(const vle::Common &) override final
    {
        return { &prey, &predator };
    }

    virtual void post(const UpdatedPort &out,
                      UpdatedPort &in) const override final
    {
        (void)out;

        if (not prey.y.empty()) {
            predator.x[0] = prey.y.m_value;
            in.emplace(&predator);
        }

        if (not predator.y.empty()) {
            prey.x[1] = predator.y.m_value;
            in.emplace(&prey);
        }
    }
};

struct push_back_state_and_time {
    std::vector<std::array <double, 2>> &m_states;
    std::vector<double> &m_times;

    push_back_state_and_time(std::vector<std::array <double, 2>> &states,
                             std::vector<double> &times)
        : m_states(states)
        , m_times(times)
    {}

    void operator()(const std::array <double, 2> &x, double t)
    {
        m_states.push_back(x);
        m_times.push_back(t);
    }
};

TEST_CASE("main/simple")
{
    ShowDuration d("main/simple");
    vle::Context ctx = std::make_shared <vle::ContextImpl>();
    MyDSDE dsde_engine;
    const double dq = 1;
    const double epsilon = 1;
    vle::dsde::qss1::EquationBlock<MyTime, 1u> model(
        ctx, dq, epsilon, 0, 0u,
    [](const std::array <double, 1>& x, const double) {
        return -x[0] + 9.5;
    });
    std::ofstream ofs("simple.dat");
    REQUIRE(ofs);
    vle::SimulationStep <MyDSDE> sim(ctx, dsde_engine);
    double current = sim.init(model, 0.0);

    while (sim.step(model, current, 20.0)) {
        ofs << current << '\t'
            << model.value() << '\n';
    }
}

TEST_CASE("main/rk", "run")
{
    {
        ShowDuration d("prey/predator: rk4");
        using namespace boost::numeric::odeint;
        std::array <double, 2> x = {{ X_init, Y_init }};
        runge_kutta4 <std::array <double, 2>> stepper;
        std::vector <std::array <double, 2>> x_vec;
        std::vector <double> times;
        std::size_t steps = integrate_const(
                                stepper,
                                prey_predator_fct(),
                                x, 0.0, Finish, 0.01,
                                push_back_state_and_time(x_vec, times));
        std::ofstream ofs("RK4.dat");
        REQUIRE(ofs);

        for (auto i = 0ul; i <= steps; ++i)
            ofs << times[i] << '\t'
                << x_vec[i][0] << '\t'
                << x_vec[i][1] << '\n';
    }
    {
        ShowDuration d("prey/predator: qss1 merged integrator");
        vle::Context ctx = std::make_shared <vle::ContextImpl>();
        MyDSDE dsde_engine;
        const double dq = 0.001;
        const double epsilon = 0.001;
        MySystem11 model(ctx, dq, epsilon);
        std::ofstream ofs("Equation.dat");
        REQUIRE(ofs);
        vle::SimulationStep <MyDSDE> sim(ctx, dsde_engine);
        double current = sim.init(model, 0.0);

        while (sim.step(model, current, Finish)) {
            ofs << current << '\t'
                << model.prey.value() << '\t'
                << model.predator.value() << '\n';
        }
    }
    {
        ShowDuration d("prey/predator: qss1");
        vle::Context ctx = std::make_shared <vle::ContextImpl>();
        MyDSDE dsde_engine;
        const double dq = 0.001;
        const double epsilon = 0.001;
        MySystem1 model(ctx, dq, epsilon);
        std::ofstream ofs("EquationBlock.dat");
        REQUIRE(ofs);
        vle::SimulationStep <MyDSDE> sim(ctx, dsde_engine);
        double current = sim.init(model, 0.0);

        while (sim.step(model, current, Finish)) {
            ofs << current << '\t'
                << model.prey.value() << '\t'
                << model.predator.value() << '\n';
        }
    }
    {
        ShowDuration d("prey/predator: qss2");
        vle::Context ctx = std::make_shared <vle::ContextImpl>();
        MyDSDE dsde_engine;
        const double dq = 0.001;
        MySystem2 model(ctx, dq);
        std::ofstream ofs("EquationBlock2.dat");
        REQUIRE(ofs);
        vle::SimulationStep <MyDSDE> sim(ctx, dsde_engine);
        double current = sim.init(model, 0.0);

        while (sim.step(model, current, Finish)) {
            ofs << current << '\t'
                << model.prey.value() << '\t'
                << model.predator.value() << '\n';
        }
    }
    {
        ShowDuration d("prey/predator: qss2 merged integrator");
        vle::Context ctx = std::make_shared <vle::ContextImpl>();
        MyDSDE dsde_engine;
        const double dq = 0.001;
        MySystem22 model(ctx, dq);
        std::ofstream ofs("Equation2.dat");
        REQUIRE(ofs);
        vle::SimulationStep <MyDSDE> sim(ctx, dsde_engine);
        double current = sim.init(model, 0.0);

        while (sim.step(model, current, Finish)) {
            ofs << current << '\t'
                << model.prey.value() << '\t'
                << model.predator.value() << '\n';
        }
    }

    std::ofstream ofs("plot.gnuplot");
    REQUIRE(ofs);
    ofs << "set terminal dumb size 160,80 aspect 2,1\n"
        << "set multiplot layout 3,2 rowsfirst\n"
        << "\n"
        << "plot 'RK4.dat' using 1:2 w l\n"
        << "plot 'RK4.dat' using 1:3 w l\n"
        << "\n"
        << "plot 'Equation.dat' using 1:2 w l\n"
        << "plot 'Equation.dat' using 1:3 w l\n"
        << "\n"
        << "plot 'EquationBlock.dat' using 1:3 w l\n"
        << "plot 'EquationBlock.dat' using 1:2 w l\n"
        << "\n"
        << "plot 'EquationBlock2.dat' using 1:3 w l\n"
        << "plot 'EquationBlock2.dat' using 1:2 w l\n"
        << "\n"
        << "plot 'Equation2.dat' using 1:2 w l\n"
        << "plot 'Equation2.dat' using 1:3 w l\n"
        << "unset multiplot\n";
}


#include <algorithm>
#include <boost/optional.hpp>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <functional>
#include <iomanip>
#include <iostream>
#include <limits>
#include <list>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#define REP(i, n) for(int i = 0, i##_MACRO = (n); i < i##_MACRO; i++)
#define RANGE(i, a, b) for(int i = (a), i##_MACRO = (b); i < i##_MACRO; i++)
#define EACH(e, a) for(auto&& e : a)
#define ALL(a) std::begin(a), std::end(a)
#define RALL(a) std::rbegin(a), std::rend(a)
#define FILL(a, n) memset((a), n, sizeof(a))
#define FILLZ(a) FILL(a, 0)
#define INT(x) (static_cast<int>(x))
#define PRECISION(x) std::fixed << std::setprecision(x)

using namespace std;

using ll = long long;
using VI = vector<int>;
using VI2D = vector<vector<int>>;

constexpr int INF = 2e9;
constexpr double EPS = 1e-10;
constexpr double PI = acos(-1.0);

constexpr int dx[] = {-1, 0, 1, 0};
constexpr int dy[] = {0, -1, 0, 1};

template <typename T>
constexpr int sign(T x) {
	return x < 0 ? -1 : x > 0 ? 1 : 0;
}

template <>
constexpr int sign(double x) {
	return x < -EPS ? -1 : x > EPS ? 1 : 0;
}

template <typename T, typename U>
constexpr void chmax(T& m, U x) {
	m = max(m, x);
}

template <typename T, typename U>
constexpr void chmin(T& m, U x) {
	m = min(m, x);
}

template <typename T>
constexpr T square(T x) {
	return x * x;
}

struct Point {
	int x;
	int y;
};

// 迷路探索
// 予め端は壁にしておく
const char wall = '#';	// 壁
const char space = '.'; // 通路
std::vector<std::vector<int>> bfs(const std::vector<std::vector<char>>& maze,
								  std::pair<int, int> s) {
	const int h = maze.size();
	const int w = maze[0].size();
	std::vector<std::vector<int>> distances(h, std::vector<int>(w, INF));
	distances[s.second][s.first] = 0;
	std::queue<std::pair<int, int>> q;
	q.push(s);
	while(!q.empty()) {
		std::pair<int, int> p = q.front();
		q.pop();
		int d = distances[p.second][p.first];
		for(size_t i = 0; i < 4; i++) {
			if((p.second + dy[i] < 0 || h <= p.second + dy[i]) ||
			   (p.first + dx[i] < 0 || w <= p.first + dx[i]) ||
			   (distances[p.second + dy[i]][p.first + dx[i]] < INF) ||
			   (maze[p.second + dy[i]][p.first + dx[i]] == wall)) {
				continue;
			}

			distances[p.second + dy[i]][p.first + dx[i]] = d + 1;
			q.push(make_pair(p.first + dx[i], p.second + dy[i]));
		}
	}
	return distances;
}

int main() {
	int h, w;
	cin >> h >> w;
	vector<vector<char>> s(h, vector<char>(w));
	int wc = 0;
	REP(i, h) {
		string line;
		cin >> line;
		REP(j, w) {
			s[i][j] = line[j];
			wc += s[i][j] == wall ? 1 : 0;
		}
	}

	if(s[0][0] == '#') {
		cout << -1 << endl;
		return 0;
	}

	VI2D dists = bfs(s, make_pair(0, 0));
	cout << (dists[h - 1][w - 1] == INF
				 ? -1
				 : (h * w - (dists[h - 1][w - 1] + 1) - wc))
		 << endl;
	return 0;
}


//==================================================================================================
/**
  Copyright 2016 NumScale SAS

  Distributed under the Boost Software License, Version 1.0.
  (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
**/
//==================================================================================================
#ifndef BOOST_SIMD_ARCH_COMMON_SIMD_FUNCTION_TRUNC_HPP_INCLUDED
#define BOOST_SIMD_ARCH_COMMON_SIMD_FUNCTION_TRUNC_HPP_INCLUDED

#include <boost/simd/detail/overload.hpp>
#include <boost/simd/detail/constant/maxflint.hpp>
#include <boost/simd/function/abs.hpp>
#include <boost/simd/function/if_else.hpp>
#include <boost/simd/function/tofloat.hpp>
#include <boost/simd/function/toint.hpp>
#include <boost/simd/function/raw.hpp>
#include <boost/simd/function/std.hpp>

namespace boost { namespace simd { namespace ext
{
  namespace bd = boost::dispatch;
  namespace bs = boost::simd;

  //------------------------------------------------------------------------------------------------
  // Integer cases are no-op
  BOOST_DISPATCH_OVERLOAD ( trunc_
                          , (typename A0, typename X)
                          , bd::cpu_
                          , bs::pack_<bd::integer_<A0>, X>
                          )
  {
    BOOST_FORCEINLINE A0 operator()( const A0& a0) const BOOST_NOEXCEPT
    {
      return a0;
    }
  };

  BOOST_DISPATCH_OVERLOAD ( trunc_
                          , (typename A0, typename X)
                          , bd::cpu_
                          , bs::raw_tag
                          , bs::pack_<bd::integer_<A0>, X>
                          )
  {
    BOOST_FORCEINLINE A0 operator()(bs::raw_tag const&, const A0& a0) const BOOST_NOEXCEPT
    {
      return a0;
    }
  };

  BOOST_DISPATCH_OVERLOAD ( trunc_
                          , (typename A0, typename X)
                          , bd::cpu_
                          , bs::std_tag
                          , bs::pack_<bd::integer_<A0>, X>
                          )
  {
    BOOST_FORCEINLINE A0 operator()(bs::std_tag const&, const A0& a0) const BOOST_NOEXCEPT
    {
      return a0;
    }
  };

  //------------------------------------------------------------------------------------------------
  // FP cases
  BOOST_DISPATCH_OVERLOAD_IF( trunc_
                            , (typename A0, typename X)
                            , (detail::is_native<X>)
                            , bd::cpu_
                            , bs::pack_<bd::floating_<A0>, X>
                            )
  {
    BOOST_FORCEINLINE A0 operator()(const A0& a0) const BOOST_NOEXCEPT
    {
      return if_else( bs::abs(a0) < Maxflint<A0>(), raw_(trunc)(a0) , a0);
    }
  };

  BOOST_DISPATCH_OVERLOAD_IF( trunc_
                            , (typename A0, typename X)
                            , (detail::is_native<X>)
                            , bd::cpu_
                            , bs::raw_tag
                            , bs::pack_<bd::floating_<A0>, X>
                            )
  {
    BOOST_FORCEINLINE A0 operator()(bs::raw_tag const&, const A0& a0) const BOOST_NOEXCEPT
    {
      return tofloat(toint(a0));
    }
  };
} } }

#endif


/**
 * Copyright (C) Omar Thor <omarthoro@gmail.com> - All Rights Reserved
 * Unauthorized copying of this file, via any medium is strictly prohibited
 * Proprietary and confidential
 *
 * Written by Omar Thor <omarthoro@gmail.com>, 2017
 */

//#include "sp/util/timing.hpp"
//#include "sp/util/rand.hpp"
//#include "sp/util/typename.hpp"

#include <iostream>
#include <iomanip>
#define BOOST_TEST_MODULE sp_algo_nn
#include <boost/test/unit_test.hpp>
#include "sp/algo/nn.hpp"
#include "assert_matrix.hpp"

using namespace sp::algo::nn;
using namespace sp::testing;

BOOST_AUTO_TEST_CASE(test_normalize_class_vector_sequential) {

    class_vector_type classes{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    const size_t batch_size = 2;
    using network_def = network<fully_connected_layer<volume_dims<1, 1, 1>, 10>>;
    network_def network;

    auto [min, max] = network.out_target_range();

    auto result = prepare_labels(network, batch_size, classes);

    BOOST_REQUIRE_EQUAL(result.size(), 5);
    for(size_t b = 0; b < 5; ++b) {
        /* each batch contains 20 samples */
        BOOST_REQUIRE_EQUAL(result[b].size(), 20);
        for(size_t s = 0; s < 2; ++s) {
            for(auto& cl : classes) {
                if(classes[b * 2 + s] == cl) {
                    BOOST_REQUIRE_EQUAL(result[b](s, cl, 0, 0), max);
                } else {
                    BOOST_REQUIRE_EQUAL(result[b](s, cl, 0, 0), min);
                }
            }
        }
    }
}

BOOST_AUTO_TEST_CASE(test_normalize_class_vector_nonesequential) {

    class_vector_type classes{5, 1, 8, 3, 9, 2, 7, 4, 6, 99};
    const size_t batch_size = 2;
    using network_def = network<fully_connected_layer<volume_dims<1, 1, 1>, 150>>;
    network_def network;

    auto [min, max] = network.out_target_range();

    auto result = prepare_labels(network, batch_size, classes);

    BOOST_REQUIRE_EQUAL(result.size(), 5);
    for(size_t b = 0; b < 5; ++b) {
        BOOST_REQUIRE_EQUAL(result[b].size(), 300);
        for(size_t s = 0; s < 2; ++s) {
            for(size_t o = 0; o < 150; ++o) {
                if(classes[b * 2 + s] == o) {
                    BOOST_REQUIRE_EQUAL(result[b](s, o, 0, 0), max);
                } else {
                    BOOST_REQUIRE_EQUAL(result[b](s, o, 0, 0), min);
                }
            }
        }
    }
}
BOOST_AUTO_TEST_CASE(test_normalize_sample_vector) {
    const size_t sample_count = 10;
    sample_vector_type samples(sample_count, sample_type(1, 1, 1));
    for(size_t s = 0; s < sample_count; ++s) {
        samples[s].setRandom();
    }
    class_vector_type classes{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    using network_def = network<fully_connected_layer<volume_dims<1, 1, 1>, 10>>;
    network_def network;

    auto res = prepare_batch(network, 2, samples);
    BOOST_REQUIRE_EQUAL(res.size(), 5);
}


#include "Day05-HydrothermalVenture.h"

#include "LineSegment.h"
#include "HydrothermalVentAnalyzer.h"

#include <AdventOfCodeCommon/DisableLibraryWarningsMacros.h>

__BEGIN_LIBRARIES_DISABLE_WARNINGS
#include <boost/algorithm/string.hpp>

#include <unordered_map>
__END_LIBRARIES_DISABLE_WARNINGS

namespace AdventOfCode
{
namespace Year2021
{
namespace Day05
{

Vector2D parseCoordinatesString(const std::string& coordinatesString)
{
    std::vector<std::string> tokens;
    boost::split(tokens, coordinatesString, boost::is_any_of(","));

    return Vector2D{std::stoi(tokens.at(0)), std::stoi(tokens.at(1))};
}

LineSegment parseVentLine(const std::string& ventLine)
{
    std::vector<std::string> tokens;
    boost::split(tokens, ventLine, boost::is_any_of(" ->"), boost::token_compress_on);

    Vector2D start = parseCoordinatesString(tokens.at(0));
    Vector2D end = parseCoordinatesString(tokens.at(1));

    return LineSegment{std::move(start), std::move(end)};
}

std::vector<LineSegment> parseVentLines(const std::vector<std::string>& ventLines)
{
    std::vector<LineSegment> ventLineSegments;

    for (const auto& line : ventLines)
    {
        LineSegment ventLineSegment = parseVentLine(line);
        ventLineSegments.push_back(std::move(ventLineSegment));
    }

    return ventLineSegments;
}

int numPointsWhereHorizontalOrVerticalLinesOverlap(const std::vector<std::string>& ventLines)
{
    std::vector<LineSegment> ventLineSegments = parseVentLines(ventLines);
    std::vector<LineSegment> axisParallelVentLineSegments;
    std::copy_if(ventLineSegments.cbegin(), ventLineSegments.cend(), std::back_inserter(axisParallelVentLineSegments), [](const auto& lineSegment)
                 {
                     return lineSegment.isAxisParallel();
                 });

    HydrothermalVentAnalyzer hydrothermalVentAnalyzer{axisParallelVentLineSegments};
    hydrothermalVentAnalyzer.analyzeOverlaps();
    return hydrothermalVentAnalyzer.getNumPointsWithOverlap();
}

int numPointsWhereLinesOverlap(const std::vector<std::string>& ventLines)
{
    std::vector<LineSegment> ventLineSegments = parseVentLines(ventLines);
    HydrothermalVentAnalyzer hydrothermalVentAnalyzer{ventLineSegments};
    hydrothermalVentAnalyzer.analyzeOverlaps();
    return hydrothermalVentAnalyzer.getNumPointsWithOverlap();
}

}
}
}


/*
 * Copyright (c) 2018 Spotify AB.
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

// Just for testing
#define private public

#include <boost/test/floating_point_comparison.hpp>
#include <boost/test/unit_test.hpp>

#include "BandSplitter.h"

BOOST_AUTO_TEST_SUITE(PluginUtilTests)
using namespace nativeformat::plugin::util;

BOOST_AUTO_TEST_CASE(testBandSplitterProperties4band) {
  std::vector<double> freqs{400, 800, 1600};
  BandSplitter splitter(freqs, 44100.0, 2);
  const size_t expected_bands = freqs.size() + 1;
  size_t actual_bands = splitter.bands();
  BOOST_CHECK_EQUAL(actual_bands, expected_bands);

  for (size_t i = 0; i < freqs.size(); ++i) {
    const float actual_freq = splitter.crossover(i);
    const float expected_freq = freqs[i] / 44100.0;
    BOOST_CHECK_EQUAL(actual_freq, expected_freq);
  }

  const size_t expected_stages = std::ceil(std::log2(expected_bands));
  for (size_t i = 0; i < actual_bands; ++i) {
    const size_t actual_stages = splitter.stages(i);
    BOOST_CHECK_EQUAL(actual_stages, expected_stages);
  }

  const size_t expected_filter_count = 2 * freqs.size();
  const size_t actual_filter_count = splitter._filter_tree.size();
  BOOST_CHECK_EQUAL(actual_filter_count, expected_filter_count);
}

BOOST_AUTO_TEST_CASE(testBandSplitterProperties3band) {
  std::vector<double> freqs{400, 1200};
  BandSplitter splitter(freqs, 44100.0, 2);

  const size_t expected_bands = freqs.size() + 1;
  size_t actual_bands = splitter.bands();
  BOOST_CHECK_EQUAL(actual_bands, expected_bands);

  for (size_t i = 0; i < freqs.size(); ++i) {
    const float actual_freq = splitter.crossover(i);
    const float expected_freq = freqs[i] / 44100.0;
    BOOST_CHECK_EQUAL(actual_freq, expected_freq);
  }

  const std::vector<size_t> expected_stages{1, 2, 2};
  for (size_t i = 0; i < actual_bands; ++i) {
    const size_t actual_stages = splitter.stages(i);
    BOOST_CHECK_EQUAL(actual_stages, expected_stages[i]);
  }
  const size_t expected_filter_count = 2 * freqs.size();
  const size_t actual_filter_count = splitter._filter_tree.size();
  BOOST_CHECK_EQUAL(actual_filter_count, expected_filter_count);
}

BOOST_AUTO_TEST_CASE(testBandSplitterPropertiesNband) {
  std::vector<double> freqs{3.125};
  for (size_t i = 0; i < 13; ++i) {
    BandSplitter splitter(freqs, 44100.0, 2);
    const size_t expected_filter_count = 2 * freqs.size();
    const size_t actual_filter_count = splitter._filter_tree.size();
    BOOST_CHECK_EQUAL(actual_filter_count, expected_filter_count);
    freqs.push_back(freqs.back() * 2);
  }
}

BOOST_AUTO_TEST_CASE(testButterFilterBandpassCoefficients) {
  ButterFilter filter(3, FilterType::BandPassFilter, 2);
  filter.compute(0.04666, 0.06666);

  const size_t expected_poles = 6;
  const size_t expected_zeros = 6;
  const double expected_gain = 4.553604437e+03;
  const std::vector<double> expected_x_coeffs{-1.0, 0.0, 3.0, 0.0,
                                              -3.0, 0.0, 1.0};
  const std::vector<double> expected_y_coeffs{
      -0.7776385602, 4.5653450765,   -11.4701594530,
      15.7555532150, -12.4737077230, 5.3990179269,
      -1.0};

  BOOST_CHECK_EQUAL(filter._zplane.numpoles, expected_poles);
  BOOST_CHECK_EQUAL(filter._zplane.numzeros, expected_zeros);
  BOOST_CHECK_CLOSE(filter._gain, expected_gain, 1.0e-06);
  for (size_t i = 0; i <= expected_zeros; ++i) {
    BOOST_CHECK_CLOSE(filter._x_coeffs[i], expected_x_coeffs[i], 1.0e-04);
  }
  for (size_t i = 0; i <= expected_poles; ++i) {
    BOOST_CHECK_CLOSE(filter._y_coeffs[i], expected_y_coeffs[i], 1.0e-04);
  }
}

BOOST_AUTO_TEST_SUITE_END()