/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/*
 Copyright (C) 2018 Klaus Spanderen

 This file is part of QuantLib, a free-software/open-source library
 for financial quantitative analysts and developers - http://quantlib.org/

 QuantLib is free software: you can redistribute it and/or modify it
 under the terms of the QuantLib license.  You should have received a
 copy of the license along with this program; if not, please email
 <quantlib-dev@lists.sf.net>. The license is also available online at
 <https://www.quantlib.org/license.shtml>.

 This program 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 license for more details.
*/

#include "toplevelfixture.hpp"
#include "utilities.hpp"
#include <ql/math/comparison.hpp>
#include <ql/math/integrals/gausslobattointegral.hpp>
#include <ql/math/interpolations/bicubicsplineinterpolation.hpp>
#include <ql/math/matrixutilities/bicgstab.hpp>
#include <ql/math/optimization/levenbergmarquardt.hpp>
#include <ql/math/richardsonextrapolation.hpp>
#include <ql/methods/finitedifferences/meshers/concentrating1dmesher.hpp>
#include <ql/methods/finitedifferences/meshers/fdmhestonvariancemesher.hpp>
#include <ql/methods/finitedifferences/meshers/fdmmeshercomposite.hpp>
#include <ql/methods/finitedifferences/meshers/predefined1dmesher.hpp>
#include <ql/methods/finitedifferences/meshers/uniform1dmesher.hpp>
#include <ql/methods/finitedifferences/operators/fdmlinearopcomposite.hpp>
#include <ql/methods/finitedifferences/operators/fdmlinearoplayout.hpp>
#include <ql/methods/finitedifferences/operators/firstderivativeop.hpp>
#include <ql/methods/finitedifferences/operators/nthorderderivativeop.hpp>
#include <ql/methods/finitedifferences/operators/secondderivativeop.hpp>
#include <ql/methods/finitedifferences/operators/secondordermixedderivativeop.hpp>
#include <ql/methods/finitedifferences/solvers/fdm2dimsolver.hpp>
#include <ql/methods/finitedifferences/solvers/fdmbackwardsolver.hpp>
#include <ql/methods/finitedifferences/utilities/fdminnervaluecalculator.hpp>
#include <ql/pricingengines/vanilla/analytichestonengine.hpp>
#include <ql/quotes/simplequote.hpp>
#include <boost/numeric/ublas/banded.hpp>
#include <boost/numeric/ublas/matrix_proxy.hpp>
#include <boost/numeric/ublas/operation_sparse.hpp>
#include <numeric>
#include <utility>

using namespace QuantLib;
using namespace boost::unit_test_framework;

BOOST_FIXTURE_TEST_SUITE(QuantLibTests, TopLevelFixture)

BOOST_AUTO_TEST_SUITE(NthOrderDerivativeOpTests)

BOOST_AUTO_TEST_CASE(testSparseMatrixApply) {
    BOOST_TEST_MESSAGE("Testing sparse matrix apply...");

    SparseMatrix sm(5,7);

    BOOST_CHECK(sm.size1() == SparseMatrix::size_type(5));
    BOOST_CHECK(sm.size2() == SparseMatrix::size_type(7));

    sm(1,3) = 3.0;

    const Array x(7,0.0,1.0);
    const Array y = prod(sm, x);

    BOOST_CHECK(close_enough(y[0], 0.0));
    BOOST_CHECK(close_enough(y[1], 3.0*3.0));
    BOOST_CHECK(close_enough(y[2], 0.0));
    BOOST_CHECK(close_enough(y[3], 0.0));
    BOOST_CHECK(close_enough(y[4], 0.0));
}

BOOST_AUTO_TEST_CASE(testFirstOrder2PointsApply) {
    BOOST_TEST_MESSAGE("Testing two points first order "
            "derivative operator apply on an uniform grid...");

    const Real dx = 1/5.0;

    const NthOrderDerivativeOp op(0, 1, 3,
        ext::make_shared<FdmMesherComposite>(
            ext::make_shared<Uniform1dMesher>(0.0, 1.0, 6)));

    const Array x(6,0.0, 1.0);
    const Array y = op.apply(x);

    for (Size i=0; i < x.size(); ++i)
        BOOST_CHECK(close_enough(y[i], 1/dx));
}

