//---------------------------------------------------------------------------//
//  MIT License
//
//  Copyright (c) 2020-2021 Mikhail Komarov <nemo@nil.foundation>
//  Copyright (c) 2020-2021 Nikita Kaskov <nemo@nil.foundation>

//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//---------------------------------------------------------------------------//

#ifndef FILECOIN_STORAGE_PROOFS_POREP_STACKED_VANILLA_PARAMS_HPP
#define FILECOIN_STORAGE_PROOFS_POREP_STACKED_VANILLA_PARAMS_HPP

#include <array>
#include <string>

#include <boost/filesystem/path.hpp>
#include <boost/log/trivial.hpp>

#include <nil/crypto3/hash/sha2.hpp>
#include <nil/crypto3/hash/algorithm/hash.hpp>

#include <nil/filecoin/storage/proofs/porep/stacked/vanilla/challenges.hpp>
#include <nil/filecoin/storage/proofs/porep/stacked/vanilla/column_proof.hpp>
#include <nil/filecoin/storage/proofs/porep/stacked/vanilla/labelling_proof.hpp>
#include <nil/filecoin/storage/proofs/porep/stacked/vanilla/encoding_proof.hpp>

#include <nil/filecoin/storage/proofs/core/merkle/proof.hpp>
#include <nil/filecoin/storage/proofs/core/merkle/builders.hpp>

namespace nil {
    namespace filecoin {
        namespace stacked {
            namespace vanilla {

                /*************************  Pre-defined values  ***********************************/

                constexpr static const std::size_t BINARY_ARITY = 2;
                constexpr static const std::size_t QUAD_ARITY = 4;
                constexpr static const std::size_t OCT_ARITY = 8;

                /*************************  SetupParams  ***********************************/

                struct SetupParams {
                    // Number of nodes
                    std::size_t nodes;

                    // Base degree of DRG
                    std::size_t degree;

                    std::size_t expansion_degree;

                    std::array<std::uint8_t, 32> porep_id;
                    LayerChallenges layer_challenges;
                };

                /*************************  PublicParams  ***********************************/

                template<typename MerkleTreeType>
                struct PublicParams {
                    typedef MerkleTreeType tree_type;
                    typedef typename tree_type::hash_type hash_type;

                    std::string identifier() {
                        return std::string("layered_drgporep::PublicParams{{ graph:") +
                               std::to_string(graph.identifier()) +
                               ", challenges: " + std::to_string(layer_challenges) +
                               ", tree: " + std::to_string(MerkleTreeType::display()) + "}}";
                    }

                    StackedBucketGraph<hash_type> graph;
                    LayerChallenges layer_challenges;
                };

                /*************************  Tau  ***********************************/

                /// Tau for a single parition.
                template<typename DDomain, typename EDomain>
                struct Tau {
                    EDomain comm_d;
                    DDomain comm_r;
                };

                /*************************  PersistentAux  ***********************************/

                /// Stored along side the sector on disk.
                template<typename D>
                struct PersistentAux {
                    D comm_c;
                    D comm_r_last;
                };

                /*************************  ???  ***********************************/

                typedef std::function<void(const StoreConfig &, std::size_t, std::size_t)> VerifyCallback;

                /*************************  Labels  ***********************************/

                template<typename MerkleTreeType>
                struct Labels {
                    Labels(const std::vector<StoreConfig> &labels) : labels(labels) {
                    }

                    void verify_stores(VerifyCallback callback, const boost::filesystem::path &cache_dir) {
                        std::vector<StoreConfig> updated_path_labels = labels;
                        std::size_t required_configs = get_base_tree_count<MerkleTreeType>();
                        for (const auto &label : updated_path_labels) {
                            label.path = cache_dir;
                            callback(label, BINARY_ARITY, required_configs);
                        }
                    }

                    DiskStore<typename MerkleTreeType::hash_type::digest_type> labels_for_layer(std::size_t layer) {
                        BOOST_ASSERT_MSG(layer != 0, "Layer cannot be 0");
                        BOOST_ASSERT_MSG(
                            layer <= layers(),
                            std::format("Layer {%d} is not available (only {%d} layers available)", layer, layers()));

                        std::size_t row_index = layer - 1;
                        StoreConfig config = labels[row_index];
                        assert(config.size.is_some());

                        DiskStore::new_from_disk(config.size, MerkleTreeType::base_arity, config);
                    }

                    /// Returns label for the last layer.
                    DiskStore<typename MerkleTreeType::hash_type::digest_type> labels_for_last_layer() {
                        return labels_for_layer(labels.size() - 1);
                    }

                    /// How many layers are available.
                    std::size_t layers() const {
                        return labels.size();
                    }

                    /// Build the column for the given node.
                    Column<typename MerkleTreeType::hash_type> column(std::uint32_t node) {
                        std::vector<typename MerkleTreeType::hash_type::digest_type> rows;

                        for (const auto &label : labels) {
                            assert(label.size());
                            DiskStore store = DiskStore::new_from_disk(label.size, MerkleTreeType::base_arity, label);
                            rows.push_back(store.read_at(node));
                        }

                        return Column<typename MerkleTreeType::hash_type>(node, rows);
                    }

                    /// Update all configs to the new passed in root cache path.
                    void update_root(const boost::filesystem::path &root) {
                        for (const auto &config : labels) {
                            config.path = root;
                        }
                    }

                    std::vector<StoreConfig> labels;
                };

                /*************************  TemporaryAux  ***********************************/

                template<typename MerkleTreeType, typename Hash>
                struct TemporaryAux {
                    void set_cache_path(const boost::filesystem::path &cache_path) {
                        for (labels::iterator label = labels.begin(); label != labels.end(); ++label) {
                            (*label).path = cache_path;
                        }
                        tree_d_config.path = cache_path;
                        tree_r_last_config.path = cache_path;
                        tree_c_config.path = cache_path;
                    }

                    DiskStore<typename MerkleTreeType::hash_type::digest_type> labels_for_layer(std::size_t layer) {
                        return labels.labels_for_layer(layer);
                    }

                    typename MerkleTreeType::hash_type::digest_type domain_node_at_layer(std::size_t layer,
                                                                                std::uint32_t node_index) {
                        return labels_for_layer(layer).read_at(node_index);
                    }

                    Column<typename MerkleTreeType::hash_type> column(std::uint32_t column_index) {
                        return labels.column(column_index);
                    }

                    // 'clear_temp' will discard all persisted merkle and layer data
                    // that is no longer required.
                    void clear_temp(TemporaryAux<MerkleTreeType, Hash> t_aux) {

                        const auto delete_tree_c_store = [&](const StoreConfig &config, std::size_t tree_c_size) {
                            DiskStore<typename MerkleTreeType::hash_type::digest_type> tree_c_store =
                                DiskStore<typename MerkleTreeType::hash_type::digest_type>::new_from_disk(
                                    tree_c_size, MerkleTreeType::base_arity, config);
                            // Note: from_data_store requires the base tree leaf count
                            DiskTree<typename MerkleTreeType::hash_type, MerkleTreeType::base_arity,
                                     MerkleTreeType::sub_tree_arity, MerkleTreeType::top_tree_arity>
                                tree_c = DiskTree<typename MerkleTreeType::hash_type, MerkleTreeType::base_arity,
                                                  MerkleTreeType::sub_tree_arity, MerkleTreeType::top_tree_arity>::
                                    from_data_store(tree_c_store,
                                                    get_merkle_tree_leafs(tree_c_size, MerkleTreeType::base_arity));
                            tree_c.erase(config.clone());
                        };

                        if (is_cached(&t_aux.tree_d_config)) {
                            std::size_t tree_d_size = t_aux.tree_d_config.size.context("tree_d config has no size");
                            DiskStore<typename Hash::digest_type> tree_d_store =
                                DiskStore::new_from_disk(tree_d_size, BINARY_ARITY, &t_aux.tree_d_config)
                                    .context("tree_d");
                            // Note: from_data_store requires the base tree leaf count
                            BinaryMerkleTree<Hash> tree_d = BinaryMerkleTree<Hash>::from_data_store(
                                tree_d_store, get_merkle_tree_leafs(tree_d_size, BINARY_ARITY));

                            tree_d.erase(t_aux.tree_d_config);
                            BOOST_LOG_TRIVIAL(trace) << "tree d deleted";
                        }

                        std::size_t tree_count = get_base_tree_count<MerkleTreeType>();
                        std::size_t tree_c_size = t_aux.tree_c_config.size;
                        const auto configs = split_config(t_aux.tree_c_config.clone(), tree_count);

                        if (is_cached(&t_aux.tree_c_config)) {
                            delete_tree_c_store(&t_aux.tree_c_config, tree_c_size);
                        } else if (is_cached(configs[0])) {
                            for (configs::const_iterator config = configs.begin(); config != configs.end(); ++config) {
                                // Trees with sub-trees cannot be instantiated and deleted via the existing tree
                                // interface since knowledge of how the base trees are split exists outside of merkle
                                // light.  For now, we manually remove each on disk tree file since we know where they
                                // are here.
                                boost::filesystem::path tree_c_path = StoreConfig::data_path(config->path, config->id);
                                boost::filesystem::remove(tree_c_path);
                            }
                        }
                        BOOST_LOG_TRIVIAL(trace) << "tree c deleted";

                        for (int i = 0; i < t_aux.labels.labels.size(); i++) {
                            StoreConfig cur_config = t_aux.labels.labels[i].clone();
                            if (is_cached(cur_config)) {
                                DiskStore<typename MerkleTreeType::hash_type::digest_type>::delete (cur_config)
                                    .with_context(|| std::format("labels %d", i));
                                BOOST_LOG_TRIVIAL(trace) << std::format("layer %d deleted", i);
                            }
                        }
                    }

