/*

Copyright (c) 2005-2021, University of Oxford.
All rights reserved.

University of Oxford means the Chancellor, Masters and Scholars of the
University of Oxford, having an administrative office at Wellington
Square, Oxford OX1 2JD, UK.

This file is part of Chaste.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 * Neither the name of the University of Oxford nor the names of its
   contributors may be used to endorse or promote products derived from this
   software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/
#ifndef MUTABLEVERTEXMESH_HPP_
#define MUTABLEVERTEXMESH_HPP_

// Forward declaration prevents circular include chain
template<unsigned ELEMENT_DIM, unsigned SPACE_DIM>
class VertexMeshWriter;

#include <iostream>
#include <map>
#include <algorithm>

#include "ChasteSerialization.hpp"
#include <boost/serialization/vector.hpp>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/split_member.hpp>

#include "VertexMesh.hpp"
#include "RandomNumberGenerator.hpp"

/**
 * A mutable vertex-based mesh class, which inherits from VertexMesh and allows for local
 * remeshing. This is implemented through simple operations including node merging, neighbour
 * exchange ("T1 swap"), node/edge merging in the case of intersections ("T3 swap") and
 * removal of small triangular elements ("T2 swap").
 *
 * MutableVertexMesh is used as a member of the VertexBasedCellPopulation class to represent
 * the junctional network of cells that forms the basis of simulations of off-lattice
 * vertex-based models.
 */
template<unsigned ELEMENT_DIM, unsigned SPACE_DIM>
class MutableVertexMesh : public VertexMesh<ELEMENT_DIM, SPACE_DIM>
{
    friend class TestMutableVertexMesh;
    friend class TestMutableVertexMeshReMesh;
    friend class TestMutableVertexMeshRosetteMethods;

protected:

    /** The minimum distance apart that two nodes in the mesh can be without causing element rearrangement. */
    double mCellRearrangementThreshold;

    /**
     * The ratio between the minimum distance apart that two nodes in the mesh can be without causing element
     * rearrangement and their separation after remeshing.
     */
    double mCellRearrangementRatio;

    /** The area threshold at which T2 swaps occur in an apoptotic, triangular cell/element. */
    double mT2Threshold;

    /** The probability that, instead of a T1 swap, the relevant nodes merge to form a protorosette */
    double mProtorosetteFormationProbability;

    /** The probability that, in a given timestep, a protorosette node resolves into two rank-3 nodes */
    double mProtorosetteResolutionProbabilityPerTimestep;

    /** The probability that, in a given timestep, a rosette node resolves into two lower-rank nodes */
    double mRosetteResolutionProbabilityPerTimestep;

    /** Whether to check for edges intersections (true) or not (false). */
    bool mCheckForInternalIntersections;

    /** Indices of nodes that have been deleted. These indices can be reused when adding new elements/nodes. */
    std::vector<unsigned> mDeletedNodeIndices;

    /** Indices of elements that have been deleted. These indices can be reused when adding new elements. */
    std::vector<unsigned> mDeletedElementIndices;

    /**
     * Distance for T3 swap checking. At each time step we check for each boundary node whether
     * it intersects with any boundary elements (cells) whose centroids lie within this distance
     * to the node. Note that T3 swaps may not be resolved correctly if this distance is chosen
     * too small, while large values for mDistanceForT3SwapChecking may slow down the simulation.
     */
    double mDistanceForT3SwapChecking;

    /**
     * Locations of T1 swaps (the mid point of the moving nodes), stored so they can be accessed and output by the cell population.
     * The locations are stored until they are cleared by ClearLocationsOfT1Swaps().
     */
    std::vector< c_vector<double, SPACE_DIM> > mLocationsOfT1Swaps;

    /**
     * The location of the last T2 swap (the centre of the removed triangle), stored so it can be accessed by the T2SwapCellKiller.
     */
    c_vector<double, SPACE_DIM> mLastT2SwapLocation;

    /**
     * Locations of T3 swaps (the location of the intersection with the edge), stored so they can be accessed and output by the cell population.
     * The locations are stored until they are cleared by ClearLocationsOfT3Swaps().
     */
    std::vector< c_vector<double, SPACE_DIM> > mLocationsOfT3Swaps;

    /**
     * Locations of intersection swaps (the mid point of the switching nodes), stored so they can be accessed and output by the cell population.
     * The locations are stored until they are cleared by ClearLocationsOfIntersectionSwaps().
     */
    std::vector< c_vector<double, SPACE_DIM> > mLocationsOfIntersectionSwaps;

    /**
     * Divide an element along the axis passing through two of its nodes.
     *
     * \todo This method currently assumes SPACE_DIM = 2 (see #866)
     *
     * @param pElement the element to divide
     * @param nodeAIndex the local index of one node within this element
     * @param nodeBIndex the local index of another node within this element
     * @param placeOriginalElementBelow whether to place the original element below (in the y direction) the new element (defaults to false)
     *
     * @return the index of the new element
     */
    unsigned DivideElement(VertexElement<ELEMENT_DIM,SPACE_DIM>* pElement,
                           unsigned nodeAIndex,
                           unsigned nodeBIndex,
                           bool placeOriginalElementBelow=false);

    /**
     * Helper method for ReMesh().
     *
     * Check if any neighbouring nodes in an element are closer than the mCellRearrangementThreshold
     * and are not contained in any triangular elements. If any such pair of nodes are found, then
     * call IdentifySwapType(), which in turn implements the appropriate local remeshing operation
     * (a T1 swap, void removal, or node merge).
     *
     * @return whether we need to check for, and implement, any further local remeshing operations
     *                   (true if any swaps are performed).
     */
    virtual bool CheckForSwapsFromShortEdges();

    /**
     * Helper method for ReMesh().
     *
     * Check if any elements have become intersected and correct this by implementing the appropriate
     * local remeshing operation (a T3 swap or node merge).
     *
     * @return whether to recheck the mesh again
     */
    bool CheckForIntersections();

    /**
     * Helper method for ReMesh(), called by CheckForSwapsFromShortEdges() when
     * neighbouring nodes in an element have been found to be closer than the mCellRearrangementThreshold
     * and do not share any triangular elements.
     *
     * Identify the type of local remeshing operation required (T1 swap, void removal, or node merge).
     *
     * @param pNodeA one of the nodes to perform the swap with
     * @param pNodeB the other node to perform the swap
     */
    virtual void IdentifySwapType(Node<SPACE_DIM>* pNodeA, Node<SPACE_DIM>* pNodeB);

    /**
     * Helper method for ReMesh(), called by IdentifySwapType().
     *
     * Merge two given nodes in the mesh and update node/element ownership, by replacing
     * the node contained in the least number of elements with the other node. The merged
     * node is moved to the centre between the two old node positions.
     *
     * @param pNodeA one of the nodes to perform the merge with
     * @param pNodeB the other node to perform the merge with
     */
    void PerformNodeMerge(Node<SPACE_DIM>* pNodeA, Node<SPACE_DIM>* pNodeB);

    /**
     * Helper method for ReMesh(), called by IdentifySwapType().
     *
     * Perform a T1 swap on two given nodes contained in a given set of elements.
     * This involves replacing the two nodes with two new nodes placed on either
     * side of the previous shared edge, such that the edge formed by the two new nodes
     * is the perpendicular bisector of the previous shared edge, and 'just larger' (by a
     * factor mCellRearrangementRatio) than mThresholdDistance.
     *
     * @param pNodeA one of the nodes to perform the swap with
     * @param pNodeB the other node to perform the swap
     * @param rElementsContainingNodes set of common elements
     */
    void PerformT1Swap(Node<SPACE_DIM>* pNodeA, Node<SPACE_DIM>* pNodeB, std::set<unsigned>& rElementsContainingNodes);

    /**
     * Helper method for ReMesh(), called by CheckForIntersections().
     *
     * Perform an element swap to resolve the situation where a given node
     * has been found to overlap a given element not containing it.
     *
     * @param pNode pointer to the node
     * @param elementIndex global index of the element in the mesh
     */
    void PerformIntersectionSwap(Node<SPACE_DIM>* pNode, unsigned elementIndex);

    /**
     * Helper method for ReMesh(), called by CheckForT2Swaps().
     *
     * Perform a T2 swap on a given triangular element whose area is smaller than mT2Threshold
     * by replacing it with a new node located at the centroid of the former element and updating
     * node/element ownership.
     *
     * @param rElement the element to remove
     */
    void PerformT2Swap(VertexElement<ELEMENT_DIM,SPACE_DIM>& rElement);

    /**
     * Helper method for ReMesh(), called by CheckForIntersections().
     *
     * Perform a T3 swap on a given node that has been found to overlap a given element
     * by moving the node back onto the edge of that element, associating it with the
     * element and adding new nodes to maintain three elements per node.
     *
     * @param pNode pointer to the node
     * @param elementIndex global index of the element in the mesh
     */
    void PerformT3Swap(Node<SPACE_DIM>* pNode, unsigned elementIndex);

    /**
     * Helper method for ReMesh(), called by IdentifySwapType().
     *
     * Remove a triangular void bounded by three given nodes, in which one of the edges is
     * less than mCellRearrangementThreshold, through calls to PerformNodeMerge().
     *
     * @param pNodeA one of the nodes on the short edge
     * @param pNodeB the other node on the short edge
     * @param pNodeC the other node in the triangular void
     */
    void PerformVoidRemoval(Node<SPACE_DIM>* pNodeA, Node<SPACE_DIM>* pNodeB, Node<SPACE_DIM>* pNodeC);

    /**
     * Helper method for ReMesh(), called by IdentifySwapType().
     *
     * Handles the case where a swap involves a junction with more than three adjacent elements.
     * This is implemented in a separate method to allow child classes to override this behaviour
     * and implement junction remodelling with high-order nodes (see #2664).
     *
     * @param pNodeA one of the nodes to perform the swap with
     * @param pNodeB the other node to perform the swap
     */
    virtual void HandleHighOrderJunctions(Node<SPACE_DIM>* pNodeA, Node<SPACE_DIM>* pNodeB);

