/*Copyright (c) 2020 James Gayvert

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.
*/

/*
 * qchem_interface.cpp
 */
#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <string>
#include "qchem_interface.h"
#include "opencap_exception.h"
#include "gto_ordering.h"
#include "utils.h"
#include "BasisSet.h"
#include <Eigen/Dense>

std::array<std::vector<std::vector<Eigen::MatrixXd>>,2> qchem_read_dms(std::string fchk_filename,BasisSet &bs)
{
	std::ifstream is(fchk_filename);
	size_t nstates = 0;
	bool sep_alpha_beta;
	std::string line, rest;
	while(is.peek() != EOF )
	{
    	std::getline(is, line);
    	if(line.find("Alpha  State Density")!= std::string::npos)
    	{
    		sep_alpha_beta=true;
			int num_elements = stoi(split(line,' ').back());
			if(sqrt(num_elements)!=bs.Nbasis)
				opencap_throw("Error: dimensions of DMs do not match specified basis set.");
    		break;
    	}
    	else if(line.find("State Density")!= std::string::npos)
    	{
    		sep_alpha_beta=false;
			int num_elements = stoi(split(line,' ').back());
			if(sqrt(num_elements)!=bs.Nbasis)
				opencap_throw("Error: dimensions of state DMs do not match specified basis set.");
    		break;
    	}
	}
	is.seekg (0, ios::beg);
	while(is.peek()!=EOF)
	{
    	std::getline(is, line);
    	if(line.find("Alpha  State Density")!= std::string::npos && sep_alpha_beta)
    		nstates++;
    	else if(line.find("State Density")!= std::string::npos && !sep_alpha_beta)
    		nstates++;
	}
	if(nstates==0)
		opencap_throw("Error:Unable to find any densities in:" +fchk_filename);
	if(sep_alpha_beta)
		return qchem_read_in_dms_open_shell(fchk_filename,nstates,bs);
	else
		return qchem_read_in_dms_closed_shell(fchk_filename,nstates,bs);

}

std::array<std::vector<std::vector<Eigen::MatrixXd>>,2> qchem_read_in_dms_open_shell(std::string dmat_filename,
		size_t nstates, BasisSet bs)
{
	std::vector< std::vector<Eigen::MatrixXd>> alpha_opdms(nstates, std::vector<Eigen::MatrixXd> (nstates));
	std::vector< std::vector<Eigen::MatrixXd>> beta_opdms(nstates, std::vector<Eigen::MatrixXd> (nstates));
	//start with state density matrices, alpha and beta densities
	std::ifstream is(dmat_filename);
    if (is.good())
    {
    	std::string line, rest;
    	std::getline(is, line);
    	for (size_t i=0;i<nstates;i++)
    	{
    			//alpha first, then beta
    			for(size_t spin=1;spin<=2;spin++)
    			{
					while(line.find("State Density")== std::string::npos)
					{
						std::getline(is,line);
	    	    		if (is.peek()==EOF)
	    	    			opencap_throw("Error: Reached end of file before densities for "+
	    	    					std::to_string(nstates) + " states were found.");
					}
					//last part of line should be number of elements to read
					int num_elements = stoi(split(line,' ').back());
					if(sqrt(num_elements)!=bs.Nbasis)
						opencap_throw("Error: dimensions of TDMs do not match specified basis set.");
					size_t lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
					std::vector<double> matrix_elements;
					for (size_t k=1;k<=lines_to_read;k++)
					{
						std::getline(is,line);
						std::vector<std::string> tokens = split(line,' ');
						for (auto token:tokens)
						{
							matrix_elements.push_back(std::stod(token));
						}
					}
					Eigen::MatrixXd st_opdm(bs.Nbasis,bs.Nbasis);
					st_opdm=Eigen::MatrixXd::Zero(bs.Nbasis,bs.Nbasis);
					fill_mat(matrix_elements,st_opdm);
					to_opencap_ordering(st_opdm,bs,get_qchem_ids(bs));
					if(spin==1)
						alpha_opdms[i][i]=st_opdm;
					else
						beta_opdms[i][i]=st_opdm;
    			}
		}
    	//now tdms
    	for(size_t i=0;i<nstates;i++)
    	{
    		for (size_t j=i+1;j<nstates;j++)
    		{
    			//alpha first, then beta
    			for (size_t spin=1;spin<=2;spin++)
    			{
					while(line.find("Transition DM")== std::string::npos)
					{
						std::getline(is,line);
	    	    		if (is.peek()==EOF)
	    	    			opencap_throw("Error: Reached end of file before densities for "+
	    	    					std::to_string(nstates) + " states were found.");
					}
					//last part of line should be number of elements to read
					size_t num_elements = stoi(split(line,' ').back());
					if(sqrt(num_elements)!=bs.Nbasis)
						opencap_throw("Error: dimensions of TDMs do not match specified basis set.");
					size_t lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
					std::vector<double> matrix_elements;
					for (size_t k=1;k<=lines_to_read;k++)
					{
						std::getline(is,line);
						std::vector<std::string> tokens = split(line,' ');
						for (auto token:tokens)
							matrix_elements.push_back(std::stod(token));
					}
					Eigen::MatrixXd st_opdm(bs.Nbasis,bs.Nbasis);
					st_opdm=Eigen::MatrixXd::Zero(bs.Nbasis,bs.Nbasis);
					fill_mat(matrix_elements,st_opdm);
					to_opencap_ordering(st_opdm,bs,get_qchem_ids(bs));
					if(spin==1)
					{
						alpha_opdms[i][j]=st_opdm;
						alpha_opdms[j][i]=st_opdm;
					}
					else
					{
						beta_opdms[i][j]=st_opdm;
						beta_opdms[j][i]=st_opdm;
					}
    			}
    		}
    	}
    }
    return {alpha_opdms,beta_opdms};
}

Eigen::MatrixXd qchem_read_overlap(std::string dmat_filename, BasisSet bs)
{
	size_t num_bf = bs.Nbasis;
    std::ifstream is(dmat_filename);
	Eigen::MatrixXd smat(num_bf,num_bf);
	smat=Eigen::MatrixXd::Zero(num_bf,num_bf);
    if (is.good())
    {
    	std::string line, rest;
    	while (line.find("Overlap Matrix")== std::string::npos)
    	{
        	std::getline(is, line);
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before Overlap Matrix was found.");
    	}
    	size_t num_elements = stoi(split(line,' ').back());
		size_t lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
		std::vector<double> matrix_elements;
		for (size_t k=1;k<=lines_to_read;k++)
		{
			std::getline(is,line);
			std::vector<std::string> tokens = split(line,' ');
			for (auto token:tokens)
				matrix_elements.push_back(std::stod(token));
		}
		fill_LT(matrix_elements,smat);
		to_opencap_ordering(smat,bs,get_qchem_ids(bs));
    }
    return smat;
}

Eigen::MatrixXd read_qchem_energies(size_t nstates,std::string method,std::string output_file)
{
	Eigen::MatrixXd ZERO_ORDER_H(nstates,nstates);
	ZERO_ORDER_H=Eigen::MatrixXd::Zero(nstates,nstates);
	transform(method.begin(),method.end(),method.begin(),::toupper);
	std::ifstream is(output_file);
    if (is.good())
    {
    	std::string line, rest;
    	std::getline(is, line);
    	size_t state_idx = 1;
    	while (state_idx<=nstates)
    	{
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before "+ std::to_string(nstates) + " energies were found.");
    		std::string line_to_find = method +" transition " + std::to_string(state_idx);
    		if (line.find(line_to_find)!= std::string::npos)
    		{
				std::getline(is,line);
				ZERO_ORDER_H(state_idx-1,state_idx-1) = std::stod(split(line,' ')[3]);
				state_idx++;
    		}
    		else
    			std::getline(is,line);
    	}
    }
    return ZERO_ORDER_H;
}