                    Labels<MerkleTreeType> labels;
                    StoreConfig tree_d_config;
                    StoreConfig tree_r_last_config;
                    StoreConfig tree_c_config;

                private:
                    bool is_cached(const StoreConfig &config) {
                        return boost::filesystem::exists(
                            boost::filesystem::path(StoreConfig::data_path(config.path, config.id)));
                    };
                };

                /*************************  PublicInputs  ***********************************/

                template<typename T, typename S>
                struct PublicInputs {
                    typedef Tau<T, S> tau_type;
                    typedef std::size_t challenge_type;

                    std::vector<std::size_t> challenges(const LayerChallenges &layer_challenges, std::size_t leaves,
                                                        std::size_t partition_k = 0) {
                        k = partition_k;
                        return layer_challenges.derive<T>(leaves, replica_id, seed, k);
                    }

                    T replica_id;

                    std::array<std::uint8_t, 32> seed;

                    Tau<T, S> tau;

                    /// Partition index
                    std::size_t k;
                };

                /*************************  LabelsCache  ***********************************/

                template<typename MerkleTreeType>
                struct LabelsCache {
                    typedef MerkleTreeType tree_type;
                    typedef typename tree_type::hash_type tree_hash_type;

                    LabelsCache(const Labels<MerkleTreeType> &labels) {
                        std::vector<DiskStore<typename tree_hash_type::digest_type>> disk_store_labels(labels.size());
                        for (int i = 0; i < labels.size(); i++) {
                            disk_store_labels.emplace_back(labels.labels_for_layer(i + 1));
                        }

                        return {disk_store_labels};
                    }

                    std::size_t size() {
                        return labels.size();
                    }

                    bool empty() {
                        return labels.empty();
                    }

                    const DiskStore<typename tree_hash_type::digest_type> &labels_for_layer(std::size_t layer) {
                        BOOST_ASSERT_MSG(layer != 0, "Layer cannot be 0");
                        assert(layer <= layers());

                        std::size_t row_index = layer - 1;
                        return labels[row_index];
                    }

                    /// Returns the labels on the last layer.
                    const DiskStore<typename tree_hash_type::digest_type> &labels_for_last_layer() {
                        return labels[labels.size() - 1];
                    }

                    /// How many layers are available.
                    std::size_t layers() {
                        return labels.size();
                    }

                    /// Build the column for the given node.
                    Column<typename MerkleTreeType::hash_type> column(std::uint32_t node) {
                        std::vector<typename MerkleTreeType::hash_type::digest_type> rows;

                        for (const DiskStore<typename MerkleTreeType::hash_type::digest_type> &l : labels) {
                            rows.push_back(l.read_at(node));
                        }

                        return {node, rows};
                    }

                    std::vector<DiskStore<typename MerkleTreeType::hash_type::digest_type>> labels;
                };

                /*************************  TemporaryAuxCache  ***********************************/

                template<typename MerkleTreeType, typename Hash>
                struct TemporaryAuxCache {
                    typedef MerkleTreeType tree_type;
                    typedef Hash hash_type;

                    typedef typename tree_type::hash_type tree_hash_type;

                    /// The encoded nodes for 1..layers.
                    LabelsCache<tree_type> labels;
                    BinaryMerkleTree<hash_type> tree_d;

                    // Notably this is a LevelCacheTree instead of a full merkle.
                    LCTree<typename tree_type::hash_type, typename tree_type::Arity, typename tree_type::SubTreeArity,
                           typename tree_type::TopTreeArity>
                        tree_r_last;

                    // Store the 'rows_to_discard' value from the tree_r_last
                    // StoreConfig for later use (i.e. proof generation).
                    std::size_t tree_r_last_config_rows_to_discard;

                    DiskTree<tree_hash_type, typename tree_type::Arity, typename tree_type::SubTreeArity,
                             typename tree_type::TopTreeArity>
                        tree_c;
                    TemporaryAux<tree_type, hash_type> t_aux;
                    boost::filesystem::path replica_path;

                    TemporaryAuxCache(const TemporaryAux<tree_type, hash_type> &t_aux,
                                      const boost::filesystem::path &replica_path) {
                        // tree_d_size stored in the config is the base tree size
                        std::size_t tree_d_size = t_aux.tree_d_config.size();
                        std::size_t tree_d_leafs = get_merkle_tree_leafs(tree_d_size, BINARY_ARITY);
                        BOOST_LOG_TRIVIAL(trace)
                            << std::format("Instantiating tree d with size {} and leafs {}", tree_d_size, tree_d_leafs);
                        DiskStore<typename Hash::digest_type> tree_d_store =
                            DiskStore::new_from_disk(tree_d_size, BINARY_ARITY, &t_aux.tree_d_config)
                                .context("tree_d_store");
                        BinaryMerkleTree<hash_type> tree_d =
                            BinaryMerkleTree<Hash>::from_data_store(tree_d_store, tree_d_leafs).context("tree_d");

                        std::size_t tree_count = get_base_tree_count<MerkleTreeType>();
                        std::vector<StoreConfig> configs = split_config(t_aux.tree_c_config.clone(), tree_count);

                        // tree_c_size stored in the config is the base tree size
                        std::size_t tree_c_size = t_aux.tree_c_config.size;
                        BOOST_LOG_TRIVIAL(trace)
                            << std::format("Instantiating tree c [count {}] with size {} and arity {}", tree_count,
                                           tree_c_size, MerkleTreeType::base_arity);
                        DiskTree<typename MerkleTreeType::hash_type, MerkleTreeType::base_arity,
                                 MerkleTreeType::sub_tree_arity, MerkleTreeType::top_tree_arity>
                            tree_c = create_disk_tree<
                                DiskTree<typename MerkleTreeType::hash_type, MerkleTreeType::base_arity,
                                         MerkleTreeType::sub_tree_arity, MerkleTreeType::top_tree_arity>>(tree_c_size,
                                                                                configs);

                        // tree_r_last_size stored in the config is the base tree size
                        std::size_t tree_r_last_size = t_aux.tree_r_last_config.size;
                        std::size_t tree_r_last_config_rows_to_discard = t_aux.tree_r_last_config.rows_to_discard;
                        const auto(configs, replica_config) = split_config_and_replica(
                            t_aux.tree_r_last_config,
                            replica_path,
                            get_merkle_tree_leafs(tree_r_last_size, MerkleTreeType::base_arity),
                            tree_count);

                        BOOST_LOG_TRIVIAL(trace)
                            << std::format("Instantiating tree r last [count {}] with size {} and arity {}, {}, {}",
                                           tree_count, tree_r_last_size, MerkleTreeType::base_arity,
                                           MerkleTreeType::sub_tree_arity, MerkleTreeType::top_tree_arity);
                        LCTree<typename MerkleTreeType::hash_type, MerkleTreeType::base_arity,
                               MerkleTreeType::sub_tree_arity, MerkleTreeType::top_tree_arity>
                            tree_r_last =
                                create_lc_tree<LCTree<typename MerkleTreeType::hash_type, MerkleTreeType::base_arity,
                                                      MerkleTreeType::sub_tree_arity, MerkleTreeType::top_tree_arity>>(
                                    tree_r_last_size, configs, replica_config);

                        return {{t_aux.labels.context("labels_cache")},
                                tree_d,
                                tree_r_last,
                                tree_r_last_config_rows_to_discard,
                                tree_c,
                                replica_path,
                                t_aux};
                    }