    /**
     * Helper method for ReMesh(), called by HandleHighOrderJunctions().
     *
     * Merge a node of a non-rosette cell with the central node
     * of a rosette, by replacing the node in the non-rosette
     * cell with that of the rosette centre, keeping the rosette
     * centre in the same position.
     *
     * @param pNodeA one of the nodes to perform the merge with
     * @param pNodeB the other node to perform the merge with
     */
    void PerformRosetteRankIncrease(Node<SPACE_DIM>* pNodeA, Node<SPACE_DIM>* pNodeB);

    /**
     * Helper method for ReMesh(), called by CheckForRosettes().
     *
     * Split protorosette in random direction.
     * Create new node and redistribute nodes along line joining
     * centres of randomly selected cell and cell opposite it.
     *
     * @param pProtorosetteNode node at centre of protorosette
     */
    void PerformProtorosetteResolution(Node<SPACE_DIM>* pProtorosetteNode);

    /**
     * Helper method for ReMesh(), called by CheckForRosettes().
     *
     * Split rosette by removing one cell at random.
     * Create new node positioned along line joining
     * rosette node and centre of randomly selected cell.
     * Rosette node will remain unmoved.
     *
     * @param pRosetteNode node at centre of rosette
     */
    void PerformRosetteRankDecrease(Node<SPACE_DIM>* pRosetteNode);

    /**
     * Helper method for ReMesh().
     *
     * Check whether the mesh contains rosettes or protorosettes, and implement resolution events
     * if necessary.
     */
    void CheckForRosettes();

    /**
     * Helper method for ReMesh(), called by PerformT3Swap(). During T3 swaps nodes are merged onto edges. This
     * method checks if the edge is too short and moves its vertices apart if necessary in order to prevent T1 swaps
     * from happening right away. The method also checks that the location where the merged node is going to end up at
     * is not too close to one of the neighbouring vertices and moves it if necessary to prevent T1 swaps.
     *
     * @param indexA  index of one of the nodes on the short edge
     * @param indexB  index of the other node on the short edge
     * @param intersection  the intersection location, i.e. the location where we are planning to put the merged node
     *
     * @return intersection, the corrected location of where we are planning to put the merged node
     */
    c_vector<double, 2> WidenEdgeOrCorrectIntersectionLocationIfNecessary(unsigned indexA, unsigned indexB, c_vector<double,2> intersection);

    /** Needed for serialization. */
    friend class boost::serialization::access;

    /**
     * Serialize the mesh.
     *
     * Note that if you are calling this method (from subclasses) you should archive your
     * member variables FIRST. So that this method can call a ReMesh
     * (to convert from TrianglesMeshReader input format into your native format).
     *
     * @param archive the archive
     * @param version the current version of this class
     */
    template<class Archive>
    void serialize(Archive & archive, const unsigned int version)
    {
        // NOTE - Subclasses must archive their member variables BEFORE calling this method.
        archive & mCellRearrangementThreshold;
        archive & mCellRearrangementRatio;
        archive & mT2Threshold;
        archive & mProtorosetteFormationProbability;
        archive & mProtorosetteResolutionProbabilityPerTimestep;
        archive & mRosetteResolutionProbabilityPerTimestep;
        archive & mCheckForInternalIntersections;
        archive & mDeletedNodeIndices;
        archive & mDeletedElementIndices;
        archive & mDistanceForT3SwapChecking;
        ///\todo: maybe we should archive the mLocationsOfT1Swaps and mDeletedNodeIndices etc. as well?

        archive & boost::serialization::base_object<VertexMesh<ELEMENT_DIM, SPACE_DIM> >(*this);
    }

public:

    /**
     * Default constructor.
     *
     * @param nodes vector of pointers to nodes
     * @param vertexElements vector of pointers to VertexElements
     * @param cellRearrangementThreshold the minimum threshold distance for element rearrangement (defaults to 0.01)
     * @param t2Threshold the maximum threshold distance for Type 2 swaps (defaults to 0.001)
     * @param cellRearrangementRatio ratio between the minimum threshold distance for element
     *                                rearrangement node separation after remeshing (defaults to 1.5)
     * @param protorosetteFormationProbability the probability of a protorosette formation event happening instead of
     *                                a T1 swap
     * @param protorosetteResolutionProbabilityPerTimestep the probability that, in a given timestep, a protorosette
     *                                will resolve (similar to the completion of a T1 swap)
     * @param rosetteResolutionProbabilityPerTimestep the probability that, in a given timestep, a rosette will
     *                                resolve (reduce the number of cells sharing a common vertex by 1)
     */
    MutableVertexMesh(std::vector<Node<SPACE_DIM>*> nodes,
                      std::vector<VertexElement<ELEMENT_DIM, SPACE_DIM>*> vertexElements,
                      double cellRearrangementThreshold=0.01,
                      double t2Threshold=0.001,
                      double cellRearrangementRatio=1.5,
                      double protorosetteFormationProbability=0.0,
                      double protorosetteResolutionProbabilityPerTimestep=0.0,
                      double rosetteResolutionProbabilityPerTimestep=0.0);

    /**
     * Default constructor for use by serializer.
     */
    MutableVertexMesh();

    /**
     * Destructor.
     */
    virtual ~MutableVertexMesh();

    /**
     * Set method for mCellRearrangementThreshold.
     *
     * @param cellRearrangementThreshold
     */
    void SetCellRearrangementThreshold(double cellRearrangementThreshold);

    /**
     * Set method for mT2Threshold.
     *
     * @param t2Threshold
     */
    void SetT2Threshold(double t2Threshold);

    /**
     * Set method for mCellRearrangementRatio.
     *
     * @param cellRearrangementRatio
     */
    void SetCellRearrangementRatio(double cellRearrangementRatio);

    /**
     * Set method for mProtoRosetteFormationProbability.
     *
     * @param protorosetteFormationProbability the new value of mProtoRosetteFormationProbability
     */
    void SetProtorosetteFormationProbability(double protorosetteFormationProbability);

    /**
     * Set method for mProtoRosetteResolutionProbabilityPerTimestep.
     *
     * @param protorosetteResolutionProbabilityPerTimestep the new value of mProtoRosetteResolutionProbabilityPerTimestep
     */
    void SetProtorosetteResolutionProbabilityPerTimestep(double protorosetteResolutionProbabilityPerTimestep);

    /**
     * Set method for mRosetteResolutionProbabilityPerTimestep.
     *
     * @param rosetteResolutionProbabilityPerTimestep the new value of mRosetteResolutionProbabilityPerTimestep
     */
    void SetRosetteResolutionProbabilityPerTimestep(double rosetteResolutionProbabilityPerTimestep);

    /**
     * Move the node with a particular index to a new point in space.
     *
     * @param nodeIndex the index of the node to be moved
     * @param point the new target location of the node
     */
    virtual void SetNode(unsigned nodeIndex, ChastePoint<SPACE_DIM> point);

    /**
     * Set method for mCheckForInternalIntersections.
     *
     * @param checkForInternalIntersections
     */
    void SetCheckForInternalIntersections(bool checkForInternalIntersections);

    /**
     * @return mCellRearrangementThreshold
     */
    double GetCellRearrangementThreshold() const;

    /**
     * @return mT2Threshold
     */
    double GetT2Threshold() const;

    /**
     * @return mCellRearrangementRatio
     */
    double GetCellRearrangementRatio() const;

    /**
     * Get method for mProtoRosetteFormationProbability.
     *
     * @return mProtoRosetteFormationProbability
     */
    double GetProtorosetteFormationProbability() const;

    /**
     * Get method for mProtoRosetteResolutionProbabilityPerTimestep.
     *
     * @return mProtoRosetteResolutionProbabilityPerTimestep
     */
    double GetProtorosetteResolutionProbabilityPerTimestep() const;

    /**
     * Get method for mRosetteResolutionProbabilityPerTimestep.
     *
     * @return mRosetteResolutionProbabilityPerTimestep
     */
    double GetRosetteResolutionProbabilityPerTimestep() const;

    /**
     * Set distance for T3 swap checking. At each time step we check for each boundary node whether
     * it intersects with any boundary elements (cells) whose centroids lie within this distance
     * to the node. Note that T3 swaps may not be resolved correctly if this distance is chosen
     * too small, while large values for mDistanceForT3SwapChecking may slow down the simulation.
     *
     * @param distanceForT3SwapChecking
     */
    void SetDistanceForT3SwapChecking( double distanceForT3SwapChecking );

    /**
     * Get Distance for T3 swap checking.
     *
     * @return mDistanceForT3SwapChecking
     */
    double GetDistanceForT3SwapChecking() const;

    /**
     * @return the number of Nodes in the mesh.
     */
    unsigned GetNumNodes() const;

    /**
     * @return the number of VertexElements in the mesh.
     */
    unsigned GetNumElements() const;

    /**
     * @return mCheckForInternalIntersections, either to check for edges intersections or not.
     */
    bool GetCheckForInternalIntersections() const;

    /**
     * @return the locations of the T1 swaps
     */
    std::vector< c_vector<double, SPACE_DIM> > GetLocationsOfT1Swaps();

    /**
     * @return the location of the last T2 swap
     */
    c_vector<double, SPACE_DIM> GetLastT2SwapLocation();

    /**
     * @return the locations of the T3 swaps
     */
    std::vector< c_vector<double, SPACE_DIM> > GetLocationsOfT3Swaps();

    /**
     * @return the locations of the intersection swaps
     */
    std::vector< c_vector<double, SPACE_DIM> > GetLocationsOfIntersectionSwaps();

    /**
     * Helper method to clear the stored T1 swaps
     */
    void ClearLocationsOfT1Swaps();

    /**
     * Helper method to clear the stored T3 swaps
     */
    void ClearLocationsOfT3Swaps();

    /**
     * Helper method to clear the stored intersection swaps
     */
    void ClearLocationsOfIntersectionSwaps();