std::array<std::vector<std::vector<Eigen::MatrixXd>>,2> qchem_read_in_dms_closed_shell(std::string dmat_filename,
		size_t nstates, BasisSet bs)
{
	std::vector< std::vector<Eigen::MatrixXd>> opdms(nstates, std::vector<Eigen::MatrixXd> (nstates));
	//start with state density matrices
	std::ifstream is(dmat_filename);
    if (is.good())
    {
    	std::string line, rest;
    	std::getline(is, line);
    	for (size_t i=0;i<nstates;i++)
    	{
				while(line.find("State Density")== std::string::npos)
				{
					std::getline(is,line);
    	    		if (is.peek()==EOF)
    	    			opencap_throw("Error: Reached end of file before densities for "+
    	    					std::to_string(nstates) + " states were found.");
				}
				//last part of line should be number of elements to read
				size_t num_elements = stoi(split(line,' ').back());
				size_t lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
				std::vector<double> matrix_elements;
				for (size_t k=1;k<=lines_to_read;k++)
				{
					std::getline(is,line);
					std::vector<std::string> tokens = split(line,' ');
					for (auto token:tokens)
					{
						matrix_elements.push_back(std::stod(token));
					}
				}
				Eigen::MatrixXd st_opdm(bs.Nbasis,bs.Nbasis);
				st_opdm=Eigen::MatrixXd::Zero(bs.Nbasis,bs.Nbasis);
				fill_mat(matrix_elements,st_opdm);
				to_opencap_ordering(st_opdm,bs,get_qchem_ids(bs));
				opdms[i][i]=0.5*st_opdm;
		}
    	//now tdms
    	for(size_t i=0;i<nstates;i++)
    	{
    		for (size_t j=i+1;j<nstates;j++)
    		{
				while(line.find("Transition DM")== std::string::npos)
				{
					std::getline(is,line);
    	    		if (is.peek()==EOF)
    	    			opencap_throw("Error: Reached end of file before densities for "+
    	    					std::to_string(nstates) + " states were found.");
				}
				//last part of line should be number of elements to read
				size_t num_elements = stoi(split(line,' ').back());
				size_t lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
				std::vector<double> matrix_elements;
				for (size_t k=1;k<=lines_to_read;k++)
				{
					std::getline(is,line);
					std::vector<std::string> tokens = split(line,' ');
					for (auto token:tokens)
						matrix_elements.push_back(std::stod(token));
				}
				Eigen::MatrixXd st_opdm(bs.Nbasis,bs.Nbasis);
				st_opdm=Eigen::MatrixXd::Zero(bs.Nbasis,bs.Nbasis);
				fill_mat(matrix_elements,st_opdm);
				to_opencap_ordering(st_opdm,bs,get_qchem_ids(bs));
				opdms[i][j]=0.5*st_opdm;
				opdms[j][i]=0.5*st_opdm;
    		}
    	}
    }
    return {opdms,opdms};

}

std::vector<Atom> read_geometry_from_fchk(std::string fchk_filename)
{
	std::vector<Atom> atoms;
	std::vector<size_t> atom_nums;
	std::vector<double> coords;
	std::ifstream is(fchk_filename);
    if (is.good())
    {
    	std::string line, rest;
    	std::getline(is, line);
		while(line.find("Atomic numbers")== std::string::npos)
		{
			std::getline(is,line);
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before atomic numbers were found.");
		}
		size_t num_elements = stoi(split(line,' ').back());
		size_t lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
		for (size_t k=1;k<=lines_to_read;k++)
		{
			std::getline(is,line);
			std::vector<std::string> tokens = split(line,' ');
			for (auto token:tokens)
				atom_nums.push_back(std::stoi(token));
		}
		while(line.find("Current cartesian coordinates")== std::string::npos)
		{
			std::getline(is,line);
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before Current Cartesian Coordinates were found.");
		}
		num_elements = stoi(split(line,' ').back());
		lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
		for (size_t k=1;k<=lines_to_read;k++)
		{
			std::getline(is,line);
			std::vector<std::string> tokens = split(line,' ');
			for (auto token:tokens)
				coords.push_back(std::stod(token));
		}
		//ok now lets populate our atoms
		for(size_t i=0;i<atom_nums.size();i++)
			atoms.push_back(Atom(atom_nums[i],coords[i*3],coords[i*3+1],coords[i*3+2]));
    }
    else
    {
    	opencap_throw("Error: I couldn't read:" + fchk_filename);
    }
    return atoms;
}

BasisSet read_basis_from_fchk(std::string fchk_filename, std::vector<Atom> atoms)
{
	std::vector<int> shell_types;
	std::vector<int> prims_per_shell;
	std::vector<int> atom_ids;
	std::vector<double> exps;
	std::vector<double> coeffs;
	// needed for SP specification
	std::vector<double> p_coeffs;
	bool SP_basis_function = true;
	BasisSet bs;
	for(auto atm:atoms)
		bs.centers.push_back(atm.coords);
	std::vector<shell_id> ids;
	std::ifstream is(fchk_filename);
    if (is.good())
    {
    	std::string line, rest;
    	std::getline(is, line);
    	//First lets figure out if there are SP functions
    	//shell types
		while(line.find("Shell types")== std::string::npos)
		{
			std::getline(is,line);
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before shell types were found.");
		}
		size_t num_elements = stoi(split(line,' ').back());
		size_t lines_to_read = num_elements%6==0 ? (num_elements/6) : num_elements/6+1;
		for (size_t k=1;k<=lines_to_read;k++)
		{
			std::getline(is,line);
			std::vector<std::string> tokens = split(line,' ');
			for (auto token:tokens)
			{
				shell_types.push_back(std::stoi(token));
			}
		}
		//prims per shell
		while(line.find("Number of primitives per shell")== std::string::npos)
		{
			std::getline(is,line);
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before Number of primitives per shell were found.");
		}
		num_elements = stoi(split(line,' ').back());
		lines_to_read = num_elements%6==0 ? (num_elements/6) : num_elements/6+1;
		for (size_t k=1;k<=lines_to_read;k++)
		{
			std::getline(is,line);
			std::vector<std::string> tokens = split(line,' ');
			for (auto token:tokens)
				prims_per_shell.push_back(std::stoi(token));
		}
		//atom ids
		while(line.find("Shell to atom map")== std::string::npos)
		{
			std::getline(is,line);
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before Shell to atom map was found.");
		}
		num_elements = stoi(split(line,' ').back());
		lines_to_read = num_elements%6==0 ? (num_elements/6) : num_elements/6+1;
		for (size_t k=1;k<=lines_to_read;k++)
		{
			std::getline(is,line);
			std::vector<std::string> tokens = split(line,' ');
			for (auto token:tokens)
				atom_ids.push_back(std::stoi(token));
		}
		//prims
		while(line.find("Primitive exponents")== std::string::npos)
		{
			std::getline(is,line);
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before Primitive exponents were found.");
		}
		num_elements = stoi(split(line,' ').back());
		lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
		for (size_t k=1;k<=lines_to_read;k++)
		{
			std::getline(is,line);
			std::vector<std::string> tokens = split(line,' ');
			for (auto token:tokens)
				exps.push_back(std::stod(token));
		}
		//coeffs
		while(line.find("Contraction coefficients")== std::string::npos)
		{
			std::getline(is,line);
    		if (is.peek()==EOF)
    			opencap_throw("Error: Reached end of file before Contraction coefficients were found.");
		}
		num_elements = stoi(split(line,' ').back());
		lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
		for (size_t k=1;k<=lines_to_read;k++)
		{
			std::getline(is,line);
			std::vector<std::string> tokens = split(line,' ');
			for (auto token:tokens)
				coeffs.push_back(std::stod(token));
		}
		while(line.find("P(S=P) Contraction coefficients")== std::string::npos)
		{
			std::getline(is,line);
    		if (is.peek()==EOF)
    		{
    			SP_basis_function = false;
    			break;
    		}
		}
		if (SP_basis_function)
		{
			num_elements = stoi(split(line,' ').back());
			lines_to_read = num_elements%5==0 ? (num_elements/5) : num_elements/5+1;
			for (size_t k=1;k<=lines_to_read;k++)
			{
				std::getline(is,line);
				std::vector<std::string> tokens = split(line,' ');
				for (auto token:tokens)
					p_coeffs.push_back(std::stod(token));
			}
		}
    }
    size_t prim_idx=0;
    for(size_t i=0;i<shell_types.size();i++)
    {
    	//SP
    	if(shell_types[i]==-1)
    	{
    		if(p_coeffs.size()==0)
    			opencap_throw("Error: missing section P(S=P) Contraction coefficients.");
    		Shell s_shell(0,atoms[atom_ids[i]-1].coords);
    		Shell p_shell(1,atoms[atom_ids[i]-1].coords);
			int num_prims = prims_per_shell[i];
			for(int j=1;j<=num_prims;j++)
			{
				s_shell.add_primitive(exps[prim_idx],coeffs[prim_idx]);
				p_shell.add_primitive(exps[prim_idx],p_coeffs[prim_idx]);
				prim_idx++;
			}
			bs.add_shell(s_shell);
			bs.add_shell(p_shell);
    	}
    	else
    	{
			Shell new_shell(abs(shell_types[i]),atoms[atom_ids[i]-1].coords);
			if(shell_types[i]>0 && new_shell.l>1)
				new_shell.pure=false;
			int num_prims = prims_per_shell[i];
			for(int j=1;j<=num_prims;j++)
			{
				new_shell.add_primitive(exps[prim_idx],coeffs[prim_idx]);
				prim_idx++;
			}
			bs.add_shell(new_shell);
    	}
    }
    bs.normalize();
    return bs;
}


