/*
 * Copyright (C) 2015 INRA
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *    2. 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.
 *
 * THIS SOFTWARE IS PROVIDED "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 ORG_VLEPROJECT_KERNEL_DEVS_QSS1_HPP
#define ORG_VLEPROJECT_KERNEL_DEVS_QSS1_HPP

#include <vle/devs/devs.hpp>
#include <vle/qss-common.hpp>
#include <boost/math/special_functions/sign.hpp>

namespace vle {
namespace devs {
namespace qss {

inline static constexpr double nan() noexcept
{
    return std::numeric_limits <double>::quiet_NaN();
}

struct QssInputPort {
    QssInputPort(std::size_t size) noexcept
        : m_value(size, nan())
        , m_dirac(false)
    {}

    const double &operator[](std::size_t i) const noexcept
    {
        return m_value[i];
    }

    double &operator[](std::size_t i) noexcept
    {
        return m_value[i];
    }

    void dirac() noexcept
    {
        m_dirac = true;
    }

    void clear() noexcept
    {
        std::fill(m_value.begin(), m_value.end(), nan());
        m_dirac = false;
    }

    std::size_t size() const noexcept
    {
        return m_value.size();
    }

    bool empty() const noexcept
    {
        if (m_dirac)
            return false;

        for (auto val : m_value)
            if (not std::isnan(val))
                return false;

        return true;
    }

    std::vector <double> m_value;
    bool m_dirac;
};

struct QssOutputPort {
    constexpr QssOutputPort(double) noexcept
        : m_value(nan())
    {}

    constexpr const double &operator[](std::size_t) const noexcept
    {
        return m_value;
    }

    constexpr double &operator[](std::size_t) noexcept
    {
        return m_value;
    }

    constexpr std::size_t size() const noexcept
    {
        return 1u;
    }

    constexpr void clear() noexcept
    {
        m_value = nan();
    }

    bool empty() const noexcept
    {
        return std::isnan(m_value);
    }

    double m_value;
};

template <typename Time, typename Container>
class StaticFunction : public AtomicModel <Time, QssInputPort, QssOutputPort>
{
public:
    using parent_type = AtomicModel <Time, QssInputPort, QssOutputPort>;
    using time_format = typename parent_type::time_format;
    using time_type = typename parent_type::time_type;
    using inputport_type = typename parent_type::inputport_type;
    using outputport_type = typename parent_type::inputport_type;

    StaticFunction(const Context &ctx, std::size_t system_size,
                   Function <Time, Container> function_)
        : parent_type(ctx, system_size, nan())
        , m_integrate_function(function_)
        , m_variable(system_size)
        , m_sigma(Time::infinity())
        , m_time(Time::null())
    {
        if (system_size == 0ul)
            throw std::invalid_argument(
                "vle::dsde::qss1::StaticFunction: bad system size");
    }

    virtual time_type ta() const
    {
        return m_sigma;
    }

    virtual void lambda() const
    {
        parent_type::y[0] = m_integrate_function(m_variable, m_time);
    }

    virtual void internal()
    {
        m_sigma = Time::infinity();
    }

    virtual void external(const time_type &e)
    {
        m_time += e;
        m_sigma = Time::infinity();

        for (std::size_t i = 0ul, end = parent_type::x.size(); i != end; ++i) {
            if (not std::isnan(parent_type::x[i])) {
                m_variable[i] = parent_type::x[i];
                m_sigma = Time::null();
            }
        }
    }

private:
    Function <Time, Container> m_integrate_function;
    Container m_variable;
    time_type m_sigma;
    time_type m_time;
};

template <typename Time>
class Integrator : public AtomicModel <Time, QssInputPort, QssOutputPort>
{
public:
    using parent_type = AtomicModel <Time, QssInputPort, QssOutputPort>;
    using time_format = typename parent_type::time_format;
    using time_type = typename parent_type::time_type;

    Integrator(const Context &ctx, double dq, double epsilon, double x)
        : parent_type(ctx, 1u, nan())
        , m_sigma(Time::null())
        , m_dq(dq)
        , m_epsilon(epsilon)
        , m_X(x)
        , m_dX(0.0)
        , m_q(std::floor(x / dq) * dq)
    {}

    virtual time_type ta() const
    {
        return m_sigma;
    }

    virtual void lambda() const
    {
        parent_type::y[0] = m_q + m_dq * boost::math::sign(m_dX);
    }

    virtual void internal()
    {
        m_X += (m_sigma * m_dX);

        if (m_dX > 0.0) {
            m_sigma = m_dq / m_dX;
            m_q = m_q + m_dq;
        } else if (m_dX < 0.0) {
            m_sigma = - m_dq / m_dX;
            m_q = m_q - m_dq;
        } else {
            m_sigma = Time::infinity();
        }
    }

    virtual void external(const time_type &e)
    {
        auto xv = parent_type::x[0];
        m_X += (e * m_dX);

        if (xv > 0.0) {
            m_sigma = (m_q + m_dq - m_X) / xv;
        } else if (xv < 0.0) {
            m_sigma = (m_q - m_epsilon - m_X) / xv;
        } else {
            m_sigma = Time::infinity();
        }

        m_dX = xv;
    }

    constexpr inline double value() const noexcept
    {
        return m_X;
    }

private:
    time_type m_sigma;
    double m_dq;
    double m_epsilon;
    double m_X;
    double m_dX;
    double m_q;
};

template <typename Time, typename Container>
class EquationBlock : public CoupledModel <
    Time, QssInputPort, QssOutputPort,
    QssInputPort, QssOutputPort >
{
public:
    using parent_type = CoupledModel <Time, QssInputPort, QssOutputPort,
          QssInputPort, QssOutputPort>;
    using time_format = typename parent_type::time_format;
    using time_type = typename parent_type::time_type;
    using child_type = typename parent_type::child_type;

    using UpdatedPort = typename parent_type::UpdatedPort;

    EquationBlock(const vle::Context &ctx,
                  double dq,
                  double epsilon,
                  double x,
                  std::size_t system_size,
                  std::size_t id,
                  Function <Time, Container> function_)
        : parent_type(ctx, system_size, nan())
        , m_integrator(ctx, dq, epsilon, x)
        , m_staticfunction(ctx, system_size, function_)
        , m_id(id)
    {
        if (id >= system_size)
            throw std::invalid_argument(
                "vle::dsde::qss1::EquationBlock: bad id or system size");
    }

    virtual typename parent_type::children_t
    children() override final
    {
        return { &m_integrator, &m_staticfunction };
    }

    virtual void post(const child_type *out, UpdatedPort &in) const override
    {
        if (!out) {
            for (std::size_t i = 0ul, end = parent_type::x.size(); i != end;
                ++i) {
                if (!std::isnan(parent_type::x[i])) {
                    m_staticfunction.x[i] = parent_type::x[i];
                    in.emplace(&m_staticfunction);
                }
            }
        } else if (out == &m_integrator) {
            if (not m_integrator.y.empty()) {
                m_staticfunction.x[m_id] = m_integrator.y[0];
                parent_type::y[0] = m_integrator.y[0];
                in.emplace(&m_staticfunction);
            }
        } else if (out == &m_staticfunction) {
            if (not m_staticfunction.y.empty()) {
                m_integrator.x[0] = m_staticfunction.y[0];
                in.emplace(&m_integrator);
            }
        }
    }

    virtual std::size_t select(const std::vector <child_type *> &) const override
    {
        return 0u;
    }

    constexpr inline double value() const noexcept
    {
        return m_integrator.value();
    }

private:
    Integrator <Time> m_integrator;
    StaticFunction <Time, Container> m_staticfunction;
    std::size_t m_id;
};

template <typename Time, typename Container>
class Equation : public AtomicModel <Time, QssInputPort, QssOutputPort>
{
public:
    using parent_type = AtomicModel <Time, QssInputPort, QssOutputPort>;
    using time_format = typename parent_type::time_format;
    using time_type = typename parent_type::time_type;

    Equation(const Context &ctx,
             double dq,
             double epsilon,
             double x,
             std::size_t system_size,
             std::size_t id,
             Function <Time, Container> function)
        : parent_type(ctx, system_size, nan())
        , m_integrate_function(function)
        , m_variables(system_size)
        , m_sigma(Time::null())
        , m_time(Time::null())
        , m_dq(dq)
        , m_epsilon(epsilon)
        , m_X(x)
        , m_dX(0.0)
        , m_q(std::floor(x / dq) * dq)
        , m_id(id)
    {
        if (system_size == 0ul)
            throw std::invalid_argument(
                "vle::dsde::qss1::Equation: bad system size");

        if (id >= system_size)
            throw std::invalid_argument(
                "vle::dsde::qss1::Equation: bad id or system size");

        m_variables[m_id] = m_q;
    }

    virtual time_type ta() const
    {
        return m_sigma;
    }

    virtual void lambda() const override final
    {
        parent_type::y[0] = m_q + m_dq * boost::math::sign(m_dX);
    }

    virtual void internal()
    {
        m_time += m_sigma;
        m_X += (m_sigma * m_dX);

        if (m_dX > 0.0) {
            m_sigma = m_dq / m_dX;
            m_q = m_q + m_dq;
        } else if (m_dX < 0.0) {
            m_sigma = - m_dq / m_dX;
            m_q = m_q - m_dq;
        } else {
            m_sigma = Time::infinity();
        }
    }

    virtual void external(const time_type &e)
    {
        m_time += e;

        for (std::size_t i = 0, end = parent_type::x.size(); i != end; ++i)
            if (not std::isnan(parent_type::x[i]))
                m_variables[i] = parent_type::x[i];

        m_variables[m_id] = m_q + m_dq * boost::math::sign(m_dX);
        auto xv = m_integrate_function(m_variables, m_time);
        m_X += (e * m_dX);

        if (xv > 0.0) {
            m_sigma = (m_q + m_dq - m_X) / xv;
        } else if (xv < 0.0) {
            m_sigma = (m_q - m_epsilon - m_X) / xv;
        } else {
            m_sigma = Time::infinity();
        }

        m_dX = xv;
    }

    constexpr inline double value() const noexcept
    {
        return m_X;
    }

private:
    Function <Time, Container> m_integrate_function;
    Container m_variables;
    time_type m_sigma;
    time_type m_time;
    double m_dq;
    double m_epsilon;
    double m_X;
    double m_dX;
    double m_q;
    std::size_t m_id;
};

}
}
}

#endif


/**
 * @file LatticeModelFactory.cpp
 * @author Finn Lasse Buessen
 * @brief Create lattice representations from a lattice unit cell and specification of spin interactions.
 *
 * @copyright Copyright (c) 2020
 */

#include "LatticeModelFactory.hpp"
#include <algorithm>
#include <boost/filesystem.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/regex.hpp>
#include <boost/format.hpp>
#include "lib/Geometry.hpp"
#include "lib/InputParser.hpp"
#include "lib/Exception.hpp"
#include "lib/Log.hpp"

#define __EPSILON 0.00001
#define PI 3.14159265358979323846

namespace LatticeModelFactory
{
	#pragma region LatticeSite definition
	LatticeSite::LatticeSite()
	{
		a0 = 0;
		a1 = 0;
		a2 = 0;
		b = 0;
	}

	LatticeSite::LatticeSite(const int a0, const int a1, const int a2, const int b) : LatticeSite()
	{
		this->a0 = a0;
		this->a1 = a1;
		this->a2 = a2;
		this->b = b;
	}

	bool LatticeSite::operator==(const LatticeSite& rhs) const
	{
		if (a0 == rhs.a0 && a1 == rhs.a1 && a2 == rhs.a2 && b == rhs.b) return true;
		else return false;
	}

	bool LatticeSite::operator!=(const LatticeSite& rhs) const
	{
		return !operator==(rhs);
	}
	#pragma endregion

	#pragma region LatticeBond definition
	LatticeBond::LatticeBond(const int fromB, const int toB, const int da0, const int da1, const int da2)
	{
		this->fromB = fromB;
		this->toB = toB;
		this->da0 = da0;
		this->da1 = da1;
		this->da2 = da2;
	}

	bool LatticeBond::isAttachedToSite(const LatticeSite& site) const
	{
		if (fromB == site.b || toB == site.b) return true;
		else return false;
	}

	bool LatticeBond::isConnectingSites(const LatticeSite& site1, const LatticeSite& site2) const
	{
		if (isConnectingFromTo(site1, site2)) return true;
		else if (isConnectingFromTo(site2, site1)) return true;
		else return false;
	}

	bool LatticeBond::isConnectingFromTo(const LatticeSite& siteFrom, const LatticeSite& siteTo) const
	{
		if (da0 == (siteTo.a0 - siteFrom.a0) && da1 == (siteTo.a1 - siteFrom.a1) && da2 == (siteTo.a2 - siteFrom.a2) && fromB == siteFrom.b && toB == siteTo.b) return true;
		else return false;
	}