BOOST_AUTO_TEST_CASE(testFirstOrder3PointsOnUniformGrid) {
    BOOST_TEST_MESSAGE("Testing three points first order "
            "derivative operator on an uniform grid...");

    const Real ddx = 1.0/0.2;

    const SparseMatrix m =
        NthOrderDerivativeOp(0, 1, 3,
            ext::make_shared<FdmMesherComposite>(
                ext::make_shared<Uniform1dMesher>(0.0, 1.0, 6))).toMatrix();

    // to reproduce the reference results use
    // http://web.media.mit.edu/~crtaylor/calculator.html

    BOOST_CHECK(close_enough(m(2,0), 0.0));
    BOOST_CHECK(close_enough(m(2,1), -0.5*ddx));
    BOOST_CHECK(m(2,2) < 42*QL_EPSILON);
    BOOST_CHECK(close_enough(m(2,3), 0.5*ddx));
    BOOST_CHECK(close_enough(m(2,4), 0.0));
    BOOST_CHECK(close_enough(m(2,5), 0.0));

    BOOST_CHECK(close_enough(m(0,0), -3.0/2.0*ddx));
    BOOST_CHECK(close_enough(m(0,1), 2.0*ddx));
    BOOST_CHECK(close_enough(m(0,2), -0.5*ddx));
    BOOST_CHECK(close_enough(m(0,3), 0.0));
    BOOST_CHECK(close_enough(m(0,4), 0.0));
    BOOST_CHECK(close_enough(m(0,5), 0.0));

    BOOST_CHECK(close_enough(m(5,0), 0.0));
    BOOST_CHECK(close_enough(m(5,1), 0.0));
    BOOST_CHECK(close_enough(m(5,2), 0.0));
    BOOST_CHECK(close_enough(m(5,3), 0.5*ddx));
    BOOST_CHECK(close_enough(m(5,4), -2.0*ddx));
    BOOST_CHECK(close_enough(m(5,5), 3.0/2.0*ddx));
}

BOOST_AUTO_TEST_CASE(testFirstOrder5PointsOnUniformGrid) {
    BOOST_TEST_MESSAGE("Testing five points first order "
            "derivative operator on an uniform grid...");

    const Real ddx = 1.0/0.4;

    const SparseMatrix m =
        NthOrderDerivativeOp(0, 1, 5,
            ext::make_shared<FdmMesherComposite>(
                ext::make_shared<Uniform1dMesher>(0.0, 2.0, 6))).toMatrix();

    BOOST_CHECK(close_enough(m(2,0), 1.0/12.0*ddx));
    BOOST_CHECK(close_enough(m(2,1), -2.0/3.0*ddx));
    BOOST_CHECK(m(2,2) < 42*QL_EPSILON);
    BOOST_CHECK(close_enough(m(2,3), 2.0/3.0*ddx));
    BOOST_CHECK(close_enough(m(2,4), -1.0/12.0*ddx));
    BOOST_CHECK(close_enough(m(2,5), 0.0));

    BOOST_CHECK(close_enough(m(0,0), -25.0/12.0*ddx));
    BOOST_CHECK(close_enough(m(0,1), 4.0*ddx));
    BOOST_CHECK(close_enough(m(0,2), -3.0*ddx));
    BOOST_CHECK(close_enough(m(0,3), 4.0/3.0*ddx));
    BOOST_CHECK(close_enough(m(0,4), -0.25*ddx));
    BOOST_CHECK(close_enough(m(0,5), 0.0));

    BOOST_CHECK(close_enough(m(1,0), -0.25*ddx));
    BOOST_CHECK(close_enough(m(1,1), -5.0/6.0*ddx));
    BOOST_CHECK(close_enough(m(1,2), 3.0/2.0*ddx));
    BOOST_CHECK(close_enough(m(1,3), -0.5*ddx));
    BOOST_CHECK(close_enough(m(1,4), 1.0/12.0*ddx));
    BOOST_CHECK(close_enough(m(1,5), 0.0));

    BOOST_CHECK(close_enough(m(4,5), 0.25*ddx));
    BOOST_CHECK(close_enough(m(4,4), 5.0/6.0*ddx));
    BOOST_CHECK(close_enough(m(4,3), -3.0/2.0*ddx));
    BOOST_CHECK(close_enough(m(4,2), 0.5*ddx));
    BOOST_CHECK(close_enough(m(4,1), -1.0/12.0*ddx));
    BOOST_CHECK(close_enough(m(4,0), 0.0));

    BOOST_CHECK(close_enough(m(5,0), 0.0));
    BOOST_CHECK(close_enough(m(5,1), 0.25*ddx));
    BOOST_CHECK(close_enough(m(5,2), -4.0/3.0*ddx));
    BOOST_CHECK(close_enough(m(5,3), 3.0*ddx));
    BOOST_CHECK(close_enough(m(5,4), -4.0*ddx));
    BOOST_CHECK(close_enough(m(5,5), 25.0/12.0*ddx));
}

