using Statistics

include("NodePool.jl")

MSE(v1, v2) = mean((v1.-v2).^2);

function createVariableValues(inputs::Array{<:Real,2}; dataInRows=true)
    if (dataInRows)
        inputValues = [inputs[:,numInput] for numInput in 1:size(inputs,2)];
    else
        inputValues = [inputs[numInput,:] for numInput in 1:size(inputs,1)];
    end;
    return inputValues;
end;

mutable struct DoME
    tree::Tree
    mse::Real
    goalMSE::Real
    nodePoolVariables::NodePool
    inputs::Array{<:Real,2}
    targets::Array{<:Real,1}
    initialEquation::NodeEquation
    minimumReductionMSE::Real
    maximumHeight::Number
    maximumNodes::Number
    strategy::Function
    checkForErrors::Bool
    function DoME(inputs::Array{<:Real,2}, targets::Array{<:Real,1};
        dataInRows=true ,
        maximumNodes = Inf ,
        # initialTreeOrder = 1 ,
        checkForErrors = false
    )

        @assert(!any(isnan.(inputs)));
        @assert(!any(isinf.(inputs)));
        @assert(!anyNaN(targets));
        @assert(!anyInf(targets));

        @assert(eltype(inputs)==eltype(targets));
        @assert(eltype(inputs)==Float32 || eltype(inputs)==Float64);

        nodePoolVariables = NodePool();

        if (dataInRows)
            @assert (length(targets)==size(inputs,1))
        else
            @assert (length(targets)==size(inputs,2))
        end;

        # Create variables, add them to the node pool and to the prototypes
        variableValues = createVariableValues(inputs; dataInRows=dataInRows);
        for numVariable in 1:length(variableValues)
            # if (length(unique(variableValues[numVariable]))>1)
            if (!allequal(variableValues[numVariable]))
                nodeVariable = Variable(UInt(numVariable); semantic=variableValues[numVariable]);
                addNodeToPool!(nodePoolVariables, nodeVariable, UInt(1), UInt(1));
            end;
        end;
        if (length(nodePoolVariables.nodes)==0)
            error("No variables have been added");
        end;

        # Build initial tree
        tree = Constant(mean(targets));

        mse = MSE(targets, evaluateTree(tree));

        initialEquation = NodeEquation(eltype(inputs)(1.), targets, eltype(inputs)(0.), eltype(inputs)(-1.));

        maximumHeight = Inf;
        strategy(obj::DoME) = PerformSearches!(obj; whichNodesPerformConstantSearch=Any, whichNodesPerformVariableSearch=Any, whichNodesPerformConstantVariableSearch=Any, performConstantExpressionSearch=true);

        return new(tree, mse, eltype(inputs)(0), nodePoolVariables, inputs, targets, initialEquation, eltype(inputs)(1e-6), maximumHeight, maximumNodes, strategy, checkForErrors);
    end;
end;

#################################################################################################################################################################
#
# The four searches and the function to optimize constants
#

function constantSearch(nodes::Array{Tree,1}, mse::Real, numNodes::Array{UInt,1}, whichNodesPerformConstantSearch::Union{DataType,Union}; checkForErrors=false)
    bestReductionMSE = -Inf;
    bestNumNodes = -Inf;
    constantBest = -Inf;
    numNodeBest = -1;
    for numNode in 2:length(nodes)
        node = nodes[numNode];
        if isa(node, whichNodesPerformConstantSearch) && !isNaN(node.equation.S)
            (constant, reduction) = calculateConstantMinimizeEquation(node.equation, mse; checkForErrors=checkForErrors);
            if (reduction>bestReductionMSE)
                constantBest = constant;
                bestReductionMSE = reduction;
                numNodeBest = numNode;
            end;
        end;
    end;
    return (bestReductionMSE, numNodeBest, constantBest);
end;

function variableSearch(nodes::Array{Tree,1}, nodePoolVariables::NodePool, mse::Real, whichNodesPerformVariableSearch::Union{DataType,Union}; checkForErrors=false)

    maxReductionMSE = -Inf;
    indexNodePoolMaxReductionMSE = nothing;
    numNodeMaxReductionMSE = -1;

    if (checkForErrors)
        @assert(unique(nodePoolVariables.heights)==[1]);
        @assert(unique(nodePoolVariables.numNodes)==[1]);
        @assert(unique(isa.(nodePoolVariables.nodes,Variable))==[true]);
    end;

    for numNode in 1:length(nodes)

        node = nodes[numNode];

        if isa(node, whichNodesPerformVariableSearch) && !isNaN(node.equation.S)

            for numVar in 1:length(nodePoolVariables.nodes)

                # If looking into the variables of the tree, do not look into the same variables
                isa(node,Variable) && (node.variableNumber==nodePoolVariables.nodes[numVar].variableNumber) && continue;

                semantic = nodePoolVariables.semantics[numVar];

                # If the semantic of the variable is not in the domain, do not check it
                semanticNotInDomain(semantic, node.equation) && continue;

                reduction = calculateMSEReduction(semantic, node.equation, mse; checkForErrors=checkForErrors);
                if (reduction>maxReductionMSE)
                    maxReductionMSE = reduction;
                    indexNodePoolMaxReductionMSE = numVar;
                    numNodeMaxReductionMSE = numNode;
                end;
            end;
        end;
    end;

    return (maxReductionMSE, numNodeMaxReductionMSE, indexNodePoolMaxReductionMSE);

end;

function constantVariableSearch(nodes::Array{Tree,1}, nodePool::NodePool, mse::Real, maximumHeights, maximumNodes, whichNodesPerformConstantVariableSearch::Union{DataType,Union}; checkForErrors=false)

    maxReductionMSE = -Inf;
    operationMaxReductionMSE = nothing;
    constantMaxReductionMSE = -Inf;
    variableMaxReductionMSE = nothing;
    numNodeMaxReductionMSE = -1;

    for numNode in 1:length(nodes)

        node = nodes[numNode];

        if isa(node, whichNodesPerformConstantVariableSearch) && !isNaN(node.equation.S)

            # (a,b,c,d) = node.equation;
            eq=node.equation; a=eq.a; b=eq.b; c=eq.c; d=eq.d; S=eq.S;

            for numVar = findall(((nodePool.numNodes.+2).<=maximumNodes[numNode]) .& (nodePool.heights.<=(maximumHeights[numNode]-1)));

                semantic = nodePool.nodes[numVar].semantic;

                if ( (!isa(node,BinaryNode)) || (node.name!="+") || (!isa(node.child1,Constant)) || (!isa(node.child2,Variable)) || (node.child2.variableNumber!=numVar) )
                    equationConstant = equationAddChild1(semantic, a, b, c, d, S);
                    if (checkForErrors) checkEquation(equationConstant); end;
                    # If there are possible values in the domain
                    if (!isNaN(equationConstant.S))
                        (constant, reductionMSE) = calculateConstantMinimizeEquation(equationConstant, mse; checkForErrors=checkForErrors);
                        if (reductionMSE>maxReductionMSE)
                            maxReductionMSE = reductionMSE;
                            constantMaxReductionMSE = constant;
                            operationMaxReductionMSE = AddNode;
                            numNodeMaxReductionMSE = numNode;
                            variableMaxReductionMSE = nodePool.nodes[numVar];
                        end;
                    end;
                end;

                if ( (!isa(node,BinaryNode)) || (node.name!="-") || (!isa(node.child1,Constant)) || (!isa(node.child2,Variable)) || (node.child2.variableNumber!=numVar) )
                    equationConstant = equationSubChild1(semantic, a, b, c, d, S);
                    # b2 = cleanEquation(b2);
                    # d2 = cleanEquation(d2);
                    if (checkForErrors) checkEquation(equationConstant); end;
                    # If there are possible values in the domain
                    if (!isNaN(equationConstant.S))
                        (constant, reductionMSE) = calculateConstantMinimizeEquation(equationConstant, mse; checkForErrors=checkForErrors);
                        if (reductionMSE>maxReductionMSE)
                            maxReductionMSE = reductionMSE;
                            constantMaxReductionMSE = constant;
                            operationMaxReductionMSE = SubNode;
                            numNodeMaxReductionMSE = numNode;
                            variableMaxReductionMSE = nodePool.nodes[numVar];
                        end;
                    end;
                end;

                if ( (!isa(node,BinaryNode)) || (node.name!="*") || (!isa(node.child1,Constant)) || (!isa(node.child2,Variable)) || (node.child2.variableNumber!=numVar) )
                    equationConstant = equationMulChild1(semantic, a, b, c, d, S);
                    if (checkForErrors) checkEquation(equationConstant) end;
                    # If there are possible values in the domain
                    if (!isNaN(equationConstant.S))
                        (constant, reductionMSE) = calculateConstantMinimizeEquation(equationConstant, mse; checkForErrors=checkForErrors);
                        if (reductionMSE>maxReductionMSE)
                            maxReductionMSE = reductionMSE;
                            constantMaxReductionMSE = constant;
                            operationMaxReductionMSE = MulNode;
                            numNodeMaxReductionMSE = numNode;
                            variableMaxReductionMSE = nodePool.nodes[numVar];
                        end;
                    end;
                end;

                if ( (!isa(node,BinaryNode)) || (node.name!="/") || (!isa(node.child1,Constant)) || (!isa(node.child2,Variable)) || (node.child2.variableNumber!=numVar) )
                    if !(nodePool.hasZerosInSemantics[numVar])
                        equationConstant = equationDivChild1(semantic, a, b, c, d, S);
                        # b2 = cleanEquation(b2);
                        # d2 = cleanEquation(d2);
                        if (checkForErrors) checkEquation(equationConstant); end;
                        # If there are possible values in the domain
                        if (!isNaN(equationConstant.S))
                            (constant, reductionMSE) = calculateConstantMinimizeEquation(equationConstant, mse; checkForErrors=checkForErrors);
                            if (reductionMSE>maxReductionMSE)
                                maxReductionMSE = reductionMSE;
                                constantMaxReductionMSE = constant;
                                operationMaxReductionMSE = DivNode;
                                numNodeMaxReductionMSE = numNode;
                                variableMaxReductionMSE = nodePool.nodes[numVar];
                            end;
                        end;
                    end;
                end;

            end;
        end;
    end;

    return (maxReductionMSE, numNodeMaxReductionMSE, operationMaxReductionMSE, constantMaxReductionMSE, variableMaxReductionMSE);
end;

function constantExpressionSearch(nodes::Array{Tree,1}, mse::Real, heights, maximumHeights, numNodes, maximumNodes; checkForErrors=false)

    maxReductionMSE = -Inf;
    subTree = nothing;
    operationMaxReductionMSE = nothing;
    constantMaxReductionMSE = -Inf;
    numNodeMaxReductionMSE = -1;

    # Search only on non terminal nodes
    for numNode in findall(isa.(nodes,NonTerminal) .& (heights.<maximumHeights) .& ((numNodes.+2).<=maximumNodes))
        node = nodes[numNode];
        semantic = node.semantic;
        eq=node.equation; a=eq.a; b=eq.b; c=eq.c; d=eq.d; S=eq.S;

        if !isNaN(S)

            if ( ((node.name!="+") && (node.name!="-")) || ((!isa(node.child1,Constant)) || (!isa(node.child2,Constant))) )
                equationConstant = equationAddChild1(node.semantic, a, b, c, d, S);
                if (checkForErrors) checkEquation(equationConstant); end;
                # If there are possible values in the domain
                if (!isNaN(equationConstant.S))
                    (constant, reductionMSE) = calculateConstantMinimizeEquation(equationConstant, mse; checkForErrors=checkForErrors);
                    if (reductionMSE>maxReductionMSE)
                        maxReductionMSE = reductionMSE;
                        constantMaxReductionMSE = constant;
                        operationMaxReductionMSE = AddNode;
                        numNodeMaxReductionMSE = numNode;
                    end;
                end;
            end

            if ( ((node.name!="*") && (node.name!="/")) || ((!isa(node.child1,Constant)) || (!isa(node.child2,Constant))) )
                equationConstant = equationMulChild1(node.semantic, a, b, c, d, S);
                if (checkForErrors) checkEquation(equationConstant); end;
                # If there are possible values in the domain
                if (!isNaN(equationConstant.S))
                    (constant, reductionMSE) = calculateConstantMinimizeEquation(equationConstant, mse; checkForErrors=checkForErrors);
                    if (reductionMSE>maxReductionMSE)
                        maxReductionMSE = reductionMSE;
                        constantMaxReductionMSE = constant;
                        operationMaxReductionMSE = MulNode;
                        numNodeMaxReductionMSE = numNode;
                    end;
                end;
            end;

        end;
    end;

    return (maxReductionMSE, numNodeMaxReductionMSE, operationMaxReductionMSE, constantMaxReductionMSE);
end;