	std::vector<LatticeSite> LatticeBond::getOtherEnd(const LatticeSite& site) const
	{
		if (isAttachedToSite(site))
		{
			if (fromB == site.b)
			{
				if (fromB == toB) return std::vector<LatticeSite>({ LatticeSite(site.a0 + da0, site.a1 + da1, site.a2 + da2, toB), LatticeSite(site.a0 - da0, site.a1 - da1, site.a2 - da2, fromB) });
				else return std::vector<LatticeSite>({ LatticeSite(site.a0 + da0, site.a1 + da1, site.a2 + da2, toB) });
			}
			else return std::vector<LatticeSite>({ LatticeSite(site.a0 - da0, site.a1 - da1, site.a2 - da2, fromB) });
		}
		else throw Exception(Exception::Type::ArgumentError, "Lattice bond is not attached to specified site");
	}
	#pragma endregion

	#pragma region LatticeUniteCell definition
	LatticeUnitCell::LatticeUnitCell() {}

	LatticeUnitCell::LatticeUnitCell(const std::string &latticeName, const std::string &bundle)
	{
		if (!_initFromResBundle(latticeName, bundle)) throw Exception(Exception::Type::InitializationError, "Invalid lattice unit cell definition.");
	}

	bool LatticeUnitCell::_initFromResBundle(const std::string &latticeName, const std::string &bundle)
	{
		boost::filesystem::directory_iterator end;
		for (boost::filesystem::directory_iterator it(bundle); it != end; ++it)
		{
			if (boost::filesystem::is_regular_file(it->path()) && it->path().extension() == ".xml")
			{
				boost::property_tree::ptree latticeDefinition;
				boost::property_tree::xml_parser::read_xml(it->path().string(), latticeDefinition);

				for (auto u : latticeDefinition)
				{
					if (u.first != "unitcell") continue;
					auto uc = u.second;
					if (!uc.get_optional<std::string>("<xmlattr>.name")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell. Unit cell name is undefined.");

					if (uc.get<std::string>("<xmlattr>.name") == latticeName)
					{
						//read unit cell
						if (uc.count("primitive") != 3) throw Exception(Exception::Type::InitializationError, "Invalid unit cell. Unit cell must define three primitive vectors.");
						if (uc.count("site") == 0) throw Exception(Exception::Type::InitializationError, "Invalid unit cell. Unit cell must contain at least one lattice site.");
						if (uc.count("bond") == 0) throw Exception(Exception::Type::InitializationError, "Invalid unit cell. Unit cell must contain at least one lattice bond.");

						for (auto p : uc)
						{
							//lattice vector
							if (p.first == "primitive")
							{
								if (!p.second.get_optional<std::string>("<xmlattr>.x")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell primitive. x attribute missing.");
								if (!p.second.get_optional<std::string>("<xmlattr>.y")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell primitive. y attribute missing.");
								if (!p.second.get_optional<std::string>("<xmlattr>.z")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell primitive. z attribute missing.");

								latticeVectors.push_back(geometry::Vec3<double>(
									InputParser::stringToDouble(p.second.get<std::string>("<xmlattr>.x")),
									InputParser::stringToDouble(p.second.get<std::string>("<xmlattr>.y")),
									InputParser::stringToDouble(p.second.get<std::string>("<xmlattr>.z"))
									));
							}
							//basis site
							else if (p.first == "site")
							{
								if (!p.second.get_optional<std::string>("<xmlattr>.x")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell site. x attribute missing.");
								if (!p.second.get_optional<std::string>("<xmlattr>.y")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell site. y attribute missing.");
								if (!p.second.get_optional<std::string>("<xmlattr>.z")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell site. z attribute missing.");

								basisSites.push_back(geometry::Vec3<double>(
									InputParser::stringToDouble(p.second.get<std::string>("<xmlattr>.x")),
									InputParser::stringToDouble(p.second.get<std::string>("<xmlattr>.y")),
									InputParser::stringToDouble(p.second.get<std::string>("<xmlattr>.z"))
									));
							}
							//bond
							else if (p.first == "bond")
							{
								if (!p.second.get_optional<std::string>("<xmlattr>.from")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell bond. 'from' attribute missing. ");
								if (!p.second.get_optional<std::string>("<xmlattr>.to")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell bond. 'to' attribute missing. ");
								if (!p.second.get_optional<std::string>("<xmlattr>.da0")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell bond. 'da0' attribute missing. ");
								if (!p.second.get_optional<std::string>("<xmlattr>.da1")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell bond. 'da1' attribute missing. ");
								if (!p.second.get_optional<std::string>("<xmlattr>.da2")) throw Exception(Exception::Type::InitializationError, "Invalid unit cell bond. 'da2' attribute missing. ");

								int from = std::stoi(p.second.get<std::string>("<xmlattr>.from"));
								int to = std::stoi(p.second.get<std::string>("<xmlattr>.to"));
								int da0 = std::stoi(p.second.get<std::string>("<xmlattr>.da0"));
								int da1 = std::stoi(p.second.get<std::string>("<xmlattr>.da1"));
								int da2 = std::stoi(p.second.get<std::string>("<xmlattr>.da2"));
								this->latticeBonds.push_back(LatticeBond(from, to, da0, da1, da2));
							}
						}
						return true;
					}
				}
			}
		}
		return false;
	}
	#pragma endregion

	#pragma region SpinInteraction definition
	SpinInteraction::SpinInteraction()
	{
		memset(&interactionStrength[0][0], 0, 9 * sizeof(float));
	};

	SpinInteraction::SpinInteraction(const LatticeSite& from, const LatticeSite& to) : from(from), to(to)
	{
		memset(&interactionStrength[0][0], 0, 9 * sizeof(float));
	};

	bool SpinInteraction::isConnectingFromTo(const LatticeSite& siteFrom, const LatticeSite& siteTo) const
	{
		if ((to.a0 - from.a0) == (siteTo.a0 - siteFrom.a0) && (to.a1 - from.a1) == (siteTo.a1 - siteFrom.a1) && (to.a2 - from.a2) == (siteTo.a2 - siteFrom.a2) && from.b == siteFrom.b && to.b == siteTo.b) return true;
		else return false;
	}

	int SpinInteraction::isConnectingSites(const LatticeSite& site1, const LatticeSite& site2) const
	{
		if (isConnectingFromTo(site1, site2)) return 1;
		else if (isConnectingFromTo(site2, site1)) return -1;
		else return 0;
	}

	SpinInteraction& SpinInteraction::operator+=(const SpinInteraction& rhs)
	{
		if (from == rhs.from && to == rhs.to)
		{
			for (int s1 = 0; s1 < 3; ++s1)
			{
				for (int s2 = 0; s2 < 3; ++s2)
				{
					interactionStrength[s1][s2] += rhs.interactionStrength[s1][s2];
				}
			}
		}
		else if (from == rhs.to && to == rhs.from)
		{
			for (int s1 = 0; s1 < 3; ++s1)
			{
				for (int s2 = 0; s2 < 3; ++s2)
				{
					interactionStrength[s1][s2] += rhs.interactionStrength[s2][s1];
				}
			}
		}
		else throw Exception(Exception::Type::ArgumentError, "Cannnot combine incompatible spin interactions.");

		return *this;
	}
	#pragma endregion

	#pragma region SpinModelUnitCell definition
	SpinModelUnitCell::SpinModelUnitCell() {}

	SpinModelUnitCell::SpinModelUnitCell(const std::string &modelName, const std::string &bundle, const std::map<std::string, std::string> &modelOptions)
	{
		if (!_initFromResBundle(modelName, bundle, modelOptions)) throw Exception(Exception::Type::InitializationError, "Invalid spin model definition.");
	}

	bool SpinModelUnitCell::_initFromResBundle(const std::string &res, const std::string &bundle, const std::map<std::string, std::string> &modelOptions)
	{
		//read spin model
		boost::filesystem::directory_iterator end;
		for (boost::filesystem::directory_iterator it(bundle); it != end; ++it)
		{
			if (boost::filesystem::is_regular_file(it->path()) && it->path().extension() == ".xml")
			{
				boost::property_tree::ptree modelDefinition;
				boost::property_tree::xml_parser::read_xml(it->path().string(), modelDefinition);

				for (auto m : modelDefinition)
				{
					if (m.first != "model") continue;
					auto model = m.second;
					if (!model.get_optional<std::string>("<xmlattr>.name")) throw Exception(Exception::Type::InitializationError, "Invalid spin model. Model name is undefined.");

					if (model.get<std::string>("<xmlattr>.name") == res)
					{
						//read model
						if (model.count("interaction") == 0) throw Exception(Exception::Type::InitializationError, "Invalid spin model. Spin model must define at least one interaction.");

						for (auto i : model)
						{
							if (i.first != "interaction") continue;

							if (!i.second.get_optional<std::string>("<xmlattr>.parameter")) throw Exception(Exception::Type::InitializationError, "Invalid interaction. Parameter name not specified. ");
							if (!i.second.get_optional<std::string>("<xmlattr>.type")) throw Exception(Exception::Type::InitializationError, "Invalid interaction. Interaction type not specified. ");
							if (!i.second.get_optional<std::string>("<xmlattr>.from")) throw Exception(Exception::Type::InitializationError, "Invalid interaction. No target sites specified. ");
							if (!i.second.get_optional<std::string>("<xmlattr>.to")) throw Exception(Exception::Type::InitializationError, "Invalid interaction. No target sites specified. ");

							std::string parameter = i.second.get<std::string>("<xmlattr>.parameter");
							std::string from = i.second.get<std::string>("<xmlattr>.from");
							std::string to = i.second.get<std::string>("<xmlattr>.to");
							std::string type = i.second.get<std::string>("<xmlattr>.type");

							//parse lattice sites
							boost::regex pattern("(-{0,1}\\d+)[, ](-{0,1}\\d+)[, ](-{0,1}\\d+)[, ](\\d+)");
							boost::smatch matchSite1;
							boost::smatch matchSite2;
							if (!boost::regex_match(from, matchSite1, pattern)) throw Exception(Exception::Type::InitializationError, "Invalid spin model. Site '" + from + "' is ill-defined. ");
							if (!boost::regex_match(to, matchSite2, pattern)) throw Exception(Exception::Type::InitializationError, "Invalid spin model. Site '" + to + "' is ill-defined. ");

							//parse coupling strength
							float interactionStrength;
							if (modelOptions.count(parameter)) interactionStrength = InputParser::stringToFloat(modelOptions.at(parameter));
							else throw Exception(Exception::Type::InitializationError, "Interaction parameter '" + parameter + "' is not defined in the taskfile");

							//generate interaction
							SpinInteraction interaction(LatticeSite(std::stoi(matchSite1[1].str()), std::stoi(matchSite1[2].str()), std::stoi(matchSite1[3].str()), std::stoi(matchSite1[4].str())), LatticeSite(std::stoi(matchSite2[1].str()), std::stoi(matchSite2[2].str()), std::stoi(matchSite2[3].str()), std::stoi(matchSite2[4].str())));
							if (type == "heisenberg")
							{
								interaction.interactionStrength[0][0] += interactionStrength;
								interaction.interactionStrength[1][1] += interactionStrength;
								interaction.interactionStrength[2][2] += interactionStrength;
							}
							else if (type == "xxyy")
							{
								interaction.interactionStrength[0][0] += interactionStrength;
								interaction.interactionStrength[1][1] += interactionStrength;
							}
							else if (type == "gx")
							{
								interaction.interactionStrength[1][2] += interactionStrength;
								interaction.interactionStrength[2][1] += interactionStrength;
							}
							else if (type == "gy")
							{
								interaction.interactionStrength[2][0] += interactionStrength;
								interaction.interactionStrength[0][2] += interactionStrength;
							}
							else if (type == "gz")
							{
								interaction.interactionStrength[0][1] += interactionStrength;
								interaction.interactionStrength[1][0] += interactionStrength;
							}
							else
							{
								boost::regex pattern("(-?)([xyz])([xyz])");
								boost::smatch match;
								if (boost::regex_match(type, match, pattern))
								{
									float sign = (match[1] == "-") ? -1.0f : 1.0f;
									int s1 = 0;
									int s2 = 0;
									if (match[2] == "x") s1 = 0;
									else if (match[2] == "y") s1 = 1;
									else if (match[2] == "z") s1 = 2;
									if (match[3] == "x") s2 = 0;
									else if (match[3] == "y") s2 = 1;
									else if (match[3] == "z") s2 = 2;

									interaction.interactionStrength[s1][s2] += sign * interactionStrength;
								}
								else throw Exception(Exception::Type::InitializationError, "Invalid spin model. Unknown interaction type '" + type + "'.");
							}

							//add interaction to spin model
							auto addInteraction = [&](const SpinInteraction &i)->void
							{
								for (auto j = interactions.begin(); j != interactions.end(); ++j)
								{
									if (j->isConnectingSites(i.from, i.to) != 0)
									{
										(*j) += i;
										return;
									}
								}
								interactions.push_back(i);
							};
							addInteraction(interaction);

							//add required interaction parameter
							interactionParameters.insert(parameter);
						}
						return true;
					}
				}
			}
		}
		return false;
	};
	#pragma endregion

	#pragma region private interface
	struct SpinPermutation
	{
	public:
		SpinPermutation()
		{
			transformedComponent[0] = SpinComponent::X;
			transformedComponent[1] = SpinComponent::Y;
			transformedComponent[2] = SpinComponent::Z;
		}

		SpinPermutation(SpinComponent transformedX, SpinComponent transformedY, SpinComponent transformedZ)
		{
			transformedComponent[0] = transformedX;
			transformedComponent[1] = transformedY;
			transformedComponent[2] = transformedZ;
		}

		SpinPermutation(const int n)
		{
			switch (n)
			{
			case 0:
				transformedComponent[0] = SpinComponent::X;
				transformedComponent[1] = SpinComponent::Y;
				transformedComponent[2] = SpinComponent::Z;
				break;
			case 1:
				transformedComponent[0] = SpinComponent::MinusX;
				transformedComponent[1] = SpinComponent::Y;
				transformedComponent[2] = SpinComponent::Z;
				break;
			case 2:
				transformedComponent[0] = SpinComponent::X;
				transformedComponent[1] = SpinComponent::MinusY;
				transformedComponent[2] = SpinComponent::Z;
				break;
			case 3:
				transformedComponent[0] = SpinComponent::X;
				transformedComponent[1] = SpinComponent::Y;
				transformedComponent[2] = SpinComponent::MinusZ;
				break;
			case 4:
				transformedComponent[0] = SpinComponent::MinusX;
				transformedComponent[1] = SpinComponent::MinusY;
				transformedComponent[2] = SpinComponent::Z;
				break;
			case 5:
				transformedComponent[0] = SpinComponent::MinusX;
				transformedComponent[1] = SpinComponent::Y;
				transformedComponent[2] = SpinComponent::MinusZ;
				break;
			case 6:
				transformedComponent[0] = SpinComponent::X;
				transformedComponent[1] = SpinComponent::MinusY;
				transformedComponent[2] = SpinComponent::MinusZ;
				break;
			case 7:
				transformedComponent[0] = SpinComponent::MinusX;
				transformedComponent[1] = SpinComponent::MinusY;
				transformedComponent[2] = SpinComponent::MinusZ;
				break;
			case 8:
				transformedComponent[0] = SpinComponent::X;
				transformedComponent[1] = SpinComponent::Z;
				transformedComponent[2] = SpinComponent::Y;
				break;
			case 9:
				transformedComponent[0] = SpinComponent::MinusX;
				transformedComponent[1] = SpinComponent::Z;
				transformedComponent[2] = SpinComponent::Y;
				break;
			case 10:
				transformedComponent[0] = SpinComponent::X;
				transformedComponent[1] = SpinComponent::MinusZ;
				transformedComponent[2] = SpinComponent::Y;
				break;
			case 11:
				transformedComponent[0] = SpinComponent::X;
				transformedComponent[1] = SpinComponent::Z;
				transformedComponent[2] = SpinComponent::MinusY;
				break;
			case 12:
				transformedComponent[0] = SpinComponent::MinusX;
				transformedComponent[1] = SpinComponent::MinusZ;
				transformedComponent[2] = SpinComponent::Y;
				break;
			case 13:
				transformedComponent[0] = SpinComponent::MinusX;
				transformedComponent[1] = SpinComponent::Z;
				transformedComponent[2] = SpinComponent::MinusY;
				break;
			case 14:
				transformedComponent[0] = SpinComponent::X;
				transformedComponent[1] = SpinComponent::MinusZ;
				transformedComponent[2] = SpinComponent::MinusY;
				break;
			case 15:
				transformedComponent[0] = SpinComponent::MinusX;
				transformedComponent[1] = SpinComponent::MinusZ;
				transformedComponent[2] = SpinComponent::MinusY;
				break;
			case 16:
				transformedComponent[0] = SpinComponent::Y;
				transformedComponent[1] = SpinComponent::X;
				transformedComponent[2] = SpinComponent::Z;
				break;
			case 17:
				transformedComponent[0] = SpinComponent::MinusY;
				transformedComponent[1] = SpinComponent::X;
				transformedComponent[2] = SpinComponent::Z;
				break;
			case 18:
				transformedComponent[0] = SpinComponent::Y;
				transformedComponent[1] = SpinComponent::MinusX;
				transformedComponent[2] = SpinComponent::Z;
				break;
			case 19:
				transformedComponent[0] = SpinComponent::Y;
				transformedComponent[1] = SpinComponent::X;
				transformedComponent[2] = SpinComponent::MinusZ;
				break;
			case 20:
				transformedComponent[0] = SpinComponent::MinusY;
				transformedComponent[1] = SpinComponent::MinusX;
				transformedComponent[2] = SpinComponent::Z;
				break;
			case 21:
				transformedComponent[0] = SpinComponent::MinusY;
				transformedComponent[1] = SpinComponent::X;
				transformedComponent[2] = SpinComponent::MinusZ;
				break;
			case 22:
				transformedComponent[0] = SpinComponent::Y;
				transformedComponent[1] = SpinComponent::MinusX;
				transformedComponent[2] = SpinComponent::MinusZ;
				break;
			case 23:
				transformedComponent[0] = SpinComponent::MinusY;
				transformedComponent[1] = SpinComponent::MinusX;
				transformedComponent[2] = SpinComponent::MinusZ;
				break;
			case 24:
				transformedComponent[0] = SpinComponent::Y;
				transformedComponent[1] = SpinComponent::Z;
				transformedComponent[2] = SpinComponent::X;
				break;
			case 25:
				transformedComponent[0] = SpinComponent::MinusY;
				transformedComponent[1] = SpinComponent::Z;
				transformedComponent[2] = SpinComponent::X;
				break;
			case 26:
				transformedComponent[0] = SpinComponent::Y;
				transformedComponent[1] = SpinComponent::MinusZ;
				transformedComponent[2] = SpinComponent::X;
				break;
			case 27:
				transformedComponent[0] = SpinComponent::Y;
				transformedComponent[1] = SpinComponent::Z;
				transformedComponent[2] = SpinComponent::MinusX;
				break;
			case 28:
				transformedComponent[0] = SpinComponent::MinusY;
				transformedComponent[1] = SpinComponent::MinusZ;
				transformedComponent[2] = SpinComponent::X;
				break;
			case 29:
				transformedComponent[0] = SpinComponent::MinusY;
				transformedComponent[1] = SpinComponent::Z;
				transformedComponent[2] = SpinComponent::MinusX;
				break;
			case 30:
				transformedComponent[0] = SpinComponent::Y;
				transformedComponent[1] = SpinComponent::MinusZ;
				transformedComponent[2] = SpinComponent::MinusX;
				break;
			case 31:
				transformedComponent[0] = SpinComponent::MinusY;
				transformedComponent[1] = SpinComponent::MinusZ;
				transformedComponent[2] = SpinComponent::MinusX;
				break;
			case 32:
				transformedComponent[0] = SpinComponent::Z;
				transformedComponent[1] = SpinComponent::X;
				transformedComponent[2] = SpinComponent::Y;
				break;
			case 33:
				transformedComponent[0] = SpinComponent::MinusZ;
				transformedComponent[1] = SpinComponent::X;
				transformedComponent[2] = SpinComponent::Y;
				break;
			case 34:
				transformedComponent[0] = SpinComponent::Z;
				transformedComponent[1] = SpinComponent::MinusX;
				transformedComponent[2] = SpinComponent::Y;
				break;
			case 35:
				transformedComponent[0] = SpinComponent::Z;
				transformedComponent[1] = SpinComponent::X;
				transformedComponent[2] = SpinComponent::MinusY;
				break;
			case 36:
				transformedComponent[0] = SpinComponent::MinusZ;
				transformedComponent[1] = SpinComponent::MinusX;
				transformedComponent[2] = SpinComponent::Y;
				break;
			case 37:
				transformedComponent[0] = SpinComponent::MinusZ;
				transformedComponent[1] = SpinComponent::X;
				transformedComponent[2] = SpinComponent::MinusY;
				break;
			case 38:
				transformedComponent[0] = SpinComponent::Z;
				transformedComponent[1] = SpinComponent::MinusX;
				transformedComponent[2] = SpinComponent::MinusY;
				break;
			case 39:
				transformedComponent[0] = SpinComponent::MinusZ;
				transformedComponent[1] = SpinComponent::MinusX;
				transformedComponent[2] = SpinComponent::MinusY;
				break;
			case 40:
				transformedComponent[0] = SpinComponent::Z;
				transformedComponent[1] = SpinComponent::Y;
				transformedComponent[2] = SpinComponent::X;
				break;
			case 41:
				transformedComponent[0] = SpinComponent::MinusZ;
				transformedComponent[1] = SpinComponent::Y;
				transformedComponent[2] = SpinComponent::X;
				break;
			case 42:
				transformedComponent[0] = SpinComponent::Z;
				transformedComponent[1] = SpinComponent::MinusY;
				transformedComponent[2] = SpinComponent::X;
				break;
			case 43:
				transformedComponent[0] = SpinComponent::Z;
				transformedComponent[1] = SpinComponent::Y;
				transformedComponent[2] = SpinComponent::MinusX;
				break;
			case 44:
				transformedComponent[0] = SpinComponent::MinusZ;
				transformedComponent[1] = SpinComponent::MinusY;
				transformedComponent[2] = SpinComponent::X;
				break;
			case 45:
				transformedComponent[0] = SpinComponent::MinusZ;
				transformedComponent[1] = SpinComponent::Y;
				transformedComponent[2] = SpinComponent::MinusX;
				break;
			case 46:
				transformedComponent[0] = SpinComponent::Z;
				transformedComponent[1] = SpinComponent::MinusY;
				transformedComponent[2] = SpinComponent::MinusX;
				break;
			case 47:
				transformedComponent[0] = SpinComponent::MinusZ;
				transformedComponent[1] = SpinComponent::MinusY;
				transformedComponent[2] = SpinComponent::MinusX;
				break;
			default:
				throw Exception(Exception::Type::ArgumentError, "Specified spin permutation does not exist");
				break;
			}
		}

		static SpinPermutation identity()
		{
			return SpinPermutation(SpinComponent::X, SpinComponent::Y, SpinComponent::Z);
		}

		static SpinPermutation inverse(const SpinPermutation& rhs)
		{
			SpinPermutation p;
			if (rhs.transformedComponent[0] == SpinComponent::X) p.transformedComponent[0] = SpinComponent::X;
			else if (rhs.transformedComponent[0] == SpinComponent::Y) p.transformedComponent[1] = SpinComponent::X;
			else if (rhs.transformedComponent[0] == SpinComponent::Z) p.transformedComponent[2] = SpinComponent::X;

			if (rhs.transformedComponent[1] == SpinComponent::X) p.transformedComponent[0] = SpinComponent::Y;
			else if (rhs.transformedComponent[1] == SpinComponent::Y) p.transformedComponent[1] = SpinComponent::Y;
			else if (rhs.transformedComponent[1] == SpinComponent::Z) p.transformedComponent[2] = SpinComponent::Y;

			if (rhs.transformedComponent[2] == SpinComponent::X) p.transformedComponent[0] = SpinComponent::Z;
			else if (rhs.transformedComponent[2] == SpinComponent::Y) p.transformedComponent[1] = SpinComponent::Z;
			else if (rhs.transformedComponent[2] == SpinComponent::Z) p.transformedComponent[2] = SpinComponent::Z;

			return p;
		}

		bool operator==(const SpinPermutation& rhs) const
		{
			return transformedComponent[0] == rhs.transformedComponent[0] && transformedComponent[1] == rhs.transformedComponent[1] && transformedComponent[2] == rhs.transformedComponent[2];
		}

		SpinComponent transformedComponent[3];
	};

	//return a list of all nearest neighbors of given lattice site
	std::vector<LatticeSite> getNeighbors(const LatticeUnitCell& uc, const LatticeSite& site)
	{
		std::vector<LatticeSite> neighbors;

		for (auto bond : uc.latticeBonds)
		{
			if (bond.isAttachedToSite(site))
			{
				std::vector<LatticeSite> newNeighbors = bond.getOtherEnd(site);
				neighbors.insert(neighbors.end(), newNeighbors.begin(), newNeighbors.end());
			}
		}
		return neighbors;
	}

	//return a list of all neighbors of given lattice site within a specified range
	std::vector<LatticeSite> constructRangeAroundSite(const LatticeUnitCell& uc, const LatticeSite& site, int range)
	{
		std::vector<LatticeSite> sites({ site });
		for (int i = 0; i < range; ++i)
		{
			//for each site of the set, add all neighbors
			std::vector<LatticeSite> neighbors;
			for (auto site : sites)
			{
				std::vector<LatticeSite> ns = getNeighbors(uc, site);
				for (auto n : ns) if (std::find(neighbors.begin(), neighbors.end(), n) == neighbors.end()) neighbors.push_back(n);
			}
			for (auto n : neighbors) if (std::find(sites.begin(), sites.end(), n) == sites.end()) sites.push_back(n);
		}
		return sites;
	}

	//return the coordinates of given lattice site
	geometry::Vec3<double> getSitePosition(const LatticeUnitCell& uc, const LatticeSite& site)
	{
		return double(site.a0) * uc.latticeVectors[0] + double(site.a1) * uc.latticeVectors[1] + double(site.a2) * uc.latticeVectors[2] + uc.basisSites[site.b];
	}

	//return true and write result to LatticeSite &site if a lattice site exists at the specified coordinates, otherwise return false
	bool siteAtPosition(const LatticeUnitCell& uc, const geometry::Vec3<double>& position, LatticeSite& site)
	{
		for (int b = 0; b < int(uc.basisSites.size()); ++b)
		{
			geometry::Vec3<double> n = geometry::Mat3<double>(uc.latticeVectors[0], uc.latticeVectors[1], uc.latticeVectors[2]).inverse() * (position - uc.basisSites[b]);
			if (fabs(n.x - std::round(n.x)) < __EPSILON && fabs(n.y - std::round(n.y)) < __EPSILON && fabs(n.z - std::round(n.z)) < __EPSILON)
			{
				site.a0 = std::lround(n.x);
				site.a1 = std::lround(n.y);
				site.a2 = std::lround(n.z);
				site.b = b;
				return true;
			}
		}
		return false;
	};

	//searches lattice automorphisms f_i that map site2 onto (0,0,0,0). Return f_i(site1). If reducedSearch==true, return after the first f_i has been found.
	std::vector<std::pair<LatticeSite, SpinPermutation>> symmetryReduce(const LatticeUnitCell& uc, const SpinModelUnitCell& spinModel, const LatticeSite& site1, const LatticeSite& site2, bool reducedSearch = false)
	{
		//define reference site ref
		LatticeSite ref = LatticeSite(0, 0, 0, 0);

		//collect minimal set of lattice sites which needs to be considered in order to verify symmetry transformations
		std::vector<LatticeSite> preImageSites;
		for (unsigned int b = 0; b < uc.basisSites.size(); ++b)
		{
			LatticeSite site(0, 0, 0, b);
			if (std::find(preImageSites.begin(), preImageSites.end(), site) == preImageSites.end()) preImageSites.push_back(site);
		}
		for (auto bond : uc.latticeBonds)
		{
			LatticeSite site(bond.da0, bond.da1, bond.da2, bond.toB);
			if (std::find(preImageSites.begin(), preImageSites.end(), site) == preImageSites.end()) preImageSites.push_back(site);
		}

		//collect minimal set of spin interactions which need to be considered in order to verify symmetry transformations
		std::vector<SpinInteraction> preImageInteractions = spinModel.interactions; //TODO select only those that are within neighborhood[site2.b]

		//choose two neighbors (n1, n2) of site2 such that the sites are non-collinear
		LatticeSite n1, n2;
		std::vector<LatticeSite> neighbors = getNeighbors(uc, site2);
		if (neighbors.size() < 3) throw Exception(Exception::Type::InitializationError, "Could not build lattice. Too few neighbors. ");
		n1 = neighbors[0];
		for (unsigned int i = 1; i < neighbors.size(); ++i)
		{
			if (geometry::cross(getSitePosition(uc, n1) - getSitePosition(uc, site2), getSitePosition(uc, neighbors[i]) - getSitePosition(uc, site2)).norm() > __EPSILON)
			{
				n2 = neighbors[i];
				break;
			}
			if (i == neighbors.size() - 1) throw Exception(Exception::Type::InitializationError, "Could not build lattice. Unfavorable lattice geometry. ");
		}

		//container to store all valid transformations f_i
		std::vector<std::pair<geometry::Mat4<double>, SpinPermutation>> validTransformations;

		//try to match (site2, n1, n2) with any combination of neighbors (ref, c1, c2) where c1 and c2 are neighbors of ref
		std::vector<LatticeSite> refNeighbors = getNeighbors(uc, ref);
		for (auto c1 : refNeighbors)
		{
			for (auto c2 : refNeighbors)
			{
				//skip if (c1, c2, ref) collinear
				if (cross(getSitePosition(uc, c1) - getSitePosition(uc, ref), getSitePosition(uc, c2) - getSitePosition(uc, ref)).norm() < __EPSILON) continue;

				for (int inversion = 0; inversion <= 1; ++inversion)
				{
					if (reducedSearch && validTransformations.size() > 0) break;

					//init transformation
					geometry::Mat4<double> transformation = (inversion) ? geometry::Mat4<double>::inversion() : geometry::Mat4<double>::identity();
					SpinPermutation spinTransformation;

					//move site2 to ref
					transformation = geometry::Mat4<double>::translation(getSitePosition(uc, ref) - transformation * getSitePosition(uc, site2)) * transformation;

					//rotate n1 onto c1
					geometry::Vec3<double> a = (transformation * getSitePosition(uc, n1) - getSitePosition(uc, ref)).normalize();
					geometry::Vec3<double> b = (getSitePosition(uc, c1) - getSitePosition(uc, ref)).normalize();
					geometry::Vec3<double> axis = cross(a, b);
					double angle = acos(dot(a, b));
					if (axis.norm() < __EPSILON)
					{
						//a and b are already antiparallel or parallel. If they are antiparallel, rotate them. Otherwise do nothing.
						if (dot(a, b) < 0)
						{
							axis = (a.x != 0) ? geometry::Vec3<double>(-a.y, a.x, 0) : geometry::Vec3<double>(0, -a.z, a.y);
							angle = PI;
						}
						else
						{
							axis = geometry::Vec3<double>(1, 0, 0);
							angle = 0;
						}
					}
					transformation = geometry::Mat4<double>::rotation(axis, getSitePosition(uc, ref), angle) * transformation;

					//try to rotate n2 onto c2
					geometry::Vec3<double> x = (transformation * getSitePosition(uc, n2) - getSitePosition(uc, ref) - dot(transformation * getSitePosition(uc, n2) - getSitePosition(uc, ref), (getSitePosition(uc, c1) - getSitePosition(uc, ref)).normalize()) * (getSitePosition(uc, c1) - getSitePosition(uc, ref)).normalize()).normalize();
					geometry::Vec3<double> y = (getSitePosition(uc, c2) - getSitePosition(uc, ref) - dot(getSitePosition(uc, c2) - getSitePosition(uc, ref), (getSitePosition(uc, c1) - getSitePosition(uc, ref)).normalize()) * (getSitePosition(uc, c1) - getSitePosition(uc, ref)).normalize()).normalize();
					axis = cross(x, y);
					angle = acos(dot(x, y));
					if (axis.norm() < __EPSILON)
					{
						axis = b;
						angle = (dot(x, y) < 0) ? PI : 0.0;
					}
					transformation = geometry::Mat4<double>::rotation(axis, getSitePosition(uc, ref), angle) * transformation;

					//perform sanity checks on the basic transformation properties
					//T(site2) should match ref
					double e = (transformation * getSitePosition(uc, site2) - getSitePosition(uc, ref)).norm();
					if (e > __EPSILON) throw Exception(Exception::Type::InternalError, "Lattice symmetry calculation has failed. (Deviation of symmetry transformed site [s2] from target [ref] is " + std::to_string(e) + ", should be zero)");

					//T(n1) should match of c1
					e = (transformation * getSitePosition(uc, n1) - getSitePosition(uc, c1)).norm();
					if (e > __EPSILON) throw Exception(Exception::Type::InternalError, "Lattice symmetry calculation has failed. (Deviation of symmetry transformed site [n1] from target [c1] is " + std::to_string(e) + ", should be zero)");

					//T(n2) should be coplanar with c1-ref and c2-ref
					e = dot(transformation * getSitePosition(uc, n2) - getSitePosition(uc, ref), cross(getSitePosition(uc, c1) - getSitePosition(uc, ref), getSitePosition(uc, c2) - getSitePosition(uc, ref)));
					if (fabs(e) > __EPSILON) throw Exception(Exception::Type::InternalError, "Lattice symmetry calculation has failed. (Symmetry transformed site [n2] is not coplanar with [c1-ref] and [c2-ref]. Deviation is " + std::to_string(e) + ", should be zero)");

					//check if the transformation is valid
					bool validTransformation = true;

					//check if lattice sites are invariant under transformation
					LatticeSite imageSite;
					for (auto p : preImageSites)
					{
						if (!siteAtPosition(uc, transformation * getSitePosition(uc, p), imageSite))
						{
							validTransformation = false;
							break;
						}
					}
					if (!validTransformation) continue;

					//check if spin couplings are invariant under transformation up to a global permutation of spin components
					bool spinTransformationExists = false;
					for (int i = 0; i < 48; ++i)  // Extend this to all 48 permutations.
					{
						SpinPermutation permutation(i);
						bool validPermutation = true;

						//check whether permutation is compatible with all interactions in the model
						for (auto interaction : preImageInteractions)
						{
							//determine transformed interaction
							LatticeSite fromTransformed;
							siteAtPosition(uc, transformation * getSitePosition(uc, interaction.from), fromTransformed);
							LatticeSite toTransformed;
							siteAtPosition(uc, transformation * getSitePosition(uc, interaction.to), toTransformed);

							float targetInteractionStrength[3][3];
							for (int s1 = 0; s1 < 3; ++s1)
							{
								for (int s2 = 0; s2 < 3; ++s2)
								{
									targetInteractionStrength[s1][s2] = 0.0f;
								}
							}

							for (auto i : spinModel.interactions)
							{
								int connection = i.isConnectingSites(fromTransformed, toTransformed);

								if (connection == 1)
								{
									for (int s1 = 0; s1 < 3; ++s1)
									{
										for (int s2 = 0; s2 < 3; ++s2)
										{
											targetInteractionStrength[s1][s2] = i.interactionStrength[s1][s2];
										}
									}
								}
								else if (connection == -1)
								{
									for (int s1 = 0; s1 < 3; ++s1)
									{
										for (int s2 = 0; s2 < 3; ++s2)
										{
											targetInteractionStrength[s1][s2] = i.interactionStrength[s2][s1];
										}
									}
								}
							}

							//verify transformation
							for (int s1 = 0; s1 < 3; ++s1)
							{
								for (int s2 = 0; s2 < 3; ++s2)
								{

									SpinComponent transformedComponent1 = permutation.transformedComponent[s1];
									SpinComponent transformedComponent2 = permutation.transformedComponent[s2];
									// Spincomponets  can be X,Y,Z ,and MinusX,MinusY, MinusZ.

									// Now we need to perform the mapping from MinusX->X,etc.

								 	float sign =1.0f;
									if(transformedComponent1 == SpinComponent::MinusX)
									{
										transformedComponent1  = SpinComponent::X;
										sign =-sign;
									}
									if(transformedComponent1  == SpinComponent::MinusY)
									{
										transformedComponent1  = SpinComponent::Y;
										sign=-sign;
									}
									if(transformedComponent1  == SpinComponent::MinusZ)
									{
										transformedComponent1  == SpinComponent::Z;
										sign = -sign;
									}

									if(transformedComponent2 == SpinComponent::MinusX)
									{
										transformedComponent2 = SpinComponent::X;
										sign =-sign;
									}
									if(transformedComponent2 == SpinComponent::MinusY)
									{
										transformedComponent2 = SpinComponent::Y;
										sign=-sign;
									}
									if(transformedComponent2 == SpinComponent::MinusZ)
									{
										transformedComponent2 = SpinComponent::Z;
										sign = -sign;
									}
								// here the transformed components must be X,Y,Z

									if (interaction.interactionStrength[s1][s2] != sign*targetInteractionStrength[static_cast<int>(transformedComponent1)][static_cast<int>(transformedComponent2)]) validPermutation = false;
									//if (interaction.interactionStrength[s1][s2] != targetInteractionStrength[static_cast<int>(permutation.transformedComponent[s1])][static_cast<int>(permutation.transformedComponent[s2])]) validPermutation = false;
								}
							}
						}

						if (validPermutation)
						{
							spinTransformationExists = true;
							spinTransformation = permutation;
							break;
						}
					}
					if (!spinTransformationExists) validTransformation = false;

					if (!validTransformation) continue;
					else validTransformations.push_back(std::pair<geometry::Mat4<double>, SpinPermutation>(transformation, spinTransformation));
				}
			}
		}

		//compute f_i(site1) and return
		std::vector<std::pair<LatticeSite, SpinPermutation>> fsite1;
		if (validTransformations.size() > 0)
		{
			fsite1.reserve(validTransformations.size());

			//build equivalence class from all transformed sites
			for (auto t : validTransformations)
			{
				LatticeSite ts1;
				if (!siteAtPosition(uc, t.first * getSitePosition(uc, site1), ts1)) throw Exception(Exception::Type::InternalError, "Lattice symmetry calculation has failed. (Internal error. Unexpected outcome of an allegedly valid symmetry)");
				std::pair<LatticeSite, SpinPermutation> transformation(ts1, t.second);

				if (std::find(fsite1.begin(), fsite1.end(), transformation) == fsite1.end()) fsite1.push_back(transformation);
			}
		}
		else throw Exception(Exception::Type::InternalError, "Lattice symmetry calculation has failed. (Internal error. Could not establish identity as a valid symmetry operation)");
		return fsite1;
	}
	#pragma endregion

	std::pair<Lattice *, SpinModel *> newLatticeModel(const LatticeUnitCell &uc, const SpinModelUnitCell &spinModelDefinition, const int latticeRange, const std::string &ldfPath)
	{
		Log::log << Log::LogLevel::Info << "Building lattice spin model..." << Log::endl;

		//generate empty lattice
		Lattice *lattice = new Lattice;

		//construct neighborhoods around basis sites
		std::vector<std::vector<LatticeSite> > neighborhoods;
		for (int b = 0; b < int(uc.basisSites.size()); ++b) neighborhoods.push_back(constructRangeAroundSite(uc, LatticeSite(0, 0, 0, b), latticeRange));

		//set lattice->_bravaisLattice
		lattice->_bravaisLattice = uc.latticeVectors;

		//set lattice->_basis
		lattice->_basis = uc.basisSites;

		//construct lattice parametrization
		Log::log << Log::LogLevel::Info << "\t...finding lattice parametrization" << Log::endl;
		std::vector<std::pair<int, SpinPermutation>> equivalenceClasses;
		equivalenceClasses.resize(neighborhoods[0].size());
		for (int i = 0; i < int(equivalenceClasses.size()); ++i) equivalenceClasses[i] = std::pair<int, SpinPermutation>(-1, SpinPermutation());
		int rid = 0;
		for (int i = 0; i < int(equivalenceClasses.size()); ++i)
		{
			//skip if the equivalence class of this site has already been defined
			if (equivalenceClasses[i].first != -1) continue;

			//compute symmetry related sites
			auto equiv = symmetryReduce(uc, spinModelDefinition, neighborhoods[0][i], LatticeSite(0, 0, 0, 0));

			//define trivial representative of the group
			equivalenceClasses[i].first = rid;
			equivalenceClasses[i].second = SpinPermutation::identity();

			//assign the same representative id (rid) to all equivalent sites
			for (auto e : equiv)
			{
				//determine symmetry related partner, skip if it has been addressed before
				int j = int(std::find(neighborhoods[0].begin(), neighborhoods[0].end(), e.first) - neighborhoods[0].begin());
				if (equivalenceClasses[j].first != -1) continue;

				//e contains a symmetry transformation of site(i) to site(j), i.e. we need to store the inverse transformation for site(j)
				equivalenceClasses[j].first = rid;
				equivalenceClasses[j].second = SpinPermutation::inverse(e.second);
			}
			++rid;
		}
		//sort lattice parametrization to store the trivial representative of each equivalence class upfront
		for (int r = 0; r < rid; ++r)
		{
			for (int i = 0; i < int(equivalenceClasses.size()); ++i)
			{
				if (equivalenceClasses[i].first == r && equivalenceClasses[i].second == SpinPermutation::identity())
				{
					std::swap(equivalenceClasses[r], equivalenceClasses[i]);
					std::swap(neighborhoods[0][r], neighborhoods[0][i]);
					continue;
				}
			}
		}

		//set lattice->size
		lattice->size = rid;

		//generate lattice sites
		std::vector<LatticeSite> sites = neighborhoods[0];
		for (int b = 0; b < int(uc.basisSites.size()); ++b)
		{
			for (auto n : neighborhoods[b]) if (std::find(sites.begin(), sites.end(), n) == sites.end()) sites.push_back(n);
		}

		//set lattice->_geometryTable
		Log::log << Log::LogLevel::Info << "\t...initializing lattice geometry buffers" << Log::endl;
		lattice->_geometryTable.resize(sites.size());
		for (int i = 0; i < int(sites.size()); ++i) lattice->_geometryTable[i] = std::tuple<int, int, int, int>(sites[i].a0, sites[i].a1, sites[i].a2, sites[i].b);

		//set lattice->_dataSize
		lattice->_dataSize = int(sites.size());

		//set lattice->_bufferBasis
		lattice->_bufferBasis = new int[uc.basisSites.size() + 1];
		for (int b = 0; b < int(uc.basisSites.size()); ++b)
		{
			for (int i = 0; i < int(sites.size()); ++i) if (sites[i] == LatticeSite(0, 0, 0, b)) lattice->_bufferBasis[b] = i;
		}
		lattice->_bufferBasis[uc.basisSites.size()] = int(sites.size());

		//set lattice->_bufferLatticeRange
		lattice->_bufferLatticeRange = new int*[uc.basisSites.size()];
		for (int b = 0; b < int(uc.basisSites.size()); ++b)
		{
			lattice->_bufferLatticeRange[b] = new int[neighborhoods[b].size() + 1];
			for (int n = 0; n < int(neighborhoods[b].size()); ++n)
			{
				for (int i = 0; i < int(sites.size()); ++i) if (neighborhoods[b][n] == sites[i]) lattice->_bufferLatticeRange[b][n] = i;
			}
			lattice->_bufferLatticeRange[b][neighborhoods[b].size()] = int(sites.size());
		}

		//generate lattice->_symmetryTable
		Log::log << Log::LogLevel::Info << "\t...calculating lattice symmetries" << Log::endl;
		lattice->_symmetryTable = new LatticeSiteDescriptor[sites.size() * sites.size()];
		for (int i = 0; i < int(sites.size() * sites.size()); ++i)
		{
			lattice->_symmetryTable[i].rid = -1;
			lattice->_symmetryTable[i].spinPermutation[0] = SpinComponent::X;
			lattice->_symmetryTable[i].spinPermutation[1] = SpinComponent::Y;
			lattice->_symmetryTable[i].spinPermutation[2] = SpinComponent::Z;
		}
		//init entries related to lattice parametrization (transformations with site1=LatticeSite(0,0,0,0))
		for (int n = 0; n < int(neighborhoods[0].size()); ++n)
		{
			lattice->_symmetryTable[0 * sites.size() + n].rid = equivalenceClasses[n].first;
			lattice->_symmetryTable[0 * sites.size() + n].spinPermutation[0] = equivalenceClasses[n].second.transformedComponent[0];
			lattice->_symmetryTable[0 * sites.size() + n].spinPermutation[1] = equivalenceClasses[n].second.transformedComponent[1];
			lattice->_symmetryTable[0 * sites.size() + n].spinPermutation[2] = equivalenceClasses[n].second.transformedComponent[2];
		}
		//init entries related to fundamental transformations with site1=LatticeSite(0,0,0,b) for b non-zero)
		for (int b = 1; b < int(uc.basisSites.size()); ++b)
		{
			for (auto n : neighborhoods[b])
			{
				auto equiv = symmetryReduce(uc, spinModelDefinition, n, LatticeSite(0, 0, 0, b), true);
				if (equiv.size() == 0) throw Exception(Exception::Type::InitializationError, "Could not build lattice. Could not find enough symmetries");
				int bid = int(std::find(sites.begin(), sites.end(), LatticeSite(0, 0, 0, b)) - sites.begin());
				int nid = int(std::find(sites.begin(), sites.end(), n) - sites.begin());
				int eid = int(std::find(sites.begin(), sites.end(), equiv[0].first) - sites.begin());

				lattice->_symmetryTable[bid * sites.size() + nid].rid = lattice->_symmetryTable[0 * sites.size() + eid].rid;
				lattice->_symmetryTable[bid * sites.size() + nid].spinPermutation[0] = equivalenceClasses[eid].second.transformedComponent[static_cast<int>(equiv[0].second.transformedComponent[static_cast<int>(SpinComponent::X)])];
				lattice->_symmetryTable[bid * sites.size() + nid].spinPermutation[1] = equivalenceClasses[eid].second.transformedComponent[static_cast<int>(equiv[0].second.transformedComponent[static_cast<int>(SpinComponent::Y)])];
				lattice->_symmetryTable[bid * sites.size() + nid].spinPermutation[2] = equivalenceClasses[eid].second.transformedComponent[static_cast<int>(equiv[0].second.transformedComponent[static_cast<int>(SpinComponent::Z)])];
			}
		}
		//init remaining entries of all overlapping pairs of sites
		for (int s1 = 0; s1 < int(sites.size()); ++s1)
		{
			for (int s2 = 0; s2 < int(sites.size()); ++s2)
			{
				LatticeSite s1p(0, 0, 0, sites[s1].b);
				LatticeSite s2p(sites[s2].a0 - sites[s1].a0, sites[s2].a1 - sites[s1].a1, sites[s2].a2 - sites[s1].a2, sites[s2].b);
				if (std::find(neighborhoods[s1p.b].begin(), neighborhoods[s1p.b].end(), s2p) == neighborhoods[s1p.b].end()) continue;

				int s1pid = int(std::find(sites.begin(), sites.end(), s1p) - sites.begin());
				int s2pid = int(std::find(sites.begin(), sites.end(), s2p) - sites.begin());
				lattice->_symmetryTable[s1 * sites.size() + s2] = lattice->_symmetryTable[s1pid * sites.size() + s2pid];
			}
		}

		//generate lattice->_bufferSites
		lattice->_bufferSites = new LatticeSiteDescriptor[lattice->size];
		for (int i = 0; i < lattice->size; ++i)
		{
			lattice->_bufferSites[i] = lattice->_symmetryTable[0 * sites.size() + i];
		}

		//generate lattice->_bufferInvertedSites
		lattice->_bufferInvertedSites = new LatticeSiteDescriptor[lattice->size];
		for (int i = 0; i < lattice->size; ++i)
		{
			lattice->_bufferInvertedSites[i] = lattice->_symmetryTable[i * sites.size() + 0];
		}

		//generate lattice->_bufferOverlapMatrices
		auto bufferNewOverlapTable = [&](int rid)
		{
			//overlap table buffer
			std::vector<int> overlapRid1;
			std::vector<int> overlapRid2;
			std::vector<SpinComponent> overlapTX1;
			std::vector<SpinComponent> overlapTY1;
			std::vector<SpinComponent> overlapTZ1;
			std::vector<SpinComponent> overlapTX2;
			std::vector<SpinComponent> overlapTY2;
			std::vector<SpinComponent> overlapTZ2;

			//generate overlap buffer
			LatticeSite i1 = sites[0];
			LatticeSite i2 = sites[rid];

			for (int j = 0; j < int(sites.size()); ++j)
			{
				//verify that j is in range of i1
				if (std::find(neighborhoods[i1.b].begin(), neighborhoods[i1.b].end(), sites[j]) == neighborhoods[i1.b].end()) continue;

				//verify that j is in range of i2
				if (std::find(neighborhoods[i2.b].begin(), neighborhoods[i2.b].end(), LatticeSite(sites[j].a0 - i2.a0, sites[j].a1 - i2.a1, sites[j].a2 - i2.a2, sites[j].b)) == neighborhoods[i2.b].end()) continue;

				//symmetry transform the pair (0,j) to obtain rid1 and (j,rid) to obtain rid2
				LatticeSiteDescriptor t1 = lattice->_symmetryTable[0 * sites.size() + j];
				LatticeSiteDescriptor t2 = lattice->_symmetryTable[j * sites.size() + rid];

				overlapRid1.push_back(t1.rid);
				overlapTX1.push_back(t1.spinPermutation[0]);
				overlapTY1.push_back(t1.spinPermutation[1]);
				overlapTZ1.push_back(t1.spinPermutation[2]);
				overlapRid2.push_back(t2.rid);
				overlapTX2.push_back(t2.spinPermutation[0]);
				overlapTY2.push_back(t2.spinPermutation[1]);
				overlapTZ2.push_back(t2.spinPermutation[2]);
			}

			LatticeOverlap overlap(int(overlapRid1.size()));
			memcpy(overlap.rid1, overlapRid1.data(), overlapRid1.size() * sizeof(int));
			memcpy(overlap.rid2, overlapRid2.data(), overlapRid2.size() * sizeof(int));
			memcpy(overlap.transformedX1, overlapTX1.data(), overlapTX1.size() * sizeof(SpinComponent));
			memcpy(overlap.transformedY1, overlapTY1.data(), overlapTY1.size() * sizeof(SpinComponent));
			memcpy(overlap.transformedZ1, overlapTZ1.data(), overlapTZ1.size() * sizeof(SpinComponent));
			memcpy(overlap.transformedX2, overlapTX2.data(), overlapTX2.size() * sizeof(SpinComponent));
			memcpy(overlap.transformedY2, overlapTY2.data(), overlapTY2.size() * sizeof(SpinComponent));
			memcpy(overlap.transformedZ2, overlapTZ2.data(), overlapTZ2.size() * sizeof(SpinComponent));

			return overlap;
		};
		lattice->_bufferOverlapMatrices = new LatticeOverlap[lattice->size];
		for (int rid = 0; rid < lattice->size; ++rid) lattice->_bufferOverlapMatrices[rid] = bufferNewOverlapTable(rid);

		//init SpinModel
		SpinModel* spinModel = new SpinModel();

		//add interaction parameters
		spinModel->interactionParameters.insert(spinModel->interactionParameters.begin(), spinModelDefinition.interactionParameters.begin(), spinModelDefinition.interactionParameters.end());

		//add interaction strengths
		for (int rid = 0; rid < lattice->size; ++rid)
		{
			auto i1 = lattice->getSiteParameters(lattice->fromParametrization(rid));

			for (auto interaction : spinModelDefinition.interactions)
			{
				int connection = interaction.isConnectingSites(LatticeSite(0, 0, 0, 0), LatticeSite(std::get<0>(i1), std::get<1>(i1), std::get<2>(i1), std::get<3>(i1)));

				if (connection == 1)
				{
					SpinModel::SpinInteraction i;
					for (int s1 = 0; s1 < 3; ++s1)
					{
						for (int s2 = 0; s2 < 3; ++s2) i.interactionStrength[s1][s2] = interaction.interactionStrength[s1][s2];
					}
					spinModel->interactions.push_back(std::pair<LatticeIterator, SpinModel::SpinInteraction>(lattice->fromParametrization(rid), i));
				}
				else if (connection == -1)
				{
					SpinModel::SpinInteraction i;
					for (int s1 = 0; s1 < 3; ++s1)
					{
						for (int s2 = 0; s2 < 3; ++s2) i.interactionStrength[s1][s2] = interaction.interactionStrength[s2][s1];
					}
					spinModel->interactions.push_back(std::pair<LatticeIterator, SpinModel::SpinInteraction>(lattice->fromParametrization(rid), i));
				}
			}
		}

		//print ldf file
		if (ldfPath != "")
		{
			//open file file
			std::ofstream ldfFile(ldfPath, std::ios::out);
			if (!ldfFile.is_open()) throw Exception(Exception::Type::IOError, "Could not write lattice debug information to file. ");
			ldfFile << "<lattice>" << std::endl;

			//write sites
			for (auto i1 = lattice->getRange(0); i1 != lattice->end(); ++i1)
			{
				geometry::Vec3<double> p = lattice->getSitePosition(i1);
				std::string parametrized = (i1 - lattice->begin() < lattice->size) ? "true" : "false";
				ldfFile << boost::format("\t<site id=\"%d\" x=\"%f\" y=\"%f\" z=\"%f\" parametrized=\"%s\"/>") % (i1 - lattice->begin()) % p.x % p.y % p.z % parametrized << std::endl;
			}

			//write bonds
			for (auto i1 = lattice->getRange(0); i1 != lattice->end(); ++i1)
			{
				for (auto i2 = lattice->getRange(0); i2 != lattice->end(); ++i2)
				{
					auto s1Parm = lattice->getSiteParameters(i1);
					auto s2Parm = lattice->getSiteParameters(i2);
					LatticeSite s1 = LatticeSite(std::get<0>(s1Parm), std::get<1>(s1Parm), std::get<2>(s1Parm), std::get<3>(s1Parm));
					LatticeSite s2 = LatticeSite(std::get<0>(s2Parm), std::get<1>(s2Parm), std::get<2>(s2Parm), std::get<3>(s2Parm));

					for (auto bond : uc.latticeBonds)
					{
						if (bond.isConnectingFromTo(s1, s2)) ldfFile << boost::format("\t<bond from=\"%d\" to=\"%d\" />") % (i1 - lattice->begin()) % (i2 - lattice->begin()) << std::endl;
					}
				}
			}

			//write interactions
			for (auto i1 = lattice->getRange(0); i1 != lattice->end(); ++i1)
			{
				for (auto i2 = lattice->getRange(0); i2 != lattice->end(); ++i2)
				{
					auto s1Parm = lattice->getSiteParameters(i1);
					auto s2Parm = lattice->getSiteParameters(i2);
					LatticeSite s1 = LatticeSite(std::get<0>(s1Parm), std::get<1>(s1Parm), std::get<2>(s1Parm), std::get<3>(s1Parm));
					LatticeSite s2 = LatticeSite(std::get<0>(s2Parm), std::get<1>(s2Parm), std::get<2>(s2Parm), std::get<3>(s2Parm));

					for (auto i : spinModelDefinition.interactions)
					{
						if (i.isConnectingFromTo(s1, s2))
						{
							ldfFile << boost::format("\t<interaction from=\"%d\" to=\"%d\" value=\"[[%f,%f,%f],[%f,%f,%f],[%f,%f,%f]]\" />") % (i1 - lattice->begin()) % (i2 - lattice->begin()) % i.interactionStrength[0][0] % i.interactionStrength[0][1] % i.interactionStrength[0][2] % i.interactionStrength[1][0] % i.interactionStrength[1][1] % i.interactionStrength[1][2] % i.interactionStrength[2][0] % i.interactionStrength[2][1] % i.interactionStrength[2][2] << std::endl;
						}
					}
				}
			}

			//finalize file
			ldfFile << "</lattice>" << std::endl;
			ldfFile.close();
		}

		//return lattice
		return std::pair<Lattice*, SpinModel*>(lattice, spinModel);
	}
}


//=======================================================================
// Copyright 2015 - 2020 Jeff Linahan
//
// Distributed under 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)
//=======================================================================
#include "lipton-tarjan.h"
#include "theorem4.h"
#include "lemmas.h"
#include "strutil.h"
#include "BFSVert.h"
#include "BFSVisitorData.h"
#include "BFSVisitor.h"
#include "EmbedStruct.h"
#include "ScanVisitor.h"
#include "graphutil.h"
#include <boost/lexical_cast.hpp>
#include <boost/graph/graph_concepts.hpp>
#include <boost/graph/planar_canonical_ordering.hpp>
#include <boost/graph/is_straight_line_drawing.hpp>
#include <boost/graph/boyer_myrvold_planar_test.hpp>
#include <boost/graph/make_biconnected_planar.hpp>
#include <boost/graph/make_maximal_planar.hpp>
#include <boost/graph/connected_components.hpp>
#include <boost/graph/copy.hpp>
#include <boost/graph/breadth_first_search.hpp>
#include <boost/pending/indirect_cmp.hpp>
#include <boost/range/irange.hpp>
#include <boost/bimap.hpp>
#include <boost/config.hpp>
#include <iostream>
#include <algorithm>
#include <utility>
#include <csignal>
#include <iterator>
using namespace std;
using namespace boost;

// Step 10: construct_vertex_partition
// Time:    O(n)
//
// Use the fundamental cycle found in Step 9 and the levels found in Step 4 (l1_and_k) to construct a satisfactory vertex partition as described in the proof of Lemma 3
// Extend this partition from the connected component chosen in Step 2 to the entire graph as described in the proof of Theorem 4.
Partition construct_vertex_partition(GraphCR g_orig, Graph& g_shrunk, vector<uint> const& L, uint l[3], BFSVisitorData const& vis_data_orig, BFSVisitorData const& vis_data_shrunken, vector<vertex_t> const& fundamental_cycle)
{
        vertex_map idx;
        associative_property_map<vertex_map> vertid_to_component(idx);
        uint num_components = connected_components(g_orig, vertid_to_component);
        //BOOST_ASSERT(1 == num_components);

        uint n = num_vertices(g_orig);
        uint n_biggest_comp = vis_data_orig.verts.size(); // if num_components > 1 then num_vertices(g_orig) will not be what we want
        //cout << "n_biggest: " << n_biggest_comp << '\n';

        cout  << "\n------------ 10  - Construct Vertex Partition --------------\n";
        cout << "g_orig:\n";
        print_graph(g_orig);
        cout << "l0: " << l[0] << '\n';
        cout << "l1: " << l[1] << '\n';
        cout << "l2: " << l[2] << '\n';

        uint r = vis_data_orig.num_levels;
        cout << "r max distance: " << r << '\n';

        Partition biggest_comp_p = lemma3(g_orig, L, l[1], l[2], r, vis_data_orig, vis_data_shrunken, fundamental_cycle, &g_shrunk);
        biggest_comp_p.verify_edges(g_orig);
        biggest_comp_p.verify_sizes_lemma3(L, l[1], l[2]);
        if( 1 == num_components ){

                if( biggest_comp_p.verify_sizes(g_orig) && biggest_comp_p.verify_edges(g_orig) ) return biggest_comp_p;

                return theorem4_connected(g_orig, L, l, r, &g_shrunk, &vis_data_shrunken);
        }

        //associative_property_map<vertex_map> const& vertid_to_component, vector<uint> const& num_verts_per_component)
        vector<uint> num_verts_per_component(num_components, 0);
        VertIter vit, vjt;
        for( tie(vit, vjt) = vertices(g_orig); vit != vjt; ++vit ){
	       	++num_verts_per_component[vertid_to_component[*vit]];
	}

        // somehow the two Partitions need to be stiched together

        cout << "biggest comp p:\n";
        biggest_comp_p.print(&g_orig);

        Partition extended_p = theorem4_disconnected(g_orig, n, num_components, vertid_to_component, num_verts_per_component, biggest_comp_p);

        return extended_p; // TODO replace this with extended_p
}

// Locate the triangle (vi, y, wi) which has (vi, wi) as a boundary edge and lies inside the (vi, wi) cycle.
// one of the vertices in the set neighbors_vw is y.  Maybe it's .begin(), so we use is_edge_inside_outside_or_on_cycle to test if it is.j
vertex_t findy(vertex_t vi, set<vertex_t> const& neighbors_vw, vector<vertex_t> const& cycle, GraphCR g_shrunk, EmbedStruct const& em,  decltype(get(vertex_index, const_cast<Graph&>(g_shrunk))) prop_map)
{
	for( vertex_t y_candidate : neighbors_vw ){
                pair<edge_t, bool> vi_cy = edge(vi, y_candidate, g_shrunk);
                BOOST_ASSERT(vi_cy.second);
                edge_t e = vi_cy.first;
                InsideOutOn insideout = is_edge_inside_outside_or_on_cycle(e, vi, cycle, g_shrunk, em.em);
                if( INSIDE == insideout ){
                        return y_candidate;
                }
	}
	/*pair<edge_t, bool> maybe_y = edge(vi, *neighbors_vw.begin(), g_shrunk);
	BOOST_ASSERT(maybe_y.second); // I'm assuming the bool means that the edge_t exists?  Boost Graph docs don't say
	cout << "maybe_y: " << to_string(maybe_y.first, g_shrunk) << '\n';

	cout << "cycle:\n";
	print_cycle(cycle, g_shrunk);

	vertex_t common_vert_on_cycle = find(STLALL(cycle), *neighbors_vw.begin()) == cycle.end() ?
					*neighbors_vw.rbegin() :
					*neighbors_vw.begin() ;
	cout << "common vert on cycle: " << prop_map[common_vert_on_cycle] << '\n';
	BOOST_ASSERT(find(STLALL(cycle), common_vert_on_cycle) != cycle.end());

	InsideOutOn insideout = is_edge_inside_outside_or_on_cycle(maybe_y.first, common_vert_on_cycle, cycle, g_shrunk, em.em);
	BOOST_ASSERT(insideout != ON);
	vertex_t y = (insideout == INSIDE) ? *neighbors_vw.begin() : *neighbors_vw.rbegin();*/
	// We now have the (vi, y, wi) triangle
        BOOST_ASSERT(0);
}

// Step 9: Improve Separator
// Time:   O(n)
//
// Let (vi, wi) be the nontree edge whose cycle is the current candidate to complete the separator.
// If the cost inside the cycle exceeds 2/3, find a better cycle by the following method.
// 	Locate the triangle (vi, y, wi) which has (vi, wi) as a boundary edge and lies inside the (vi, wi) cycle.
//      If either (vi, y) or (y, wi) is a tree edge, let (vi+1, wi+1) be the nontree edge among (vi, y) and (y, wi).
//      Compute the cost inside the (vi+1, wi+1) cycle from the cost inside the (vi, wi) cycle and the cost of vi, y and wi.
// 	If neither (vi, y) nor (y, wi) is a tree edge, determine the tree path from y to the (vi, wi) cycle by following parent pointers from y.
//      Let z be the vertex on the (vi, wi) cycle reached during this search.  Compute the total cost of all vertices except z on this tree path.
//      Scan the tree edges inside the (y, wi) cycle, alternately scanning an edge in one cycle and an edge in the other cycle.
//      Stop scanning when all edges inside one of the cycles have been scanned.  Compute the cost inside this cycle by summing the associated costs of all scanned edges.
//      Use this cost, the cost inside the (vi, wi) cycle, and the cost on the tree path from y to z to compute the cost inside the other cycle.
//      Let (vi+1, wi+1) be the edge among (vi, y) and (y, wi) whose cycle has more cost inside it.
// 	Repeat Step 9 until finding a cycle whose inside has cost not exceeding 2/3.
Partition improve_separator(GraphCR g_orig, Graph& g_shrunk, CycleCost& cc, edge_t completer_candidate_edge, BFSVisitorData const& vis_data_orig,
                            BFSVisitorData const& vis_data_shrunken, vector<vertex_t> cycle, EmbedStruct const& em, bool cost_swapped, vector<uint> const& L, uint l[3])
{
        cout << "---------------------------- 9 - Improve Separator -----------\n";
        print_graph(g_shrunk);

        auto prop_map = get(vertex_index, g_shrunk); // writing to this property map has side effects in the graph

        cout << "cycle: ";
        for( uint i = 0; i < cycle.size(); ++i ) cout << prop_map[cycle[i]] << ' ';
        cout << '\n';

        uint n_orig = num_vertices(g_orig);
        uint n = num_vertices(g_shrunk);
        cout << "n_orig: " << n_orig << ", n: " << n << '\n';

        while( cc.inside > 2.*n/3 ){
                cout << "nontree completer candidate edge: " << to_string(completer_candidate_edge, g_shrunk) << '\n';
                cout << "cost inside: " << cc.inside  << '\n';
                cout << "cost outide: " << cc.outside << '\n';
                cout << "looking for a better cycle\n";

		// Let (vi, wi) be the nontree edge whose cycle is the current candidate to complete the separator
                vertex_t vi = source(completer_candidate_edge, g_shrunk);
                vertex_t wi = target(completer_candidate_edge, g_shrunk);
                BOOST_ASSERT(!vis_data_shrunken.is_tree_edge(completer_candidate_edge));
                cout << "   vi: " << prop_map[vi] << '\n';
                cout << "   wi: " << prop_map[wi] << '\n';

                set<vertex_t> neighbors_of_v = get_neighbors(vi, g_shrunk);
                set<vertex_t> neighbors_of_w = get_neighbors(wi, g_shrunk);
                set<vertex_t> neighbors_vw = get_intersection(neighbors_of_v, neighbors_of_w);
                for( auto& ne : neighbors_vw ){
                        cout << "neighbor: " << ne << " prop_map: " << prop_map[ne] << '\n';
                }
                cout << "   neighbors_vw_begin : " << prop_map[*neighbors_vw.begin()] << '\n';
                cout << "   neighbors_vw_rbegin: " << prop_map[*neighbors_vw.rbegin()] << '\n';

		vertex_t y = findy(vi, neighbors_vw, cycle, g_shrunk, em, prop_map);

                cout << "   y: " << prop_map[y] << '\n';
                pair<edge_t, bool> viy_e = edge(vi, y, g_shrunk); BOOST_ASSERT(viy_e.second); edge_t viy = viy_e.first;
                pair<edge_t, bool> ywi_e = edge(y, wi, g_shrunk); BOOST_ASSERT(ywi_e.second); edge_t ywi = ywi_e.first;
                edge_t next_edge;

		// if either (vi, y) or (y, wi) is a tree edge,
                if ( vis_data_shrunken.is_tree_edge(viy) || vis_data_shrunken.is_tree_edge(ywi) ){
			// determine the tree path from y to the (vi, wi) cycle by following parent pointers from y.
                        cout << "   at least one tree edge\n";
                        next_edge = vis_data_shrunken.is_tree_edge(viy) ? ywi : viy;
                        BOOST_ASSERT(!vis_data_shrunken.is_tree_edge(next_edge));

                        // Compute the cost inside the (vi+1 wi+1) cycle from the cost inside the (vi, wi) cycle and the cost of vi, y, and wi.  See Fig 4.
                        uint cost[4] = {vis_data_shrunken.verts.find(vi)->second.descendant_cost,
					vis_data_shrunken.verts.find(y )->second.descendant_cost,
					vis_data_shrunken.verts.find(wi)->second.descendant_cost,
					cc.inside};
                        vector<vertex_t> new_cycle = vis_data_shrunken.get_cycle(source(next_edge, g_shrunk), target(next_edge, g_shrunk));
                        cc = compute_cycle_cost(new_cycle, g_shrunk, vis_data_shrunken, em); // !! CHEATED !!
                        if( cost_swapped ) swap(cc.outside, cc.inside);
                } else {
                        // Determine the tree path from y to the (vi, wi) cycle by following parents of y.
                        cout << "   neither are tree edges\n";
                        vector<vertex_t> y_parents = vis_data_shrunken.ancestors(y);

                        for( vertex_t vp : y_parents ){
                                cout << "y parent: " << prop_map[vp] << '\n';
                        }

                        uint i = 0;
                        while( !on_cycle(y_parents.at(i), cycle, g_shrunk) ){
                                cout << "yparents[" << i << "]: " << y_parents[i] << " propmap: " << prop_map[y_parents[i]] << '\n';
                                ++i;
                        }
                        cout << "yparents[" << i << "]: " << y_parents.at(i) << " propmap: " << prop_map[y_parents[i]] << '\n';

                        // Let z be the vertex on the (vi, wi) cycle reached during the search.
			cout << "i: " << i << '\n';
                        vertex_t z = y_parents.at(i);
			cout << "z: " << prop_map[z] << '\n';
                        BOOST_ASSERT(on_cycle(z, cycle, g_shrunk));
                        cout << "    z: " << prop_map[z] << '\n';
                        y_parents.erase(y_parents.begin()+i, y_parents.end());
                        BOOST_ASSERT(y_parents.size() == i);

                        // Compute the total cost af all vertices except z on this tree path.
                        uint path_cost = y_parents.size() - 1;
                        cout << "    y-to-z-minus-z cost: " << path_cost << '\n';

                        // Scan the tree edges inside the (y, wi) cycle, alternately scanning an edge in one cycle and an edge in the other cycle.
                        // Stop scanning when all edges inside one of the cycles have been scanned.  Compute the cost inside this cycle by summing the associated costs of all scanned edges.
                        // Use this cost, the cost inside the (vi, wi) cycle, and the cost on the tree path from y to z to compute the cost inside the other cycle.
                        vector<vertex_t> cycle1 = vis_data_shrunken.get_cycle(vi, y);
                        vector<vertex_t> cycle2 = vis_data_shrunken.get_cycle(y, wi);
                        CycleCost cost1 = compute_cycle_cost(cycle1, g_shrunk, vis_data_shrunken, em);
                        CycleCost cost2 = compute_cycle_cost(cycle2, g_shrunk, vis_data_shrunken, em);
                        if( cost_swapped ){
                                swap(cost1.inside, cost1.outside);
                                swap(cost2.inside, cost2.outside);
                        }

                        // Let (vi+1, wi+1) be the edge among (vi, y) and (i, wi) whose cycle has more cost inside it.
                        if( cost1.inside > cost2.inside ){ next_edge = edge(vi, y, g_shrunk).first; cc = cost1; cycle = cycle1;}
                        else                             { next_edge = edge(y, wi, g_shrunk).first; cc = cost2; cycle = cycle2;}
                }
                completer_candidate_edge = next_edge;
        }
        cout << "found fundamental cycle with inside cost " << cc.inside << " which is less than 2/3\n";
        print_cycle(cycle, g_shrunk);

        //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components
        //BOOST_ASSERT(vis_data_orig.assert_data());
        //BOOST_ASSERT(vis_data_shrunken.assert_data());

	return construct_vertex_partition(g_orig, g_shrunk, L, l, vis_data_orig, vis_data_shrunken, cycle); // step 10
}

// Step 8: locate_cycle
// Time: O(n)
//
// Choose any nontree edge (v1, w1).
// Locate the corresponding cycle by following parent pointers from v1 and w1.
// Compute the cost on each side of this cycle by scanning the tree edges incident on either side of the cycle and summing their associated costs.
// If (v, w) is a tree edge with v on the cycle and w not on the cycle, the cost associated with (v,w) is the descendant cost of w if v is the parent of w,
// and the cost of all vertices minus the descendant cost of v if w is the parent of v.
// Determine which side of the cycle has greater cost and call it the "inside"
Partition locate_cycle(GraphCR g_orig, Graph& g_shrunk, BFSVisitorData const& vis_data_orig, BFSVisitorData const& vis_data_shrunken, vector<uint> const& L, uint l[3])
{
        //BOOST_ASSERT(vis_data_orig.assert_data()); //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components
        //BOOST_ASSERT(vis_data_shrunken.assert_data()); //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components

        uint n = num_vertices(g_orig);
        cout  << "----------------------- 8 - Locate Cycle -----------------\n";
        print_graph(g_shrunk);
        edge_t completer_candidate_edge;

        try {
                completer_candidate_edge = vis_data_shrunken.arbitrary_nontree_edge(g_shrunk);
        } catch (NoNontreeEdgeException const& e){
                vector<vertex_t> cycle;
                CycleCost cc;
                cc.inside = 0;
                cc.outside = n; // force Step 9 to exit without going through any iterations
                bool cost_swapped = false;
                EmbedStruct em(&g_shrunk);
                return improve_separator(g_orig, g_shrunk, cc, completer_candidate_edge, vis_data_orig, vis_data_shrunken, cycle, em, cost_swapped, L, l); // step 9
        }
        vertex_t v1 = source(completer_candidate_edge, g_shrunk);
        vertex_t w1 = target(completer_candidate_edge, g_shrunk);
        cout << "ancestors v1...\n";
        vector<vertex_t> parents_v   = vis_data_shrunken.ancestors(v1);
        cout << "ancestors v2...\n";
        vector<vertex_t> parents_w   = vis_data_shrunken.ancestors(w1);
        vertex_t common_ancestor    = get_common_ancestor(parents_v, parents_w);
        cout << "common ancestor: " << common_ancestor << '\n';
        vector<vertex_t> cycle = vis_data_shrunken.get_cycle(v1, w1, common_ancestor);

        EmbedStruct em(&g_shrunk);
        CycleCost cc = compute_cycle_cost(cycle, g_shrunk, vis_data_shrunken, em);
	bool cost_swapped;
        if( cc.outside > cc.inside ){
                swap(cc.outside, cc.inside);
                cost_swapped = true;
                cout << "!!!!!! cost swapped !!!!!!!!\n";
        } else cost_swapped = false;
        cout << "total inside cost:  " << cc.inside  << '\n';
        cout << "total outside cost: " << cc.outside << '\n';

	return improve_separator(g_orig, g_shrunk, cc, completer_candidate_edge, vis_data_orig, vis_data_shrunken, cycle, em, cost_swapped, L, l); // step 9
}

// Step 7: new_bfs_and_make_max_planar
// Time:   O(n)
//
// Construct a breadth-first spanning tree rooted at x in the new (shrunken) graph.
// (This can be done by modifying the breadth-first spanning tree constructed in Step 3 bfs_and_levels.)
// Record, for each vertex v, the parent of v in the tree, and the total cost of all descendants of v includiing v itself.
// Make all faces of the new graph into triangles by scanning the boundary of each face and adding (nontree) edges as necessary.
Partition new_bfs_and_make_max_planar(GraphCR g_orig, Graph& g_shrunk, BFSVisitorData const& vis_data_orig, vertex_t x, vector<uint> const& L, uint l[3])
{
        cout  << "-------------------- 7 - New BFS and Make Max Planar -----\n";
        cout << "g_orig:\n";
        print_graph(g_orig);
        print_graph_addresses(g_orig);
        cout << "g_shrunk:\n";
        print_graph(g_shrunk);
        print_graph_addresses(g_shrunk);
        //reset_vertex_indices(g_shrunk);
        //reset_edge_index(g_shrunk);
        BFSVisitorData shrunken_vis_data(&g_shrunk, x);

        //vis_data.reset(&g_shrunk);
        shrunken_vis_data.root = x;
        ++shrunken_vis_data.verts[shrunken_vis_data.root].descendant_cost;

        uint n = num_vertices(g_shrunk);
        cout << "n:    " << n << '\n';
        cout << "null vertex: " << Graph::null_vertex() << '\n';

        BFSVisitor visit = BFSVisitor(shrunken_vis_data);

        auto bvs = boost::visitor(visit);

        cout << "g_shrunk:\n";
        print_graph(g_shrunk);

        // workaround for https://github.com/boostorg/graph/issues/195
        VertIter vit, vjt;
        tie(vit, vjt) = vertices(g_shrunk);
        for( VertIter next = vit; vit != vjt; ++vit){
                if( 0 == in_degree(*vit, g_shrunk) + out_degree(*vit, g_shrunk) ){
                        add_edge(*vit, *vit, g_shrunk);
                }
        }

        cout << "g_shrunk with workaround:\n";
        print_graph(g_shrunk);

        BOOST_ASSERT(vertex_exists(shrunken_vis_data.root, g_shrunk));

        breadth_first_search(g_shrunk, shrunken_vis_data.root, bvs);

        make_max_planar(g_shrunk);
        //reset_vertex_indices(g_shrunk);
        reset_edge_index(g_shrunk);

        print_graph(g_shrunk);

	return locate_cycle(g_orig, g_shrunk, vis_data_orig, shrunken_vis_data, L, l);  // step 8
}

// Step 6: Shrinktree
// Time:   big-theta(n)
//
// Delete all vertices on level l2 and above.
// Construct a new vertex x to represent all vertices on levels 0 through l0.
// Construct a boolean table with one entry per vertex.
// Initialize to true the entry for each vertex on levels 0 through l0 and
// initialize to false the entry for each vertex on levels l0 + 1 through l2 - 1.
// The vertices on levels 0 through l0 correspond to a subtree of the breadth-first spanning tree
// generated in Step 3 (bfs_and_levels).
// Scan the edges incident to this tree clockwise around the tree.
// When scanning an edge(v, w) with v in the tree, check the table entry for w.
// If it is true, delete edge(v, w).
// If it is false, change it to true, construct an edge(x,w) and delete edge(v,w).
// The result of this step is a planar representation of the shrunken graph to which Lemma 2 is to be applied.

//const uint X_VERT_UINT = 9999;

vector<vertex_t> shrinktree_deletel2andabove(Graph& g, uint l[3], BFSVisitorData const& vis_data_copy, vertex_t x)
{
        cout << "l[0]: " << l[0] << '\n';
        cout << "l[1]: " << l[1] << '\n';
        cout << "l[2]: " << l[2] << '\n';

        vector<vertex_t> replaced_verts;
	VertIter vit, vjt;
        tie(vit, vjt) = vertices(g);
        for( VertIter next = vit; vit != vjt; vit = next ){
                ++next;
                if( *vit == x ) continue; // don't delete x
                if( !vis_data_copy.verts.contains(*vit) ) continue; // *vit is in a different connected component

                uint level = vis_data_copy.verts.find(*vit)->second.level;

                if( level >= l[2] ){
                        kill_vertex(*vit, g);
                }
                if( level <= l[0] ){
                        replaced_verts.push_back(*vit);
                }
        }
        return replaced_verts;
}

Partition shrinktree(GraphCR g_orig, Graph& g_copy, BFSVisitorData const& vis_data_orig, BFSVisitorData const& vis_data_copy, vector<uint> const& L, uint l[3])
{
        Graph& g_shrunk = g_copy;
        cout << "---------------------------- 6 - Shrinktree -------------\n";
        print_graph(g_copy);
        cout << "n: " << num_vertices(g_shrunk) << '\n';

        // delete all vertices on level l2 and above
        vertex_t x = add_vertex(g_shrunk);
        BFSVisitorData vis_data_addx(vis_data_copy);
        vis_data_addx.root = x;
        vis_data_addx.verts[x].level = 0;
        vis_data_addx.verts[x].parent = Graph::null_vertex();
        vis_data_addx.verts[x].descendant_cost = -1;
        BOOST_ASSERT(vertex_exists(x, g_shrunk));
        //BOOST_ASSERT(assert_verts(g_copy, vis_data_addx)); //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components
        vector<vertex_t> replaced_verts = shrinktree_deletel2andabove(g_shrunk, l, vis_data_addx, x);

        BOOST_ASSERT(vertex_exists(x, g_shrunk));
        //BOOST_ASSERT(assert_verts(g_copy, vis_data_addx)); //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components
        //prop_map[x] = X_VERT_UINT;

        map<vertex_t, bool> table; // x will not be in this table
	VertIter vit, vjt;
        for( tie(vit, vjt) = vertices(g_shrunk); vit != vjt; ++vit ){
                if( *vit == x ) continue; // the new x isn't in visdata, std::map::find() will fail
                if( !vis_data_addx.verts.contains(*vit) ) continue; // *vit may be in a different connected component
                uint level = vis_data_addx.verts.find(*vit)->second.level;
                table[*vit] = level <= l[0];
        }

        BOOST_ASSERT(vertex_exists(x, g_shrunk));
        //BOOST_ASSERT(assert_verts(g_copy, vis_data_addx)); //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components

        cout << "g_shrunk:\n";
        print_graph(g_shrunk);
        reset_vertex_indices(g_shrunk);

        //reset_vertex_indices(g_shrunk);
        //reset_edge_index(g_shrunk);
        EmbedStruct em = ctor_workaround(&g_shrunk);
        BOOST_ASSERT(test_planar_workaround(em.em, &g_shrunk));

        //BOOST_ASSERT(assert_verts(g_copy, vis_data_addx)); //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components

        VertIter vei, vend;
        for( tie(vei, vend) = vertices(g_orig); vei != vend; ++vei ){
                vertex_t v = *vei;
                if( !vis_data_orig.verts.contains(v) ){
                        cerr << "lipton-tarjan.cpp: ignoring bad vertex : " << v << '\n';
                        continue;
                }
        }

        ScanVisitor svis(&table, &g_shrunk, x, l[0]);
        svis.scan_nonsubtree_edges_clockwise(*vertices(g_shrunk).first, g_shrunk, em.em, vis_data_addx);
        svis.finish();

        BOOST_ASSERT(vertex_exists(x, g_shrunk));

        cout << "deleting all vertices x has replaced\n"; for( vertex_t& v : replaced_verts ) {cout << "killing " << v << '\n'; kill_vertex(v, g_shrunk); }// delete all vertices x has replaced

        reset_vertex_indices(g_shrunk);

        BOOST_ASSERT(vertex_exists(x, g_shrunk));

        cout << "g_shrunk:\n";
        print_graph(g_shrunk);

        uint n2 = num_vertices(g_shrunk);
        cout << "shrunken size: " << n2 << '\n';

        auto prop_map = get(vertex_index, g_shrunk);
        cout << "x prop_map: " << prop_map[x] << '\n';
        cout << "x : " << x << '\n';

	return new_bfs_and_make_max_planar(g_orig, g_shrunk, vis_data_orig, x, L, l); // step 7
}

// Step 5: find_more_levels
// Time:   O(n)
//
// Find the highest level l0 <= l1 such that L(l0) + 2(l1 - l0) <= 2*sqrt(k).
// Find the lowest level l2 >= l1 + 1 such that L(l2) + 2(l2-l1-1) <= 2*sqrt(n-k)
Partition find_more_levels(GraphCR g_orig, Graph& g_copy, uint k, uint l[3], vector<uint> const& L, BFSVisitorData const& vis_data_orig, BFSVisitorData const& vis_data_copy)
{
        //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components

        cout  << "---------------------------- 5 - Find More Levels -------\n";
        //print_graph(g_copy);
        float sq  = 2 * sqrt(k);
        float snk = 2 * sqrt(num_vertices(g_copy) - k);
        cout << "sq:     " << sq << '\n';
        cout << "snk:    " << snk << '\n';
        cout << "L size: " << L.size() << '\n';

        l[0] = l[1];
        cout << "l[0]:   " << l[0] << '\n';
        while( l[0] < L.size() ){
                float val = L.at(l[0]) + 2*(l[1] - l[0]);
                if( val <= sq ) break;
                --l[0];
        }
        cout << "l0: " << l[0] << "     highest level <= l1\n";

        l[2] = l[1] + 1;
        cout << "l[2]" << l[2] << '\n';
        while( l[2] < L.size() ){
                float val = L.at(l[2]) + 2*(l[2] - l[1] - 1);
                if( val <= snk ) break;
                ++l[2];
        }
        cout << "l2: " << l[2] << "     lowest  level >= l1 + 1\n";

	return shrinktree(g_orig, g_copy, vis_data_orig, vis_data_copy, L, l); // step 6
}

// Step 4: l1_and_k
// Time:   O(n)
//
// Find the level l1 such that the total cost of levels 0 through l1 - 1 does not exceed 1/2,
// but the total cost of levels 0 through l1 does exceed 1/2.
// Let k be the number of vertices in levels 0 through l1
Partition l1_and_k(GraphCR g_orig, Graph& g_copy, vector<uint> const& L, BFSVisitorData const& vis_data_orig, BFSVisitorData const& vis_data_copy)
{
        //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy)); // disabled because it doesn't support connected components

        cout  << "---------------------------- 4 - l1 and k  ------------\n";
        uint k = L[0];
        uint l[3];
        uint n = num_vertices(g_copy);
        l[1] = 0;
        while( k <= n/2 ){
                uint indx = ++l[1];
                uint lsize = L.size();
                if( indx >= lsize ) break;
	       	k += L.at(indx);
	}

        cout << "k:  " << k    << "      # of verts in levels 0 thru l1\n";
        cout << "l1: " << l[1] << "      total cost of levels 0 thru l1 barely exceeds 1/2\n";
	BOOST_ASSERT(k <= n);
	return find_more_levels(g_orig, g_copy, k, l, L, vis_data_orig, vis_data_copy); // step 5
}

// Step 3: bfs_and_levels
// Time:   O(n)
//
// Find a breadth-first spanning tree of the most costly component.
// Compute the level of each vertex and the number of vertices L(l) in each level l.
Partition bfs_and_levels(GraphCR g_orig, Graph& g_copy)
{
        cout << "---------------------------- 3 - BFS and Levels ------------\n";
        BFSVisitorData vis_data_copy(&g_copy, *vertices(g_copy).first);
        breadth_first_search(g_copy, vis_data_copy.root, boost::visitor(BFSVisitor(vis_data_copy)));
        BFSVisitorData vis_data_orig(&g_orig, *vertices(g_orig).first);
        breadth_first_search(g_orig, vis_data_orig.root, boost::visitor(BFSVisitor(vis_data_orig)));

        // disabled because they don't support multiple connected components
        //BOOST_ASSERT(assert_verts(g_orig, vis_data_orig));
        //BOOST_ASSERT(assert_verts(g_copy, vis_data_copy));

        vector<uint> L(vis_data_copy.num_levels + 1, 0);
	cout << "L levels: " << L.size() << '\n';
        for( auto& d : vis_data_copy.verts ){
                cout << "level: " << d.second.level << '\n';
	       	++L[d.second.level];
	}

	VertIter vit, vjt;
        for( tie(vit, vjt) = vertices(g_copy); vit != vjt; ++vit ){
                if( vis_data_copy.verts.contains(*vit) ) cout << "level/cost of vert " << *vit << ": " << vis_data_copy.verts[*vit].level << '\n';
	}
        for( uint i = 0; i < L.size(); ++i ){
		cout << "L[" << i << "]: " << L[i] << '\n';
	}

	return l1_and_k(g_orig, g_copy, L, vis_data_orig, vis_data_copy); // step 4
}

// Step 2: find_connected_components
// Time:   O(n)
//
// Find the connected components of G and determine the cost of each one.
// If none has cost exceeding 2/3, construct the partition as described in the proof of Theorem 4.
// If some component has cost exceeding 2/3, go to Step 3.
Partition find_connected_components(GraphCR g_orig, Graph& g_copy)
{
	uint n = num_vertices(g_copy);
        //cout << "---------------------------- 2 - Find Connected Components --------\n";
        vertex_map idx;
        associative_property_map<vertex_map> vertid_to_component(idx);
        VertIter vit, vjt;
        tie(vit, vjt) = vertices(g_copy);
        for( uint i = 0; vit != vjt; ++vit, ++i ){
		//cout << "checking vertex number: " << i << ' ' << *vit << '\n';
	       	put(vertid_to_component, *vit, i);
	}
        uint num_components = connected_components(g_copy, vertid_to_component);

        //cout << "# of connected components: " << num_components << '\n';
        vector<uint> num_verts_per_component(num_components, 0);
        for( tie(vit, vjt) = vertices(g_copy); vit != vjt; ++vit ){
	       	++num_verts_per_component[vertid_to_component[*vit]];
	}
        uint biggest_component_index = 0;
        uint biggest_size            = 0;
        bool bigger_than_two_thirds  = false;
        for( uint i = 0; i < num_components; ++i ){
                if( 3*num_verts_per_component[i] > 2*num_vertices(g_copy) ){
                        //cout << "component " << i << " is bigger than two thirds of the entire graph\n";
                        bigger_than_two_thirds = true;
                }
                if( num_verts_per_component[i] > biggest_size ){
                        biggest_size = num_verts_per_component[i];
                        biggest_component_index = i;
                }
        }

        if( !bigger_than_two_thirds ){
		cout << "exiting early through theorem 4 - no component has cost exceeding two thirds\n";
		// connected graphs can never get here because they would have a single component with total cost > 2/3
                Partition defp;
		return theorem4_disconnected(g_copy, n, num_components, vertid_to_component, num_verts_per_component, defp);
        }
        cout << "index of biggest component: " << biggest_component_index << '\n';

	return bfs_and_levels(g_orig, g_copy); // step 3
}

// Step 1: check_planarity
// Time:   big-theta(n)
//
// Find a planar embedding of G and construct a representation for it of the kind described above.
Partition lipton_tarjan_separator(GraphCR g_orig)
{
	Graph g_copy(g_orig);

	//cout << "@#$original g:\n";
	print_graph(g_orig);
	//cout << "@#$g_copy:\n";
	//print_graph2(g_copy);

        /*auto prop_map = get(vertex_index, g_copy);
        VertIter vi, vend;
        for( tie(vi, vend) = vertices(g_copy); vi != vend; ++vi ){
                cout << "vert#: " << prop_map[*vi] << '\n';
        }*/

	/*cout << "---------------------------- 0 - Printing Edges -------------------\n";
	cout << "edges of g:\n";*/
	//cout << "edges of g_copy:\n" << std::endl;

        cout << "---------------------------- 1 - Check Planarity  ------------\n";
        EmbedStruct em(&g_copy);
        if( !em.test_planar() ) throw NotPlanarException();

	return find_connected_components(g_orig, g_copy);
}