BOOST_AUTO_TEST_CASE(testFirstOrder2PointsOnUniformGrid) {
    BOOST_TEST_MESSAGE("Testing two points first order "
            "derivative operator on an uniform grid...");

    const Real ddx = 1.0/0.2;

    const SparseMatrix m =
        NthOrderDerivativeOp(0, 1, 2,
            ext::make_shared<FdmMesherComposite>(
                ext::make_shared<Uniform1dMesher>(0.0, 0.6, 4))).toMatrix();

    BOOST_CHECK(close_enough(m(0,0), -ddx));
    BOOST_CHECK(close_enough(m(0,1), ddx));
    BOOST_CHECK(close_enough(m(0,2), 0.0));
    BOOST_CHECK(close_enough(m(0,3), 0.0));

    BOOST_CHECK(close_enough(m(1,0), -ddx));
    BOOST_CHECK(close_enough(m(1,1), ddx));
    BOOST_CHECK(close_enough(m(1,2), 0.0));
    BOOST_CHECK(close_enough(m(1,3), 0.0));

    BOOST_CHECK(close_enough(m(2,0), 0.0));
    BOOST_CHECK(close_enough(m(2,1), -ddx));
    BOOST_CHECK(close_enough(m(2,2), ddx));
    BOOST_CHECK(close_enough(m(2,3), 0.0));

    BOOST_CHECK(close_enough(m(3,0), 0.0));
    BOOST_CHECK(close_enough(m(3,1), 0.0));
    BOOST_CHECK(close_enough(m(3,2), -ddx));
    BOOST_CHECK(close_enough(m(3,3), ddx));
}

BOOST_AUTO_TEST_CASE(testFirstOrder4PointsOnUniformGrid) {
    BOOST_TEST_MESSAGE("Testing four points first order "
            "derivative operator on an uniform grid...");

    const Real ddx = 1.0/0.2;

    const SparseMatrix m =
        NthOrderDerivativeOp(0, 1, 4,
            ext::make_shared<FdmMesherComposite>(
                ext::make_shared<Uniform1dMesher>(0.0, 0.6, 4))).toMatrix();

    BOOST_CHECK(close_enough(m(0,0), -11.0/6.0*ddx));
    BOOST_CHECK(close_enough(m(0,1), 3.0*ddx));
    BOOST_CHECK(close_enough(m(0,2), -1.5*ddx));
    BOOST_CHECK(close_enough(m(2,3), 1.0/3.0*ddx));

    BOOST_CHECK(close_enough(m(1,0), -1.0/3.0*ddx));
    BOOST_CHECK(close_enough(m(1,1), -0.5*ddx));
    BOOST_CHECK(close_enough(m(1,2), ddx));
    BOOST_CHECK(close_enough(m(1,3), -1.0/6.0*ddx));

    BOOST_CHECK(close_enough(m(2,0), 1.0/6.0*ddx));
    BOOST_CHECK(close_enough(m(2,1), -ddx));
    BOOST_CHECK(close_enough(m(2,2), 0.5*ddx));
    BOOST_CHECK(close_enough(m(2,3), 1.0/3.0*ddx));

    BOOST_CHECK(close_enough(m(3,0), -1.0/3.0*ddx));
    BOOST_CHECK(close_enough(m(3,1), 1.5*ddx));
    BOOST_CHECK(close_enough(m(3,2), -3.0*ddx));
    BOOST_CHECK(close_enough(m(3,3), 11.0/6.0*ddx));
}

BOOST_AUTO_TEST_CASE(testFirstOrder2PointsOn2DimUniformGrid) {
    BOOST_TEST_MESSAGE("Testing two points first order "
            "derivative operator on a 2 dimensional uniform grid...");

    const Real ddx = 1.0/0.2;

    const Size xGrid=4;
    const ext::shared_ptr<FdmMesher> mesher =
        ext::make_shared<FdmMesherComposite>(
            ext::make_shared<Uniform1dMesher>(0.0, 1, xGrid),
            ext::make_shared<Uniform1dMesher>(0.0, 0.4, 3));

    const SparseMatrix m = NthOrderDerivativeOp(1, 1, 2, mesher).toMatrix();

    for (const auto& iter : *mesher->layout()) {
        const Size i = iter.index();
        const Size ix = iter.coordinates()[1];

        switch (ix) {
          case 0:
            BOOST_CHECK(close_enough(m(i, i        ),-ddx));
            BOOST_CHECK(close_enough(m(i, i+  xGrid), ddx));
            BOOST_CHECK(close_enough(m(i, i+2*xGrid), 0.0));
            break;
          case 1:
            BOOST_CHECK(close_enough(m(i, i-  xGrid),-ddx));
            BOOST_CHECK(close_enough(m(i, i        ), ddx));
            BOOST_CHECK(close_enough(m(i, i+2*xGrid), 0.0));
            break;
          case 2:
            BOOST_CHECK(close_enough(m(i, i-2*xGrid), 0.0));
            BOOST_CHECK(close_enough(m(i, i-  xGrid),-ddx));
            BOOST_CHECK(close_enough(m(i, i        ), ddx));
            break;
          default:
            QL_FAIL("inconsistent coordinate");
        }
    }
}