    /**
     * Add a node to the mesh.
     *
     * Note: After calling this one or more times, you must then call ReMesh.
     *
     * @param pNewNode pointer to the new node
     * @return the global index of the new node in the mesh.
     */
    unsigned AddNode(Node<SPACE_DIM>* pNewNode);

    /**
     * Mark an element as deleted. Note that it DOES NOT deal with the associated
     * nodes and therefore should only be called immediately prior to a ReMesh()
     * being called.
     *
     * @param index  the global index of a specified vertex element
     */
    void DeleteElementPriorToReMesh(unsigned index);

    /**
     * Mark a given node as deleted. Note that this method DOES NOT deal with the
     * associated elements and therefore should only be called immediately prior
     * to a ReMesh() being called.
     *
     * @param index The index of the node to delete
     */
    void DeleteNodePriorToReMesh(unsigned index);

    /**
     * Divide an element along its short axis.
     *
     * @param pElement the element to divide
     * @param placeOriginalElementBelow whether to place the original element below (in the y direction) the new element (defaults to false)
     *
     * @return the index of the new element
     */
    unsigned DivideElementAlongShortAxis(VertexElement<ELEMENT_DIM,SPACE_DIM>* pElement,
                                         bool placeOriginalElementBelow=false);

    /**
     * Divide an element along a specified axis.
     *
     * If the new nodes (intersections of axis with element) are within
     * mCellRearrangementThreshold of existing nodes then they are
     * moved 2*mCellRearrangementThreshold away.
     *
     * @param pElement the element to divide
     * @param axisOfDivision axis to divide the element by
     * @param placeOriginalElementBelow whether to place the original element below (in the y direction) the new element (defaults to false)
     *
     * @return the index of the new element
     */
    unsigned DivideElementAlongGivenAxis(VertexElement<ELEMENT_DIM,SPACE_DIM>* pElement,
                                         c_vector<double, SPACE_DIM> axisOfDivision,
                                         bool placeOriginalElementBelow=false);

    /**
     * Add an element to the mesh.
     *
     * @param pNewElement the new element
     *
     * @return the index of the new element in the mesh
     */
    unsigned AddElement(VertexElement<ELEMENT_DIM, SPACE_DIM>* pNewElement);

    /**
     * Helper method for ReMesh().
     *
     * Check for any triangular element whose area is smaller than mT2Threshold
     * and call PerformT2Swap() on any such element.
     *
     * @param rElementMap a VertexElementMap which associates the indices of VertexElements in the old mesh
     *                   with indices of VertexElements in the new mesh.  This should be created
     *                   with the correct size, GetNumElements()
     *
     * @return whether we need to check for, and implement, any further local remeshing operations
     */
    bool CheckForT2Swaps(VertexElementMap& rElementMap);

    /**
     * Delete mNodes and mElements.
     */
    void Clear();

    /**
     * Add a node on the edge between two nodes.
     *
     * @param pNodeA a pointer to one node
     * @param pNodeB a pointer to the other nodes
     */
    void DivideEdge(Node<SPACE_DIM>* pNodeA, Node<SPACE_DIM>* pNodeB);

    /**
     * Helper method for ReMesh(). Removes the deleted nodes and elements from the mesh and updates the
     * rElementMap accordingly.
     *
     * @param rElementMap a VertexElementMap which associates the indices of VertexElements in the old mesh
     *                   with indices of VertexElements in the new mesh.  This should be created
     *                   with the correct size, GetNumElements()
     */
    void RemoveDeletedNodesAndElements(VertexElementMap& rElementMap);

    /**
     * Helper method for ReMesh(). Removes the deleted nodes from the mesh and relabels the node indices.
     */
    void RemoveDeletedNodes();

    /**
     * Update the state of the mesh by implementing any local remeshing operations (node merging,
     * or T1, T2 or T3 swaps) that are required, and store any changes in element indices using
     * the given VertexElementMap.
     *
     * This method calls several other methods, in particular CheckForT2Swaps(), CheckForSwapsFromShortEdges()
     * and CheckForIntersections().
     *
     * @param rElementMap a VertexElementMap which associates the indices of VertexElements in the old mesh
     *                   with indices of VertexElements in the new mesh.  This should be created
     *                   with the correct size, GetNumElements()
     */
    virtual void ReMesh(VertexElementMap& rElementMap);

    /**
     * Alternative version of ReMesh which takes no parameters and does not require a VertexElementMap.
     * Note: inherited classes should overload ReMesh(VertexElementMap&).
     *
     * \todo This method seems to be redundant; remove it? (#2401)
     */
    void ReMesh();
};

#include "SerializationExportWrapper.hpp"
EXPORT_TEMPLATE_CLASS_ALL_DIMS(MutableVertexMesh)

#endif /*MUTABLEVERTEXMESH_HPP_*/


/*
 * Copyright (c) 2011, Mattia Penati <mattia.penati@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 *     * Neither the name of the Politecnico di Milano nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#ifndef AMA_TENSOR_MUL_MUL_REDUCER_HPP
#define AMA_TENSOR_MUL_MUL_REDUCER_HPP 1

#include <ama/tensor/detail/tensor_base.hpp>
#include <ama/tensor/mul/mul_calculator.hpp>
#include <ama/tensor/mul/mul_temporary.hpp>
#include <ama/tensor/iexp/iexp_copy.hpp>
#include <ama/tensor/iexp/indices.hpp>
#include <ama/tensor/iexp/sum.hpp>
#include <boost/mpl/assert.hpp>
#include <boost/mpl/bool.hpp>
#include <boost/mpl/equal_to.hpp>
#include <boost/mpl/size.hpp>

namespace ama
{
  namespace tensor_
  {

    /* forward declaration */
    template <typename LEFT, typename RIGHT> class mul_reducer;

    /* specialization of traits */
    template <typename LEFT, typename RIGHT>
    struct tensor_traits< mul_reducer<LEFT, RIGHT> >
    {
      typedef typename LEFT::value_type value_type;

      typedef typename LEFT::dimension_type dimension_type;

      typedef mpl::size<typename iexp_reduce<
            typename mul_index<LEFT, RIGHT>::tensor_type
          , typename mul_index<LEFT, RIGHT>::index_list
          >::controvariant> controvariant_type;
      typedef mpl::size<typename iexp_reduce<
            typename mul_index<LEFT, RIGHT>::tensor_type
          , typename mul_index<LEFT, RIGHT>::index_list
          >::covariant> covariant_type;

      typedef ::boost::mpl::false_ is_assignable;
      typedef ::boost::mpl::true_ is_temporary;
    };

    /* class declaration */
    template <typename LEFT, typename RIGHT>
    class mul_reducer:
          public tensor_base< mul_reducer<LEFT, RIGHT> >
    {
    protected:
      typedef tensor_base< mul_reducer<LEFT, RIGHT> > base_type;
      typedef mul_reducer<LEFT, RIGHT> derived_type;

      typedef LEFT left_operand;
      typedef RIGHT right_operand;

      typedef mul_temporary<left_operand, right_operand> iexp_type;

    public:
      /* constructor */
      mul_reducer(left_operand const & left,
                  right_operand const & right)
          : m_iexp(left, right) { }

    public:
      typedef typename base_type::value_type value_type;
      typedef typename base_type::dimension_type dimension_type;

      template <typename ILIST>
      value_type at() const
      {
        namespace mpl = ::boost::mpl;

        typedef typename iexp_reduce<
              typename mul_index<LEFT, RIGHT>::tensor_type
            , typename mul_index<LEFT, RIGHT>::index_list
            >::index_list index_list; /* other indices */
        typedef typename iexp_reduce<
              typename mul_index<LEFT, RIGHT>::tensor_type
            , typename mul_index<LEFT, RIGHT>::index_list
            >::reduce_list sum_indices; /* index of sum */

        BOOST_MPL_ASSERT_MSG(
              (mpl::equal_to< mpl::size<ILIST> , mpl::size<index_list> >::value)
            , THE_INDICES_LIST_MUST_HAVE_THE_SAME_SIZE
            , (ILIST));

        /* indeces (not for sum) */
        typedef typename make_imap<index_list, ILIST>::type imap;
        typedef typename mpl::size<sum_indices>::type order;

        return sum<dimension_type, order>::template apply<imap, sum_indices>(m_iexp);
      }

    protected:
      iexp_type const m_iexp;
    };

  }
}

#endif /* AMA_TENSOR_MUL_MUL_REDUCER_HPP */


/*!
  \file gpp_python_knowledge_gradient.cpp
  \rst
  This file has the logic to invoke C++ functions pertaining to knowledge gradient from Python.
  The data flow follows the basic 4 step from gpp_python_common.hpp.

  .. NoteL: several internal functions of this source file are only called from ``Export*()`` functions,
  so their description, inputs, outputs, etc. comments have been moved. These comments exist in
  ``Export*()`` as Python docstrings, so we saw no need to repeat ourselves.
\endrst*/
// This include violates the Google Style Guide by placing an "other" system header ahead of C and C++ system headers.  However,
// it needs to be at the top, otherwise compilation fails on some systems with some versions of python: OS X, python 2.7.3.
// Putting this include first prevents pyport from doing something illegal in C++; reference: http://bugs.python.org/issue10910
#include "Python.h"  // NOLINT(build/include)

#include "gpp_python_knowledge_gradient.hpp"

// NOLINT-ing the C, C++ header includes as well; otherwise cpplint gets confused
#include <string>  // NOLINT(build/include_order)
#include <vector>  // NOLINT(build/include_order)

#include <boost/python/bases.hpp>  // NOLINT(build/include_order)
#include <boost/python/class.hpp>  // NOLINT(build/include_order)
#include <boost/python/def.hpp>  // NOLINT(build/include_order)
#include <boost/python/dict.hpp>  // NOLINT(build/include_order)
#include <boost/python/extract.hpp>  // NOLINT(build/include_order)
#include <boost/python/list.hpp>  // NOLINT(build/include_order)
#include <boost/python/object.hpp>  // NOLINT(build/include_order)

