#include <iostream>
#include <experimental/filesystem>

namespace fs = std::experimental::filesystem;

#include "include.hh"

#include <boost/numeric/ublas/matrix.hpp>
#include <boost/numeric/ublas/io.hpp>

#include <iostream>
#include <map>
#include <string>

using namespace EMC;
using namespace std;

namespace EMC {

enum task {
    tNONE = 0,
    ASSEMBLE,
	REBIN,
    INVERT,
	REBIN_INVERT,
	DE_PILEUP,
    SUBTRACT,
	MULTIPLY_RESPONSE_CURVE,
    PRINT_STATS,
	APPLY,
	FIT_VALUES
};

map<string, task> taskNameMap = {
    { "assemble", ASSEMBLE },
    { "rebin", REBIN },
    { "invert", INVERT},
    { "rebin_invert", REBIN_INVERT},
    { "de_pileup", DE_PILEUP},
    { "subtract", SUBTRACT},
    { "multiply_resp_curve", MULTIPLY_RESPONSE_CURVE},
    { "print_stats", PRINT_STATS},
    { "apply", APPLY },
    { "fit_values", FIT_VALUES }
};

} /* namespace EMC */

using namespace boost::numeric;

int main(int argc, char* argv[]) {
    cout << endl << "error-matrix-calculation v1 (c) Simon Michalke 2020" << endl;

    task todo = tNONE;
    bool forceOverwrite = false;
    string output;
    string input;

    string matrixFileName;

    std::vector<double> inputBinCenters;
    matrixType matType = mtNumber;
    int countRate = -1;

    double minRow = -1;
    double minCol = -1;
    double minRowBin = -1;
    double minColBin = -1;
    double maxRow = -1;
    double maxCol = -1;

    float rowErrorEst = -1;
    float colErrorEst = -1;

    double lowerBound = 0;

    bool fail = false;

	for (int argPos = 1; argPos < argc; argPos++) {
		if (argv[argPos][0] == '-') {
			switch (argv[argPos][1]) {
			case 't': // what task
				argPos++;
				if (taskNameMap.find(argv[argPos]) == taskNameMap.end()) {
					cout << "tasks not recognized!" << endl;
					fail = true;
				} else {
					todo = taskNameMap.at(argv[argPos]);
				}
				break;
			case 'f':
				forceOverwrite = true;
				break;
			case 'i': {
				argPos++;
				input = argv[argPos];
			}
				break;
			case 'o':
				argPos++;
				output = argv[argPos];
				break;
			case 'b': {
				argPos++;
				auto binVec = split(argv[argPos], ',');
				unsigned int i = 0;
				unsigned int binCount = binVec.size();
				double binIncrement = -1;
				for (;i<binVec.size();i++) {
					if (binVec[i] == "...") {
						binIncrement = inputBinCenters[i-1] - inputBinCenters[i-2];
						inputBinCenters.push_back(inputBinCenters[i-1] + binIncrement);
						binCount = std::stod(binVec[i+1]) / (inputBinCenters[i-1] - inputBinCenters[i-2]) - 1;
						break;
					}
					inputBinCenters.push_back(std::stod(binVec[i]));
				}
				for (;i<binCount;i++) {
					inputBinCenters.push_back(inputBinCenters[i] + binIncrement);
				}
			}
				break;
			case 'm': {
				argPos++;
				auto maxStrVec = split(argv[argPos], ',');
				minRow = std::stod(maxStrVec[0]);
				minCol = std::stod(maxStrVec[1]);
				minRowBin = std::stod(maxStrVec[2]);
				minColBin = std::stod(maxStrVec[3]);
				maxRow = std::stod(maxStrVec[4]);
				maxCol = std::stod(maxStrVec[5]);
			}
				break;
			case 'T':
				argPos++;
				if (matrixTypeMap.find(argv[argPos]) == matrixTypeMap.end()) {
					cout << "matrix type not recognized!" << endl;
					fail = true;
				} else {
					matType = matrixTypeMap.at(argv[argPos]);
				}
				break;
			case 'c':
				argPos++;
				countRate = std::stoi(argv[argPos]);
				break;
			case 'l':
				argPos++;
				lowerBound = std::stof(argv[argPos]);
				break;
			case 'M':
				argPos++;
				matrixFileName = argv[argPos];
				break;
			case 'd': {
				argPos++;
				auto strVec = split(argv[argPos], ',');
				rowErrorEst = std::stod(strVec[0]);
				colErrorEst = std::stod(strVec[1]);
				break;
			}
			default:
				cout << "unexpected flag in argument number " << argPos << endl;
				fail = true;
			}
		} else {
			cout << "expected \"-\" in argument number " << argPos << endl;
			fail = true;
		}
	}

    if (fail) {
        cout << "aborted" << endl;
        todo = tNONE;
    }

	switch (todo) {
	case ASSEMBLE: {
		cout << "assembling matrix" << endl;
		int retVal = assemble(forceOverwrite, input, output, inputBinCenters,
				matType, countRate);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case REBIN: {
		cout << "rebinning matrix" << endl;
		int retVal = rebin(forceOverwrite, input, output, countRate, minRow,
				minCol, minRowBin, minColBin, maxRow, maxCol);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case INVERT: {
		cout << "inverting matrix" << endl;
		int retVal = invert(forceOverwrite, input, output);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case REBIN_INVERT: {
		cout << "rebinning and inverting matrix" << endl;
		int retVal = rebin_invert(forceOverwrite, input, output, countRate, minRow,
				minCol, minRowBin, minColBin, maxRow, maxCol, rowErrorEst, colErrorEst);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case DE_PILEUP: {
		cout << "reversing pileup" << endl;
		int retVal = de_pileup(forceOverwrite, input, output, countRate, lowerBound);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case SUBTRACT: {
		cout << "subtracting matrix" << endl;
		int retVal = subtract(forceOverwrite, input, output, matrixFileName);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case MULTIPLY_RESPONSE_CURVE: {
		cout << "multiply-response-curving matrix" << endl;
		int retVal = multiply_resp_curve(forceOverwrite, input, output, matrixFileName);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case PRINT_STATS: {
		cout << "printing matrix stats matrix" << endl;
		int retVal = print_stats(matrixFileName);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case APPLY: {
		cout << "applying matrix" << endl;
		int retVal = apply(forceOverwrite, input, output, matrixFileName);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case FIT_VALUES: {
		cout << "fitting matrix parameters" << endl;
		int retVal = fit_values(input, output, countRate, minRow,
				minCol, minRowBin, minColBin, maxRow, maxCol);
		cout << "finished with status " << retVal << endl;
		break;
	}
	case tNONE:
    default:
        cout << "" << endl << endl;
        cout << " supported general arguments:" << endl;
        cout << "   -t [ de_pileup | assemble | rebin | invert | rebin_invert | apply | print_stats | fit_values ]" << endl;
        cout << "      specifies what task is being executed" << endl;
        cout << "      should be specified first" << endl;
        cout << "   -f overwrite output files / directories, deletes target files before writing" << endl;
        cout << "   -i <filename>" << endl;
        cout << "      input file name" << endl;
        cout << "   -o <filename>" << endl;
        cout << "      output file name" << endl << endl;
        cout << "   In case of a vector the columns are expected to be bin center, value, value error" << endl;
        cout << "   with space character for spacing" << endl;
        cout << " arguments for specific tasks:" << endl;
        cout << "   de_pileup: reconstructs pileup numerically" << endl;
        cout << "     -c <integer>" << endl;
        cout << "        total pixel count" << endl;
        cout << "     -l <float>" << endl;
        cout << "        up to this energy counts are set to 0" << endl;
        cout << "   assemble: assembles a matrix based on putting spectrums into columns, output bins determined by input spectrum" << endl;
        cout << "     -i <filename format string>" << endl;
        cout << "        input file name. String that will be formatted using sprintf with data passed by -b" << endl;
        cout << "        expects 'bin_center counts' in each line" << endl;
        cout << "     -b <bin0>,<bin1>,..." << endl;
        cout << "        list of input bin centers (floating point possible). Passed to -i via sprintf" << endl;
        cout << "        for evently spaced bins e.g. 1,2,...,100 is also possible" << endl;
        cout << "     -T <type>" << endl;
        cout << "        output type; possible values are 'number', 'probability' and 'density'" << endl;
        cout << "        in case of density bin width is determined based on the bin width in input files" << endl;
        cout << "     -c <integer>" << endl;
        cout << "        total events, count rate for normalization" << endl;
        cout << "   rebin: rebins a matrix by merging bins in order to increase statistical significance" << endl;
        cout << "     -c <integer>" << endl;
        cout << "        dimension of output square matrix" << endl;
        cout << "     -m <minRow><minCol><minRowBin><minColBin><maxRow><maxCol>" << endl;
        cout << "        row and column bin parameters, float" << endl;
        cout << "   rebin_invert: rebins a matrix and inverts it. Estimates errors as well." << endl;
        cout << "     -c <integer>" << endl;
        cout << "        dimension of output square matrix" << endl;
        cout << "     -m <minRow><minCol><minRowBin><minColBin><maxRow><maxCol>" << endl;
        cout << "        row and column bin parameters, float" << endl;
        cout << "     -d <row Error, float>,<column Error, float>" << endl;
        cout << "        estimate error based on row and column error." << endl;
        cout << "   invert: inverts a matrix" << endl;
        cout << "   subtract: subtracts spectra" << endl;
        cout << "        uses <filelane>.cntr and <filelane>.step for input, output and filename of subtracted spectrum." << endl;
        cout << "     -M <filename>" << endl;
        cout << "        filename of the spectrum that will be subtracted" << endl;
        cout << "   multiply_resp_curve: scales spectrum with response curve" << endl;
        cout << "        uses <filelane>.cntr and <filelane>.step for input and output" << endl;
        cout << "     -M <filename>" << endl;
        cout << "        filename of the transmission curve" << endl;
        cout << "        this is assumed to be more dense than the input spectrum" << endl;
        cout << "   print_stats: inverts a matrix" << endl;
        cout << "     -M <filename>" << endl;
        cout << "        filename of the matriy" << endl;
        cout << "   apply: inverts a matrix; input and output are spectrum files" << endl;
        cout << "        first column is expected to be bin center and second the counts in that bin" << endl;
        cout << "        vector will be rebinned and rescaled to matrix dimensions" << endl;
        cout << "        output is bin center and count rate" << endl;
        cout << "     -M <filename>" << endl;
        cout << "        filename of the matrix" << endl;
        cout << "   fit_values: optimizes rebinning based on inverted matrix error metric" << endl;
        cout << "               output is the base filename for the rebinned (.rebin.mat) and inverted (.rebin.inverted.mat) matrix" << endl;
        cout << "               these matrices will be overwritten multiple times!" << endl;
        cout << "     -c <integer>" << endl;
        cout << "        dimension of output square matrix" << endl;
        cout << "     -m <minRow><minCol><minRowBin><minColBin><maxRow><maxCol>" << endl;
        cout << "        initial row and column bin parameters; minCol and maxCol will stay constant" << endl;
        cout << endl;
    }

    return 0;
}


/* Siconos is a program dedicated to modeling, simulation and control
 * of non smooth dynamical systems.
 *
 * Copyright 2021 INRIA.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

#include <boost/numeric/ublas/matrix_proxy.hpp>
#include <boost/numeric/ublas/vector_proxy.hpp>

#include "SiconosVector.hpp"
#include "SimpleMatrix.hpp"
#include "BlockMatrixIterators.hpp"
#include "BlockMatrix.hpp"
#include "BlockVector.hpp"

#include "SiconosAlgebra.hpp"

#include "SiconosAlgebraProd.hpp" // for subprod
#include "SiconosException.hpp"
using namespace Siconos;

void subprod(const SiconosMatrix& A, const SiconosVector& x, SiconosVector& y, const Index& coord, bool init)
{
  // To compute subY = subA * subX in an "optimized" way (in comparison with y = prod(A,x) )
  // or subY += subA*subX if init = false.

  // coord is [r0A r1A c0A c1A r0x r1x r0y r1y]
  //
  // subA is the sub-matrix of A, for row numbers between r0A and r1A-1 and columns between c0A and c1A-1.
  // The same for x and y with rix and riy.

  // Check dims
  unsigned int rowA = coord[1] - coord[0];
  unsigned int colA = coord[3] - coord[2];
  unsigned int dimX = coord[5] - coord[4];
  unsigned int dimY = coord[7] - coord[6];
  if(colA != dimX)
    THROW_EXCEPTION("inconsistent sizes between A and x.");

  if(rowA != dimY)
    THROW_EXCEPTION("inconsistent sizes between A and y.");

  if(dimX > x.size() || dimY > y.size() || rowA > A.size(0) || colA > A.size(1))
    THROW_EXCEPTION("input index too large.");

  Siconos::UBLAS_TYPE numA = A.num();
  Siconos::UBLAS_TYPE numX = x.num();
  Siconos::UBLAS_TYPE numY = y.num();

  if(numA == Siconos::BLOCK)   // If A,x or y is Block
    THROW_EXCEPTION("not yet implemented for A block matrices.");

  if(numA == Siconos::ZERO)  // A = 0
  {
    if(init)
    {
      if(numY == Siconos::DENSE)
        ublas::subrange(*y.dense(), coord[6], coord[7]) *= 0.0;
      else //if(numY==Siconos::SPARSE)
        ublas::subrange(*y.sparse(), coord[6], coord[7]) *= 0.0;
    }
    //else nothing
  }
  else if(numA == Siconos::IDENTITY)  // A = identity
  {
    if(!init)
      ublas::subrange(*y.dense(), coord[6], coord[7]) += ublas::subrange(*x.dense(), coord[4], coord[5]);
    else
    {
      // if x and y do not share memory (ie are different objects)
      if(&x != &y)
        noalias(ublas::subrange(*y.dense(), coord[6], coord[7])) = ublas::subrange(*x.dense(), coord[4], coord[5]);

      // else nothing
    }
  }

  else // A is not 0 or identity
  {
    {
      if(init)
      {
        if(&x != &y)  // if no common memory between x and y.
        {
          if(numX == Siconos::DENSE)
          {
            ublas::vector_range<DenseVect> subX(*x.dense(), ublas::range(coord[4], coord[5]));

            if(numY != Siconos::DENSE)
              THROW_EXCEPTION("y (output) must be a dense vector.");
            ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));

            if(numA == Siconos::DENSE)
            {
              ublas::matrix_range<DenseMat> subA(*A.dense(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
            }
            else if(numA == Siconos::TRIANGULAR)
            {
              ublas::matrix_range<TriangMat> subA(*A.triang(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
            }
            else if(numA == Siconos::SYMMETRIC)
            {
              ublas::matrix_range<SymMat> subA(*A.sym(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
            }
            else if(numA == Siconos::SPARSE)
            {
#ifdef BOOST_LIMITATION
              THROW_EXCEPTION("ublas::matrix_range<SparseMat> does not exist for your boost distribution and your architecture.");
#else
              ublas::matrix_range<SparseMat> subA(*A.sparse(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
#endif
            }
            else //if(numA==Siconos::BANDED)
            {
              ublas::matrix_range<BandedMat> subA(*A.banded(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
            }
          }
          else //if(numX == Siconos::SPARSE)
          {
            ublas::vector_range<SparseVect> subX(*x.sparse(), ublas::range(coord[4], coord[5]));
            if(numY != Siconos::DENSE && numA != Siconos::SPARSE)
              THROW_EXCEPTION("y (output) must be a dense vector.");

            if(numA == Siconos::DENSE)
            {
              ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
              ublas::matrix_range<DenseMat> subA(*A.dense(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
            }
            else if(numA == Siconos::TRIANGULAR)
            {
              ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
              ublas::matrix_range<TriangMat> subA(*A.triang(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
            }
            else if(numA == Siconos::SYMMETRIC)
            {
              ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
              ublas::matrix_range<SymMat> subA(*A.sym(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
            }
            else if(numA == Siconos::SPARSE)
            {
#ifdef BOOST_LIMITATION
              THROW_EXCEPTION("ublas::matrix_range<SparseMat> does not exist for your boost distribution and your architecture.");
#else
              ublas::matrix_range<SparseMat> subA(*A.sparse(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));

              if(numY == Siconos::DENSE)
              {
                ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
                noalias(subY) = ublas::prod(subA, subX);
              }
              else
              {
                ublas::vector_range<SparseVect> subY(*y.sparse(), ublas::range(coord[6], coord[7]));
                noalias(subY) = ublas::prod(subA, subX);
              }
#endif
            }
            else //if(numA==Siconos::BANDED)
            {
              ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
              ublas::matrix_range<BandedMat> subA(*A.banded(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) = ublas::prod(subA, subX);
            }
          }
        }
        else // if x and y are the same object => alias
        {
          if(numX == Siconos::DENSE)
          {
            ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[4], coord[5]));
            if(numA == Siconos::DENSE)
            {
              ublas::matrix_range<DenseMat> subA(*A.dense(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
            }
            else if(numA == Siconos::TRIANGULAR)
            {
              ublas::matrix_range<TriangMat> subA(*A.triang(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
            }
            else if(numA == Siconos::SYMMETRIC)
            {
              ublas::matrix_range<SymMat> subA(*A.sym(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
            }
            else if(numA == Siconos::SPARSE)
            {
#ifdef BOOST_LIMITATION
              THROW_EXCEPTION("ublas::matrix_range<SparseMat> and vector_range<SparseVect> does not exist for your boost distribution and your architecture.");
#else
              ublas::matrix_range<SparseMat> subA(*A.sparse(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
#endif
            }
            else //if(numA==Siconos::BANDED)
            {
              ublas::matrix_range<BandedMat> subA(*A.banded(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
            }
          }
          else //if(numX == Siconos::SPARSE)
          {
            ublas::vector_range<SparseVect> subY(*y.sparse(), ublas::range(coord[4], coord[5]));
            if(numA == Siconos::DENSE)
            {
              ublas::matrix_range<DenseMat> subA(*A.dense(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
            }
            else if(numA == Siconos::TRIANGULAR)
            {
              ublas::matrix_range<TriangMat> subA(*A.triang(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
            }
            else if(numA == Siconos::SYMMETRIC)
            {
              ublas::matrix_range<SymMat> subA(*A.sym(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
            }
            else if(numA == Siconos::SPARSE)
            {
#ifdef BOOST_LIMITATION
              THROW_EXCEPTION("ublas::matrix_range<SparseMat> does not exist for your boost distribution and your architecture.");
#else
              ublas::matrix_range<SparseMat> subA(*A.sparse(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
#endif
            }
            else //if(numA==Siconos::BANDED)
            {
              ublas::matrix_range<BandedMat> subA(*A.banded(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY = ublas::prod(subA, subY);
            }
          }
        }
      }
      else // += case
      {
        if(&x != &y)  // if no common memory between x and y.
        {
          if(numX == Siconos::DENSE)
          {
            ublas::vector_range<DenseVect> subX(*x.dense(), ublas::range(coord[4], coord[5]));

            if(numY != Siconos::DENSE)
              THROW_EXCEPTION("y (output) must be a dense vector.");
            ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));

            if(numA == Siconos::DENSE)
            {
              ublas::matrix_range<DenseMat> subA(*A.dense(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
            }
            else if(numA == Siconos::TRIANGULAR)
            {
              ublas::matrix_range<TriangMat> subA(*A.triang(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
            }
            else if(numA == Siconos::SYMMETRIC)
            {
              ublas::matrix_range<SymMat> subA(*A.sym(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
            }
            else if(numA == Siconos::SPARSE)
            {
#ifdef BOOST_LIMITATION
              THROW_EXCEPTION("ublas::matrix_range<SparseMat> does not exist for your boost distribution and your architecture.");
#else
              ublas::matrix_range<SparseMat> subA(*A.sparse(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
#endif
            }
            else //if(numA==Siconos::BANDED)
            {
              ublas::matrix_range<BandedMat> subA(*A.banded(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
            }
          }
          else //if(numX == Siconos::SPARSE)
          {
            ublas::vector_range<SparseVect> subX(*x.sparse(), ublas::range(coord[4], coord[5]));
            if(numY != Siconos::DENSE && numA != Siconos::SPARSE)
              THROW_EXCEPTION("y (output) must be a dense vector.");

            if(numA == Siconos::DENSE)
            {
              ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
              ublas::matrix_range<DenseMat> subA(*A.dense(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
            }
            else if(numA == Siconos::TRIANGULAR)
            {
              ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
              ublas::matrix_range<TriangMat> subA(*A.triang(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
            }
            else if(numA == Siconos::SYMMETRIC)
            {
              ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
              ublas::matrix_range<SymMat> subA(*A.sym(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
            }
            else if(numA == Siconos::SPARSE)
            {
#ifdef BOOST_LIMITATION
              THROW_EXCEPTION("ublas::matrix_range<SparseMat> does not exist for your boost distribution and your architecture.");
#else
              ublas::matrix_range<SparseMat> subA(*A.sparse(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              if(numY == Siconos::DENSE)
              {
                ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
                noalias(subY) += ublas::prod(subA, subX);
              }
              else
              {
                ublas::vector_range<SparseVect> subY(*y.sparse(), ublas::range(coord[6], coord[7]));
                noalias(subY) += ublas::prod(subA, subX);
              }
#endif
            }
            else //if(numA==Siconos::BANDED)
            {
              ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[6], coord[7]));
              ublas::matrix_range<BandedMat> subA(*A.banded(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              noalias(subY) += ublas::prod(subA, subX);
            }
          }
        }
        else // if x and y are the same object => alias
        {
          if(numX == Siconos::DENSE)
          {
            ublas::vector_range<DenseVect> subY(*y.dense(), ublas::range(coord[4], coord[5]));
            if(numA == Siconos::DENSE)
            {
              ublas::matrix_range<DenseMat> subA(*A.dense(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
            }
            else if(numA == Siconos::TRIANGULAR)
            {
              ublas::matrix_range<TriangMat> subA(*A.triang(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
            }
            else if(numA == Siconos::SYMMETRIC)
            {
              ublas::matrix_range<SymMat> subA(*A.sym(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
            }
            else if(numA == Siconos::SPARSE)
            {
#ifdef BOOST_LIMITATION
              THROW_EXCEPTION("ublas::matrix_range<SparseMat> does not exist for your boost distribution and your architecture.");
#else
              ublas::matrix_range<SparseMat> subA(*A.sparse(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
#endif
            }
            else //if(numA==Siconos::BANDED)
            {
              ublas::matrix_range<BandedMat> subA(*A.banded(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
            }
          }
          else //if(numX == Siconos::SPARSE)
          {
            ublas::vector_range<SparseVect> subY(*y.sparse(), ublas::range(coord[4], coord[5]));
            if(numA == Siconos::DENSE)
            {
              ublas::matrix_range<DenseMat> subA(*A.dense(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
            }
            else if(numA == Siconos::TRIANGULAR)
            {
              ublas::matrix_range<TriangMat> subA(*A.triang(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
            }
            else if(numA == Siconos::SYMMETRIC)
            {
              ublas::matrix_range<SymMat> subA(*A.sym(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
            }
            else if(numA == Siconos::SPARSE)
            {
#ifdef BOOST_LIMITATION
              THROW_EXCEPTION("ublas::matrix_range<SparseMat> does not exist for your boost distribution and your architecture.");
#else
              ublas::matrix_range<SparseMat> subA(*A.sparse(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
#endif
            }
            else //if(numA==Siconos::BANDED)
            {
              ublas::matrix_range<BandedMat> subA(*A.banded(), ublas::range(coord[0], coord[1]), ublas::range(coord[2], coord[3]));
              subY += ublas::prod(subA, subY);
            }
          }
        }
      }
    }
  }
}

void subprod(const SiconosMatrix& A, const BlockVector& x, SiconosVector& y, const Index& coord, bool init)
{
  assert(!(A.isFactorized()) && "A is Factorized in prod !!");

  // Number of the subvector of x that handles element at position coord[4]
  std::size_t firstBlockNum = x.getNumVectorAtPos(coord[4]);
  // Number of the subvector of x that handles element at position coord[5]
  unsigned int lastBlockNum = x.getNumVectorAtPos(coord[5]);
  Index subCoord = coord;
  SPC::SiconosVector tmp = x.vector(firstBlockNum);
  std::size_t subSize =  tmp->size(); // Size of the sub-vector
  const SP::Index xTab = x.tabIndex();
  if(firstBlockNum != 0)
  {
    subCoord[4] -= (*xTab)[firstBlockNum - 1];
    subCoord[5] =  std::min(coord[5] - (*xTab)[firstBlockNum - 1], subSize);
  }
  else
    subCoord[5] =  std::min(coord[5], subSize);

  if(firstBlockNum == lastBlockNum)
  {
    subprod(A, *tmp, y, subCoord, init);
  }
  else
  {
    unsigned int xPos = 0 ; // Position in x of the current sub-vector of x
    bool firstLoop = true;
    subCoord[3] = coord[2] + subCoord[5] - subCoord[4];
    for(VectorOfVectors::const_iterator it = x.begin(); it != x.end(); ++it)
    {
      if((*it)->num() == Siconos::BLOCK)
        THROW_EXCEPTION("not yet implemented for x block of blocks ...");
      if(xPos >= firstBlockNum && xPos <= lastBlockNum)
      {
        tmp = x.vector(xPos);
        if(firstLoop)
        {
          subprod(A, *tmp, y, subCoord, init);
          firstLoop = false;
        }
        else
        {
          subCoord[2] += subCoord[5] - subCoord[4]; // !! old values for 4 and 5
          subSize = tmp->size();
          subCoord[4] = 0;
          subCoord[5] = std::min(coord[5] - (*xTab)[xPos - 1], subSize);
          subCoord[3] = subCoord[2] + subCoord[5] - subCoord[4];
          subprod(A, *tmp, y, subCoord, false);
        }
      }
      xPos++;
    }
  }
}


#include <cassert>
#include <iostream>
#include <boost/static_assert.hpp>
#include <boost/utility/enable_if.hpp>
#include <boost/type_traits/is_same.hpp>

template <typename V1, typename V2> class vector_sum;

template <typename T>
class vector
{
    void check_size(int that_size) const { assert(my_size == that_size); }
    void check_index(int i) const { assert(i >= 0 && i < my_size); }
  public:
    typedef T    value_type;

    explicit vector(int size)
      : my_size(size), data( new T[my_size] )
    {}

    vector()
      : my_size(0), data(0)
    {}

    vector( const vector& that )
      : my_size(that.my_size), data( new T[my_size] )
    {
	for (int i= 0; i < my_size; ++i)
	    data[i]= that.data[i];
    }

    ~vector() { if (data) delete [] data ; }

    template <typename Src>
    vector& operator=( const Src& that )
    {
	check_size(that.size());
	for (int i= 0; i < my_size; ++i)
	    data[i]= that[i];
	return *this;
    }

    int size() const { return my_size ; }

    const T& operator[]( int i ) const
    {
	check_index(i);
	return data[i];
    }

    T& operator[]( int i )
    {
	check_index(i);
	return data[i] ;
    }

  private:
    int   my_size ;
    T*    data ;
};

template <typename T>
std::ostream& operator<<(std::ostream& os, const vector<T>& v)
{
  os << '[';
  for (int i= 0; i < v.size(); ++i) os << v[i] << ',';
  os << ']';
  return os ;
}

template <typename V1, typename V2>
class vector_sum
{
  public:
    typedef typename V1::value_type value_type;
    BOOST_STATIC_ASSERT((boost::is_same<typename V2::value_type, value_type>::value));

    vector_sum(const V1& v1, const V2& v2) : v1(v1), v2(v2)
    {
	assert(v1.size() == v2.size());
    }

    int size() const { return v1.size(); }

    value_type operator[](int i) const { return v1[i] + v2[i]; }

  private:
    const V1 &v1;
    const V2 &v2;
};

template <typename  V1, typename V2>
// typename boost::enable_if_c<is_vector<V1>::value && is_vector<V2>::value, vector_sum<V1, V2> >::type
vector_sum<V1, V2>
inline operator+( const V1& x, const V2& y )
{
    return vector_sum<V1, V2>(x, y) ;
}

int main()
{

    vector<float> v( 4 ), w(4), x(4), y(4)  ;
    v[0]= v[1]= 1.0; v[2]= 2.0 ; v[3] = -3.0 ;
    w[0]= w[1]= 1.3; w[2]= 2.5 ; w[3] = -13.0 ;
    x[0]= x[1]= 1.0; x[2]= 2.0 ; x[3] = 3.0 ;

    std::cout << "v = " << v << std::endl ;

    y= v + w + x;
    std::cout << "y = " << y << std::endl ;

    return 0 ;
}


#include "cru/platform/GraphicsBase.hpp"
#include "cru/platform/bootstrap/Bootstrap.hpp"
#include "cru/platform/graphics/Factory.hpp"
#include "cru/platform/graphics/Painter.hpp"
#include "cru/platform/gui/UiApplication.hpp"
#include "cru/platform/gui/Window.hpp"

#include <dlib/matrix.h>
#include <dlib/numeric_constants.h>
#include <cmath>

using cru::platform::Point;

using matrix14 = dlib::matrix<float, 1, 4>;
using matrix13 = dlib::matrix<float, 1, 3>;
using matrix44 = dlib::matrix<float, 4, 4>;

matrix44 Identity() {
  matrix44 m;
  m = dlib::identity_matrix<float, 4>();
  return m;
}

matrix44 T1(float a, float b, float c) {
  auto m = Identity();

  m(0, 3) = -a;
  m(1, 3) = -b;
  m(2, 3) = -c;

  return m;
}

matrix44 T2(float theta) {
  auto m = Identity();

  m(0, 0) = -std::cos(theta);
  m(0, 2) = -std::sin(theta);
  m(2, 0) = std::sin(theta);
  m(2, 2) = -std::cos(theta);

  return m;
}

matrix44 T3(float phi) {
  auto m = Identity();

  m(1, 1) = std::sin(phi);
  m(1, 2) = -std::cos(phi);
  m(2, 1) = std::cos(phi);
  m(2, 2) = std::sin(phi);

  return m;
}

matrix44 T4(float alpha) {
  auto m = Identity();

  m(0, 0) = std::cos(alpha);
  m(1, 0) = std::sin(alpha);
  m(0, 1) = -std::sin(alpha);
  m(1, 1) = std::cos(alpha);

  return m;
}

matrix44 T5() {
  auto m = Identity();

  m(0, 0) = -1;

  return m;
}

struct Args {
  float a;
  float b;
  float c;
  float theta;
  float phi;
  float alpha;
};

matrix44 Tv(Args args) {
  return T1(args.a, args.b, args.c) * T2(args.theta) * T3(args.phi) *
         T4(args.alpha) * T5();
}

matrix14 Transform(matrix14 point, Args args) { return point * Tv(args); }

matrix14 Transform(matrix13 point, Args args) {
  return matrix14{point(0), point(1), point(2), 1} * Tv(args);
}

Point TransformTo2D(matrix14 point, float d) {
  return Point{point(1), point(2)};
}

const float length = 100;

matrix13 points[] = {
    {0, 0, 0},
    {length, 0, 0},
    {length, 0, 0},
    {length, length, 0},
    {length, length, 0},
    {0, length, 0},
    {0, length, 0},
    {0, 0, 0},
    {0, 0, 0},
    {0, 0, length},
    {length, 0, 0},
    {length, 0, length},
    {length, length, 0},
    {length, length, length},
    {0, length, 0},
    {0, length, length},
    {0, 0, length},
    {length, 0, length},
    {length, 0, length},
    {length, length, length},
    {length, length, length},
    {0, length, length},
    {0, length, length},
    {0, 0, length},
};

const float pi = static_cast<float>(dlib::pi);

Args args{30, 40, 50, pi / 3.f, pi / 4.f, pi / 5.f};

int main() {
  std::vector<Point> points2d;

  for (auto p : points) {
    auto point2d = TransformTo2D(Transform(std::move(p), args), length);
    points2d.push_back(point2d);
  }

  auto application = cru::platform::bootstrap::CreateUiApplication();
  auto window = application->CreateWindow();

  auto brush = application->GetGraphicsFactory()->CreateSolidColorBrush(
      cru::platform::colors::black);

  window->SetClientSize(cru::platform::Size(400, 400));

  window->PaintEvent()->AddHandler([window, &brush, points2d](nullptr_t) {
    auto painter = window->BeginPaint();
    painter->PushState();
    painter->ConcatTransform(cru::platform::Matrix::Translation(200, 200));
    for (int i = 0; i < points2d.size(); i += 2) {
      painter->DrawLine(points2d[i], points2d[i + 1], brush.get(), 1.f);
    }
    painter->PopState();
  });

  window->SetVisibility(cru::platform::gui::WindowVisibilityType::Show);

  return application->Run();
}


/* pcmsolver_copyright_start */
/*
 *     PCMSolver, an API for the Polarizable Continuum Model
 *     Copyright (C) 2013-2016 Roberto Di Remigio, Luca Frediani and contributors
 *
 *     This file is part of PCMSolver.
 *
 *     PCMSolver is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     PCMSolver is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 *
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with PCMSolver.  If not, see <http://www.gnu.org/licenses/>.
 *
 *     For information on the complete list of contributors to the
 *     PCMSolver API, see: <http://pcmsolver.readthedocs.io/>
 */
/* pcmsolver_copyright_end */

#ifndef IONICLIQUID_HPP
#define IONICLIQUID_HPP

#include <cmath>
#include <iosfwd>
#include <vector>

#include "Config.hpp"

#include <Eigen/Core>

class Element;

#include "DerivativeTypes.hpp"
#include "DerivativeUtils.hpp"
#include "bi_operators/IntegratorForward.hpp"
#include "GreensFunction.hpp"
#include "dielectric_profile/Yukawa.hpp"

/*! \file IonicLiquid.hpp
 *  \class IonicLiquid
 *  \brief Green's functions for ionic liquid, described by the linearized Poisson-Boltzmann equation.
 *  \author Luca Frediani, Roberto Di Remigio
 *  \date 2013-2015
 *  \tparam DerivativeTraits evaluation strategy for the function and its derivatives
 *  \tparam IntegratorPolicy policy for the calculation of the matrix represenation of S and D
 */

template <typename DerivativeTraits = AD_directional,
          typename IntegratorPolicy = CollocationIntegrator>
class IonicLiquid __final : public GreensFunction<DerivativeTraits, IntegratorPolicy, Yukawa,
                                     IonicLiquid<DerivativeTraits, IntegratorPolicy> >
{
public:
    IonicLiquid(double eps, double k) : GreensFunction<DerivativeTraits, IntegratorPolicy, Yukawa,
                                                  IonicLiquid<DerivativeTraits, IntegratorPolicy> >() { this->profile_ = Yukawa(eps, k); }
    IonicLiquid(double eps, double k, double f) : GreensFunction<DerivativeTraits, IntegratorPolicy, Yukawa,
                                                  IonicLiquid<DerivativeTraits, IntegratorPolicy> >(f) { this->profile_ = Yukawa(eps, k); }
    virtual ~IonicLiquid() {}

    /*! Calculates the matrix representation of the S operator
     *  \param[in] e list of finite elements
     */
    virtual Eigen::MatrixXd singleLayer(const std::vector<Element> & e) const __override
    {
        return this->integrator_.singleLayer(*this, e);
    }
    /*! Calculates the matrix representation of the D operator
     *  \param[in] e list of finite elements
     */
    virtual Eigen::MatrixXd doubleLayer(const std::vector<Element> & e) const __override
    {
        return this->integrator_.doubleLayer(*this, e);
    }

    friend std::ostream & operator<<(std::ostream & os, IonicLiquid & gf) {
        return gf.printObject(os);
    }
private:
    /*!
     *  Evaluates the Green's function given a pair of points
     *
     *  \param[in] sp the source point
     *  \param[in] pp the probe point
     */
    virtual DerivativeTraits operator()(DerivativeTraits * sp, DerivativeTraits * pp) const __override
    {
        double eps = this->profile_.epsilon;
	    double k = this->profile_.kappa;
        return (exp(-k * distance(sp, pp)) / (eps * distance(sp, pp)));
    }
    /*! Returns value of the directional derivative of the
     *  Greens's function for the pair of points p1, p2:
     *  \f$ \nabla_{\mathbf{p_2}}G(\mathbf{p}_1, \mathbf{p}_2)\cdot \mathbf{n}_{\mathbf{p}_2}\f$
     *  Notice that this method returns the directional derivative with respect
     *  to the probe point, thus assuming that the direction is relative to that point.
     *  \param[in] direction the direction
     *  \param[in]        p1 first point
     *  \param[in]        p2 second point
     */
    virtual double kernelD_impl(const Eigen::Vector3d & direction,
                              const Eigen::Vector3d & p1, const Eigen::Vector3d & p2) const __override
    {
        return this->profile_.epsilon * (this->derivativeProbe(direction, p1, p2));
    }
    virtual KernelS exportKernelS_impl() const __override {
      return pcm::bind(&IonicLiquid<DerivativeTraits, IntegratorPolicy>::kernelS, *this, pcm::_1, pcm::_2);
    }
    virtual KernelD exportKernelD_impl() const __override {
      return pcm::bind(&IonicLiquid<DerivativeTraits, IntegratorPolicy>::kernelD, *this, pcm::_1, pcm::_2, pcm::_3);
    }
    virtual std::ostream & printObject(std::ostream & os) __override
    {
        os << "Green's function type: ionic liquid" << std::endl;
        os << this->profile_;
        return os;
    }
};

#endif // IONICLIQUID_HPP


#include "MirrorPlasma.hpp"
#include "PlasmaPhysics.hpp"
#include "AtomicPhysics.hpp"

#include <cmath>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm>
#include <boost/math/quadrature/gauss_kronrod.hpp>

Species Electron{ .type = Species::Electron, .Charge = -1, .Mass = ElectronMass,  .Name = "Electron" };
Species Proton{ .type = Species::Ion, .Charge = 1, .Mass = ProtonMass, .Name = "Proton"};
Species Deuteron{ .type = Species::Ion, .Charge = 1, .Mass = 1.999*ProtonMass, .Name = "Deuteron" };
Species NeutralHydrogen{ .type = Species::Neutral, .Charge = 0, .Mass = ProtonMass + ElectronMass, .Name = "Neutral Hydrogen"};

// Cross Section objects for integration
CrossSection protonImpactIonization( protonImpactIonizationCrossSection, 200, 1e6, Proton, NeutralHydrogen );
CrossSection HydrogenChargeExchange( HydrogenChargeExchangeCrossSection, 0.1, 1e6, Proton, NeutralHydrogen );
CrossSection electronImpactIonization( electronImpactIonizationCrossSection, 13.6, 1e6, Electron, NeutralHydrogen );

double neutralsRateCoefficientHot( CrossSection const & sigma, MirrorPlasma const & plasma )
{
	// E and T in eV, sigma in cm^2
	// k=<σv> in m^3/s
	// Assumes a Maxwellian distribution, COM energy (as opposed to incident)

	double temperature;
	if ( sigma.Particle.Name == "Electron" ){
		temperature = plasma.ElectronTemperature * ReferenceTemperature; // Convert to Joules
	}
	else{
		temperature = plasma.IonTemperature * ReferenceTemperature; // Convert to Joules
	}

	double Jacobian = ElectronCharge; // The integral is over Energy, which is in units of electronvolts, so transform the integrand back to eV
	auto integrand = [&]( double Energy ) {
		double sigmaM2 = sigma( Energy ) * 1e-4; // sigma is in cm^2, we need m^2
		return Energy * ElectronCharge * sigmaM2 * ::exp( -Energy * ElectronCharge / temperature ) * Jacobian;
	};

	constexpr double tolerance = 1e-5;
	constexpr unsigned MaxDepth = 10;
	double HotRateCoeff = 4.0 / ( ::sqrt(2 * M_PI * sigma.ReducedMass * temperature) * temperature )
	         * boost::math::quadrature::gauss_kronrod<double, 255>::integrate( integrand, sigma.MinEnergy, sigma.MaxEnergy, MaxDepth, tolerance );

#if defined( DEBUG ) && defined( ATOMIC_PHYSICS_DEBUG )
	std::cerr << "Computing a hot rate coefficient at T = " << plasma.ElectronTemperature/1000 << " eV and M = " << plasma.MachNumber << " gave <sigma v> = " << HotRateCoeff  << std::endl;
#endif

	return HotRateCoeff;

}

double neutralsRateCoefficientCold( CrossSection const & sigma, MirrorPlasma const & plasma )
{
	// sigma in cm^2
	// k=<σv> in m^3/s
	// Assumes ions are Maxwellian and neutrals are stationary
	double temperature;
	if ( sigma.Particle.Name == "Electron" ){
		temperature = plasma.ElectronTemperature * ReferenceTemperature; // Convert to Joules
	}
	else{
		temperature = plasma.IonTemperature * ReferenceTemperature; // Convert to Joules
	}

	double thermalSpeed = ::sqrt( 2.0 * temperature / sigma.Particle.Mass );
	double thermalMachNumber = plasma.MachNumber * ::sqrt( ::abs( sigma.Particle.Charge ) * plasma.ElectronTemperature * ReferenceTemperature / ( 2 * temperature ) );

	double Jacobian = ElectronCharge / (sigma.ReducedMass * thermalSpeed * thermalSpeed); // The integral is over Energy, which is in units of electronvolts, so transform the integrand back to eV, including change of variables from du to dE (less one power of u, which cancels with one in the integrand
	auto integrand = [&]( double Energy ) {
		double velocity = ::sqrt( 2.0 * Energy * ElectronCharge / sigma.ReducedMass );
		double u = velocity / thermalSpeed;
		double sigmaM2 = sigma( Energy ) * 1e-4; // sigma is in cm^2, we need m^2
		return u * sigmaM2 * ( ::exp( -::pow( thermalMachNumber - u, 2 ) ) - ::exp( -::pow( thermalMachNumber + u, 2 ) ) ) * Jacobian;
	};

	constexpr double tolerance = 1e-5;
	constexpr unsigned MaxDepth = 10;
	double ColdRateCoeff = thermalSpeed / ( thermalMachNumber * ::sqrt(M_PI) )
	        * boost::math::quadrature::gauss_kronrod<double, 255>::integrate( integrand, sigma.MinEnergy, sigma.MaxEnergy, MaxDepth, tolerance );

#if defined( DEBUG ) && defined( ATOMIC_PHYSICS_DEBUG )
	std::cerr << "Computing a cold rate coefficient at T = " << plasma.ElectronTemperature*1000 << " eV and M = " << plasma.MachNumber << " gave <sigma v> = " << ColdRateCoeff  << std::endl;
#endif

	return ColdRateCoeff;
}

/*
double meanFreePath( double density, double crossSection )
{
	return 1.0 / ( density * crossSection );
}

bool isShortMeanFreePathRegime( double meanFreePath, double characteristicLength, double minRatio = 10.0 )
{
	return ( characteristicLength / meanFreePath ) >= minRatio;
}

bool isCoronalEquilibrium( double excitationRate, double deexcitationRate, double minRatio = 10.0 )
{
	return ( deexcitationRate / excitationRate ) >= minRatio;
}
*/

double reactionRate( double density_1, double density_2, double rateCoefficient, std::shared_ptr<MirrorPlasma> pMirrorPlasma )
{
	return density_1 * density_2 * rateCoefficient * pMirrorPlasma->pVacuumConfig->PlasmaVolume();
}

double EvaluateJanevCrossSectionFit ( std::vector<double> PolynomialCoefficients, double Energy )
{
	constexpr unsigned int N_JANEV_COEFFS = 9;
	if ( PolynomialCoefficients.size() != N_JANEV_COEFFS )
		throw std::invalid_argument( "Janev uses fixed order fits, there are not " + std::to_string( N_JANEV_COEFFS ) + " numbers. Something is wrong." );
	double sum = 0.0;
	for ( size_t n = 0; n < PolynomialCoefficients.size(); n++ ) {
      sum += PolynomialCoefficients.at( n ) * ::pow( ::log(Energy), n );
   }
	double sigma = ::exp(sum);
	return sigma;
}

/*
double evaluateJanevDFunction( double beta )
{
	// Function from Janev 1987 Appendix C
	// Used in some analytical fits for cross sections
	double DFunctionVal;
	if ( beta < 1e-3 ){
		DFunctionVal = 4 * beta * ::log( 1.4 / beta );
	} else if ( beta > 10 ) {
		DFunctionVal = beta / 2 * ::exp( - ::sqrt( 2 * beta ) );
	} else {
		// Create look up table and linearly interpolate to find DFunction
		std::vector<double> betaInverse = { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 6.0, 8.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 40.0, 60.0, 80.0, 100.0, 15.0, 200.0, 300.0, 400.0, 600.0, 800.0, 1000.0 };
		std::vector<double> DFunction = { 0.057, 0.104, 0.135, 0.157, 0.175, 0.194, 0.230, 0.264, 0.296, 0.328, 0.367, 0.388, 0.399, 0.405, 0.410, 0.405, 0.399, 0.380, 0.345, 0.318, 0.285, 0.263, 0.227, 0.205, 0.190, 0.168, 0.141, 0.124, 0.110, 0.092, 0.080, 0.054, 0.042, 0.035, 0.028 };
		for (size_t i = 0; i < betaInverse.size(); i++ ){
			if ( 1.0 / beta < betaInverse.at( i ) ){}
			else {
				double x1 = betaInverse.at( i - 1 );
				double x2 = betaInverse.at ( i );
				double y1 = DFunction.at( i - 1 );
				double y2 = DFunction.at ( i );
				DFunctionVal = y1 + ( y2 - y1 ) * ( 1.0 / beta - x1 ) / ( x2 - x1 );
			}
		}
	}
	return DFunctionVal;
}
*/

double electronImpactIonizationCrossSection( double CoMEnergy )
{
	// Minimum energy of cross section in eV
	constexpr double ionizationEnergy = 13.6;
	constexpr double minimumEnergySigma = ionizationEnergy;

	// Contribution from ground state
	// Janev 1993, ATOMIC AND PLASMA-MATERIAL INTERACTION DATA FOR FUSION, Volume 4
	// Equation 1.2.1
	// e + H(1s) --> e + H+ + e
	// Accuracy is 10% or better
	constexpr double fittingParamA = 0.18450;
	constexpr std::array<double,5> fittingParamB{ -0.032226, -0.034539, 1.4003, -2.8115, 2.2986 };

	double sigma;
	if ( CoMEnergy < minimumEnergySigma ) {
		sigma = 0;
	}
	else {
		double sum = 0.0;
		double x = 1.0 - ionizationEnergy / CoMEnergy;
		for ( size_t n = 0; n < fittingParamB.size(); n++ ) {
	      sum += fittingParamB.at( n ) * ::pow( x, n );
	   }
		sigma = 1.0e-13 / ( ionizationEnergy * CoMEnergy ) * ( fittingParamA * ::log( CoMEnergy / ionizationEnergy ) + sum );
	}
	return sigma;
}

// Energy in electron volts, returns cross section in cm^2
double protonImpactIonizationCrossSection( double Energy )
{
	// Minimum energy of cross section in keV
	const double minimumEnergySigma = 0.2;
	// Convert to keV
	double CoMEnergy = Energy / 1000;

	// Contribution from ground state
	// Janev 1993, ATOMIC AND PLASMA-MATERIAL INTERACTION DATA FOR FUSION, Volume 4
	// Equation 2.2.1
	// H+ + H(1s) --> H+ + H+ + e
	// Accuracy is 30% or better
	constexpr double A1 = 12.899;
	constexpr double A2 = 61.897;
	constexpr double A3 = 9.2731e3;
	constexpr double A4 = 4.9749e-4;
	constexpr double A5 = 3.9890e-2;
	constexpr double A6 = -1.5900;
	constexpr double A7 = 3.1834;
	constexpr double A8 = -3.7154;

	double sigma;
	if ( CoMEnergy < minimumEnergySigma ) {
		sigma = 0;
	}
	else {
		// Energy is in units of keV
		sigma = 1e-16 * A1 * ( ::exp( -A2 / CoMEnergy ) * ::log( 1 + A3 * CoMEnergy ) / CoMEnergy + A4 * ::exp( -A5 * CoMEnergy ) / ( ::pow( CoMEnergy, A6 ) + A7 * ::pow( CoMEnergy, A8 ) ) );
	}
	return sigma;
}

double HydrogenChargeExchangeCrossSection( double CoMEnergy )
{
	// Minimum energy of cross section in eV
	// const double minimumEnergySigma_1s = 0.1;
	const double minimumEnergySigma_2p = 19.0;
	const double minimumEnergySigma_2s = 0.1;

	// Contribution from ground -> ground state
	// Janev 1987 3.1.8
	// p + H(1s) --> H(1s) + p
	double sigma_1s;
	if ( CoMEnergy < minimumEnergySigma_2p ) {
		sigma_1s = 0;
	} else {
		sigma_1s = 0.6937e-14 * ::pow( 1 - 0.155 * ::log10( CoMEnergy ), 2 ) / (1 + 0.1112e-14 * ::pow( CoMEnergy, 3.3 ));
	}

	// Janev 1987 3.1.9
	// p + H(1s) --> H(2p) + p
	std::vector<double> aSigma_2p = {-2.197571949935e+01, -4.742502251260e+01, 3.628013140596e+01, -1.423003075866e+01, 3.273090240144e+00, -4.557928912260e-01, 3.773588347458e-02, -1.707904867106e-03, 3.251203344615e-05};
	// Janev 1987 3.1.10
	// p + H(1s) --> H(2s) + p
	std::vector<double> aSigma_2s = {-1.327325087764e+04, 1.317576614520e+04, -5.683932157858e+03, 1.386309780149e+03, -2.089794561307e+02, 1.992976245274e+01, -1.173800576157e+00, 3.902422810767e-02, -5.606240339932e-04};

	// Contribution from ground -> 2p orbital
	double sigma_2p;
	if ( CoMEnergy < minimumEnergySigma_2p ) {
		sigma_2p = 0;
	} else {
		sigma_2p = EvaluateJanevCrossSectionFit( aSigma_2p, CoMEnergy );
	}

	// Contribution from ground -> 2s orbital
	double sigma_2s;
	if ( CoMEnergy < minimumEnergySigma_2s ) {
		sigma_2s = 0;
	} else {
		sigma_2s = EvaluateJanevCrossSectionFit( aSigma_2s, CoMEnergy );
	}

	return sigma_1s + sigma_2p + sigma_2s;
}

// Energy in electron volts, returns cross section in cm^2
double radiativeRecombinationCrossSection( double Energy )
{
	// From https://iopscience-iop-org.proxy-um.researchport.umd.edu/article/10.1088/1402-4896/ab060a
	// Igor A Kotelnikov and Alexander I Milstein 2019 Phys. Scr. 94 055403
	// Equation 9
	// H+ + e --> H + hν

	int Z = 1;
	double IonizationEnergy = 13.59844; // eV
	double J_Z = ::pow( Z, 2 ) * IonizationEnergy;
	double eta = ::sqrt( J_Z / Energy );
	double sigma = ::pow( 2, 8 ) * ::pow( M_PI * BohrRadius, 2 ) / 3 * ::pow( eta, 6 ) * ::exp( - 4 * eta * atan( 1 / eta ) ) / ( ( 1 - ::exp( -2 * M_PI * eta ) ) * ::pow( ::pow( eta, 2 ) + 1, 2 ) ) * ::pow( FineStructureConstant, 3 );

	sigma *= 1e4; // convert to cm^2
	return sigma;
}

// double electronHydrogenExcitationN2CrossSection( double Te )
// {
// 	// Minimum energy of cross section in eV
// 	const double excitationEnergy = 10.2;
// 	const double cutoffEnergy1 = 11.56;
// 	const double cutoffEnergy2 = 12.23;
//
// 	// Contribution from ground state
// 	// Janev 1993, ATOMIC AND PLASMA-MATERIAL INTERACTION DATA FOR FUSION, Volume 4
// 	// Equation 1.1.3
// 	// e + H(1s) --> e + H*(n=2)
// 	// Accuracy is 10% or better
// 	std::vector<double> fittingParamA = { 1.4182, -20.877, 49.735, -46.249, 17.442, 4.4979 };
//
// 	double sigma;
// 	if ( Te < excitationEnergy ) {
// 		sigma = 0;
// 	}
// 	else if ( Te < cutoffEnergy1 ) {
// 		sigma = 1e-16 * ( 0.255 + 0.1865 * ( Te - excitationEnergy ) );
// 	}
// 	else if ( Te < cutoffEnergy2 ) {
// 		sigma = 5.025e-17;
// 	}
// 	else {
// 		double sum = 0.0;
// 		double XEnergy = Te / excitationEnergy;
// 		for ( size_t n = 0; n < fittingParamA.size() - 1; n++ ) {
// 	      sum += fittingParamA.at( n ) / ::pow( XEnergy, n - 1 );
// 	   }
// 		sigma = 5.984e-16 / Te * ( sum + fittingParamA.back() * ::log( XEnergy ) );
// 	}
// 	return sigma;
// }
//
// double protonHydrogenExcitationN2CrossSection( double Ti )
// {
// 	// Minimum energy of cross section in keV
// 	const double minimumEnergySigma = 0.6;
// 	double TiKEV = Ti / 1000;
//
// 	// Contribution from ground state
// 	// Janev 1993, ATOMIC AND PLASMA-MATERIAL INTERACTION DATA FOR FUSION, Volume 4
// 	// Equation 2.2.1
// 	// H+ + H(1s) --> H+ + H+ + e
// 	// Accuracy is 100% or better
// 	const double A1 = 34.433;
// 	const double A2 = 44.057;
// 	const double A3 = 0.56870;
// 	const double A4 = 8.5476;
// 	const double A5 = 7.8501;
// 	const double A6 = -9.2217;
// 	const double A7 = 1.8020e-2;
// 	const double A8 = 1.6931;
// 	const double A9 = 1.9422e-3;
// 	const double A10 = 2.9068;
//
// 	double sigma;
// 	if ( TiKEV < minimumEnergySigma ) {
// 		sigma = 0;
// 	}
// 	else {
// 		// Energy is in units of keV
// 		sigma = 1e-16 * A1 * ( ::exp( -A2 / TiKEV ) * ::log( 1 + A3 * TiKEV ) / TiKEV + A4 * ::exp( -A5 * TiKEV ) / ( ::pow( TiKEV, A6 ) ) + A7 * ::exp( -A8 / TiKEV ) / ( 1 + A9 * ::pow( TiKEV, A10 ) ) );
// 	}
// 	return sigma;
// }

/*
 * Use the above functions to set steady-state neutral density/source
 *
 */
void MirrorPlasma::ComputeSteadyStateNeutrals()
{
	// Do not recalculate if we're in fixed-neutral-density mode
	if ( FixedNeutralDensity )
		return;

	// Calculate the Ionization Rate of cold neutrals from proton and electron impact:
	double IonizationRate =
		neutralsRateCoefficientCold( protonImpactIonization, *this ) * IonDensity * ReferenceDensity +
		neutralsRateCoefficientCold( electronImpactIonization, *this ) * ElectronDensity * ReferenceDensity;

#if defined( DEBUG ) && defined( ATOMIC_PHYSICS_DEBUG )
	std::cerr << "Current Ionization Rate is " << IonizationRate << std::endl;
#endif

	// Assume mix of neutrals is such that the particle densities are maintained so we just need to produce enough electrons from the source gas to balance the losses
	double ElectronLossRate = ParallelElectronParticleLoss() + ClassicalElectronParticleLosses();
	// Steady State requires
	//		Losses = n_N * n_e * IonizationRateCoefficient * Volume
	// so n_N = (Losses/Volume) / ( n_e * IonizationRateCoefficient )
	//		    = (Losses/Volume) / IonizationRate
	NeutralDensity = ElectronLossRate / ( IonizationRate );
	// Normalize neutral density
	NeutralDensity = NeutralDensity / ReferenceDensity;
	NeutralSource = ElectronLossRate;
}

// Number of ions lost as neutrals per second per unit volume
// requires computed neutral density
double MirrorPlasma::CXLossRate() const
{
	if ( !pVacuumConfig->IncludeCXLosses )
		return 0.0;

	double CXRateCoefficient = neutralsRateCoefficientCold( HydrogenChargeExchange, *this );

#if defined( DEBUG ) && defined( ATOMIC_PHYSICS_DEBUG )
	std::cerr << "Current CX Loss Rate is " << CXRateCoefficient * ( NeutralDensity * ReferenceDensity * IonDensity * ReferenceDensity ) << " particles/s"<<std::endl;
#endif

	return CXRateCoefficient * ( NeutralDensity * ReferenceDensity * IonDensity * ReferenceDensity );
}


#include <opengv/optimization_tools/objective_function_tools/GlobalPnPFunctionInfo.hpp>
#include <Eigen/Dense>
#include <opengv/Indices.hpp>
#include <iostream>

GlobalPnPFunctionInfo::GlobalPnPFunctionInfo(const opengv::absolute_pose::AbsoluteAdapterBase & adapter){

  opengv::Indices indices(adapter.getNumberCorrespondences());
  int total_points = (int) indices.size();
  //std::cout << "Total points: " << total_points << std::endl;
  Mt =  Eigen::Matrix3d::Zero(3,3);
  Mrt = Eigen::MatrixXd::Zero(3,9);
  Mr =  Eigen::MatrixXd::Zero(9,9);
  vt =  Eigen::VectorXd::Zero(3,1);
  vr =  Eigen::VectorXd::Zero(9,1);
  //Eigen::MatrixXd Constant = Eigen::MatrixXd::Zero(1,1);

  //Variables defined for debugging
  /*Eigen::MatrixXd C_all = Eigen::MatrixXd::Zero(3, total_points);
  Eigen::MatrixXd V_all = Eigen::MatrixXd::Zero(3, total_points);
  Eigen::MatrixXd X_all = Eigen::MatrixXd::Zero(3, total_points);*/
  Eigen::Matrix3d id = Eigen::Matrix3d::Identity(3,3);

  Eigen::Matrix<double,3,1> vi;
  Eigen::Matrix<double,3,1> xi;
  Eigen::Matrix<double,3,1> ci;
  Eigen::Matrix3d Qi;
  Eigen::Matrix3d Vi;
  Eigen::MatrixXd Mr_i  = Eigen::MatrixXd::Zero(3,9);
  Eigen::MatrixXd Mrt_i = Eigen::MatrixXd::Zero(3,9);
  Eigen::Matrix3d I_V   = Eigen::MatrixXd::Zero(3,3);
  Eigen::MatrixXd vr_1  = Eigen::MatrixXd::Zero(1,3);
  Eigen::MatrixXd vr_2  = Eigen::MatrixXd::Zero(1,9);
  for( int i = 0; i < total_points; i++ )
  {
    vi = adapter.getCamRotation(indices[i]) * adapter.getBearingVector(indices[i]);
    xi = adapter.getPoint(indices[i]);
    ci = adapter.getCamOffset(indices[i]);
    /*C_all.block<3,1>(0,i) = ci;
    X_all.block<3,1>(0,i) = xi;
    V_all.block<3,1>(0,i) = vi;*/
    Vi = vi * vi.transpose() / (vi.transpose() * vi);
    Qi = (id - Vi).transpose() * (id - Vi);

    I_V = id - Vi;
    Mt = Mt + Qi;
    vt = vt - (2 * Qi * ci);
    //Constant = Constant + ci.transpose() * Qi * ci;

    //Calculate Mr
    Mr_i.block<3,3>(0,0) = xi(0,0) * I_V;
    Mr_i.block<3,3>(0,3) = xi(1,0) * I_V;
    Mr_i.block<3,3>(0,6) = xi(2,0) * I_V;

    Mr = Mr + Mr_i.transpose() * Mr_i;

    //Calculate Mrt
    Mrt_i.block<3,3>(0,0) = xi(0,0) * Qi;
    Mrt_i.block<3,3>(0,3) = xi(1,0) * Qi;
    Mrt_i.block<3,3>(0,6) = xi(2,0) * Qi;

    Mrt = Mrt + 2*Mrt_i;

    //Calculate vr
    vr_1 = ci.transpose() * Qi;
    vr_2.block<1,3>(0,0) = -2 * xi(0,0) * vr_1;
    vr_2.block<1,3>(0,3) = -2 * xi(1,0) * vr_1;
    vr_2.block<1,3>(0,6) = -2 * xi(2,0) * vr_1;
    vr = vr + vr_2.transpose();
  }
  /*std::cout << "Data:" << std::endl;
  std::cout << "ci" << std::endl << C_all << std::endl;
  std::cout << "xi" << std::endl << X_all << std::endl;
  std::cout << "vi" << std::endl << V_all << std::endl;

  std::cout << "Final results: " << std::endl;
  std::cout << "Mt: "  << std::endl << Mt << std::endl;
  std::cout << "Mrt: " << std::endl << Mrt << std::endl;
  std::cout << "Mr:"   << std::endl << Mr  << std::endl;
  std::cout << "vt:"   << std::endl << vt  << std::endl;
  std::cout << "vr:"   << std::endl << vr   << std::endl;
  std::cout << "Const: " << std::endl << Constant << std::endl;*/
}

GlobalPnPFunctionInfo::~GlobalPnPFunctionInfo(){};

double GlobalPnPFunctionInfo::objective_function_value(const opengv::rotation_t & rotation, const opengv::translation_t & translation){
  const double * p = &rotation(0);
  Map<const Matrix<double,1,9> > r(p, 1, 9);
  Eigen::MatrixXd e = (translation.transpose() * Mt * translation)
    + (translation.transpose() * Mrt *  r.transpose())
    + (vt.transpose() * translation)
    + (r * Mr * r.transpose() )
    + (vr.transpose() * r.transpose());
  return ( e(0,0));
}

opengv::rotation_t GlobalPnPFunctionInfo::rotation_gradient(const opengv::rotation_t & rotation, const opengv::translation_t & translation){
  const double * p = &rotation(0);
  Map<const Matrix<double,1,9> > r(p, 1, 9);
  Eigen::MatrixXd result = (2 * Mr * r.transpose()) + ( Mrt.transpose() * translation ) + vr;
  double * ptr = &result(0);
  Map<Matrix<double, 3,3> > m(ptr, 3, 3);
  return m;
}

opengv::translation_t GlobalPnPFunctionInfo::translation_gradient(const opengv::rotation_t & rotation, const opengv::translation_t & translation){
  const double * p = &rotation(0);
  Map<const Matrix<double,1,9> > r(p, 1, 9);
  return ( (2 * Mt * translation) + (  Mrt * r.transpose() ) + vt );
}


//Machine-generated by miind.py. Edit at your own risk.

#include <boost/timer/timer.hpp>
#include <GeomLib.hpp>
#include <TwoDLib.hpp>
#include <MPILib/include/MPINetworkCode.hpp>
#include <MPILib/include/RateAlgorithmCode.hpp>
#include <MPILib/include/SimulationRunParameter.hpp>
#include <MPILib/include/report/handler/RootReportHandler.hpp>
#include <MPILib/include/WilsonCowanAlgorithm.hpp>
#include <MPILib/include/PersistantAlgorithm.hpp>
#include <MPILib/include/DelayAlgorithmCode.hpp>
#include <MPILib/include/RateFunctorCode.hpp>
#include "Euler.hpp"

typedef MPILib::MPINetwork<MPILib::DelayedConnection, MPILib::utilities::CircularDistribution> Network;
	// defining variables
const double TIME_END = 10000.0;

vector<TwoDLib::Redistribution> RetrieveMappingFromXML(const std::string& type, pugi::xml_node root)
{
	Pred pred(type);
	pugi::xml_node rev_node = root.find_child(pred);

	if (rev_node.name() != std::string("Mapping") ||
	    rev_node.attribute("type").value() != type)
	  throw TwoDLib::TwoDLibException("Couldn't find mapping in model file");

	std::ostringstream ostrev;
	rev_node.print(ostrev);
	std::istringstream istrev(ostrev.str());
	vector<TwoDLib::Redistribution> vec_rev = TwoDLib::ReMapping(istrev);
	return vec_rev;
}

TwoDLib::Mesh RetrieveMeshFromXML(pugi::xml_node root)
{
        pugi::xml_node mesh_node = root.first_child();
	if (mesh_node.name() != std::string("Mesh") )
	  throw TwoDLib::TwoDLibException("Couldn't find mesh node in model file");
	std::ostringstream ostmesh;
	mesh_node.print(ostmesh);
	std::istringstream istmesh(ostmesh.str());
	TwoDLib::Mesh mesh(istmesh);
	return mesh;
}

MPILib::Rate RateFunction_1(MPILib::Time t){
	return 3.5;
}
MPILib::Rate RateFunction_2(MPILib::Time t){
	return t > 1500 ? 3.5 : 0;
}

int main(int argc, char *argv[]){
	Network network;
	boost::timer::auto_cpu_timer t;

#ifdef ENABLE_MPI
	// initialise the mpi environment this cannot be forwarded to a class
	boost::mpi::environment env(argc, argv);
#endif

	try {	// generating algorithms

	pugi::xml_document doc;
	pugi::xml_parse_result result = doc.load_file("rinzel.model");
	pugi::xml_node  root = doc.first_child();

	TwoDLib::Mesh mesh1 = RetrieveMeshFromXML(root);
	TwoDLib::Mesh mesh2 = mesh1;

	std::vector<TwoDLib::Redistribution> vec_rev1 = RetrieveMappingFromXML("Reversal",root);
	std::vector<TwoDLib::Redistribution> vec_rev2 = vec_rev1;
	std::vector<TwoDLib::Redistribution> vec_res1 = RetrieveMappingFromXML("Reset",root);
	std::vector<TwoDLib::Redistribution> vec_res2 = vec_res1;

	std::vector<TwoDLib::Mesh> vec_vec_mesh { mesh1, mesh2 };
	std::vector< std::vector<TwoDLib::Redistribution> > vec_vec_rev { vec_rev1, vec_rev2 };
	std::vector< std::vector<TwoDLib::Redistribution> > vec_vec_res { vec_res1, vec_res2 };

	TwoDLib::Ode2DSystemGroup group(vec_vec_mesh,vec_vec_rev, vec_vec_res);
	group.Initialize(0,0,0);
	group.Initialize(1,0,0);

	TwoDLib::TransitionMatrix mat1("rinzel_0.1_0_0_0_.mat");
        TwoDLib::TransitionMatrix mat2("rinzel_-0.1_0_0_0_.mat");

	TwoDLib::CSRMatrix csr01(mat2,group,0); // inhibitory so mat2
	TwoDLib::CSRMatrix csr02(mat1,group,0); // excitatory so mat1; mesh id 0 relates to the first mesh
	TwoDLib::CSRMatrix csr10(mat2,group,1); // inhibitory so mat2; mesh id 1 relates to the second mesh
	TwoDLib::CSRMatrix csr13(mat1,group,1); // excitatory so mat 1

	std::vector<TwoDLib::CSRMatrix> vecmat{csr01, csr02, csr10, csr13}; // the input rates corresponding to these matrices must be presented in the same order

	// Establish number of steps
	MPILib::Time t_step = mesh1.TimeStep();
	MPILib::Number n_steps = static_cast<MPILib::Number>(floor(TIME_END/t_step));
	std::cout << "Number of simulation steps: " << n_steps << std::endl;;

	// Placeholder for firing rates
	const MPILib::Number nr_populations = 4;
	std::vector<MPILib::Rate> vec_rates(nr_populations,0.0);
	std::vector<MPILib::Rate> vec_input(nr_populations,0.0);

	// create a vector for the derivative
	std::vector<double> dydt(group.Mass().size());

	// Euler step parameter
	TwoDLib::MasterParameter par(500);
	double h = 1./par._N_steps*mesh1.TimeStep();

	// generating connections
	DelayedConnection con_0_2_0(1.,0.1,0);
	DelayedConnection con_1_3_0(1.,0.1,0);
	DelayedConnection con_1_0_0(500.,-0.1,0);
	DelayedConnection con_0_1_0(500.,-0.1,0);

	for(MPILib::Index istep = 0; istep < n_steps; istep++){
	  MPILib::Time t = t_step*istep;
	  group.Evolve();
	  // retain Hugh's original numbering convention

	  vec_input[0] = con_0_1_0._number_of_connections*vec_rates[1];
	  vec_input[1] = con_0_2_0._number_of_connections*vec_rates[2];
	  vec_input[2] = con_1_0_0._number_of_connections*vec_rates[0];
	  vec_input[3] = con_1_3_0._number_of_connections*vec_rates[3];

	  for (MPILib::Index i_part = 0; i_part < par._N_steps; i_part++ ){
	    TwoDLib::ClearDerivative(dydt);
	    TwoDLib::CalculateDerivative(group,dydt,vecmat,vec_input);
	    TwoDLib::AddDerivative(group.Mass(),dydt,h);
	  }

	  group.RedistributeProbability();
	  group.RemapReversal();

	  vec_rates[0] = group.F()[0];
	  vec_rates[1] = group.F()[1];
	  vec_rates[2] = RateFunction_1(t);
	  vec_rates[3] = RateFunction_2(t);

	  if (istep%1 == 0) std::cout << mesh1.TimeStep()*istep << " " << group.F()[0] << " " << group.F()[1] << std::endl;
	}

	std::ofstream ofst1("dens1.dat");
	std::ofstream ofst2("dens2.dat");

	std::vector<std::ostream*> vec_stream{ &ofst1, &ofst2};
	group.Dump(vec_stream);

	} catch(std::exception& exc){
		std::cout << exc.what() << std::endl;
#ifdef ENABLE_MPI
	//Abort the MPI environment in the correct way :
	env.abort(1);
	#endif
	}

	std::cout << "Overall time spend\n";
	t.report();

	return 0;
}


// Copyright 2018-2019 The Simons Foundation, Inc. - All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef NETKET_SR_HPP
#define NETKET_SR_HPP

#include <complex>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

#include <Eigen/Dense>
#include <Eigen/IterativeLinearSolvers>

#include "Utils/parallel_utils.hpp"
#include "Utils/random_utils.hpp"
#include "common_types.hpp"
#include "matrix_replacement.hpp"

namespace netket {

// Generalized Stochastic Reconfiguration Updates
class SR {
  double sr_diag_shift_;
  bool use_iterative_;
  bool use_cholesky_;
  bool is_holomorphic_;

  Eigen::MatrixXd Sreal_;
  Eigen::MatrixXcd Scomplex_;

 public:
  SR() { setDefaultParameters(); }

  void ComputeUpdate(const Eigen::Ref<const Eigen::MatrixXcd> Oks,
                     const Eigen::Ref<const Eigen::VectorXcd> grad,
                     Eigen::Ref<Eigen::VectorXcd> deltaP) {
    double nsamp = Oks.rows();

    SumOnNodes(nsamp);
    auto npar = grad.size();
    if (is_holomorphic_) {
      if (!use_iterative_) {
        // Explicit construction of the S matrix
        Scomplex_.resize(npar, npar);
        Scomplex_ = (Oks.adjoint() * Oks);
        SumOnNodes(Scomplex_);
        Scomplex_ /= nsamp;

        // Adding diagonal shift
        Scomplex_ += Eigen::MatrixXd::Identity(npar, npar) * sr_diag_shift_;

        if (use_cholesky_ == false) {
          Eigen::FullPivHouseholderQR<Eigen::MatrixXcd> qr(npar, npar);
          qr.setThreshold(1.0e-6);
          qr.compute(Scomplex_);
          deltaP = qr.solve(grad);
        } else {
          Eigen::LLT<Eigen::MatrixXcd> llt(npar);
          llt.compute(Scomplex_);
          deltaP = llt.solve(grad);
        }
      } else {
        Eigen::ConjugateGradient<SrMatrixComplex, Eigen::Lower | Eigen::Upper,
                                 Eigen::IdentityPreconditioner>
            it_solver;
        // Eigen::GMRES<MatrixReplacement, Eigen::IdentityPreconditioner>
        // it_solver;
        it_solver.setTolerance(1.0e-3);
        SrMatrixComplex S;
        S.attachMatrix(Oks);
        S.setShift(sr_diag_shift_);
        S.setScale(1. / nsamp);

        it_solver.compute(S);
        deltaP = it_solver.solve(grad);
        MPI_Barrier(MPI_COMM_WORLD);
      }
    } else {
      if (!use_iterative_) {
        // Explicit construction of the S matrix
        Sreal_.resize(npar, npar);
        Sreal_ = (Oks.adjoint() * Oks).real();
        SumOnNodes(Sreal_);
        Sreal_ /= nsamp;

        // Adding diagonal shift
        Sreal_ += Eigen::MatrixXd::Identity(npar, npar) * sr_diag_shift_;

        if (use_cholesky_ == false) {
          Eigen::FullPivHouseholderQR<Eigen::MatrixXd> qr(npar, npar);
          qr.setThreshold(1.0e-6);
          qr.compute(Sreal_);
          deltaP.real() = qr.solve(grad.real());
        } else {
          Eigen::LLT<Eigen::MatrixXd> llt(npar);
          llt.compute(Sreal_);
          deltaP.real() = llt.solve(grad.real());
          deltaP.imag().setZero();
        }
      } else {
        Eigen::ConjugateGradient<SrMatrixReal, Eigen::Lower | Eigen::Upper,
                                 Eigen::IdentityPreconditioner>
            it_solver;
        // Eigen::GMRES<MatrixReplacement, Eigen::IdentityPreconditioner>
        // it_solver;
        it_solver.setTolerance(1.0e-3);
        SrMatrixReal S;
        S.attachMatrix(Oks);
        S.setShift(sr_diag_shift_);
        S.setScale(1. / nsamp);

        it_solver.compute(S);
        deltaP.real() = it_solver.solve(grad.real());
        deltaP.imag().setZero();
        MPI_Barrier(MPI_COMM_WORLD);
      }
    }
  }

  void setDefaultParameters() {
    sr_diag_shift_ = 0.01;
    use_iterative_ = false;
    use_cholesky_ = true;
    is_holomorphic_ = true;
  }

  void setParameters(double diagshift = 0.01, bool use_iterative = false,
                     bool use_cholesky = true, bool is_holomorphic = true) {
    sr_diag_shift_ = diagshift;
    use_iterative_ = use_iterative;
    use_cholesky_ = use_cholesky;
    is_holomorphic_ = is_holomorphic;

    InfoMessage() << "Using the Stochastic reconfiguration method" << std::endl;

    if (use_iterative_) {
      InfoMessage() << "With iterative solver" << std::endl;
    } else {
      if (use_cholesky_) {
        InfoMessage() << "Using Cholesky decomposition" << std::endl;
      }
    }
  }
};  // namespace netket

}  // namespace netket

#endif


// Copyright Nick Thompson, 2019
// Use, modification and distribution are subject to the
// Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt
// or copy at http://www.boost.org/LICENSE_1_0.txt)
#ifndef BOOST_MATH_QUADRATURE_DETAIL_OOURA_FOURIER_INTEGRALS_DETAIL_HPP
#define BOOST_MATH_QUADRATURE_DETAIL_OOURA_FOURIER_INTEGRALS_DETAIL_HPP
#include <utility> // for std::pair.
#include <mutex>
#include <atomic>
#include <vector>
#include <iostream>
#include <boost/math/special_functions/expm1.hpp>
#include <boost/math/special_functions/sin_pi.hpp>
#include <boost/math/special_functions/cos_pi.hpp>
#include <boost/math/constants/constants.hpp>

namespace boost { namespace math { namespace quadrature { namespace detail {

// Ooura and Mori, A robust double exponential formula for Fourier-type integrals,
// eta is the argument to the exponential in equation 3.3:
template<class Real>
std::pair<Real, Real> ooura_eta(Real x, Real alpha) {
    using std::expm1;
    using std::exp;
    using std::abs;
    Real expx = exp(x);
    Real eta_prime = 2 + alpha/expx + expx/4;
    Real eta;
    // This is the fast branch:
    if (abs(x) > 0.125) {
        eta = 2*x - alpha*(1/expx - 1) + (expx - 1)/4;
    }
    else {// this is the slow branch using expm1 for small x:
        eta = 2*x - alpha*expm1(-x) + expm1(x)/4;
    }
    return {eta, eta_prime};
}

// Ooura and Mori, A robust double exponential formula for Fourier-type integrals,
// equation 3.6:
template<class Real>
Real calculate_ooura_alpha(Real h)
{
    using boost::math::constants::pi;
    using std::log1p;
    using std::sqrt;
    Real x = sqrt(16 + 4*log1p(pi<Real>()/h)/h);
    return 1/x;
}

template<class Real>
std::pair<Real, Real> ooura_sin_node_and_weight(long n, Real h, Real alpha)
{
    using std::expm1;
    using std::exp;
    using std::abs;
    using boost::math::constants::pi;
    using std::isnan;

    if (n == 0) {
        // Equation 44 of https://arxiv.org/pdf/0911.4796.pdf
        // Fourier Transform of the Stretched Exponential Function: Analytic Error Bounds,
        // Double Exponential Transform, and Open-Source Implementation,
        // Joachim Wuttke,
        // The C library libkww provides functions to compute the Kohlrausch-Williams-Watts function,
        // the Laplace-Fourier transform of the stretched (or compressed) exponential function exp(-t^beta)
        // for exponent beta between 0.1 and 1.9 with sixteen decimal digits accuracy.

        Real eta_prime_0 = Real(2) + alpha + Real(1)/Real(4);
        Real node = pi<Real>()/(eta_prime_0*h);
        Real weight = pi<Real>()*boost::math::sin_pi(1/(eta_prime_0*h));
        Real eta_dbl_prime = -alpha + Real(1)/Real(4);
        Real phi_prime_0 = (1 - eta_dbl_prime/(eta_prime_0*eta_prime_0))/2;
        weight *= phi_prime_0;
        return {node, weight};
    }
    Real x = n*h;
    auto p = ooura_eta(x, alpha);
    auto eta = p.first;
    auto eta_prime = p.second;

    Real expm1_meta = expm1(-eta);
    Real exp_meta = exp(-eta);
    Real node = -n*pi<Real>()/expm1_meta;

    // I have verified that this is not a significant source of inaccuracy in the weight computation:
    Real phi_prime = -(expm1_meta + x*exp_meta*eta_prime)/(expm1_meta*expm1_meta);

    // The main source of inaccuracy is in computation of sin_pi.
    // But I've agonized over this, and I think it's as good as it can get:
    Real s = pi<Real>();
    Real arg;
    if(eta > 1) {
        arg = n/( 1/exp_meta - 1 );
        s *= boost::math::sin_pi(arg);
        if (n&1) {
            s *= -1;
        }
    }
    else if (eta < -1) {
        arg = n/(1-exp_meta);
        s *= boost::math::sin_pi(arg);
    }
    else {
        arg = -n*exp_meta/expm1_meta;
        s *= boost::math::sin_pi(arg);
        if (n&1) {
            s *= -1;
        }
    }

    Real weight = s*phi_prime;
    return {node, weight};
}

#ifdef BOOST_MATH_INSTRUMENT_OOURA
template<class Real>
void print_ooura_estimate(size_t i, Real I0, Real I1, Real omega) {
    using std::abs;
    std::cout << std::defaultfloat
              << std::setprecision(std::numeric_limits<Real>::digits10)
              << std::fixed;
    std::cout << "h = " << Real(1)/Real(1<<i) << ", I_h = " << I0/omega
              << " = " << std::hexfloat << I0/omega << ", absolute error estimate = "
              << std::defaultfloat << std::scientific << abs(I0-I1)  << std::endl;
}
#endif

template<class Real>
std::pair<Real, Real> ooura_cos_node_and_weight(long n, Real h, Real alpha)
{
    using std::expm1;
    using std::exp;
    using std::abs;
    using boost::math::constants::pi;

    Real x = h*(n-Real(1)/Real(2));
    auto p = ooura_eta(x, alpha);
    auto eta = p.first;
    auto eta_prime = p.second;
    Real expm1_meta = expm1(-eta);
    Real exp_meta = exp(-eta);
    Real node = pi<Real>()*(Real(1)/Real(2)-n)/expm1_meta;

    Real phi_prime = -(expm1_meta + x*exp_meta*eta_prime)/(expm1_meta*expm1_meta);

    // Takuya Ooura and Masatake Mori,
    // Journal of Computational and Applied Mathematics, 112 (1999) 229-241.
    // A robust double exponential formula for Fourier-type integrals.
    // Equation 4.6
    Real s = pi<Real>();
    Real arg;
    if (eta < -1) {
        arg = -(n-Real(1)/Real(2))/expm1_meta;
        s *= boost::math::cos_pi(arg);
    }
    else {
        arg = -(n-Real(1)/Real(2))*exp_meta/expm1_meta;
        s *= boost::math::sin_pi(arg);
        if (n&1) {
            s *= -1;
        }
    }

    Real weight = s*phi_prime;
    return {node, weight};
}

template<class Real>
class ooura_fourier_sin_detail {
public:
    ooura_fourier_sin_detail(const Real relative_error_goal, size_t levels) {
#ifdef BOOST_MATH_INSTRUMENT_OOURA
      std::cout << "ooura_fourier_sin with relative error goal " << relative_error_goal
        << " & " << levels << " levels." << std::endl;
#endif // BOOST_MATH_INSTRUMENT_OOURA
        if (relative_error_goal < std::numeric_limits<Real>::epsilon() * 2) {
            throw std::domain_error("The relative error goal cannot be smaller than the unit roundoff.");
        }
        using std::abs;
        requested_levels_ = levels;
        starting_level_ = 0;
        rel_err_goal_ = relative_error_goal;
        big_nodes_.reserve(levels);
        bweights_.reserve(levels);
        little_nodes_.reserve(levels);
        lweights_.reserve(levels);

        for (size_t i = 0; i < levels; ++i) {
            if (std::is_same<Real, float>::value) {
                add_level<double>(i);
            }
            else if (std::is_same<Real, double>::value) {
                add_level<long double>(i);
            }
            else {
                add_level<Real>(i);
            }
        }
    }

    std::vector<std::vector<Real>> const & big_nodes() const {
        return big_nodes_;
    }

    std::vector<std::vector<Real>> const & weights_for_big_nodes() const {
        return bweights_;
    }

    std::vector<std::vector<Real>> const & little_nodes() const {
        return little_nodes_;
    }

    std::vector<std::vector<Real>> const & weights_for_little_nodes() const {
        return lweights_;
    }

    template<class F>
    std::pair<Real,Real> integrate(F const & f, Real omega) {
        using std::abs;
        using std::max;
        using boost::math::constants::pi;

        if (omega == 0) {
            return {Real(0), Real(0)};
        }
        if (omega < 0) {
            auto p = this->integrate(f, -omega);
            return {-p.first, p.second};
        }

        Real I1 = std::numeric_limits<Real>::quiet_NaN();
        Real relative_error_estimate = std::numeric_limits<Real>::quiet_NaN();
        // As we compute integrals, we learn about their structure.
        // Assuming we compute f(t)sin(wt) for many different omega, this gives some
        // a posteriori ability to choose a refinement level that is roughly appropriate.
        size_t i = starting_level_;
        do {
            Real I0 = estimate_integral(f, omega, i);
#ifdef BOOST_MATH_INSTRUMENT_OOURA
            print_ooura_estimate(i, I0, I1, omega);
#endif
            Real absolute_error_estimate = abs(I0-I1);
            Real scale = (max)(abs(I0), abs(I1));
            if (!isnan(I1) && absolute_error_estimate <= rel_err_goal_*scale) {
                starting_level_ = (max)(long(i) - 1, long(0));
                return {I0/omega, absolute_error_estimate/scale};
            }
            I1 = I0;
        } while(++i < big_nodes_.size());

        // We've used up all our precomputed levels.
        // Now we need to add more.
        // It might seems reasonable to just keep adding levels indefinitely, if that's what the user wants.
        // But in fact the nodes and weights just merge into each other and the error gets worse after a certain number.
        // This value for max_additional_levels was chosen by observation of a slowly converging oscillatory integral:
        // f(x) := cos(7cos(x))sin(x)/x
        size_t max_additional_levels = 4;
        while (big_nodes_.size() < requested_levels_ + max_additional_levels) {
            size_t ii = big_nodes_.size();
            if (std::is_same<Real, float>::value) {
                add_level<double>(ii);
            }
            else if (std::is_same<Real, double>::value) {
                add_level<long double>(ii);
            }
            else {
                add_level<Real>(ii);
            }
            Real I0 = estimate_integral(f, omega, ii);
            Real absolute_error_estimate = abs(I0-I1);
            Real scale = (max)(abs(I0), abs(I1));
#ifdef BOOST_MATH_INSTRUMENT_OOURA
            print_ooura_estimate(ii, I0, I1, omega);
#endif
            if (absolute_error_estimate <= rel_err_goal_*scale) {
                starting_level_ = (max)(long(ii) - 1, long(0));
                return {I0/omega, absolute_error_estimate/scale};
            }
            I1 = I0;
            ++ii;
        }

        starting_level_ = static_cast<long>(big_nodes_.size() - 2);
        return {I1/omega, relative_error_estimate};
    }

private:

    template<class PreciseReal>
    void add_level(size_t i) {
        using std::abs;
        size_t current_num_levels = big_nodes_.size();
        Real unit_roundoff = std::numeric_limits<Real>::epsilon()/2;
        // h0 = 1. Then all further levels have h_i = 1/2^i.
        // Since the nodes don't nest, we could conceivably divide h by (say) 1.5, or 3.
        // It's not clear how much benefit (or loss) would be obtained from this.
        PreciseReal h = PreciseReal(1)/PreciseReal(1<<i);

        std::vector<Real> bnode_row;
        std::vector<Real> bweight_row;

        // This is a pretty good estimate for how many elements will be placed in the vector:
        bnode_row.reserve((static_cast<size_t>(1)<<i)*sizeof(Real));
        bweight_row.reserve((static_cast<size_t>(1)<<i)*sizeof(Real));

        std::vector<Real> lnode_row;
        std::vector<Real> lweight_row;

        lnode_row.reserve((static_cast<size_t>(1)<<i)*sizeof(Real));
        lweight_row.reserve((static_cast<size_t>(1)<<i)*sizeof(Real));

        Real max_weight = 1;
        auto alpha = calculate_ooura_alpha(h);
        long n = 0;
        Real w;
        do {
            auto precise_nw = ooura_sin_node_and_weight(n, h, alpha);
            Real node = static_cast<Real>(precise_nw.first);
            Real weight = static_cast<Real>(precise_nw.second);
            w = weight;
            if (bnode_row.size() == bnode_row.capacity()) {
                bnode_row.reserve(2*bnode_row.size());
                bweight_row.reserve(2*bnode_row.size());
            }

            bnode_row.push_back(node);
            bweight_row.push_back(weight);
            if (abs(weight) > max_weight) {
                max_weight = abs(weight);
            }
            ++n;
            // f(t)->0 as t->infty, which is why the weights are computed up to the unit roundoff.
        } while(abs(w) > unit_roundoff*max_weight);

        // This class tends to consume a lot of memory; shrink the vectors back down to size:
        bnode_row.shrink_to_fit();
        bweight_row.shrink_to_fit();
        // Why we are splitting the nodes into regimes where t_n >> 1 and t_n << 1?
        // It will create the opportunity to sensibly truncate the quadrature sum to significant terms.
        n = -1;
        do {
            auto precise_nw = ooura_sin_node_and_weight(n, h, alpha);
            Real node = static_cast<Real>(precise_nw.first);
            if (node <= 0) {
                break;
            }
            Real weight = static_cast<Real>(precise_nw.second);
            w = weight;
            using std::isnan;
            if (isnan(node)) {
                // This occurs at n = -11 in quad precision:
                break;
            }
            if (lnode_row.size() > 0) {
                if (lnode_row[lnode_row.size()-1] == node) {
                    // The nodes have fused into each other:
                    break;
                }
            }
            if (lnode_row.size() == lnode_row.capacity()) {
                lnode_row.reserve(2*lnode_row.size());
                lweight_row.reserve(2*lnode_row.size());
            }
            lnode_row.push_back(node);
            lweight_row.push_back(weight);
            if (abs(weight) > max_weight) {
                max_weight = abs(weight);
            }
            --n;
            // f(t)->infty is possible as t->0, hence compute up to the min.
        } while(abs(w) > (std::numeric_limits<Real>::min)()*max_weight);

        lnode_row.shrink_to_fit();
        lweight_row.shrink_to_fit();

        // std::scoped_lock once C++17 is more common?
        std::lock_guard<std::mutex> lock(node_weight_mutex_);
        // Another thread might have already finished this calculation and appended it to the nodes/weights:
        if (current_num_levels == big_nodes_.size()) {
            big_nodes_.push_back(bnode_row);
            bweights_.push_back(bweight_row);

            little_nodes_.push_back(lnode_row);
            lweights_.push_back(lweight_row);
        }
    }

    template<class F>
    Real estimate_integral(F const & f, Real omega, size_t i) {
        // Because so few function evaluations are required to get high accuracy on the integrals in the tests,
        // Kahan summation doesn't really help.
        //auto cond = boost::math::tools::summation_condition_number<Real, true>(0);
        Real I0 = 0;
        auto const & b_nodes = big_nodes_[i];
        auto const & b_weights = bweights_[i];
        // Will benchmark if this is helpful:
        Real inv_omega = 1/omega;
        for(size_t j = 0 ; j < b_nodes.size(); ++j) {
            I0 += f(b_nodes[j]*inv_omega)*b_weights[j];
        }

        auto const & l_nodes = little_nodes_[i];
        auto const & l_weights = lweights_[i];
        // If f decays rapidly as |t|->infty, not all of these calls are necessary.
        for (size_t j = 0; j < l_nodes.size(); ++j) {
            I0 += f(l_nodes[j]*inv_omega)*l_weights[j];
        }
        return I0;
    }

    std::mutex node_weight_mutex_;
    // Nodes for n >= 0, giving t_n = pi*phi(nh)/h. Generally t_n >> 1.
    std::vector<std::vector<Real>> big_nodes_;
    // The term bweights_ will indicate that these are weights corresponding
    // to the big nodes:
    std::vector<std::vector<Real>> bweights_;

    // Nodes for n < 0: Generally t_n << 1, and an invariant is that t_n > 0.
    std::vector<std::vector<Real>> little_nodes_;
    std::vector<std::vector<Real>> lweights_;
    Real rel_err_goal_;
    std::atomic<long> starting_level_;
    size_t requested_levels_;
};

template<class Real>
class ooura_fourier_cos_detail {
public:
    ooura_fourier_cos_detail(const Real relative_error_goal, size_t levels) {
#ifdef BOOST_MATH_INSTRUMENT_OOURA
      std::cout << "ooura_fourier_cos with relative error goal " << relative_error_goal
        << " & " << levels << " levels." << std::endl;
      std::cout << "epsilon for type = " << std::numeric_limits<Real>::epsilon() << std::endl;
#endif // BOOST_MATH_INSTRUMENT_OOURA
        if (relative_error_goal < std::numeric_limits<Real>::epsilon() * 2) {
            throw std::domain_error("The relative error goal cannot be smaller than the unit roundoff!");
        }

        using std::abs;
        requested_levels_ = levels;
        starting_level_ = 0;
        rel_err_goal_ = relative_error_goal;
        big_nodes_.reserve(levels);
        bweights_.reserve(levels);
        little_nodes_.reserve(levels);
        lweights_.reserve(levels);

        for (size_t i = 0; i < levels; ++i) {
            if (std::is_same<Real, float>::value) {
                add_level<double>(i);
            }
            else if (std::is_same<Real, double>::value) {
                add_level<long double>(i);
            }
            else {
                add_level<Real>(i);
            }
        }

    }

    template<class F>
    std::pair<Real,Real> integrate(F const & f, Real omega) {
        using std::abs;
        using std::max;
        using boost::math::constants::pi;

        if (omega == 0) {
            throw std::domain_error("At omega = 0, the integral is not oscillatory. The user must choose an appropriate method for this case.\n");
        }

        if (omega < 0) {
            return this->integrate(f, -omega);
        }

        Real I1 = std::numeric_limits<Real>::quiet_NaN();
        Real absolute_error_estimate = std::numeric_limits<Real>::quiet_NaN();
        Real scale = std::numeric_limits<Real>::quiet_NaN();
        size_t i = starting_level_;
        do {
            Real I0 = estimate_integral(f, omega, i);
#ifdef BOOST_MATH_INSTRUMENT_OOURA
            print_ooura_estimate(i, I0, I1, omega);
#endif
            absolute_error_estimate = abs(I0-I1);
            scale = (max)(abs(I0), abs(I1));
            if (!isnan(I1) && absolute_error_estimate <= rel_err_goal_*scale) {
                starting_level_ = (max)(long(i) - 1, long(0));
                return {I0/omega, absolute_error_estimate/scale};
            }
            I1 = I0;
        } while(++i < big_nodes_.size());

        size_t max_additional_levels = 4;
        while (big_nodes_.size() < requested_levels_ + max_additional_levels) {
            size_t ii = big_nodes_.size();
            if (std::is_same<Real, float>::value) {
                add_level<double>(ii);
            }
            else if (std::is_same<Real, double>::value) {
                add_level<long double>(ii);
            }
            else {
                add_level<Real>(ii);
            }
            Real I0 = estimate_integral(f, omega, ii);
#ifdef BOOST_MATH_INSTRUMENT_OOURA
            print_ooura_estimate(ii, I0, I1, omega);
#endif
            absolute_error_estimate = abs(I0-I1);
            scale = (max)(abs(I0), abs(I1));
            if (absolute_error_estimate <= rel_err_goal_*scale) {
                starting_level_ = (max)(long(ii) - 1, long(0));
                return {I0/omega, absolute_error_estimate/scale};
            }
            I1 = I0;
            ++ii;
        }

        starting_level_ = static_cast<long>(big_nodes_.size() - 2);
        return {I1/omega, absolute_error_estimate/scale};
    }

private:

    template<class PreciseReal>
    void add_level(size_t i) {
        using std::abs;
        size_t current_num_levels = big_nodes_.size();
        Real unit_roundoff = std::numeric_limits<Real>::epsilon()/2;
        PreciseReal h = PreciseReal(1)/PreciseReal(1<<i);

        std::vector<Real> bnode_row;
        std::vector<Real> bweight_row;
        bnode_row.reserve((static_cast<size_t>(1)<<i)*sizeof(Real));
        bweight_row.reserve((static_cast<size_t>(1)<<i)*sizeof(Real));

        std::vector<Real> lnode_row;
        std::vector<Real> lweight_row;

        lnode_row.reserve((static_cast<size_t>(1)<<i)*sizeof(Real));
        lweight_row.reserve((static_cast<size_t>(1)<<i)*sizeof(Real));

        Real max_weight = 1;
        auto alpha = calculate_ooura_alpha(h);
        long n = 0;
        Real w;
        do {
            auto precise_nw = ooura_cos_node_and_weight(n, h, alpha);
            Real node = static_cast<Real>(precise_nw.first);
            Real weight = static_cast<Real>(precise_nw.second);
            w = weight;
            if (bnode_row.size() == bnode_row.capacity()) {
                bnode_row.reserve(2*bnode_row.size());
                bweight_row.reserve(2*bnode_row.size());
            }

            bnode_row.push_back(node);
            bweight_row.push_back(weight);
            if (abs(weight) > max_weight) {
                max_weight = abs(weight);
            }
            ++n;
            // f(t)->0 as t->infty, which is why the weights are computed up to the unit roundoff.
        } while(abs(w) > unit_roundoff*max_weight);

        bnode_row.shrink_to_fit();
        bweight_row.shrink_to_fit();
        n = -1;
        do {
            auto precise_nw = ooura_cos_node_and_weight(n, h, alpha);
            Real node = static_cast<Real>(precise_nw.first);
            // The function cannot be singular at zero,
            // so zero is not a unreasonable node,
            // unlike in the case of the Fourier Sine.
            // Hence only break if the node is negative.
            if (node < 0) {
                break;
            }
            Real weight = static_cast<Real>(precise_nw.second);
            w = weight;
            if (lnode_row.size() > 0) {
                if (lnode_row.back() == node) {
                    // The nodes have fused into each other:
                    break;
                }
            }
            if (lnode_row.size() == lnode_row.capacity()) {
                lnode_row.reserve(2*lnode_row.size());
                lweight_row.reserve(2*lnode_row.size());
            }

            lnode_row.push_back(node);
            lweight_row.push_back(weight);
            if (abs(weight) > max_weight) {
                max_weight = abs(weight);
            }
            --n;
        } while(abs(w) > (std::numeric_limits<Real>::min)()*max_weight);

        lnode_row.shrink_to_fit();
        lweight_row.shrink_to_fit();

        std::lock_guard<std::mutex> lock(node_weight_mutex_);
        // Another thread might have already finished this calculation and appended it to the nodes/weights:
        if (current_num_levels == big_nodes_.size()) {
            big_nodes_.push_back(bnode_row);
            bweights_.push_back(bweight_row);

            little_nodes_.push_back(lnode_row);
            lweights_.push_back(lweight_row);
        }
    }

    template<class F>
    Real estimate_integral(F const & f, Real omega, size_t i) {
        Real I0 = 0;
        auto const & b_nodes = big_nodes_[i];
        auto const & b_weights = bweights_[i];
        Real inv_omega = 1/omega;
        for(size_t j = 0 ; j < b_nodes.size(); ++j) {
            I0 += f(b_nodes[j]*inv_omega)*b_weights[j];
        }

        auto const & l_nodes = little_nodes_[i];
        auto const & l_weights = lweights_[i];
        for (size_t j = 0; j < l_nodes.size(); ++j) {
            I0 += f(l_nodes[j]*inv_omega)*l_weights[j];
        }
        return I0;
    }

    std::mutex node_weight_mutex_;
    std::vector<std::vector<Real>> big_nodes_;
    std::vector<std::vector<Real>> bweights_;

    std::vector<std::vector<Real>> little_nodes_;
    std::vector<std::vector<Real>> lweights_;
    Real rel_err_goal_;
    std::atomic<long> starting_level_;
    size_t requested_levels_;
};

}}}}
#endif