BOOST_AUTO_TEST_CASE(testSecondOrder3PointsNonUniformGrid) {
    BOOST_TEST_MESSAGE("Testing three points second order "
            "derivative operator on a non-uniform grid...");

    std::vector<Real> xValues(4);
    xValues[0] = 0.5;
    xValues[1] = 1.0;
    xValues[2] = 2.0;
    xValues[3] = 4.0;

    const SparseMatrix m =
        NthOrderDerivativeOp(0, 2, 3,
            ext::make_shared<FdmMesherComposite>(
                ext::make_shared<Predefined1dMesher>(xValues))).toMatrix();

    BOOST_CHECK(close_enough(m(0,0), 8.0/3.0));
    BOOST_CHECK(close_enough(m(0,1), -4.0));
    BOOST_CHECK(close_enough(m(0,2), 4.0/3.0));
    BOOST_CHECK(close_enough(m(0,3), 0.0));

    BOOST_CHECK(close_enough(m(1,0), 8.0/3.0));
    BOOST_CHECK(close_enough(m(1,1), -4.0));
    BOOST_CHECK(close_enough(m(1,2), 4.0/3.0));
    BOOST_CHECK(close_enough(m(1,3), 0.0));

    BOOST_CHECK(close_enough(m(2,0), 0.0));
    BOOST_CHECK(close_enough(m(2,1), 2.0/3.0));
    BOOST_CHECK(close_enough(m(2,2), -1.0));
    BOOST_CHECK(close_enough(m(2,3), 1.0/3.0));

    BOOST_CHECK(close_enough(m(3,0), 0.0));
    BOOST_CHECK(close_enough(m(3,1), 2.0/3.0));
    BOOST_CHECK(close_enough(m(3,2), -1.0));
    BOOST_CHECK(close_enough(m(3,3), 1.0/3.0));
}

BOOST_AUTO_TEST_CASE(testSecondOrder4PointsNonUniformGrid) {
    BOOST_TEST_MESSAGE("Testing four points second order "
            "derivative operator on a non-uniform grid...");

    std::vector<Real> xValues(5);
    xValues[0] = 0.5;
    xValues[1] = 1.0;
    xValues[2] = 2.0;
    xValues[3] = 4.0;
    xValues[4] = 8.0;

    const SparseMatrix m =
        NthOrderDerivativeOp(0, 2, 4,
            ext::make_shared<FdmMesherComposite>(
                ext::make_shared<Predefined1dMesher>(xValues))).toMatrix();

    BOOST_CHECK(close_enough(m(0,0), 88.0/21.0));
    BOOST_CHECK(close_enough(m(0,1), -140.0/21.0));
    BOOST_CHECK(close_enough(m(0,2), 56.0/21.0));
    BOOST_CHECK(close_enough(m(0,3), -4.0/21.0));
    BOOST_CHECK(close_enough(m(0,4), 0.0));

    BOOST_CHECK(close_enough(m(1,0), 64.0/21.0));
    BOOST_CHECK(close_enough(m(1,1), -98.0/21.0));
    BOOST_CHECK(close_enough(m(1,2), 35.0/21.0));
    BOOST_CHECK(close_enough(m(1,3), -1.0/21.0));
    BOOST_CHECK(close_enough(m(1,4), 0.0));

    BOOST_CHECK(close_enough(m(2,0), 16.0/21.0));
    BOOST_CHECK(close_enough(m(2,1), -2.0/3.0));
    BOOST_CHECK(close_enough(m(2,2), -1.0/3.0));
    BOOST_CHECK(close_enough(m(2,3), 5.0/21.0));
    BOOST_CHECK(close_enough(m(2,4), 0.0));

    BOOST_CHECK(close_enough(m(3,0), 0.0));
    BOOST_CHECK(close_enough(m(3,1), 4.0/21.0));
    BOOST_CHECK(close_enough(m(3,2), -1.0/6.0));
    BOOST_CHECK(close_enough(m(3,3), -1.0/12.0));
    BOOST_CHECK(close_enough(m(3,4), 5.0/84.0));

    BOOST_CHECK(close_enough(m(4,0), 0.0));
    BOOST_CHECK(close_enough(m(4,1), -20.0/21.0));
    BOOST_CHECK(close_enough(m(4,2), 11.0/6.0));
    BOOST_CHECK(close_enough(m(4,3), -13.0/12.0));
    BOOST_CHECK(close_enough(m(4,4), 17.0/84.0));
}