#include "gpp_common.hpp"
#include "gpp_domain.hpp"
#include "gpp_exception.hpp"
#include "gpp_knowledge_gradient_optimization.hpp"
#include "gpp_geometry.hpp"
#include "gpp_math.hpp"
#include "gpp_optimization.hpp"
#include "gpp_optimizer_parameters.hpp"
#include "gpp_python_common.hpp"

namespace optimal_learning {

namespace {

double ComputePosteriorMeanWrapper(const GaussianProcess& gaussian_process,
                                   const int num_fidelity,
                                   const boost::python::list& points_to_sample) {
  int num_derivatives_input = 0;
  const boost::python::list gradients;

  PythonInterfaceInputContainer input_container(points_to_sample, gradients, gaussian_process.dim()-num_fidelity, 1, num_derivatives_input);

  bool configure_for_gradients = false;
  PosteriorMeanEvaluator ps_evaluator(gaussian_process);
  PosteriorMeanEvaluator::StateType ps_state(ps_evaluator, num_fidelity, input_container.points_to_sample.data(), configure_for_gradients);

  return ps_evaluator.ComputePosteriorMean(&ps_state);
}

boost::python::list ComputeGradPosteriorMeanWrapper(const GaussianProcess& gaussian_process,
                                                    const int num_fidelity,
                                                    const boost::python::list& points_to_sample) {
  int num_derivatives_input = 0;
  const boost::python::list gradients;

  PythonInterfaceInputContainer input_container(points_to_sample, gradients, gaussian_process.dim()-num_fidelity, 1, num_derivatives_input);

  std::vector<double> grad_PS(input_container.dim);
  bool configure_for_gradients = true;

  PosteriorMeanEvaluator ps_evaluator(gaussian_process);
  PosteriorMeanEvaluator::StateType ps_state(ps_evaluator, num_fidelity, input_container.points_to_sample.data(), configure_for_gradients);

  ps_evaluator.ComputeGradPosteriorMean(&ps_state, grad_PS.data());
  return VectorToPylist(grad_PS);
}

double ComputeKnowledgeGradientWrapper(const GaussianProcess& gaussian_process,
                                       const int num_fidelity,
                                       const boost::python::object& optimizer_parameters,
                                       const boost::python::list& domain_bounds,
                                       const boost::python::list& discrete_pts,
                                       const boost::python::list& points_to_sample,
                                       const boost::python::list& points_being_sampled,
                                       int num_pts, int num_to_sample, int num_being_sampled,
                                       int max_int_steps, double best_so_far, RandomnessSourceContainer& randomness_source) {
  int num_derivatives_input = 0;
  const boost::python::list gradients;

  PythonInterfaceInputContainer input_container_discrete(discrete_pts, gradients, gaussian_process.dim()-num_fidelity, num_pts, num_derivatives_input);
  PythonInterfaceInputContainer input_container(points_to_sample, points_being_sampled, gradients, gaussian_process.dim(),
                                                num_to_sample, num_being_sampled, num_derivatives_input);

  bool configure_for_gradients = false;

  std::vector<ClosedInterval> domain_bounds_C(input_container.dim-num_fidelity);
  CopyPylistToClosedIntervalVector(domain_bounds, input_container.dim-num_fidelity, domain_bounds_C);
  //TensorProductDomain domain(domain_bounds_C.data(), input_container.dim);
  TensorProductDomain inner_domain(domain_bounds_C.data(), input_container.dim-num_fidelity);

  const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));

  KnowledgeGradientEvaluator<TensorProductDomain> kg_evaluator(gaussian_process, num_fidelity, input_container_discrete.points_to_sample.data(),
                                                               num_pts, max_int_steps, inner_domain, gradient_descent_parameters, best_so_far);
  KnowledgeGradientEvaluator<TensorProductDomain>::StateType kg_state(kg_evaluator, input_container.points_to_sample.data(),
                                                                      input_container.points_being_sampled.data(),
                                                                      input_container.num_to_sample,
                                                                      input_container.num_being_sampled,
                                                                      input_container_discrete.num_to_sample,
                                                                      gaussian_process.derivatives().data(), gaussian_process.num_derivatives(),
                                                                      configure_for_gradients,
                                                                      randomness_source.normal_rng_vec.data());
  return kg_evaluator.ComputeKnowledgeGradient(&kg_state);
}

boost::python::list ComputeGradKnowledgeGradientWrapper(const GaussianProcess& gaussian_process,
                                                        const int num_fidelity,
                                                        const boost::python::object& optimizer_parameters,
                                                        const boost::python::list& domain_bounds,
                                                        const boost::python::list& discrete_pts,
                                                        const boost::python::list& points_to_sample,
                                                        const boost::python::list& points_being_sampled,
                                                        int num_pts, int num_to_sample, int num_being_sampled,
                                                        int max_int_steps, double best_so_far, RandomnessSourceContainer& randomness_source) {
  int num_derivatives_input = 0;
  const boost::python::list gradients;

  PythonInterfaceInputContainer input_container_discrete(discrete_pts, gradients, gaussian_process.dim()-num_fidelity, num_pts, num_derivatives_input);

  PythonInterfaceInputContainer input_container(points_to_sample, points_being_sampled, gradients, gaussian_process.dim(),
                                                num_to_sample, num_being_sampled, num_derivatives_input);

  std::vector<double> grad_KG(num_to_sample*input_container.dim);
  bool configure_for_gradients = true;

  std::vector<ClosedInterval> domain_bounds_C(input_container.dim-num_fidelity);
  CopyPylistToClosedIntervalVector(domain_bounds, input_container.dim-num_fidelity, domain_bounds_C);
  //TensorProductDomain domain(domain_bounds_C.data(), input_container.dim);
  TensorProductDomain inner_domain(domain_bounds_C.data(), input_container.dim- num_fidelity);

  const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));

  KnowledgeGradientEvaluator<TensorProductDomain> kg_evaluator(gaussian_process, num_fidelity, input_container_discrete.points_to_sample.data(),
                                                               num_pts, max_int_steps, inner_domain, gradient_descent_parameters, best_so_far);
  KnowledgeGradientEvaluator<TensorProductDomain>::StateType kg_state(kg_evaluator, input_container.points_to_sample.data(),
                                                                      input_container.points_being_sampled.data(),
                                                                      input_container.num_to_sample,
                                                                      input_container.num_being_sampled,
                                                                      input_container_discrete.num_to_sample,
                                                                      gaussian_process.derivatives().data(), gaussian_process.num_derivatives(),
                                                                      configure_for_gradients, randomness_source.normal_rng_vec.data());
  kg_evaluator.ComputeGradKnowledgeGradient(&kg_state, grad_KG.data());

  return VectorToPylist(grad_KG);
}

/*!\rst
  Utility that dispatches KG optimization based on optimizer type and num_to_sample.
  This is just used to reduce copy-pasted code.

  \param
    :optimizer_parameters: python/cpp_wrappers/optimization._CppOptimizerParameters
      Python object containing the DomainTypes domain_type and OptimizerTypes optimzer_type to use as well as
      appropriate parameter structs e.g., NewtonParameters for type kNewton).
      See comments on the python interface for multistart_expected_improvement_optimization_wrapper
    :gaussian_process: GaussianProcess object (holds points_sampled, values, noise_variance, derived quantities) that describes the
      underlying GP
    :input_container: PythonInterfaceInputContainer object containing data about points_being_sampled
    :domain: object specifying the domain to optimize over (see gpp_domain.hpp)
    :optimizer_type: type of optimization to use (e.g., null, gradient descent)
    :num_to_sample: how many simultaneous experiments you would like to run (i.e., the q in q,p-KG)
    :best_so_far: value of the best sample so far (must be min(points_sampled_value))
    :max_int_steps: maximum number of MC iterations
    :max_num_threads: maximum number of threads for use by OpenMP (generally should be <= # cores)
    :randomness_source: object containing randomness sources (sufficient for multithreading) used in KG computation
    :status: pydict object; cannot be None
  \output
    :randomness_source: PRNG internal states modified
    :status: modified on exit to describe whether convergence occurred
    :best_points_to_sample[num_to_sample][dim]: next set of points to evaluate
\endrst*/

template <typename DomainType>
void DispatchKnowledgeGradientOptimization(const boost::python::object& optimizer_parameters,
                                           const boost::python::object& optimizer_parameters_inner,
                                           const GaussianProcess& gaussian_process, const int num_fidelity,
                                           const PythonInterfaceInputContainer& input_container_discrete,
                                           const PythonInterfaceInputContainer& input_container,
                                           const DomainType& domain, const DomainType& inner_domain,
                                           OptimizerTypes optimizer_type, int num_to_sample, double best_so_far,
                                           int max_int_steps, int max_num_threads,
                                           RandomnessSourceContainer& randomness_source,
                                           boost::python::dict& status,
                                           double * restrict best_points_to_sample) {

  bool found_flag = false;
  switch (optimizer_type) {
    case OptimizerTypes::kNull: {
      ThreadSchedule thread_schedule(max_num_threads, omp_sched_static);
      // optimizer_parameters must contain an int num_random_samples field, extract it
      const GradientDescentParameters& gradient_descent_parameters_inner = boost::python::extract<GradientDescentParameters&>(optimizer_parameters_inner.attr("optimizer_parameters"));
      int num_random_samples = boost::python::extract<int>(optimizer_parameters.attr("num_random_samples"));

      ComputeKGOptimalPointsToSampleViaLatinHypercubeSearch(gaussian_process, num_fidelity, gradient_descent_parameters_inner, domain, inner_domain,
                                                            thread_schedule, input_container.points_being_sampled.data(),
                                                            input_container_discrete.points_to_sample.data(),
                                                            num_random_samples, num_to_sample,
                                                            input_container.num_being_sampled,
                                                            input_container_discrete.num_to_sample,
                                                            best_so_far, max_int_steps,
                                                            &found_flag, &randomness_source.uniform_generator,
                                                            randomness_source.normal_rng_vec.data(),
                                                            best_points_to_sample);
      status[std::string("lhc_") + domain.kName + "_domain_found_update"] = found_flag;
      break;
    }  // end case kNull optimizer_type
    case OptimizerTypes::kGradientDescent: {
      // optimizer_parameters must contain a optimizer_parameters field
      // of type GradientDescentParameters. extract it
      const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));
      const GradientDescentParameters& gradient_descent_parameters_inner = boost::python::extract<GradientDescentParameters&>(optimizer_parameters_inner.attr("optimizer_parameters"));
      ThreadSchedule thread_schedule(max_num_threads, omp_sched_dynamic);
      int num_random_samples = boost::python::extract<int>(optimizer_parameters.attr("num_random_samples"));

      bool random_search_only = false;
      ComputeKGOptimalPointsToSample(gaussian_process, num_fidelity, gradient_descent_parameters, gradient_descent_parameters_inner, domain, inner_domain, thread_schedule,
                                     input_container.points_being_sampled.data(), input_container_discrete.points_to_sample.data(),
                                     num_to_sample, input_container.num_being_sampled, input_container_discrete.num_to_sample,
                                     best_so_far, max_int_steps, random_search_only, num_random_samples, &found_flag,
                                     &randomness_source.uniform_generator,
                                     randomness_source.normal_rng_vec.data(), best_points_to_sample);

      status[std::string("gradient_descent_") + domain.kName + "_domain_found_update"] = found_flag;
      break;
    }  // end case kGradientDescent optimizer_type
    default: {
      std::fill(best_points_to_sample, best_points_to_sample + input_container.dim*num_to_sample, 0.0);
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid optimizer choice. Setting all coordinates to 0.0.");
      break;
    }
  }  // end switch over optimizer_type
}

