/*
 Copyright (C) 2016 Quaternion Risk Management Ltd
 All rights reserved.

 This file is part of ORE, a free-software/open-source library
 for transparent pricing and risk analysis - http://opensourcerisk.org

 ORE is free software: you can redistribute it and/or modify it
 under the terms of the Modified BSD License.  You should have received a
 copy of the license along with this program.
 The license is also available online at <http://opensourcerisk.org>

 This program is distributed on the basis that it will form a useful
 contribution to risk analytics and model standardisation, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
*/

/*! \file models/crossassetmodel.hpp
    \brief cross asset model
    \ingroup crossassetmodel
*/

#ifndef quantext_crossasset_model_hpp
#define quantext_crossasset_model_hpp

#include <qle/models/crlgm1fparametrization.hpp>
#include <qle/models/eqbsparametrization.hpp>
#include <qle/models/fxbsparametrization.hpp>
#include <qle/models/infdkparametrization.hpp>
#include <qle/models/lgm.hpp>

#include <qle/processes/crossassetstateprocess.hpp>

#include <ql/math/distributions/normaldistribution.hpp>
#include <ql/math/integrals/integral.hpp>
#include <ql/math/matrix.hpp>
#include <ql/models/model.hpp>

#include <boost/bind.hpp>

namespace QuantExt {
using namespace QuantLib;

namespace CrossAssetModelTypes {
//! Cross Asset Type
//! \ingroup crossassetmodel
enum AssetType { IR, FX, INF, CR, EQ };
} // namespace CrossAssetModelTypes

using namespace CrossAssetModelTypes;

//! Cross Asset Model
/*! \ingroup crossassetmodel
 */
class CrossAssetModel : public LinkableCalibratedModel {
public:
    /*! Parametrizations must be given in the following order
        - IR  (first parametrization defines the domestic currency)
        - FX  (for all pairs domestic-ccy defined by the IR models)
        - INF (optionally, ccy must be a subset of the IR ccys)
        - CR  (optionally, ccy must be a subset of the IR ccys)
        - EQ  (for all names equity currency defined in Parametrization)
        If the correlation matrix is not given, it is initialized
        as the unit matrix (and can be customized after
        construction of the model).
    */
    CrossAssetModel(const std::vector<boost::shared_ptr<Parametrization> >& parametrizations,
                    const Matrix& correlation = Matrix(),
                    SalvagingAlgorithm::Type salvaging = SalvagingAlgorithm::None);

    /*! IR-FX model based constructor */
    CrossAssetModel(const std::vector<boost::shared_ptr<LinearGaussMarkovModel> >& currencyModels,
                    const std::vector<boost::shared_ptr<FxBsParametrization> >& fxParametrizations,
                    const Matrix& correlation = Matrix(),
                    SalvagingAlgorithm::Type salvaging = SalvagingAlgorithm::None);

    /*! returns the state process with a given discretization */
    const boost::shared_ptr<StochasticProcess>
    stateProcess(CrossAssetStateProcess::discretization disc = CrossAssetStateProcess::exact) const;

    /*! total dimension of model (sum of number of state variables) */
    Size dimension() const;

    /*! total number of Brownian motions (this is less or equal to dimension) */
    Size brownians() const;

    /*! total number of parameters that can be calibrated */
    Size totalNumberOfParameters() const;

    /*! number of components for an asset class */
    Size components(const AssetType t) const;

    /*! number of brownian motions for a component */
    Size brownians(const AssetType t, const Size i) const;

    /*! number of state variables for a component */
    Size stateVariables(const AssetType t, const Size i) const;

    /*! return index for currency (0 = domestic, 1 = first
      foreign currency and so on) */
    Size ccyIndex(const Currency& ccy) const;

    /*! return index for equity (0 = first equity) */
    Size eqIndex(const std::string& eqName) const;

    /*! return index for inflation (0 = first inflation index) */
    Size infIndex(const std::string& index) const;

    /*! observer and linked calibrated model interface */
    void update();
    void generateArguments();

    /*! LGM1F components, ccy=0 refers to the domestic currency */
    const boost::shared_ptr<LinearGaussMarkovModel> lgm(const Size ccy) const;

    const boost::shared_ptr<IrLgm1fParametrization> irlgm1f(const Size ccy) const;

    Real numeraire(const Size ccy, const Time t, const Real x,
                   Handle<YieldTermStructure> discountCurve = Handle<YieldTermStructure>()) const;

    Real discountBond(const Size ccy, const Time t, const Time T, const Real x,
                      Handle<YieldTermStructure> discountCurve = Handle<YieldTermStructure>()) const;

    Real reducedDiscountBond(const Size ccy, const Time t, const Time T, const Real x,
                             Handle<YieldTermStructure> discountCurve = Handle<YieldTermStructure>()) const;

    Real discountBondOption(const Size ccy, Option::Type type, const Real K, const Time t, const Time S, const Time T,
                            Handle<YieldTermStructure> discountCurve = Handle<YieldTermStructure>()) const;

    /*! FXBS components, ccy=0 referes to the first foreign currency,
        so it corresponds to ccy+1 if you want to get the corresponding
        irmgl1f component */
    const boost::shared_ptr<FxBsParametrization> fxbs(const Size ccy) const;

    /*! INF DK components */
    const boost::shared_ptr<InfDkParametrization> infdk(const Size i) const;

    /*! CR LGM 1F components */
    const boost::shared_ptr<CrLgm1fParametrization> crlgm1f(const Size i) const;

    /*! EQBS components */
    const boost::shared_ptr<EqBsParametrization> eqbs(const Size ccy) const;

    /* ... add more components here ...*/

    /*! correlation linking the different marginal models, note that
        the use of asset class pairs specific inspectors is
        recommended instead of the global matrix directly */
    const Matrix& correlation() const;

    /*! check if correlation matrix is valid */
    void checkCorrelationMatrix() const;

    /*! index of component in the parametrization vector */
    Size idx(const AssetType t, const Size i) const;

    /*! index of component in the correlation matrix, by offset */
    Size cIdx(const AssetType t, const Size i, const Size offset = 0) const;

    /*! index of component in the stochastic process array, by offset */
    Size pIdx(const AssetType t, const Size i, const Size offset = 0) const;

    /*! correlation between two components */
    const Real& correlation(const AssetType s, const Size i, const AssetType t, const Size j, const Size iOffset = 0,
                            const Size jOffset = 0) const;
    /*! set correlation */
    void correlation(const AssetType s, const Size i, const AssetType t, const Size j, const Real value,
                     const Size iOffset = 0, const Size jOffset = 0);

    /*! analytical moments require numerical integration,
      which can be customized here */
    void setIntegrationPolicy(const boost::shared_ptr<Integrator> integrator,
                              const bool usePiecewiseIntegration = true) const;
    const boost::shared_ptr<Integrator> integrator() const;

    /*! return (V(t), V^tilde(t,T)) in the notation of the book */
    std::pair<Real, Real> infdkV(const Size i, const Time t, const Time T);

    /*! return (I(t), I^tilde(t,T)) in the notation of the book, note that
        I(0) is normalized to 1 here, i.e. you have to multiply the result
        with the index value (as of the base date of the inflation ts) */
    std::pair<Real, Real> infdkI(const Size i, const Time t, const Time T, const Real z, const Real y);

    /*! return YoYIIS(t) in the notation of the book, the year on year
        swaplet price from S to T, at time t */
    Real infdkYY(const Size i, const Time t, const Time S, const Time T, const Real z, const Real y, const Real irz);

    /*! returns (S(t), S^tilde(t,T)) in the notation of the book */
    std::pair<Real, Real> crlgm1fS(const Size i, const Size ccy, const Time t, const Time T, const Real z,
                                   const Real y) const;

    /*! calibration procedures */