BOOST_AUTO_TEST_CASE(testThirdOrder4PointsUniformGrid) {
    BOOST_TEST_MESSAGE("Testing four points third order "
            "derivative operator on a uniform grid...");

    const SparseMatrix m =
        NthOrderDerivativeOp(0, 3, 4,
            ext::make_shared<FdmMesherComposite>(
                ext::make_shared<Uniform1dMesher>(0.0, 0.6, 4))).toMatrix();

    for (Size i=0; i < 4; ++i) {
        BOOST_CHECK(close_enough(m(i,0), -125.0));
        BOOST_CHECK(close_enough(m(i,1), 375.0));
        BOOST_CHECK(close_enough(m(i,2), -375.0));
        BOOST_CHECK(close_enough(m(i,3), 125));
    }
}


struct GridSetup {
    Real alpha;
    Real density;
    bool cellAvg;
    bool midPoint;
    Size nPoints;
    Size tGrid;
    Size yGrid;
    Size vGrid;
    FdmSchemeDesc scheme;
};

class FdmHestonNthOrderOp : public FdmLinearOpComposite {
  public:
    FdmHestonNthOrderOp(Size nPoints,
                        const ext::shared_ptr<HestonProcess>& hestonProcess,
                        const ext::shared_ptr<FdmMesher>& mesher,
                        Size direction = 0)
    : vol2_(0.5 * hestonProcess->theta()),
      preconditioner_(SecondDerivativeOp(direction, mesher).mult(Array(mesher->layout()->size(), vol2_))) {

        const Array vv(mesher->locations(1));
        Array varianceValues(0.5*vv);

        ext::shared_ptr<FdmLinearOpLayout> layout = mesher->layout();
        for (const auto& iter : *layout) {
            if (   iter.coordinates()[0] == 0
                || iter.coordinates()[0] == layout->dim()[0]-1) {
                varianceValues[iter.index()] = 0.0;
            }
        }
        const Size n = mesher->layout()->size();

        using namespace boost::numeric::ublas;
        banded_matrix<Real> v(n, n), u(n, n), rV(n, n);
        for (Size i=0; i < n; ++i) {
            v(i, i) = varianceValues[i];
            u(i, i) = vv[i];
            rV(i, i) = varianceValues[i] - 0.5*hestonProcess->theta();
        }

        const SparseMatrix dx = NthOrderDerivativeOp(0, 1, nPoints, mesher).toMatrix();
        const SparseMatrix dxx = NthOrderDerivativeOp(0, 2, nPoints, mesher).toMatrix();
        const SparseMatrix dv = NthOrderDerivativeOp(1, 1, nPoints, mesher).toMatrix();
        const SparseMatrix dvv = NthOrderDerivativeOp(1, 2, nPoints, mesher).toMatrix();

        const Real kappa = hestonProcess->kappa();
        const Real theta = hestonProcess->theta();
        const Real sigma = hestonProcess->sigma();
        const Real rho = hestonProcess->rho();

        map_ = sparse_prod<SparseMatrix>(-rV, dx)
            + sparse_prod<SparseMatrix>(v, dxx)
            + (0.5*rho*sigma)*sparse_prod<SparseMatrix>(u, sparse_prod<SparseMatrix>(dx, dv)
                                                        + sparse_prod<SparseMatrix>(dv, dx))
            + (0.5*sigma*sigma)*sparse_prod<SparseMatrix>(u, dvv)
            + kappa*sparse_prod<SparseMatrix>(theta*identity_matrix<Real>(n) - u, dv);
    }

    SparseMatrix toMatrix() const override {
        return map_;
    }

    Size size() const override { return 2; }
    void setTime(Time t1, Time t2) override { }

    Array apply(const Array& r) const override {
        return prod(map_, r);
    }

    Array apply_mixed(const Array& r) const override {
        QL_FAIL("operator splitting is not supported");
    }

    Array apply_direction(Size direction, const Array& r) const override {
        QL_FAIL("operator splitting is not supported");
    }

    Array solve_splitting(Size direction, const Array& r, Real dt) const override {
        QL_FAIL("operator splitting is not supported");
    }

    Array preconditioner(const Array& r, Real dt) const override {
        return preconditioner_.solve_splitting(r, dt, 1.0);
    }

  private:
    Array solve_apply(const Array& r, Real dt) const {
        return r - dt*apply(r);
    }