                    DiskStore<typename MerkleTreeType::hash_type::digest_type> &labels_for_layer(std::size_t layer) {
                        return labels.labels_for_layer(layer);
                    }

                    typename MerkleTreeType::hash_type::digest_type domain_node_at_layer(std::size_t layer,
                                                                                std::uint32_t node_index) {
                        return labels_for_layer(layer).read_at(node_index);
                    }

                    Column<typename MerkleTreeType::hash_type> column(std::uint32_t column_index) {
                        return labels.column(column_index);
                    }
                };

                /*************************  PrivateInputs  ***********************************/

                template<typename MerkleTreeType, typename Hash>
                struct PrivateInputs {
                    PersistentAux<typename MerkleTreeType::hash_type::digest_type> p_aux;
                    TemporaryAuxCache<MerkleTreeType, Hash> t_aux;
                };

                /*************************  Proof  ***********************************/

                template<typename MerkleTreeType, typename Hash>
                struct Proof {
                    typedef Hash hash_type;
                    typedef MerkleTreeType tree_type;
                    typedef typename tree_type::hash_type tree_hash_type;

                    MerkleProof<hash_type, MerkleTreeType::base_arity, MerkleTreeType::sub_tree_arity,
                                MerkleTreeType::top_tree_arity>
                        comm_d_proofs;
                    MerkleProof<tree_hash_type, MerkleTreeType::base_arity, MerkleTreeType::sub_tree_arity,
                                MerkleTreeType::top_tree_arity>
                        comm_r_last_proof;
                    ReplicaColumnProof<MerkleProof<tree_hash_type, MerkleTreeType::base_arity,
                                                   MerkleTreeType::sub_tree_arity, MerkleTreeType::top_tree_arity>>
                        replica_column_proofs;

                    /// Indexed by layer in 1..layers.
                    std::vector<LabelingProof<typename MerkleTreeType::hash_type>> labeling_proofs;
                    EncodingProof<typename MerkleTreeType::hash_type> encoding_proof;
                };

                /*************************  ReplicaColumnProof  ***********************************/

                template<typename MerkleProofType>
                struct ReplicaColumnProof {
                    typedef MerkleProofType proof_type;

                    ColumnProof<proof_type> c_x;
                    std::vector<ColumnProof<proof_type>> drg_parents;
                    std::vector<ColumnProof<proof_type>> exp_parents;
                };

                /*************************  TransformedLayers  ***********************************/

                template<typename MerkleTreeType, typename Hash>
                using TransformedLayers =
                    std::tuple<Tau<typename MerkleTreeType::hash_type::digest_type, typename Hash::digest_type>,
                               PersistentAux<typename MerkleTreeType::hash_type::digest_type>,
                               TemporaryAux<MerkleTreeType, Hash>>;

                /*************************  ???  ***********************************/

                template<typename Hash, typename InputDataRange>
                typename Hash::digest_type get_node(const InputDataRange &data, std::size_t index) {
                    return Hash::digest_type::try_from_bytes(data_at_node(data, index));
                }

                /// Generate the replica id as expected for Stacked DRG.
                template<typename InputDataRange, typename Hash = crypto3::hashes::sha2<256>>
                typename Hash::digest_type
                    generate_replica_id(const std::array<std::uint8_t, 32> &prover_id, std::uint64_t sector_id,
                                        const std::array<std::uint8_t, 32> &ticket, const InputDataRange &comm_d,
                                        const std::array<std::uint8_t, 32> &porep_seed) {
                    using namespace nil::crypto3;

                    accumulator_set<Hash> acc;

                    hash<Hash>(prover_id, acc);
                    hash<Hash>(sector_id, acc);
                    hash<Hash>(ticket, acc);
                    hash<Hash>(comm_d, acc);
                    hash<Hash>(porep_seed, acc);

                    return accumulators::extract::hash<Hash>(acc);
                }
            }    // namespace vanilla
        }        // namespace stacked
    }            // namespace filecoin
}    // namespace nil

#endif


/*    Copyright (c) 2010-2019, Delft University of Technology
 *    All rigths reserved
 *
 *    This file is part of the Tudat. Redistribution and use in source and
 *    binary forms, with or without modification, are permitted exclusively
 *    under the terms of the Modified BSD license. You should have received
 *    a copy of the license with this file. If not, please or visit:
 *    http://tudat.tudelft.nl/LICENSE.
 */

#include <boost/lambda/lambda.hpp>

#include "tudat/simulation/environment_setup/createAerodynamicCoefficientInterface.h"