function OptimizeConstants!(obj::DoME)

    nodes, = iterateTree(obj.tree);

    constantNodes = [isa(node,Constant) for node in nodes];
    nodes = nodes[constantNodes];
    isempty(nodes) && return nothing;
    pathsToConstantNodes = findNode.([obj.tree], nodes);

    if (length(nodes)==1)
        calculateEquations!(obj.tree, obj.initialEquation, path=pathsToConstantNodes[1]; checkForErrors=obj.checkForErrors);
        node = nodes[1];
        minimumReduction = obj.minimumReductionMSE * obj.mse;
        (constant, reduction) = calculateConstantMinimizeEquation(node.equation, obj.mse; checkForErrors=obj.checkForErrors);
        clearEquations!(obj.tree);
        if (reduction>minimumReduction) && (constant!=nodes[1].semantic)
            node.semantic = constant;
            mse = MSE(obj.targets, reevaluatePath(obj.tree, pathsToConstantNodes[1]; checkForErrors=obj.checkForErrors));
            if (obj.checkForErrors)
                @assert(isa(node, Constant))
                @assert(!isnan(mse));
                @assert(equal(mse+reduction, obj.mse));
            end;
            obj.mse = mse;
        end;
        return nothing;
    end;

    numNode = 0;
    numIterationsWithNoImprovement = 0;
    while (true)

        numNode = (numNode>=length(nodes)) ? 1 : numNode+1;

        calculateEquations!(obj.tree, obj.initialEquation; path=pathsToConstantNodes[numNode], checkForErrors=obj.checkForErrors)

        if (obj.checkForErrors)
            checkEquations(obj.tree, obj.mse);
        end;

        minimumReduction = obj.minimumReductionMSE * obj.mse;
        (constant, reduction) = calculateConstantMinimizeEquation(nodes[numNode].equation, obj.mse; checkForErrors=obj.checkForErrors);

        clearEquations!(obj.tree);
        if (reduction>minimumReduction) && (constant!=nodes[numNode].semantic)

            oldConstant = nodes[numNode].semantic;
            nodes[numNode].semantic = constant;

            mse = MSE(obj.targets, reevaluatePath(obj.tree, pathsToConstantNodes[numNode]; checkForErrors=obj.checkForErrors));

            # Check that the resulting MSE is lower than the previous MSE in obj.minimumReductionMSE
            # This is expected to be. However, precision errors may happen
            if (mse >= (obj.mse - (obj.minimumReductionMSE * obj.mse)))

                # Undo the change in the tree
                nodes[numNode].semantic = oldConstant;
                mse = MSE(obj.targets, reevaluatePath(obj.tree, pathsToConstantNodes[numNode]; checkForErrors=obj.checkForErrors));
                obj.checkForErrors && @assert(mse==obj.mse);
                numIterationsWithNoImprovement += 1;
                if (numIterationsWithNoImprovement>=length(nodes))
                    return;
                end;

            else

                if (obj.checkForErrors)
                    @assert(isa(nodes[numNode], Constant))
                    @assert(!isnan(mse));
                    @assert(equal(mse+reduction, obj.mse));
                end;
                obj.mse = mse;
                numIterationsWithNoImprovement = 0;

            end;
        else
            numIterationsWithNoImprovement += 1;
            if (numIterationsWithNoImprovement>=length(nodes))
                return;
            end;
        end;
    end;

end;

#################################################################################################################################################################
#
# The function that performs the searches, called by a strategy
#

function PerformSearches!(obj::DoME;
    whichNodesPerformConstantSearch        ::Union{DataType,Union} = Nothing ,
    whichNodesPerformVariableSearch        ::Union{DataType,Union} = Nothing ,
    whichNodesPerformConstantVariableSearch::Union{DataType,Union} = Nothing ,
    performConstantExpressionSearch::Bool = false)

    if (obj.mse<=obj.goalMSE)
        return false;
    end;

    (nodes, heights, depths, _, numNodesEach) = iterateTree(obj.tree);

    # If the equations of the nodes were already calculated (the last search was unsuccessful), do not calculate them again
    if (obj.tree.equation==nothing)
        calculateEquations!(obj.tree, obj.initialEquation)
    end;

    if (obj.checkForErrors)
        checkEquations(obj.tree, obj.mse);
        # The equation must be correct: none of the semantic sets must be NaN (meaning that there are possible values in the domain)
        for node in nodes
            @assert(!isNaN(node.equation.S))
        end;
    end;

    subTreeInsert = nothing;
    indexNode = -1;
    maximumReduction = -Inf;
    maximumReduction = obj.minimumReductionMSE * obj.mse;

    #########################################################################################
    # Constant search
    if (whichNodesPerformConstantSearch!=Nothing)
        # Check if a constant can be inserted anywhere in the tree
        (maxReductionMSEConstantSearch, numNodeMaxReductionMSEConstantSearch, constantMaxReductionMSEConstantSearch) = constantSearch(nodes, obj.mse, numNodesEach, whichNodesPerformConstantSearch; checkForErrors=obj.checkForErrors);
        # if (maxReductionMSEConstantSearch>maximumReduction) && (maxReductionMSEConstantSearch>(obj.mse*obj.minimumDivReductionMSEConstantSearch))
        if (maxReductionMSEConstantSearch>maximumReduction)
            maximumReduction = maxReductionMSEConstantSearch;
            indexNode = numNodeMaxReductionMSEConstantSearch;
            # A subtree has been found
            subTreeInsert = Constant(constantMaxReductionMSEConstantSearch);
        end;

    end;

    #########################################################################################
    # Variable search
    if (whichNodesPerformVariableSearch!=Nothing)
        (maxReductionVariableSearch, indexNodeVariableSearch, indexNodePoolVariableSearch) = variableSearch(nodes, obj.nodePoolVariables, obj.mse, whichNodesPerformVariableSearch; checkForErrors=obj.checkForErrors);
        if (maxReductionVariableSearch>maximumReduction)
            maximumReduction = maxReductionVariableSearch;
            indexNode = indexNodeVariableSearch;
            # It is necessary to clone the variables; otherwise, they will be the same node in different parts of the tree, and will share the equation
            subTreeInsert = clone(obj.nodePoolVariables.nodes[indexNodePoolVariableSearch]);
        end;
    end;

    if (whichNodesPerformConstantVariableSearch!=Nothing) || (performConstantExpressionSearch)
        maximumHeightSubTree = (obj.maximumHeight+1).-depths;
        maximumNodesSubTree = (obj.maximumNodes - numNodesEach[1]) .+ numNodesEach;
    end;

    #########################################################################################
    # Constant-Variable search
    if (whichNodesPerformConstantVariableSearch!=Nothing)
        (maxReductionMSEConstantVariableSearch, numNodeMaxReductionMSEConstantVariableSearch, operationConstantVariableSearch, constantMaxReductionMSEConstantVariableSearch, variableMaxReductionMSEConstantVariableSearch) = constantVariableSearch(nodes, obj.nodePoolVariables, obj.mse, maximumHeightSubTree, maximumNodesSubTree, whichNodesPerformConstantVariableSearch; checkForErrors=obj.checkForErrors);
        if (maxReductionMSEConstantVariableSearch>maximumReduction)
            maximumReduction = maxReductionMSEConstantVariableSearch;
            indexNode = numNodeMaxReductionMSEConstantVariableSearch;
            # A subtree has been found
            subTreeInsert = operationConstantVariableSearch(Constant(constantMaxReductionMSEConstantVariableSearch), clone(variableMaxReductionMSEConstantVariableSearch));
        end;
    end;

    #########################################################################################
    # Constant-Expression search
    if (performConstantExpressionSearch)
        (maxReductionMSEConstantExpressionSearch, numNodeMaxReductionMSEConstantExpressionSearch, operationConstantExpressionSearch, constantMaxReductionMSEConstantExpressionSearch) = constantExpressionSearch(nodes, obj.mse, heights, maximumHeightSubTree, numNodesEach, maximumNodesSubTree; checkForErrors=obj.checkForErrors);
        if (maxReductionMSEConstantExpressionSearch>maximumReduction)
            maximumReduction = maxReductionMSEConstantExpressionSearch;
            indexNode = numNodeMaxReductionMSEConstantExpressionSearch;
            # A subtree has been found
            subTreeInsert = operationConstantExpressionSearch(Constant(constantMaxReductionMSEConstantExpressionSearch), nodes[indexNode]);
        end;
    end;

    # Do not clear equations if the search was unsuccessful: they can be used for the next search

    if (subTreeInsert==nothing)
        return false;
    end;

    # A subtree that can be inserted (whether it is constant, variable or a subtree) has been found

    clearEquations!(obj.tree);

    if (indexNode==1)
        obj.tree = subTreeInsert;
    else
        evaluateTree(subTreeInsert; checkForErrors=obj.checkForErrors);
        replaceSubtree!(obj.tree, nodes[indexNode], subTreeInsert);
    end;

    mse = MSE(obj.targets, evaluateTree(obj.tree; checkForErrors=obj.checkForErrors));

    # Check that the resulting MSE is lower than the previous MSE in obj.minimumReductionMSE
    # This is expected to be. However, precision errors may happen
    if (mse >= (obj.mse - (obj.minimumReductionMSE * obj.mse)))
        # Undo the change in the tree
        if (indexNode==1)
            obj.tree = nodes[1];
        else
            replaceSubtree!(obj.tree, subTreeInsert, nodes[indexNode]);
        end;
        mse = MSE(obj.targets, evaluateTree(obj.tree; checkForErrors=obj.checkForErrors));
        obj.checkForErrors && @assert(mse==obj.mse);
        return false;
    end;

    if (obj.checkForErrors)
        @assert(!isnan(mse));
        @assert(!isinf(mse));
        @assert(equal(mse+maximumReduction, obj.mse));
        if (!isinf(obj.maximumHeight))
            @assert (height(obj.tree)<=obj.maximumHeight);
        end;
        if (!isinf(obj.maximumNodes))
            @assert (numNodes(obj.tree)<=obj.maximumNodes);
        end;
        checkIntegrity(obj.nodePoolVariables);
    end;
    obj.mse = mse;

    return true;
end;

##################################################################################################
#
# Strategies
#    Return true if change has been done or false otherwise
#

function StrategyExhaustive(obj::DoME)
    changeDone = PerformSearches!(obj;
        whichNodesPerformConstantSearch=Any ,
        whichNodesPerformVariableSearch=Any ,
        whichNodesPerformConstantVariableSearch=Any ,
        performConstantExpressionSearch=true);
    return changeDone;
end;

function StrategyExhaustiveWithConstantOptimization(obj::DoME)
    changeDone = PerformSearches!(obj;
        whichNodesPerformConstantSearch=Union{Variable,NonTerminal} ,
        whichNodesPerformVariableSearch=Any ,
        whichNodesPerformConstantVariableSearch=Any ,
        performConstantExpressionSearch=true);
    changeDone && OptimizeConstants!(obj);
    return changeDone;
end;

function StrategySelectiveWithConstantOptimization(obj::DoME)
    # Variable search only on constants
    changeDone =               PerformSearches!(obj; whichNodesPerformVariableSearch=Constant);
    changeDone = changeDone || PerformSearches!(obj; performConstantExpressionSearch=true);
    # Constant-variable search only on terminals
    changeDone = changeDone || PerformSearches!(obj; whichNodesPerformConstantVariableSearch=Union{Constant,Variable});
    if (!changeDone)
        # Constant search on variables and non-terminals, variable seach on variables and non-terminals, and constant-variable search on non-terminals
        changeDone = PerformSearches!(obj;
            whichNodesPerformConstantSearch=Union{Variable,NonTerminal} ,
            whichNodesPerformVariableSearch=Union{Variable,NonTerminal} ,
            whichNodesPerformConstantVariableSearch=NonTerminal );
    end;
    changeDone && OptimizeConstants!(obj);
    return changeDone;
end;

function StrategySelective(obj::DoME)
    changeDone =               PerformSearches!(obj; whichNodesPerformConstantSearch=Constant);
    # Variable search only on constants
    changeDone = changeDone || PerformSearches!(obj; whichNodesPerformVariableSearch=Constant);
    changeDone = changeDone || PerformSearches!(obj; performConstantExpressionSearch=true);
    # Constant-variable search only on terminals
    changeDone = changeDone || PerformSearches!(obj; whichNodesPerformConstantVariableSearch=Union{Constant,Variable});
    if (!changeDone)
        # Constant search on variables and non-terminals, variable seach on variables and non-terminals, and constant-variable search on non-terminals
        changeDone = PerformSearches!(obj;
            whichNodesPerformConstantSearch=Union{Variable,NonTerminal} ,
            whichNodesPerformVariableSearch=Union{Variable,NonTerminal} ,
            whichNodesPerformConstantVariableSearch=NonTerminal );
    end;
    return changeDone;
end;

Strategy1 = StrategyExhaustive;
Strategy2 = StrategyExhaustiveWithConstantOptimization;
Strategy3 = StrategySelectiveWithConstantOptimization;
Strategy4 = StrategySelective;

#################################################################################################################################################################
#
# The function Step!, that performs one iteration of the algorithm
#

function Step!(obj::DoME)
    obj.strategy(obj) && return true;
    # No change was done in the first tree: build different initial trees with a higher order
    if isa(obj.tree, Constant)
        constantValue = obj.tree.semantic;
        numNodesPreviousTree = 0;
        numNodesThisTree = numNodes(obj.tree);
        order = 0;
        while (numNodesPreviousTree!=numNodesThisTree)
            order = order + 1;
            obj.tree = buildInitialTree(order, convert(Array{Variable,1},obj.nodePoolVariables.nodes), constantValue, obj.maximumNodes);
            obj.mse = MSE(obj.targets, evaluateTree(obj.tree));
            obj.strategy(obj) && return true;
            numNodesPreviousTree = numNodesThisTree;
            numNodesThisTree = numNodes(obj.tree);
        end;
    end;
    return false;
end;

#################################################################################################################################################################
#
# The function is an interface that creates the DoME object and calls the function Step! on each iteration
#