    const Volatility vol2_;
    SparseMatrix map_;
    TripleBandLinearOp preconditioner_;
};


class AvgPayoffFct {
  public:
    AvgPayoffFct(ext::shared_ptr<PlainVanillaPayoff> payoff,
                 Volatility vol, Time T, Real growthFactor)
    : payoff_(std::move(payoff)),
      vol2_(0.5*vol*vol*T),
      growthFactor_(growthFactor) { }

    Real operator()(Real x) const {
        return (*payoff_)(std::exp(x - vol2_)*growthFactor_);
    }

  private:
    const ext::shared_ptr<PlainVanillaPayoff> payoff_;
    const Volatility vol2_;
    const Real growthFactor_;
};

Array priceReport(const GridSetup& setup, const Array& strikes) {

    const Date today(2, May, 2018);
    const DayCounter dc = Actual365Fixed();
    const Date maturity = today + Period(1, Years);
    const Time T = dc.yearFraction(today, maturity);

    const ext::shared_ptr<YieldTermStructure> rTS
        = flatRate(today, 0.05, dc);
    const ext::shared_ptr<YieldTermStructure> qTS
        = flatRate(today, 0.0, dc);

    const Real S = 100.0;
    const Volatility vol = 0.2;
    const Real v0 = vol*vol;
    const Real kappa = 1.0;
    const Real theta = vol*vol;
    const Real sig = 0.2;
    const Real rho = -0.75;

    const ext::shared_ptr<HestonProcess> hestonProcess
        = ext::make_shared<HestonProcess>(
                  Handle<YieldTermStructure>(rTS),
                  Handle<YieldTermStructure>(qTS),
                  Handle<Quote>(ext::make_shared<SimpleQuote>(S)),
                  v0, kappa, theta, sig, rho);

    const Real stdDev = vol * std::sqrt(T);
    const DiscountFactor df
        = qTS->discount(maturity)/rTS->discount(maturity);

    const Real y = std::log(S);

    const Real ymin = y - setup.alpha*stdDev;
    const Real ymax = y + setup.alpha*stdDev;

    const Size yGrid = setup.yGrid;
    const Size vGrid = setup.vGrid;

    Array diffs(strikes.size()), fdmPrices(strikes.size());
    for (Size k=0; k < strikes.size(); ++k) {
        const Real strike = strikes[k];
        const Real specialPoint = std::log(strike/df) + 0.5*vol*vol*T;

        const ext::shared_ptr<Fdm1dMesher> mesher1d =
                ext::make_shared<Concentrating1dMesher>(
                    ymin, ymax, yGrid,
                    std::pair<Real, Real>(specialPoint, setup.density));

        std::vector<Real> loc = mesher1d->locations();
        for (Size i = 0; setup.midPoint && i < loc.size()-1; ++i)
            if (loc[i] < specialPoint && loc[i+1]>= specialPoint) {
                const Real d = loc[i+1] - loc[i];

                const Real offset = (specialPoint - 0.5*d) - loc[i];

                for (Real& l : loc)
                    l += offset;

                break;
            }

        const ext::shared_ptr<FdmMesherComposite> mesher =
            ext::make_shared<FdmMesherComposite>(
                    ext::make_shared<Predefined1dMesher>(loc),
                    ext::make_shared<FdmHestonVarianceMesher>(
                        vGrid, hestonProcess, 1.0));

        const Array g = mesher->locations(0);
        const Array sT = Exp(g - 0.5*vol*vol*T)*df;

        Array rhs(mesher->layout()->size());

        const ext::shared_ptr<PlainVanillaPayoff> payoff =
            ext::make_shared<PlainVanillaPayoff>(Option::Put, strike);

        for (const auto& iter : *mesher->layout()) {
            const Size idx = iter.index();
            const Size idxm1 = mesher->layout()->neighbourhood(iter,  0,-1);
            const Size idxp1 = mesher->layout()->neighbourhood(iter,  0, 1);

            const Size nx = iter.coordinates()[0];

            if (nx != 0 && nx != yGrid-1
                && setup.cellAvg &&
                ((sT[idx] < strike && sT[idxp1] >= strike)
                 || (sT[idxm1] < strike && sT[idx] >= strike))) {

                const Real gMin = 0.5*(g[idxm1] + g[idx]);
                const Real gMax = 0.5*(g[idxp1] + g[idx]);

                const AvgPayoffFct f(payoff, vol, T, df);

                rhs[idx] = GaussLobattoIntegral(1000, 1e-12)(f, gMin, gMax)/(gMax - gMin);
            }
            else
                rhs[idx] = (*payoff)(sT[idx]);
        }

        const ext::shared_ptr<FdmHestonNthOrderOp> heatEqn =
            ext::make_shared<FdmHestonNthOrderOp>(
                    setup.nPoints, hestonProcess, mesher);

        FdmBackwardSolver solver(
                                 heatEqn,
                                 FdmBoundaryConditionSet(),
                                 ext::shared_ptr<FdmStepConditionComposite>(),
                                 setup.scheme);

        solver.rollback(rhs, T, 0.0, setup.tGrid, 1);

        rhs *= rTS->discount(maturity);

        const std::vector<Real>& x =
            mesher->getFdm1dMeshers()[0]->locations();
        const std::vector<Real>& v =
            mesher->getFdm1dMeshers()[1]->locations();

        Matrix resultValues_(mesher->layout()->dim()[1], mesher->layout()->dim()[0]);
        std::copy(rhs.begin(), rhs.end(), resultValues_.begin());

        const ext::shared_ptr<BicubicSpline> interpolation =
            ext::make_shared<BicubicSpline>(
                    x.begin(), x.end(), v.begin(), v.end(), resultValues_);

        const Real fdmPrice = (*interpolation)(y, hestonProcess->v0());

        VanillaOption option(
                             ext::make_shared<PlainVanillaPayoff>(Option::Put, strike),
                             ext::make_shared<EuropeanExercise>(maturity)
                             );
        option.setPricingEngine(
                ext::make_shared<AnalyticHestonEngine>(
                    ext::make_shared<HestonModel>(hestonProcess), 192)
                                );
        const Real npv = option.NPV();

        diffs[k] = npv - fdmPrice;
    }

    return diffs;
}