namespace tudat
{

namespace simulation_setup
{

//! Function to create aerodynamic coefficient settings from coefficients stored in data files
std::shared_ptr< AerodynamicCoefficientSettings > readTabulatedAerodynamicCoefficientsFromFiles(
        const std::map< int, std::string > forceCoefficientFiles,
        const std::map< int, std::string > momentCoefficientFiles,
        const double referenceLength,
        const double referenceArea,
        const double lateralReferenceLength,
        const Eigen::Vector3d& momentReferencePoint,
        const std::vector< aerodynamics::AerodynamicCoefficientsIndependentVariables > independentVariableNames,
        const bool areCoefficientsInAerodynamicFrame,
        const bool areCoefficientsInNegativeAxisDirection,
        const std::shared_ptr< interpolators::InterpolatorSettings > interpolatorSettings )
{
    // Retrieve number of independent variables from file.
    int numberOfIndependentVariables =
            input_output::getNumberOfIndependentVariablesInCoefficientFile( forceCoefficientFiles.begin( )->second );

    // Call approriate file reading function for N independent variables
    std::shared_ptr< AerodynamicCoefficientSettings > coefficientSettings;
    if( numberOfIndependentVariables == 1 )
    {
        coefficientSettings = readGivenSizeTabulatedAerodynamicCoefficientsFromFiles< 1 >(
                    forceCoefficientFiles, momentCoefficientFiles, referenceLength, referenceArea, lateralReferenceLength,
                    momentReferencePoint, independentVariableNames, areCoefficientsInAerodynamicFrame,
                    areCoefficientsInNegativeAxisDirection, interpolatorSettings );
    }
    else if( numberOfIndependentVariables == 2 )
    {
        coefficientSettings = readGivenSizeTabulatedAerodynamicCoefficientsFromFiles< 2 >(
                    forceCoefficientFiles, momentCoefficientFiles, referenceLength, referenceArea, lateralReferenceLength,
                    momentReferencePoint, independentVariableNames, areCoefficientsInAerodynamicFrame,
                    areCoefficientsInNegativeAxisDirection, interpolatorSettings );
    }
    else if( numberOfIndependentVariables == 3 )
    {
        coefficientSettings = readGivenSizeTabulatedAerodynamicCoefficientsFromFiles< 3 >(
                    forceCoefficientFiles, momentCoefficientFiles, referenceLength, referenceArea, lateralReferenceLength,
                    momentReferencePoint, independentVariableNames, areCoefficientsInAerodynamicFrame,
                    areCoefficientsInNegativeAxisDirection, interpolatorSettings );
    }
    else
    {
        throw std::runtime_error( "Error when reading aerodynamic coefficient settings from file, found " +
                                  std::to_string( numberOfIndependentVariables ) +
                                  " independent variables, up to 3 currently supported" );
    }
    return coefficientSettings;
}

//! Function to create aerodynamic coefficient settings from coefficients stored in data files
std::shared_ptr< AerodynamicCoefficientSettings >
readTabulatedAerodynamicCoefficientsFromFiles(
        const std::map< int, std::string > forceCoefficientFiles,
        const double referenceArea,
        const std::vector< aerodynamics::AerodynamicCoefficientsIndependentVariables > independentVariableNames,
        const bool areCoefficientsInAerodynamicFrame,
        const bool areCoefficientsInNegativeAxisDirection,
        const std::shared_ptr< interpolators::InterpolatorSettings > interpolatorSettings )
{
    // Retrieve number of independent variables from file.
    int numberOfIndependentVariables =
            input_output::getNumberOfIndependentVariablesInCoefficientFile( forceCoefficientFiles.begin( )->second );

    // Call approriate file reading function for N independent variables
    std::shared_ptr< AerodynamicCoefficientSettings > coefficientSettings;
    if( numberOfIndependentVariables == 1 )
    {
        coefficientSettings = readGivenSizeTabulatedAerodynamicCoefficientsFromFiles< 1 >(
                    forceCoefficientFiles, referenceArea, independentVariableNames,
                    areCoefficientsInAerodynamicFrame, areCoefficientsInNegativeAxisDirection, interpolatorSettings );
    }
    else if( numberOfIndependentVariables == 2 )
    {
        coefficientSettings = readGivenSizeTabulatedAerodynamicCoefficientsFromFiles< 2 >(
                    forceCoefficientFiles, referenceArea, independentVariableNames,
                    areCoefficientsInAerodynamicFrame, areCoefficientsInNegativeAxisDirection, interpolatorSettings );
    }
    else if( numberOfIndependentVariables == 3 )
    {
        coefficientSettings = readGivenSizeTabulatedAerodynamicCoefficientsFromFiles< 3 >(
                    forceCoefficientFiles, referenceArea, independentVariableNames,
                    areCoefficientsInAerodynamicFrame, areCoefficientsInNegativeAxisDirection, interpolatorSettings );
    }
    else
    {
        throw std::runtime_error( "Error when reading aerodynamic coefficient settings from file, found " +
                                  std::to_string( numberOfIndependentVariables ) +
                                  " independent variables, up to 3 currently supported" );
    }
    return coefficientSettings;
}

//! Function to create an aerodynamic coefficient interface containing constant coefficients.
std::shared_ptr< aerodynamics::AerodynamicCoefficientInterface >
createConstantCoefficientAerodynamicCoefficientInterface(
        const Eigen::Vector3d constantForceCoefficient,
        const Eigen::Vector3d constantMomentCoefficient,
        const double referenceLength,
        const double referenceArea,
        const double lateralReferenceLength,
        const Eigen::Vector3d& momentReferencePoint,
        const bool areCoefficientsInAerodynamicFrame,
        const bool areCoefficientsInNegativeAxisDirection )
{
    // Create coefficient interface
    std::shared_ptr< aerodynamics::AerodynamicCoefficientInterface > coefficientInterface =
            std::make_shared< aerodynamics::CustomAerodynamicCoefficientInterface >(
                [ = ]( const std::vector< double >& ){ return constantForceCoefficient; },
                [ = ]( const std::vector< double >& ){ return constantMomentCoefficient; },
                referenceLength, referenceArea, lateralReferenceLength, momentReferencePoint,
                std::vector< aerodynamics::AerodynamicCoefficientsIndependentVariables >( ),
                areCoefficientsInAerodynamicFrame, areCoefficientsInNegativeAxisDirection );
    coefficientInterface->updateFullCurrentCoefficients( std::vector< double >( ) );
    return coefficientInterface;
}

//! Function to create an aerodynamic coefficient interface containing constant coefficients.
std::shared_ptr< aerodynamics::AerodynamicCoefficientInterface >
createZeroParameterAerodynamicCoefficientInterface(
        const std::function< Eigen::Vector3d( ) > constantForceCoefficientFunction,
        const std::function< Eigen::Vector3d( ) > constantMomentCoefficientFunction,
        const double referenceLength,
        const double referenceArea,
        const double lateralReferenceLength,
        const Eigen::Vector3d& momentReferencePoint,
        const bool areCoefficientsInAerodynamicFrame,
        const bool areCoefficientsInNegativeAxisDirection )
{
    // Create coefficient interface
    std::shared_ptr< aerodynamics::AerodynamicCoefficientInterface > coefficientInterface =
            std::make_shared< aerodynamics::CustomAerodynamicCoefficientInterface >(
                [ = ]( const std::vector< double >& ){ return constantForceCoefficientFunction( ); },
                [ = ]( const std::vector< double >& ){ return constantMomentCoefficientFunction( ); },
                referenceLength, referenceArea, lateralReferenceLength, momentReferencePoint,
                std::vector< aerodynamics::AerodynamicCoefficientsIndependentVariables >( ),
                areCoefficientsInAerodynamicFrame, areCoefficientsInNegativeAxisDirection );
    coefficientInterface->updateFullCurrentCoefficients( std::vector< double >( ) );
    return coefficientInterface;
}

//! Factory function for tabulated (1-D independent variables) aerodynamic coefficient interface from coefficient settings.
std::shared_ptr< aerodynamics::AerodynamicCoefficientInterface >
createUnivariateTabulatedCoefficientAerodynamicCoefficientInterface(
        const std::shared_ptr< AerodynamicCoefficientSettings > coefficientSettings,
        const std::string& body )
{
    using namespace tudat::interpolators;

    // Check consistency of type.
    std::shared_ptr< TabulatedAerodynamicCoefficientSettings< 1 > > tabulatedCoefficientSettings =
            std::dynamic_pointer_cast< TabulatedAerodynamicCoefficientSettings< 1 > >( coefficientSettings );
    if( tabulatedCoefficientSettings == nullptr )
    {
        throw std::runtime_error(
                    "Error, expected tabulated aerodynamic coefficients of size " +
                    std::to_string( 1 ) + "for body " + body );
    }
    else
    {

        // Retrieve or generate interpolation settings
        std::shared_ptr< OneDimensionalInterpolator< double, Eigen::Vector3d > > forceInterpolator;
        std::shared_ptr< OneDimensionalInterpolator< double, Eigen::Vector3d > > momentInterpolator;
        if ( tabulatedCoefficientSettings->getInterpolatorSettings( ) == nullptr )
        {
            forceInterpolator = createOneDimensionalInterpolator( tabulatedCoefficientSettings->getForceCoefficients( ),
                                                                  std::make_shared< InterpolatorSettings >( linear_interpolator ) );
            momentInterpolator = createOneDimensionalInterpolator( tabulatedCoefficientSettings->getForceCoefficients( ),
                                                                   std::make_shared< InterpolatorSettings >( linear_interpolator ) );
        }
        else
        {
            forceInterpolator = createOneDimensionalInterpolator( tabulatedCoefficientSettings->getForceCoefficients( ),
                                                                  std::dynamic_pointer_cast< InterpolatorSettings >(
                                                                      tabulatedCoefficientSettings->getInterpolatorSettings( ) ) );
            momentInterpolator = createOneDimensionalInterpolator( tabulatedCoefficientSettings->getForceCoefficients( ),
                                                                   std::dynamic_pointer_cast< InterpolatorSettings >(
                                                                       tabulatedCoefficientSettings->getInterpolatorSettings( ) ) );
        }

        // Create aerodynamic coefficient interface.
        return  std::make_shared< aerodynamics::CustomAerodynamicCoefficientInterface >(
                    std::bind( &Interpolator< double, Eigen::Vector3d >::interpolate, forceInterpolator, std::placeholders::_1 ),
                    std::bind( &Interpolator< double, Eigen::Vector3d >::interpolate, momentInterpolator, std::placeholders::_1 ),
                    tabulatedCoefficientSettings->getReferenceLength( ),
                    tabulatedCoefficientSettings->getReferenceArea( ),
                    tabulatedCoefficientSettings->getReferenceLength( ),
                    tabulatedCoefficientSettings->getMomentReferencePoint( ),
                    tabulatedCoefficientSettings->getIndependentVariableNames( ),
                    tabulatedCoefficientSettings->getAreCoefficientsInAerodynamicFrame( ),
                    tabulatedCoefficientSettings->getAreCoefficientsInNegativeAxisDirection( ) );
    }
}

//! Function to create and aerodynamic coefficient interface.
std::shared_ptr< aerodynamics::AerodynamicCoefficientInterface >
createAerodynamicCoefficientInterface(
        const std::shared_ptr< AerodynamicCoefficientSettings > coefficientSettings,
        const std::string& body )
{
    using namespace tudat::aerodynamics;

    std::shared_ptr< AerodynamicCoefficientInterface > coefficientInterface;

    // Check type of interface that is to be created.
    switch( coefficientSettings->getAerodynamicCoefficientType( ) )
    {
    case constant_aerodynamic_coefficients:
    {
        // Check consistency of type.
        std::shared_ptr< ConstantAerodynamicCoefficientSettings > constantCoefficientSettings =
                std::dynamic_pointer_cast< ConstantAerodynamicCoefficientSettings >(
                    coefficientSettings );
        if( constantCoefficientSettings == nullptr )
        {
            throw std::runtime_error(
                        "Error, expected constant aerodynamic coefficients for body " + body );
        }
        else
        {
            // create constant interface.
            coefficientInterface = createConstantCoefficientAerodynamicCoefficientInterface(
                        constantCoefficientSettings->getConstantForceCoefficient( ),
                        constantCoefficientSettings->getConstantMomentCoefficient( ),
                        constantCoefficientSettings->getReferenceLength( ),
                        constantCoefficientSettings->getReferenceArea( ),
                        constantCoefficientSettings->getReferenceLength( ),
                        constantCoefficientSettings->getMomentReferencePoint( ),
                        constantCoefficientSettings->getAreCoefficientsInAerodynamicFrame( ),
                        constantCoefficientSettings->getAreCoefficientsInNegativeAxisDirection( ) );
        }
        break;
    }
    case custom_aerodynamic_coefficients:
    {
        // Check consistency of type.
        std::shared_ptr< CustomAerodynamicCoefficientSettings > customCoefficientSettings =
                std::dynamic_pointer_cast< CustomAerodynamicCoefficientSettings >(
                    coefficientSettings );
        if( customCoefficientSettings == nullptr )
        {
            throw std::runtime_error(
                        "Error, expected custom aerodynamic coefficients for body " + body );
        }
        else
        {
            // create constant interface.
            coefficientInterface = std::make_shared< CustomAerodynamicCoefficientInterface >(
                        customCoefficientSettings->getForceCoefficientFunction( ),
                        customCoefficientSettings->getMomentCoefficientFunction( ),
                        customCoefficientSettings->getReferenceLength( ),
                        customCoefficientSettings->getReferenceArea( ),
                        customCoefficientSettings->getReferenceLength( ),
                        customCoefficientSettings->getMomentReferencePoint( ),
                        customCoefficientSettings->getIndependentVariableNames( ),
                        customCoefficientSettings->getAreCoefficientsInAerodynamicFrame( ),
                        customCoefficientSettings->getAreCoefficientsInNegativeAxisDirection( ) );
        }
        break;
    }
    case tabulated_coefficients:
    {
        // Check number of dimensions of tabulated coefficients.
        int numberOfDimensions = coefficientSettings->getIndependentVariableNames( ).size( );
        switch( numberOfDimensions )
        {
        case 1:
        {
            coefficientInterface = createUnivariateTabulatedCoefficientAerodynamicCoefficientInterface(
                        coefficientSettings, body );
            break;
        }
        case 2:
        {
            coefficientInterface = createTabulatedCoefficientAerodynamicCoefficientInterface< 2 >(
                        coefficientSettings, body );
            break;
        }
        case 3:
        {
            coefficientInterface = createTabulatedCoefficientAerodynamicCoefficientInterface< 3 >(
                        coefficientSettings, body );
            break;
        }
        case 4:
        {
            coefficientInterface = createTabulatedCoefficientAerodynamicCoefficientInterface< 4 >(
                        coefficientSettings, body );
            break;
        }
        case 5:
        {
            coefficientInterface = createTabulatedCoefficientAerodynamicCoefficientInterface< 5 >(
                        coefficientSettings, body );
            break;
        }
        case 6:
        {
            coefficientInterface = createTabulatedCoefficientAerodynamicCoefficientInterface< 6 >(
                        coefficientSettings, body );
            break;
        }
        default:
            throw std::runtime_error( "Error when making tabulated aerodynamic coefficient interface, " +
                                      std::to_string( numberOfDimensions ) + " dimensions not yet implemented" );
        }
        break;
    }
    case scaled_coefficients:
    {
        // Check consistency of type and class.
        std::shared_ptr< ScaledAerodynamicCoefficientInterfaceSettings > scaledCoefficientSettings =
                std::dynamic_pointer_cast< ScaledAerodynamicCoefficientInterfaceSettings >(
                    coefficientSettings );
        if( scaledCoefficientSettings == nullptr )
        {
            throw std::runtime_error(
                        "Error, expected scaled aerodynamic coefficient settings for body " + body );
        }
        else
        {
            std::shared_ptr< AerodynamicCoefficientInterface > baseInterface = createAerodynamicCoefficientInterface(
                        scaledCoefficientSettings->getBaseSettings( ), body );
            coefficientInterface = std::make_shared< ScaledAerodynamicCoefficientInterface >(
                        baseInterface, scaledCoefficientSettings->getForceScaling( ),
                        scaledCoefficientSettings->getMomentScaling( ), scaledCoefficientSettings->getIsScalingAbsolute( ) );
        }
        break;
    }
    default:
        throw std::runtime_error( "Error, do not recognize aerodynamic coefficient settings for " + body );
    }

    // Create and set control surfaces
    if( coefficientSettings->getControlSurfaceSettings( ).size( ) != 0 )
    {
        std::map< std::string, std::shared_ptr< ControlSurfaceIncrementAerodynamicInterface > >
                controlSurfaceIncrementInterfaces;
        std::map< std::string, std::shared_ptr< ControlSurfaceIncrementAerodynamicCoefficientSettings > >
                controlSurfaceSettings = coefficientSettings->getControlSurfaceSettings( );
        for( std::map< std::string, std::shared_ptr< ControlSurfaceIncrementAerodynamicCoefficientSettings > >::iterator
             settingIterator = controlSurfaceSettings.begin( ); settingIterator != controlSurfaceSettings.end( );
             settingIterator++ )
        {
            controlSurfaceIncrementInterfaces[ settingIterator->first ] =
                    createControlSurfaceIncrementAerodynamicCoefficientInterface(
                        settingIterator->second, body );
        }
        coefficientInterface->setControlSurfaceIncrements( controlSurfaceIncrementInterfaces );

    }

    return coefficientInterface;
}

} // simulation_setup

} // tudat