boost::python::list MultistartKnowledgeGradientOptimizationWrapper(const boost::python::object& optimizer_parameters,
                                                                   const boost::python::object& optimizer_parameters_inner,
                                                                   const GaussianProcess& gaussian_process, const int num_fidelity,
                                                                   const boost::python::list& domain_bounds,
                                                                   const boost::python::list& discrete_pts,
                                                                   const boost::python::list& points_being_sampled,
                                                                   int num_pts, int num_to_sample, int num_being_sampled,
                                                                   double best_so_far, int max_int_steps, int max_num_threads,
                                                                   RandomnessSourceContainer& randomness_source,
                                                                   boost::python::dict& status) {
  // TODO(GH-131): make domain objects constructible from python; and pass them in through
  // the optimizer_parameters python object

  // abort if we do not have enough sources of randomness to run with max_num_threads
  if (unlikely(max_num_threads > static_cast<int>(randomness_source.normal_rng_vec.size()))) {
    OL_THROW_EXCEPTION(LowerBoundException<int>, "Fewer randomness_sources than max_num_threads.", randomness_source.normal_rng_vec.size(), max_num_threads);
  }

  int num_to_sample_input = 0;  // No points to sample; we are generating these via KG optimization
  const boost::python::list points_to_sample_dummy;

  int num_derivatives_input = 0;
  const boost::python::list gradients;

  PythonInterfaceInputContainer input_container_discrete(discrete_pts, gradients, gaussian_process.dim()-num_fidelity, num_pts, num_derivatives_input);
  PythonInterfaceInputContainer input_container(points_to_sample_dummy, points_being_sampled, gradients, gaussian_process.dim(),
                                                num_to_sample_input, num_being_sampled, num_derivatives_input);

  std::vector<ClosedInterval> domain_bounds_C(input_container.dim);
  CopyPylistToClosedIntervalVector(domain_bounds, input_container.dim, domain_bounds_C);

  std::vector<double> best_points_to_sample_C(input_container.dim*num_to_sample);

  DomainTypes domain_type = boost::python::extract<DomainTypes>(optimizer_parameters.attr("domain_type"));
  OptimizerTypes optimizer_type = boost::python::extract<OptimizerTypes>(optimizer_parameters.attr("optimizer_type"));
  switch (domain_type) {
    case DomainTypes::kTensorProduct: {
      TensorProductDomain domain(domain_bounds_C.data(), input_container.dim);
      TensorProductDomain inner_domain(domain_bounds_C.data(), input_container.dim- num_fidelity);

      DispatchKnowledgeGradientOptimization(optimizer_parameters, optimizer_parameters_inner, gaussian_process, num_fidelity, input_container_discrete,
                                            input_container, domain, inner_domain, optimizer_type, num_to_sample, best_so_far,
                                            max_int_steps, max_num_threads, randomness_source, status, best_points_to_sample_C.data());
      break;
    }  // end case OptimizerTypes::kTensorProduct
    case DomainTypes::kSimplex: {
      SimplexIntersectTensorProductDomain domain(domain_bounds_C.data(), input_container.dim);
      SimplexIntersectTensorProductDomain inner_domain(domain_bounds_C.data(), input_container.dim- num_fidelity);

      DispatchKnowledgeGradientOptimization(optimizer_parameters, optimizer_parameters_inner, gaussian_process, num_fidelity, input_container_discrete,
                                            input_container, domain, inner_domain, optimizer_type, num_to_sample, best_so_far,
                                            max_int_steps, max_num_threads, randomness_source, status, best_points_to_sample_C.data());
      break;
    }  // end case OptimizerTypes::kSimplex
    default: {
      std::fill(best_points_to_sample_C.begin(), best_points_to_sample_C.end(), 0.0);
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid domain choice. Setting all coordinates to 0.0.");
      break;
    }
  }  // end switch over domain_type
  return VectorToPylist(best_points_to_sample_C);
}

boost::python::list ComputeOptimalPosteriorMeanWrapper(const GaussianProcess& gaussian_process, const int num_fidelity,
                                                       const boost::python::object& optimizer_parameters,
                                                       const boost::python::list& domain_bounds,
                                                       const boost::python::list& initial_guess,
                                                       boost::python::dict& status) {
    int dim = gaussian_process.dim();

    int num_derivatives_input = 0;
    const boost::python::list gradients;

    PythonInterfaceInputContainer input_container(initial_guess, gradients, gaussian_process.dim()-num_fidelity, 1, num_derivatives_input);

    std::vector<ClosedInterval> domain_bounds_C(dim-num_fidelity);
    CopyPylistToClosedIntervalVector(domain_bounds, dim-num_fidelity, domain_bounds_C);

    std::vector<double> best_points_to_sample_C(dim-num_fidelity);

    bool found_flag = false;
    const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));

    DomainTypes domain_type = boost::python::extract<DomainTypes>(optimizer_parameters.attr("domain_type"));
    switch (domain_type) {
      case DomainTypes::kTensorProduct: {
        TensorProductDomain inner_domain(domain_bounds_C.data(), dim - num_fidelity);

        ComputeOptimalPosteriorMean(gaussian_process, num_fidelity, gradient_descent_parameters, inner_domain, input_container.points_to_sample.data(),
                                    &found_flag, best_points_to_sample_C.data());
        break;
      }  // end case OptimizerTypes::kTensorProduct
      case DomainTypes::kSimplex: {
        SimplexIntersectTensorProductDomain inner_domain(domain_bounds_C.data(), dim - num_fidelity);

        ComputeOptimalPosteriorMean(gaussian_process, num_fidelity, gradient_descent_parameters, inner_domain, input_container.points_to_sample.data(),
                                    &found_flag, best_points_to_sample_C.data());
        break;
      }  // end case OptimizerTypes::kSimplex
      default: {
        std::fill(best_points_to_sample_C.begin(), best_points_to_sample_C.end(), 0.0);
        OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid domain choice. Setting all coordinates to 0.0.");
        break;
      }
    }  // end switch over domain_type
    return VectorToPylist(best_points_to_sample_C);
}

boost::python::list EvaluateKGAtPointListWrapper(const GaussianProcess& gaussian_process,
                                                 const int num_fidelity, const boost::python::object& optimizer_parameters,
                                                 const boost::python::list& domain_bounds,
                                                 const boost::python::list& discrete_being_sampled,
                                                 const boost::python::list& initial_guesses,
                                                 int num_multistarts, int num_pts, int num_to_sample,
                                                 int num_being_sampled, double best_so_far,
                                                 int max_int_steps, int max_num_threads,
                                                 RandomnessSourceContainer& randomness_source,
                                                 boost::python::dict& status) {
  // abort if we do not have enough sources of randomness to run with max_num_threads
  if (unlikely(max_num_threads > static_cast<int>(randomness_source.normal_rng_vec.size()))) {
    OL_THROW_EXCEPTION(LowerBoundException<int>, "Fewer randomness_sources than max_num_threads.", randomness_source.normal_rng_vec.size(), max_num_threads);
  }

  int num_derivatives_input = 0;
  const boost::python::list gradients;

  PythonInterfaceInputContainer input_container(discrete_being_sampled, gradients, gaussian_process.dim(), num_pts+num_being_sampled, num_derivatives_input);

  std::vector<double> result_point_C(input_container.dim);  // not used
  std::vector<double> result_function_values_C(num_multistarts);
  std::vector<double> initial_guesses_C(input_container.dim * num_multistarts);

  CopyPylistToVector(initial_guesses, input_container.dim * num_multistarts, initial_guesses_C);

  ThreadSchedule thread_schedule(max_num_threads, omp_sched_static);
  bool found_flag = false;

  std::vector<ClosedInterval> domain_bounds_C(input_container.dim);
  CopyPylistToClosedIntervalVector(domain_bounds, input_container.dim, domain_bounds_C);
  TensorProductDomain domain(domain_bounds_C.data(), input_container.dim);
  TensorProductDomain inner_domain(domain_bounds_C.data(), input_container.dim- num_fidelity);

  const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));

  EvaluateKGAtPointList(gaussian_process, num_fidelity, gradient_descent_parameters, domain, inner_domain, thread_schedule, initial_guesses_C.data(),
                        input_container.points_to_sample.data() + input_container.dim*num_pts, input_container.points_to_sample.data(),
                        num_multistarts, num_to_sample, num_being_sampled,
                        num_pts, best_so_far, max_int_steps, &found_flag, randomness_source.normal_rng_vec.data(),
                        result_function_values_C.data(), result_point_C.data());

  status["evaluate_KG_at_point_list"] = found_flag;

  return VectorToPylist(result_function_values_C);
}
}  // end unnamed namespace