class FdmMispricingCostFunction : public CostFunction {
  public:
    FdmMispricingCostFunction(const GridSetup& setup, Array strikes)
    : setup_(setup), strikes_(std::move(strikes)) { }

    Array values(const Array& x) const override {
        const GridSetup g = {
            x[0], x[1],
            setup_.cellAvg, setup_.midPoint,
            setup_.nPoints,
            setup_.tGrid, setup_.yGrid, setup_.vGrid,
            setup_.scheme
        };

        try {
            return priceReport(g, strikes_);
        }
        catch (std::exception const&) {
            Array q(2, 1000);
            return q;
        }
    }

  private:
    const GridSetup setup_;
    const Array strikes_;
};


BOOST_AUTO_TEST_CASE(testHigherOrderHestonOptionPricing) {
    BOOST_TEST_MESSAGE("Testing Heston model option pricing convergence with "
            "higher order finite difference operators...");

    const Array strikes = {50, 75, 90, 100, 110, 125, 150, 200};

    const GridSetup initSetup = {
        3.87773, 0.043847, true, false,
        5, 21, 20, 11, FdmSchemeDesc::CrankNicolson()
    };

    const Array initialValues = {initSetup.alpha, initSetup.density};

    FdmMispricingCostFunction costFct(initSetup, strikes);
    NoConstraint noConstraint;

    Problem prob(costFct, noConstraint, initialValues);

    LevenbergMarquardt().minimize(
        prob, EndCriteria(400, 40, 1.0e-4, 1.0e-4, 1.0e-4));

    const GridSetup optimalSetup = {
        prob.currentValue()[0], prob.currentValue()[1],
        initSetup.cellAvg, initSetup.midPoint,
        initSetup.nPoints,
        initSetup.tGrid,
        initSetup.yGrid/2,
        initSetup.vGrid,
        initSetup.scheme
    };

    const Array q = priceReport(optimalSetup, strikes);
    const Real ac = std::sqrt(DotProduct(q, q)/q.size());

    const Array p = priceReport(initSetup, strikes);
    const Real ap = std::sqrt(DotProduct(p, p)/p.size());

    const Real convergence = std::log(ac/ap)*M_LOG2E;

    if (convergence < 3.6) {
        BOOST_ERROR("convergence order is too low"
                << "\n expected convergence: 4.0"
                << "\n measured convergence: " << convergence
                << "\n tolerance           : 0.4");
    }
}


Real priceQuality(Real h) {

    const Array strikes = {100};

    const Size yGrid = Size(1/h);
    const GridSetup setup = {
        5.50966, 0.0130581,
        true, false,
        5, 401, yGrid, 21,
        FdmSchemeDesc::CrankNicolson()
    };

    return  std::fabs(priceReport(setup, strikes)[0]);
}