// ====================================================================
// This file is part of FlexibleSUSY.
//
// FlexibleSUSY is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published
// by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// FlexibleSUSY is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with FlexibleSUSY.  If not, see
// <http://www.gnu.org/licenses/>.
// ====================================================================

// File generated at Thu 10 May 2018 14:41:56

#ifndef MSSMEFTHiggs_PHYSICAL_H
#define MSSMEFTHiggs_PHYSICAL_H

#include <Eigen/Core>

#include <iosfwd>

namespace flexiblesusy {

struct MSSMEFTHiggs_physical {
   void clear();
   void convert_to_hk();   ///< converts pole masses to HK convention
   void convert_to_slha(); ///< converts pole masses to SLHA convention
   Eigen::ArrayXd get() const; ///< returns array with all masses and mixings
   void set(const Eigen::ArrayXd&); ///< set all masses and mixings
   Eigen::ArrayXd get_masses() const; ///< returns array with all masses
   void set_masses(const Eigen::ArrayXd&); ///< set all masses
   void print(std::ostream&) const;

   double MVG{};
   double MGlu{};
   Eigen::Array<double,3,1> MFv{Eigen::Array<double,3,1>::Zero()};
   Eigen::Array<double,6,1> MSd{Eigen::Array<double,6,1>::Zero()};
   Eigen::Array<double,3,1> MSv{Eigen::Array<double,3,1>::Zero()};
   Eigen::Array<double,6,1> MSu{Eigen::Array<double,6,1>::Zero()};
   Eigen::Array<double,6,1> MSe{Eigen::Array<double,6,1>::Zero()};
   Eigen::Array<double,2,1> Mhh{Eigen::Array<double,2,1>::Zero()};
   Eigen::Array<double,2,1> MAh{Eigen::Array<double,2,1>::Zero()};
   Eigen::Array<double,2,1> MHpm{Eigen::Array<double,2,1>::Zero()};
   Eigen::Array<double,4,1> MChi{Eigen::Array<double,4,1>::Zero()};
   Eigen::Array<double,2,1> MCha{Eigen::Array<double,2,1>::Zero()};
   Eigen::Array<double,3,1> MFe{Eigen::Array<double,3,1>::Zero()};
   Eigen::Array<double,3,1> MFd{Eigen::Array<double,3,1>::Zero()};
   Eigen::Array<double,3,1> MFu{Eigen::Array<double,3,1>::Zero()};
   double MVWm{};
   double MVP{};
   double MVZ{};