//| Copyright Inria May 2015
//| This project has received funding from the European Research Council (ERC) under
//| the European Union's Horizon 2020 research and innovation programme (grant
//| agreement No 637972) - see http://www.resibots.eu
//|
//| Contributor(s):
//|   - Jean-Baptiste Mouret (jean-baptiste.mouret@inria.fr)
//|   - Antoine Cully (antoinecully@gmail.com)
//|   - Konstantinos Chatzilygeroudis (konstantinos.chatzilygeroudis@inria.fr)
//|   - Federico Allocati (fede.allocati@gmail.com)
//|   - Vaios Papaspyros (b.papaspyros@gmail.com)
//|   - Roberto Rama (bertoski@gmail.com)
//|
//| This software is a computer library whose purpose is to optimize continuous,
//| black-box functions. It mainly implements Gaussian processes and Bayesian
//| optimization.
//| Main repository: http://github.com/resibots/limbo
//| Documentation: http://www.resibots.eu/limbo
//|
//| This software is governed by the CeCILL-C license under French law and
//| abiding by the rules of distribution of free software.  You can  use,
//| modify and/ or redistribute the software under the terms of the CeCILL-C
//| license as circulated by CEA, CNRS and INRIA at the following URL
//| "http://www.cecill.info".
//|
//| As a counterpart to the access to the source code and  rights to copy,
//| modify and redistribute granted by the license, users are provided only
//| with a limited warranty  and the software's author,  the holder of the
//| economic rights,  and the successive licensors  have only  limited
//| liability.
//|
//| In this respect, the user's attention is drawn to the risks associated
//| with loading,  using,  modifying and/or developing or reproducing the
//| software by the user in light of its specific status of free software,
//| that may mean  that it is complicated to manipulate,  and  that  also
//| therefore means  that it is reserved for developers  and  experienced
//| professionals having in-depth computer knowledge. Users are therefore
//| encouraged to load and test the software's suitability as regards their
//| requirements in conditions enabling the security of their systems and/or
//| data to be ensured and,  more generally, to use and operate it in the
//| same conditions as regards security.
//|
//| The fact that you are presently reading this means that you have had
//| knowledge of the CeCILL-C license and that you accept its terms.
//|
#ifndef LIMBO_OPT_CMAES_HPP
#define LIMBO_OPT_CMAES_HPP

#include <Eigen/Core>
#include <iostream>
#include <vector>

#include <limbo/opt/optimizer.hpp>
#include <limbo/tools/macros.hpp>
#include <limbo/tools/parallel.hpp>

#ifndef USE_LIBCMAES
#warning NO libcmaes support
#else

#include <libcmaes/cmaes.h>

namespace limbo {
    namespace defaults {
        struct opt_cmaes {
            /// @ingroup opt_defaults
            /// number of restarts of CMA-ES
            BO_PARAM(int, restarts, 1);
            /// @ingroup opt_defaults
            /// maximum number of calls to the function to be optimized
            BO_PARAM(int, max_fun_evals, -1);
            /// @ingroup opt_defaults
            /// maximum number of iterations to be optimized
            BO_PARAM(int, max_iters, -1);
            /// @ingroup opt_defaults
            /// enable/disable automatic max iterations
            BO_PARAM(bool, auto_max_iters, true);
            /// @ingroup opt_defaults
            /// enable/disable stopping if getting the same function value repeatedly
            BO_PARAM(bool, equal_fun_evals, true);
            /// @ingroup opt_defaults
            BO_PARAM(bool, stagnation, true);
            /// @ingroup opt_defaults
            /// threshold based on the difference in value of a fixed number
            /// of trials: if bigger than 0, it enables the tolerance criteria
            /// for stopping based in the history of rewards.
            BO_PARAM(double, fun_tolerance, -1);
            /// @ingroup opt_defaults
            /// tolerance for convergence: stop when an optimization step (or an
            /// estimate of the optimum) changes all the parameter values by
            /// less than tol multiplied by the absolute value of the parameter
            /// value.
            /// IGNORED if negative
            BO_PARAM(double, xrel_tolerance, -1);
            /// @ingroup opt_defaults
            /// function value target: if bigger than 0, enables the function
            /// target criteria for stopping if the performance is greater than this value.
            BO_PARAM(double, fun_target, -1);
            /// @ingroup opt_defaults
            /// computes initial objective function value: if true, it evaluates the
            /// provided starting point (if any).
            BO_PARAM(bool, fun_compute_initial, false);
            /// @ingroup opt_defaults
            /// sets the version of cmaes to use (possible values are: CMAES_DEFAULT, IPOP_CMAES, BIPOP_CMAES, aCMAES, aIPOP_CMAES, aBIPOP_CMAES, sepCMAES, sepIPOP_CMAES, sepBIPOP_CMAES, sepaCMAES, sepaIPOP_CMAES, sepaBIPOP_CMAES, VD_CMAES, VD_IPOP_CMAES, VD_BIPOP_CMAES
            BO_PARAM(int, variant, aIPOP_CMAES);
            /// @ingroup opt_defaults
            /// defines elitism strategy:
            /// 0 -> no elitism
            /// 1 -> elitism: reinjects the best-ever seen solution
            /// 2 -> initial elitism: reinject x0 as long as it is not improved upon
            /// 3 -> initial elitism on restart: restart if best encountered solution is not the the final
            /// solution and reinjects the best solution until the population has better fitness, in its majority
            BO_PARAM(int, elitism, 0);
            /// @ingroup opt_defaults
            /// enables or disables uncertainty handling: https://hal.inria.fr/file/index/docid/276216/filename/TEC2008.pdf
            BO_PARAM(bool, handle_uncertainty, false);
            /// @ingroup opt_defaults
            /// enables or disables verbose mode for cmaes
            BO_PARAM(bool, verbose, false);
            /// @ingroup opt_defaults
            /// lower bound (in input) for cmaes
            BO_PARAM(double, lbound, 0.0);
            /// @ingroup opt_defaults
            /// upper bound (in input) for cmaes
            BO_PARAM(double, ubound, 1.0);
            /// @ingroup opt_defaults
            /// if stochastic, the mean of the
            /// last distribution is returned
            /// otherwise, the best ever candidate
            /// is returned. If handle_uncertainty is on,
            /// this is also enabled.
            BO_PARAM(bool, stochastic, false);
            /// @ingroup opt_defaults
            /// number of parent population
            /// -1 to automatically determine
            BO_PARAM(int, lambda, -1);
        };
    } // namespace defaults

    namespace opt {
        /// @ingroup opt
        /// Covariance Matrix Adaptation Evolution Strategy by Hansen et al.
        /// (See: https://www.lri.fr/~hansen/cmaesintro.html)
        /// - our implementation is based on libcmaes (https://github.com/beniz/libcmaes)
        /// - Support bounded and unbounded optimization
        /// - Only available if libcmaes is installed (see the compilation instructions)
        ///
        /// - Parameters :
        ///   - int variant
        ///   - int elitism
        ///   - int restarts
        ///   - int max_fun_evals
        ///   - int max_iters
        ///   - bool auto_max_iters
        ///   - bool equal_fun_evals
        ///   - bool stagnation
        ///   - double fun_tolerance
        ///   - double xrel_tolerance
        ///   - double fun_target
        ///   - bool fun_compute_initial
        ///   - bool handle_uncertainty
        ///   - bool verbose
        ///   - double lb (lower bounds)
        ///   - double ub (upper bounds)
        ///   - bool stochastic
        ///   - int lambda
        template <typename Params>
        struct Cmaes {
        public:
            using ProgressFunction = std::function<void(const libcmaes::CMASolutions&)>;
            using ProgressFunctionUnbounded = std::function<int(const libcmaes::CMAParameters<libcmaes::GenoPheno<libcmaes::NoBoundStrategy>>&, const libcmaes::CMASolutions&)>;
            using ProgressFunctionBounded = std::function<int(const libcmaes::CMAParameters<libcmaes::GenoPheno<libcmaes::pwqBoundStrategy>>&, const libcmaes::CMASolutions&)>;

            template <typename F>
            Eigen::VectorXd operator()(const F& f, const Eigen::VectorXd& init, double bounded) const
            {
                size_t dim = init.size();

                // wrap the function
                libcmaes::FitFunc f_cmaes = [&](const double* x, const int n) {
                    Eigen::Map<const Eigen::VectorXd> m(x, n);
                    // remember that our optimizers maximize
                    return -eval(f, m);
                };

                if (bounded)
                    return _opt_bounded(f_cmaes, dim, init);
                else
                    return _opt_unbounded(f_cmaes, dim, init);
            }

            void set_progress_function(const ProgressFunction& pfunc) { _pfunc = pfunc; }
            void set_unbounded_progress_function(const ProgressFunctionUnbounded& pfunc) { _pfunc_unbounded = pfunc; }
            void set_bounded_progress_function(const ProgressFunctionBounded& pfunc) { _pfunc_bounded = pfunc; }

        private:
            ProgressFunction _pfunc;
            ProgressFunctionUnbounded _pfunc_unbounded;
            ProgressFunctionBounded _pfunc_bounded;