BOOST_AUTO_TEST_CASE(testHigherOrderAndRichardsonExtrapolation) {
    BOOST_TEST_MESSAGE(
            "Testing Heston option pricing convergence with "
            "higher order FDM operators and Richardson Extrapolation...");

    const Real n1 = priceQuality(1.0/25);
    const Real n3
        = std::fabs(RichardsonExtrapolation(priceQuality, 1.0/25, 4.0)(2.0));

    const Real r2 = std::log(n1/n3)*M_LOG2E;

    if (r2 < 4.9) {
        BOOST_ERROR("convergence order is too low using Richardson extrapolation"
                << "\n expected convergence: 5.0"
                << "\n measured convergence: " << r2
                << "\n tolerance           : 0.1");
    }
}

BOOST_AUTO_TEST_CASE(testCompareFirstDerivativeOpNonUniformGrid) {
    BOOST_TEST_MESSAGE(
        "Testing with FirstDerivativeOp on a non-uniform grid...");

    Array xValues = Exp(Array(7, 0, 0.1));

    const ext::shared_ptr<Fdm1dMesher> m
        = ext::make_shared<Predefined1dMesher>(
            std::vector<Real>(xValues.begin(), xValues.end()));

    const ext::shared_ptr<FdmMesher> m1d(
        ext::make_shared<FdmMesherComposite>(m));

    const ext::shared_ptr<FirstDerivativeOp> fx
        = ext::make_shared<FirstDerivativeOp>(0, m1d);

    const ext::shared_ptr<NthOrderDerivativeOp> dx
        = ext::make_shared<NthOrderDerivativeOp>(0, 1, 3, m1d);

    const SparseMatrix fm = fx->toMatrix();
    const SparseMatrix dm = dx->toMatrix();

    for (Size i=1; i < m->size()-1; ++i) // different boundary conditions
        for (Size j=0; j < m->size(); ++j)
            BOOST_CHECK(std::fabs(fm(i, j)- dm(i, j)) < 1e-12);
}

BOOST_AUTO_TEST_CASE(testCompareFirstDerivativeOp2dUniformGrid) {
    BOOST_TEST_MESSAGE(
        "Testing with FirstDerivativeOp on a 2d uniform grid...");

    const ext::shared_ptr<Fdm1dMesher> m1
        = ext::make_shared<Uniform1dMesher>(0.0, 0.6, 5);
    const ext::shared_ptr<Fdm1dMesher> m2
        = ext::make_shared<Uniform1dMesher>(0.0, 1.6, 6);

    const ext::shared_ptr<FdmMesher> mc(
        ext::make_shared<FdmMesherComposite>(m1, m2));

    const Size n = mc->layout()->dim()[0];
    const Size m = mc->layout()->dim()[1];

    SparseMatrix fm = FirstDerivativeOp(0, mc).toMatrix();
    SparseMatrix dm = NthOrderDerivativeOp(0, 1, 3, mc).toMatrix();

    for (Size k=0; k < m; ++k) {
        const Size idx = k*n;
        for (Size i=1; i < n-1; ++i)
            for (Size j=0; j < n*m; ++j)
                BOOST_CHECK(std::fabs(Real(fm(idx + i, j)) - Real(dm(idx + i, j))) < 1e-12);
    }

    fm = FirstDerivativeOp(1, mc).toMatrix();
    dm = NthOrderDerivativeOp(1, 1, 3, mc).toMatrix();

    for (Size i=n; i < n*(m-1); ++i)
        for (Size j=0; j < n*m; ++j)
            BOOST_CHECK(std::fabs(Real(fm(i, j)) - Real(dm(i, j))) < 1e-12);
}

BOOST_AUTO_TEST_CASE(testMixedSecondOrder9PointsOnUniformGrid) {
    BOOST_TEST_MESSAGE(
            "Testing nine points mixed second order "
            "derivative operator on a uniform grid...");

    const ext::shared_ptr<Fdm1dMesher> m
        = ext::make_shared<Uniform1dMesher>(0.0, 0.6, 5);

    const ext::shared_ptr<FdmMesher> mc(
        ext::make_shared<FdmMesherComposite>(m, m));

    const SparseMatrix cc =
            prod(NthOrderDerivativeOp(0, 1, 3, mc).toMatrix(),
                 NthOrderDerivativeOp(1, 1, 3, mc).toMatrix());

    const SparseMatrix mm = SecondOrderMixedDerivativeOp(0,1,mc).toMatrix();

    const Size n = m->size();

    for (Size i=1; i < n-1; ++i)
        for (Size j=1; j < n-1; ++j) {
            const Size idx = i*n+j;
            for (Size k=1; k < n-1; ++k)
                for (Size l=1; l < n-1; ++l) {
                    const Size kdx = k*n+l;
                    BOOST_CHECK(std::fabs(mm(idx,kdx) - cc(idx,kdx)) < 1e-12);
                }
        }
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()