   Eigen::Matrix<double,6,6> ZD{Eigen::Matrix<double,6,6>::Zero()};
   Eigen::Matrix<double,3,3> ZV{Eigen::Matrix<double,3,3>::Zero()};
   Eigen::Matrix<double,6,6> ZU{Eigen::Matrix<double,6,6>::Zero()};
   Eigen::Matrix<double,6,6> ZE{Eigen::Matrix<double,6,6>::Zero()};
   Eigen::Matrix<double,2,2> ZH{Eigen::Matrix<double,2,2>::Zero()};
   Eigen::Matrix<double,2,2> ZA{Eigen::Matrix<double,2,2>::Zero()};
   Eigen::Matrix<double,2,2> ZP{Eigen::Matrix<double,2,2>::Zero()};
   Eigen::Matrix<std::complex<double>,4,4> ZN{Eigen::Matrix<std::complex<double>,4,4>::Zero()};
   Eigen::Matrix<std::complex<double>,2,2> UM{Eigen::Matrix<std::complex<double>,2,2>::Zero()};
   Eigen::Matrix<std::complex<double>,2,2> UP{Eigen::Matrix<std::complex<double>,2,2>::Zero()};
   Eigen::Matrix<std::complex<double>,3,3> ZEL{Eigen::Matrix<std::complex<double>,3,3>::Zero()};
   Eigen::Matrix<std::complex<double>,3,3> ZER{Eigen::Matrix<std::complex<double>,3,3>::Zero()};
   Eigen::Matrix<std::complex<double>,3,3> ZDL{Eigen::Matrix<std::complex<double>,3,3>::Zero()};
   Eigen::Matrix<std::complex<double>,3,3> ZDR{Eigen::Matrix<std::complex<double>,3,3>::Zero()};
   Eigen::Matrix<std::complex<double>,3,3> ZUL{Eigen::Matrix<std::complex<double>,3,3>::Zero()};
   Eigen::Matrix<std::complex<double>,3,3> ZUR{Eigen::Matrix<std::complex<double>,3,3>::Zero()};
   Eigen::Matrix<double,2,2> ZZ{Eigen::Matrix<double,2,2>::Zero()};

};

std::ostream& operator<<(std::ostream&, const MSSMEFTHiggs_physical&);

} // namespace flexiblesusy

#endif


#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>

#include <Eigen/Core>

#ifndef NO_AUTODIFF
#include <cmath>
#ifdef USE_STAN_MATH
#include <vector>
#include <stan/math.hpp>
#else
#include <cfloat>
#include <unsupported/Eigen/AutoDiff>
#endif
#endif

#include "celerite/celerite.h"
#include "celerite/carma.h"

namespace py = pybind11;

//
// By sub-classing CholeskySolver here, we can make it picklable but not make
// C++11 a requirement for the C++ library.
//
// The pybind11 pickle docs are here:
// http://pybind11.readthedocs.io/en/master/advanced/classes.html#pickling-support
// but the gist is that class just needs to expose __getstate__ and __setstate__.
// These will just directly call serialize and deserialize below.
//
class PicklableCholeskySolver : public celerite::solver::CholeskySolver<double> {
public:
  PicklableCholeskySolver () : celerite::solver::CholeskySolver<double>() {};

  auto serialize () const {
    return std::make_tuple(
      this->computed_, this->N_, this->J_, this->log_det_,
      this->phi_, this->u_, this->W_, this->D_
    );
  };

  void deserialize (
      bool computed, int n, int J, double log_det,
      Eigen::MatrixXd phi,
      Eigen::MatrixXd u,
      Eigen::MatrixXd W,
      Eigen::VectorXd D) {
    this->computed_ = computed;
    this->N_        = n;
    this->J_        = J;
    this->log_det_  = log_det;
    this->phi_      = phi;
    this->u_        = u;
    this->W_        = W;
    this->D_        = D;
  };
};