void ExportKnowldegeGradientFunctions() {
  boost::python::def("compute_posterior_mean", ComputePosteriorMeanWrapper, R"%%(
    Compute knowledge gradient.
    If ``num_to_sample == 1`` and ``num_being_sampled == 0`` AND ``force_monte_carlo is false``, this will
    use (fast/accurate) analytic evaluation.
    Otherwise monte carlo-based KG computation is used.

    :param gaussian_process: GaussianProcess object (holds points_sampled, values, noise_variance, derived quantities)
    :type gaussian_process: GPP.GaussianProcess (boost::python ctor wrapper around optimal_learning::GaussianProcess)
    :param points_to_sample: initial points to load into state (must be a valid point for the problem);
      i.e., points at which to evaluate EI and/or its gradient
    :type points_to_sample: list of float64 with shape (num_to_sample, dim)
    :param points_being_sampled: points that are being sampled in concurrently experiments
    :type points_being_sampled: list of float64 with shape (num_being_sampled, dim)
    :param num_to_sample: number of potential future samples; gradients are evaluated wrt these points (i.e., the "q" in q,p-EI)
    :type num_to_sample: int > 0
    :param num_being_sampled: number of points being sampled concurrently (i.e., the p in q,p-EI)
    :type num_being_sampled: int >= 0
    :param max_int_steps: number of MC integration points in EI
    :type max_int_steps: int >= 0
    :param best_so_far: best known value of objective so far
    :type best_so_far: float64
    :param force_monte_carlo: true to force monte carlo evaluation of EI
    :type force_monte_carlo: bool
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :return: computed EI
    :rtype: float64 >= 0.0
    )%%");

  boost::python::def("compute_grad_posterior_mean", ComputeGradPosteriorMeanWrapper, R"%%(
    Compute the gradient of knowledge gradient evaluated at points_to_sample.
    If num_to_sample = 1 and num_being_sampled = 0 AND force_monte_carlo is false, this will
    use (fast/accurate) analytic evaluation.
    Otherwise monte carlo-based KG computation is used.

    :param gaussian_process: GaussianProcess object (holds points_sampled, values, noise_variance, derived quantities)
    :type gaussian_process: GPP.GaussianProcess (boost::python ctor wrapper around optimal_learning::GaussianProcess)
    :param points_to_sample: initial points to load into state (must be a valid point for the problem);
      i.e., points at which to evaluate EI and/or its gradient
    :type points_to_sample: list of float64 with shape (num_to_sample, dim)
    :param points_being_sampled: points that are being sampled in concurrently experiments
    :type points_being_sampled: list of float64 with shape (num_being_sampled, dim)
    :param num_to_sample: number of potential future samples; gradients are evaluated wrt these points (i.e., the "q" in q,p-EI)
    :type num_to_sample: int > 0
    :param num_being_sampled: number of points being sampled concurrently (i.e., the p in q,p-EI)
    :type num_being_sampled: int >= 0
    :param max_int_steps: number of MC integration points in EI
    :type max_int_steps: int >= 0
    :param best_so_far: best known value of objective so far
    :type best_so_far: float64
    :param force_monte_carlo: true to force monte carlo evaluation of EI
    :type force_monte_carlo: bool
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :return: gradient of EI (computed at points_to_sample + points_being_sampled, wrt points_to_sample)
    :rtype: list of float64 with shape (num_to_sample, dim)
    )%%");

  boost::python::def("compute_knowledge_gradient", ComputeKnowledgeGradientWrapper, R"%%(
    Compute knowledge gradient.
    If ``num_to_sample == 1`` and ``num_being_sampled == 0`` AND ``force_monte_carlo is false``, this will
    use (fast/accurate) analytic evaluation.
    Otherwise monte carlo-based KG computation is used.

    :param gaussian_process: GaussianProcess object (holds points_sampled, values, noise_variance, derived quantities)
    :type gaussian_process: GPP.GaussianProcess (boost::python ctor wrapper around optimal_learning::GaussianProcess)
    :param points_to_sample: initial points to load into state (must be a valid point for the problem);
      i.e., points at which to evaluate EI and/or its gradient
    :type points_to_sample: list of float64 with shape (num_to_sample, dim)
    :param points_being_sampled: points that are being sampled in concurrently experiments
    :type points_being_sampled: list of float64 with shape (num_being_sampled, dim)
    :param num_to_sample: number of potential future samples; gradients are evaluated wrt these points (i.e., the "q" in q,p-EI)
    :type num_to_sample: int > 0
    :param num_being_sampled: number of points being sampled concurrently (i.e., the p in q,p-EI)
    :type num_being_sampled: int >= 0
    :param max_int_steps: number of MC integration points in EI
    :type max_int_steps: int >= 0
    :param best_so_far: best known value of objective so far
    :type best_so_far: float64
    :param force_monte_carlo: true to force monte carlo evaluation of EI
    :type force_monte_carlo: bool
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :return: computed EI
    :rtype: float64 >= 0.0
    )%%");

  boost::python::def("compute_grad_knowledge_gradient", ComputeGradKnowledgeGradientWrapper, R"%%(
    Compute the gradient of knowledge gradient evaluated at points_to_sample.
    If num_to_sample = 1 and num_being_sampled = 0 AND force_monte_carlo is false, this will
    use (fast/accurate) analytic evaluation.
    Otherwise monte carlo-based KG computation is used.

    :param gaussian_process: GaussianProcess object (holds points_sampled, values, noise_variance, derived quantities)
    :type gaussian_process: GPP.GaussianProcess (boost::python ctor wrapper around optimal_learning::GaussianProcess)
    :param points_to_sample: initial points to load into state (must be a valid point for the problem);
      i.e., points at which to evaluate EI and/or its gradient
    :type points_to_sample: list of float64 with shape (num_to_sample, dim)
    :param points_being_sampled: points that are being sampled in concurrently experiments
    :type points_being_sampled: list of float64 with shape (num_being_sampled, dim)
    :param num_to_sample: number of potential future samples; gradients are evaluated wrt these points (i.e., the "q" in q,p-EI)
    :type num_to_sample: int > 0
    :param num_being_sampled: number of points being sampled concurrently (i.e., the p in q,p-EI)
    :type num_being_sampled: int >= 0
    :param max_int_steps: number of MC integration points in EI
    :type max_int_steps: int >= 0
    :param best_so_far: best known value of objective so far
    :type best_so_far: float64
    :param force_monte_carlo: true to force monte carlo evaluation of EI
    :type force_monte_carlo: bool
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :return: gradient of EI (computed at points_to_sample + points_being_sampled, wrt points_to_sample)
    :rtype: list of float64 with shape (num_to_sample, dim)
    )%%");

  boost::python::def("multistart_knowledge_gradient_optimization", MultistartKnowledgeGradientOptimizationWrapper, R"%%(
    Optimize expected improvement (i.e., solve q,p-EI) over the specified domain using the specified optimization method.
    Can optimize for num_to_sample new points to sample (i.e., aka "q", experiments to run) simultaneously.
    Allows the user to specify num_being_sampled (aka "p") ongoing/concurrent experiments.

    The _CppOptimizerParameters object is a python class defined in:
    python/cpp_wrappers/optimization._CppOptimizerParameters
    See that class definition for more details.

    This function expects it to have the fields:

    * domain_type (DomainTypes enum from this file)
    * optimizer_type (OptimizerTypes enum from this file)
    * num_random_samples (int, number of samples to 'dumb' search over, if 'dumb' search is being used.
      e.g., if optimizer = kNull or if to_sample > 1)
    * optimizer_parameters (*Parameters struct (gpp_optimizer_parameters.hpp) where * matches optimizer_type
      unused if optimizer_type == kNull)

    This function also has the option of using GPU to compute general q,p-EI via MC simulation. To enable it,
    make sure you have installed GPU components of MOE, otherwise, it will throw Runtime excpetion.

    .. WARNING:: this function FAILS and returns an EMPTY LIST if the number of random sources < max_num_threads

    :param optimizer_parameters: python object containing the DomainTypes domain_type and
      OptimizerTypes optimzer_type to use as well as
      appropriate parameter structs e.g., NewtonParameters for type kNewton)
    :type optimizer_parameters: _CppOptimizerParameters
    :param gaussian_process: GaussianProcess object (holds points_sampled, values, noise_variance, derived quantities)
    :type gaussian_process: GPP.GaussianProcess (boost::python ctor wrapper around optimal_learning::GaussianProcess)
    :param domain: [lower, upper] bound pairs for each dimension
    :type domain: list of float64 with shape (dim, 2)
    :param points_being_sampled: points that are being sampled in concurrently experiments
    :type points_being_sampled: list of float64 with shape (num_being_sampled, dim)
    :param num_to_sample: number of potential future samples; gradients are evaluated wrt these points (i.e., the "q" in q,p-EI)
    :type num_to_sample: int > 0
    :param num_being_sampled: number of points being sampled concurrently (i.e., the p in q,p-EI)
    :type num_being_sampled: int >= 0
    :param best_so_far: best known value of objective so far
    :type best_so_far: float64
    :param max_int_steps: number of MC integration points in EI
    :type max_int_steps: int >= 0
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param use_gpu: set to 1 if user wants to use GPU for MC computation
    :type use_gpu: bool
    :param which_gpu: GPU device ID
    :type which_gpu: int >= 0
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: next set of points to eval
    :rtype: list of float64 with shape (num_to_sample, dim)
    )%%");

  boost::python::def("posterior_mean_optimization", ComputeOptimalPosteriorMeanWrapper, R"%%(
    Optimize expected improvement (i.e., solve q,p-EI) over the specified domain using the specified optimization method.
    Can optimize for num_to_sample new points to sample (i.e., aka "q", experiments to run) simultaneously.
    Allows the user to specify num_being_sampled (aka "p") ongoing/concurrent experiments.

    The _CppOptimizerParameters object is a python class defined in:
    python/cpp_wrappers/optimization._CppOptimizerParameters
    See that class definition for more details.

    This function expects it to have the fields:

    * domain_type (DomainTypes enum from this file)
    * optimizer_type (OptimizerTypes enum from this file)
    * num_random_samples (int, number of samples to 'dumb' search over, if 'dumb' search is being used.
      e.g., if optimizer = kNull or if to_sample > 1)
    * optimizer_parameters (*Parameters struct (gpp_optimizer_parameters.hpp) where * matches optimizer_type
      unused if optimizer_type == kNull)

    This function also has the option of using GPU to compute general q,p-EI via MC simulation. To enable it,
    make sure you have installed GPU components of MOE, otherwise, it will throw Runtime excpetion.

    .. WARNING:: this function FAILS and returns an EMPTY LIST if the number of random sources < max_num_threads

    :param optimizer_parameters: python object containing the DomainTypes domain_type and
      OptimizerTypes optimzer_type to use as well as
      appropriate parameter structs e.g., NewtonParameters for type kNewton)
    :type optimizer_parameters: _CppOptimizerParameters
    :param gaussian_process: GaussianProcess object (holds points_sampled, values, noise_variance, derived quantities)
    :type gaussian_process: GPP.GaussianProcess (boost::python ctor wrapper around optimal_learning::GaussianProcess)
    :param domain: [lower, upper] bound pairs for each dimension
    :type domain: list of float64 with shape (dim, 2)
    :param points_being_sampled: points that are being sampled in concurrently experiments
    :type points_being_sampled: list of float64 with shape (num_being_sampled, dim)
    :param num_to_sample: number of potential future samples; gradients are evaluated wrt these points (i.e., the "q" in q,p-EI)
    :type num_to_sample: int > 0
    :param num_being_sampled: number of points being sampled concurrently (i.e., the p in q,p-EI)
    :type num_being_sampled: int >= 0
    :param best_so_far: best known value of objective so far
    :type best_so_far: float64
    :param max_int_steps: number of MC integration points in EI
    :type max_int_steps: int >= 0
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param use_gpu: set to 1 if user wants to use GPU for MC computation
    :type use_gpu: bool
    :param which_gpu: GPU device ID
    :type which_gpu: int >= 0
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: next set of points to eval
    :rtype: list of float64 with shape (num_to_sample, dim)
    )%%");

  boost::python::def("evaluate_KG_at_point_list", EvaluateKGAtPointListWrapper, R"%%(
    Evaluates the expected improvement at each point in initial_guesses; can handle q,p-EI.
    Useful for plotting.

    Equivalent to::

      result = []
      for point in initial_guesses:
          result.append(compute_expected_improvement(point, ...))

    But this method is substantially faster (loop in C++ and multithreaded).

    .. WARNING:: this function FAILS and returns an EMPTY LIST if the number of random sources < max_num_threads

    :param num_to_sample: number of potential future samples; gradients are evaluated wrt these points (i.e., the "q" in q,p-EI)
    :type num_to_sample: int > 0

    :param gaussian_process: GaussianProcess object (holds points_sampled, values, noise_variance, derived quantities)
    :type gaussian_process: GPP.GaussianProcess (boost::python ctor wrapper around optimal_learning::GaussianProcess)
    :param initial_guesses: points at which to evaluate EI
    :type initial_guesses: list of flaot64 with shape (num_multistarts, num_to_sample, dim)
    :param points_being_sampled: points that are being sampled in concurrently experiments
    :type points_being_sampled: list of float64 with shape (num_being_sampled, dim)
    :param num_multistarts: number of points at which to evaluate EI
    :type num_multistarts: int > 0
    :param num_to_sample: number of potential future samples; gradients are evaluated wrt these points (i.e., the "q" in q,p-EI)
    :type num_to_sample: int > 0
    :param num_being_sampled: number of points being sampled concurrently (i.e., the p in q,p-EI)
    :type num_being_sampled: int >= 0
    :param best_so_far: best known value of objective so far
    :type best_so_far: float64
    :param max_int_steps: number of MC integration points in EI
    :type max_int_steps: int >= 0
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: EI values at each point of the initial_guesses list, in the same order
    :rtype: list of float64 with shape (num_multistarts, )
    )%%");
}

}  // end namespace optimal_learning