function dome(inputs::Array{<:Real,2}, targets::Array{<:Real,1};
    # Each instance in inputs is in a row or in a column
    dataInRows          ::Bool           = true,
    # Hyperparameters of the algorithm
    minimumReductionMSE ::Real           = 1e-6,
    maximumNodes        ::Int64          = 50 ,
    strategy            ::Function       = StrategyExhaustive ,
    # Other hyperparameter that the user might find useful
    maximumHeight       ::Real           = Inf ,
    # Stopping criteria
    goalMSE             ::Real           = 0 ,
    maxIterations       ::Real           = Inf ,
    # Indices of the instances used for validation and test (validation has not been tested and may fail)
    validationIndices   ::Array{Int64,1} = Int64[],
    testIndices         ::Array{Int64,1} = Int64[],
    # Validation and test ratios (these parameters have not been tested and may fail)
    validationRatio     ::Real           = 0. ,
    testRatio           ::Real           = 0. ,
    # If you want to see the iterations on screen. This makes the execution slower
    showText            ::Bool           = false ,
    # This parameter was used only for development. If it is set to true, the execution becomes much slower
    checkForErrors      ::Bool           = false
    )

    if (dataInRows)
        @assert (length(targets)==size(inputs,1))
    else
        @assert (length(targets)==size(inputs,2))
    end;

    @assert(isempty(validationIndices) | (validationRatio==0.))
    @assert(isempty(testIndices) | (testRatio==0.))
    @assert(validationRatio+testRatio<1.);
    if ((!isempty(validationIndices)) & (!isempty(testIndices)))
        @assert(isempty(intersect(validationIndices,testIndices)));
    end;

    # Hold-out
    randomIndices = randperm(length(targets));
    if (isempty(validationIndices))
        numValidation = Int(round(length(targets)*validationRatio));
        validationIndices = randomIndices[1:numValidation];
        indices = sort(randomIndices[(numValidation+1):end]);
    else
        numValidation = length(validationIndices);
        randomIndices = setdiff(randomIndices, validationIndices);
    end;
    if (isempty(testIndices))
        numTest = Int(round(length(targets)*testRatio));
        testIndices = sort(randomIndices[1:numTest]);
        randomIndices = randomIndices[(numTest+1):end];
    else
        numTest = length(testIndices);
        randomIndices = setdiff(randomIndices, testIndices);
    end;
    numTraining = length(targets) - numValidation - numTest;
    trainingIndices = sort(randomIndices);
    @assert(numTraining == length(trainingIndices));
    @assert(isempty(intersect(trainingIndices,validationIndices)) & isempty(intersect(trainingIndices,testIndices)) & isempty(intersect(validationIndices,testIndices)));
    @assert(!isempty(trainingIndices));

    # Create variable values for validation and test sets
    TrainingVariableValues   = createVariableValues(dataInRows ? inputs[trainingIndices,:]   : inputs[:,trainingIndices];   dataInRows=dataInRows);
    ValidationVariableValues = createVariableValues(dataInRows ? inputs[validationIndices,:] : inputs[:,validationIndices]; dataInRows=dataInRows);
    TestVariableValues       = createVariableValues(dataInRows ? inputs[testIndices,:]       : inputs[:,testIndices];       dataInRows=dataInRows);

    sr = DoME(dataInRows ? inputs[trainingIndices,:] : inputs[:,trainingIndices], targets[trainingIndices]; dataInRows=dataInRows, maximumNodes=maximumNodes)

    sr.minimumReductionMSE = eltype(inputs)(minimumReductionMSE);
    sr.maximumHeight = maximumHeight;
    sr.checkForErrors = checkForErrors;
    sr.goalMSE = eltype(inputs)(goalMSE);
    sr.strategy = strategy;

    iteration = 0;

    BestTrainingMSE = Inf; BestValidationMSE = Inf; BestTestMSE = Inf; BestExpression = ""; BestTree =  nothing;

    function evaluateTreeIndices(tree, indices, variableValues)
        setVariableValues!(tree, variableValues);
        outputs = evaluateTree(tree);
        return MSE(targets[indices], outputs);
    end;

    function evaluateIteration()
        if (showText)
            println(" Iteration ", iteration);
            println(" Expression: ", string(sr.tree));
            println("   Nuber of nodes: ", numNodes(sr.tree), " - Height: ", height(sr.tree));
            println("   Current training:   MSE: ", sr.mse);
        end;

        # Perform validation and test of the expression, only if text is shown on screen (to make the algorithm faster)
        if (!isempty(testIndices) || !isempty(validationIndices)) && showText
            treeCopy = clone(sr.tree);

            # If there is validation set, the expression improves if it improves in the validation set
            if !isempty(validationIndices)
                mseValidation = evaluateTreeIndices(treeCopy, validationIndices, ValidationVariableValues);
                if (mseValidation<BestValidationMSE)
                    BestValidationMSE = mseValidation;
                    improves = true;
                else
                    improves = false;
                end;
            else
                # If there is no validation set, the expression is always improved
                improves = true;
            end;

            # If this expression is improved, calculate the result on the test set
            if improves
                BestTree = treeCopy;
                BestTrainingMSE = sr.mse;
                BestTestMSE = evaluateTreeIndices(treeCopy, testIndices, TestVariableValues);
            end;

            if showText
                # Print the overall results
                if !isempty(validationIndices)
                    println("   Overall training:   MSE: ", BestTrainingMSE);
                    println("           validation: MSE: ", BestValidationMSE);
                end;
                println("           test:       MSE: ", BestTestMSE);
                println("           Nuber of nodes: ", numNodes(BestTree), " - Height: ", height(BestTree));
                # clearEquations!(sr.tree); calculateEquations!(sr.tree, sr.initialEquation); println("   Max memory usage: ", Base.summarysize(sr)/(2^20), " MiB"); clearEquations!(sr.tree); # <- missing memory of the equations in each node
            end;
        end;
        if showText
            println("-------------------------------------------------------------------------------------")
        end;
    end;

    # Evaluate iteration 0
    evaluateIteration();

    stoppingCriteria = false;
    while !stoppingCriteria

        stoppingCriteria = !Step!(sr);

        if !stoppingCriteria

            iteration += 1;

            # Evaluate each iteration
            evaluateIteration();

            # Stopping criteria
            if (sr.mse<=goalMSE) || (iteration>=maxIterations)
                stoppingCriteria = true;
            end;

        end;

    end;

    if BestTree==nothing
        BestTree = sr.tree;
        BestTrainingMSE = sr.mse;
        BestValidationMSE = evaluateTreeIndices(sr.tree, validationIndices, ValidationVariableValues);
        BestTestMSE = evaluateTreeIndices(sr.tree, testIndices, TestVariableValues);
    end;

    if showText
        println("Best expression found: ", string(BestTree));
    end;

    return (BestTrainingMSE, BestValidationMSE, BestTestMSE, BestTree);

end;


export AffineChartHomotopy, on_affine_chart

"""
    AffineChartHomotopy(H::AbstractHomotopy, v::PVector{T,N})

Given a homotopy ``H(x,t): (ℙ^{m_1} × ⋯ × ℙ^{m_N}) × ℂ → ℂⁿ`` this creates a new affine
homotopy ``H̄`` which operates on the affine chart defined by the vector
``v ∈ ℙ^{m_1} × ⋯ × ℙ^{m_N}`` and the augmented conditions ``vᵀx = 1``.
"""
struct AffineChartHomotopy{H<:AbstractHomotopy,N} <: AbstractHomotopy
    homotopy::H
    chart::PVector{ComplexF64,N}
end

"""
    on_affine_chart(H::Union{Homotopy,AbstractHomotopy}, proj_dims)

Construct an `AffineChartHomotopy` on a randomly generated chart `v`. Each entry is drawn
idepdently from a univariate normal distribution.
"""
function on_affine_chart(H::Homotopy, proj_dims = nothing)
    on_affine_chart(ModelKitHomotopy(H), proj_dims)
end
function on_affine_chart(H::AbstractHomotopy, proj_dims = nothing)
    if proj_dims === nothing
        proj_dims = [size(H, 2) - 1]
    end
    N = length(proj_dims)
    chart = PVector(randn(ComplexF64, sum(proj_dims) + N), tuple(proj_dims...))
    AffineChartHomotopy(H, chart)
end

function Base.size(H::AffineChartHomotopy{<:AbstractHomotopy,N}) where {N}
    m, n = size(H.homotopy)
    (m + N, n)
end

function set_solution!(x::AbstractVector, H::AffineChartHomotopy, y::AbstractVector, t)
    x .= y
    on_chart!(x, H.chart)
    x
end

start_parameters!(H::AffineChartHomotopy, p) = start_parameters!(H.homotopy, p)
target_parameters!(H::AffineChartHomotopy, p) = target_parameters!(H.homotopy, p)

function on_chart!(x::AbstractVector, v::PVector)
    ranges = dimension_indices(v)
    for range in ranges
        λ = zero(eltype(x))
        @inbounds for i in range
            λ += v[i] * x[i]
        end
        λ⁻¹ = @fastmath inv(λ)
        for i in range
            x[i] *= λ⁻¹
        end
    end
    x
end

function evaluate!(u, H::AffineChartHomotopy{<:Any,N}, x::AbstractVector, t) where {N}
    evaluate!(u, H.homotopy, x, t)
    evaluate_chart!(u, H.chart, x)
    u
end

function evaluate_and_jacobian!(
    u,
    U,
    H::AffineChartHomotopy{<:Any,N},
    x::AbstractVector,
    t,
) where {N}
    evaluate_and_jacobian!(u, U, H.homotopy, x, t)
    evaluate_chart!(u, H.chart, x)
    jacobian_chart!(U, H.chart, x)
    nothing
end

function taylor!(u, v::Val, H::AffineChartHomotopy, tx, t)
    u .= zero(eltype(u))
    taylor!(u, v, H.homotopy, tx, t)
    # affine chart part is always zero since it is an affine linear form
    u
end


using Juno
using BenchmarkTools