            // F is a CMA-ES style function, not our function
            template <typename F>
            Eigen::VectorXd _opt_unbounded(F& f_cmaes, int dim, const Eigen::VectorXd& init) const
            {
                using namespace libcmaes;
                // initial step-size, i.e. estimated initial parameter error.
                double sigma = 0.5;
                std::vector<double> x0(init.data(), init.data() + init.size());

                CMAParameters<> cmaparams(x0, sigma, Params::opt_cmaes::lambda());
                _set_common_params(cmaparams, dim);

                auto pfunc = CMAStrategy<CovarianceUpdate, GenoPheno<NoBoundStrategy>>::_defaultPFunc;
                if (_pfunc_unbounded) {
                    pfunc = _pfunc_unbounded;
                }
                else if (_pfunc) {
                    pfunc = [this](const CMAParameters<GenoPheno<NoBoundStrategy>>& params, const CMASolutions& sols) { _pfunc(sols); return 0; };
                }

                // the optimization itself
                CMASolutions cmasols = cmaes<>(f_cmaes, cmaparams, pfunc);
                if (Params::opt_cmaes::stochastic() || Params::opt_cmaes::handle_uncertainty())
                    return cmasols.xmean();

                return cmasols.get_best_seen_candidate().get_x_dvec();
            }

            // F is a CMA-ES style function, not our function
            template <typename F>
            Eigen::VectorXd _opt_bounded(F& f_cmaes, int dim, const Eigen::VectorXd& init) const
            {
                using namespace libcmaes;
                // create the parameter object
                // boundary_transformation
                double lbounds[dim], ubounds[dim]; // arrays for lower and upper parameter bounds, respectively
                for (int i = 0; i < dim; i++) {
                    lbounds[i] = Params::opt_cmaes::lbound();
                    ubounds[i] = Params::opt_cmaes::ubound();
                }
                GenoPheno<pwqBoundStrategy> gp(lbounds, ubounds, dim);
                // initial step-size, i.e. estimated initial parameter error.
                double sigma = 0.5 * std::abs(Params::opt_cmaes::ubound() - Params::opt_cmaes::lbound());
                Eigen::VectorXd init_geno = gp.geno(init);
                std::vector<double> x0(init_geno.data(), init_geno.data() + init_geno.size());
                // -1 for automatically decided lambda, 0 is for random seeding of the internal generator.
                CMAParameters<GenoPheno<pwqBoundStrategy>> cmaparams(dim, &x0.front(), sigma, Params::opt_cmaes::lambda(), 0, gp);
                _set_common_params(cmaparams, dim);

                auto pfunc = CMAStrategy<CovarianceUpdate, GenoPheno<pwqBoundStrategy>>::_defaultPFunc;
                if (_pfunc_bounded) {
                    pfunc = _pfunc_bounded;
                }
                else if (_pfunc) {
                    pfunc = [this](const CMAParameters<GenoPheno<pwqBoundStrategy>>& params, const CMASolutions& sols) { _pfunc(sols); return 0; };
                }

                // the optimization itself
                CMASolutions cmasols = cmaes<GenoPheno<pwqBoundStrategy>>(f_cmaes, cmaparams, pfunc);
                if (Params::opt_cmaes::stochastic() || Params::opt_cmaes::handle_uncertainty())
                    return gp.pheno(cmasols.xmean());

                return gp.pheno(cmasols.get_best_seen_candidate().get_x_dvec());
            }

            template <typename P>
            void _set_common_params(P& cmaparams, int dim) const
            {
                using namespace libcmaes;

                // set multi-threading to true
                cmaparams.set_mt_feval(true);
                // aCMAES should be the best choice
                // [see: https://github.com/beniz/libcmaes/wiki/Practical-hints ]
                // but we want the restart -> aIPOP_CMAES
                cmaparams.set_algo(Params::opt_cmaes::variant());
                cmaparams.set_restarts(Params::opt_cmaes::restarts());
                cmaparams.set_elitism(Params::opt_cmaes::elitism());

                // if no max fun evals provided, we compute a recommended value
                size_t max_evals = Params::opt_cmaes::max_fun_evals() < 0
                    ? (900.0 * (dim + 3.0) * (dim + 3.0))
                    : Params::opt_cmaes::max_fun_evals();
                cmaparams.set_max_fevals(max_evals);
                cmaparams.set_stopping_criteria(MAXFEVALS, true);

                // if no max iters provided, we put a safety limit (to not take forever)
                size_t max_iters = Params::opt_cmaes::max_iters() < 0
                    ? 1000000
                    : Params::opt_cmaes::max_iters();
                cmaparams.set_max_iter(max_iters);
                cmaparams.set_stopping_criteria(MAXITER, true);

                // enable/disable automatic max iterations
                cmaparams.set_stopping_criteria(AUTOMAXITER, Params::opt_cmaes::auto_max_iters());

                if (Params::opt_cmaes::fun_tolerance() < 0) {
                    cmaparams.set_stopping_criteria(TOLHISTFUN, false);
                }
                else {
                    // the FTARGET criteria also allows us to enable ftolerance
                    cmaparams.set_stopping_criteria(TOLHISTFUN, true);
                    cmaparams.set_ftolerance(Params::opt_cmaes::fun_tolerance());
                }

                // we allow to set the ftarget parameter
                if (Params::opt_cmaes::fun_target() > 0) {
                    cmaparams.set_stopping_criteria(FTARGET, true);
                    cmaparams.set_ftarget(-Params::opt_cmaes::fun_target());
                }
                else {
                    // we do not know what is the actual maximum / minimum of the function
                    // therefore we deactivate this stopping criterion
                    cmaparams.set_stopping_criteria(FTARGET, false);
                }

                // enable stopping criteria by several equalfunvals
                cmaparams.set_stopping_criteria(EQUALFUNVALS, Params::opt_cmaes::equal_fun_evals());

                // enable additional criteria to stop
                // set different tolerance if available
                if (Params::opt_cmaes::xrel_tolerance() > 0) {
                    cmaparams.set_stopping_criteria(TOLX, true);
                    cmaparams.set_xtolerance(Params::opt_cmaes::xrel_tolerance());
                }
                else {
                    cmaparams.set_stopping_criteria(TOLX, false);
                }

                // enable stopping criteria because of mal-conditions
                cmaparams.set_stopping_criteria(CONDITIONCOV, true);
                cmaparams.set_stopping_criteria(TOLUPSIGMA, true);

                // disable stopping criteria for partial success
                cmaparams.set_stopping_criteria(NOEFFECTAXIS, false);
                cmaparams.set_stopping_criteria(NOEFFECTCOOR, false);

                cmaparams.set_stopping_criteria(STAGNATION, Params::opt_cmaes::stagnation());

                // enable or disable different parameters
                cmaparams.set_initial_fvalue(Params::opt_cmaes::fun_compute_initial());
                cmaparams.set_uh(Params::opt_cmaes::handle_uncertainty());
                cmaparams.set_quiet(!Params::opt_cmaes::verbose());
            }
        };
    } // namespace opt
} // namespace limbo
#endif
#endif


#ifndef BOOST_GEOMETRY_PROJECTIONS_ISEA_HPP
#define BOOST_GEOMETRY_PROJECTIONS_ISEA_HPP

// Boost.Geometry - extensions-gis-projections (based on PROJ4)
// This file is automatically generated. DO NOT EDIT.

// Copyright (c) 2008-2015 Barend Gehrels, Amsterdam, the Netherlands.

// Use, modification and distribution is subject to the Boost Software License,
// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

// This file is converted from PROJ4, http://trac.osgeo.org/proj
// PROJ4 is originally written by Gerald Evenden (then of the USGS)
// PROJ4 is maintained by Frank Warmerdam
// PROJ4 is converted to Boost.Geometry by Barend Gehrels

// Last updated version of proj: 4.9.1

// Original copyright notice:

// This code was entirely written by Nathan Wagner
// and is in the public domain.

// 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.

#include <boost/core/ignore_unused.hpp>
#include <boost/geometry/util/math.hpp>

#include <boost/geometry/extensions/gis/projections/impl/base_static.hpp>
#include <boost/geometry/extensions/gis/projections/impl/base_dynamic.hpp>
#include <boost/geometry/extensions/gis/projections/impl/projects.hpp>
#include <boost/geometry/extensions/gis/projections/impl/factory_entry.hpp>

namespace boost { namespace geometry { namespace projections
{
    #ifndef DOXYGEN_NO_DETAIL
    namespace detail { namespace isea
    {