/*

	This file is part of PEST++.

	PEST++ 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.

	PEST++ 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 PEST++.  If not, see<http://www.gnu.org/licenses/>.
*/

#include "RunManagerPanther.h" //needs to be first because it includes winsock2.h
#include <iostream>
#include <fstream>
#include <Eigen/Dense>
#include <Eigen/Sparse>
#include <iomanip>
#include "config_os.h"
#include "MorrisMethod.h"
#include "sobol.h"
//#include "TornadoPlot.h"
#include "Pest.h"
#include "Transformable.h"
#include "Transformation.h"
#include "ParamTransformSeq.h"
#include "utilities.h"
#include "pest_error.h"
#include "ModelRunPP.h"
#include "FileManager.h"
#include "RunManagerSerial.h"
#include "OutputFileWriter.h"
#include "PantherAgent.h"
#include "Serialization.h"
#include "system_variables.h"

using namespace std;
using namespace pest_utils;
using Eigen::MatrixXd;
using Eigen::VectorXd;

int main(int argc, char* argv[])
{
#ifndef _DEBUG
	try
	{
#endif
	string version = PESTPP_VERSION;
	cout << endl << endl;
	cout << "             pestpp-sen: a tool for global sensitivity analysis" << endl << endl;
	cout << "                       by The PEST++ Development Team" << endl;
	cout << endl << endl << "version: " << version << endl;
	cout << "binary compiled on " << __DATE__ << " at " << __TIME__ << endl << endl;

	CmdLine cmdline(argc, argv);
	FileManager file_manager;
	string pathname = ".";
	file_manager.initialize_path(get_filename_without_ext(cmdline.ctl_file_name), pathname);

	if (cmdline.runmanagertype == CmdLine::RunManagerType::PANTHER_WORKER)
	{
		// This is a PANTHER worker, start PEST++ as a PANTHER worker
		try
		{
			ofstream frec("panther_worker.rec");
			if (frec.bad())
				throw runtime_error("error opening 'panther_worker.rec'");
			PANTHERAgent yam_agent(frec);
			string ctl_file = "";
			try {
				// process traditional PEST control file
				ctl_file = file_manager.build_filename("pst");
				yam_agent.process_ctl_file(ctl_file);

			}
			catch (PestError e)
			{
				cerr << "Error prococessing control file: " << ctl_file << endl << endl;
				cerr << e.what() << endl << endl;
				throw(e);
			}

			yam_agent.start(cmdline.panther_host_name,cmdline.panther_port);
		}
		catch (PestError &perr)
		{
			cerr << perr.what();
			throw(perr);
		}
		cout << endl << "Work Done..." << endl;
		exit(0);
	}

	if (cmdline.runmanagertype == CmdLine::RunManagerType::GENIE)
	{
		cerr << "Genie run manager ('/g') no longer supported, please use PANTHER instead" << endl;
		exit(1);

	}
	if (cmdline.runmanagertype == CmdLine::RunManagerType::EXTERNAL)
	{
		cerr << "external run manager ('/e') no longer supported, please use PANTHER instead" << endl;
		exit(1);

	}

	ofstream &fout_rec = file_manager.open_ofile_ext("rec");
	fout_rec << "             pestpp-sen: a tool for global sensitivity analysis" << endl << endl;
	fout_rec << "                         by The PEST++ Development Team" << endl << endl;
	fout_rec << endl << endl << "version: " << version << endl;
	fout_rec << "binary compiled on " << __DATE__ << " at " << __TIME__ << endl << endl;
	fout_rec << "using control file: \"" << cmdline.ctl_file_name << "\"" << endl;
	fout_rec << "in directory: \"" << OperSys::getcwd() << "\"" << endl;
	fout_rec << "on host: \"" << w_get_hostname() << "\"" << endl << endl;

	cout << endl;
	cout << "using control file: \"" << cmdline.ctl_file_name << "\"" << endl;
	cout << "in directory: \"" << OperSys::getcwd() << "\"" << endl;
	cout << "on host: \"" << w_get_hostname() << "\"" << endl << endl;

	// create pest run and process control file to initialize it
	Pest pest_scenario;
	pest_scenario.set_defaults();
	try
	{
		pest_scenario.process_ctl_file(file_manager.open_ifile_ext("pst"), file_manager.build_filename("pst"),fout_rec);
		file_manager.close_file("pst");
		//pest_scenario.check_inputs(fout_rec);
	}
	catch(PestError e)
	{
		cerr << "Error prococessing control file: " << cmdline.ctl_file_name << endl << endl;
		cerr << e.what() << endl << endl;
		fout_rec << "Error prococessing control file: " << cmdline.ctl_file_name << endl << endl;
		fout_rec << e.what() << endl;
		fout_rec.close();
		//throw(e);
		return 1;
	}
	pest_scenario.check_inputs(fout_rec);
	//OutputFileWriter(FileManager &_file_manager, Pest &_pest_scenario, bool restart_flag = false, bool _save_rei = true, int _eigenwrite = 0);

	OutputFileWriter ofw(file_manager,pest_scenario,false,false,0);
	//ofw.scenario_report(fout_rec, false);

	if (pest_scenario.get_pestpp_options().get_debug_parse_only())
	{
		cout << endl << endl << "DEBUG_PARSE_ONLY is true, exiting..." << endl << endl;
		exit(0);
	}

	RunManagerAbstract *run_manager_ptr;
	if (cmdline.runmanagertype == CmdLine::RunManagerType::PANTHER_MASTER)
	{
		const ModelExecInfo &exi = pest_scenario.get_model_exec_info();
		run_manager_ptr = new RunManagerPanther (
			file_manager.build_filename("rns"), cmdline.panther_port,
			file_manager.open_ofile_ext("rmr"),
			pest_scenario.get_pestpp_options().get_max_run_fail(),
			pest_scenario.get_pestpp_options().get_overdue_reched_fac(),
			pest_scenario.get_pestpp_options().get_overdue_giveup_fac(),
			pest_scenario.get_pestpp_options().get_overdue_giveup_minutes(),
			pest_scenario.get_pestpp_options().get_panther_echo());
	}
	else
	{
		const ModelExecInfo &exi = pest_scenario.get_model_exec_info();
		run_manager_ptr = new RunManagerSerial(exi.comline_vec,
			exi.tplfile_vec, exi.inpfile_vec, exi.insfile_vec, exi.outfile_vec,
			file_manager.build_filename("rns"), pathname,
			pest_scenario.get_pestpp_options().get_max_run_fail(),
			pest_scenario.get_pestpp_options().get_fill_tpl_zeros(),
			pest_scenario.get_pestpp_options().get_additional_ins_delimiters(),
			pest_scenario.get_pestpp_options().get_num_tpl_ins_threads());
	}

	cout << endl;
	fout_rec << endl;
	cout << "using control file: \"" <<  cmdline.ctl_file_name << "\"" << endl;
	fout_rec << "using control file: \"" <<  cmdline.ctl_file_name << "\"" << endl;

	enum class GSA_RESTART { NONE, RESTART };
	GSA_RESTART gsa_restart = GSA_RESTART::NONE;
	//process restart and  reuse jacibian directives
	if (cmdline.jac_restart)
	{
		cerr << "jacobian restart ('/j') not supported in PESTPP-SEN";
		exit(1);
	}
	if (cmdline.restart)
	{
		gsa_restart = GSA_RESTART::RESTART;
	}
	else
	{
		gsa_restart = GSA_RESTART::NONE;
	}
	//OutputFileWriter(FileManager &_file_manager, Pest &_pest_scenario, bool restart_flag = false, bool _save_rei = true, int _eigenwrite = 0);
	OutputFileWriter output_writer(file_manager, pest_scenario, false, false);
	ofstream &frec = file_manager.rec_ofstream();
	output_writer.scenario_report(frec);
	//output_writer.scenario_io_report(frec);
	//output_writer.scenario_par_report(frec);
	//output_writer.scenario_obs_report(frec);

	pest_scenario.check_inputs(frec);
	pest_scenario.check_io(frec);

	//map<string, string> gsa_opt_map;
	//process .gsa file
	string gsa_filename = file_manager.get_base_filename() + ".gsa";

	if (check_exist_in(gsa_filename))
	{
		cout << "WARNING: use of .gsa files is deprecated - .gsa file '" << gsa_filename << "' is being ignored, please use '++' args";
		return 1;
	}

	PestppOptions *pp_ptr = pest_scenario.get_pestpp_options_ptr();
	map<string, string> gsa_opt_map = pp_ptr->get_arg_map();

	//Build Transformation with ctl_2_numberic
	ParamTransformSeq base_partran_seq(pest_scenario.get_base_par_tran_seq());
	Parameters ctl_par = pest_scenario.get_ctl_parameters();

	//Build Transformation with ctl_2_numberic
	ObjectiveFunc obj_func(&(pest_scenario.get_ctl_observations()), &(pest_scenario.get_ctl_observation_info()), &(pest_scenario.get_prior_info()));
	ModelRun model_run(&obj_func, pest_scenario.get_ctl_observations());
	const set<string> &log_trans_pars = base_partran_seq.get_log10_ptr()->get_items();
	auto method = gsa_opt_map.find("GSA_METHOD");

	GsaAbstractBase* gsa_method = nullptr;
	if (method == gsa_opt_map.end() || method->second == "MORRIS")
	{
		int morris_r = 4;
		int morris_p = 4;

		double morris_delta = .666;
		bool default_delta = true;
		bool calc_pooled_obs = false;
		bool calc_morris_obs_sen = true;
		auto morris_r_it = gsa_opt_map.find("GSA_MORRIS_R");
		if (morris_r_it != gsa_opt_map.end())
		{
			convert_ip(morris_r_it->second, morris_r);
		}
		auto morris_p_it = gsa_opt_map.find("GSA_MORRIS_P");
		if (morris_p_it != gsa_opt_map.end())
		{
			convert_ip(morris_p_it->second, morris_p);
		}
		auto morris_d_it = gsa_opt_map.find("GSA_MORRIS_DELTA");
		if (morris_d_it != gsa_opt_map.end())
		{

			convert_ip(morris_d_it->second, morris_delta);
			default_delta = false;
		}
		auto morris_pool_it = gsa_opt_map.find("GSA_MORRIS_POOLED_OBS");
		if (morris_pool_it != gsa_opt_map.end())
		{
			string pooled_obs_flag = morris_pool_it->second;
			upper_ip(pooled_obs_flag);
			if (pooled_obs_flag == "TRUE") calc_pooled_obs = true;
		}

		auto morris_obs_sen_it = gsa_opt_map.find("GSA_MORRIS_OBS_SEN");
		if (morris_obs_sen_it != gsa_opt_map.end())
		{
			string obs_sen_flag = morris_obs_sen_it->second;
			upper_ip(obs_sen_flag);
			if (obs_sen_flag == "FALSE") calc_morris_obs_sen = false;
		}

		if (default_delta) morris_delta = morris_p / (2.0 * (morris_p - 1));

		MorrisMethod *m_ptr = new MorrisMethod(pest_scenario, file_manager, &obj_func,
			base_partran_seq, morris_p, morris_r, morris_delta, calc_pooled_obs,
			calc_morris_obs_sen, GsaAbstractBase::PARAM_DIST::uniform, 1.0);
		gsa_method = m_ptr;
		m_ptr->process_pooled_var_file();

		frec << endl << endl << endl << "Method of Morris settings:" << endl;

		frec << scientific << left << setw(30) << " morris_r " << morris_r << endl;
		frec << scientific << left << setw(30) << " morris_p " << morris_p << endl;
		frec << scientific << left << setw(30) << " morris_delta " << morris_delta << endl << endl;

	}

	else if (method != gsa_opt_map.end() && method->second == "SOBOL")
	{
		GsaAbstractBase::PARAM_DIST par_dist = GsaAbstractBase::PARAM_DIST::uniform;
		string par_dist_str = "UNIFORM";
		int n_sample = 100;
		auto sob_n_sam_it = gsa_opt_map.find("GSA_SOBOL_SAMPLES");
		if (sob_n_sam_it != gsa_opt_map.end())
		{
			convert_ip(sob_n_sam_it->second, n_sample);
		}
		auto sob_p_dist_it = gsa_opt_map.find("GSA_SOBOL_PAR_DIST");
		if (sob_p_dist_it != gsa_opt_map.end())
		{
			par_dist_str = sob_p_dist_it->second;
			upper_ip(par_dist_str);
			if (par_dist_str == "NORM") par_dist = GsaAbstractBase::PARAM_DIST::normal;
			else if (par_dist_str == "UNIF") par_dist = GsaAbstractBase::PARAM_DIST::uniform;
			else
			{
				ostringstream str;
				str << "SOBOL_PAR_DIST(" << par_dist_str << "):  \"" << par_dist_str << "\" is an invalid distribuation type";
				throw PestError(str.str());
			}
		}

		gsa_method = new Sobol(pest_scenario, file_manager, &obj_func,
			base_partran_seq, n_sample, par_dist, 1.0);

		frec << endl << endl << endl << "Method of Sobol settings:" << endl;

		frec << scientific << left << setw(30) << " n_sample " << n_sample << endl;
		frec << scientific << left << setw(30) << " sobol par dist " <<par_dist_str << endl;

	}
	else
	{
		throw PestError("A valid method for computing the sensitivity must be specified in the control file");
	}

	auto morris_r_it = gsa_opt_map.find("RAND_SEED");
	if (morris_r_it != gsa_opt_map.end())
	{
		unsigned int seed = convert_cp<unsigned int>(morris_r_it->second);
		gsa_method->set_seed(seed);
	}
	//gsa_method->set_seed(2);
	frec << scientific << left << setw(30) << " gsa random seed " << gsa_method->get_seed() << endl;
	// make model runs
	if (gsa_restart == GSA_RESTART::NONE)
	{
		//Allocates Space for Run Manager.  This initializes the model parameter names and observations names.
		//Neither of these will change over the course of the simulation
		cout << endl;
		cout << "Building model run parameter sets..." << endl;
		run_manager_ptr->initialize(base_partran_seq.ctl2model_cp(ctl_par), pest_scenario.get_ctl_observations());

		Parameters model_pars = base_partran_seq.ctl2model_cp(ctl_par);
		run_manager_ptr->reinitialize();
		gsa_method->assemble_runs(*run_manager_ptr);
	}
	else
	{
		run_manager_ptr->initialize_restart(file_manager.build_filename("rns"));
	}
	cout << endl;
	cout << "Performing model runs..." << endl;
	run_manager_ptr->run();

	cout << "Calculating sensitivities..." << endl;
	gsa_method->calc_sen(*run_manager_ptr, model_run);
	file_manager.close_file("srw");
	file_manager.close_file("msn");
	file_manager.close_file("orw");
	delete run_manager_ptr;

	string case_name = file_manager.get_base_filename();
	file_manager.close_file("rst");
	pest_utils::try_clean_up_run_storage_files(case_name);

	cout << endl << endl << "PESTPP-SEN Analysis Complete..." << endl;
	return 0;
	//cout << endl << "Simulation Complete - Press RETURN to close window" << endl;
	//char buf[256];
	//OperSys::gets_s(buf, sizeof(buf));
#ifndef _DEBUG
	}
	catch (exception &e)
	{
		cout << "Error condition prevents further execution: " << endl << e.what() << endl;
		//cout << "press enter to continue" << endl;
		//char buf[256];
		//OperSys::gets_s(buf, sizeof(buf));
		return 1;
	}
	catch (...)
	{
		cout << "Error condition prevents further execution" << endl;
		return 1;
	}

#endif
}