f = (x,y) -> x^2 + y
x = linspace(0.0, 10.0, 500);
Auw = f.(x, x')
A = f.(x, x') .% (2π);
unwrap!(A, (false, false), 1234)
@btime unwrap!($A)
@code_warntype(unwrap!(A))
Juno.@profiler unwrap!(A)

a = 6rand()
b = 6rand()
@btime find_period($a, $b)

f = (x) -> x^2
x = linspace(0.0, 20.0, 1e6);
B = f.(x) .% (2π);
Juno.@profiler unwrap(B)
@btime unwrap!($B)
p1 = TestPixel(0.1)
p2 = TestPixel(0.2)
p3 = TestPixel(0.3)
p4 = TestPixel(0.4)
p5 = TestPixel(0.5)
p6 = TestPixel(0.6)
p7 = TestPixel(0.7)
p8 = TestPixel(0.8)
merge_pixels!(p1, p2, 1)
merge_pixels!(p1, p3, 0)
merge_pixels!(p4, p5, 0)
merge_pixels!(p6, p7, 0)
merge_into_group!(p5, p7, 0)
merge_into_group!(p2, p7, 0)
merge_pixels!(p1, p8, 0)

p8.head.last


saivdr_permdctmtx(sz::Integer...) = saivdr_permdctmtx(sz)

saivdr_permdctmtx(sz::NTuple{1}) = permdctmtx(sz)

function saivdr_permdctmtx(sz::NTuple{2})
    T = Float64
    mtx = MDCDL.representationmatrix(x->dct(T.(x)), sz)

    # cls[1,1] = 1
    # cls[2,1] = 3
    # cls[1,2] = 4
    # cls[2,2] = 2
    cls = [ 1 4; 3 2 ]
    indexorders = map(CartesianIndices(sz)) do ci
        cls[(mod.(ci.I .- 1, 2) .+ 1)...]
    end
    permids = sortperm(vec(indexorders); alg=Base.DEFAULT_STABLE)

    @views vcat([ transpose(mtx[pi,:]) for pi in permids ]...)
end

# function saivdr_permdctmtx(sz::NTuple{3})
#     T = Float64
#     mtx = MDCDL.representationmatrix(x->dct(T.(x)), sz)
#
#     # cls[1,1,1] = 1
#     # cls[2,1,1] = 8
#     # cls[1,2,1] = 6
#     # cls[2,2,1] = 3
#     # cls[1,1,2] = 5
#     # cls[2,1,2] = 4
#     # cls[1,2,2] = 2
#     # cls[2,2,2] = 7
#     cls = cat([1 6; 8 3], [5 2; 4 7], dims=3)
#     indexorders = map(CartesianIndices(sz)) do ci
#         cls[(mod.(ci.I .- 1, 2) .+ 1)...]
#     end
#     permids = sortperm(vec(indexorders); alg=Base.DEFAULT_STABLE)
#
#     @views vcat([ transpose(mtx[pi,:]) for pi in permids ]...)
# end

function analyze_compatible(nsolt::AbstractNsolt{T,2}, px::AbstractMatrix, nBlocks::NTuple; kwargs...) where {T}
    ty = initialStep(nsolt, px; kwargs...)

    nShifts = fld.(size(px, 2), nBlocks)
    rotatedimsfcns = (t->rotatedimspv(t, nBlocks[1]), copy)
    exty = extendAtoms(nsolt, ty, nShifts, rotatedimsfcns; kwargs...)
    exty = rotatedimspv(exty, nBlocks[2])
    return shiftFilterSymmetry(nsolt, exty)
end

# function analyze_compatible(nsolt::AbstractNsolt{T,3}, px::AbstractMatrix, nBlocks::NTuple; kwargs...) where {T}
#     ty = initialStep(nsolt, px; kwargs...)
#
#     nShifts = fld.(size(px, 2), nBlocks)
#     rotatedimsfcns = (t->rotatedimspv(t, nShifts[1]), t->rotatedimspv(t, nShifts[2], copy)
#     exty = extendAtoms(nsolt, ty, nShifts, rotatedimsfcns; kwargs...)
#     exty = rotatedimspv(exty, nBlocks[3])
#     return shiftFilterSymmetry(nsolt, exty)
# end


"""
Package: DataWrangler

impute\\[!\\]([x], y; method = "loess", q)

Impute missing values in vector `y` either in-place or returning a copy of `y` with the imputed values.

# Parameters
- `x`: Optional vector containing the support for `y` (no missing value allowed)
- `y`: Vector of type Real with missing values to be imputed
- `method`: This parameter can take three valid values and defaults to "loess":
    - "loess": Runs loess with a window size of `q` on the dataset and interpolate/extrapolate results on the `missing` values.
    - "normal": Random imputation using a Normal empirical distribution based on the size of `q`.
    - "uniform": Random imputation using an Uniform empirical distribution based on the size of `q`.
- `q`: Number of closest vector values to the imputation to be considered, it defaults to 3*length(y)÷4).

# Description

The imputation replaces missing values either with loess, local random uniform or local random normal methods. When the vector's type is of Integers then a rounding is performed on the results.

# Returns

Nothing if the imputation is done in-place or the original vector `y` with all the missing values imputed.

# Examples
```julia
x = sort(rand(100));
y = Array{Union{Missing,Float64}}(undef,100);
y[:] = rand(100);
y[rand(1:100,10)] .= missing;

impute!(x,y)
```
"""
function impute!(y::AbstractVector{<:Union{Missing,T}};
                 method::String = "loess", q = 3*length(y)÷4) where  {T<:Real}

    impute!(T.(1:length(y)), y; method, q)

end

function impute!(x::AbstractVector{R},
                 y::AbstractVector{<:Union{Missing,T}};
                 method::String = "loess", q = 3*length(y)÷4) where  {R<:Real,T<:Real}

    @assert !any(ismissing, x) "No missing values allowed in `x`"
    @assert issorted(x) "x values must be sorted"
    @assert length(x) == length(y) "x and y must have the same length"

    @assert method ∈ ["loess","uniform","normal"]
    # @assert !(T <: Integer) "Vector with Integers cannot be updated in place, use `impute(x)` instead"

    impxi = findall(ismissing,y)
    impx = x[impxi]

    length(impx) == 0 && return nothing

    loq = loess(x,y; q)
    loqy = loq(x)

    if method == "loess"
        loess_impx = loq(impx)
        y[impxi] = T <: Integer ? round.(T,loess_impx) : loess_impx
    end

    if method == "normal"
        n = length(x)
        rnd = randn(n)
        for (i,(ix,ixi)) in enumerate(zip(impx,impxi))
            xv = @. abs(x-ix)
            qidx = sortperm(xv)[1:min(q,n)]

            smy = skipmissing(y[qidx].-loqy[qidx])
            μ = mean(smy)
            σ = std(smy)
            y[ixi] = T <: Integer ? round(T, μ + rnd[i] * σ + loqy[ixi]) : μ + rnd[i] * σ + loqy[ixi]
        end
    end

    if method == "uniform"
        n = length(x)
        for (i,(ixi,ix)) in enumerate(zip(impxi,impx))
            xv = @. abs(x-ix)
            qidx = sortperm(xv)[1:min(q,n)]

            m,M = extrema(skipmissing(y[qidx].-loqy[qidx]))
            y[ixi] = T <: Integer ? round(T,m+rand(1)[1]*(M-m)+loqy[ixi]) : m+rand(1)[1]*(M-m)+loqy[ixi]
        end
    end

end

"""
Package: DataWrangler

Check 'impute! for further information.
"""
function impute(y::AbstractVector{<:Union{Missing,T}};
                 method::String = "loess", q = 3*length(y)÷4) where  {T<:Real}

    impute(T.(1:length(y)),y; method, q)

end

function impute(x::AbstractVector{R},
                y::AbstractVector{<:Union{Missing,T}};
                method::String = "loess", q = 3*length(y)÷4) where  {R<:Real,T<:Real}

    P = any(ismissing,y) ? Union{Missing, Base.promote_op(/,T,T)} : Base.promote_op(/,T,T)
    cy = Vector{P}(undef,length(y))
    cy[:] = y
    impute!(x,cy; method, q)
    return cy

end


@testset "Sequence" begin
    𝒯 = Taylor(1)
    ℱ = Fourier(1, 1.0)
    𝒞 = Chebyshev(2)
    𝑇 = 𝒯 ⊗ ℱ ⊗ 𝒞
    𝕊 = 𝑇^1
    coeffs = vec([inv(prod((2, 2, 2) .^ abs.(α))) for α ∈ indices(𝑇)])
    a = Sequence(𝕊, coeffs)
    #
    @test space(a) == 𝕊
    @test coefficients(a) == coeffs
    @test order(a) == order(𝕊)
    @test order(a, 1) == order(𝕊, 1)
    @test_throws MethodError frequency(a)
    @test frequency(component(a, 1), 2) == frequency(𝑇, 2)
    @test firstindex(a) == RadiiPolynomial._firstindex(𝕊)
    @test lastindex(a) == RadiiPolynomial._lastindex(𝕊)
    @test length(a) == length(coeffs)
    @test size(a) == size(coeffs)
    @test iterate(a) == iterate(coeffs)
    @test iterate(a, 2) == iterate(coeffs, 2)
    @test eltype(a) == eltype(typeof(a)) == eltype(coeffs)
    @test a[1:3] == coeffs[1:3]
    @test view(a, 1:3) == view(coeffs, 1:3)
    @test setindex!(copy(a), 0.0, 1) == setindex!(copy(coeffs), 0.0, 1)
    @test a == a ≈ a
    @test !iszero(a)
    @test copy(a) == a
    @test iszero(zero(a))
    @test float(a) == Sequence(𝕊, float(coeffs))
    @test complex(a) == Sequence(𝕊, complex(coeffs))
    @test real(a) == Sequence(𝕊, real(coeffs))
    @test imag(a) == Sequence(𝕊, imag(coeffs))
    @test conj(a) == Sequence(𝕊, conj(coeffs))
    @test conj!(copy(a)) == Sequence(𝕊, conj!(copy(coeffs)))
    #
    a_𝑇 = component(a, 1)
    @test coefficients(a_𝑇) == coeffs
    @test selectdim(a_𝑇, 2, 0) == selectdim(reshape(coeffs, 2, 3, 3), 2, 2)
end


"""
    might_intersect(bb, region)

Returns true if the bounding box bb *might* intersect the region, otherwise returns
false.  To be used as an optimisation to exclude a bounding box before testing
points within the bounding box.
"""
@inline might_intersect(r1::AbstractRegion, r2::AbstractRegion) = intersects(boundingbox(r1), r2)
@inline might_intersect(bb1::BoundingBox, r2::AbstractRegion) = intersects(bb1, boundingbox(r2))
@inline might_intersect(bb::BoundingBox, region::Circle) = _might_intersect_circle(bb, region)
@inline might_intersect(bb::BoundingBox, region::Cylinder) = _might_intersect_circle(bb, region)
@inline might_intersect(bb::BoundingBox, region::Sphere) = _might_intersect_circle(bb, region)

function _might_intersect_circle(bb, circle)
    if !intersects(bb, boundingbox(circle))
        return false
    end
    # Fit a circle and find the y distance from center
    if circle.y > bb.ymax
        dy = circle.y - bb.ymax
    elseif circle.y >= bb.ymin
        dy = zero(promote_type(eltype(eltype(bb)), eltype(eltype(circle))))
    else
        dy = bb.ymin - circle.y
    end

    # Fit a circle and find the x range to query, if any
    if circle.x > bb.xmax
        dx = circle.x - bb.xmax
    elseif circle.x >= bb.xmin
        dx = zero(promote_type(eltype(eltype(bb)), eltype(eltype(circle))))
    else
        dx = bb.xmin - circle.x
    end

    h² = dx * dx + dy * dy
    r² = circle.radius * circle.radius

    return h² <= r²
end


# The SteinGaussianKernel has base kernel of the form:
#
# k(x, y) = exp(-1/(2*beta)*||x-y||^2)
#
# where ||.|| is the L2 norm.

type SteinGaussianKernel <: SteinGaussianWeightedKernel
    # the beta parameter
    beta::Float64
end

# have default parameter of beta = 1.0
SteinGaussianKernel() = SteinGaussianKernel(1.0)

function Q(ker::SteinGaussianKernel, x::Array{Float64, 1}, y::Array{Float64, 1})
    1.0
end

function dxj_Q(kernel::SteinGaussianKernel, x::Array{Float64,1}, y::Array{Float64,1}, j::Int64)
    0.0
end

function dxjyj_Q(kernel::SteinGaussianKernel, x::Array{Float64,1}, y::Array{Float64,1}, j::Int64)
    0.0
end


# Code adapted from https://github.com/Chemellia/AtomicGraphNets.jl/blob/main/src/layers.jl
# Layers have been GPUified (will try to get them upstreamed)
# AGN stands for AtomicGraphNets
function pool_out_features(num_f::Int64, dim::Int64, stride::Int64, pad::Int64)
    return Int64(floor((num_f + 2 * pad - dim) / stride + 1))
end

function compute_pool_params(num_f_in::Int64, num_f_out::Int64, dim_frac::AbstractFloat;
                             start_dim=Int64(round(dim_frac * num_f_in)), start_str=Int64(floor(num_f_in / num_f_out)))
    # take starting guesses
    dim = start_dim
    str = start_str
    p_numer = str * (num_f_out - 1) - (num_f_in - dim)
    if p_numer < 0
        p_numer == -1 ? dim = dim + 1 : str = str + 1
    end
    p_numer = str * (num_f_out - 1) - (num_f_in - dim)
    if p_numer < 0
        error("problem, negative p!")
    end
    if p_numer % 2 == 0
        pad = Int64(p_numer / 2)
    else
        dim = dim - 1
        pad = Int64((str * (num_f_out - 1) - (num_f_in - dim)) / 2)
    end
    out_fea_len = pool_out_features(num_f_in, dim, str, pad)
    if !(out_fea_len == num_f_out)
        print("problem, output feature wrong length!")
    end
    # check if pad gets comparable to width...
    if pad >= 0.8 * dim
        @warn "specified pooling width was hard to satisfy without nonsensically large padding relative to width, had to increase from desired width"
        dim, str, pad = compute_pool_params(num_f_in, num_f_out, dim_frac; start_dim=Int64(round(1.2 * start_dim)))
    end
    return dim, str, pad
end

abstract type AtomicGraphLayer end

function Base.show(io::IO, l::AtomicGraphLayer)
    p, s, _ = destructure_parameters_states(l)
    device = on_gpu(p, s) ? "GPU" : "CPU"
    return print(io, string(typeof(l).name.name), "() ", string(length(p)), " Trainable Parameters & ",
                 string(length(s)), " States & Device = ", device)
end

(l::AtomicGraphLayer)(x::Tuple) = l(x[1], x[2])

struct AGNConv{W,B,F} <: AtomicGraphLayer
    weight::W
    bias::B
    activation::F
end

function AGNConv(ch::Pair{<:Integer,<:Integer}, activation=gelu; initW=Flux.glorot_uniform, initb=zeros,
                 T::DataType=Float32)
    weight = T(2) .* T.(initW(ch[2], ch[1]))
    b = initb(ch[2], 1)
    if !(b isa Flux.Zeros)
        b = T.(b)
    end
    return AGNConv(weight, b, activation)
end

Flux.@functor AGNConv

function (l::AGNConv)(lapl::AbstractMatrix, X::AbstractMatrix)
    out_mat = normalise01(l.activation.(l.weight * X * (lapl + I) .+ l.bias))
    return lapl, out_mat
end

struct AGNMaxPool <: AtomicGraphLayer
    dim::Int64
    stride::Int64
    pad::Int64
end

struct AGNMeanPool <: AtomicGraphLayer
    dim::Int64
    stride::Int64
    pad::Int64
end

function AGNPool(pool_type::Symbol, ch::Pair{<:Integer,<:Integer}, pool_width_frac::AbstractFloat)
    in_num_features, out_num_features = ch
    dim, stride, pad = compute_pool_params(in_num_features, out_num_features, pool_width_frac)
    return (pool_type == :max ? AGNMaxPool : AGNMeanPool)(dim, stride, pad)
end

AGNMaxPool(args...) = AGNPool(:max, args...)

AGNMeanPool(args...) = AGNPool(:mean, args...)

function (m::AGNMaxPool)(lapl::AbstractMatrix, X::AbstractMatrix)
    x = reshape(X, (size(X)..., 1, 1))
    pdims = PoolDims(x, (m.dim, 1); padding=(m.pad, 0), stride=(m.stride, 1))
    return lapl, mean(maxpool(x, pdims); dims=2)[:, :, 1, 1]
end

function (m::AGNMeanPool)(lapl::AbstractMatrix, X::AbstractMatrix)
    x = reshape(X, (size(X)..., 1, 1))
    pdims = PoolDims(x, (m.dim, 1); padding=(m.pad, 0), stride=(m.stride, 1))
    return lapl, mean(meanpool(x, pdims); dims=2)[:, :, 1, 1]
end


# # High angular resolution diffusion imaging
#
# In this example we will compute the signal using the same gradient sequence in many
# different directions.

# ## Building a biological model
#
# We start by loading SpinDoctor and a Makie plotting backend.

# LSP indexing solution                                                          #src
# https://github.com/julia-vscode/julia-vscode/issues/800#issuecomment-650085983 #src
if isdefined(@__MODULE__, :LanguageServer)                                       #src
    include("../src/SpinDoctor.jl")                                              #src
    using .SpinDoctor                                                            #src
end                                                                              #src

using SpinDoctor
using LinearAlgebra

if haskey(ENV, "GITHUB_ACTIONS")
    using CairoMakie
else
    using GLMakie
end

# Here we create a recipe for five stacked plates with isotropic diffusion tensors. They
# should allow for free diffusion in the horizontal direction, but a rather restricted
# vertical diffusion with the permeable membranes.

ncell = 5
setup = PlateSetup(;
    name = "Plates",
    width = 50.0,
    depth = 50.0,
    heights = fill(5.0, ncell),
    bend = 0.0,
    twist = 0.0,
)
coeffs = coefficients(
    setup;
    D = [0.002 * I(3) for _ = 1:ncell],
    T₂ = fill(Inf, ncell),
    ρ = fill(1.0, ncell),
    κ = (; interfaces = fill(1e-4, ncell - 1), boundaries = fill(0.0, ncell)),
    γ = 2.67513e-4,
)

# We then proceed to build the geometry and finite element mesh.

mesh, = create_geometry(setup; recreate = true)
plot_mesh(mesh)

# The mesh looks good, so we may then proceed to assemble the biological model and the
# associated finite element matrices.

model = Model(; mesh, coeffs...)
matrices = assemble_matrices(model);

# We may also compute some useful quantities, including a scalar diffusion coefficient from
# the diffusion tensors.

volumes = get_cmpt_volumes(model.mesh)
D_avg = 1 / 3 * tr.(model.D)' * volumes / sum(volumes)
ncompartment = length(model.mesh.points)

# The gradient pulse sequence will be a PGSE with both vertical and horizontal components.
# This allows for both restricted vertical diffusion and almost unrestricted horizontal
# diffusion. The different approaches should hopefully confirm this behaviour.

directions = unitsphere(200)
profile = PGSE(2500.0, 4000.0)
b = 1000
g = √(b / int_F²(profile)) / model.γ
gradients = [ScalarGradient(d, profile, g) for d ∈ eachcol(directions)]

# We may solve the BTPDE for each gradient.

btpde = BTPDE(; model, matrices)
solver = IntervalConstantSolver(; timestep = 10.0)
ξ, = solve_multigrad(btpde, gradients, solver)

# The signals are computed from the magnetization field through quadrature.

signal = [abs(compute_signal(matrices.M, ξ)) for ξ ∈ ξ]

# We may plot the directionalized signal.

plot_hardi(directions, signal)

# The signal attenuates the most in the horizontal direction, as that is where diffusion is
# restricted the least.


#=

# 311: Heat Equation with boundary diffusion
([source code](SOURCE_URL))

=#

module Example311_HeatEquation_BoundaryDiffusion
using Printf
using VoronoiFVM
using ExtendableGrids

"""
  We solve the following system

      ∂_tu - εΔu = 0            in [0,T] × Ω
           ε∇u⋅ν = k(u-v)       on [0,T] × Γ_1
           ε∇u⋅ν = 0            on [0,T] × (∂Ω ∖ Γ_1)
  ∂_tv -ε_ΓΔ_Γ v = f(x) +k(u-v) on [0,T] × Γ_1
          u(0)   = 0.5          in   {0} × Ω
          v(0)   = 0.5          on   {0} × Γ_1
"""

function main(n=1)

    breg = 5 # boundary region number for surface diffusion

    println("Surface diffusion example")

    hmin   = 0.01 * 2.0^(-n+1)
    hmax   = 0.1  * 2.0^(-n+1)
    XLeft  = geomspace(0.0, 0.5, hmax, hmin)
    XRight = geomspace(0.5, 1.0, hmin, hmax)
    X      = glue(XLeft, XRight)

    Z      = geomspace(0.0, 1.0, hmin, 2*hmax)

    grid   = VoronoiFVM.Grid(X, X, Z)

    # parameters
    eps      = 1.0e0  # bulk heat conduction coefficient
    eps_surf = 1.0e-2 # surface diffusion coefficient
    k        = 1.0    # transmission coefficient
    physics  = VoronoiFVM.Physics(
        flux        = function(f,u,edge)
                            f[1] = eps*(u[1,1]-u[1,2])
                      end,

        bflux       = function(f, u, edge)
                        if edge.region == breg
                          f[2]     = eps_surf*(u[2,1]-u[2,2])
                        else
                          f[2] = 0.0
                        end

                      end,
        breaction   = function(f,u,node)
                        if node.region == breg
                          f[1] = k*(u[1]-u[2])
                          f[2] = k*(u[2]-u[1])
                        else
                          f[1] = 0.0
                          f[2] = 0.0
                        end
                      end,
        bsource      = function(f, bnode)
                        x1   = bnode[1]-0.5
                        x2   = bnode[2]-0.5
                        x3   = bnode[3]-0.5
                        f[2] = 1.0e4*exp(-20.0*(x1^2+x2^2+x3^2))
                      end,

        bstorage    = function(f,u,node)
                        if node.region == breg
                          f[2] = u[2]
                        end
                      end,

        storage     = function(f,u,node)
                        f[1] = u[1]
                      end
    )

    sys=VoronoiFVM.System(grid,physics,unknown_storage=:sparse)
    enable_species!(sys,1,[1])
    enable_boundary_species!(sys,2,[breg])

    function tran32!(a,b)
        a[1]=b[2]
    end

    bgrid2=subgrid(grid,[breg],boundary=true,transform=tran32!)

    inival  = unknowns(sys)
    U       = unknowns(sys)
    inival .= 0.5

    control             = VoronoiFVM.NewtonControl()
    control.verbose     = true
    control.tol_linear  = 1.0e-5
    control.max_lureuse = 10

    tstep = 0.1
    time  = 0.0
    step  = 0
    T     = 1.0
    while time < T
        time=time+tstep
        solve!(U, inival, sys, control=control, tstep=tstep)
        inival.=U
        U_surf=view(U[2,:],bgrid2)

        if true
            @printf("time=%g\n",time)
        end

        tstep*=1.0

        step += 1
    end

    nothing
end

function test()
    main()
    true
end

end


function findMaxThroughput(SubmittedTransactions)
	sm1 = 1000; #%1000: lower part, 5000:middle
	sm2 = 10;
	mslope = 10; #%lower this number sooner it declares a max throughput!

	sTPS = DoSmooth(SubmittedTransactions, sm1);
	#%sTPS = smooth(SubmittedTransactions, sm1);
	#%sTPS = SubmittedTransactions;

	Y,I = localmax(sTPS')
	I = sort(I)
	if length(I) == 0
		tmp = []
	else
		tmp = diff(vec(DoSmooth(I,sm2)))
	end

	ind = -1

	for i=1:length(tmp)
		if tmp[i]<mslope
			ind = i
			break
		end
	end
	if ind == -1 || ind+1 > length(I)
		xMaxThroughput = [];
	else
		xMaxThroughput = I[ind + 1];
	end
	yMaxThroughput = sTPS[xMaxThroughput];
	return xMaxThroughput,yMaxThroughput

end


using Statistics
using NCDatasets

using ArgParse
using JSON
using Formatting

include("LinearRegression.jl")
include("nanop.jl")
include("CESMReader2.jl")

using .CESMReader2

correlation = (x1, x2) -> x1' * x2 / (sum(x1.^2)*sum(x2.^2)).^0.5

function parse_commandline()

    s = ArgParseSettings()
    @add_arg_table s begin

        "--data-file-prefix"
            help = "Data filename prefix including folder and path until the timestamp. File extension `nc` is assumed."
            arg_type = String
            required = true

        "--output-file"
            help = "Output file."
            arg_type = String
            required = true

        "--beg-year"
            help = "Year of begin."
            arg_type = Int64
            required = true

        "--end-year"
            help = "Year of end."
            arg_type = Int64
            required = true

        "--varname"
            help = "Variable name. If it is 2D then it should be (lon, lat, time), if it is 3D then it should be (lon, lat, lev/depth, time)"
            arg_type = String
            required = true

    end

    return parse_args(ARGS, s)
end

parsed = parse_commandline()
print(json(parsed, 4))

output_file = parsed["output-file"]

filename_format = format("{:s}{{:04d}}-{{:02d}}.nc", joinpath(parsed["data-file-prefix"]))

# Determine the shape of the variable

CESMReader2.iterFilenames(filename_format, (parsed["beg-year"], parsed["end-year"])) do filename, y, m

    Dataset(filename, "r") do ds
        v = ds[parsed["varname"]]
        global var_dins = dimnames(v)
        global var_dims = [ ds.dim[var_dins[i]] for i=1:length(var_dins) ]

        if var_dins[end] != "time"
            throw(ErrorException("The last dimension is not time"))
        end

        if var_dims[end] != 1
            throw(ErrorException("The last dimension time has more than 1 record"))
        end

        global spatial_dins = var_dins[1:end-1]
        global spatial_dims = var_dims[1:end-1]
        global spatial_cols = [Colon() for i=1:length(spatial_dims)]

        println("var_dins: ", string(var_dins))
        println("var_dims: ", string(var_dims))

    end

    return false
end

# M = monthly
# S = seasonal
# A = annual
data_MM    = zeros(Float64, spatial_dims..., 12)
data_MMSQR = zeros(Float64, spatial_dims..., 12)

tmp_AM     = zeros(Float64, spatial_dims...)
data_AM    = zeros(Float64, spatial_dims...)
data_AMSQR = zeros(Float64, spatial_dims...)

years   = parsed["end-year"] - parsed["beg-year"] + 1
months  = years * 12

CESMReader2.iterFilenames(filename_format, (parsed["beg-year"], parsed["end-year"])) do filename, y, m
    println("Loading $filename")
    Dataset(filename, "r") do ds

        global data_MM, data_MMSQR, tmp_AM, data_AM, data_AMSQR

        v = ds[parsed["varname"]][spatial_cols..., 1]

        data_MM[spatial_cols..., m] .+= v
        data_MMSQR[spatial_cols..., m] .+= v.^2

        tmp_AM .+= v
        if m == 12
            @. tmp_AM /= 12.0
            @. data_AM += tmp_AM
            @. data_AMSQR += tmp_AM^2
            @. tmp_AM = 0.0
        end
    end
end

data_MM ./= years
data_MMSQR ./= years
data_MMVAR = abs.(data_MMSQR - data_MM.^2)
data_MMSTD = data_MMVAR.^0.5

data_AM ./= years
data_AMSQR ./= years
data_AMVAR = abs.(data_AMSQR - data_AM.^2)
data_AMSTD = data_AMVAR.^0.5

Dataset(output_file, "c") do ds

    defDim(ds, "months", 12)

    for i=1:length(var_dins)
        defDim(ds, var_dins[i], var_dims[i])
    end

    datas =  convert(Array{Any}, [

        (format("{:s}_MM",    parsed["varname"]),       data_MM,    (spatial_dins..., "months"), Dict()),
        (format("{:s}_MMVAR", parsed["varname"]),       data_MMVAR, (spatial_dins..., "months"), Dict()),
        (format("{:s}_MMSTD", parsed["varname"]),       data_MMSTD, (spatial_dins..., "months"), Dict()),

        (format("{:s}_AM",    parsed["varname"]),       data_AM,    (spatial_dins...,), Dict()),
        (format("{:s}_AMVAR", parsed["varname"]),       data_AMVAR, (spatial_dins...,), Dict()),
        (format("{:s}_AMSTD", parsed["varname"]),       data_AMSTD, (spatial_dins...,), Dict()),

    ])

    for (varname, vardata, vardim, attrib) in datas
        if ! haskey(ds, varname)
            var = defVar(ds, varname, Float64, vardim)
            var.attrib["_FillValue"] = 1e20
        end

        println("Writing variable:  ", varname)
        var = ds[varname]

        for (k, v) in attrib
            var.attrib[k] = v
        end
        var[:] = vardata
    end

end


using StaticArrays

########################################
# SArrays and SVectors
########################################
function cembed(ts::Vector{SArray{Size,T,N,L}},
        in_which_pos,
        at_what_lags) where {Size, T, N, L}
    dim = length(in_which_pos)
    minlag, maxlag = minimum(at_what_lags), maximum(at_what_lags)
    npts = length(ts[1]) - (maxlag + abs(minlag))
    E = zeros(T, dim, npts)

    for i in 1:length(in_which_pos)
        ts_ind = in_which_pos[i]
        TS = ts[ts_ind]
        lag = at_what_lags[i]

        if lag > 0
            E[i, :] = TS[((1 + abs(minlag)) + lag):(end - maxlag) + lag]
        elseif lag < 0
            E[i, :] = TS[((1 + abs(minlag)) - abs(lag)):(end - maxlag - abs(lag))]
        elseif lag == 0
            E[i, :] = TS[(1 + abs(minlag)):(end - maxlag)]
        end
    end
    embeddingdata = EmbeddingData{dim, T}(
        ts,  # the dataseries
        ["" for i = 1:length(ts)], # empty labels by default
        ["" for i = 1:length(ts)], # empty descriptions by default
        EmbeddingPositions(in_which_pos), # the positions in which the dataseries appear
        EmbeddingLags(at_what_lags) # the embedding lag for each column
        )
    Embedding(Array(E), embeddingdata)
end

"""
    cembed(data::SArray) where T

Construct an embedding from data series gathered in a `SArray`.
"""
function cembed(data::SArray)
	if size(data, 1) > size(data, 2)
		#info("Treating each row as a point")
		dim = size(data, 2)
        which_pos = [i for i = 1:dim]
        which_lags = [0 for i in 1:dim]
		cembed([data[:, i] for i = 1:dim], which_pos, which_lags)
	else
		#info("Treating each column of data as a point")
		dim = size(data, 1)
        which_pos = [i for i = 1:dim]
        which_lags = [0 for i in 1:dim]
		cembed([data[i, :] for i = 1:dim], which_pos, which_lags)
	end
end

"""
    cembed(data::SArray) where T

Construct an embedding from data series gathered in a `SArray`, specifying the
dimensionality, which variables in the embedding are represented by which
variables in the `data` array, and what the embedding lags should be.
"""
function cembed(data::SArray, which_pos, which_lags)
    if size(data, 1) > size(data, 2)
		#info("Treating each row as a point")
		dim = size(data, 2)
		cembed([data[:, i] for i = 1:dim], which_pos, which_lags)
	else
		#info("Treating each column of data as a point")
		dim = size(data, 1)
		cembed([data[i, :] for i = 1:dim], which_pos, which_lags)
	end
end

export cembed


module Trapz
    export trapz

    @inline function bringlast(T::Tuple,el)
        ifelse(el==T[1],
            (bringlast(Base.tail(T),el)...,T[1]) ,
            (T[1],bringlast(Base.tail(T),el)...)
        )
    end
    @inline function bringlast(T::Tuple{},el); T; end

    @inline function trapz_colon(k) Colon(); end
    @inline function idxlast(i,::Val{N}) where N; Base.tail((ntuple(trapz_colon,Val(N))...,i)) end
    function killzerodim(A::AbstractArray{fT,0}) where fT
        A[]
    end
    function killzerodim(A::AbstractArray{fT,N}) where {fT,N}
        A
    end
    function killzerodim(A::fT) where {fT<:Number}
        A
    end
    function trapz(x::T1, y::T2) where {N,fT,T1<:AbstractVector{fT},T2<:AbstractArray{fT,N}}
        n = length(x)
        s = size(y)
        @assert s[end]==n
        @inbounds begin
            r = similar(y,Base.reverse(Base.tail(Base.reverse(s))))
            if n <= 1;
                r.=zero(fT)
                @goto retval;
            end
            @fastmath r .= (x[2] - x[1]) .* view(y,idxlast(1,Val(N))...)
            for i in 2:n-1
               @fastmath r .+= (x[i+1] - x[i-1]) .* view(y,idxlast(i,Val(N))...)
            end
            @fastmath r .+= (x[end]-x[end-1]) .* view(y,idxlast(n,Val(N))...)
            @label retval
            return killzerodim(r./2)
        end
    end

    function trapz(x::T1, y::T2, axis::T3) where {N,fT,T1<:AbstractVector{fT},T2<:AbstractArray{fT,N},T3<:Integer}
        @assert 1<=axis<=N
        trapz(x,PermutedDimsArray(y,bringlast(ntuple(identity,Val(N)),axis)))
    end

    function gen_trapz_impl(xs::Type{T}, M) where T <: NTuple{N} where N
        ex = :(trapz(xs[$N], M))
        for i=(N-1):-1:1
            ex = :(trapz(xs[$i],$ex))
        end
        return ex
    end

    @generated function trapz(xs::NTuple{N}, M) where N
        return gen_trapz_impl(xs, M)
    end

    function trapz(xs::NTuple{N}, M::AbstractArray{T,S}, axes::NTuple{N}) where {N, T, S}
        if S > N
            axes = ((i for i=1:S if !in(i,axes))..., axes...)
        end
        trapz(xs, PermutedDimsArray(M, axes))
    end

    """
        trapz(x,y,axis=End)
        Calculates ∫y[..., i (axis) ,...] dx[i]
        for performance reasons x must have the same float type as y.
        1-D Example:
    ```julia
        vx=0:0.01:1
        vy=(x->x^2).(vx)
        trapz(vx,vy)
    ```
            Result ≈ 1/3

        2-D Example:
    ```julia
        vx=0:0.01:1
        vy=0:0.01:2
        z=[x^2+y^2 for x=vx, y=vy]
        trapz((vx,vy),z) # equivalent to trapz(vx, trapz(vy, z))
    ```
            Result ≈ 4/3

        2-D Example in reverse integration order:
    ```julia
        vx=0:0.01:1
        vy=0:0.01:2
        z=[x^2+y^2 for x=vx, y=vy]
        trapz((vy,vx),z,(2,1)) # equivalent to trapz(vy, trapz(vx, z, 1), 1)
    ```
            Result ≈ 4/3
    """
    trapz

end # module

#Some testing code
# using BenchmarkTools,GPUArrays
#
# vx=range(0,1,length=100)
# vy=vx.^2
# My=[x^2+y^2 for x in vx,y in vx]
# Ty=[x^2+y^2+z^2 for x in vx,y in vx,z in vx]
#
#
# Main.Trapz.trapz(vx,JLArray(Ty))
# Main.Trapz.trapz(vx,JLArray(My))
# Main.Trapz.trapz(vx,JLArray(vy))
# Main.Trapz.trapz(vx,My)
# Main.Trapz.trapz(vx,Ty)
# Main.Trapz.trapz(vx,vy)
#


using StructuralCausalModels
#using StatsPlots
#gr(size=(700,600))

ProjDir = @__DIR__
cd(ProjDir) #do

df = DataFrame!(CSV.File(scm_path("..", "data", "WaffleDivorce.csv"), delim=';'));
df = DataFrame(
  :s => df[:, :South],
  :a => df[:, :MedianAgeMarriage],
  :m => df[:, :Marriage],
  :w => df[:, :WaffleHouses],
  :d => df[:, :Divorce]
)
first(df, 5) |> display

if isdefined(Main, :StatsPlots)
  StatsPlots.cornerplot(Array(df), label=names(df))
  savefig("$(ProjDir)/SR6.4.3.png")
end

d = OrderedDict(
  [:w, :m, :a] => :s,
  :d => [:a, :w, :m],
  :m => [:a]
);

dag = DAG("sr6_4_3", d, df=df);
show(dag)

fname = ProjDir * "/sr6.4.3.dot"
to_graphviz(dag, fname)
Sys.isapple() && run(`open -a GraphViz.app $(fname)`)

display(dag.s); println()

println("Basis set:")
bs = basis_set(dag)
display(bs); println()

t = shipley_test(dag)
display(t); println()

f = :w; s = :d;

e = d_separation(dag, [f], [s], c=[:m, :a])
println("d_separation($(dag.name), $f, $s, c=[:m, :a]) = $e\n")

#ap = all_paths(dag, f, s)
ap = all_paths(dag, f, s)
ap |> display
println()

bp = backdoor_paths(dag, ap, f)
bp |> display
println()

adjustmentsets = adjustment_sets(dag, :w, :d)
println("Adjustment sets:")
adjustmentsets |> display

#end


using IntervalArithmetic
using Test

let x, b

setprecision(Interval, Float64)

@testset "setformat tests" begin

    @testset "Interval" begin

        a = 1..2
        b = -1.1..1.3
        c = Interval(pi)

        @testset "6 sig figs" begin
            setformat(:standard, sigfigs=6)

            @test string(a) == "[1, 2]"
            @test string(b) == "[-1.10001, 1.30001]"
            @test string(c) == "[3.14159, 3.1416]"
        end

        @testset "10 sig figs" begin
            setformat(sigfigs=10)

            @test string(a) == "[1, 2]"
            @test string(b) == "[-1.100000001, 1.300000001]"
            @test string(c) == "[3.141592653, 3.141592654]"
        end

        @testset "20 sig figs" begin
            setformat(sigfigs=20)

            @test string(a) == "[1, 2]"
            @test string(b) == "[-1.1000000000000000889, 1.3000000000000000445]"
            @test string(c) == "[3.1415926535897931159, 3.1415926535897935601]"
        end

        @testset "Full" begin
            setformat(:full)

            @test string(a) == "Interval(1.0, 2.0)"
            @test string(b) == "Interval(-1.1, 1.3)"
            @test string(c) == "Interval(3.141592653589793, 3.1415926535897936)"
        end

        @testset "Midpoint" begin
            setformat(:midpoint, sigfigs=6)

            @test string(a) == "1.5 ± 0.5"
            @test string(b) == "0.1 ± 1.20001"
            @test string(c) == "3.14159 ± 4.4409e-16"

            # issue 175:
            @test string(@biginterval(1, 2)) == "1.5 ± 0.5"
        end
    end

    @testset "Interval{Rational{T}}" begin
        a = Interval(1//3, 5//4)
        @test typeof(a)== Interval{Rational{Int}}
        setformat(:standard)
        @test string(a) == "[1//3, 5//4]"

        setformat(:full)
        @test string(a) == "Interval(1//3, 5//4)"

        setformat(:midpoint)
        @test string(a) == "19//24 ± 11//24"
    end

    @testset "Interval{Float32}" begin
        a = Interval{Float32}(1, 2)
        b = Interval{Float32}(-1, Inf)
        setformat(:standard)
        @test string(a) == "[1f0, 2f0]"
        @test string(b) == "[-1f0, ∞]"

        setformat(:full)
        @test string(a) == "Interval(1.0f0, 2.0f0)"
        @test string(b) == "Interval(-1.0f0, ∞)"

        setformat(:midpoint)
        @test string(a) == "1.5f0 ± 0.5f0"
    end

    @testset "Complex{Interval}" begin
        a = Complex(Interval(0, 2), 1)
        @test typeof(a) == Complex{Interval{Float64}}
        setformat(:standard)
        @test string(a) == "[0, 2] + [1, 1]im"
    end

    setprecision(Interval, 256)

    @testset "DecoratedInterval" begin
        a = @decorated(1, 2)
        @test typeof(a)== DecoratedInterval{Float64}

        setformat(:standard, decorations=false)
        @test string(a) == "[1, 2]"

        setformat(:standard, decorations=true)
        @test string(a) == "[1, 2]_com"

        # issue 131:
        a = DecoratedInterval(big(2), big(3), com)

        setformat(:standard, decorations=false)
        @test string(a) == "[2, 3]₂₅₆"

        setformat(decorations=true)
        @test string(a) == "[2, 3]₂₅₆_com"

        setformat(:full)
        @test string(a) == "DecoratedInterval(Interval(2.0, 3.0), com)"

        setformat(:midpoint)
        @test string(a) == "2.5 ± 0.5_com"

        setformat(decorations=false)
        @test string(a) == "2.5 ± 0.5"

    end

    setprecision(Interval, 128)

    @testset "BigFloat intervals" begin
        setformat(:standard, decorations=false)

        a = @interval big(1)
        @test typeof(a)== Interval{BigFloat}
        @test string(a) == "[1, 1]₁₂₈"

        setformat(:full)
        @test string(a) == "Interval(1.0, 1.0)"

        a = DecoratedInterval(big(2), big(3), com)
        @test typeof(a)== DecoratedInterval{BigFloat}

        setformat(:standard, decorations=false)
        @test string(a) == "[2, 3]₁₂₈"

        setformat(:standard, decorations=true)
        @test string(a) == "[2, 3]₁₂₈_com"

        setformat(:full)
        @test string(a) == "DecoratedInterval(Interval(2.0, 3.0), com)"
    end

    setprecision(Interval, Float64)

    @testset "IntervalBox" begin

        setformat(:standard, sigfigs=6)

        X = IntervalBox(1..2, 3..4)
        @test typeof(X) == IntervalBox{2,Float64}
        @test string(X) == "[1, 2] × [3, 4]"

        s = sprint(show, MIME("text/plain"), X)
        @test s == "[1, 2] × [3, 4]"

        X = IntervalBox(1.1..1.2, 2.1..2.2)
        @test string(X) == "[1.09999, 1.20001] × [2.09999, 2.20001]"

        X = IntervalBox(-Inf..Inf, -Inf..Inf)
        @test string(X) == "[-∞, ∞] × [-∞, ∞]"

        setformat(:full)
        @test string(X) == "IntervalBox(Interval(-Inf, Inf), Interval(-Inf, Inf))"

    end
end

@testset "showfull" begin
    setformat(:standard, decorations=false, sigfigs=6)
    setprecision(128)

    x = 0..1
    @test string(x) == "[0, 1]"
    @test sprint(showfull, x) == "Interval(0.0, 1.0)"

    x = @biginterval(0, 1)
    @test string(x) == "[0, 1]₁₂₈"
    @test sprint(showfull, x) == "Interval(0.0, 1.0)"

    x = DecoratedInterval(0, 1, dac)
    @test string(x) == "[0, 1]"
    @test sprint(showfull, x) == "DecoratedInterval(Interval(0.0, 1.0), dac)"

    x = DecoratedInterval(big(0), big(1), def)
    @test string(x) == "[0, 1]₁₂₈"
    @test sprint(showfull, x) == "DecoratedInterval(Interval(0.0, 1.0), def)"

    setformat(decorations=true)
    @test string(x) == "[0, 1]₁₂₈_def"

end

@testset "@format tests" begin
    x = 0.1..0.3

    @format full
    @test string(x) == "Interval(0.09999999999999999, 0.30000000000000004)"

    @format standard 3
    @test string(x) == "[0.0999, 0.301]"

    @format 10
    @test string(x) == "[0.09999999999, 0.3000000001]"
end

end


@testset "Basic arrays generators" begin
  @testset "ket" begin
    #standard tests
    @test ket(1, 2) == [1;0]
    #error tests
    @test_throws AssertionError ket(4, 2)
    @test_throws ArgumentError ket(-4, -2)
  end

  @testset "bra" begin
    #standard tests
    @test bra(1, 2) == [1 0 ]
    #error tests
    @test_throws AssertionError bra(4, 2)
    @test_throws ArgumentError bra(-4, -2)
  end

  @testset "ketbra" begin
    #standard tests
    @test ketbra(1, 2, 3) == [0 1 0; 0 0 0; 0 0 0]
    #error tests
    @test_throws AssertionError ketbra(3, 2, 2)
    @test_throws AssertionError ketbra(2, 3, 2)
    @test_throws ArgumentError ketbra(-4, -2, -1)
  end

  @testset "proj" begin
    #standard tests#
    result = [0.0+0.0im 0.0+0.0im 0.0+0.0im;
              0.0+0.0im 1.0+0.0im 0.0+0.0im;
              0.0+0.0im 0.0+0.0im 0.0+0.0im]
    @test proj(2, 3) == result
    @test proj(1/sqrt(2) * (ket(1, 3)+ket(3, 3))) ≈
                         [0.5+0.0im 0.0+0.0im 0.5+0.0im;
                          0.0+0.0im 0.0+0.0im 0.0+0.0im;
                          0.5+0.0im 0.0+0.0im 0.5+0.0im]
    #error tests
    @test_throws ArgumentError proj(2, -1)
    @test_throws AssertionError proj(3, 2)
  end
end

@testset "Array reshuffles" begin
  @testset "res and unres" begin
    #standard tests
    M = Matrix{Float64}(reshape(1:9, (3, 3))')
    v = Vector{Float64}(collect(1:9))
    A = Complex{Float64}[0.354177+0.0im 0.0891553-0.0251879im 0.0702961+0.0516828im 0.0708664+0.0767941im; 0.0891553+0.0251879im 0.336055+0.0im 0.0420202-0.0109173im 0.0683605-0.00692846im; 0.0702961-0.0516828im 0.0420202+0.0109173im 0.212401+0.0im 0.0939615+0.0553555im; 0.0708664-0.0767941im 0.0683605+0.00692846im 0.0939615-0.0553555im 0.0973671+0.0im]
    @test res(M) == v
    @test unres(v) == M
    @test unres(res(M)) == M
    @test res(unres(v)) == v
    @test unres(res(A)) == A
    #error tests
    @test_throws ArgumentError unres(collect(1:8)*1.)
  end
end


using JuMP

function homogeneous_system(model::JuMP.Model)
    A = full(JuMP.prepConstrMatrix(model))
    c, lb, ub = JuMP.prepProblemBounds(model)
    l, u = model.colLower, model.colUpper

    m, n = size(A)
    @assert m == length(lb) == length(ub)
    @assert model.nlpdata == nothing
    @assert isempty(model.quadconstr)
    @assert isempty(model.sosconstr)

    C = Array(Float64, 0, n)
    b = Float64[]
    for i in 1:m
        if !isinf(lb[i])
            C = [C, A[i,:]]
            push!(b, -lb[i])
        end
        if !isinf(ub[i])
            C = [C; -A[i,:]]
            push!(b, ub[i])
        end
    end

    F = eye(n, n)
    B = Array(Float64, 0, n)
    d = Float64[]
    for i in 1:n
        if !isinf(l[i])
            B = [B; F[i,:]]
            push!(d, -l[i])
        end
        if !isinf(u[i])
            B = [B; -F[i,:]]
            push!(d, u[i])
        end
    end

    return [C;B], [b;d]
end

function write_ine(model::JuMP.Model, fname::String)
    A, b = homogeneous_system(model)
    try
        write_ine(fname, convert(Matrix{Int},A), convert(Vector{Int},b))
    catch InexactError
        try
            write_ine(fname, convert(Matrix{Int},2A), convert(Vector{Int},2b))
        catch InexactError
            try
                write_ine(fname, convert(Matrix{Int},4A), convert(Vector{Int},4b))
            catch InexactError
                write_ine(fname, A, b)
            end
        end
    end
end

function get_extrema(model::JuMP.Model)
    A, b = homogeneous_system(model)
    dd = double_description([b A])

    vertices, rays = Vector{Float64}[], Vector{Float64}[]
    for cv in dd.R
        r = vec(cv)
        # if abs(r[1]) > 10eps()
        if abs(r[1] - 1) < ε
            push!(vertices, r[2:end])
            # push!(vertices, r[2:end] ./ r[1])
        else
            @assert abs(r[1]) < ε
            push!(rays, r[2:end] ./ minimum(r[find(r)]))
        end
    end
    return vertices, rays
end


struct Point{T<:Real}
  x::T
  y::T
end
Point(x::T) where {T<:Complex} = Point(real(x), imag(x))
Point(p::Point{T}) where {T<:Real} = Point(p.x, p.y)
function Point(x::AbstractVector{T}) where {T}
  @assert length(x) == 2
  return Point(x[1], x[2])
end

Base.:+(a::Point, b::Point) = Point(a.x + b.x, a.y + b.y)
Base.:-(a::Point, b::Point) = Point(a.x - b.x, a.y - b.y)
Base.:*(a::Point, f::Vector{T}) where {T<:Number} = Point(a.x * f[1], a.y * f[2])
Base.:/(a::Point{}, f::T) where {T<:Number} = Point(a.x / f, a.y / f)

Vector(p::Point) = [p.x, p.y]

middle(a::Point, b::Point) = Point(0.5*(a.x + b.x), 0.5*(a.y + b.y))

angle(A::Point) = atan(A.y, A.x)

function angle(A::Point, B::Point)
  ϕ = angle(B) - angle(A)
  ϕ < -π && return ϕ + 2π
  ϕ > π && return ϕ - 2π
  return ϕ
end


"Represents a 1D mesh with vertices and elements"
struct Mesh1D
    nvertices::Int   # Number of vertices
    vertex_coordinates::Array{Float64, 1}  # Coordinate of each vertex
    nelements::Int  # Number of elements

    # Map element ID to vertices
    element_vertices::Array{Int, 2}

    "Create a 1D mesh.
        vertex_coordinates is a 1D array of node locations
        elements is a 2D array, mapping elements to vertex indices"
    function Mesh1D(vertex_coordinates, elements)
        # Check that elements are in range
        @assert maximum(elements) == length(vertex_coordinates)
        @assert minimum(elements) == 1

        new(length(vertex_coordinates),
            vertex_coordinates,
            size(elements)[1],
            elements)
    end
end

"Generate simple equidistant grid with K elements"
function MeshGen1D(xmin, xmax, K)

    # Number of vertices
    Nv = K+1

    # Generate node coordinates
    VX = zeros(Nv)
    for i = 1:Nv
        VX[i] = (xmax-xmin)*(i-1)/(Nv-1) + xmin
    end

    # read element to node connectivity
    EToV = zeros(Int, K, 2)
    for k = 1:K
        EToV[k,1] = k
        EToV[k,2] = k+1
    end

    return Mesh1D(VX, EToV)
end


using HypothesisTests, Base.Test
using HypothesisTests: default_tail

# Basic exact test
@test abs(pvalue(ExactMannWhitneyUTest([1:10;], [2.1:2:21;])) - 0.0232) <= 1e-4
@test abs(pvalue(ExactMannWhitneyUTest([2.1:2:21;], [1:10;])) - 0.0232) <= 1e-4
@test abs(pvalue(ExactMannWhitneyUTest([1.5:10:100;], [2.1:2:21;])) - 0.0068) <= 1e-4
@test abs(pvalue(ExactMannWhitneyUTest([2.1:2:21;], [1.5:10:100;])) - 0.0068) <= 1e-4
@test default_tail(ExactMannWhitneyUTest([1:10;], [2.1:2:21;])) == :both
show(IOBuffer(), ExactMannWhitneyUTest([1:10;], [2.1:2:21;]))

# Exact with ties
@test abs(pvalue(ExactMannWhitneyUTest([1:10;], [1:10;])) - 1) <= 1e-4
@test abs(pvalue(ExactMannWhitneyUTest([1:10;], [2:11;])) - 0.5096) <= 1e-4
@test abs(pvalue(ExactMannWhitneyUTest([2:11;], [1:10;])) - 0.5096) <= 1e-4
@test abs(pvalue(ExactMannWhitneyUTest([1:10;], [1:5; ones(5)])) - 0.0057) <= 1e-4
@test abs(pvalue(ExactMannWhitneyUTest([1:5; ones(5)], [1:10;])) - 0.0057) <= 1e-4
show(IOBuffer(), ExactMannWhitneyUTest([1:10;], [1:10;]))

#Exact with ties and unequal lengths
@test abs(pvalue(ExactMannWhitneyUTest([1:10;], [2:2:24;])) - 0.0118) <= 1e-4
@test abs(pvalue(ExactMannWhitneyUTest([2:2:24;], [1:10;])) - 0.0118) <= 1e-4
show(IOBuffer(), ExactMannWhitneyUTest([1:10;], [2:2:24;]))

# Approximate test
@test abs(pvalue(ApproximateMannWhitneyUTest([1:10;], [2.1:2:21;])) - 0.0257) <= 1e-4
@test abs(pvalue(ApproximateMannWhitneyUTest([2.1:2:21;], [1:10;])) - 0.0257) <= 1e-4
@test abs(pvalue(ApproximateMannWhitneyUTest([1.5:10:100;], [2.1:2:21;])) - 0.0091) <= 1e-4
@test abs(pvalue(ApproximateMannWhitneyUTest([2.1:2:21;], [1.5:10:100;])) - 0.0091) <= 1e-4
@test default_tail(ApproximateMannWhitneyUTest([1:10;], [2.1:2:21;])) == :both
show(IOBuffer(), ApproximateMannWhitneyUTest([1:10;], [2.1:2:21;]))

# Approximate with ties
@test abs(pvalue(ApproximateMannWhitneyUTest([1:10;], [1:10;])) - 1) <= 1e-4
@test abs(pvalue(ApproximateMannWhitneyUTest([1:10;], [2:11;])) - 0.4948) <= 1e-4
@test abs(pvalue(ApproximateMannWhitneyUTest([2:11;], [1:10;])) - 0.4948) <= 1e-4
@test abs(pvalue(ApproximateMannWhitneyUTest([1:10;], [1:5; ones(5)])) - 0.0076) <= 1e-4
@test abs(pvalue(ApproximateMannWhitneyUTest([1:5; ones(5)], [1:10;])) - 0.0076) <= 1e-4
show(IOBuffer(), ApproximateMannWhitneyUTest([1:10;], [1:10;]))

# Tests for automatic selection
@test abs(pvalue(MannWhitneyUTest([1:10;], [2.1:2:21;])) - 0.0232) <= 1e-4
@test abs(pvalue(MannWhitneyUTest([1:10;], [2:11;])) - 0.4948) <= 1e-4
show(IOBuffer(), MannWhitneyUTest([1:10;], [2.1:2:21;]))

# Issue #39
@test abs(pvalue(ExactMannWhitneyUTest(Float32[1:10;], Float32[2:11;])) - 0.5096) <= 1e-4


## Imported math functions
## make vectorized version while we are at it
for fn in (:sin, :cos, :tan, :sinh, :cosh, :tanh, :asin, :acos, :atan,
           :asinh, :acosh, :atanh, :sec, :csc, :cot, :asec, :acsc, :acot,
           :sech, :csch,
           :coth, :acoth,
           :atan2,
           :radians2degrees, :degrees2radians,
           :log2, :log10, :log1p, :exponent, :exp, :exp2, :expm1,
           :sqrt, :square, :erf, :erfc, :erfcx, :erfi, :erfinv, :erfcinv, :dawson,
           :fresnels, :fresnelc, :Ei, :Si, :Ci,
           :ceiling, :floor, :trunc, :round, :significand,
           :factorial2,
           :airyai, :airybi
           )
    meth_name = string(fn)

    @eval begin
        @doc """
`$($meth_name)`: a SymPy function.
The SymPy documentation can be found through: http://docs.sympy.org/latest/search.html?q=$($meth_name)
""" ->
        ($fn)(x::Sym;kwargs...) = sympy_meth($meth_name, x; kwargs...)
    end
    @eval ($fn)(a::Array{Sym}) = map($fn, a)
end

## Log function handles arguments differently
log(x::Sym) = sympy_meth(:log, x)
log(b::Sym, x::Sym) = sympy_meth(:log, x, b)

### Trigonometry
Base.rad2deg(x::Sym) = radians2degrees(x)
Base.deg2rad(x::Sym) = degrees2radians(x)

## degree functions
for fn in (:cosd, :cotd, :cscd, :secd, :sind, :tand,
          :acosd, :acotd, :acscd, :asecd, :asind, :atand)

    rad_fn = string(fn)[1:end-1]
    @eval ($fn)(x::Sym) = sympy[@compat(Symbol($rad_fn))](project(x * Sym(sympy[:pi]/180)))
    @eval ($fn)(a::Array{Sym}) = map($fn, a)
end

for fn in (:cospi, :sinpi)
    rad_fn = string(fn)[1:end-2]
    @eval ($fn)(x::Sym) = sympy[@compat(Symbol($rad_fn))](project(x * Sym(sympy[:pi])))
    @eval ($fn)(a::Array{Sym}) = map($fn, a)
end

## :asech, :acsch, :sinc, :cosc,
## These fail, so define from definitions
## http://mathworld.wolfram.com/InverseHyperbolicSecant.html
asech(z::Sym) = log(sqrt(1/z-1)*sqrt(1/z+1) + 1/z)
asech(as::Array{Sym}) = map(asech, as)
## http://mathworld.wolfram.com/InverseHyperbolicCosecant.html
acsch(z::Sym) = log(sqrt(1+1/z^2) + 1/z)
acsch(as::Array{Sym}) = map(acsch, as)
sinc(x::Sym) = sin(Sym(PI*x))/(PI*x)
sinc(as::Array{Sym}) = map(sinc, as)
cosc(x::Sym) = diff(sinc(x))
cosc(as::Array{Sym}) = map(cosc, as)

## (x:Sym, ...) , export
sympy_math_methods = (:Prod,
                      :Ylm,
                      :assoc_legendre,
                      :chebyshevt
                      )
for meth in sympy_math_methods
    meth_name = string(meth)
    @eval begin
           @doc """
`$($meth_name)`: a SymPy function.
The SymPy documentation can be found through: http://docs.sympy.org/latest/search.html?q=$($meth_name)
""" ->
        ($meth)(ex, args...; kwargs...) = sympy_meth($meth_name, ex, args...; kwargs...) # no ::Sym
    end
    eval(Expr(:export, meth))
end

## in julia, not SymPy
cbrt(x::Sym) = x^(1//3)
Base.ceil(x::Sym) = ceiling(x)

functions_sympy_methods = (
                           :arg,
                           :conjugate,
                           :re,
                           :sign
                           )

## map Abs->abs, Max->max, Min->min
abs(ex::Sym, args...; kwargs...) = sympy_meth(:Abs, ex, args...; kwargs...)
Base.real(x::Sym) = sympy_meth(:re, x)
Base.imag(x::Sym) = sympy_meth(:im, x)

## sign-related functions
abs(x::Sym) = sympy_meth(:Abs, x)
abs(a::Array{Sym}) = map(abs, a)
Base.abs2(x::Sym) = re(x*conj(x))
Base.copysign(x::Sym, y::Sym) = abs(x)*sign(y)

#minimum(ex::Sym,x::NAtype) = x
#minimum(ex::Sym, args...; kwargs...) = sympy_meth(:Min, ex, args...; kwargs...)
#maximum(ex::Sym,x::NAtype) = x
#maximum(ex::Sym, args...; kwargs...) = sympy_meth(:Max, ex, args...; kwargs...)

## use SymPy Names here...
Min(ex::Sym, ex1::Sym) = sympy_meth(:Min, ex, ex1)
Max(ex::Sym, ex1::Sym) = sympy_meth(:Max, ex, ex1)

for meth in (:separate, :flatten,
             :igcd, :ilcm,
             :sqf,
             :together,
             :Derivative
             )
    meth_name = string(meth)
    @eval begin
           @doc """
`$($meth_name)`: a SymPy function.
The SymPy documentation can be found through: http://docs.sympy.org/latest/search.html?q=$($meth_name)
""" ->
        ($meth)(ex, args...; kwargs...) = sympy_meth($meth_name, ex, args...; kwargs...) # was ex::Sym
    end
    eval(Expr(:export, meth))
end

## Calculus functions

## bring over for function calls (not expressions)
## returns a symbolic expression
## limit(f, c) or limit(f,c,dir="-") or limit(f, c, dir="-")

"""

Compute symbolic limit of a function

Examples
```
limit(x -> sin(x)/x, 0)
limit(x -> x^x, 0, dir="+")
```
"""
function limit(f::Function, c::Number=0; kwargs...)
    x = Sym("x")
    limit(f(x), x, c; kwargs...)
end

## """

## Limit using pairs notation

## ```
## @vars x y
## ex = 1/(x^2 - y^2)
## limit(ex, x=>3)
## ```
## """

limit(ex::Sym, d::Pair; kwargs...) = limit(ex, d.first, d.second;kwargs...)
## XXX why can I not have dir="+" as keyword argument?
function limit(ex::Sym, d::Pair...)
    for p in d
        ex = limit(ex, p)
    end
    ex
end

limit(ex::Sym, x::Sym, c; kwargs...) = sympy_meth(:limit, ex, x, Sym(c); kwargs...)
limit(ex::Sym, args...; kwargs...) = sympy_meth(:limit, ex, args...; kwargs...)
export limit

function Base.diff(ex::Sym, args...; kwargs...)
    if ex.x[:is_Equality]
        Eq(diff(lhs(ex), args...; kwargs...), diff(rhs(ex), args...; kwargs...))
    else
        sympy_meth(:diff, ex, args...; kwargs...)
    end
end

##

## diff for matrix doesn't handle vectors well, so we vectorize here
diff(exs::Array{Sym}, args...; kwargs...) = map(ex -> diff(ex, args...;kwargs...), exs)

## find symbolic derivatives from a function
function diff(f::Function, k::Int=1; kwargs...)
    x = Sym("x")
    diff(f(x), x, k; kwargs...)
end

## Comparisons Real, Sym
Base.isless(a::Real, b::Sym) = isless(a, convert(Float64, b))
Base.isless(a::Sym, b::Real) = isless(b, a)
Base.isfinite(x::Sym) = isfinite(convert(Float64, x))

## Handle ininf, and isnan by coercion to float
Base.isinf(x::Sym) = try isinf(convert(Float64, x)) catch e false end
Base.isnan(x::Sym) = try isnan(convert(Float64, x)) catch e false end

## we rename sympy.div -> polydiv
Base.div(x::Sym, y::SymOrNumber) = convert(Sym, sympy[:floor](x/convert(Sym,y)))
Base.rem(x::Sym, y::SymOrNumber) = x-Sym(y)*Sym(sympy[:floor](x/y))

## zero and one (zeros?)
Base.zero(x::Sym) = Sym(0)
Base.zero(::Type{Sym}) = Sym(0)

Base.one(x::Sym) = Sym(1)
Base.one(::Type{Sym}) = Sym(1)

#### Piecewise functions

"""

Create a piecewise defined function.

To create conditions on the variable, the functions `Lt`, `Le`, `Eq`, `Ge`, and `Gt` can be used. For infix notation,
unicode operators can be used: `\ll<tab>`, `\le<tab>`, `\Equal<tab>`, `\ge<tab>`, and `\gg<tab>`.

To combine terms, the unicode `\vee<tab>` (for "or"), `\wedge<tab>` (for "and") can be used

Examples:
```
x,a = symbols("x,a")
p = piecewise((1, x ≪ 1), (2, (1 ≤ x) ∨ (x ≤ 2)), (3, x ≫ 2)) ## using ∨ and ∧ for & and or
subs(p, x, 2) ## 2
p = piecewise((1, Lt(x, a)), (2, Ge(x,a)))  # same as piecewise((1,  x ≪ a), (2, x ≥ a))
subs(p, x, a - 1)
```

[Note: there is also an alias `Piecewise` for copy-n-pasting from python code, but despite the capital letter, this is not a constructor for a type.]
"""
function piecewise(args...)
    args = [map(project, x) for x in args]
    sympy_meth(:Piecewise, args...)
end
const Piecewise = piecewise

piecewise_fold(ex::Sym) = sympy_meth(:piecewise_fold, ex)

Base.ifelse(ex::Sym, a, b) = piecewise((a, ex), (b, true))

"""
Indicator function

`Χ(x, a, b)` is `1` on `[a,b]` and 0 otherwise.

"""
Χ(x, a=-oo, b=oo) = piecewise((1, (a <= x) ∧ (x <= b)), (0,true))
Indicator(x, a=-oo, b=oo) = Χ(x, a, b)
export Indicator, Χ

##################################################
## special numbers are initialized after compilation
function init_math()
    "PI is a symbolic  π. Using `julia`'s `pi` will give round off errors."
    global const PI = Sym(sympy[:pi])

    "E is a symbolic  `e`. Using `julia`'s `e` will give round off errors."
    global const E = Sym(sympy[:exp](1))

    "IM is a symbolic `im`"
    global const IM = Sym(sympy[:I])

    "oo is a symbolic infinity. Example: `integrate(exp(-x), x, 0, oo)`."
    global const oo = Sym(sympy[:oo])

    ## math constants
    Base.convert(::Type{Sym}, x::Irrational{:π}) = PI
    Base.convert(::Type{Sym}, x::Irrational{:e}) = E
    Base.convert(::Type{Sym}, x::Irrational{:γ}) = sympy[:EulerGamma]
    Base.convert(::Type{Sym}, x::Irrational{:catalan}) = sympy[:Catalan]
    Base.convert(::Type{Sym}, x::Irrational{:φ}) = (1 + Sym(5)^(1//2))/2
end


## Test grenander and related methods ##
module Test_grenander

using MultipleTesting
using Test

@testset "Grenander estimators" begin

    @testset "Grenander estimator" begin

        pv = [0.2, 0.4, 0.6, 0.8, 1.0]
        p, f, F = MultipleTesting.grenander(pv)
        @test p ≈ pv
        @test f ≈ fill(1.0, size(p))
        @test F ≈ pv

        pv = [0.1, 0.3, 0.5, 0.7, 0.9]
        p, f, F = MultipleTesting.grenander(pv)
        @test p ≈ pv
        @test f ≈ fill(1.0, size(p))
        @test F ≈ pv .+ 0.1

        pv = [0.1, 0.1, 0.3, 0.3, 0.5, 0.5, 0.7, 0.7, 0.9, 0.9]
        p, f, F = MultipleTesting.grenander(pv)
        @test p ≈ unique(pv)
        @test f ≈ fill(1.0, size(p))
        @test F ≈ p .+ 0.1

        pv = [0.1, 0.2, 0.5, 0.6, 0.9]
        p, f, F = MultipleTesting.grenander(pv)
        @test p ≈ pv
        @test f ≈ [2.0, 1.0, 1.0, 2 / 3, 2 / 3]
        @test F ≈ [0.2, 0.4, 0.7, 0.8, 1.0]

    end

    @testset "Isotonic regression" begin

        function isotonic_regression_reference(y::AbstractVector{T}, w::AbstractVector{T}) where T <: AbstractFloat
            y = copy(y)
            w = copy(w)
            cnts = fill(1, length(y))
            i = 2
            while i <= length(y)
                if y[i] < y[i - 1]
                    y[i - 1] = (w[i] * y[i] + w[i - 1] * y[i - 1]) / (w[i] + w[i - 1])
                    w[i - 1] = w[i] + w[i - 1]
                    cnts[i - 1] += cnts[i]
                    deleteat!(y, i)
                    deleteat!(w, i)
                    deleteat!(cnts, i)
                    i = max(i - 2, 1)
                end
                i += 1
            end
            yisotonic = vcat([y[idx] .* fill(1.0, cnt) for (idx, cnt) in enumerate(cnts)]...)
            return yisotonic
        end

        function isotonic_regression_reference(y::AbstractVector{T}) where T <: AbstractFloat
            isotonic_regression_reference(y, fill(1.0, size(y)))
        end

        # pooled adjacent violators example: Robertson, page 10
        @test isotonic_regression_reference([22.5; 23.333; 20.833; 24.25], [3.0;3.0;3.0;2.0]) ≈ [22.222; 22.222; 22.222; 24.25]

        # if input already ordered, then output should be the same
        @test isotonic_regression_reference([1.0; 2.0; 3.0]) ≈ [1.0; 2.0; 3.0]
        @test isotonic_regression_reference([1., 41., 51., 1., 2., 5., 24.], [1., 2., 3., 4., 5., 6., 7.]) ≈ [1.0, 13.95, 13.95, 13.95, 13.95, 13.95, 24.]

        # single value or empty vector remains unchanged
        r = rand(1)
        @test MultipleTesting.isotonic_regression(r) == r
        r = Vector{Float64}()
        @test MultipleTesting.isotonic_regression(r) == r

        r = rand(10)
        @test MultipleTesting.isotonic_regression(r, fill(1.0, size(r))) == MultipleTesting.isotonic_regression(r)
        @test_throws DimensionMismatch MultipleTesting.isotonic_regression(rand(10), ones(5))

    end

end

end


using SEC, Test, Statistics, Distributions, Random
import SEC.Sample: markov_link, markov_chain

@testset "markov link correctness" begin
    Random.seed!(1234)
    nll(X) = 10
    proposal(X) = 2
    current_X = 1
    current_nll = nll(current_X)
    (; new_X, new_nll, proposal_X, proposal_nll) = markov_link(
        nll = nll,
        proposal = proposal,
        current_nll = current_nll,
        current_X = current_X
    )
    @test new_X == 2
    @test new_nll == 10
    @test proposal_X == 2
    @test proposal_nll == 10
end

@testset "markov chain reproducibility" begin
    nll(X) = X^2 / 2
    proposal(X) = X + randn()
    seed_X = 0.0
    chain_length = 10000
    Random.seed!(1234)
    (; chain_X, chain_nll) = markov_chain(nll, proposal, seed_X, chain_length)
    original_chain_X = copy(chain_X)
    original_chain_nll = copy(chain_nll)
    Random.seed!(1234)
    (; chain_X, chain_nll) = markov_chain(nll, proposal, seed_X, chain_length)
    @test all((original_chain_X - chain_X) .== 0.0)
    @test all((original_chain_nll - chain_nll) .== 0.0)
    # hand unrolling loop
    Random.seed!(1234)
    current_X = 0.0
    current_nll = nll(current_X)
    proposal_X = proposal(seed_X)
    proposal_nll = nll(proposal_X)
    Δ = current_nll - proposal_nll
    accept_bool = log(rand(Uniform(0, 1))) < Δ
    current_X = accept_bool ? proposal_X : current_X
    @test chain_X[2] == current_X
end

@testset "markov chain convergence" begin
    Random.seed!(1234)
    nll(X) = X^2 / 2
    proposal(X) = X + randn()
    seed_X = 0.15
    chain_length = 10000
    (; chain_X, chain_nll) = markov_chain(nll, proposal, seed_X, chain_length)
    @test abs(mean(chain_X)) < 1 / sqrt(chain_length)
    @test abs(std(chain_X) - 1.0) < 1 / sqrt(chain_length)
end