            static const double E = 52.62263186;
            static const double F = 10.81231696;
            static const double DEG60 = 1.04719755119659774614;
            static const double DEG120 = 2.09439510239319549229;
            static const double DEG72 = 1.25663706143591729537;
            static const double DEG90 = 1.57079632679489661922;
            static const double DEG144 = 2.51327412287183459075;
            static const double DEG36 = 0.62831853071795864768;
            static const double DEG108 = 1.88495559215387594306;
            static const double DEG180 = geometry::math::pi<double>();
            static const double ISEA_SCALE = 0.8301572857837594396028083;
            static const double V_LAT = 0.46364760899944494524;
            static const double E_RAD = 0.91843818702186776133;
            static const double F_RAD = 0.18871053072122403508;
            static const double TABLE_G = 0.6615845383;
            static const double TABLE_H = 0.1909830056;
            static const double RPRIME = 0.91038328153090290025;
            static const double PRECISION = 0.0000000000005;
            static const double ISEA_STD_LAT = 1.01722196792335072101;
            static const double ISEA_STD_LON = .19634954084936207740;

            #define DOWNTRI(tri) (((tri - 1) / 5) % 2 == 1)

            /*
             * Proj 4 provides its own entry points into
             * the code, so none of the library functions
             * need to be global
             */

            struct hex {
                    int iso;
                    int x, y, z;
            };

            /* y *must* be positive down as the xy /iso conversion assumes this */
            static
            int hex_xy(struct hex *h) {
                if (!h->iso) return 1;
                if (h->x >= 0) {
                    h->y = -h->y - (h->x+1)/2;
                } else {
                    /* need to round toward -inf, not toward zero, so x-1 */
                    h->y = -h->y - h->x/2;
                }
                h->iso = 0;

                return 1;
            }

            static
            int hex_iso(struct hex *h) {
                if (h->iso) return 1;

                if (h->x >= 0) {
                    h->y = (-h->y - (h->x+1)/2);
                } else {
                    /* need to round toward -inf, not toward zero, so x-1 */
                    h->y = (-h->y - (h->x)/2);
                }

                h->z = -h->x - h->y;
                h->iso = 1;
                return 1;
            }

            static
            int hexbin2(int horizontal, double width, double x, double y,
                            int *i, int *j) {
                double z, rx, ry, rz;
                double abs_dx, abs_dy, abs_dz;
                int ix, iy, iz, s;
                struct hex h;

                x = x / cos(30 * geometry::math::d2r<double>()); /* rotated X coord */
                y = y - x / 2.0; /* adjustment for rotated X */

                /* adjust for actual hexwidth */
                x /= width;
                y /= width;

                z = -x - y;

                ix = rx = floor(x + 0.5);
                iy = ry = floor(y + 0.5);
                iz = rz = floor(z + 0.5);

                s = ix + iy + iz;

                if (s) {
                    abs_dx = fabs(rx - x);
                    abs_dy = fabs(ry - y);
                    abs_dz = fabs(rz - z);

                    if (abs_dx >= abs_dy && abs_dx >= abs_dz) {
                        ix -= s;
                    } else if (abs_dy >= abs_dx && abs_dy >= abs_dz) {
                        iy -= s;
                    } else {
                        iz -= s;
                    }
                }
                h.x = ix;
                h.y = iy;
                h.z = iz;
                h.iso = 1;

                hex_xy(&h);
                *i = h.x;
                *j = h.y;
                    return ix * 100 + iy;
            }

            enum isea_poly { ISEA_NONE, ISEA_ICOSAHEDRON = 20 };
            enum isea_topology { ISEA_HEXAGON=6, ISEA_TRIANGLE=3, ISEA_DIAMOND=4 };
            enum isea_address_form { ISEA_GEO, ISEA_Q2DI, ISEA_SEQNUM, ISEA_INTERLEAVE,
                ISEA_PLANE, ISEA_Q2DD, ISEA_PROJTRI, ISEA_VERTEX2DD, ISEA_HEX
            };

            struct isea_dgg {
                int    polyhedron; /* ignored, icosahedron */
                double    o_lat, o_lon, o_az; /* orientation, radians */
                int    pole; /* true if standard snyder */
                int    topology; /* ignored, hexagon */
                int    aperture; /* valid values depend on partitioning method */
                int    resolution;
                double    radius; /* radius of the earth in meters, ignored 1.0 */
                int    output; /* an isea_address_form */
                int    triangle; /* triangle of last transformed point */
                int    quad; /* quad of last transformed point */
                unsigned long serial;
            };

            struct isea_pt {
                double x, y;
            };

            struct isea_geo {
                double lon, lat;
            };

            struct isea_address {
                int    type; /* enum isea_address_form */
                int    number;
                double    x,y; /* or i,j or lon,lat depending on type */
            };

            /* ENDINC */

            enum snyder_polyhedron {
                SNYDER_POLY_HEXAGON, SNYDER_POLY_PENTAGON,
                SNYDER_POLY_TETRAHEDRON, SNYDER_POLY_CUBE,
                SNYDER_POLY_OCTAHEDRON, SNYDER_POLY_DODECAHEDRON,
                SNYDER_POLY_ICOSAHEDRON
            };

            struct snyder_constants {
                double          g, G, theta, ea_w, ea_a, ea_b, g_w, g_a, g_b;
            };