//
// Below is the boilerplate code for the pybind11 extension module.
//
PYBIND11_PLUGIN(solver) {
  typedef Eigen::MatrixXd matrix_t;
  typedef Eigen::VectorXd vector_t;

  py::module m("solver", R"delim(
This is the low-level interface to the C++ implementation of the celerite
algorithm. These methods do most of the heavy lifting but most users shouldn't
need to call these directly. This interface was built using `pybind11
<http://pybind11.readthedocs.io/>`_.

)delim");

  m.def("get_library_version", []() { return CELERITE_VERSION_STRING; },
        "The version of the linked C++ library");

  m.def("has_autodiff", []() {
#ifdef NO_AUTODIFF
    return false;
#else
    return true;
#endif
  }, "Returns True if celerite was compiled with autodiff support");

  py::register_exception<celerite::linalg_exception>(m, "LinAlgError");

  m.def("get_kernel_value",
    [](
      const vector_t& a_real,
      const vector_t& c_real,
      const vector_t& a_comp,
      const vector_t& b_comp,
      const vector_t& c_comp,
      const vector_t& d_comp,
      py::array_t<double> tau
    ) {
      auto get_kernel_value_closure = [a_real, c_real, a_comp, b_comp, c_comp, d_comp] (double t) {
        return celerite::get_kernel_value(
          a_real, c_real, a_comp, b_comp, c_comp, d_comp, t
        );
      };
      return py::vectorize(get_kernel_value_closure)(tau);
    },
    R"delim(
Get the value of the kernel for given parameters and lags

Args:
    a_real (array[j_real]): The coefficients of the real terms.
    c_real (array[j_real]): The exponents of the real terms.
    a_comp (array[j_complex]): The real part of the coefficients of the
        complex terms.
    b_comp (array[j_complex]): The imaginary part of the coefficients of
        the complex terms.
    c_comp (array[j_complex]): The real part of the exponents of the
        complex terms.
    d_comp (array[j_complex]): The imaginary part of the exponents of the
        complex terms.
    tau (array[n]): The time lags where the kernel should be evaluated.

Returns:
    array[n]: The kernel evaluated at ``tau``.

)delim");

  m.def("get_psd_value",
    [](
      const vector_t& a_real,
      const vector_t& c_real,
      const vector_t& a_comp,
      const vector_t& b_comp,
      const vector_t& c_comp,
      const vector_t& d_comp,
      py::array_t<double> omega
    ) {
      auto get_psd_value_closure = [a_real, c_real, a_comp, b_comp, c_comp, d_comp] (double t) {
        return celerite::get_psd_value(
          a_real, c_real, a_comp, b_comp, c_comp, d_comp, t
        );
      };
      return py::vectorize(get_psd_value_closure)(omega);
    },
    R"delim(
Get the PSD of the kernel for given parameters and angular frequencies

Args:
    a_real (array[j_real]): The coefficients of the real terms.
    c_real (array[j_real]): The exponents of the real terms.
    a_comp (array[j_complex]): The real part of the coefficients of the
        complex terms.
    b_comp (array[j_complex]): The imaginary part of the coefficients of
        the complex terms.
    c_comp (array[j_complex]): The real part of the exponents of the
        complex terms.
    d_comp (array[j_complex]): The imaginary part of the exponents of the
        complex terms.
    omega (array[n]): The frequencies where the PSD should be evaluated.

Returns:
    array[n]: The PSD evaluated at ``omega``.

)delim");

  m.def("check_coefficients",
    [](
      const vector_t& a_real,
      const vector_t& c_real,
      const vector_t& a_comp,
      const vector_t& b_comp,
      const vector_t& c_comp,
      const vector_t& d_comp
    ) {
      return celerite::check_coefficients(a_real, c_real, a_comp, b_comp, c_comp, d_comp);
    },
    R"delim(
Apply Sturm's theorem to check if parameters yield a positive PSD

Args:
    a_real (array[j_real]): The coefficients of the real terms.
    c_real (array[j_real]): The exponents of the real terms.
    a_comp (array[j_complex]): The real part of the coefficients of the
        complex terms.
    b_comp (array[j_complex]): The imaginary part of the coefficients of
        the complex terms.
    c_comp (array[j_complex]): The real part of the exponents of the
        complex terms.
    d_comp (array[j_complex]): The imaginary part of the exponents of the
        complex terms.

Returns:
    bool: ``True`` if the PSD is everywhere positive.

)delim");

  //
  // ------ CARMA ------
  //
  py::class_<celerite::carma::CARMASolver> carma_solver(m, "CARMASolver", R"delim(
A thin wrapper around the C++ CARMASolver class

This solver is parameterized following carma_pack:
https://github.com/brandonckelly/carma_pack

Args:
    log_sigma (float): The log of the variance of the process.
    arparams (array[p]): The parameters of the autoregressive component.
    maparams (array[q]): The parameters of the moving average component.

)delim");
  carma_solver.def(py::init<double, vector_t, vector_t>());

  carma_solver.def("log_likelihood",
    [](celerite::carma::CARMASolver& solver, const vector_t& t, const vector_t& y, const vector_t& yerr) {
      return solver.log_likelihood(t, y, yerr);
    }, R"delim(
Compute the log likelihood using a Kalman filter

Args:
    t (array[n]): The input coordinates of the observations.
    y (array[n]): The observations.
    yerr (array[n]): The measurement uncertainties of the observations.

)delim");

  carma_solver.def("get_celerite_coeffs",
    [](celerite::carma::CARMASolver& solver) {
      vector_t a_real, c_real, a_comp, b_comp, c_comp, d_comp;
      solver.get_celerite_coeffs(a_real, c_real, a_comp, b_comp, c_comp, d_comp);
      return std::make_tuple(a_real, c_real, a_comp, b_comp, c_comp, d_comp);
    }, R"delim(
Compute the coefficients of the celerite model for the given CARMA model

)delim");

  //
  // ------ CHOLESKY ------
  //
  py::class_<PicklableCholeskySolver> cholesky_solver(m, "CholeskySolver", R"delim(
A thin wrapper around the C++ CholeskySolver class
)delim");
  cholesky_solver.def(py::init<>());

#ifdef USE_STAN_MATH
  cholesky_solver.def("grad_log_likelihood",
    [](
        PicklableCholeskySolver& nothing,
        double jitter,
        const vector_t& a_real,
        const vector_t& c_real,
        const vector_t& a_comp,
        const vector_t& b_comp,
        const vector_t& c_comp,
        const vector_t& d_comp,
        const vector_t& A,
        const matrix_t& U,
        const matrix_t& V,
        const vector_t& x,
        const vector_t& y,
        const vector_t& diag
    ) {

#ifndef NO_AUTODIFF
      typedef stan::math::var g_t;
      typedef Eigen::Matrix<g_t, Eigen::Dynamic, 1> v_t;

      int J_real = a_real.rows();
      int J_comp = a_comp.rows();

      // Set up the solver to track the gradients
      celerite::solver::CholeskySolver<g_t> solver;
      g_t jitter_ = g_t(jitter);
      v_t a_real_(J_real), c_real_(J_real),
          a_comp_(J_comp), b_comp_(J_comp), c_comp_(J_comp), d_comp_(J_comp);
      a_real_ << a_real;
      c_real_ << c_real;
      a_comp_ << a_comp;
      b_comp_ << b_comp;
      c_comp_ << c_comp;
      d_comp_ << d_comp;

      // Factorize the matrix while propagating the gradients
      solver.compute(
        jitter_, a_real_, c_real_, a_comp_, b_comp_, c_comp_, d_comp_,
        A, U, V, x, diag
      );

      // Compute the likelihood
      g_t ll = -0.5 * (solver.dot_solve(y) + solver.log_determinant() + M_PI * log(x.rows()));
      double ll_val = ll.val();

      // Evaluate the backpropagated gradients
      std::vector<g_t> params;
      params.push_back(jitter_);
      for (int i = 0; i < J_real; ++i) params.push_back(a_real_(i));
      for (int i = 0; i < J_real; ++i) params.push_back(c_real_(i));
      for (int i = 0; i < J_comp; ++i) params.push_back(a_comp_(i));
      for (int i = 0; i < J_comp; ++i) params.push_back(b_comp_(i));
      for (int i = 0; i < J_comp; ++i) params.push_back(c_comp_(i));
      for (int i = 0; i < J_comp; ++i) params.push_back(d_comp_(i));
      std::vector<double> g;
      ll.grad(params, g);

      // Copy the results to a numpy array
      auto result = py::array_t<double>(g.size());
      auto buf = result.request();
      double* ptr = (double *) buf.ptr;
      for (size_t i = 0; i < g.size(); ++i) ptr[i] = g[i];

      // Tell stan that we don't need these gradients anymore
      stan::math::recover_memory();

      return std::make_tuple(ll_val, result);
#else
      throw std::exception();
#endif
    }, R"delim(
Compute the gradient of the log likelihood of the model using autodiff

The returned gradient is with respect to the jitter and the coefficients.

Args:
    jitter (float): The jitter of the kernel.
    a_real (array[j_real]): The coefficients of the real terms.
    c_real (array[j_real]): The exponents of the real terms.
    a_comp (array[j_complex]): The real part of the coefficients of the
        complex terms.
    b_comp (array[j_complex]): The imaginary part of the coefficients of
        the complex terms.
    c_comp (array[j_complex]): The real part of the exponents of the
        complex terms.
    d_comp (array[j_complex]): The imaginary part of the exponents of the
        complex terms.
    x (array[n]): The _sorted_ array of input coordinates.
    y (array[n]): The observations at ``x``.
    diag (array[n]): An array that should be added to the diagonal of the
        matrix. This often corresponds to measurement uncertainties and in
        that case, ``diag`` should be the measurement _variance_
        (i.e. sigma^2).

)delim");

#else

  cholesky_solver.def("grad_log_likelihood",
    [](
      PicklableCholeskySolver& nothing,
      double jitter,
      const vector_t& a_real,
      const vector_t& c_real,
      const vector_t& a_comp,
      const vector_t& b_comp,
      const vector_t& c_comp,
      const vector_t& d_comp,
      const vector_t& A,
      const matrix_t& U,
      const matrix_t& V,
      const vector_t& x,
      const vector_t& y,
      const vector_t& diag
    ) {

#ifndef NO_AUTODIFF
      typedef Eigen::AutoDiffScalar<Eigen::VectorXd> g_t;
      typedef Eigen::Matrix<g_t, Eigen::Dynamic, 1> v_t;

      int J_real = a_real.rows();
      int J_comp = a_comp.rows();
      int g_tot = 2 * J_real + 4 * J_comp;

      celerite::solver::CholeskySolver<g_t> solver;
      v_t a_real_(J_real), c_real_(J_real),
          a_comp_(J_comp), b_comp_(J_comp), c_comp_(J_comp), d_comp_(J_comp);

      // This hack is needed because, if jitter is zero, we end up with
      // numerically unstable gradients in all dimensions.
      bool compute_jitter = false;
      int i0 = 0, i = 0;
      g_t jitter_ = g_t(jitter);
      if (jitter > DBL_EPSILON) {
        compute_jitter = true;
        i0 = 1;
        g_tot ++;
        jitter_ = g_t(jitter, g_tot, 0);
      } else {
        jitter_ = g_t(jitter);
      }

      // Keep track of the coordinates of each gradient
      if (J_real) {
        for (i = 0; i < J_real; ++i) a_real_(i) = g_t(a_real(i), g_tot, i0+i);
        i0 += i;
        for (i = 0; i < J_real; ++i) c_real_(i) = g_t(c_real(i), g_tot, i0+i);
        i0 += i;
      }
      if (J_comp) {
        for (i = 0; i < J_comp; ++i) a_comp_(i) = g_t(a_comp(i), g_tot, i0+i);
        i0 += i;
        for (i = 0; i < J_comp; ++i) b_comp_(i) = g_t(b_comp(i), g_tot, i0+i);
        i0 += i;
        for (i = 0; i < J_comp; ++i) c_comp_(i) = g_t(c_comp(i), g_tot, i0+i);
        i0 += i;
        for (i = 0; i < J_comp; ++i) d_comp_(i) = g_t(d_comp(i), g_tot, i0+i);
      }

      // Factorize and track the gradients
      solver.compute(
        jitter_, a_real_, c_real_, a_comp_, b_comp_, c_comp_, d_comp_,
        A, U, V, x, diag
      );

      // Compute the likelihood and the gradients
      g_t ll = -0.5 * (solver.dot_solve(y) + solver.log_determinant() + M_PI * log(x.rows()));
      double ll_val = ll.value();

      // Deal with our zero jitter hack
      Eigen::VectorXd g;
      if (compute_jitter) {
        g = ll.derivatives();
      } else {
        g.resize(g_tot + 1);
        g(0) = 0.0;
        g.tail(g_tot) = ll.derivatives();
      }

      // Copy the result to a numpy array
      auto result = py::array_t<double>(g.size());
      auto buf = result.request();
      double* ptr = (double *) buf.ptr;
      for (int i = 0; i < g.rows(); ++i) ptr[i] = g(i);

      return std::make_tuple(ll_val, result);
#else
      throw std::exception();
#endif

    }, R"delim(
Compute the gradient of the log likelihood of the model using autodiff

The returned gradient is with respect to the jitter and the coefficients.

Args:
    jitter (float): The jitter of the kernel.
    a_real (array[j_real]): The coefficients of the real terms.
    c_real (array[j_real]): The exponents of the real terms.
    a_comp (array[j_complex]): The real part of the coefficients of the
        complex terms.
    b_comp (array[j_complex]): The imaginary part of the coefficients of
        the complex terms.
    c_comp (array[j_complex]): The real part of the exponents of the
        complex terms.
    d_comp (array[j_complex]): The imaginary part of the exponents of the
        complex terms.
    x (array[n]): The _sorted_ array of input coordinates.
    y (array[n]): The observations at ``x``.
    diag (array[n]): An array that should be added to the diagonal of the
        matrix. This often corresponds to measurement uncertainties and in
        that case, ``diag`` should be the measurement _variance_
        (i.e. sigma^2).

)delim");

#endif

  cholesky_solver.def("compute", [](PicklableCholeskySolver& solver,
      double jitter,
      const vector_t& a_real,
      const vector_t& c_real,
      const vector_t& a_comp,
      const vector_t& b_comp,
      const vector_t& c_comp,
      const vector_t& d_comp,
      const vector_t& A,
      const matrix_t& U,
      const matrix_t& V,
      const vector_t& x,
      const vector_t& diag) {
    return solver.compute(
      jitter, a_real, c_real, a_comp, b_comp, c_comp, d_comp, A, U, V, x, diag
    );
  },
  R"delim(
Compute the Cholesky factorization of the matrix

Args:
    jitter (float): The jitter of the kernel.
    a_real (array[j_real]): The coefficients of the real terms.
    c_real (array[j_real]): The exponents of the real terms.
    a_comp (array[j_complex]): The real part of the coefficients of the
        complex terms.
    b_comp (array[j_complex]): The imaginary part of the coefficients of
        the complex terms.
    c_comp (array[j_complex]): The real part of the exponents of the
        complex terms.
    d_comp (array[j_complex]): The imaginary part of the exponents of the
        complex terms.
    x (array[n]): The _sorted_ array of input coordinates.
    diag (array[n]): An array that should be added to the diagonal of the
        matrix. This often corresponds to measurement uncertainties and in
        that case, ``diag`` should be the measurement _variance_
        (i.e. sigma^2).

)delim");

  cholesky_solver.def("solve", [](PicklableCholeskySolver& solver, const matrix_t& b) {
    return solver.solve(b);
  },
  R"delim(
Solve a linear system for the matrix defined in ``compute``

A previous call to :func:`solver.CholeskySolver.compute` defines a matrix
``A`` and this method solves for ``x`` in the matrix equation ``A.x = b``.

Args:
    b (array[n] or array[n, nrhs]): The right hand side of the linear system.

Returns:
    array[n] or array[n, nrhs]: The solution of the linear system.

Raises:
    ValueError: For mismatched dimensions.

)delim");

  cholesky_solver.def("dot_solve", [](PicklableCholeskySolver& solver, const matrix_t& b) {
    return solver.dot_solve(b);
  },
  R"delim(
Solve the system ``b^T . A^-1 . b``

A previous call to :func:`solver.Solver.compute` defines a matrix ``A``
and this method solves ``b^T . A^-1 . b`` for a vector ``b``.

Args:
    b (array[n]): The right hand side of the linear system.

Returns:
    float: The solution of ``b^T . A^-1 . b``.

Raises:
    ValueError: For mismatched dimensions.

)delim");

  cholesky_solver.def("dot_L", [](PicklableCholeskySolver& solver, const matrix_t& z) {
    return solver.dot_L(z);
  },
  R"delim(
Compute the dot product of the square root of a ``celerite`` matrix

This method computes ``L.z`` where ``A = L.L^T`` is the matrix defined in
``compute``.

Args:
    z (array[n] or array[n, neq]): The matrix ``z`` described above.

Returns:
    array[n] or array[n, neq]: The dot product ``L.b`` as described above.

Raises:
    ValueError: For mismatched dimensions.

)delim");

  cholesky_solver.def("dot", [](PicklableCholeskySolver& solver,
      double jitter,
      const vector_t& a_real,
      const vector_t& c_real,
      const vector_t& a_comp,
      const vector_t& b_comp,
      const vector_t& c_comp,
      const vector_t& d_comp,
      const vector_t& A,
      const matrix_t& U,
      const matrix_t& V,
      const vector_t& x,
      const matrix_t& b) {
    return solver.dot(jitter, a_real, c_real, a_comp, b_comp, c_comp, d_comp, A, U, V, x, b);
  },
  R"delim(
Compute the dot product of a ``celerite`` matrix and another arbitrary matrix

This method computes ``A.b`` where ``A`` is defined by the parameters and
``b`` is an arbitrary matrix of the correct shape.

Args:
    jitter (float): The jitter of the kernel.
    a_real (array[j_real]): The coefficients of the real terms.
    c_real (array[j_real]): The exponents of the real terms.
    a_comp (array[j_complex]): The real part of the coefficients of the
        complex terms.
    b_comp (array[j_complex]): The imaginary part of the coefficients of
        the complex terms.
    c_comp (array[j_complex]): The real part of the exponents of the
        complex terms.
    d_comp (array[j_complex]): The imaginary part of the exponents of the
        complex terms.
    x (array[n]): The _sorted_ array of input coordinates.
    b (array[n] or array[n, neq]): The matrix ``b`` described above.

Returns:
    array[n] or array[n, neq]: The dot product ``A.b`` as described above.

Raises:
    ValueError: For mismatched dimensions.

)delim");

  cholesky_solver.def("predict", [](PicklableCholeskySolver& solver,
      const vector_t& y,
      const vector_t& x) {
    return solver.predict(y, x);
  },
  R"delim(

)delim");

  cholesky_solver.def("log_determinant", [](PicklableCholeskySolver& solver) {
    return solver.log_determinant();
  },
  R"delim(
Get the log-determinant of the matrix defined by ``compute``

Returns:
    float: The log-determinant of the matrix defined by
    :func:`solver.CholeskySolver.compute`.

)delim");

  cholesky_solver.def("computed", [](PicklableCholeskySolver& solver) {
      return solver.computed();
  },
  R"delim(
A flag that indicates if ``compute`` has been executed

Returns:
    bool: ``True`` if :func:`solver.CholeskySolver.compute` was previously
    executed successfully.

)delim");

  cholesky_solver.def("__getstate__", [](const PicklableCholeskySolver& solver) {
    return solver.serialize();
  });

  cholesky_solver.def("__setstate__", [](PicklableCholeskySolver& solver, py::tuple t) {
    if (t.size() != 8) throw std::runtime_error("Invalid state!");
    new (&solver) PicklableCholeskySolver();
    solver.deserialize(
      t[0].cast<bool>(),
      t[1].cast<int>(),
      t[2].cast<int>(),
      t[3].cast<double>(),

      t[4].cast<matrix_t>(),
      t[5].cast<matrix_t>(),
      t[6].cast<matrix_t>(),

      t[7].cast<vector_t>()
    );
  });

  return m.ptr();
}