    /*! calibrate irlgm1f volatilities to a sequence of ir options with
        expiry times equal to step times in the parametrization */
    void calibrateIrLgm1fVolatilitiesIterative(const Size ccy,
                                               const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                               OptimizationMethod& method, const EndCriteria& endCriteria,
                                               const Constraint& constraint = Constraint(),
                                               const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate irlgm1f reversion to a sequence of ir options with
        maturities equal to step times in the parametrization */
    void calibrateIrLgm1fReversionsIterative(const Size ccy,
                                             const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                             OptimizationMethod& method, const EndCriteria& endCriteria,
                                             const Constraint& constraint = Constraint(),
                                             const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate irlgm1f parameters for one ccy globally to a set
        of ir options */
    void calibrateIrLgm1fGlobal(const Size ccy, const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                OptimizationMethod& method, const EndCriteria& endCriteria,
                                const Constraint& constraint = Constraint(),
                                const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate eq or fx volatilities to a sequence of options with
            expiry times equal to step times in the parametrization */
    void calibrateBsVolatilitiesIterative(const AssetType& assetType, const Size aIdx,
                                          const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                          OptimizationMethod& method, const EndCriteria& endCriteria,
                                          const Constraint& constraint = Constraint(),
                                          const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate eq/fx volatilities globally to a set of fx options */
    void calibrateBsVolatilitiesGlobal(const AssetType& assetType, const Size aIdx,
                                       const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                       OptimizationMethod& method, const EndCriteria& endCriteria,
                                       const Constraint& constraint = Constraint(),
                                       const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate infdk volatilities to a sequence of cpi options with
        expiry times equal to step times in the parametrization */
    void calibrateInfDkVolatilitiesIterative(const Size index,
                                             const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                             OptimizationMethod& method, const EndCriteria& endCriteria,
                                             const Constraint& constraint = Constraint(),
                                             const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate infdk reversions to a sequence of cpi options with
        maturity times equal to step times in the parametrization */
    void calibrateInfDkReversionsIterative(const Size index,
                                           const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                           OptimizationMethod& method, const EndCriteria& endCriteria,
                                           const Constraint& constraint = Constraint(),
                                           const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate infdk volatilities globally to a sequence of cpi cap/floors */
    void calibrateInfDkVolatilitiesGlobal(const Size index,
                                          const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                          OptimizationMethod& method, const EndCriteria& endCriteria,
                                          const Constraint& constraint = Constraint(),
                                          const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate infdk reversions globally to a sequence of cpi cap/floors */
    void calibrateInfDkReversionsGlobal(const Size index,
                                        const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                        OptimizationMethod& method, const EndCriteria& endCriteria,
                                        const Constraint& constraint = Constraint(),
                                        const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate crlgm1f volatilities to a sequence of cds options with
        expiry times equal to step times in the parametrization */
    void calibrateCrLgm1fVolatilitiesIterative(const Size index,
                                               const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                               OptimizationMethod& method, const EndCriteria& endCriteria,
                                               const Constraint& constraint = Constraint(),
                                               const std::vector<Real>& weights = std::vector<Real>());

    /*! calibrate crlgm1f reversions to a sequence of cds options with
        maturity times equal to step times in the parametrization */
    void calibrateCrLgm1fReversionsIterative(const Size index,
                                             const std::vector<boost::shared_ptr<BlackCalibrationHelper> >& helpers,
                                             OptimizationMethod& method, const EndCriteria& endCriteria,
                                             const Constraint& constraint = Constraint(),
                                             const std::vector<Real>& weights = std::vector<Real>());

    /* ... add more calibration procedures here ... */

protected:
    /* ctor to be used in extensions, initialize is not called */
    CrossAssetModel(const std::vector<boost::shared_ptr<Parametrization> >& parametrizations, const Matrix& correlation,
                    SalvagingAlgorithm::Type salvaging, const bool)
        : LinkableCalibratedModel(), p_(parametrizations), rho_(correlation), salvaging_(salvaging) {}

    /*! number of arguments for a component */
    Size arguments(const AssetType t, const Size i) const;

    /*! index of component in the arguments vector, by offset */
    Size aIdx(const AssetType t, const Size i, const Size offset = 0) const;

    /* init methods */
    virtual void initialize();
    virtual void initializeParametrizations();
    virtual void initializeCorrelation();
    virtual void initializeArguments();
    virtual void finalizeArguments();
    virtual void checkModelConsistency() const;
    virtual void initDefaultIntegrator();
    virtual void initStateProcess();

    /* helper function for infdkI, crlgm1fS */
    Real infV(const Size idx, const Size ccy, const Time t, const Time T) const;
    Real crV(const Size idx, const Size ccy, const Time t, const Time T) const;

    // cache for infdkI, crlgm1fS method
    struct cache_key {
        Size i, ccy;
        double t, T;
        bool operator==(const cache_key& o) const { return (i == o.i) && (ccy == o.ccy) && (t == o.t) && (T == o.T); }
    };

    struct cache_hasher : std::unary_function<cache_key, std::size_t> {
        std::size_t operator()(cache_key const& x) const {
            std::size_t seed = 0;
            boost::hash_combine(seed, x.i);
            boost::hash_combine(seed, x.ccy);
            boost::hash_combine(seed, x.t);
            boost::hash_combine(seed, x.T);
            return seed;
        }
    };

    mutable boost::unordered_map<cache_key, std::pair<Real, Real>, cache_hasher> cache_crlgm1fS_, cache_infdkI_;

    /* members */

    Size nIrLgm1f_, nFxBs_, nInfDk_, nCrLgm1f_, nEqBs_;
    Size totalNumberOfParameters_;
    std::vector<boost::shared_ptr<Parametrization> > p_;
    std::vector<boost::shared_ptr<LinearGaussMarkovModel> > lgm_;
    Matrix rho_;
    SalvagingAlgorithm::Type salvaging_;
    mutable boost::shared_ptr<Integrator> integrator_;
    boost::shared_ptr<CrossAssetStateProcess> stateProcessExact_, stateProcessEuler_;

    /* calibration constraints */

    // move parameter param (e.g. vol, reversion) of asset type component t / index
    // at step i (or at all steps if i is null)
    Disposable<std::vector<bool> > MoveParameter(const AssetType t, const Size param, const Size index, const Size i) {
        switch (t) {
        case IR:
            QL_REQUIRE(param <= 1, "irlgm1f parameter " << param << " out of bounds 0...1");
            QL_REQUIRE(i < irlgm1f(index)->parameter(param)->size(),
                       "irlgm1f parameter (" << param << ") index (" << i << ") for ccy " << index
                                             << " out of bounds 0..." << irlgm1f(index)->parameter(param)->size() - 1);
            break;
        case FX:
            QL_REQUIRE(param == 0, "fxbs parameter " << param << " out of bounds 0...0");
            QL_REQUIRE(i < fxbs(index)->parameter(param)->size(),
                       "fxbs volatility index (" << i << ") for ccy " << index << " out of bounds 0..."
                                                 << fxbs(index)->parameter(param)->size() - 1);
            break;
        case INF:
            QL_REQUIRE(param <= 1, "infdk parameter " << param << " out of bounds 0...1");
            QL_REQUIRE(i < infdk(index)->parameter(param)->size(),
                       "infdk parameter (" << param << ") index (" << i << ") for inflation component " << index
                                           << " out of bounds 0..." << infdk(index)->parameter(param)->size() - 1);
            break;
        case CR:
            QL_REQUIRE(param <= 1, "crlgm1f parameter " << param << " out of bounds 0...1");
            QL_REQUIRE(i < crlgm1f(index)->parameter(param)->size(),
                       "crlgm1f parameter (" << param << ") index (" << i << ") for credit component " << index
                                             << " out of bounds 0..." << crlgm1f(index)->parameter(param)->size() - 1);
            break;
        case EQ:
            QL_REQUIRE(param == 0, "eqbs parameter " << param << " out of bounds 0...0");
            QL_REQUIRE(i < eqbs(index)->parameter(param)->size(),
                       "eqbs volatility index (" << i << ") for index " << index << " out of bounds 0..."
                                                 << eqbs(index)->parameter(param)->size() - 1);
            break;
        default:
            QL_FAIL("asset type not recognised");
        }
        std::vector<bool> res(0);
        for (Size j = 0; j < nIrLgm1f_; ++j) {
            std::vector<bool> tmp1(p_[idx(IR, j)]->parameter(0)->size(), true);
            std::vector<bool> tmp2(p_[idx(IR, j)]->parameter(1)->size(), true);
            std::vector<std::vector<bool> > tmp;
            tmp.push_back(tmp1);
            tmp.push_back(tmp2);
            if (t == IR && index == j) {
                for (Size ii = 0; ii < tmp[param].size(); ++ii) {
                    if (i == Null<Size>() || i == ii) {
                        tmp[param][ii] = false;
                    }
                }
            }
            res.insert(res.end(), tmp[0].begin(), tmp[0].end());
            res.insert(res.end(), tmp[1].begin(), tmp[1].end());
        }
        for (Size j = 0; j < nFxBs_; ++j) {
            std::vector<bool> tmp(p_[idx(FX, j)]->parameter(0)->size(), true);
            if (t == FX && index == j) {
                for (Size ii = 0; ii < tmp.size(); ++ii) {
                    if (i == Null<Size>() || i == ii) {
                        tmp[i] = false;
                    }
                }
            }
            res.insert(res.end(), tmp.begin(), tmp.end());
        }
        for (Size j = 0; j < nInfDk_; ++j) {
            std::vector<bool> tmp1(p_[idx(INF, j)]->parameter(0)->size(), true);
            std::vector<bool> tmp2(p_[idx(INF, j)]->parameter(1)->size(), true);
            std::vector<std::vector<bool> > tmp;
            tmp.push_back(tmp1);
            tmp.push_back(tmp2);
            if (t == INF && index == j) {
                for (Size ii = 0; ii < tmp[param].size(); ++ii) {
                    if (i == Null<Size>() || i == ii) {
                        tmp[param][ii] = false;
                    }
                }
            }
            res.insert(res.end(), tmp[0].begin(), tmp[0].end());
            res.insert(res.end(), tmp[1].begin(), tmp[1].end());
        }
        for (Size j = 0; j < nCrLgm1f_; ++j) {
            std::vector<bool> tmp1(p_[idx(CR, j)]->parameter(0)->size(), true);
            std::vector<bool> tmp2(p_[idx(CR, j)]->parameter(1)->size(), true);
            std::vector<std::vector<bool> > tmp;
            tmp.push_back(tmp1);
            tmp.push_back(tmp2);
            if (t == CR && index == j) {
                for (Size ii = 0; ii < tmp[param].size(); ++ii) {
                    if (i == Null<Size>() || i == ii) {
                        tmp[param][ii] = false;
                    }
                }
            }
            res.insert(res.end(), tmp[0].begin(), tmp[0].end());
            res.insert(res.end(), tmp[1].begin(), tmp[1].end());
        }
        for (Size j = 0; j < nEqBs_; ++j) {
            std::vector<bool> tmp(p_[idx(EQ, j)]->parameter(0)->size(), true);
            if (t == EQ && index == j) {
                for (Size ii = 0; ii < tmp.size(); ++ii) {
                    if (i == Null<Size>() || i == ii) {
                        tmp[i] = false;
                    }
                }
            }
            res.insert(res.end(), tmp.begin(), tmp.end());
        }
        return res;
    }
};

// inline

inline const boost::shared_ptr<StochasticProcess>
CrossAssetModel::stateProcess(CrossAssetStateProcess::discretization disc) const {
    return disc == CrossAssetStateProcess::exact ? stateProcessExact_ : stateProcessEuler_;
}

inline Size CrossAssetModel::dimension() const {
    // this assumes specific models, as soon as other model types are added
    // this formula has to be generalized as well
    return nIrLgm1f_ * 1 + nFxBs_ * 1 + nInfDk_ * 2 + nCrLgm1f_ * 2 + nEqBs_ * 1;
}

inline Size CrossAssetModel::brownians() const {
    // this assumes specific models, as soon as other model types are added
    // this formula has to be generalized as well
    return nIrLgm1f_ * 1 + nFxBs_ * 1 + nInfDk_ * 1 + nCrLgm1f_ * 1 + nEqBs_ * 1;
}

inline Size CrossAssetModel::totalNumberOfParameters() const { return totalNumberOfParameters_; }

inline const boost::shared_ptr<LinearGaussMarkovModel> CrossAssetModel::lgm(const Size ccy) const {
    return lgm_[idx(IR, ccy)];
}

inline const boost::shared_ptr<IrLgm1fParametrization> CrossAssetModel::irlgm1f(const Size ccy) const {
    return lgm(ccy)->parametrization();
}

inline const boost::shared_ptr<InfDkParametrization> CrossAssetModel::infdk(const Size i) const {
    return boost::static_pointer_cast<InfDkParametrization>(p_[idx(INF, i)]);
}

inline const boost::shared_ptr<CrLgm1fParametrization> CrossAssetModel::crlgm1f(const Size i) const {
    return boost::static_pointer_cast<CrLgm1fParametrization>(p_[idx(CR, i)]);
}

inline const boost::shared_ptr<EqBsParametrization> CrossAssetModel::eqbs(const Size name) const {
    return boost::dynamic_pointer_cast<EqBsParametrization>(p_[idx(EQ, name)]);
}

inline Real CrossAssetModel::numeraire(const Size ccy, const Time t, const Real x,
                                       Handle<YieldTermStructure> discountCurve) const {
    return lgm(ccy)->numeraire(t, x, discountCurve);
}

inline Real CrossAssetModel::discountBond(const Size ccy, const Time t, const Time T, const Real x,
                                          Handle<YieldTermStructure> discountCurve) const {
    return lgm(ccy)->discountBond(t, T, x, discountCurve);
}

inline Real CrossAssetModel::reducedDiscountBond(const Size ccy, const Time t, const Time T, const Real x,
                                                 Handle<YieldTermStructure> discountCurve) const {
    return lgm(ccy)->reducedDiscountBond(t, T, x, discountCurve);
}

inline Real CrossAssetModel::discountBondOption(const Size ccy, Option::Type type, const Real K, const Time t,
                                                const Time S, const Time T,
                                                Handle<YieldTermStructure> discountCurve) const {
    return lgm(ccy)->discountBondOption(type, K, t, S, T, discountCurve);
}

inline const boost::shared_ptr<FxBsParametrization> CrossAssetModel::fxbs(const Size ccy) const {
    return boost::dynamic_pointer_cast<FxBsParametrization>(p_[idx(FX, ccy)]);
}

inline const Matrix& CrossAssetModel::correlation() const { return rho_; }

inline const boost::shared_ptr<Integrator> CrossAssetModel::integrator() const { return integrator_; }

} // namespace QuantExt

#endif


#include "tile/math/polynomial.h"

#include <boost/format.hpp>

#include "base/util/lookup.h"

namespace vertexai {
namespace tile {
namespace math {

template <typename T>
Polynomial<T>::Polynomial() {}

template <typename T>
Polynomial<T>::Polynomial(const T& c) : Polynomial<T>("", c) {}

template <typename T>
Polynomial<T>::Polynomial(const std::string& i, const T& c) {
  if (c) {
    map_[i] = c;
  }
}

template <typename T>
T Polynomial<T>::eval(const std::map<std::string, T>& values) const {
  T res = 0;
  for (const auto& kvp : map_) {
    if (kvp.first == "") {
      res += kvp.second;
    } else if (values.find(kvp.first) != values.end()) {
      res += kvp.second * safe_at(values, kvp.first);
    } else {
      throw std::runtime_error(
          str(boost::format("Failed to find value for %s, when evaluating %s") % kvp.first % toString()));
    }
  }
  return res;
}

template <typename T>
Polynomial<T> Polynomial<T>::partial_eval(const std::map<std::string, T>& values) const {
  Polynomial<T> r = *this;
  T off = 0;
  for (const auto& kvp : values) {
    off += get(kvp.first) * kvp.second;
    r.map_.erase(kvp.first);
  }
  r += off;
  return r;
}

template <typename T>
T Polynomial<T>::operator[](const std::string& var) const {
  auto it = map_.find(var);
  if (it == map_.end()) {
    return 0;
  }
  return it->second;
}

template <typename T>
const std::map<std::string, T>& Polynomial<T>::getMap() const {
  return map_;
}

template <typename T>
std::map<std::string, T>& Polynomial<T>::mutateMap() {
  return map_;
}

template <typename T>
Polynomial<T>& Polynomial<T>::operator+=(const Polynomial<T>& rhs) {
  for (const auto& kvp : rhs.map_) {
    T new_val = (map_[kvp.first] += kvp.second);
    if (new_val == 0) {
      map_.erase(kvp.first);
    }
  }
  return *this;
}

template <typename T>
bool Polynomial<T>::operator==(const Polynomial<T>& rhs) const {
  return map_ == rhs.map_;
}

template <typename T>
bool Polynomial<T>::operator<(const Polynomial<T>& rhs) const {
  return map_ < rhs.map_;
}

template <typename T>
Polynomial<T>& Polynomial<T>::operator-=(const Polynomial<T>& rhs) {
  return *this += -1 * rhs;
}

template <typename T>
Polynomial<T> Polynomial<T>::operator-() const {
  return -1 * (*this);
}

template <typename T>
Polynomial<T>& Polynomial<T>::operator*=(const T& rhs) {
  if (rhs == 0) {
    map_.clear();
  } else {
    for (auto& kvp : map_) {
      kvp.second *= rhs;
    }
  }
  return *this;
}

template <typename T>
Polynomial<T>& Polynomial<T>::operator/=(const T& rhs) {
  return *this *= (1 / rhs);
}

template <typename T>
T Polynomial<T>::constant() const {
  auto it = map_.find("");
  return (it == map_.end() ? 0 : it->second);
}

template <typename T>
void Polynomial<T>::setConstant(T value) {
  if (value == T(0)) {
    map_.erase("");
  } else {
    map_[""] = value;
  }
}

template <typename T>
T Polynomial<T>::tryDivide(const Polynomial<T>& p, bool ignoreConst) const {
  auto it = p.map_.begin();
  if (ignoreConst && it != p.map_.end() && it->first == "") {
    it++;
  }
  T val = 0;
  for (const auto& kvp : map_) {
    if (ignoreConst && kvp.first == "") {
      continue;
    }
    if (it == p.map_.end() || it->first != kvp.first) {
      return 0;  // Indexes don't exactly line up, fail
    }
    T div = kvp.second / it->second;
    if (val != 0 && div != val) {
      return 0;  // They don't all divide by the same number
    }
    val = div;
    it++;
  }
  if (it != p.map_.end()) {
    return 0;
  }
  return val;
}

template <typename T>
void Polynomial<T>::substitute(const std::string& var, const Polynomial<T>& replacement) {
  if (map_.count(var) == 0) {
    // If var isn't in this polynomial, nothing needs to be done
    return;
  }
  T coeff = safe_at(map_, var);
  map_.erase(var);
  (*this) += coeff * replacement;
}

template <typename T>
void Polynomial<T>::substitute(const std::map<std::string, Polynomial<T>>& replacements) {
  Polynomial result;
  for (const auto& name_value : map_) {
    auto replacement = replacements.find(name_value.first);
    if (replacement == replacements.end()) {
      result += Polynomial{name_value.first, name_value.second};
      continue;
    }
    result += replacement->second * name_value.second;
  }
  map_.swap(result.map_);
}

template <typename T>
void Polynomial<T>::substitute(const std::string& var, const T& replacement) {
  substitute(var, Polynomial<T>(replacement));
}

template <typename T>
Polynomial<T> Polynomial<T>::sym_eval(const std::map<std::string, Polynomial> values) const {
  Polynomial<T> out;
  for (const auto& kvp : map_) {
    if (kvp.first.empty()) {
      out += Polynomial<T>(kvp.second);
    } else {
      out += safe_at(values, kvp.first) * kvp.second;
    }
  }
  return out;
}

template <typename T>
std::string Polynomial<T>::GetNonzeroIndex() const {
  // Returns a nonconstant nonzero index, if one exists; otherwise returns empty string
  for (const auto& kvp : map_) {
    if (!(kvp.first.empty()) && kvp.second != 0) return kvp.first;
  }

  // No nonconstant index has a nonzero coefficient
  return std::string();
}

template <typename T>
T Polynomial<T>::get(const std::string& name) const {
  auto it = map_.find(name);
  if (it == map_.end()) {
    return T();
  }
  return it->second;
}

int64_t abs_value(int64_t value) { return std::llabs(value); }

Rational abs_value(Rational value) { return abs(value); }

template <typename T>
std::string Polynomial<T>::toString() const {
  std::stringstream ss;
  if (map_.size() == 0) {
    return "0";
  }
  bool first = true;
  for (const auto& kvp : map_) {
    if (first) {
      if (kvp.second < 0) {
        ss << "-";
      }
      first = false;
    } else {
      if (kvp.second > 0) {
        ss << " + ";
      } else {
        ss << " - ";
      }
    }
    auto value = abs_value(kvp.second);
    if (value != 1 || kvp.first == "") {
      ss << value;
      if (kvp.first != "") {
        ss << "*";
      }
    }
    ss << kvp.first;
  }
  return ss.str();
}

template class Polynomial<Rational>;
template class Polynomial<int64_t>;

SimpleConstraint::SimpleConstraint(const Polynomial<Rational>& _poly, int64_t _rhs) : poly(_poly), rhs(_rhs) {}

RangeConstraint::RangeConstraint(const Polynomial<Rational>& _poly, int64_t _range) : poly(_poly), range(_range) {}

bool RangeConstraint::IsParallel(const RangeConstraint& c) {
  if (this->poly.tryDivide(c.poly, true) != 0) return true;
  return false;
}

SimpleConstraint RangeConstraint::lowerBound() const { return SimpleConstraint(-poly, 0); }

SimpleConstraint RangeConstraint::upperBound() const { return SimpleConstraint(poly, range - 1); }

}  // namespace math
}  // namespace tile
}  // namespace vertexai


/**
 * Copyright (c) 2009 Carnegie Mellon University.
 *     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.
 *
 * For more about this software visit:
 *
 *      http://www.graphlab.ml.cmu.edu
 *
 */

/**
 * This file contains an example of graphlab used for discrete loopy
 * belief propagation in a pairwise markov random field to denoise a
 * synthetic noisy image.
 *
 *  \author Joseph Gonzalez
 */

// INCLUDES ===================================================================>

// Including Standard Libraries
#include <cstdlib>
#include <cassert>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <limits>
#include <cmath>

#include <boost/program_options.hpp>

#include <graphlab.hpp>

#include "image.hpp"

// Include the macro for the for each operation
#include <graphlab/macros_def.hpp>

// STRUCTS (Edge and Vertex data) =============================================>

/**
 * The data associated with each directed edge in the pairwise markov
 * random field
 */
struct edge_data {
  graphlab::unary_factor message;
  graphlab::unary_factor old_message;
}; // End of edge data

/**
 * The data associated with each variable in the pairwise markov
 * random field
 */
struct vertex_data {
  graphlab::unary_factor potential;
  graphlab::unary_factor belief;
}; // End of vertex data

typedef graphlab::graph<vertex_data, edge_data> graph_type;
typedef graphlab::types<graph_type> gl_types;

gl_types::glshared<double> sh_bound;
gl_types::glshared<double> sh_damping;
gl_types::glshared<double> sh_ipfdamping;
gl_types::glshared<graphlab::binary_factor> sh_edgepot;
gl_types::glshared<graphlab::binary_factor> sh_truecounts;

// GraphLab Update Function ===================================================>

/** Construct denoising ising model based on the image */
void construct_graph(image& img,
                     size_t num_rings,
                     double sigma,
                     gl_types::graph& graph);

/** Get the counts of the true image */
void get_image_counts(image &trueimg,
                      graphlab::binary_factor &truebinarycount,
                      size_t arity);

/** Tests if two binary factors are equal */
bool binary_factor_equal(const graphlab::binary_factor &a,
                         const graphlab::binary_factor &b);
/**
 * The core belief propagation update function.  This update satisfies
 * the graphlab update_function interface.
 */
void bp_update(gl_types::iscope& scope,
               gl_types::icallback& scheduler);

/**
 * The edge potential sync simply counts the belief across the edges
 */
void edgepot_sync(gl_types::iscope &scope,  graphlab::any& acc);

/**
 * Performs an IPF update using the counts and the true counts
 */
void edgepot_apply(graphlab::any& result,  const graphlab::any& acc);

void edgepot_merge(graphlab::any& result,  const graphlab::any& acc);

// Command Line Parsing =======================================================>

struct options {
  size_t ncpus;
  double bound;
  double damping;
  size_t num_rings;
  size_t rows;
  size_t cols;
  double sigma;
  double lambda;
  size_t splash_size;
  std::string smoothing;
  std::string engine;
  std::string scope;
  std::string scheduler;
  std::string orig_fn;
  std::string noisy_fn;
  std::string pred_fn;
  std::string pred_type;
  std::string visualizer;
  std::string partmethod;
  size_t clustersize;
};

void get_image_counts(image &trueimg,
                      graphlab::binary_factor &truebinarycount,
                      size_t arity) {
  truebinarycount.resize(arity,arity);
  for (size_t i = 0; i < trueimg.rows(); ++i) {
    for (size_t j = 0; j < trueimg.cols(); ++j) {
      size_t color = trueimg.pixel(i,j);
      // check neighbors
      if (i > 1) {
        size_t color2 = trueimg.pixel(i-1,j);
        truebinarycount.logP(color,color2)++;
      }
      if (i < trueimg.rows() - 1) {
        size_t color2 = trueimg.pixel(i+1,j);
        truebinarycount.logP(color,color2)++;
      }
      if (j > 1) {
        size_t color2 = trueimg.pixel(i,j-1);
        truebinarycount.logP(color,color2)++;
      }
      if (j < trueimg.cols() - 1) {
        size_t color2 = trueimg.pixel(i,j+1);
        truebinarycount.logP(color,color2)++;
      }
    }
  }
}

bool binary_factor_equal(const graphlab::binary_factor &a,
                         const graphlab::binary_factor &b) {
  ASSERT_EQ(a.arity1(), b.arity1());
  ASSERT_EQ(a.arity2(), b.arity2());
  for (size_t i = 0;i < a.arity1(); ++i) {
     for (size_t j = 0;j < a.arity2(); ++j) {
      if ((a.logP(i,j) - b.logP(i,j)) > std::numeric_limits<double>::epsilon()) {
        return false;
      }
    }
  }
  return true;
}
// MAIN =======================================================================>
int main(int argc, char** argv) {
  std::cout << "This program creates and denoises a synthetic " << std::endl
            << "image using loopy belief propagation inside " << std::endl
            << "the graphlab framework." << std::endl;

  // set the global logger
  global_logger().set_log_level(LOG_WARNING);
  global_logger().set_log_to_console(true);

  double bound = 1E-4;
  double damping = 0.1;
  double ipfdamping = 0.1;
  size_t colors = 4;
  size_t rows = 100;
  size_t cols = 100;
  double sigma = 2;
  double lambda = 2;
  std::string smoothing = "laplace";
  std::string orig_fn = "source_img.pgm";
  std::string noisy_fn = "noisy_img.pgm";
  std::string pred_fn = "pred_img.pgm";
  std::string pred_type = "map";

  // Parse command line arguments --------------------------------------------->
  graphlab::command_line_options clopts("Loopy BP image denoising");
  clopts.attach_option("bound",
                       &bound, bound,
                       "Residual termination bound");
  clopts.attach_option("damping",
                       &damping, damping,
                       "The amount of message damping (higher = more damping)");
  clopts.attach_option("ipfdamping",
                       &ipfdamping, ipfdamping,
                       "The amount of IPF damping. (lower == more damping)");
  clopts.attach_option("colors",
                       &colors, colors,
                       "The number of colors in the noisy image");
  clopts.attach_option("rows",
                       &rows, rows,
                       "The number of rows in the noisy image");
  clopts.attach_option("cols",
                       &cols, cols,
                       "The number of columns in the noisy image");
  clopts.attach_option("sigma",
                       &sigma, sigma,
                       "Standard deviation of noise.");
  clopts.attach_option("lambda",
                       &lambda, lambda,
                       "Smoothness parameter (larger => smoother).");
  clopts.attach_option("smoothing",
                       &smoothing, smoothing,
                       "Options are {square, laplace}");
  clopts.attach_option("orig",
                       &orig_fn, orig_fn,
                       "Original image file name.");
  clopts.attach_option("noisy",
                       &noisy_fn, noisy_fn,
                       "Noisy image file name.");
  clopts.attach_option("pred",
                       &pred_fn, pred_fn,
                       "Predicted image file name.");
  clopts.attach_option("pred_type",
                       &pred_type, pred_type,
                       "Predicted image type {map, exp}");

  clopts.set_scheduler_type("splash(splash_size=100)");
  clopts.set_scope_type("edge");

  bool success = clopts.parse(argc, argv);
  if(!success) {
    return EXIT_FAILURE;
  }

  std::cout << "ncpus:          " << clopts.get_ncpus() << std::endl
            << "bound:          " << bound << std::endl
            << "damping:        " << damping << std::endl
            << "colors:         " << colors << std::endl
            << "rows:           " << rows << std::endl
            << "cols:           " << cols << std::endl
            << "sigma:          " << sigma << std::endl
            << "lambda:         " << lambda << std::endl
            << "smoothing:      " << smoothing << std::endl
            << "engine:         " << clopts.get_engine_type() << std::endl
            << "scope:          " << clopts.get_scope_type() << std::endl
            << "scheduler:      " << clopts.get_scheduler_type() << std::endl
            << "orig_fn:        " << orig_fn << std::endl
            << "noisy_fn:       " << noisy_fn << std::endl
            << "pred_fn:        " << pred_fn << std::endl
            << "pred_type:      " << pred_type << std::endl;

  // Create synthetic images -------------------------------------------------->
  // Creating image for denoising
  std::cout << "Creating a synthetic image. " << std::endl;
  image img(rows, cols);
  img.paint_sunset(colors);
  graphlab::binary_factor truecounts;
  get_image_counts(img, truecounts, colors);
  std::cout << "Saving image. " << std::endl;
  img.save(orig_fn.c_str());
  std::cout << "Corrupting Image. " << std::endl;
  img.corrupt(sigma);
  std::cout << "Saving corrupted image. " << std::endl;
  img.save(noisy_fn.c_str());

  std::cout << "True Counts: " << std::endl;
  std::cout << truecounts;

  // Create the graph --------------------------------------------------------->
  gl_types::core core;
  // Set the engine options
  core.set_engine_options(clopts);

  std::cout << "Constructing pairwise Markov Random Field. " << std::endl;
  construct_graph(img, colors, sigma, core.graph());

  // Setup global shared variables -------------------------------------------->
  // Initialize the edge agreement factor
  std::cout << "Initializing shared edge agreement factor. " << std::endl;

  // dummy variables 0 and 1 and num_rings by num_rings
  graphlab::binary_factor edge_potential(0, colors, 0, colors);
  // Set the smoothing type
  if(smoothing == "square") {
    edge_potential.set_as_agreement(lambda);
  } else if (smoothing == "laplace") {
    edge_potential.set_as_laplace(lambda);
  } else {
    std::cout << "Invalid smoothing stype!" << std::endl;
    return EXIT_FAILURE;
  }
  std::cout << edge_potential << std::endl;

  // fill the shared variales
  sh_bound.set(bound);
  sh_damping.set(damping);
  sh_truecounts.set(truecounts);
  sh_edgepot.set(edge_potential);
  sh_ipfdamping.set(ipfdamping);
  // make the true counts

  // Running the engine ------------------------------------------------------->
  core.sched_options().add_option("update_function",bp_update);
  std::cout << "Running the engine. " << std::endl;

  graphlab::binary_factor zero;
  zero.resize(colors,colors);
  zero.set_as_agreement(0);
  core.set_sync(sh_edgepot,
                edgepot_sync,
                edgepot_apply,
                zero,
                rows*cols,
                edgepot_merge);
  graphlab::binary_factor oldedgepot = sh_edgepot.get_val();
  graphlab::timer ti;
  ti.start();
  // loop it a few times
  size_t update_count = 0;
  for (size_t i = 0;i < 10; ++i) {
    std::cout << "restart " << i << "\n";
    // Add the bp update to all vertices
    core.add_task_to_all(bp_update, 100.0);
    // Start the engine
    core.start();
    update_count += core.last_update_count();
    graphlab::binary_factor newedgepot = sh_edgepot.get_val();
    std::cout << newedgepot;
    if (binary_factor_equal(oldedgepot, newedgepot)) break;
    oldedgepot = newedgepot;
  }

  double runtime = ti.current_time();
  std::cout << "Finished Running engine in " << runtime
            << " seconds." << std::endl
            << "Total updates: " << update_count << std::endl
            << "Efficiency: " << (double(update_count) / runtime)
            << " updates per second "
            << std::endl;

  // Saving the output ----------------------------------------------------->
  std::cout << "Rendering the cleaned image. " << std::endl;
  if(pred_type == "map") {
    for(size_t v = 0; v < core.graph().num_vertices(); ++v) {
      const vertex_data& vdata = core.graph().vertex_data(v);
      img.pixel(v) = vdata.belief.max_asg();
    }
  } else if(pred_type == "exp") {
    for(size_t v = 0; v < core.graph().num_vertices(); ++v) {
      const vertex_data& vdata = core.graph().vertex_data(v);
      img.pixel(v) = vdata.belief.expectation();
    }
  } else {
    std::cout << "Invalid prediction type! : " << pred_type
              << std::endl;
    return EXIT_FAILURE;
  }
  std::cout << "Saving cleaned image. " << std::endl;
  img.save(pred_fn.c_str());

  std::cout << "Done!" << std::endl;
  return EXIT_SUCCESS;
} // End of main

// Implementations
// ============================================================>
void bp_update(gl_types::iscope& scope,
               gl_types::icallback& scheduler) {
  //  std::cout << scope.vertex();;
  //  std::getchar();
  double bound = sh_bound.get_val();
  double damping = sh_damping.get_val();

  // Grab the state from the scope
  // ---------------------------------------------------------------->
  // Get the vertex data
  vertex_data& v_data = scope.vertex_data();

  // Get the in and out edges by reference
  gl_types::edge_list in_edges = scope.in_edge_ids();
  gl_types::edge_list out_edges = scope.out_edge_ids();
  assert(in_edges.size() == out_edges.size()); // Sanity check

  // Flip the old and new messages to improve safety when using the
  // unsynch scope
  foreach(graphlab::edge_id_t ineid, in_edges) {
    // Get the in and out edge data
    edge_data& in_edge = scope.edge_data(ineid);
    // Since we are about to receive the current message make it the
    // old message
    in_edge.old_message = in_edge.message;
  }

  // Compute the belief
  // ---------------------------------------------------------------->
  // Initialize the belief as the value of the factor
  v_data.belief = v_data.potential;
  foreach(graphlab::edge_id_t ineid, in_edges) {
    // Get the message
    const edge_data& e_data = scope.edge_data(ineid);
    // Notice we now use the old message since neighboring vertices
    // could be changing the new messages
    v_data.belief.times( e_data.old_message );
  }
  v_data.belief.normalize(); // finally normalize the belief

  // Compute outbound messages
  // ---------------------------------------------------------------->
  boost::shared_ptr<const graphlab::binary_factor> edge_factor_ptr =
    sh_edgepot.get_ptr();

  // Send outbound messages
  graphlab::unary_factor cavity, tmp_msg;
  for(size_t i = 0; i < in_edges.size(); ++i) {
    // Get the edge ids
    graphlab::edge_id_t outeid = out_edges[i];
    graphlab::edge_id_t ineid = in_edges[i];
    // CLEVER HACK: Here we are expoiting the sorting of the edge ids
    // to do fast O(1) time edge reversal
    assert(scope.target(outeid) == scope.source(ineid));
    // Get the in and out edge data
    const edge_data& in_edge = scope.edge_data(ineid);
    edge_data& out_edge = scope.edge_data(outeid);

    // Compute cavity
    cavity = v_data.belief;
    cavity.divide(in_edge.old_message); // Make the cavity a cavity
    cavity.normalize();

    // convolve cavity with the edge factor storing the result in the
    // temporary message
    tmp_msg.resize(out_edge.message.arity());
    tmp_msg.var() = out_edge.message.var();
    tmp_msg.convolve(*edge_factor_ptr, cavity);
    tmp_msg.normalize();

    // Damp the message
    tmp_msg.damp(out_edge.message, damping);

    // Compute message residual
    double residual = tmp_msg.residual(out_edge.old_message);

    // Assign the out message
    out_edge.message = tmp_msg;

    if(residual > bound) {
      gl_types::update_task task(scope.target(outeid), bp_update);
      scheduler.add_task(task, residual);
    }
  }
} // end of BP_update

void edgepot_sync(gl_types::iscope &scope,  graphlab::any& acc) {
  gl_types::edge_list in_edges = scope.in_edge_ids();
  gl_types::edge_list out_edges = scope.out_edge_ids();
  assert(in_edges.size() == out_edges.size()); // Sanity check

  graphlab::binary_factor& counts = acc.as<graphlab::binary_factor>();

  // Get the in and out edge data
  // the edge belief of u -- v
  // belief of u / msg_{v->u) * belief of v / msg_{u->v} * edgepot;

  const graphlab::unary_factor& blfu = scope.const_vertex_data().belief;

  foreach(graphlab::edge_id_t ineid, in_edges) {

    graphlab::vertex_id_t srcv = scope.source(ineid);
    // message from v->u
    const graphlab::unary_factor &msgvu =
      scope.const_edge_data(ineid).message;
    // belief at v
    const graphlab::unary_factor& blfv=
      scope.const_neighbor_vertex_data(srcv).belief;
    // get the message from u->v. requires the reverse edge
    graphlab::edge_id_t outeid = scope.reverse_edge(ineid);
    const graphlab::unary_factor &msguv =
      scope.const_edge_data(outeid).message;

    boost::shared_ptr<const graphlab::binary_factor> edge_factor =
      sh_edgepot.get_ptr();

    graphlab::binary_factor edge_belief;
    edge_belief.resize(blfu.arity(), blfv.arity());
    // loop through my assignments and my neighbor assignments

    // using logP to store actual counts
    for (size_t i = 0;i < blfu.arity(); ++i) {
      for (size_t j = 0;j < blfv.arity(); ++j) {
        edge_belief.logP(i,j) =
          blfu.logP(i) - msgvu.logP(i) + blfv.logP(j) -
          msguv.logP(j) + edge_factor->logP(i,j);
      }
    }
    edge_belief.normalize();
    for (size_t i = 0;i < edge_belief.arity1(); ++i) {
      for (size_t j = 0;j < edge_belief.arity2(); ++j) {
        counts.logP(i,j) += std::exp(edge_belief.logP(i,j));
      }
    }

  }
}

void edgepot_merge(graphlab::any& result,  const graphlab::any& acc) {
  graphlab::binary_factor& res = result.as<graphlab::binary_factor>();
  const graphlab::binary_factor& a = acc.as<graphlab::binary_factor>();

  for (size_t i = 0;i < res.arity1(); ++i) {
    for (size_t j = 0;j < res.arity2(); ++j) {
      res.logP(i,j) += a.logP(i,j);
    }
  }
}

void edgepot_apply(graphlab::any& result,  const graphlab::any& acc) {
  // IPF update
  graphlab::binary_factor& res = result.as<graphlab::binary_factor>();
  graphlab::binary_factor truecounts = sh_truecounts.get_val();
  const graphlab::binary_factor& curcounts =
    acc.as<graphlab::binary_factor>();
  double ipfdamping = sh_ipfdamping.get_val();
  // perform the IPF update
  // note that BP+IPF can be quite unstable.
  // (We do recommend the gradient update in practice)
  // so lets only update the parameter values if they change by > 1E-1
  for (size_t i = 0;i < res.arity1(); ++i) {
    for (size_t j = 0;j < res.arity2(); ++j) {
      // + 100 to avoid divide by 0 problems
      double newval =
        ipfdamping * log((truecounts.logP(i,j)+100) /
                         (curcounts.logP(i,j)+100)) +
        (1 - ipfdamping) * res.logP(i,j);
      if (std::fabs(res.logP(i,j) - newval) >= 1E-1) {
        res.logP(i,j) = newval;
      }
    }
  }
  std::cout << "sync of edge pot!\n";
  std::cout << res << std::endl;
}

void construct_graph(image& img,
                     size_t num_rings,
                     double sigma,
                     gl_types::graph& graph) {
  // Construct a single blob for the vertex data
  vertex_data vdata;
  vdata.potential.resize(num_rings);
  vdata.belief.resize(num_rings);
  vdata.belief.uniform();
  vdata.potential.uniform();
  vdata.belief.normalize();
  vdata.potential.normalize();
  // Add all the vertices
  double sigmaSq = sigma*sigma;
  for(size_t i = 0; i < img.rows(); ++i) {
    for(size_t j = 0; j < img.cols(); ++j) {
      // initialize the potential and belief
      uint32_t pixel_id = img.vertid(i, j);
      vdata.potential.var() = vdata.belief.var() = pixel_id;
      // Set the node potential
      double obs = img.pixel(i, j);
      for(size_t pred = 0; pred < num_rings; ++pred) {
        vdata.potential.logP(pred) =
          -(obs - pred)*(obs - pred) / (2.0 * sigmaSq);
      }
      vdata.potential.normalize();
      // Store the actual data in the graph
      size_t vertid = graph.add_vertex(vdata);
      // Ensure that we are using a consistent numbering
      assert(vertid == img.vertid(i, j));
    } // end of for j in cols
  } // end of for i in rows

  // Add the edges
  edge_data edata;
  edata.message.resize(num_rings);
  edata.message.uniform();
  edata.message.normalize();
  edata.old_message = edata.message;

  for(size_t i = 0; i < img.rows(); ++i) {
    for(size_t j = 0; j < img.cols(); ++j) {
      size_t vertid = img.vertid(i,j);
      if(i-1 < img.rows()) {
        edata.message.var() = img.vertid(i-1, j);
        edata.old_message.var() = edata.message.var();
        graph.add_edge(vertid, img.vertid(i-1, j), edata);
      }
      if(i+1 < img.rows()) {
        edata.message.var() = img.vertid(i+1, j);
        edata.old_message.var() = edata.message.var();
        graph.add_edge(vertid, img.vertid(i+1, j), edata);
      }
      if(j-1 < img.cols()) {
        edata.message.var() = img.vertid(i, j-1);
        edata.old_message.var() = edata.message.var();
        graph.add_edge(vertid, img.vertid(i, j-1), edata);
      } if(j+1 < img.cols()) {
        edata.message.var() = img.vertid(i, j+1);
        edata.old_message.var() = edata.message.var();
        graph.add_edge(vertid, img.vertid(i, j+1), edata);
      }
    } // end of for j in cols
  } // end of for i in rows
  graph.finalize();
} // End of construct graph


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

/*
 Copyright (C) 2015 Peter Caspers

 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
 <http://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 <ql/experimental/math/mersennetwister_multithreaded.hpp>
#include <boost/make_shared.hpp>

#define MT_MT_DISPATCH(method)                                                 \
    switch (threadId) {                                                        \
    case 0:                                                                    \
        return m0_->method();                                                  \
    case 1:                                                                    \
        return m1_->method();                                                  \
    case 2:                                                                    \
        return m2_->method();                                                  \
    case 3:                                                                    \
        return m3_->method();                                                  \
    case 4:                                                                    \
        return m4_->method();                                                  \
    case 5:                                                                    \
        return m5_->method();                                                  \
    case 6:                                                                    \
        return m6_->method();                                                  \
    case 7:                                                                    \
        return m7_->method();                                                  \
    default:                                                                   \
        QL_FAIL("thread " << threadId << " out of range [0...7]");             \
    }

namespace QuantLib {

MersenneTwisterMultiThreaded::MersenneTwisterMultiThreaded(
    const unsigned long seed) {
    m0_ = boost::make_shared<MersenneTwisterCustomRng<Mtdesc19937_0> >(seed);
    m1_ = boost::make_shared<MersenneTwisterCustomRng<Mtdesc19937_1> >(seed);
    m2_ = boost::make_shared<MersenneTwisterCustomRng<Mtdesc19937_2> >(seed);
    m3_ = boost::make_shared<MersenneTwisterCustomRng<Mtdesc19937_3> >(seed);
    m4_ = boost::make_shared<MersenneTwisterCustomRng<Mtdesc19937_4> >(seed);
    m5_ = boost::make_shared<MersenneTwisterCustomRng<Mtdesc19937_5> >(seed);
    m6_ = boost::make_shared<MersenneTwisterCustomRng<Mtdesc19937_6> >(seed);
    m7_ = boost::make_shared<MersenneTwisterCustomRng<Mtdesc19937_7> >(seed);
}

MersenneTwisterMultiThreaded::sample_type
MersenneTwisterMultiThreaded::next(unsigned int threadId) const {
    MT_MT_DISPATCH(next);
}

Real MersenneTwisterMultiThreaded::nextReal(unsigned int threadId) const {
    MT_MT_DISPATCH(nextReal);
}

unsigned long MersenneTwisterMultiThreaded::
operator()(unsigned int threadId) const {
    MT_MT_DISPATCH(operator());
}

inline unsigned long
MersenneTwisterMultiThreaded::nextInt32(unsigned int threadId) const {
    MT_MT_DISPATCH(nextInt32);
}
}


// Copyright (c) 2007  INRIA Sophia-Antipolis (France).
// All rights reserved.
//
// This file is part of CGAL (www.cgal.org).
//
// $URL$
// $Id$
// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial
//
// Author(s)     : Laurent Rineau

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <stack>
#include <boost/tuple/tuple.hpp>
#include <boost/tuple/tuple_comparison.hpp>
#include <boost/format.hpp>

using std::cout;
using std::cin;
using std::endl;

int main()
{
  std::string header;
  cin >> header;

  if(header != "OFF")
  {
    std::cerr << "header is \"" << header << "\"\nshould be \"OFF\".\n";
    return 1;
  }

  cout << header << endl;
  unsigned int n_vertices;
  unsigned int n_facets;
  std::string dummy;

  cin >> n_vertices;
  cin >> n_facets;
  getline(cin, dummy);

  // Vector that maps from old vertex index to new vertex index,
  // as some (old) vertices may have identical point coordinates.
  std::vector<int> new_index(n_vertices);

  typedef boost::tuple<double, double, double> Point;
  // Vector that stores the points coordinates (in a Point).
  std::vector<Point> points(n_vertices);

  // Map that retains the mapping from the point coordinates (in a Point)
  // to the nex index.
  typedef std::map<Point, int> Renumber;
  Renumber renumber;

  unsigned int index = 0;
  for(unsigned int i = 0; i < n_vertices; ++i)
  {
    cin >> boost::get<0>(points[index])
        >> boost::get<1>(points[index]) >> boost::get<2>(points[index]);
    Renumber::const_iterator it = renumber.find(points[index]);
    if( it == renumber.end() )
    {
      renumber[points[index]] = index;
      new_index[i] = index;
      ++index;
    }
    else
    {
      new_index[i] = it->second;
    }
  }

  cout << index << " " << n_facets << dummy << endl;
  for(unsigned int i = 0; i < index; ++i)
  {
    cout << boost::get<0>(points[i]) << " "
         << boost::get<1>(points[i]) << " "
         << boost::get<2>(points[i]) << "\n";
  }

  // Vector that stores each facet.
  typedef std::vector<boost::tuple<int, int, int> > Facets;
  Facets facets;

  // For each (oriented) edge, that map stores a vector of adjacent facets,
  // with a boolean that tells if the edge is in the opposite orientation,
  // in the facet.
  // Edges are stores in the direction from the smallest index to the
  // greatest.
  typedef std::pair<int, int> Edge;
  typedef std::map<Edge, std::vector<std::pair<int, bool> > > Edges_map;
  Edges_map edges;

  // "nested function" opposite, that returns the edge, in the opposite
  // direction.
  struct {
    Edge operator()(Edge e) const {
      return std::make_pair(e.second, e.first);
    };
  } opposite;

  for(unsigned int i_facet = 0; i_facet < n_facets; ++i_facet)
  {
    // Read a facet, then reindex its vertices.
    int i, j, k;
    cin >> dummy >> i >> j >> k;
    if( dummy != "3" )
    {
      std::cerr << "In facet #" << i_facet << ", expected \"3\", found \""
                << dummy << "\"!\n";
      return 1;
    }
    i = new_index[i];
    j = new_index[j];
    k = new_index[k];
    facets.push_back(boost::make_tuple(i, j, k));

    // Create the three edges of the facet.
    Edge e[3];
    e[0] = std::make_pair(i, j);
    e[1] = std::make_pair(j, k);
    e[2] = std::make_pair(k, i);
    for(int i_edge = 0; i_edge < 3; ++i_edge)
    {
      if( e[i_edge].first < e[i_edge].second )
        edges[e[i_edge]].push_back(std::make_pair(i_facet, false));
      else
        edges[opposite(e[i_edge])].push_back(std::make_pair(i_facet, true));
    }
  }

  // Map that stores all already passed facet, and retains the orientation
  // of the facet. "true" means that the facet needs to be reoriented.
  std::map<int, bool> oriented_set;

  // Stack of facets indices to be handled.
  std::stack<int> stack;
  int seed_facet_candidate = 0;

  while (oriented_set.size() != n_facets) {
    // find a facet index that is not yet in 'oriented_set'.
    while( oriented_set.find(seed_facet_candidate) != oriented_set.end() )
      ++seed_facet_candidate;
    std::cerr << "Need seed facet: " << seed_facet_candidate << "\n";
    // push it in oriented set
    oriented_set[seed_facet_candidate] = false;
    stack.push(seed_facet_candidate);

    while(! stack.empty() ) {
      const int f = stack.top();
      stack.pop();
      const int i = boost::get<0>(facets[f]);
      const int j = boost::get<1>(facets[f]);
      const int k = boost::get<2>(facets[f]);
      Edge e[3];
      e[0] = std::make_pair(i, j);
      e[1] = std::make_pair(j, k);
      e[2] = std::make_pair(k, i);
      for(int ih = 0 ; ih < 3 ; ++ih)
      {
        bool f_orient = false;
        if(e[ih].first > e[ih].second) {
          f_orient = true;
          e[ih] = opposite(e[ih]);
        }

        Edges_map::iterator edge_it = edges.find(e[ih]);
        if(edge_it->second.size() == 2) { // regular edge
          int fn = edge_it->second[0].first;
          bool fn_orient = edge_it->second[0].second;
          if(fn == f)
          {
            fn = edge_it->second[1].first;
            fn_orient = edge_it->second[1].second;
          }
          if (oriented_set.find(fn) == oriented_set.end())
          {
            if(f_orient == fn_orient)
              oriented_set[fn] = ! oriented_set[f];
            else
              oriented_set[fn] = oriented_set[f];
            stack.push(fn);
          }
        } // end "if the edge is regular"
        else {
          std::cerr << boost::format("Irregular edge: (%1%,%2%)"
                                     ", %3% facets.\n")
            % e[ih].first % e[ih].second
            % edge_it->second.size();
        }
      } // end "for each neighbor of f"
    } // end "stack non empty"
  } // end "oriented_set not full"

  for(unsigned int i_facet = 0; i_facet < n_facets; ++i_facet)
  {
    const int i = boost::get<0>(facets[i_facet]);
    const int j = boost::get<1>(facets[i_facet]);
    const int k = boost::get<2>(facets[i_facet]);
    if(oriented_set[i_facet])
      cout << "3 " << j << " " << i << " " << k << "\n";
    else
      cout << "3 " << i << " " << j << " " << k << "\n";
  }
}


/*
 * Copyright (C) 2005-2017 Centre National d'Etudes Spatiales (CNES)
 *
 * This file is part of Orfeo Toolbox
 *
 *     https://www.orfeo-toolbox.org/
 *
 * 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 "itkNumericTraits.h"

#include "otbProspectModel.h"
#include "otb_boost_expint_header.h"
#include <boost/shared_ptr.hpp>
#include "otbMath.h"

//TODO check EPSILON matlab
#define EPSILON 0.0000000000000000000000001

namespace otb
{

/** Constructor */
ProspectModel
::ProspectModel()
{
   this->ProcessObject::SetNumberOfRequiredInputs(1);
   this->ProcessObject::SetNumberOfRequiredOutputs(2);

   SpectralResponseType::Pointer outputRefl = static_cast<SpectralResponseType *>(this->MakeOutput(0).GetPointer());
   this->itk::ProcessObject::SetNthOutput(0, outputRefl.GetPointer());

   SpectralResponseType::Pointer outputTrans = static_cast<SpectralResponseType *>(this->MakeOutput(1).GetPointer());
   this->itk::ProcessObject::SetNthOutput(1, outputTrans.GetPointer());
}

/** Destructor */
ProspectModel
::~ProspectModel()
{}

/** Set Input */
void
ProspectModel
::SetInput(const LeafParametersType * object)
{
   this->itk::ProcessObject::SetNthInput(0, const_cast<LeafParametersType *>(object));
}

/** Get Input */
ProspectModel::LeafParametersType *
ProspectModel
::GetInput()
{
   if(this->GetNumberOfInputs() != 1)
   {
      //exit
      return nullptr;
   }
   return static_cast<LeafParametersType *>(this->itk::ProcessObject::GetInput(0));
}

/** Make outputs */
ProspectModel::DataObjectPointer
ProspectModel
::MakeOutput(DataObjectPointerArraySizeType)
{
   return static_cast<itk::DataObject *>(SpectralResponseType::New().GetPointer());
}

/** Get Reflectance */
ProspectModel::SpectralResponseType *
ProspectModel
::GetReflectance()
{
   if(this->GetNumberOfOutputs() < 2)
   {
      //exit
      return nullptr;
   }
   return static_cast<SpectralResponseType *>(this->itk::ProcessObject::GetOutput(0));
}

/** Get Transmittance */
ProspectModel::SpectralResponseType *
ProspectModel
::GetTransmittance()
{
   if(this->GetNumberOfOutputs() < 2)
   {
      //exit
      return nullptr;
   }
   return static_cast<SpectralResponseType *>(this->itk::ProcessObject::GetOutput(1));
}

/** Set Parameters */
void
ProspectModel
::SetInput(const ParametersType & params)
{
//    m_Parameters = params;
   if(params.Size()!=6) itkExceptionMacro( << "Must have 6 parameters in that order : Cab, Car, CBrown, Cw, Cm, N" );
   LeafParametersType::Pointer leafParams = LeafParametersType::New();
   leafParams->SetCab(params[0]);
   leafParams->SetCar(params[1]);
   leafParams->SetCBrown(params[2]);
   leafParams->SetCw(params[3]);
   leafParams->SetCm(params[4]);
   leafParams->SetN(params[5]);

   this->itk::ProcessObject::SetNthInput(0, leafParams);
}

/** Plant Leaf Reflectance and Transmittance computation from 400nm to 2500 nm*/
void
ProspectModel
::GenerateData()
{

   LeafParametersType::Pointer leafParameters = this->GetInput();
   SpectralResponseType::Pointer outRefl = this->GetReflectance();
   SpectralResponseType::Pointer outTrans = this->GetTransmittance();

   unsigned int alpha=40;
   double lambda, n, k, trans, t12, temp, t21, r12, r21, x, y, ra, ta, r90, t90;
   double delta, beta, va, vb, vbNN, vbNNinv, vainv, s1, s2, s3, RN, TN;
   double N, Cab, Car, CBrown, Cw, Cm;

   N = leafParameters->GetN();
   Cab = leafParameters->GetCab();
   Car = leafParameters->GetCar();
   CBrown = leafParameters->GetCBrown();
   Cw = leafParameters->GetCw();
   Cm = leafParameters->GetCm();

   int nbdata = sizeof(DataSpecP5B) / sizeof(DataSpec);
   for (int i = 0; i < nbdata; ++i)
   {
      lambda = DataSpecP5B[i].lambda;
      n = DataSpecP5B[i].refLeafMatInd;

      k = Cab*DataSpecP5B[i].chlAbsCoef+Car*DataSpecP5B[i].carAbsCoef+CBrown*DataSpecP5B[i].brownAbsCoef+Cw*DataSpecP5B[i].waterAbsCoef;
      k = k + Cm*DataSpecP5B[i].dryAbsCoef;
      k = k / N;
      if(k == itk::NumericTraits<double>::ZeroValue() ) k=EPSILON;

      trans=(1.-k)*exp(-k)+k*k*boost::math::expint(1, k);

      t12 = this->Tav(alpha, n);
      temp = this->Tav(90, n);

      t21 = temp/(n*n);
      r12 = 1.-t12;
      r21 = 1.-t21;
      x = t12/temp;
      y = x*(temp-1)+1-t12;

      ra = r12+(t12*t21*r21*(trans*trans))/(1.-r21*r21*trans*trans);
      ta = (t12*t21*trans)/(1.-r21*r21*trans*trans);
      r90 = (ra-y)/x;
      t90 = ta/x;

      delta = (t90*t90-r90*r90-1.)*(t90*t90-r90*r90-1.) - 4.*r90*r90;
      if(delta < 0) delta = EPSILON;
      else delta=std::sqrt(delta);

      beta = (1.+r90*r90-t90*t90-delta)/(2.*r90);
      va=(1.+r90*r90-t90*t90+delta)/(2.*r90);
      if ((beta-r90)<=0)
         vb=std::sqrt(beta*(va-r90)/(va*EPSILON));
      else
         vb=std::sqrt(beta*(va-r90)/(va*(beta-r90)));

      vbNN = std::pow(vb, N-1.);
      vbNNinv = 1./vbNN;
      vainv = 1./va;
      s1=ta*t90*(vbNN-vbNNinv);
      s2=ta*(va-vainv);
      s3=va*vbNN-vainv*vbNNinv-r90*(vbNN-vbNNinv);

      RN=ra+s1/s3;
      TN=s2/s3;

      SpectralResponseType::PairType rrefl;
      SpectralResponseType::PairType ttrans;
      rrefl.first=lambda/1000.0;
      rrefl.second=RN;
      ttrans.first=lambda/1000.0;
      ttrans.second=TN;
      outRefl->GetResponse().push_back(rrefl);
      outTrans->GetResponse().push_back(ttrans);
   }
}

double
ProspectModel
::Tav(const int theta, double ref)
{

   double theta_rad = theta*CONST_PI/180;
   double r2, rp, rm, a, k, ds, k2, rm2, res, b1, b2, b;
   double ts, tp1, tp2, tp3, tp4, tp5, tp;

   r2=ref*ref;
   rp=r2+1;
   rm=r2-1;
   a=(ref+1)*(ref+1)/2;
   k=-(r2-1)*(r2-1)/4;
   ds=sin(theta_rad);

   k2=k*k;
   rm2=rm*rm;

   if(theta_rad==0) res=4*ref/((ref+1)*(ref+1));
   else
   {
      if(theta_rad==CONST_PI/2) b1=itk::NumericTraits<double>::ZeroValue();
      else b1=std::sqrt((ds*ds-rp/2)*(ds*ds-rp/2)+k);

      b2=ds*ds-rp/2;
      b=b1-b2;
      ts=(k2/(6*std::pow(b, 3))+k/b-b/2)-(k2/(6*std::pow(a, 3))+k/a-a/2);
      tp1=-2*r2*(b-a)/(rp*rp);
      tp2=-2*r2*rp*log(b/a)/rm2;
      tp3=r2*(1./b-1./a)/2;
      tp4=16*r2*r2*(r2*r2+1)*log((2*rp*b-rm2)/(2*rp*a-rm2))/(std::pow(rp, 3)*rm2);
      tp5=16*std::pow(r2, 3)*(1./(2*rp*b-rm2)-1./(2*rp*a-rm2))/std::pow(rp, 3);
      tp=tp1+tp2+tp3+tp4+tp5;
      res=(ts+tp)/(2*ds*ds);
   }
   return res;

}

void
ProspectModel
::PrintSelf(std::ostream& os, itk::Indent indent) const
{
   Superclass::PrintSelf(os, indent);

}
} // end namespace otb


// Copyright (c) 2020 CNES
//
// All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#pragma once
#include <array>
#include <complex>
#include <memory>
#include <string>
#include <vector>
#include <Eigen/Core>
#include "angle.hpp"
#include "astronomic_angle.hpp"
#include "math.hpp"

/// @brief Wave definition
class Wave : public std::enable_shared_from_this<Wave> {
 public:
  /// Typename to a function pointer for calculate the nodal factor
  using NodalFactor = double (AstronomicAngle::*)() const;

  /// @brief Possible type of tidal wave.
  enum TidalType {
    kLongPeriod = 0,  //!< Long period tidal waves
    kShortPeriod      //!< Short period tidal waves
  };

  /// @brief Index to access the wave in the internal table
  enum Ident : size_t {
    kMm = 0,        //!< %Mm
    kMf = 1,        //!< %Mf
    kMtm = 2,       //!< %Mtm
    kMsqm = 3,      //!< %Msqm
    k2Q1 = 4,       //!< 2Q₁
    kSigma1 = 5,    //!< σ₁
    kQ1 = 6,        //!< Q₁
    kRho1 = 7,      //!< ρ₁
    kO1 = 8,        //!< O₁
    kMP1 = 9,       //!< MP₁
    kM11 = 10,      //!< M₁₁
    kM12 = 11,      //!< M₁₂
    kM13 = 12,      //!< M₁₃
    kChi1 = 13,     //!< χ₁
    kPi1 = 14,      //!< π₁
    kP1 = 15,       //!< P₁
    kS1 = 16,       //!< S₁
    kK1 = 17,       //!< K₁
    kPsi1 = 18,     //!< ψ₁
    kPhi1 = 19,     //!< φ₁
    kTheta1 = 20,   //!< θ₁
    kJ1 = 21,       //!< J₁
    kOO1 = 22,      //!< OO₁
    kMNS2 = 23,     //!< MNS₂
    kEps2 = 24,     //!< ε₂
    k2N2 = 25,      //!< 2N₂
    kMu2 = 26,      //!< µ₂
    k2MS2 = 27,     //!< 2MS₂
    kN2 = 28,       //!< N₂
    kNu2 = 29,      //!< ν₂
    kM2 = 30,       //!< M₂
    kMKS2 = 31,     //!< MKS₂
    kLambda2 = 32,  //!< λ₂
    kL2 = 33,       //!< L₂
    k2MN2 = 34,     //!< 2MN₂
    kT2 = 35,       //!< T₂
    kS2 = 36,       //!< S₂
    kR2 = 37,       //!< R₂
    kK2 = 38,       //!< K₂
    kMSN2 = 39,     //!< MSN₂
    kEta2 = 40,     //!< η₂
    k2SM2 = 41,     //!< 2SM₂
    kMO3 = 42,      //!< MO₃
    k2MK3 = 43,     //!< 2MK₃
    kM3 = 44,       //!< M₃
    kMK3 = 45,      //!< MK₃
    kN4 = 46,       //!< N₄
    kMN4 = 47,      //!< MN₄
    kM4 = 48,       //!< M₄
    kSN4 = 49,      //!< SN₄
    kMS4 = 50,      //!< MS₄
    kMK4 = 51,      //!< MK₄
    kS4 = 52,       //!< S₄
    kSK4 = 53,      //!< SK₄
    kR4 = 54,       //!< R₄
    k2MN6 = 55,     //!< 2MN₆
    kM6 = 56,       //!< M₆
    kMSN6 = 57,     //!< MSN₆
    k2MS6 = 58,     //!< 2MS₆
    k2MK6 = 59,     //!< 2MK₆
    k2SM6 = 60,     //!< 2SM₆
    kMSK6 = 61,     //!< MSK₆
    kS6 = 62,       //!< S₆
    kM8 = 63,       //!< %M8
    kMSf = 64,      //!< %MSf
    kSsa = 65,      //!< %Ssa
    kSa = 66,       //!< %Sa
  };

 protected:
  /// nodal correction for phase
  double u_{std::numeric_limits<double>::quiet_NaN()};

 private:
  /// Wave ident
  Ident ident_;

  /// Type of tide.
  TidalType type_;

  /// Function to call for computing the node factor
  NodalFactor calculate_node_factor_;

  /// Wave frequency.
  double freq_;

  /// greenwich argument
  double v_{std::numeric_limits<double>::quiet_NaN()};

  /// Nodal correction for amplitude.
  double f_{std::numeric_limits<double>::quiet_NaN()};

  /// Harmonic constituents (T, s, h, p, N′, p₁, shift, ξ, ν, ν′, ν″)
  std::array<int16_t, 11> argument_;

  /// Computes the wave frequency from the doodson arguments
  ///
  /// @param t Mean solar angle relative to Greenwich
  /// @param s moon's mean longitude
  /// @param h sun's mean longitude
  /// @param p longitude of moon's perigee
  /// @param n longitude of moon's ascending node
  /// @param p1 longitude of sun's perigee
  static constexpr double frequency(const int16_t t, const int16_t s,
                                    const int16_t h, const int16_t p,
                                    const int16_t n, const int16_t p1) {
    return ((frequency::tau() + frequency::s() - frequency::h()) * t +
            frequency::s() * s + frequency::h() * h + frequency::p() * p +
            frequency::n() * n + frequency::p1() * p1) *
           360;
  }

 public:
  /// Initializes the properties of the wave (frequency, doodson's coefficients,
  /// etc.).
  ///
  /// @param ident Index of the wave in the internal table
  /// @param t Mean solar angle relative to Greenwich
  /// @param s moon's mean longitude
  /// @param h sun's mean longitude
  /// @param p longitude of moon's perigee
  /// @param n longitude of moon's ascending node
  /// @param p1 longitude of sun's perigee
  /// @param shift TODO
  /// @param eps Coefficient for the longitude in moon's orbit of lunar
  ///   intersection
  /// @param nu Coefficient for the right ascension of lunar intersection
  /// @param nuprim Coefficient for the term in argument of lunisolar
  ///   constituent K₁
  /// @param nusec Coefficient for the term in argument of lunisolar constituent
  ///   K₂
  /// @param type Type of tidal wave
  /// @param calculate_node_factor Function used to calculate the nodal factor
  Wave(const Ident ident, const int16_t t, const int16_t s, const int16_t h,
       const int16_t p, const int16_t n, const int16_t p1, const int16_t shift,
       const int16_t eps, const int16_t nu, const int16_t nuprim,
       const int16_t nusec, TidalType type, NodalFactor calculate_node_factor)
      : ident_(ident),
        type_(type),
        calculate_node_factor_(calculate_node_factor),
        freq_(radians(frequency(t, s, h, p, n, p1)) / 3600.0) {
    argument_[0] = t;
    argument_[1] = s;
    argument_[2] = h;
    argument_[3] = p;
    argument_[4] = n;
    argument_[5] = p1;
    argument_[6] = shift;
    argument_[7] = eps;
    argument_[8] = nu;
    argument_[9] = nuprim;
    argument_[10] = nusec;
  }

  /// Default destructor
  virtual ~Wave() = default;

  /// Default copy constructor
  Wave(const Wave&) = default;

  /// Default copy assignment operator
  Wave& operator=(const Wave&) = default;

  /// Move constructor
  Wave(Wave&&) noexcept = default;

  /// Move assignment operator
  Wave& operator=(Wave&&) noexcept = default;

  /// Compute nodal corrections from SCHUREMAN (1958).
  ///
  /// @param a Astronomic angle
  void nodal_a(const AstronomicAngle& a) { f_ = (a.*calculate_node_factor_)(); }

  /// Compute nodal corrections from SCHUREMAN (1958).
  ///
  /// @param a Astronomic angle
  virtual void nodal_g(const AstronomicAngle& a);

  /// Gets the wave ident
  constexpr Ident ident() const noexcept { return ident_; }

  /// Gets the wave frequency (radians per seconds)
  constexpr double freq() const noexcept { return freq_; }

  /// Gets the wave type
  constexpr TidalType type() const noexcept { return type_; }

  /// Gets v (greenwich argument) + u (nodal correction for phase)
  double vu() const noexcept { return std::fmod(v_ + u_, two_pi<double>()); }

  /// Gets v0 (greenwich argument)
  double v() const noexcept { return v_; }

  /// Gets the nodal correction for amplitude
  constexpr double f() const noexcept { return f_; }

  /// Gets the nodal correction for phase
  constexpr double u() const noexcept { return u_; }

  /// Gets the wave name
  std::string name() const;
};

/// Mm
///
/// V = s - p;
/// u = 0;
/// f = f(Mm)
class Mm : public Wave {
 public:
  Mm()
      : Wave(kMm, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, kLongPeriod,
             &AstronomicAngle::f_mm) {}
};

/// Mf
///
/// V = 2s
/// u = -2ξ
/// f = f(Mf)
class Mf : public Wave {
 public:
  Mf()
      : Wave(kMf, 0, 2, 0, 0, 0, 0, 0, -2, 0, 0, 0, kLongPeriod,
             &AstronomicAngle::f_mf) {}
};

/// Mtm
///
/// V = 3s - p
/// u = -2ξ
/// f = f(Mf)
class Mtm : public Wave {
 public:
  Mtm()
      : Wave(kMtm, 0, 3, 0, -1, 0, 0, 0, -2, 0, 0, 0, kLongPeriod,
             &AstronomicAngle::f_mf) {}
};

/// Msqm
///
/// V = 4s - 2h
/// u = -2ξ
/// f = f(Mf)
class Msqm : public Wave {
 public:
  Msqm()
      : Wave(kMsqm, 0, 4, -2, 0, 0, 0, 0, -2, 0, 0, 0, kLongPeriod,
             &AstronomicAngle::f_mf) {}
};

/// Ssa
///
/// V = 2h
/// u = 0
/// f = 1
class Ssa : public Wave {
 public:
  Ssa()
      : Wave(kSsa, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, kLongPeriod,
             &AstronomicAngle::f_1) {}
};

/// Sa
///
/// V = h
/// u = 0
/// f = 1
class Sa : public Wave {
 public:
  Sa()
      : Wave(kSa, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, kLongPeriod,
             &AstronomicAngle::f_1) {}
};

/// 2Q₁
///
/// V = T - 4s + h + 2p + 90°
/// u = +2ξ - ν
/// f = f(O₁)
class _2Q1 : public Wave {
 public:
  _2Q1()
      : Wave(k2Q1, 1, -4, 1, 2, 0, 0, 1, 2, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_o1) {}
};

/// σ₁
///
/// V = T - 4s + 3h + 90°
/// u = +2ξ - ν
/// f = f(O₁)
class Sigma1 : public Wave {
 public:
  Sigma1()
      : Wave(kSigma1, 1, -4, 3, 0, 0, 0, 1, 2, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_o1) {}
};

/// Q₁
///
/// V = T - 3s + h + p + 90°
/// u = +2ξ - ν
/// f = f(O₁)
class Q1 : public Wave {
 public:
  Q1()
      : Wave(kQ1, 1, -3, 1, 1, 0, 0, 1, 2, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_o1) {}
};

/// ρ₁
///
/// V = T - 3s + 3h - p + 90°
/// u = +2ξ - ν
/// f = f(O₁)
class Rho1 : public Wave {
 public:
  Rho1()
      : Wave(kRho1, 1, -3, 3, -1, 0, 0, 1, 2, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_o1) {}
};

// O₁
///
/// V = T - 2s + h + 90°
/// u = +2ξ - ν
/// f = f(O₁)
///
class O1 : public Wave {
 public:
  O1()
      : Wave(kO1, 1, -2, 1, 0, 0, 0, 1, 2, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_o1) {}
};

/// MP₁
///
/// V = T - 2s + 3h - 90°
/// u = -ν
/// f = f(J₁)
class MP1 : public Wave {
 public:
  MP1()
      : Wave(kMP1, 1, -2, 3, 0, 0, 0, -1, 0, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_j1) {}
};

/// M₁₂ (Formula A16)
///
/// V = T - s + h - p - 90°
/// u = +2ξ - ν
/// f = f(O₁)
class M12 : public Wave {
 public:
  M12()
      : Wave(kM12, 1, -1, 1, -1, 0, 0, -1, 2, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_o1) {}
};

/// M₁₃ (= M11 + M12)
///
/// V = T - s + h + p - 90
/// u = -ν
/// f = f(M₁₃)
class M13 : public Wave {
 public:
  M13()
      : Wave(kM13, 1, -1, 1, 1, 0, 0, -1, 0, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m13) {}

  /// Compute nodal corrections from SCHUREMAN (1958).
  ///
  /// @param a Astronomic angle
  void nodal_g(const AstronomicAngle& a) final {
    Wave::nodal_g(a);
    u_ -= radians(1.0 /
                  std::sqrt(2.310 + 1.435 * std::cos(2 * (a.p() - a.xi()))));
  }
};

/// M₁₁ (Formula A23)
///
/// V = T - s + h + p - 90°
/// u = -ν
/// f = f(J₁)
class M11 : public Wave {
 public:
  M11()
      : Wave(kM11, 1, -1, 1, 1, 0, 0, -1, 0, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_j1) {}
};

/// χ₁
///
/// V = T - s + 3h - p - 90°
/// u = -ν
/// f = f(J₁)
class Chi1 : public Wave {
 public:
  Chi1()
      : Wave(kChi1, 1, -1, 3, -1, 0, 0, -1, 0, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_j1) {}
};

/// π₁
///
/// V = T - 2h + p1 + 90°
/// u = 0
/// f = 1
class Pi1 : public Wave {
 public:
  Pi1()
      : Wave(kPi1, 1, 0, -2, 0, 0, 1, 1, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// P₁
///
/// V = T - h + 90°
/// u = 0
/// f = 1
class P1 : public Wave {
 public:
  P1()
      : Wave(kP1, 1, 0, -1, 0, 0, 0, 1, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// S₁
///
/// V = T
/// u = 0
/// f = 1
class S1 : public Wave {
 public:
  S1()
      : Wave(kS1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// K₁
///
/// V = T + h - 90°
/// u = - ν'
/// f = f(k₁)
class K1 : public Wave {
 public:
  K1()
      : Wave(kK1, 1, 0, 1, 0, 0, 0, -1, 0, 0, -1, 0, kShortPeriod,
             &AstronomicAngle::f_k1) {}
};

/// ψ₁
///
/// V = T + 2h - p1 - 90°
/// u = 0
/// f = 1
class Psi1 : public Wave {
 public:
  Psi1()
      : Wave(kPsi1, 1, 0, 2, 0, 0, -1, -1, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// φ₁
///
/// V = T + 3h - 90°
/// u = 0
/// f = 1
class Phi1 : public Wave {
 public:
  Phi1()
      : Wave(kPhi1, 1, 0, 3, 0, 0, 0, -1, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// θ₁
///
/// V = T + s - h + p - 90°
/// u = -ν
/// f = f(J₁)
class Theta1 : public Wave {
 public:
  Theta1()
      : Wave(kTheta1, 1, 1, -1, 1, 0, 0, -1, 0, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_j1) {}
};

/// J₁
///
/// V = T + s + h - p - 90°
/// u = -ν
/// f = f(J₁)
class J1 : public Wave {
 public:
  J1()
      : Wave(kJ1, 1, 1, 1, -1, 0, 0, -1, 0, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_j1) {}
};

/// OO₁
///
/// V = T + 2s + h - 90°
/// u = -2ξ - ν
/// f = f(OO₁)
class OO1 : public Wave {
 public:
  OO1()
      : Wave(kOO1, 1, 2, 1, 0, 0, 0, -1, -2, -1, 0, 0, kShortPeriod,
             &AstronomicAngle::f_oo1) {}
};

/// MNS₂ = M₂ + N₂ + S₂
///
/// V = 2T - 5s + 4h + p
/// u = +4ξ - 4ν
/// f = f(M₂)²
class MNS2 : public Wave {
 public:
  MNS2()
      : Wave(kMNS2, 2, -5, 4, 1, 0, 0, 0, 4, -4, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m22) {}
};

/// ε₂
///
/// V = 2T - 5s + 4h + p
/// u = +2ξ - 2ν
/// f = f(M₂)
class Eps2 : public Wave {
 public:
  Eps2()
      : Wave(kEps2, 2, -5, 4, 1, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// 2N₂
///
/// V = 2T - 4s + 2h + 2p
/// u = +2ξ - 2ν
/// f = f(M₂)
class _2N2 : public Wave {
 public:
  _2N2()
      : Wave(k2N2, 2, -4, 2, 2, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// µ₂
///
/// V = 2T - 4s + 4h
/// u = +2ξ - 2ν
/// f = f(M₂)
class Mu2 : public Wave {
 public:
  Mu2()
      : Wave(kMu2, 2, -4, 4, 0, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// 2MS₂ = 2M₂ - S₂
///
/// V = 2T - 4s + 4h
/// u = +4ξ - 4ν
/// f = f(M₂)²
class _2MS2 : public Wave {
 public:
  _2MS2()
      : Wave(k2MS2, 2, -4, 4, 0, 0, 0, 0, 4, -4, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m22) {}
};

/// N₂
///
/// V = 2T - 3s + 2h + p
/// u = +2ξ - 2ν
/// f = f(M₂)
class N2 : public Wave {
 public:
  N2()
      : Wave(kN2, 2, -3, 2, 1, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// ν₂
///
/// V = 2T - 3s + 4h - p
/// u = +2ξ - 2ν
/// f = f(M₂)
class Nu2 : public Wave {
 public:
  Nu2()
      : Wave(kNu2, 2, -3, 4, -1, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// M₂
///
/// V = 2T - 2s + 2h
/// u = +2ξ - 2ν
/// f = f(M₂)
class M2 : public Wave {
 public:
  M2()
      : Wave(kM2, 2, -2, 2, 0, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// MKS₂ = M₂ + K₂ - S₂
///
/// V = 2T - 2s + 4h
/// u = +2ξ - 2ν -2ν''
/// f = f(M₂) × f(K₂)
class MKS2 : public Wave {
 public:
  MKS2()
      : Wave(kMKS2, 2, -2, 4, 0, 0, 0, 0, 2, -2, 0, -2, kShortPeriod,
             &AstronomicAngle::f_m2_k2) {}
};

/// λ₂
///
/// V = 2T - s + p + 180°
/// u = +2ξ - 2ν
/// f = f(M₂)
class Lambda2 : public Wave {
 public:
  Lambda2()
      : Wave(kLambda2, 2, -1, 0, 1, 0, 0, 2, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// L₂
///
/// V = 2T - s + 2h - p + 180°
/// u = +2ξ - 2ν - R
/// f = f(L₂)
class L2 : public Wave {
 public:
  L2()
      : Wave(kL2, 2, -1, 2, -1, 0, 0, 2, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_l2) {}

  /// Compute nodal corrections from SCHUREMAN (1958).
  ///
  /// @param a Astronomic angle
  void nodal_g(const AstronomicAngle& a) final {
    Wave::nodal_g(a);
    u_ -= a.r();
  }
};

/// 2MN₂ = 2M₂ - N₂
///
/// V = 2T - s + 2h - p + 180°
/// u = +2ξ - 2ν
/// f = f(M₂)³
class _2MN2 : public Wave {
 public:
  _2MN2()
      : Wave(k2MN2, 2, -1, 2, -1, 0, 0, 2, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m23) {}
};

/// T₂
///
/// V = 2T - h + p₁
/// u = 0
/// f = 1
class T2 : public Wave {
 public:
  T2()
      : Wave(kT2, 2, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// S₂
///
/// V = 2T
/// u = 0
/// f = 1
class S2 : public Wave {
 public:
  S2()
      : Wave(kS2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// R₂
///
/// V = 2T + h - p1 + 180°
/// u = 0
/// f = 1
class R2 : public Wave {
 public:
  R2()
      : Wave(kR2, 2, 0, 1, 0, 0, -1, 2, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// K₂
///
/// V = 2T + 2h
/// u = -2ν″
/// f = f(K₂)
class K2 : public Wave {
 public:
  K2()
      : Wave(kK2, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, -2, kShortPeriod,
             &AstronomicAngle::f_k2) {}
};

/// MSN₂ = M2 + S2 - N2
///
/// V = 2T + s -p
/// u = 0
/// f = f(M₂)²
class MSN2 : public Wave {
 public:
  MSN2()
      : Wave(kMSN2, 2, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m22) {}
};

/// η₂ = KJ₂
///
/// V = 2T + s + 2h - p
/// u = -2ν
/// f = f(KJ₂)
class Eta2 : public Wave {
 public:
  Eta2()
      : Wave(kEta2, 2, 1, 2, -1, 0, 0, 0, 0, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_kj2) {}
};

/// 2SM₂ = 2S₂ - M₂
///
/// V = 2T + 2s - 2h
/// u = -2ξ + 2ν
/// f = f(M₂)
class _2SM2 : public Wave {
 public:
  _2SM2()
      : Wave(k2SM2, 2, 2, -2, 0, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// MO₃ = M₂ + O₁
///
/// V = 3T - 4s + 3h + 90°
/// u = 4ξ - 3ν
/// f = f(M₂) × f(O₁)
class MO3 : public Wave {
 public:
  MO3()
      : Wave(kMO3, 3, -4, 3, 0, 0, 0, 1, 4, -3, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2_o1) {}
};

/// 2MK₃ = 2M₂ - K₁
///
/// V = 3T - 4s + 3h + 90°
/// u = 4ξ - 4ν + ν′
/// f = f(M₂)² × f(K₁)
class _2MK3 : public Wave {
 public:
  _2MK3()
      : Wave(k2MK3, 3, -4, 3, 0, 0, 0, 1, 4, -4, 1, 0, kShortPeriod,
             &AstronomicAngle::f_m22_k1) {}
};

/// M₃
///
/// V = 3T - 3s + 3h
/// u = +3ξ - 3ν
/// f = f(M₃)
class M3 : public Wave {
 public:
  M3()
      : Wave(kM3, 3, -3, 3, 0, 0, 0, 0, 3, -3, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m3) {}
};

/// MK₃ = M₂ + K₁
///
/// V = 3T - 2s + 3h - 90°
/// u = 2ξ - 2ν - ν′
/// f = f(M₂) × f(K₁)
class MK3 : public Wave {
 public:
  MK3()
      : Wave(kMK3, 3, -2, 3, 0, 0, 0, -1, 2, -2, -1, 0, kShortPeriod,
             &AstronomicAngle::f_m2_k1) {}
};

/// N4 = N₂ + N₂
///
/// V = 4T - 6s + 4h + 2p
/// u = +4ξ - 4ν
/// f = f(M₂)²
class N4 : public Wave {
 public:
  N4()
      : Wave(kN4, 4, -6, 4, 2, 0, 0, 0, 4, -4, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m22) {}
};

/// MN₄ = M₂ + N₂
///
/// V = 4T - 5s + 4h + p
/// u = +4ξ - 4ν
/// f = f(M₂)²
class MN4 : public Wave {
 public:
  MN4()
      : Wave(kMN4, 4, -5, 4, 1, 0, 0, 0, 4, -4, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m22) {}
};

/// M₄ = 2M₂
///
/// V = 4T - 4s + 4h
/// u = +4ξ - 4ν
/// f = f²(M₂)
class M4 : public Wave {
 public:
  M4()
      : Wave(kM4, 4, -4, 4, 0, 0, 0, 0, 4, -4, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m22) {}
};

/// SN₄ = S₂ + N₂
///
/// V = 4T - 3s + 2h + p
/// u = 2ξ - 2ν
/// f = f(M₂)
class SN4 : public Wave {
 public:
  SN4()
      : Wave(kSN4, 4, -3, 2, 1, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// MS₄ = M₂ + S₂
///
/// V = 4T - 2s + 2h
/// u = +2ξ - 2ν
/// f = f(M₂)
class MS4 : public Wave {
 public:
  MS4()
      : Wave(kMS4, 4, -2, 2, 0, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// MK₄ = M₂ + K₂
///
/// V = 4T - 2s + 4h
/// u = 2ξ - 2ν - 2ν''
/// f = f(MK₄)
class MK4 : public Wave {
 public:
  MK4()
      : Wave(kMK4, 4, -2, 4, 0, 0, 0, 0, 2, -2, -2, 0, kShortPeriod,
             &AstronomicAngle::f_m2_k2) {}
};

/// S₄ = S₂ + S₂
///
/// V = 4T
/// u = 0
/// f = 1
class S4 : public Wave {
 public:
  S4()
      : Wave(kS4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// SK₄ = S₂ + K₂
///
/// V = 4T + 2h
/// u = -2ν''
/// f = f(K₂)
class SK4 : public Wave {
 public:
  SK4()
      : Wave(kSK4, 4, 0, 2, 0, 0, 0, 0, 0, 0, 0, -2, kShortPeriod,
             &AstronomicAngle::f_k2) {}
};

/// R₄ = R₂ + R₂
///
/// V = 4T + 2h - 2p1
/// u = 0
/// f = 1
class R4 : public Wave {
 public:
  R4()
      : Wave(kR4, 4, 0, 2, 0, 0, -2, 0, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// 2MN₆ = 2M₂ + N₂
///
/// V = 6T - 7s + 6h + p
/// u = 6ξ - 6ν
/// f = f(M₂)³
class _2MN6 : public Wave {
 public:
  _2MN6()
      : Wave(k2MN6, 6, -7, 6, 1, 0, 0, 0, 6, -6, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m23) {}
};

/// M₆ = 3M₂
///
/// V = 6T - 6s + 6h
/// u = +6ξ - 6ν
/// f = f(M₂)³
class M6 : public Wave {
 public:
  M6()
      : Wave(kM6, 6, -6, 6, 0, 0, 0, 0, 6, -6, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m23) {}
};

/// MSN₆ = M₂ + S₂ + N₂
///
/// V = 6T - 5s + 4h + p
/// u = 4ξ - 4ν
/// f = f(M₂)²
class MSN6 : public Wave {
 public:
  MSN6()
      : Wave(kMSN6, 6, -5, 4, 1, 0, 0, 0, 4, -4, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m22) {}
};

/// 2MS₆ = 2M₂ + S₂
///
/// V = 6T - 4s + 4h
/// u = 4ξ - 4ν
/// f = f(M₂)²
class _2MS6 : public Wave {
 public:
  _2MS6()
      : Wave(k2MS6, 6, -4, 4, 0, 0, 0, 0, 4, -4, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m22) {}
};

/// 2MK₆ = 2M₂ + K₂
///
/// V = 6T - 4s + 6h
/// u = 4ξ - 4ν - 2ν''
/// f = f(M₂)² × f(K₂)
class _2MK6 : public Wave {
 public:
  _2MK6()
      : Wave(k2MK6, 6, -4, 6, 0, 0, 0, 0, 4, -4, 0, -2, kShortPeriod,
             &AstronomicAngle::f_m23_k2) {}
};

/// 2SM₆ = 2S₂ + M₂
///
/// V = 6T - 2s + 2h
/// u = 2ξ - 2ν
/// f = f(M₂)
class _2SM6 : public Wave {
 public:
  _2SM6()
      : Wave(k2SM6, 6, -2, 2, 0, 0, 0, 0, 2, -2, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m2) {}
};

/// MSK₆ = M₂ + K₂ + S₂
///
/// V = 6T - 2s + 4h
/// u = 2ξ - 2ν - 2ν''
/// f = f(M₂) × f(K₂)
class MSK6 : public Wave {
 public:
  MSK6()
      : Wave(kMSK6, 6, -2, 4, 0, 0, 0, 0, 2, -2, -2, 0, kShortPeriod,
             &AstronomicAngle::f_m2_k2) {}
};

/// S₆ = 3S₂
///
/// V = 6T
/// u = 0
/// f = 1
class S6 : public Wave {
 public:
  S6()
      : Wave(kS6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, kShortPeriod,
             &AstronomicAngle::f_1) {}
};

/// M₈ = 4M₂
///
/// V = 8T - 8s + 8h
/// u = 8ξ - 8ν
/// f = f(M₂)⁴
class M8 : public Wave {
 public:
  M8()
      : Wave(kM8, 8, -8, 8, 0, 0, 0, 0, 8, -8, 0, 0, kShortPeriod,
             &AstronomicAngle::f_m24) {}
};

/// MSf = M₂ - S₂
///
/// V = 2s - 2h
/// u = 2ξ - 2ν
/// f = f(M₂) * f(S2) = f(M₂)
///
/// @warning Same frequency as MSf LP : 2s -2h
class MSf : public Wave {
 public:
  MSf()
      : Wave(kMSf, 0, 2, -2, 0, 0, 0, 0, 2, -2, 0, 0, kLongPeriod,
             &AstronomicAngle::f_m2) {}
};

/// Properties of tide waves computed
class WaveTable {
 private:
  std::vector<std::shared_ptr<Wave>> waves_{};

  /// Compute nodal corrections from SCHUREMAN (1958).
  ///
  /// Indexes used in this routine are internal to the code
  /// and corresponds to the "original" ondes.dat file.
  ///
  /// @param a Astronomic angle
  void nodal_a(const AstronomicAngle& a) {
    for (auto& item : waves_) {
      item->nodal_a(a);
    }
  }

  /// Compute nodal corrections from SCHUREMAN (1958).
  ///
  /// Indexes used in this routine are internal to the code and corresponds to
  /// the "original" ondes.dat file.
  ///
  /// @param a Astronomic angle
  void nodal_g(const AstronomicAngle& a) {
    for (auto& item : waves_) {
      item->nodal_g(a);
    }
  }

 public:
  /// Default constructor
  WaveTable(const std::vector<std::string>& waves = {});

  /// Gets the tidal waves known
  static std::vector<std::string> known_constituents();

  /// Compute nodal corrections.
  ///
  /// @param epoch Desired UTC time expressed in number of seconds elapsed since
  /// 1970-01-01T00:00:00
  /// @return the astronomic angle, indicating the date on which the tide is to
  /// be calculated.
  AstronomicAngle compute_nodal_corrections(const double epoch) {
    auto angles = AstronomicAngle(epoch);

    nodal_a(angles);
    nodal_g(angles);

    return angles;
  }

  /// Gets the wave properties
  std::shared_ptr<Wave> wave(const Wave::Ident ident) const {
    auto it = find(ident);
    return it != end() ? *it : nullptr;
  }

  /// Gets the wave properties
  const std::shared_ptr<Wave>& operator[](const size_t index) const {
    return waves_.at(index);
  }

  /// Gets the wave properties
  std::shared_ptr<Wave> wave(const std::string& ident) const {
    auto it = find(ident);
    return it != end() ? *it : nullptr;
  }

  /// Gets the wave properties
  std::shared_ptr<Wave>& wave(const Wave::Ident ident) { return waves_[ident]; }

  /// Returns an iterator to the beginning of the wave table
  std::vector<std::shared_ptr<Wave>>::const_iterator begin() const {
    return waves_.begin();
  }

  /// Returns an iterator to the end of the wave table
  std::vector<std::shared_ptr<Wave>>::const_iterator end() const {
    return waves_.end();
  }

  /// Returns an iterator to the beginning of the wave table
  std::vector<std::shared_ptr<Wave>>::iterator begin() {
    return waves_.begin();
  }

  /// Returns an iterator to the end of the wave table
  std::vector<std::shared_ptr<Wave>>::iterator end() { return waves_.end(); }

  /// Searches the properties of a wave from its name.
  std::vector<std::shared_ptr<Wave>>::const_iterator find(
      const std::string& name) const {
    for (auto it = begin(); it != end(); ++it) {
      if (name == (*it)->name()) {
        return it;
      }
    }
    return end();
  }

  /// Searches the properties of a wave from its identifier.
  std::vector<std::shared_ptr<Wave>>::const_iterator find(
      const Wave::Ident& ident) const {
    for (auto it = begin(); it != end(); ++it) {
      if (ident == (*it)->ident()) {
        return it;
      }
    }
    return end();
  }

  /// Returns the size of the table
  size_t size() const { return waves_.size(); }

  static Eigen::VectorXcd harmonic_analysis(
      const Eigen::Ref<const Eigen::VectorXd>& h,
      const Eigen::Ref<const Eigen::MatrixXd>& f,
      const Eigen::Ref<const Eigen::MatrixXd>& vu);
};