            /* TODO put these in radians to avoid a later conversion */
            static
            struct snyder_constants constants[] = {
                {23.80018260, 62.15458023, 60.0, 3.75, 1.033, 0.968, 5.09, 1.195, 1.0},
                {20.07675127, 55.69063953, 54.0, 2.65, 1.030, 0.983, 3.59, 1.141, 1.027},
                {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
                {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
                {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
                {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
                {37.37736814, 36.0, 30.0, 17.27, 1.163, 0.860, 13.14, 1.584, 1.0},
            };

            /* sqrt(5)/M_PI */

            /* 26.565051177 degrees */

            static
            struct isea_geo vertex[] = {
                {0.0, DEG90},
                {DEG180, V_LAT},
                {-DEG108, V_LAT},
                {-DEG36, V_LAT},
                {DEG36, V_LAT},
                {DEG108, V_LAT},
                {-DEG144, -V_LAT},
                {-DEG72, -V_LAT},
                {0.0, -V_LAT},
                {DEG72, -V_LAT},
                {DEG144, -V_LAT},
                {0.0, -DEG90}
            };

            /* TODO make an isea_pt array of the vertices as well */

            static int      tri_v1[] = {0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 2, 3, 4, 5, 1, 11, 11, 11, 11, 11};

            /* 52.62263186 */

            /* 10.81231696 */

            /* triangle Centers */
            struct isea_geo icostriangles[] = {
                {0.0, 0.0},
                {-DEG144, E_RAD},
                {-DEG72, E_RAD},
                {0.0, E_RAD},
                {DEG72, E_RAD},
                {DEG144, E_RAD},
                {-DEG144, F_RAD},
                {-DEG72, F_RAD},
                {0.0, F_RAD},
                {DEG72, F_RAD},
                {DEG144, F_RAD},
                {-DEG108, -F_RAD},
                {-DEG36, -F_RAD},
                {DEG36, -F_RAD},
                {DEG108, -F_RAD},
                {DEG180, -F_RAD},
                {-DEG108, -E_RAD},
                {-DEG36, -E_RAD},
                {DEG36, -E_RAD},
                {DEG108, -E_RAD},
                {DEG180, -E_RAD},
            };

            static double
            az_adjustment(int triangle)
            {
                double          adj;

                struct isea_geo v;
                struct isea_geo c;

                v = vertex[tri_v1[triangle]];
                c = icostriangles[triangle];

                /* TODO looks like the adjustment is always either 0 or 180 */
                /* at least if you pick your vertex carefully */
                adj = atan2(cos(v.lat) * sin(v.lon - c.lon),
                        cos(c.lat) * sin(v.lat)
                        - sin(c.lat) * cos(v.lat) * cos(v.lon - c.lon));
                return adj;
            }

            /* R tan(g) sin(60) */

            /* H = 0.25 R tan g = */

            static
            struct isea_pt
            isea_triangle_xy(int triangle)
            {
                struct isea_pt  c;
                double Rprime = 0.91038328153090290025;

                triangle = (triangle - 1) % 20;

                c.x = TABLE_G * ((triangle % 5) - 2) * 2.0;
                if (triangle > 9) {
                    c.x += TABLE_G;
                }
                switch (triangle / 5) {
                case 0:
                    c.y = 5.0 * TABLE_H;
                    break;
                case 1:
                    c.y = TABLE_H;
                    break;
                case 2:
                    c.y = -TABLE_H;
                    break;
                case 3:
                    c.y = -5.0 * TABLE_H;
                    break;
                default:
                    /* should be impossible */
                    throw proj_exception();
                };
                c.x *= Rprime;
                c.y *= Rprime;

                return c;
            }

            /* snyder eq 14 */
            static double
            sph_azimuth(double f_lon, double f_lat, double t_lon, double t_lat)
            {
                double          az;

                az = atan2(cos(t_lat) * sin(t_lon - f_lon),
                       cos(f_lat) * sin(t_lat)
                       - sin(f_lat) * cos(t_lat) * cos(t_lon - f_lon)
                    );
                return az;
            }

            /* coord needs to be in radians */
            static
            int
            isea_snyder_forward(struct isea_geo * ll, struct isea_pt * out)
            {
                int             i;

                /*
                 * spherical distance from center of polygon face to any of its
                 * vertexes on the globe
                 */
                double          g;

                /*
                 * spherical angle between radius vector to center and adjacent edge
                 * of spherical polygon on the globe
                 */
                double          G;

                /*
                 * plane angle between radius vector to center and adjacent edge of
                 * plane polygon
                 */
                double          theta;

                /* additional variables from snyder */
                double          q, Rprime, H, Ag, Azprime, Az, dprime, f, rho,
                                x, y;

                /* variables used to store intermediate results */
                double          cot_theta, tan_g, az_offset;

                /* how many multiples of 60 degrees we adjust the azimuth */
                int             Az_adjust_multiples;

                struct snyder_constants c;

                /*
                 * TODO by locality of reference, start by trying the same triangle
                 * as last time
                 */

                /* TODO put these constants in as radians to begin with */
                c = constants[SNYDER_POLY_ICOSAHEDRON];
                theta = c.theta * geometry::math::d2r<double>();
                g = c.g * geometry::math::d2r<double>();
                G = c.G * geometry::math::d2r<double>();

                for (i = 1; i <= 20; i++) {
                    double          z;
                    struct isea_geo center;

                    center = icostriangles[i];

                    /* step 1 */
            #if 0
                    z = sph_distance(center.lon, center.lat, ll->lon, ll->lat);
            #else
                    z = acos(sin(center.lat) * sin(ll->lat)
                         + cos(center.lat) * cos(ll->lat) * cos(ll->lon - center.lon));
            #endif

                    /* not on this triangle */
                    if (z > g + 0.000005) { /* TODO DBL_EPSILON */
                        continue;
                    }
                    Az = sph_azimuth(ll->lon, ll->lat, center.lon, center.lat);

                    Az = atan2(cos(ll->lat) * sin(ll->lon - center.lon),
                           cos(center.lat) * sin(ll->lat)
                    - sin(center.lat) * cos(ll->lat) * cos(ll->lon - center.lon)
                        );

                    /* step 2 */

                    /* This calculates "some" vertex coordinate */
                    az_offset = az_adjustment(i);

                    Az -= az_offset;

                    /* TODO I don't know why we do this.  It's not in snyder */
                    /* maybe because we should have picked a better vertex */
                    if (Az < 0.0) {
                        Az += geometry::math::two_pi<double>();
                    }
                    /*
                     * adjust Az for the point to fall within the range of 0 to
                     * 2(90 - theta) or 60 degrees for the hexagon, by
                     * and therefore 120 degrees for the triangle
                     * of the icosahedron
                     * subtracting or adding multiples of 60 degrees to Az and
                     * recording the amount of adjustment
                     */

                    Az_adjust_multiples = 0;
                    while (Az < 0.0) {
                        Az += DEG120;
                        Az_adjust_multiples--;
                    }
                    while (Az > DEG120 + DBL_EPSILON) {
                        Az -= DEG120;
                        Az_adjust_multiples++;
                    }

                    /* step 3 */
                    cot_theta = 1.0 / tan(theta);
                    tan_g = tan(g);    /* TODO this is a constant */

                    /* Calculate q from eq 9. */
                    /* TODO cot_theta is cot(30) */
                    q = atan2(tan_g, cos(Az) + sin(Az) * cot_theta);

                    /* not in this triangle */
                    if (z > q + 0.000005) {
                        continue;
                    }
                    /* step 4 */

                    /* Apply equations 5-8 and 10-12 in order */

                    /* eq 5 */
                    /* Rprime = 0.9449322893 * R; */
                    /* R' in the paper is for the truncated */
                    Rprime = 0.91038328153090290025;

                    /* eq 6 */
                    H = acos(sin(Az) * sin(G) * cos(g) - cos(Az) * cos(G));

                    /* eq 7 */
                    /* Ag = (Az + G + H - DEG180) * M_PI * R * R / DEG180; */
                    Ag = Az + G + H - DEG180;

                    /* eq 8 */
                    Azprime = atan2(2.0 * Ag, Rprime * Rprime * tan_g * tan_g - 2.0 * Ag * cot_theta);

                    /* eq 10 */
                    /* cot(theta) = 1.73205080756887729355 */
                    dprime = Rprime * tan_g / (cos(Azprime) + sin(Azprime) * cot_theta);

                    /* eq 11 */
                    f = dprime / (2.0 * Rprime * sin(q / 2.0));

                    /* eq 12 */
                    rho = 2.0 * Rprime * f * sin(z / 2.0);

                    /*
                     * add back the same 60 degree multiple adjustment from step
                     * 2 to Azprime
                     */

                    Azprime += DEG120 * Az_adjust_multiples;

                    /* calculate rectangular coordinates */

                    x = rho * sin(Azprime);
                    y = rho * cos(Azprime);

                    /*
                     * TODO
                     * translate coordinates to the origin for the particular
                     * hexagon on the flattened polyhedral map plot
                     */

                    out->x = x;
                    out->y = y;

                    return i;
                }

                /*
                 * should be impossible, this implies that the coordinate is not on
                 * any triangle
                 */

                fprintf(stderr, "impossible transform: %f %f is not on any triangle\n",
                    ll->lon * geometry::math::r2d<double>(), ll->lat * geometry::math::r2d<double>());

                throw proj_exception();

                /* not reached */
                return 0;        /* supresses a warning */
            }

            /*
             * return the new coordinates of any point in orginal coordinate system.
             * Define a point (newNPold) in orginal coordinate system as the North Pole in
             * new coordinate system, and the great circle connect the original and new
             * North Pole as the lon0 longitude in new coordinate system, given any point
             * in orginal coordinate system, this function return the new coordinates.
             */

            /* formula from Snyder, Map Projections: A working manual, p31 */
            /*
             * old north pole at np in new coordinates
             * could be simplified a bit with fewer intermediates
             *
             * TODO take a result pointer
             */
            static
            struct isea_geo
            snyder_ctran(struct isea_geo * np, struct isea_geo * pt)
            {
                struct isea_geo npt;
                double          alpha, phi, lambda, lambda0, beta, lambdap, phip;
                double          sin_phip;
                double          lp_b;    /* lambda prime minus beta */
                double          cos_p, sin_a;

                phi = pt->lat;
                lambda = pt->lon;
                alpha = np->lat;
                beta = np->lon;
                lambda0 = beta;

                cos_p = cos(phi);
                sin_a = sin(alpha);

                /* mpawm 5-7 */
                sin_phip = sin_a * sin(phi) - cos(alpha) * cos_p * cos(lambda - lambda0);

                /* mpawm 5-8b */

                /* use the two argument form so we end up in the right quadrant */
                lp_b = atan2(cos_p * sin(lambda - lambda0),
                   (sin_a * cos_p * cos(lambda - lambda0) + cos(alpha) * sin(phi)));

                lambdap = lp_b + beta;

                /* normalize longitude */
                /* TODO can we just do a modulus ? */
                lambdap = fmod(lambdap, geometry::math::two_pi<double>());
                while (lambdap > geometry::math::pi<double>())
                    lambdap -= geometry::math::two_pi<double>();
                while (lambdap < -geometry::math::pi<double>())
                    lambdap += geometry::math::two_pi<double>();

                phip = asin(sin_phip);

                npt.lat = phip;
                npt.lon = lambdap;

                return npt;
            }

            static
            struct isea_geo
            isea_ctran(struct isea_geo * np, struct isea_geo * pt, double lon0)
            {
                struct isea_geo npt;

                np->lon += geometry::math::pi<double>();
                npt = snyder_ctran(np, pt);
                np->lon -= geometry::math::pi<double>();

                npt.lon -= (geometry::math::pi<double>() - lon0 + np->lon);

                /*
                 * snyder is down tri 3, isea is along side of tri1 from vertex 0 to
                 * vertex 1 these are 180 degrees apart
                 */
                npt.lon += geometry::math::pi<double>();
                /* normalize longitude */
                npt.lon = fmod(npt.lon, geometry::math::two_pi<double>());
                while (npt.lon > geometry::math::pi<double>())
                    npt.lon -= geometry::math::two_pi<double>();
                while (npt.lon < -geometry::math::pi<double>())
                    npt.lon += geometry::math::two_pi<double>();

                return npt;
            }

            /* in radians */

            /* fuller's at 5.2454 west, 2.3009 N, adjacent at 7.46658 deg */

            static
            int
            isea_grid_init(struct isea_dgg * g)
            {
                if (!g)
                    return 0;

                g->polyhedron = 20;
                g->o_lat = ISEA_STD_LAT;
                g->o_lon = ISEA_STD_LON;
                g->o_az = 0.0;
                g->aperture = 4;
                g->resolution = 6;
                g->radius = 1.0;
                g->topology = 6;

                return 1;
            }

            static
            int
            isea_orient_isea(struct isea_dgg * g)
            {
                if (!g)
                    return 0;
                g->o_lat = ISEA_STD_LAT;
                g->o_lon = ISEA_STD_LON;
                g->o_az = 0.0;
                return 1;
            }

            static
            int
            isea_orient_pole(struct isea_dgg * g)
            {
                if (!g)
                    return 0;
                g->o_lat = geometry::math::half_pi<double>();
                g->o_lon = 0.0;
                g->o_az = 0;
                return 1;
            }

            static
            int
            isea_transform(struct isea_dgg * g, struct isea_geo * in,
                       struct isea_pt * out)
            {
                struct isea_geo i, pole;
                int             tri;

                pole.lat = g->o_lat;
                pole.lon = g->o_lon;

                i = isea_ctran(&pole, in, g->o_az);

                tri = isea_snyder_forward(&i, out);
                out->x *= g->radius;
                out->y *= g->radius;
                g->triangle = tri;

                return tri;
            }

            static
            void
            isea_rotate(struct isea_pt * pt, double degrees)
            {
                double          rad;

                double          x, y;

                rad = -degrees * geometry::math::d2r<double>();
                while (rad >= geometry::math::two_pi<double>()) rad -= geometry::math::two_pi<double>();
                while (rad <= -geometry::math::two_pi<double>()) rad += geometry::math::two_pi<double>();

                x = pt->x * cos(rad) + pt->y * sin(rad);
                y = -pt->x * sin(rad) + pt->y * cos(rad);

                pt->x = x;
                pt->y = y;
            }

            static
            int isea_tri_plane(int tri, struct isea_pt *pt, double radius) {
                struct isea_pt tc; /* center of triangle */

                if (DOWNTRI(tri)) {
                    isea_rotate(pt, 180.0);
                }
                tc = isea_triangle_xy(tri);
                tc.x *= radius;
                tc.y *= radius;
                pt->x += tc.x;
                pt->y += tc.y;

                return tri;
            }

            /* convert projected triangle coords to quad xy coords, return quad number */
            static
            int
            isea_ptdd(int tri, struct isea_pt *pt) {
                int             downtri, quad;

                downtri = (((tri - 1) / 5) % 2 == 1);
                boost::ignore_unused(downtri);
                quad = ((tri - 1) % 5) + ((tri - 1) / 10) * 5 + 1;

                isea_rotate(pt, downtri ? 240.0 : 60.0);
                if (downtri) {
                    pt->x += 0.5;
                    /* pt->y += cos(30.0 * M_PI / 180.0); */
                    pt->y += .86602540378443864672;
                }
                return quad;
            }

            static
            int
            isea_dddi_ap3odd(struct isea_dgg *g, int quad, struct isea_pt *pt, struct isea_pt *di)
            {
                struct isea_pt  v;
                double          hexwidth;
                double          sidelength;    /* in hexes */
                int             d, i;
                int             maxcoord;
                struct hex      h;

                /* This is the number of hexes from apex to base of a triangle */
                sidelength = (pow(2.0, g->resolution) + 1.0) / 2.0;

                /* apex to base is cos(30deg) */
                hexwidth = cos(geometry::math::pi<double>() / 6.0) / sidelength;

                /* TODO I think sidelength is always x.5, so
                 * (int)sidelength * 2 + 1 might be just as good
                 */
                maxcoord = (int) (sidelength * 2.0 + 0.5);

                v = *pt;
                hexbin2(0, hexwidth, v.x, v.y, &h.x, &h.y);
                h.iso = 0;
                hex_iso(&h);

                d = h.x - h.z;
                i = h.x + h.y + h.y;

                /*
                 * you want to test for max coords for the next quad in the same
                 * "row" first to get the case where both are max
                 */
                if (quad <= 5) {
                    if (d == 0 && i == maxcoord) {
                        /* north pole */
                        quad = 0;
                        d = 0;
                        i = 0;
                    } else if (i == maxcoord) {
                        /* upper right in next quad */
                        quad += 1;
                        if (quad == 6)
                            quad = 1;
                        i = maxcoord - d;
                        d = 0;
                    } else if (d == maxcoord) {
                        /* lower right in quad to lower right */
                        quad += 5;
                        d = 0;
                    }
                } else if (quad >= 6) {
                    if (i == 0 && d == maxcoord) {
                        /* south pole */
                        quad = 11;
                        d = 0;
                        i = 0;
                    } else if (d == maxcoord) {
                        /* lower right in next quad */
                        quad += 1;
                        if (quad == 11)
                            quad = 6;
                        d = maxcoord - i;
                        i = 0;
                    } else if (i == maxcoord) {
                        /* upper right in quad to upper right */
                        quad = (quad - 4) % 5;
                        i = 0;
                    }
                }

                di->x = d;
                di->y = i;

                g->quad = quad;
                return quad;
            }

            static
            int
            isea_dddi(struct isea_dgg *g, int quad, struct isea_pt *pt, struct isea_pt *di) {
                struct isea_pt  v;
                double          hexwidth;
                int             sidelength;    /* in hexes */
                struct hex      h;

                if (g->aperture == 3 && g->resolution % 2 != 0) {
                    return isea_dddi_ap3odd(g, quad, pt, di);
                }
                /* todo might want to do this as an iterated loop */
                if (g->aperture >0) {
                    sidelength = (int) (pow(static_cast<double>(g->aperture), g->resolution / 2.0) + 0.5);
                } else {
                    sidelength = g->resolution;
                }

                hexwidth = 1.0 / sidelength;

                v = *pt;
                isea_rotate(&v, -30.0);
                hexbin2(0, hexwidth, v.x, v.y, &h.x, &h.y);
                h.iso = 0;
                hex_iso(&h);

                /* we may actually be on another quad */
                if (quad <= 5) {
                    if (h.x == 0 && h.z == -sidelength) {
                        /* north pole */
                        quad = 0;
                        h.z = 0;
                        h.y = 0;
                        h.x = 0;
                    } else if (h.z == -sidelength) {
                        quad = quad + 1;
                        if (quad == 6)
                            quad = 1;
                        h.y = sidelength - h.x;
                        h.z = h.x - sidelength;
                        h.x = 0;
                    } else if (h.x == sidelength) {
                        quad += 5;
                        h.y = -h.z;
                        h.x = 0;
                    }
                } else if (quad >= 6) {
                    if (h.z == 0 && h.x == sidelength) {
                        /* south pole */
                        quad = 11;
                        h.x = 0;
                        h.y = 0;
                        h.z = 0;
                    } else if (h.x == sidelength) {
                        quad = quad + 1;
                        if (quad == 11)
                            quad = 6;
                        h.x = h.y + sidelength;
                        h.y = 0;
                        h.z = -h.x;
                    } else if (h.y == -sidelength) {
                        quad -= 4;
                        h.y = 0;
                        h.z = -h.x;
                    }
                }
                di->x = h.x;
                di->y = -h.z;

                g->quad = quad;
                return quad;
            }

            static
            int isea_ptdi(struct isea_dgg *g, int tri, struct isea_pt *pt,
                           struct isea_pt *di) {
                struct isea_pt  v;
                int             quad;

                v = *pt;
                quad = isea_ptdd(tri, &v);
                quad = isea_dddi(g, quad, &v, di);
                return quad;
            }

            /* q2di to seqnum */
            static
            int isea_disn(struct isea_dgg *g, int quad, struct isea_pt *di) {
                int             sidelength;
                int             sn, height;
                int             hexes;

                if (quad == 0) {
                    g->serial = 1;
                    return g->serial;
                }
                /* hexes in a quad */
                hexes = (int) (pow(static_cast<double>(g->aperture), g->resolution) + 0.5);
                if (quad == 11) {
                    g->serial = 1 + 10 * hexes + 1;
                    return g->serial;
                }
                if (g->aperture == 3 && g->resolution % 2 == 1) {
                    height = (int) (pow(static_cast<double>(g->aperture), (g->resolution - 1) / 2.0));
                    sn = ((int) di->x) * height;
                    sn += ((int) di->y) / height;
                    sn += (quad - 1) * hexes;
                    sn += 2;
                } else {
                    sidelength = (int) (pow(static_cast<double>(g->aperture), g->resolution / 2.0) + 0.5);
                    sn = (quad - 1) * hexes + sidelength * di->x + di->y + 2;
                }

                g->serial = sn;
                return sn;
            }

            /* TODO just encode the quad in the d or i coordinate
             * quad is 0-11, which can be four bits.
             * d' = d << 4 + q, d = d' >> 4, q = d' & 0xf
             */
            /* convert a q2di to global hex coord */
            static
            int isea_hex(struct isea_dgg *g, int tri,
                    struct isea_pt *pt, struct isea_pt *hex) {
                struct isea_pt v;
                int sidelength;
                int d, i, x, y, quad;

                quad = isea_ptdi(g, tri, pt, &v);

                hex->x = ((int)v.x << 4) + quad;
                hex->y = v.y;

                return 1;

                d = v.x;
                i = v.y;

                /* Aperture 3 odd resolutions */
                if (g->aperture == 3 && g->resolution % 2 != 0) {
                    int offset = (int)(pow(3.0, g->resolution - 1) + 0.5);

                    d += offset * ((g->quad-1) % 5);
                    i += offset * ((g->quad-1) % 5);

                    if (quad == 0) {
                        d = 0;
                        i = offset;
                    } else if (quad == 11) {
                        d = 2 * offset;
                        i = 0;
                    } else if (quad > 5) {
                        d += offset;
                    }

                    x = (2*d - i) /3;
                    y = (2*i - d) /3;

                    hex->x = x + offset / 3;
                    hex->y = y + 2 * offset / 3;
                    return 1;
                }

                /* aperture 3 even resolutions and aperture 4 */
                sidelength = (int) (pow(static_cast<double>(g->aperture), g->resolution / 2.0) + 0.5);
                if (g->quad == 0) {
                    hex->x = 0;
                    hex->y = sidelength;
                } else if (g->quad == 11) {
                    hex->x = sidelength * 2;
                    hex->y = 0;
                } else {
                    hex->x = d + sidelength * ((g->quad-1) % 5);
                    if (g->quad > 5) hex->x += sidelength;
                    hex->y = i + sidelength * ((g->quad-1) % 5);
                }

                return 1;
            }

            static
            struct isea_pt
            isea_forward(struct isea_dgg *g, struct isea_geo *in)
            {
                int             tri, downtri;
                struct isea_pt  out, coord;

                tri = isea_transform(g, in, &out);

                downtri = (((tri - 1) / 5) % 2 == 1);
                boost::ignore_unused(downtri);

                if (g->output == ISEA_PLANE) {
                    isea_tri_plane(tri, &out, g->radius);
                    return out;
                }

                /* convert to isea standard triangle size */
                out.x = out.x / g->radius * ISEA_SCALE;
                out.y = out.y / g->radius * ISEA_SCALE;
                out.x += 0.5;
                out.y += 2.0 * .14433756729740644112;

                switch (g->output) {
                case ISEA_PROJTRI:
                    /* nothing to do, already in projected triangle */
                    break;
                case ISEA_VERTEX2DD:
                    g->quad = isea_ptdd(tri, &out);
                    break;
                case ISEA_Q2DD:
                    /* Same as above, we just don't print as much */
                    g->quad = isea_ptdd(tri, &out);
                    break;
                case ISEA_Q2DI:
                    g->quad = isea_ptdi(g, tri, &out, &coord);
                    return coord;
                    break;
                case ISEA_SEQNUM:
                    isea_ptdi(g, tri, &out, &coord);
                    /* disn will set g->serial */
                    isea_disn(g, g->quad, &coord);
                    return coord;
                    break;
                case ISEA_HEX:
                    isea_hex(g, tri, &out, &coord);
                    return coord;
                    break;
                }

                return out;
            }
            /*
             * Proj 4 integration code follows
             */

            struct par_isea
            {
                struct isea_dgg dgg;
            };

            // template class, using CRTP to implement forward/inverse
            template <typename Geographic, typename Cartesian, typename Parameters>
            struct base_isea_spheroid : public base_t_f<base_isea_spheroid<Geographic, Cartesian, Parameters>,
                     Geographic, Cartesian, Parameters>
            {

                 typedef double geographic_type;
                 typedef double cartesian_type;

                par_isea m_proj_parm;

                inline base_isea_spheroid(const Parameters& par)
                    : base_t_f<base_isea_spheroid<Geographic, Cartesian, Parameters>,
                     Geographic, Cartesian, Parameters>(*this, par) {}

                // FORWARD(s_forward)
                // Project coordinates from geographic (lon, lat) to cartesian (x, y)
                inline void fwd(geographic_type& lp_lon, geographic_type& lp_lat, cartesian_type& xy_x, cartesian_type& xy_y) const
                {
                    struct isea_pt out;
                    struct isea_geo in;

                    in.lon = lp_lon;
                    in.lat = lp_lat;

                    isea_dgg copy = this->m_proj_parm.dgg;
                    out = isea_forward(&copy, &in);

                    xy_x = out.x;
                    xy_y = out.y;
                }

                static inline std::string get_name()
                {
                    return "isea_spheroid";
                }

            };

            // Icosahedral Snyder Equal Area
            template <typename Parameters>
            void setup_isea(Parameters& par, par_isea& proj_parm)
            {
                std::string opt;

                    isea_grid_init(&proj_parm.dgg);

                    proj_parm.dgg.output = ISEA_PLANE;
            /*        proj_parm.dgg.radius = par.a; / * otherwise defaults to 1 */
                /* calling library will scale, I think */

                opt = pj_param(par.params, "sorient").s;
                if (! opt.empty()) {
                    if (opt == std::string("isea")) {
                        isea_orient_isea(&proj_parm.dgg);
                    } else if (opt == std::string("pole")) {
                        isea_orient_pole(&proj_parm.dgg);
                    } else {
                        throw proj_exception(-34);
                    }
                }

                if (pj_param(par.params, "tazi").i) {
                    proj_parm.dgg.o_az = pj_param(par.params, "razi").f;
                }

                if (pj_param(par.params, "tlon_0").i) {
                    proj_parm.dgg.o_lon = pj_param(par.params, "rlon_0").f;
                }

                if (pj_param(par.params, "tlat_0").i) {
                    proj_parm.dgg.o_lat = pj_param(par.params, "rlat_0").f;
                }

                if (pj_param(par.params, "taperture").i) {
                    proj_parm.dgg.aperture = pj_param(par.params, "iaperture").i;
                }

                if (pj_param(par.params, "tresolution").i) {
                    proj_parm.dgg.resolution = pj_param(par.params, "iresolution").i;
                }

                opt = pj_param(par.params, "smode").s;
                if (! opt.empty()) {
                    if (opt == std::string("plane")) {
                        proj_parm.dgg.output = ISEA_PLANE;
                    } else if (opt == std::string("di")) {
                        proj_parm.dgg.output = ISEA_Q2DI;
                    }
                    else if (opt == std::string("dd")) {
                        proj_parm.dgg.output = ISEA_Q2DD;
                    }
                    else if (opt == std::string("hex")) {
                        proj_parm.dgg.output = ISEA_HEX;
                    }
                    else {
                        /* TODO verify error code.  Possibly eliminate magic */
                        throw proj_exception(-34);
                    }
                }

                if (pj_param(par.params, "trescale").i) {
                    proj_parm.dgg.radius = ISEA_SCALE;
                }

                if (pj_param(par.params, "tresolution").i) {
                    proj_parm.dgg.resolution = pj_param(par.params, "iresolution").i;
                } else {
                    proj_parm.dgg.resolution = 4;
                }

                if (pj_param(par.params, "taperture").i) {
                    proj_parm.dgg.aperture = pj_param(par.params, "iaperture").i;
                } else {
                    proj_parm.dgg.aperture = 3;
                }
            }

        }} // namespace detail::isea
    #endif // doxygen

    /*!
        \brief Icosahedral Snyder Equal Area projection
        \ingroup projections
        \tparam Geographic latlong point type
        \tparam Cartesian xy point type
        \tparam Parameters parameter type
        \par Projection characteristics
         - Spheroid
        \par Projection parameters
         - orient (string)
         - azi: Azimuth (or Gamma) (degrees)
         - lon_0: Central meridian (degrees)
         - lat_0: Latitude of origin (degrees)
         - aperture (integer)
         - resolution (integer)
         - mode (string)
         - rescale
        \par Example
        \image html ex_isea.gif
    */
    template <typename Geographic, typename Cartesian, typename Parameters = parameters>
    struct isea_spheroid : public detail::isea::base_isea_spheroid<Geographic, Cartesian, Parameters>
    {
        inline isea_spheroid(const Parameters& par) : detail::isea::base_isea_spheroid<Geographic, Cartesian, Parameters>(par)
        {
            detail::isea::setup_isea(this->m_par, this->m_proj_parm);
        }
    };

    #ifndef DOXYGEN_NO_DETAIL
    namespace detail
    {

        // Factory entry(s)
        template <typename Geographic, typename Cartesian, typename Parameters>
        class isea_entry : public detail::factory_entry<Geographic, Cartesian, Parameters>
        {
            public :
                virtual projection<Geographic, Cartesian>* create_new(const Parameters& par) const
                {
                    return new base_v_f<isea_spheroid<Geographic, Cartesian, Parameters>, Geographic, Cartesian, Parameters>(par);
                }
        };

        template <typename Geographic, typename Cartesian, typename Parameters>
        inline void isea_init(detail::base_factory<Geographic, Cartesian, Parameters>& factory)
        {
            factory.add_to_factory("isea", new isea_entry<Geographic, Cartesian, Parameters>);
        }

    } // namespace detail
    #endif // doxygen

}}} // namespace boost::geometry::projections

#endif // BOOST_GEOMETRY_PROJECTIONS_ISEA_HPP