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

# Stage-decomposition generation #
# ============================== #
function generate!(stochasticprogram::StochasticProgram{N}, structure::StageDecompositionStructure{N}) where N
    # Generate all stages
    for stage in 1:N
        generate!(stochasticprogram, structure, stage)
    end
    return nothing
end

function generate!(stochasticprogram::StochasticProgram{N}, structure::StageDecompositionStructure{N}, stage::Integer) where N
    1 <= stage <= N || error("Stage $stage not in range 1 to $N.")
    if stage == 1
        # Check generators
        has_generator(stochasticprogram, :stage_1) || error("First-stage problem not defined in stochastic program. Consider @stage 1.")
        # Set the optimizer (if any)
        if has_provided_optimizer(stochasticprogram.optimizer)
            master_opt = master_optimizer(stochasticprogram)
            if master_opt != nothing
                set_optimizer(structure.first_stage, master_opt)
            end
        end
        # Prepare decisions
        structure.first_stage.ext[:decisions] = Decisions((structure.decisions[1],))
        add_decision_bridges!(structure.first_stage)
        # Generate first stage
        generator(stochasticprogram, :stage_1)(structure.first_stage, stage_parameters(stochasticprogram, 1))
    else
        # Check generators
        stage_key = Symbol(:stage_, stage)
        decision_key = Symbol(:stage_, stage - 1, :_decisions)
        has_generator(stochasticprogram, stage_key) || error("Stage problem $stage not defined in stochastic program. Consider @stage $stage.")
        has_generator(stochasticprogram, decision_key) || error("No decision variables defined in stage problem $(stage-1).")
        # Sanity check on scenario probabilities
        if num_scenarios(stochasticprogram, stage) > 0
            p = stage_probability(stochasticprogram, stage)
            abs(p - 1.0) <= 1e-6 || @warn "Scenario probabilities do not add up to one. The probability sum is given by $p"
        end
        # Generate
        generate_vertical!(scenarioproblems(structure, stage),
                           stage,
                           generator(stochasticprogram, decision_key),
                           generator(stochasticprogram, stage_key),
                           stage_parameters(stochasticprogram, stage - 1),
                           stage_parameters(stochasticprogram, stage),
                           structure.decisions[stage],
                           subproblem_optimizer(stochasticprogram))
    end
    return nothing
end

function generate_vertical!(scenarioproblems::ScenarioProblems,
                            stage::Integer,
                            decision_generator::Function,
                            generator::Function,
                            decision_params::Any,
                            stage_params::Any,
                            decision_map::DecisionMap,
                            optimizer)
    for i in num_subproblems(scenarioproblems)+1:num_scenarios(scenarioproblems)
        # Create subproblem
        subproblem = optimizer == nothing ? Model() : Model(optimizer)
        # Prepare decisions
        decisions = ntuple(Val{stage}()) do s
            if s == stage - 1
                # Known decisions from the previous stages are
                # the same everywhere.
                decision_map
            else
                # Remaining decisions are unique to each subproblem
                DecisionMap()
            end
        end
        subproblem.ext[:decisions] = Decisions(decisions; is_node = true)
        add_decision_bridges!(subproblem)
        # Generate and return the stage model
        decision_generator(subproblem, decision_params)
        generator(subproblem, stage_params, scenario(scenarioproblems, i))
        push!(scenarioproblems.problems, subproblem)
    end
    return nothing
end
function generate_vertical!(scenarioproblems::DistributedScenarioProblems,
                            stage::Integer,
                            decision_generator::Function,
                            generator::Function,
                            decision_params::Any,
                            stage_params::Any,
                            ::DecisionMap,
                            optimizer)
    @sync begin
        for w in workers()
            @async remotecall_fetch(
                w,
                scenarioproblems[w-1],
                stage,
                decision_generator,
                generator,
                decision_params,
                stage_params,
                scenarioproblems.decisions[w-1],
                optimizer) do sp, stage, dgenerator, generator, dparams, params, decisions, opt
                    generate_vertical!(fetch(sp),
                                       stage,
                                       dgenerator,
                                       generator,
                                       dparams,
                                       params,
                                       fetch(decisions),
                                       opt)
                end
        end
    end
    return nothing
end

function clear!(structure::StageDecompositionStructure{N}) where N
    # Clear decisions
    clear!(structure.decisions)
    # Clear all stages
    for stage in 1:N
        clear_stage!(structure, stage)
    end
    return nothing
end

function clear_stage!(structure::StageDecompositionStructure{N}, s::Integer) where N
    1 <= s <= N || error("Stage $s not in range 1 to $N.")
    if s == 1
        empty!(structure.first_stage)
    else
        clear!(scenarioproblems(structure, s))
    end
    return nothing
end

# Getters #
# ========================== #
function first_stage(stochasticprogram::StochasticProgram, structure::StageDecompositionStructure; optimizer = nothing)
    if optimizer == nothing
        return structure.first_stage
    end
    stage_one = copy(structure.first_stage)
    set_optimizer(stage_one, optimizer)
    return stage_one
end


## Julia script to randomly decide the author of first authors
## Claudia (November 2020)

using Random
s = 9159277212
Random.seed!(s);
people = ["chunrong", "liule", "luke", "zhiwen"] ##alphabetical
people[randperm(length(people))]


using ArgParse
using JLD,MAT,JSON

include("util.jl")

function main(args)
    s = ArgParseSettings()
    s.description = "CNN-HMM hybrid ASR system in Knet by Ilker Kesen."

    @add_arg_table s begin
        ("--jsonfile"; required=true)
        ("--savefile"; required=true)
    end

    isa(args, AbstractString) && (args=split(args))
    o = parse_args(args, s; as_symbols=true); println(o); flush(STDOUT)

    jsondata = JSON.parsefile(abspath(o[:jsonfile]))
    p2i, i2p = build_vocab(jsondata)

    posteriors, priors = get_probabilities(jsondata, p2i, i2p)
    save(o[:savefile], "posteriors", posteriors, "priors", priors)
end

!isinteractive() && !isdefined(Core.Main, :load_only) && main(ARGS)


module SimpleMeshes

using OffsetArrays
using LinearAlgebra
using DataStructures

include("points.jl")
include("faces.jl")

include("meshes/base.jl")
include("meshes/mesh.jl")
include("meshes/halfedge.jl")

include("utils.jl")

include("io/files.jl")
include("io/ply.jl")

include("lab1.jl")

export AbstractPoint, Point, MetaPoint,
  AbstractPolygon, Polygon, Triangle,
  AbstractMesh, Mesh, TriangleMesh, HalfEdgeMesh,
  face, vertex, edges,
  File, FileFormat, format_str, add_format, load,
  faceswith, vneighbors, fneighbors, traverse, flipdiag!

end


# Object-based wrapper for BLIS utility routines.
#

macro blis_object_api_lvutil_mkspetial(funcname)
    return quote
        @blis_ccall_object($funcname,
                           Cvoid,
                           a,      xObj)
    end
end

@blis_object_api_lvutil_mkspetial mkherm
@blis_object_api_lvutil_mkspetial mksymm
@blis_object_api_lvutil_mkspetial mktrim

macro blis_object_api_lvutil_norm(funcname)
    return quote
        @blis_ccall_object($funcname,
                           Cvoid,
                           x,      xObj,
                           norm,   xObj)
    end
end

@blis_object_api_lvutil_norm norm1v
@blis_object_api_lvutil_norm normfv
@blis_object_api_lvutil_norm normiv
@blis_object_api_lvutil_norm norm1m
@blis_object_api_lvutil_norm normfm
@blis_object_api_lvutil_norm normim

macro blis_object_api_lvutil_rand(funcname)
    return quote
        @blis_ccall_object($funcname,
                           Cvoid,
                           x,      xObj)
    end
end

@blis_object_api_lvutil_rand randv
@blis_object_api_lvutil_rand randnv
@blis_object_api_lvutil_rand randm
@blis_object_api_lvutil_rand randnm

macro blis_object_api_lvutil_print(funcname)
    return quote
        @blis_ccall_object($funcname,
                           Cvoid,
                           s1,     Ptr{Cchar},
                           x,      xObj,
                           format, Ptr{Cchar},
                           s2,     Ptr{Cchar})
    end
end

@blis_ccall_object(asumv,
                   Cvoid,
                   x,     xObj,
                   asum,  xObj)
@blis_ccall_object(sumsqv,
                   Cvoid,
                   x,     xObj,
                   scale, xObj,
                   sumsq, xObj)


using Test
using NQCDynamics
using NQCDynamics: Calculators
using Unitful

atoms = Atoms([:C, :H])
classical_sim = Simulation(atoms, Harmonic())
ring_polymer_sim = RingPolymerSimulation(atoms, Harmonic(), 10; temperature=100u"K")
sims = [classical_sim, ring_polymer_sim]

name(sim) = typeof(sim).name.wrapper

@testset "classical_potential_energy : $(name(sim))" for sim in sims
    u = DynamicsVariables(sim, randn(size(sim)), randn(size(sim)))
    r = DynamicsUtils.get_positions(u)
    @test DynamicsUtils.classical_potential_energy(sim, r) == DynamicsUtils.classical_potential_energy(sim, u)
end

@testset "classical_kinetic_energy : $(name(sim))" for sim in sims
    u = DynamicsVariables(sim, randn(size(sim)), randn(size(sim)))
    v = DynamicsUtils.get_velocities(u)
    @test DynamicsUtils.classical_kinetic_energy(sim, v) == DynamicsUtils.classical_kinetic_energy(sim, u)
end

@testset "divide_by_mass! : $(name(sim))" for sim in sims
    dv = randn(size(sim))
    true_value = copy(dv)
    for I in CartesianIndices(dv)
        true_value[I] /= masses(sim)[I[2]]
    end
    DynamicsUtils.divide_by_mass!(dv, masses(sim))
    @test true_value ≈ dv
end

rs = (fill(0.0, 1, 1), fill(0.0, 1, 1, 1))
calculators = (
    Calculators.Calculator(DoubleWell(), 1, Float64),
    Calculators.Calculator(DoubleWell(), 1, 1, Float64),
    )
@testset "transform_density!" for (r, calc) ∈ zip(rs, calculators)
    density = [1.0 0; 0 0]
    DynamicsUtils.transform_density!(density, calc, r, :to_adiabatic)
    @test density ≈ [0.5 0.5; 0.5 0.5]
    DynamicsUtils.transform_density!(density, calc, r, :to_diabatic)
    @test density ≈ [1.0 0; 0 0]
    @test_throws ArgumentError DynamicsUtils.transform_density!(density, calc, r, :blah)
end

@testset "initialise_adiabatic_density_matrix" for (r, calc) ∈ zip(rs, calculators)
    @testset "Adiabatic" begin
        electronics = PureState(1, Adiabatic())
        density = DynamicsUtils.initialise_adiabatic_density_matrix(electronics, calc, r)
        @test density[1,1] == 1
    end

    @testset "Diabatic" begin
        electronics = PureState(1, Diabatic())
        density = DynamicsUtils.initialise_adiabatic_density_matrix(electronics, calc, r)
        @test all(density .≈ 0.5)
    end
end


module Mancala
  export Game, Board
  include("game.jl")
  module Training
    using AlphaZero
    include("params.jl")
  end
end


module LowLevelJPEG

using AbstractTrees
using ColorTypes
using FixedPointNumbers
using StaticArrays

import Base: ==
export JPEGGray, JPEGGray8, JPEGGray16, JPEGYCbCr, JPEGYCbCr8, JPEGYCbCr16,
       Marker, Markers,
       CodeBits,
       DCSymbol, ACSymbol, DCTCoeff, DCCoeff, ACCoeff,
       DCTBlock,
       QuantizationTable, QuantizationTable8, QuantizationTable16,
       quantize, dequantize,
       HuffmanTreeNode, HuffmanBranchNode,
       HuffmanSymbolNode, DCSymbolNode, ACSymbolNode,
       HuffmanTree, DCHuffmanTree, ACHuffmanTree,
       HuffmanTable, DCHuffmanTable, ACHuffmanTable

u8(io::IO) = read(io, UInt8)
u16(io::IO) = ntoh(read(io, UInt16))
u4u4(io::IO) = (b = u8(io); (b >> 0x4, b & 0xF))

hex(x::Unsigned, pad=2length(x)) = string(x, base=16, pad=pad)

include("colors.jl")
include("markers.jl")
include("codebits.jl")
include("coeffs.jl")
include("blocks.jl")
include("qt.jl")
include("huffman.jl")

end # module


# Collections of data

struct EdgeGradient{C <: CellType,D <: CellType, NX,NY, T<: Number} <: GridData{NX,NY,T}
  dudx :: Nodes{C,NX,NY,T}
  dvdy :: Nodes{C,NX,NY,T}
  dudy :: Nodes{D,NX,NY,T}
  dvdx :: Nodes{D,NX,NY,T}
end

function EdgeGradient(T::Type{C}, dualnodedims::Tuple{Int, Int};dtype=Float64) where {C <: CellType}
    dudxdims = node_inds(T, dualnodedims)
    dudydims = node_inds(othertype(C), dualnodedims)

    EdgeGradient{T, othertype(T), dualnodedims...,dtype}(
            Nodes(T,dualnodedims,dtype=dtype),Nodes(T,dualnodedims,dtype=dtype),
            Nodes(othertype(T),dualnodedims,dtype=dtype),Nodes(othertype(T),dualnodedims,dtype=dtype)
            )
end

(::Type{EdgeGradient{R,S,NX,NY,T}})() where {R<:CellType,S<:CellType,NX,NY,T} = EdgeGradient(R, (NX, NY),dtype=T)

EdgeGradient(C, ::GridData{NX,NY,T}; dtype=T) where {NX, NY,T} = EdgeGradient(C, (NX,NY),dtype=dtype)

Base.similar(::EdgeGradient{R,S,NX,NY,T};element_type=T) where {R,S,NX,NY,T} = EdgeGradient(R, (NX, NY),dtype=element_type)

function Base.fill!(g::EdgeGradient, s::Number)
    fill!(g.dudx, s)
    fill!(g.dudy, s)
    fill!(g.dvdx, s)
    fill!(g.dvdy, s)
    g
end

Base.size(A::EdgeGradient{C,D,NX,NY,T}) where {C,D,NX,NY,T} = (2length(A.dudx)+2length(A.dudy),1)
@propagate_inbounds Base.getindex(A::EdgeGradient{C,D,NX,NY,T},i::Int) where {C,D,NX,NY,T} =
   i > length(A.dudx) ? (i > length(A.dudx)+length(A.dudy) ? (i > length(A.dudx)+2length(A.dudy) ? A.dvdy[i-length(A.dudx)-2length(A.dudy)] : A.dvdx[i-length(A.dudx)-length(A.dudy)]) : A.dudy[i-length(A.dudx)]) : A.dudx[i]
@propagate_inbounds Base.setindex!(A::EdgeGradient{C,D,NX,NY,T}, v, i::Int) where {C,D,NX,NY,T} =
   i > length(A.dudx) ? (i > length(A.dudx)+length(A.dudy) ? (i > length(A.dudx)+2length(A.dudy) ? A.dvdy[i-length(A.dudx)-2length(A.dudy)] = convert(T, v) : A.dvdx[i-length(A.dudx)-length(A.dudy)] = convert(T, v)) : A.dudy[i-length(A.dudx)]= convert(T, v)) : A.dudx[i] = convert(T, v)
Base.IndexStyle(::Type{<:EdgeGradient}) = IndexLinear()

function Base.show(io::IO, nodes::EdgeGradient{R, S, NX, NY, T}) where {R, S, NX, NY, T}
    nodedims = "(nx = $NX, ny = $NY)"
    udims = "(nx = $(size(nodes.dudx,1)), ny = $(size(nodes.dudx,2)))"
    vdims = "(nx = $(size(nodes.dudy,1)), ny = $(size(nodes.dudy,2)))"
    println(io, "Edge gradient data in a $nodedims cell grid of type $T data")
    println(io, "  du/dx and dv/dy as $R nodes : $udims")
    println(io, "  du/dy and dv/dx as $S nodes : $vdims")
end

struct NodePair{C <: CellType,D <: CellType, NX,NY,T} <: GridData{NX,NY,T}
  u :: Nodes{C,NX,NY,T}
  v :: Nodes{D,NX,NY,T}
end

function NodePair(R::Type{C},S::Type{D},dualnodedims::Tuple{Int,Int};dtype=Float64) where {C <: CellType, D <: CellType}
    udims = node_inds(R, dualnodedims)
    vdims = node_inds(S, dualnodedims)

    NodePair{R, S, dualnodedims...,dtype}(
            Nodes(R,dualnodedims,dtype=dtype),
            Nodes(S,dualnodedims,dtype=dtype)
            )
end
NodePair(R::Type{C}, U::Type{D},nodes::Nodes{S,NX,NY,T}) where
        {C <: CellType, D <: CellType, S <: CellType, NX,NY,T} = NodePair(R, U, (NX, NY),dtype=T)

NodePair(R::Type{C},dualnodedims::Tuple{Int,Int};dtype=Float64) where {C <: CellType}= NodePair(R,R,dualnodedims,dtype=dtype)

NodePair(R::Type{C}, nodes::Nodes{S,NX,NY,T}) where {C <: CellType, S <: CellType, NX,NY,T} = NodePair(R, (NX, NY),dtype=T)

(::Type{NodePair{R,S,NX,NY,T}})() where {R<:CellType,S<:CellType,NX,NY,T} = NodePair(R, S, (NX, NY),dtype=T)

NodePair(R, S, ::GridData{NX,NY,T};dtype=T) where {NX, NY,T} = NodePair(R, S, (NX,NY),dtype=dtype)

Base.similar(::NodePair{R,S,NX,NY,T};element_type=T) where {R<:CellType,S<:CellType,NX,NY,T} = NodePair(R, S, (NX, NY),dtype=element_type)

function Base.show(io::IO, nodes::NodePair{R, S, NX, NY, T}) where {R, S, NX, NY, T}
    nodedims = "(nx = $NX, ny = $NY)"
    udims = "(nx = $(size(nodes.u,1)), ny = $(size(nodes.u,2)))"
    vdims = "(nx = $(size(nodes.v,1)), ny = $(size(nodes.v,2)))"
    println(io, "($R,$S) node data pair in a $nodedims cell grid of type $T data")
    println(io, "  Number of $R nodes: $udims")
    println(io, "  Number of $S nodes: $vdims")
end

function fill!(nodes::NodePair, s::Number)
    fill!(nodes.u, s)
    fill!(nodes.v, s)
    nodes
end

Base.size(A::NodePair{C,D,NX,NY,T}) where {C,D,NX,NY,T} = (length(A.u)+length(A.v),1)
@propagate_inbounds Base.getindex(A::NodePair{C,D,NX,NY,T},i::Int) where {C,D,NX,NY,T} =
   i > length(A.u) ? A.v[i-length(A.u)] : A.u[i]
@propagate_inbounds Base.setindex!(A::NodePair{C,D,NX,NY,T}, v, i::Int) where {C,D,NX,NY,T} =
   i > length(A.u) ? A.v[i-length(A.u)] = convert(T, v) : A.u[i] = convert(T, v)
Base.IndexStyle(::Type{<:NodePair}) = IndexLinear()


import Knet.Ops21: batchnorm
using AutoGrad: value

using CUDA.CUDNN:
    cudnnNormalizationForward,
    cudnnNormalizationForward!,
    cudnnNormalizationForwardInference,
    cudnnNormalizationForwardTraining,
    cudnnNormalizationBackward,
    cudnnGetNormalizationForwardTrainingWorkspaceSize,
    cudnnGetNormalizationTrainingReserveSpaceSize,
    cudnnActivationDescriptor,
    cudnnTensorDescriptor,
    cudnnNormMode_t,
        CUDNN_NORM_PER_ACTIVATION, # 0, bnScale, bnBias tensor dims are 1xCxHxWx.. (one value per CHW...-slice, normalized over N slice)
        CUDNN_NORM_PER_CHANNEL,    # 1, bnScale, bnBias tensor dims are 1xCx1x1 (one value per C-dim normalized over Nx1xHxW subtensors)
    cudnnNormOps_t,
        CUDNN_NORM_OPS_NORM,                # 0, /* do normalization only */
        CUDNN_NORM_OPS_NORM_ACTIVATION,     # 1, /* do Norm, then activation */
        CUDNN_NORM_OPS_NORM_ADD_ACTIVATION, # 2, /* do Norm, then elemWiseAdd, then activation */
    cudnnNormAlgo_t,
        CUDNN_NORM_ALGO_STANDARD, # 0
        CUDNN_NORM_ALGO_PERSIST,  # 1
    cudnnActivationMode_t,
        CUDNN_ACTIVATION_SIGMOID,      # 0
        CUDNN_ACTIVATION_RELU,         # 1
        CUDNN_ACTIVATION_TANH,         # 2
        CUDNN_ACTIVATION_CLIPPED_RELU, # 3
        CUDNN_ACTIVATION_ELU,          # 4
        CUDNN_ACTIVATION_IDENTITY,     # 5
    cudnnNanPropagation_t,
        CUDNN_NOT_PROPAGATE_NAN, # 0
        CUDNN_PROPAGATE_NAN,     # 1
    cudnnTensorFormat_t,
        CUDNN_TENSOR_NCHW,        # 0, /* row major (wStride = 1, hStride = w) */
        CUDNN_TENSOR_NHWC,        # 1, /* feature maps interleaved ( cStride = 1 )*/
        CUDNN_TENSOR_NCHW_VECT_C, # 2, /* each image point is vector of element of C, vector length in data type */
    handle

function batchnorm(
    x::GPUVal, mean_estimate::GPUVal, var_estimate::GPUVal, bias::GPUVal, scale::GPUVal;
    use_estimates = !Knet.training(),
    update = Knet.training() ? 0.1 : 0.0,
    epsilon = 1e-5,
    mode = nothing,
    format = nothing,
    savedMean = nothing,
    savedVar = nothing,
    workspace = nothing,
    reserveSpace = nothing,
    dx = Ref{Any}(nothing),
    dscale = Ref{Any}(nothing),
    dbias = Ref{Any}(nothing),
    o...)
    @assert size(mean_estimate) == size(var_estimate) == size(bias) == size(scale)
    n = ndims(x)
    if size(mean_estimate) == ntuple(i->(i===n-1 ? size(x,i) : 1), n)
        mode === nothing ? mode = CUDNN_NORM_PER_CHANNEL : @assert mode === CUDNN_NORM_PER_CHANNEL
        format === nothing ? format = CUDNN_TENSOR_NCHW : @assert format === CUDNN_TENSOR_NCHW
    elseif size(mean_estimate) == ntuple(i->(i===1 ? size(x,i) : 1), n)
        mode === nothing ? mode = CUDNN_NORM_PER_CHANNEL : @assert mode === CUDNN_NORM_PER_CHANNEL
        format === nothing ? format = CUDNN_TENSOR_NHWC : @assert format === CUDNN_TENSOR_NHWC
    elseif size(mean_estimate) == ntuple(i->(i===n ? 1 : size(x,i)), n)
        mode === nothing ? mode = CUDNN_NORM_PER_ACTIVATION : @assert mode === CUDNN_NORM_PER_ACTIVATION
        format === nothing ? format = CUDNN_TENSOR_NCHW : @assert format === CUDNN_TENSOR_NCHW
    else
        error("Unsupported batchnorm size x=$(size(x)) m=$(size(m))")
    end
    # default training  => update > 0, use_estimates=false, gradients calculated
    # default inference => update = 0, use_estimates=true,  gradients not calculated
    # Other combinations must be manually implemented
    kw = (; mode, format, epsilon, savedMean, savedInvVariance=savedVar, workspace, reserveSpace, dx, dscale, dbias)
    if Knet.training() && !use_estimates && update == 0
        cudnnNormalizationForward(x, nothing, nothing, bias, scale; training=true, exponentialAverageFactor=0, kw...)
    elseif Knet.training() && !use_estimates && update > 0
        (mean_estimate, var_estimate) = value.((mean_estimate, var_estimate))
        cudnnNormalizationForward(x, mean_estimate, var_estimate, bias, scale; training=true, exponentialAverageFactor=update, kw...)
    elseif Knet.training() && use_estimates && update == 0
        ((x .- mean_estimate) ./ sqrt.(epsilon .+ var_estimate)) .* scale .+ bias
    elseif Knet.training() && use_estimates && update > 0
        update_estimates!(x, mean_estimate, var_estimate, update)
        ((x .- mean_estimate) ./ sqrt.(epsilon .+ var_estimate)) .* scale .+ bias
    elseif !Knet.training() && !use_estimates && update == 0
        cudnnNormalizationForward(x, nothing, nothing, bias, scale; training=true, exponentialAverageFactor=0, kw...)
    elseif !Knet.training() && !use_estimates && update > 0
        (mean_estimate, var_estimate) = value.((mean_estimate, var_estimate))
        cudnnNormalizationForward(x, mean_estimate, var_estimate, bias, scale; training=true, exponentialAverageFactor=update, kw...)
    elseif !Knet.training() && use_estimates && update == 0
        (mean_estimate, var_estimate) = value.((mean_estimate, var_estimate))
        cudnnNormalizationForward(x, mean_estimate, var_estimate, bias, scale; training=false, kw...)
    elseif !Knet.training() && use_estimates && update > 0
        (mean_estimate, var_estimate) = value.((mean_estimate, var_estimate))
        update_estimates!(x, mean_estimate, var_estimate, update)
        cudnnNormalizationForward(x, mean_estimate, var_estimate, bias, scale; training=false, kw...)
    end
end

function update_estimates!(x, mean_estimate, var_estimate, update)
    (x, mean_estimate, var_estimate, update) = value.((x, mean_estimate, var_estimate, update))
    dims = findall(size(mean_estimate) .== 1)
    xmean = mean(x; dims)
    xvar  = var(x; dims, mean=xmean, corrected=false)
    update = eltype(x)(update)
    mean_estimate .= xmean * update + mean_estimate * (1-update)
    var_estimate  .= xvar  * update + var_estimate  * (1-update)
end


## Defines all hashing functionality
## As usual, check out http://www.lysator.liu.se/~nisse/nettle/nettle.html#Hash-functions

import Base: show
export HashState, HashAlgorithm, HashAlgorithms

# All Hash Algorithms derive from this abstract type
abstract HashAlgorithm

# This is our rather poorly named list of HashAlgorithm types
HashAlgorithms = DataType[]

# This is the user-facing type that is used to actually hash stuff
type HashState{T<:HashAlgorithm}
  ctx::Array{Uint8,1}
end

# This is a mirror of the nettle-meta.h:nettle_hash struct
immutable NettleHash
    name::Ptr{Uint8}
    context_size::Cuint
    digest_size::Cuint
    block_size::Cuint
    init::Ptr{Void}     # nettle_hash_init_func
    update::Ptr{Void}   # nettle_hash_update_func
    digest::Ptr{Void}   # nettle_hash_digest_func
end

# We're going to load in each nettle_hash struct individually, deriving
# HashAlgorithm types off of the names we find, and calculating the output
# and context size from the data members in the C structures
begin
  hash_idx = 1
  while( true )
    nhptr = unsafe_load(cglobal(("nettle_hashes",nettle),Ptr{Ptr{Void}}),hash_idx)
    if nhptr == C_NULL
      break
    end
    nh = unsafe_load(convert(Ptr{NettleHash}, nhptr))

    # Otherwise, we continue on to derive the information from this struct
    name = symbol(uppercase(bytestring(nh.name)))

    # Load in the Ptr{Uint32}'s
    ctx_size = nh.context_size
    dgst_size = nh.digest_size
    block_size = nh.block_size

    # Save the function pointers as well
    fptr_init = nh.init
    fptr_update = nh.update
    fptr_digest = nh.digest

    # First, create the type itself
    @eval immutable $name <: HashAlgorithm; end

    # Next, record all the important information about this hash algorithm
    @eval output_size(::Type{$name}) = $(convert(Int,dgst_size))
    @eval ctx_size(::Type{$name}) = $(convert(Int,ctx_size))
    @eval hash_type(::Type{$name}) = $nhptr

    # Generate the init, update, and digest functions while we're at it!
    # Since we have the function pointers from nh, we'll use those
    @eval function HashState(::Type{$name})
      ctx = Array(Uint8, ctx_size($name))
      ccall($fptr_init,Void,(Ptr{Void},),ctx)
      HashState{$name}(ctx)
    end

    @eval function update!(state::HashState{$name},data)
      ccall($fptr_update,Void,(Ptr{Void},Csize_t,Ptr{Uint8}),state.ctx,sizeof(data),pointer(data))
      state
    end

    @eval function digest!(state::HashState{$name})
      dgst = Array(Uint8,output_size($name))
      ccall($fptr_digest,Void,(Ptr{Void},Uint32,Ptr{Uint8}),state.ctx,sizeof(dgst),pointer(dgst))
      dgst
    end

    # Generate e.g. sha256_hash(string) and sha256_hmac(string)
    name_hash = symbol("$(bytestring(nh.name))_hash")
    @eval $name_hash(string) = digest!(update!(HashState($name), string))

    name_hmac = symbol("$(bytestring(nh.name))_hmac")
    @eval $name_hmac(key, string) = digest!(update!(HMACState($name, key), string))

    # Add this type into the HashAlgorithms group
    @eval push!(HashAlgorithms, $name)

    # Finally, export the type we just created
    for sym in [name, name_hash, name_hmac]
        eval(current_module(), Expr(:toplevel, Expr(:export, sym)))
    end

    hash_idx += 1
  end
end

function show{T<:HashAlgorithm}( io::IO, ::HashState{T} )
  write(io, "$(string(T)) Hash state")
end


#=
constraints:
- species groups e.g. as polytomies (type 1), or
- clades in the major tree (type 2), or
- clades in *some* displayed tree (type 3, not implemented).
outgroups "grades" can be coded as ingroup clades

move types, all under topological constraints:
- NNI = nearest neighbor interchange for undirected network,
  similar to NNIs for rooted networks defined in
  Gambette, van Iersel, Jones, Lafond, Pardi and Scornavacca (2017),
  Rearrangement moves on rooted phylogenetic networks.
  PLOS Computational Biology 13(8):e1005611.
- move the root: needed for optimization of a semi-directed network,
  because the root position affects the feasibility of the
  NNIs starting from a BR configuration (bifurcation -> reticulation).

- remove a hybrid edge: `deletehybridedge!` in file deleteHybrid.jl,
  but does not check for clade constraints
- add a hybrid edge: in file addHybrid.jl
- change the direction of a hybrid edge

in `moves_snaq.jl`, functions are tailored to level-1 networks;
here, functions apply to semidirected networks of all levels.
=#

"""
Type for various topological constraints, such as:

1. a set of taxa forming a clade in the major tree
2. a set of individuals belonging to the same species
3. a set of taxa forming a clade in any one of the displayed trees
"""
mutable struct TopologyConstraint
    "type of constraint: 1=species, 2=clade in major tree. 3=clade in some displayed tree: not yet implemented."
    type::UInt8
    "names of taxa in the constraint (clade / species members)"
    taxonnames::Vector{String}
    """
    node numbers of taxa in the constraint (clade / species members).
    warning: interpretation dependent on the internal network representation
    """
    taxonnums::Set{Int}
    "stem edge of the constrained group"
    edge::Edge
    "crown node, that is, child node of stem edge"
    node::Node
end

"""
    TopologyConstraint(type::UInt8, taxonnames::Vector{String}, net::HybridNetwork)

Create a topology constraint from user-given type, taxon names, and network.
There are 3 types of constraints:

- type 1: A set of tips that forms a species.
- type 2: A set of tips that forms a clade in the major tree.
  Note that the root matters. Constraining a set of species to be an outgroup
  is equivalent to constraining the ingroup to form a clade.
- type 3: A set of tips that forms a clade in any one of the displayed trees.

Note: currently, with type-1 constraints, hybridizations are prevented
from coming into or going out of a species group.

# examples

```jldoctest
julia> net = readTopology("(((2a,2b),(((((1a,1b,1c),4),(5)#H1),(#H1,(6,7))))#H2),(#H2,10));");

julia> c_species1 = PhyloNetworks.TopologyConstraint(0x01, ["1a","1b","1c"], net)
Species constraint, on tips: 1a, 1b, 1c
 stem edge number 7
 crown node number -9

julia> c_species2 = PhyloNetworks.TopologyConstraint(0x01, ["2a","2b"], net)
Species constraint, on tips: 2a, 2b
 stem edge number 3
 crown node number -4

julia> nni!(net , net.edge[3], true, true, [c_species2]) === nothing # we get nothing: would break species 2
true

julia> # the following gives an error, because 4,5 do not form a clade in "net"
       # PhyloNetworks.TopologyConstraint(0x02, ["4","5"], net)

julia> c_clade145 = PhyloNetworks.TopologyConstraint(0x02, ["1a","1b","1c","4","5"], net)
Clade constraint, on tips: 1a, 1b, 1c, 4, 5
 stem edge number 12
 crown node number -7

julia> nni!(net , net.edge[12], true, true, [c_species1, c_clade145]) # we get nothing (failed NNI): would break the clade
```
"""
function TopologyConstraint(type::UInt8, taxonnames::Vector{String}, net::HybridNetwork)

    ntax_clade = length(taxonnames)
    ntax_clade >= 2 ||
        error("there must be 2 or more taxon name in a clade or species constraint.")
    taxonnums = Set{Int}()
    indices = findall(in(taxonnames), [n.name for n in net.leaf])
    length(indices) == ntax_clade || error("taxa cannot be matched to nodes. Check for typos.")
    outsideclade = setdiff([n.name for n in net.leaf], taxonnames)
    # find the set of node numbers that correspond to taxonnames
    for (i,taxon) in enumerate(taxonnames)
        index = findfirst(n -> n.name == taxon, net.leaf)
        !isnothing(index) || error("taxon $taxon is not found in the network. Check for typos.")
        push!(taxonnums, net.leaf[index].number) # note: not ordered as in taxonnames
    end
    # get interior ancestor edges in major tree (no hybrids)
    matrix = hardwiredClusters(majorTree(net), vcat(taxonnames, outsideclade))
    # look for row with ones in relevant columns, zeros everywhere else (or zeros there and ones everywhere else)
    edgenum = 0 # 0 until we find the stem edge
    comparator = zeros(Int8, size(matrix)[2]-2)
    comparator[1:ntax_clade] = ones(Int8, ntax_clade)
    # go through matrix row by row to find match with comparator. Return number
    for i in 1:size(matrix, 1)
        if matrix[i,2:size(matrix)[2]-1] == comparator # found the mrca!
            edgenum = matrix[i, 1]
            break
        end
    end
    if edgenum == 0
        comparator .= 1 .- comparator
        for i in 1:size(matrix)[1]
            if matrix[i,2:size(matrix)[2]-1] == comparator
                error("""The clade given is not rooted correctly, making it a grade instead of a clade.
                You can re-try after modifying the root of your network.""")
            end
        end
        error("The taxa given do not form a clade in the network")
    end
    edgei = findfirst(e -> e.number == edgenum, net.edge)
    edgei !== nothing || error("hmm. hardwiredClusters on the major tree got an edge number not in the network")
    stemedge = net.edge[edgei]
    mrcanode = getChild(stemedge)
    TopologyConstraint(type, taxonnames, taxonnums, stemedge, mrcanode)
end
function Base.show(io::IO, obj::TopologyConstraint)
    str = (obj.type == 0x01 ? "Species constraint, on tips: " : "Clade constraint, on tips: " )
    str *= join(obj.taxonnames, ", ")
    str *= "\n stem edge number $(obj.edge.number)\n crown node number $(obj.node.number)"
    print(io, str)
end

"""
    constraintviolated(network::HybridNetwork,
                       constraints::Vector{TopologyConstraint})

True if `network` violates one (or more) of the constraints of type 1
(individuals in a species group) or type 2 (must be clades in the major tree).
Warning: constraints of type 3 are not implemented.
"""
function constraintviolated(net::HybridNetwork, constraints::Vector{TopologyConstraint})
    # fixit next PR: add option to give a vector of constraint types to check,
    #                then only check these constraint types
    if isempty(constraints)
        return false
    end # avoids extracting major tree when no constraint
    tree = majorTree(net)
    for con in constraints # checks directionality of stem edge hasn't changed
        if con.type in [0x01, 0x02]
            getChild(con.edge) === con.node || return true
            tei = findfirst(e -> e.number == con.edge.number, tree.edge)
            tei !== nothing ||
                error("hmm. edge number $(con.edge.number) was not found in the network's major tree")
            treeedge = tree.edge[tei]
            des = descendants(treeedge) # vector of node numbers, descendant tips only by default
            Set(des) == con.taxonnums || return true
        end
    end
    return false
end

"""
    updateconstraints!(constraints::Vector{TopologyConstraint}, net::HybridNetwork)

Update the set `taxonnum` in each constraint, assuming that the stem edge and
the crown node are still correct, and that their descendants are still correct.
May be needed if the node and edge numbers were modified by
[`resetNodeNumbers!`](@ref) or [`resetEdgeNumbers!`](@ref).

Warning: does *not* check that the names of leaves with numbers in `taxonnum`
are `taxonnames`.

```jldoctest
julia> net = readTopology("(((2a,2b),(((((1a,1b,1c),4),(5)#H1),(#H1,(6,7))))#H2),(#H2,10));");

julia> c_species1 = PhyloNetworks.TopologyConstraint(0x01, ["1a","1b","1c"], net)
Species constraint, on tips: 1a, 1b, 1c
 stem edge number 7
 crown node number -9

julia> c_species1.taxonnums
Set{Int64} with 3 elements:
  5
  4
  3

julia> c_clade145 = PhyloNetworks.TopologyConstraint(0x02, ["1a","1b","1c","4","5"], net)
Clade constraint, on tips: 1a, 1b, 1c, 4, 5
 stem edge number 12
 crown node number -7

julia> PhyloNetworks.resetNodeNumbers!(net)

julia> net.node[4].number = 111;

julia> PhyloNetworks.updateconstraints!([c_species1, c_clade145], net)

julia> c_species1
Species constraint, on tips: 1a, 1b, 1c
 stem edge number 7
 crown node number 21

julia> c_species1.taxonnums
Set{Int64} with 3 elements:
  5
  4
  111
```
"""
function updateconstraints!(constraints::Vector{TopologyConstraint}, net::HybridNetwork)
    if isempty(constraints)
        return nothing
    end # avoids extracting major tree when no constraint
    tree = majorTree(net)
    for con in constraints
      if con.type in [0x01, 0x02]
        getChild(con.edge) === con.node ||
            error("the stem edge and crown node have been disconnected")
        tei = findfirst(e -> e.number == con.edge.number, tree.edge)
        tei !== nothing ||
            error("stem edge number $(con.edge.number) was not found in the network's major tree")
        des = descendants(tree.edge[tei]) # vector of node numbers, descendant tips only by default
        d1 = setdiff(con.taxonnums, des)  # set
        d2 = setdiff(des, con.taxonnums)  # array
        length(d1) == length(d2) || error("missing or extra taxa in the clade / species.")
        replace!(con.taxonnums, Dict(tn => d2[i] for (i,tn) in enumerate(d1))...)
      end
    end
    return nothing
end

"""
    updateconstraintfields!(constraints::Vector{TopologyConstraint}, net::HybridNetwork)

Update fields stem `edge` and crown `node` to match the given `net`.

Assumes that the constraints are still met in `net`,
and that nodes & edges are numbered identically in `net` as in the network
used to create all `constraints`.

fixit: remove the assumption that constraints are still met, since
an NNI near the crown of a constrained clade might change the crown node
and / or the stem edge (u and v exchange).
"""
function updateconstraintfields!(constraints::Vector{TopologyConstraint}, net::HybridNetwork)
    for con in constraints
        num = con.edge.number
        con.edge = net.edge[findfirst([e.number == num for e in net.edge])]
        num = con.node.number
        con.node = net.node[findfirst([n.number == num for n in net.node])]
    end
end

#=
nice to add: option to modify branch lengths during NNI
=#

"""
    nni!(net::HybridNetwork, e::Edge, nohybridladder::Bool=true, no3cycle::Bool=true,
         constraints=TopologyConstraint[]::Vector{TopologyConstraint})

Attempt to perform a nearest neighbor interchange (NNI) around edge `e`,
randomly chosen among all possible NNIs (e.g 3, sometimes more depending on `e`)
satisfying the constraints, and such that the new network is a DAG. The number of
possible NNI moves around an edge depends on whether the edge's parent/child nodes
are tree or hybrid nodes. This is calculated by [`nnimax`](@ref).

The option `no3cycle` forbids moves that would create a 3-cycle in the network.
When `no3cycle` = false, 2-cycle and 3-cycles may be generated.

Note that the defaults values are for positional (not keyword) arguments, so
two or more arguments can be used, but in a specific order: nni!(net, e) or
nni!(net, e, nohybridladder),
nni!(net, e, nohybridladder, no3cycle),
nni!(net, e, nohybridladder, no3cycle, contraints).

Assumptions:
- The starting network does not have 3-cycles, if `no3cycle=true`.
  No check for the presence of 2- and 3-cycles in the input network.
- The edges' field `isChild1` is correct in the input network. (This field
  will be correct in the output network.)

Output:
information indicating how to undo the move or `nothing` if all NNIs failed.

# examples

```jldoctest
julia> str_network = "(((S8,S9),(((((S1,S2,S3),S4),(S5)#H1),(#H1,(S6,S7))))#H2),(#H2,S10));";

julia> net = readTopology(str_network);

julia> using Random; Random.seed!(321);

julia> undoinfo = nni!(net, net.edge[3], true, true); # true's to avoid hybrid ladders and 3-cycles

julia> writeTopology(net)
"(((S8,(((((S1,S2,S3),S4),(S5)#H1),(#H1,(S6,S7))))#H2),S9),(#H2,S10));"

julia> nni!(undoinfo...);

julia> writeTopology(net) == str_network # net back to original topology: the NNI was "undone"
true
```
"""
function nni!(net::HybridNetwork, e::Edge, nohybridladder::Bool=true, no3cycle::Bool=true,
              constraints=TopologyConstraint[]::Vector{TopologyConstraint})
    for con in constraints
        # reject NNI if the focus edge is the stem of a constraint.
        # sufficient to maintain star species constraints
        # but not sufficient for clades.
        if con.edge === e
            return nothing
        end
    end
    nnirange = 0x01:nnimax(e) # 0x01 = 1 but UInt8 instead of Int
    nnis = Random.shuffle(nnirange)
    for nummove in nnis # iterate through all possible NNIs, but in random order
        moveinfo = nni!(net, e, nummove, nohybridladder, no3cycle)
        !isnothing(moveinfo) || continue # to next possible NNI
        # if constraintviolated(net, constraints)
        #     # fixit for next PR: not needed for species constraints,
        #     # not sufficient for clades if γ / isMajor modified later, or if
        #     # constraints met after NNI but stem / crows need to be moved
        #     nni!(moveinfo...) # undo the previous NNI
        #     continue          # try again
        # end
        return moveinfo
    end
    return nothing # if we get to this point, all NNIs failed
end

"""
    nnimax(e::Edge)

Return the number of NNI moves around edge `e`,
assuming that the network is semi-directed.
Return 0 if `e` is not internal, and more generally if either node
attached to `e` does not have 3 edges.

Output: UInt8 (e.g. `0x02`)
"""
function nnimax(e::Edge)
    length(e.node[1].edge) == 3 || return 0x00
    length(e.node[2].edge) == 3 || return 0x00
    # e.hybrid and hybrid parent: RR case, 2 possible NNIs only
    # e.hybrid and tree parent:   BR case, 3 or 6 NNIs if e is may contain the root
    # e not hybrid, hyb parent:   RB case, 4 NNIs
    # e not hybrid, tree parent:  BB case, 2 NNIs if directed, 8 if undirected
    hybparent = getParent(e).hybrid
    n = (e.hybrid ? (hybparent ? 0x02 : (e.containRoot ? 0x06 : 0x03)) : # RR & BR
                    (hybparent ? 0x04 : (e.containRoot ? 0x08 : 0x02)))  # RB & BB
    return n
end

"""
    nni!(net::HybridNetwork, uv::Edge, nummove::UInt8,
         nohybridladder::Bool, no3cycle::Bool)

Modify `net` with a nearest neighbor interchange (NNI) around edge `uv`.
Return the information necessary to undo the NNI, or `nothing` if the move
was not successful (such as if the resulting graph was not acyclic (not a DAG) or if
the focus edge is adjacent to a polytomy). If the move fails, the network is not
modified.
`nummove` specifies which of the available NNIs is performed.

rooted-NNI options according to Gambette et al. (2017), fig. 8:
* BB: 2 moves, both to BB, if directed edges. 8 moves if undirected.
* RR: 2 moves, both to RR.
* BR: 3 moves, 1 RB & 2 BRs, if directed. 6 moves if e is undirected.
* RB: 4 moves, all 4 BRs.
The extra options are due to assuming a semi-directed network, whereas
Gambette et al (2017) describe options for rooted networks.
On a semi-directed network, there might be a choice of how to direct
the edges that may contain the root, e.g. choice of e=uv versus vu, and
choice of labelling adjacent nodes as α/β (BB), or as α/γ (BR).

`nohybridladder` = true prevents moves that would create a hybrid ladder in
the network, that is, 2 consecutive hybrid nodes (one parent of the other).
`no3cycle` = true prevents NNI moves that would make a 3-cycle, and assumes
that the input network does not have any 2- or 3-cycles.
If `no3cycle` is false, 3-cycles can be generated, but NNIs generating
2-cycles are prevented.

The edge field `isChild1` is assumed to be correct according to the `net.root`.
"""
function nni!(net::HybridNetwork, uv::Edge, nummove::UInt8,
              nohybridladder::Bool, no3cycle::Bool)
    nmovemax = nnimax(uv)
    if nmovemax == 0x00 # uv not internal, or polytomy e.g. species represented by polytomy with >2 indiv
        return nothing
    end
    nummove <= nmovemax || error("nummove $(nummove) must be <= $nmovemax for edge number $(uv.number)")
    u = getParent(uv)
    v = getChild(uv)
    ## TASK 1: grab the edges adjacent to uv: αu, βu, vγ, vδ and adjacent nodes
    # get edges αu & βu connected to u
    # α = u's major parent if it exists, β = u's last child or minor parent
    if u.hybrid
        αu = getMajorParentEdge(u)
        βu = getMinorParentEdge(u)
        α = getParent(αu)
        β = getParent(βu)
    else # u may not have any parent, e.g. if root node
        # pick αu = parent edge if possible, first edge of u (other than uv) otherwise
        labs = [edgerelation(e, u, uv) for e in u.edge]
        pti = findfirst(isequal(:parent), labs) # parent index: there should be 1 or 0
        ci = findall(  isequal(:child), labs)  # vector. there should be 1 or 2
        if pti === nothing
            length(ci) == 2 || error("node $(u.number) should have 2 children other than node number $(v.number)")
            pti = popfirst!(ci)
            αu = u.edge[pti]
            α = getChild(αu)
        else
            αu = u.edge[pti]
            α = getParent(αu)
        end
        βu = u.edge[ci[1]]
        β = getChild(βu)
    end
    # get edges vδ & vγ connected to v
    # δ = v's last child, γ = other child or v's parent other than u
    labs = [edgerelation(e, v, uv) for e in v.edge]
    if v.hybrid # then v must have another parent edge, other than uv
        vγ = v.edge[findfirst(isequal(:parent), labs)] # γ = getParent(vδ) ?this should be vγ right?
            #on net_hybridladder edge 1 get ERROR: ArgumentError: invalid index: nothing of type Nothing
        vδ = v.edge[findfirst(isequal(:child), labs)]
        γ = getParent(vγ)
        δ = getChild(vδ)
    else
        ci = findall(isequal(:child), labs)
        length(ci) == 2 || error("node $(v.number) should have 2 children")
        vγ = v.edge[ci[1]] # γ = getChild(vγ)
        vδ = v.edge[ci[2]]
        γ = getChild(vγ)
        δ = getChild(vδ)
    end
    ## TASK 2: semi-directed network swaps:
    ## swap u <-> v, α <-> β if undirected and according to move number
    if !u.hybrid && uv.containRoot # case BB or BR, with uv that may contain the root
        nmoves = 0x03 # number of moves in the rooted (directed) case
        if !v.hybrid # BB, uv and all adjacent edges are undirected: 8 moves
            if nummove > 0x04  # switch u & v with prob 1/2
                nummove -= 0x04  # now in 1,2,3,4
                (u,v) = (v,u)
                (α,αu,β,βu,γ,vγ,δ,vδ) = (γ,vγ,δ,vδ,α,αu,β,βu)
            end
            nmoves = 0x02
        end
        # now nmoves should be in 1, ..., 2*nmoves: 1:4 (BB) or 1:6 (BR) #? should this be 2*nummoves?
        # next: switch α & β with probability 1/2
        if nummove > nmoves
            nummove -= nmoves # now nummoves in 1,2 (BB) or 1,2,3 (BR)
            (α,αu,β,βu) = (β,βu,α,αu)
        end
    end
    ## TASK 3: rooted network swaps, then do the NNI
    if u.hybrid # RR or RB cases
        # swap α & β: reduce NNIs from 2 to 1 (RR) or from 4 to 2 (RB)
        if nummove == 0x02 || nummove == 0x04
            (α,αu,β,βu) = (β,βu,α,αu)
        end
        if !v.hybrid && nummove > 0x02 # case RB, moves 3 or 4
            (γ,vγ,vδ,δ) = (δ,vδ,vγ,γ)
        end
        # detach β and graft onto vδ
        if no3cycle # don't create a 3-cycle
            if problem4cycle(α,γ, β,δ) return nothing; end
        else # don't create a 2-cycle
            if α === γ || β === δ return nothing; end
        end
        res = nni!(αu, u, uv, v, vδ)
    else # u = bifurcation: BB or BR cases
        if !v.hybrid # case BB: 2 options
            if nummove == 0x02
                (γ,vγ,vδ,δ) = (δ,vδ,vγ,γ)
            end
            # detach β and graft onto vδ
            if no3cycle
                if problem4cycle(α,γ, β,δ) return nothing; end
            elseif α === γ || β === δ return nothing
            end
            res = nni!(αu, u, uv, v, vδ)
        else # case BR: 3 options
            # DAG check
            # if α parent of u, check if β -> γ (directed or undirected)
            # if undirected with β parent of u, check α -> γ
            # if undirected and u = root:
            #   moves 1 and 3 will fail if α -> γ
            #   moves 2 and 3 will fail if β -> γ
            # nummove 3 always creates a nonDAG when u is the root
            αparentu = getChild(αu)===u
            βparentu = getChild(βu)===u
            if αparentu
                if γ === β || isdescendant(γ, β) return nothing; end
            elseif βparentu
                if γ === α || isdescendant(γ, α) return nothing; end
            else # cases when u is root
                if nummove == 0x01 && (γ === α || isdescendant(γ, α))
                    return nothing
                elseif nummove == 0x02 && (γ === β || isdescendant(γ, β))
                    return nothing
                elseif nummove == 0x03 # fail: not DAG
                    return nothing
                end
            end
            if nummove == 0x01 # graft γ onto α
                if no3cycle
                    if problem4cycle(α,γ, β,δ) return nothing; end
                elseif α === γ || β === δ return nothing
                end
                if nohybridladder && α.hybrid return nothing; end
                res = nni!(vδ, v, uv, u, αu)
            elseif nummove == 0x02 # graft γ onto β
                if nohybridladder && β.hybrid return nothing; end
                if no3cycle
                    if problem4cycle(β,γ, α,δ) return nothing; end
                elseif β === γ || α === δ return nothing
                end
                res = nni!(vδ, v, uv, u, βu)
            else # nummove == 0x03
                if αparentu # if α->u: graft δ onto α
                    if no3cycle
                        if problem4cycle(α,δ, β,γ) return nothing; end
                    elseif β === γ || α === δ return nothing
                    end
                    if nohybridladder && α.hybrid return nothing; end
                    res = nni!(vγ, v, uv, u, αu)
                else # if β->u: graft δ onto β
                    if no3cycle
                        if problem4cycle(α,γ, β,δ) return nothing; end
                    elseif α === γ || β === δ return nothing
                    end
                    # we could check nohybridladder && β.hybrid but no need because
                    # we only get here if uv.containRoot so β cannot be hybrid
                    res = nni!(vγ, v, uv, u, βu)
                # case when u is root already excluded (not DAG)
                end
            end
        end
    end
    return res
end

"""
    nni!(αu, u, uv::Edge, v, vδ)
    nni!(αu,u,uv,v,vδ, flip::Bool, inner::Bool, indices)

Transform a network locally around the focus edge `uv` with
the following NNI, that detaches u-β and grafts it onto vδ:

    α - u -- v ------ δ
        |    |
        β    γ

    α ------ v -- u - δ
             |    |
             γ    β

`flip` boolean indicates if the uv edge was flipped
`inner` boolean indicates if edges αu and uv both point toward node u,
i.e. α->u<-v<-δ. If this is true, we flip the hybrid status of αu and vδ.

`indices` give indices for nodes and edges u_in_αu, αu_in_u, vδ_in_v, and v_in_vδ.
These are interpreted as:

    u_in_αu: the index for u in the edge αu
    αu_in_u: the index for αu in node u
    vδ_in_v: the index for vδ in node v
    v_in_vδ: the index for v in edge vδ

**Warnings**:

- *No* check of assumed adjacencies
- Not implemented for cases that are not necessary thanks to symmetry,
  such as cases covered by `nni!(vδ, v, uv, u, αu)` or `nni!(βu, u, v, vγ)`.
  More specifically, these cases are not implemented (and not checked):
  * u not hybrid & v hybrid
  * u hybrid, v not hybrid, α -> u <- v -> δ
- Because of this, `nni(αu,u,uv,v,vδ, ...)` should not be used directly;
  use instead `nni!(net, uv, move_number)`.
- nni!(undoinfo...) restores the topology, but edges below hybrid nodes
  will have length 0.0 even if they didn't before.

Node numbers and edge numbers are not modified.
Edge `uv` keeps its direction unchanged *unless* the directions were
`α -> u -> v -> δ` or `α <- u <- v <- δ`,
in which case the direction of `uv` is flipped.

The second version's input has the same signature as the output, but
will undo the NNI more easily. This means that if `output = nni!(input)`,
then `nni!(output...)` is valid and undoes the first operation.

Right now, branch lengths are not modified except when below a hybrid node.
Future versions might implement options to modify branch lengths.
"""
function nni!(αu::Edge, u::Node, uv::Edge, v::Node, vδ::Edge)
    # find indices to detach αu from u, and to detach v from vδ
    u_in_αu = findfirst(n->n===u, αu.node)
    αu_in_u = findfirst(e->e===αu, u.edge)
    vδ_in_v = findfirst(e->e===vδ, v.edge)
    v_in_vδ = findfirst(n->n===v, vδ.node)
    # none of them should be 'nothing' --not checked
    αu_child = getChild(αu)
    uv_child = getChild(uv)
    vδ_child = getChild(vδ)
    # flip the direction of uv if α->u->v->δ or α<-u<-v<-δ
    flip = (αu_child === u && uv_child === v && vδ_child !== v) ||
             (αu_child !== u && uv_child === u && vδ_child === v)
    inner = (flip ? false : αu_child === u && vδ_child === v )
    # fixit: calculate new edge lengths here; give them as extra arguments to nni! below
    res = nni!(αu,u,uv,v,vδ, flip,inner, u_in_αu,αu_in_u,vδ_in_v,v_in_vδ)
    return res
end
function nni!(αu::Edge, u::Node, uv::Edge, v::Node, vδ::Edge,
              flip::Bool, inner::Bool,
              u_in_αu::Int, αu_in_u::Int, vδ_in_v::Int, v_in_vδ::Int)
              # extra arguments: new edge lengths
    # fixit: extract old edge lengths; update to new lengths; return old lengths as extra items
    ## TASK 1: do the NNI swap: to detach & re-attach at once
    (αu.node[u_in_αu], u.edge[αu_in_u], v.edge[vδ_in_v], vδ.node[v_in_vδ]) =
        (v, vδ, αu, u)
    ## TASK 2: update edges' directions
    if flip
        uv.isChild1 = !uv.isChild1
    end
    ## TASK 3: update hybrid status of nodes (#? is it ever nodes?)
             # & edges, and edges containRoot field
    if u.hybrid && !v.hybrid
        if flip # RB, BR 1 or BR 2': flip hybrid status of uv and (αu or vδ)
            if uv.hybrid # BR 1 or BR 2'
                vδ.hybrid = true
                vδ.gamma = uv.gamma
                vδ.isMajor = uv.isMajor
                # length switches: for keeping the network unzipped. could be option later
                αu.length = uv.length
                uv.length = 0.0
                uv.hybrid = false
                uv.gamma = 1.0
                uv.isMajor = true
                norootbelow!(uv)
            else # RB
                uv.hybrid = true
                uv.gamma = αu.gamma
                uv.isMajor = αu.isMajor
                # length switches: for keeping the network unzipped. could be option later
                uv.length = vδ.length
                vδ.length = 0.0
                αu.hybrid = false
                αu.gamma = 1.0
                αu.isMajor = true
                if αu.containRoot
                    allowrootbelow!(v, αu)
                end
            end
        else # assumes α<-u or v<-δ --- but not checked
            # not implemented: α->u<-v->δ: do uv.hybrid=false and γv.hybrid=true
            if inner # BR 3 or 4': α->u<-v<-δ: flip hybrid status of αu and vδ
                # no lengths to switch in these cases
                vδ.hybrid = true
                vδ.gamma = αu.gamma
                vδ.isMajor = αu.isMajor
                αu.hybrid = false
                αu.gamma = 1.0
                αu.isMajor = true
                # containRoot: nothing to update
            else # BR 1' or 2: α<-u<-v->δ : hybrid status just fine
                # length switches: for keeping the network unzipped. could be option later
                αu.length = vδ.length
                vδ.length = 0.0
                norootbelow!(vδ)
                if uv.containRoot
                    allowrootbelow!(αu)
                end
            end
        end
    elseif u.hybrid && v.hybrid # RR: hybrid edges remain hybrids, but switch γs
        # we could have -αu-> -uv-> -vδ-> or <-αu- <-uv- <-vδ-
        hyb = ( getChild(αu) == v ? αu : vδ ) # uv's hybrid parent edge: either αu or vδ
        (uv.gamma,   hyb.gamma)   = (hyb.gamma,   uv.gamma)
        (uv.isMajor, hyb.isMajor) = (hyb.isMajor, uv.isMajor)
    end
    # throw error if u not hybrid & v hybrid? (does not need to be implemented)
    return vδ, u, uv, v, αu, flip, inner, v_in_vδ, αu_in_u, vδ_in_v, u_in_αu
end

"""
    problem4cycle(β::Node, δ::Node, α::Node, γ::Node)

Check if the focus edge uv has a 4 cycle that could lead to a 3 cycle
after an chosen NNI. Return true if there is a problem 4 cycle, false if none.
"""
function problem4cycle(β::Node, δ::Node, α::Node, γ::Node)
    isconnected(β, δ) || isconnected(α, γ)
end

"""
    checkspeciesnetwork!(network::HybridNetwork,
                         constraints::Vector{TopologyConstraint})

Check that the network satisfies a number of requirements:
- no polytomies, other than at species constraints: throws an error otherwise
- no unnecessary nodes: fuse edges at nodes with degree two, including at the root
  (hence the bang: `net` may be modified)
- topology constraints are met.

Output: true if all is good, false if one or more clades are violated.
"""
function checkspeciesnetwork!(net::HybridNetwork, constraints::Vector{TopologyConstraint})
    for n in net.node # check for no polytomies: all nodes should have up to 3 edges
        if length(n.edge) > 3 # polytomies allowed at species constraints only
            coni = findfirst(c -> c.type == 1 && c.node === n, constraints)
            coni !== nothing ||
                error("The network has a polytomy at node number $(n.number). Please resolve.")
        end
    end
    removedegree2nodes!(net)
    return !constraintviolated(net, constraints)
end

"""
    mapindividuals(net::HybridNetwork, mappingFile::String)

Return a network expanded from `net`, where species listed in the mapping file
are replaced by individuals mapped to that species.
If a species has only 1 individual, the name of the leaf for that species is
replaced by the name of its one individual representative.
If a species has 2 or more individuals, the leaf for that species is
expanded into a "star" (polytomy if 3 or more individuals) with a tip for each
individual. If a species is in the network but not listed in the mapping file,
the tip for that species is left as is. Species listed in the mapping file
but not present in the network are ignored.

The mapping file should be readable by `CSV.File` and contain two columns:
one for the species names and one for the individual (or allele) names.
fixit: make this function more flexible by accepting column names

Output: individual-level network and vector of species constraint(s).

# examples

```jldoctest
julia> species_net = readTopology("(((S8,S9),((((S1,S4),(S5)#H1),(#H1,(S6,S7))))#H2),(#H2,S10));");

julia> filename = joinpath(dirname(Base.find_package("PhyloNetworks")), "..", "examples", "mappingIndividuals.csv");

julia> filename |> read |> String |> print # to see what the mapping file contains
species,individual
S1,S1A
S1,S1B
S1,S1C

julia> individual_net, species_constraints = mapindividuals(species_net, filename);

julia> writeTopology(individual_net, internallabel=true)
"(((S8,S9),(((((S1A,S1B,S1C)S1,S4),(S5)#H1),(#H1,(S6,S7))))#H2),(#H2,S10));"

julia> species_constraints
1-element Vector{PhyloNetworks.TopologyConstraint}:
 Species constraint, on tips: S1A, S1B, S1C
 stem edge number 4
 crown node number 3
```
"""
function mapindividuals(net::HybridNetwork, mappingFile::String)
    mappingDF = DataFrame(CSV.File(mappingFile); copycols=false)
    specieslist = unique(mappingDF[:, 1])
    individualnet = deepcopy(net)
    constraints = TopologyConstraint[]
    for species in specieslist
        individuals = mappingDF[mappingDF.species .== species, 2]
        res = addindividuals!(individualnet, species, individuals)
        res === nothing || # nothing if species not found in the network, or 0,1 individuals
            push!(constraints, res)
    end
    return individualnet, constraints
end

"""
    addindividuals!(net::HybridNetwork, species::AbstractString, individuals::Vector)

Add individuals to their species leaf as a star, and return the
corresponding species constraint. `nothing` is returned in 2 cases:

- if `individuals` contains only 1 individual, in which case the
  name of the leaf for that species is replaced by the name of
  its one individual representative
- if `species` is not found in the network

Spaces in individuals' names are eliminated, see [`cleantaxonname`](@ref).

Called by [`mapindividuals`](@ref).

# examples

```jldoctest
julia> net = readTopology("(S8,(((S1,S4),(S5)#H1),(#H1,S6)));");

julia> PhyloNetworks.addindividuals!(net, "S1", ["S1A", "S1B", "S1C"])
Species constraint, on tips: S1A, S1B, S1C
 stem edge number 2
 crown node number 2

julia> writeTopology(net) # 3 new nodes, S1 now internal: not a tip
"(S8,((((S1A,S1B,S1C)S1,S4),(S5)#H1),(#H1,S6)));"
```
"""
function addindividuals!(net::HybridNetwork, species::AbstractString, individuals::Vector{String})
    length(individuals) > 0 || return nothing
    species = strip(species)
    speciesnodeindex = findfirst(l -> l.name == species, net.node)
    if speciesnodeindex === nothing
        @warn "species $species not found in network (typo in mapping file?). will be ignored."
        return nothing
    end
    if length(individuals) == 1 # change species name into name of individual representative
        net.node[speciesnodeindex].name = cleantaxonname(individuals[1])
        return nothing
    end
    # there are 2+ individuals: replace the species tip by a star, will need a species constraint
    taxnames = map(cleantaxonname, individuals)
    for tax in taxnames
        addleaf!(net, net.node[speciesnodeindex], tax);
    end
    return TopologyConstraint(0x01, taxnames, net)
end

"""
    cleantaxonname(taxonname::AbstractString)

Return a String with leading and trailing spaces removed, and interior spaces
replaced by underscores: good for using as tip names in a network without
causing future error when reading the newick description of the network.
"""
function cleantaxonname(taxonname::AbstractString)
    tname = string(strip(taxonname)) # SubString if we don't do string()
    m = match(r"\s", tname)
    if m !== nothing
        @warn """Spaces in "$tname" may cause errors in future network readability: replaced by _"""
        tname = replace(tname, r"\s+" => "_")
    end
    return tname
end

"""
    moveroot!(net::HybridNetwork, constraints=TopologyConstraint[]::Vector{TopologyConstraint})

Move the root to a randomly chosen non-leaf node that is different from
the current root, and not within a constraint clade or species.
Output: `true` if successul, `nothing` otherwise.
"""
function moveroot!(net::HybridNetwork, constraints=TopologyConstraint[]::Vector{TopologyConstraint})
    newrootrandomorder = Random.shuffle(1:length(net.node))
    oldroot = net.root
    for newrooti in newrootrandomorder
        newrooti != oldroot || continue
        newrootnode = net.node[newrooti]
        newrootnode.leaf && continue # try next potential new root if current is a leaf
        # Check the new root is NOT at the top or inside contraint clade or within pecies
        newrootfound = true
        for con in constraints
            if con.node === newrootnode || isdescendant(newrootnode, con.node) # assumes the constraint is met
                newrootfound = false
                break # out of constraint loop
            end
        end
        newrootfound || continue # to next potential new root
        # Check for no directional conflict
        try
            net.root = newrooti
            directEdges!(net)
            # warning: assumes that the previous root has degree 3
            # otherwise, need to delete previous root by fusing its 2 adjacent edges
        catch e # RootMismatch error if root placement is below a hybrid
            isa(e, RootMismatch) || rethrow(e)
            net.root = oldroot
            directEdges!(net) # revert edges' directions to match original rooting
            continue # to next potential new root
        end
        # if we get here: newrooti passed all the checks
        return true
    end
    # if we get here: none of the root positions worked
    return nothing
end

"""
    fliphybrid!(net::HybridNetwork, minor=true::Bool, nohybridladder=false::Bool,
                constraints=TopologyConstraint[]::Vector{TopologyConstraint})

Cycle through hybrid nodes in random order until an admissible flip is found.
At this hybrid node, flip the indicated hybrid parent edge (minor or major).

If an admissible flip is found, return the tuple: newhybridnode, flippededge, oldchildedge.
Otherwise, return nothing.

The flip can be undone with
`fliphybrid!(net, newhybridnode, minor, constraints)`, or
`fliphybrid!(net, newhybridnode, !flippededge.isMajor, constraints)`
more generally, such as if the flipped edge had its γ modified after the
original flip.

*Warnings*
- if the root needed to be reset and if the original root was of degree 2,
  then a node of degree 2 remains in the modified network.
- undoing the flip may not recover the original root in case
  the root position was modified during the original flip.
"""
function fliphybrid!(net::HybridNetwork, minor=true::Bool,
                     nohybridladder=false::Bool,
                     constraints=TopologyConstraint[]::Vector{TopologyConstraint})
    hybridindex = Random.shuffle(1:length(net.hybrid)) # indices in net.hybrid
    while !isempty(hybridindex) # all minor edges
        i = pop!(hybridindex)
        undoinfo = fliphybrid!(net, net.hybrid[i], minor, nohybridladder, constraints)
        if !isnothing(undoinfo) # if it failed, the network was not changed
            return undoinfo
        end # else, continue until explored all i in hybridindex
    end
    return nothing
end

"""
    fliphybrid!(net::HybridNetwork, hybridnode::Node, minor=true::Bool,
                nohybridladder=false::Bool,
                constraints=TopologyConstraint[]::Vector{TopologyConstraint})

Flip the direction of a single hybrid edge:
the minor parent edge of `hybridnode` by default,
or the major parent edge if `minor` is false.
The parent node of the hybrid edge becomes the new hybrid node.
The former hybrid edge partner is converted to a tree edge (with γ=1),
and `hybridnode` becomes a tree node.

For the flip to be admissible, the new network must be a semi-directed
phylogenetic network: with a root such that the rooted version is a DAG.
If `nohybridladder` is false (default), the flip may create a hybrid ladder
If `nohybridladder` is true and if the flip would create a hybrid ladder,
then the flip is not admissible.
A hybrid ladder is when a hybrid child of another hybrid.

The new hybrid partner is an edge adjacent to the new hybrid node,
such that the flip is admissible (so it must be a tree edge).
The flipped edge retains its original γ.
The new hybrid edge is assigned inheritance 1-γ.

Output:
`(newhybridnode, flippededge, oldchildedge)` if the flip is admissible,
`nothing` otherwise.

The network is unchanged if the flip is not admissible.
If the flip is admissible, the root position may be modified, and
the direction of tree edges (via `isChild1`) is modified accordingly. If the
root needs to be modified, then the new root is set to the old hybrid node.

The index of the new hybrid node in `net.hybrid` is equal to that of the
old `hybridnode`.

Warning: Undoing this move may not recover the original root if
the root position was modified.
"""
function fliphybrid!(net::HybridNetwork, hybridnode::Node, minor=true::Bool,
                     nohybridladder=false::Bool,
                     constraints=TopologyConstraint[]::Vector{TopologyConstraint})
    #= for species constraints, there is nothing to check, because hybrids cannot point into or come out of the group
    for con in constraints
        if con.type == # 0x02/0x03 for types 2 and 3, need to consider more cases
            return nothing
        end
    end
    =#
    runDirectEdges = false
    edgetoflip, edgetokeep = minor ?
        (getMinorParentEdge(hybridnode), getMajorParentEdge(hybridnode)) :
        (getMajorParentEdge(hybridnode), getMinorParentEdge(hybridnode))
    oldchildedge = getChildEdge(hybridnode)
    newhybridnode = getParent(edgetoflip)
    if newhybridnode.hybrid # already has 2 parents: cannot had a third.
        return nothing
    end
    ## choose newhybridedge ##
    p2 = getParent(edgetokeep) # parent node
    isdesc = Bool[]
    for e in newhybridnode.edge # is p2 undirected descendant of nhn via this edge?
        isp2desc = false
        #= isp2desc = true means:
        there is a semi-directed path  newhybridnode -e-> neighbor --...-> p2
        so: flipping hybridedge without making e the new partner would:
        - make e a child of newhybridnode, and
        - create a directed cycle: p2 -hybridedge-> newhybridnode -e-> neibr --...-> p2
        so we HAVE to make e the new partner hybrid edge if its "isp2desc" is true
        =#
        if e !== edgetoflip # e cannot be hybrid --e-> nhn because earlier check
            neibr = getOtherNode(e, newhybridnode) # neighbor of new hybrid node via e
            if !neibr.leaf && (p2 === neibr ||
                isdescendant_undirected(p2, neibr, e))
                isp2desc = true
            end
        end
        push!(isdesc, isp2desc)
    end
    sum_isdesc = sum(isdesc)
    if sum_isdesc > 1 # if 2+ edges have a semi-directed path to p2,
        # flipping either would create a semi-directed cycle via the other(s)
        return nothing
    end
    if sum_isdesc == 0 # no risk to create directed cycle.
        # choose the current parent edge of nhn to: keep its current direction
        # and keep reaching all nodes from a single root
        newhybridedge = getMajorParentEdge(newhybridnode)
    else
        newhybridedge = newhybridnode.edge[findfirst(isdesc)]
        if newhybridedge.hybrid # cannot flip another hybrid edge, but we needed
            return nothing      # to calculate its isp2desc for total # of true's
        end
    end
    # @debug "edgetoflip is $edgetoflip, edgetokeep is $edgetokeep, newhybridedge is $newhybridedge"
    if nohybridladder
        # case when edgetoflip would be bottom rung of ladder
        if getOtherNode(newhybridedge, newhybridnode).hybrid
            return nothing
        end
        # case when edgetoflip would be the top rung: happens when
        # newhybridnode = center point of a W structure initially
        for e in newhybridnode.edge
            if e !== edgetoflip && e.hybrid # newhybridedge is NOT hybrid, so far
                return nothing
            end
        end
    end
    # change hybrid status and major status for nodes and edges
    hybridnode.hybrid = false
    edgetokeep.hybrid = false
    newhybridnode.hybrid = true
    newhybridedge.hybrid = true
    newhybridedge.isMajor = minor
    edgetokeep.isMajor = true
    # update node order to keep isChild1 attribute of hybrid edges true
    edgetoflip.isChild1 = !edgetoflip.isChild1 # just switch
    if getChild(newhybridedge) !== newhybridnode # includes the case when the newhybridnode was the root
        # then flip newhybridedge too: make it point towards newhybridnode
        newhybridedge.isChild1 = !newhybridedge.isChild1
        net.root = findfirst(n -> n === hybridnode, net.node)
        runDirectEdges = true # many edges are likely to need to have directions flipped here
    end
    # give new hybridnode a name
    newhybridnode.name = hybridnode.name
    hybridnode.name = "" # remove name from former hybrid node
    # update gammas
    newhybridedge.gamma = edgetokeep.gamma
    edgetokeep.gamma = 1.0
    # update hybrids in network (before directEdges!)
    hybridindex = findfirst(n -> n === hybridnode, net.hybrid)
    net.hybrid[hybridindex] = newhybridnode
    if runDirectEdges # direct edges only if root is moved
        directEdges!(net)
    else # if not, only update containRoot attributes
        # norootbelow in child edges of newhybridedge
        for ce in newhybridnode.edge
            ce !== newhybridedge || continue # skip e
            getParent(ce) === newhybridnode || continue # skip edges that aren't children of cn
            norootbelow!(ce)
        end
        # allow root for edges below old hybrid node
        if edgetokeep.containRoot #else, already forbidden below due to hybrid above
            allowrootbelow!(hybridnode, edgetokeep) # old hybrid node
        end
    end
    return newhybridnode, edgetoflip, oldchildedge
end


"""
Generic Fermion module
"""
module AbstractFermion
    #export FermionFields,Wx!,Wdagx!,clear!,substitute_fermion!,Dx!

    #export FermionFields,
    #        Wx!,Wdagx!,clear!,substitute_fermion!,Dx!,fermion_shift!,
    #        fermion_shiftB!,add!,set_wing_fermi!,WdagWx!,apply_periodicity

    import ..Actions:FermiActionParam,FermiActionParam_Wilson,
                FermiActionParam_WilsonClover,FermiActionParam_Staggered
    import ..Gaugefields:GaugeFields,GaugeFields_1d,SU3GaugeFields,SU2GaugeFields,SU3GaugeFields_1d,SU2GaugeFields_1d,
                staggered_phase,SUn,SU2,SU3,SUNGaugeFields,SUNGaugeFields_1d,SU
    using Random
    abstract type FermionFields end

    function clear!(a::FermionFields)
        n1,n2,n3,n4,n5,n6 = size(a.f)
        for i6=1:n6
            for i5=1:n5
                for i4=1:n4
                    for i3=1:n3
                        for i2=1:n2
                            @simd for i1=1:n1
                                a.f[i1,i2,i3,i4,i5,i6]= 0
                            end
                        end
                    end
                end
            end
        end
    end

    function add!(c::FermionFields,alpha::Number,a::FermionFields,beta::Number,b::FermionFields) #c = c + alpha*a + beta*b
        n1,n2,n3,n4,n5,n6 = size(a.f)

        for i6=1:n6
            for i5=1:n5
                for i4=1:n4
                    for i3=1:n3
                        for i2=1:n2
                            @simd for i1=1:n1
                                #println(a.f[i1,i2,i3,i4,i5,i6],"\t",b.f[i1,i2,i3,i4,i5,i6] )
                                c.f[i1,i2,i3,i4,i5,i6] += alpha*a.f[i1,i2,i3,i4,i5,i6] +beta*b.f[i1,i2,i3,i4,i5,i6]
                            end
                        end
                    end
                end
            end
        end
        return
    end

    function add!(c::FermionFields,alpha::Number,a::FermionFields) #c = c + alpha*a
        n1,n2,n3,n4,n5,n6 = size(a.f)

        for i6=1:n6
            for i5=1:n5
                for i4=1:n4
                    for i3=1:n3
                        for i2=1:n2
                            @simd for i1=1:n1
                                #println(a.f[i1,i2,i3,i4,i5,i6],"\t",b.f[i1,i2,i3,i4,i5,i6] )
                                c.f[i1,i2,i3,i4,i5,i6] += alpha*a.f[i1,i2,i3,i4,i5,i6]
                            end
                        end
                    end
                end
            end
        end
        return
    end

    function add!(coeff::Number,c::FermionFields,alpha::Number,a::FermionFields) #c = coeff*c + alpha*a
        n1,n2,n3,n4,n5,n6 = size(a.f)

        for i6=1:n6
            for i5=1:n5
                for i4=1:n4
                    for i3=1:n3
                        for i2=1:n2
                            @simd for i1=1:n1
                                #println(a.f[i1,i2,i3,i4,i5,i6],"\t",b.f[i1,i2,i3,i4,i5,i6] )
                                c.f[i1,i2,i3,i4,i5,i6] = coeff*c.f[i1,i2,i3,i4,i5,i6] + alpha*a.f[i1,i2,i3,i4,i5,i6]
                            end
                        end
                    end
                end
            end
        end
        return
    end

    function Wx!(xout::FermionFields,U::Array{GaugeFields,1},
        x::FermionFields,temps::Array{FermionFields,1},fparam::T)  where T <: FermiActionParam
        error("This FermionFields type is not supported yet in function Wx!. The type is $(typeof(x))")
    end

    function Dx!(xout::T,U::Array{G,1},
        x::T,temps::Array{T,1}) where  {T <: FermionFields,G <: GaugeFields}
        error("This FermionFields type is not supported yet in function Dx!. The type is $(typeof(x))")

        return
    end

    function Ddagx!(xout::T,U::Array{G,1},
        x::T,temps::Array{T,1}) where  {T <: FermionFields,G <: GaugeFields}
        error("This FermionFields type is not supported yet in function Ddagx!. The type is $(typeof(x))")

        return
    end

    function Dxplus!(xout::T,ν::Int64,U::Array{G,1},
        x::T,temps::Array{T,1}) where  {T <: FermionFields,G <: GaugeFields}
        #temp = temps[4]
        error("This FermionFields type is not supported yet in function Dx!. The type is $(typeof(x))")

        return
    end

    function fermion_shiftB!(b::T,evensite,u::Array{GaugeFields{SU{NC}},1},μ,a::T)  where {NC,T <: FermionFields}
        error("This FermionFields type is not supported yet in function fermion_shiftB!. The type is $(typeof(x))")
    end

    function Wdagx!(xout::FermionFields,U::Array{GaugeFields,1},
        x::FermionFields,temps::Array{FermionFields,1},fparam::T)  where T <: FermiActionParam
        error("This FermionFields type is not supported yet in function Wdagx!. The type is $(typeof(x))")
    end

    function WdagWx!(xout::T,U::Array{G,1},x::T,temps::Array{T,1},fparam) where {T <: FermionFields,G <: GaugeFields}
        temp = temps[5]
        Wx!(temp,U,x,temps,fparam)

        Wdagx!(xout,U,temp,temps,fparam)

        return
    end

    function WdagWx!(xout::T,U::Array{G,1},x::T,temps::Array{T,1},fparam,indices) where {T <: FermionFields,G <: GaugeFields}
        WdagWx!(xout,U,x,temps,fparam)
        return
    end

    function substitute_fermion!(H,j,x::FermionFields)
        error("substitute_fermion! is not implemented for $(typeof(x))")
    end

    function set_wing_fermi!(a::FermionFields)
        NT = a.NT
        NZ = a.NZ
        NY = a.NY
        NX = a.NX
        NC = a.NC

        #!  X-direction
        for ialpha=1:4
            for it=1:NT
                for iz = 1:NZ
                    for iy=1:NY
                        for k=1:NC
                            a[k,0,iy,iz,it,ialpha] = a.BoundaryCondition[1]*a[k,NX,iy,iz,it,ialpha]
                        end
                    end
                end
            end
        end

        for ialpha=1:4
            for it=1:NT
                for iz=1:NZ
                    for iy=1:NY
                        for k=1:NC
                            a[k,NX+1,iy,iz,it,ialpha] = a.BoundaryCondition[1]*a[k,1,iy,iz,it,ialpha]
                        end
                    end
                end
            end
        end

        #Y-direction
        for ialpha = 1:4
            for it=1:NT
                for iz=1:NZ
                    for ix=1:NX
                        for k=1:NC
                            a[k,ix,0,iz,it,ialpha] = a.BoundaryCondition[2]*a[k,ix,NY,iz,it,ialpha]
                        end
                    end
                end
            end
        end

        for ialpha=1:4
            for it=1:NT
                for iz=1:NZ
                    for ix=1:NX
                        for k=1:NC
                            a[k,ix,NY+1,iz,it,ialpha] = a.BoundaryCondition[2]*a[k,ix,1,iz,it,ialpha]
                        end
                    end
                end
            end
        end

        for ialpha=1:4
            # Z-direction
            for it=1:NT
                for iy=1:NY
                    for ix=1:NX
                        for k=1:NC
                            a[k,ix,iy,0,it,ialpha] = a.BoundaryCondition[3]*a[k,ix,iy,NZ,it,ialpha]
                            a[k,ix,iy,NZ+1,it,ialpha] = a.BoundaryCondition[3]*a[k,ix,iy,1,it,ialpha]

                        end
                    end
                end
            end

            #T-direction
            for iz=1:NZ
                for iy=1:NY
                    for ix=1:NX
                        for k=1:NC
                            a[k,ix,iy,iz,0,ialpha] = a.BoundaryCondition[4]*a[k,ix,iy,iz,NT,ialpha]
                            a[k,ix,iy,iz,NT+1,ialpha] = a.BoundaryCondition[4]*a[k,ix,iy,iz,1,ialpha]
                        end
                    end
                end
            end

        end

    end

    function fermion_shift!(b::F,u::Array{GaugeFields{SU{NC}},1},μ,a::F) where {NC,F <: FermionFields}
        if μ == 0
            substitute!(b,a)
            return
        end

        NX = a.NX
        NY = a.NY
        NZ = a.NZ
        NT = a.NT
        #NC = a.NC

        if μ > 0
            idel = zeros(Int64,4)
            idel[μ] = 1

            n6 = size(a.f)[6]
            for ialpha=1:n6
                for it=1:NT
                    it1 = it + idel[4]
                    for iz=1:NZ
                        iz1 = iz + idel[3]
                        for iy=1:NY
                            iy1 = iy + idel[2]
                            for ix=1:NX
                                ix1 = ix + idel[1]
                                for k1=1:NC
                                    b[k1,ix,iy,iz,it,ialpha] = 0
                                    @simd for k2=1:NC
                                        b[k1,ix,iy,iz,it,ialpha] += u[μ][k1,k2,ix,iy,iz,it]*a[k2,ix1,iy1,iz1,it1,ialpha]
                                    end
                                end
                            end
                        end
                    end
                end
            end

        elseif μ < 0
            idel = zeros(Int64,4)
            idel[-μ] = 1
            n6 = size(b.f)[6]
            for ialpha =1:n6
                for it=1:NT
                    it1 = it - idel[4]
                    for iz=1:NZ
                        iz1 = iz -idel[3]
                        for iy=1:NY
                            iy1 = iy -idel[2]
                            for ix=1:NX
                                ix1 = ix -idel[1]

                                for k1=1:NC
                                    b[k1,ix,iy,iz,it,ialpha] = 0
                                    @simd for k2=1:NC
                                        b[k1,ix,iy,iz,it,ialpha] += conj(u[-μ][k2,k1,ix1,iy1,iz1,it1])*a[k2,ix1,iy1,iz1,it1,ialpha]
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end

    end

    function fermion_shift!(b::F,evensite::Bool,u::Array{GaugeFields{SU{NC}},1},μ,a::F) where {NC,F <: FermionFields}
        if μ == 0
            substitute!(b,a)
            return
        end
        ibush = ifelse(evensite,0,1)

        NX = a.NX
        NY = a.NY
        NZ = a.NZ
        NT = a.NT
        #NC = a.NC

        if μ > 0
            idel = zeros(Int64,4)
            idel[μ] = 1

            n6 = size(a.f)[6]
            for ialpha=1:n6
                for it=1:NT
                    it1 = it + idel[4]
                    for iz=1:NZ
                        iz1 = iz + idel[3]
                        for iy=1:NY
                            iy1 = iy + idel[2]
                            xran =1+(1+ibush+iy+iz+it)%2:2:NX
                            for ix in xran
                                ix1 = ix + idel[1]
                                for k1=1:NC
                                    b[k1,ix,iy,iz,it,ialpha] = 0
                                    @simd for k2=1:NC
                                        b[k1,ix,iy,iz,it,ialpha] += u[μ][k1,k2,ix,iy,iz,it]*a[k2,ix1,iy1,iz1,it1,ialpha]
                                    end
                                end
                            end
                        end
                    end
                end
            end

        elseif μ < 0
            idel = zeros(Int64,4)
            idel[-μ] = 1
            n6 = size(b.f)[6]
            for ialpha =1:n6
                for it=1:NT
                    it1 = it - idel[4]
                    for iz=1:NZ
                        iz1 = iz -idel[3]
                        for iy=1:NY
                            iy1 = iy -idel[2]
                            xran =1+(1+ibush+iy+iz+it)%2:2:NX
                            for ix in xran
                                ix1 = ix -idel[1]

                                for k1=1:NC
                                    b[k1,ix,iy,iz,it,ialpha] = 0
                                    @simd for k2=1:NC
                                        b[k1,ix,iy,iz,it,ialpha] += conj(u[-μ][k2,k1,ix1,iy1,iz1,it1])*a[k2,ix1,iy1,iz1,it1,ialpha]
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end

    end

    function apply_periodicity(ix,iy,iz,it,NX,NY,NZ,NT,BC)

        ixflag1 = (ix < 1)
        ixflag2 = (ix > NX)
        ix += ifelse(ixflag1,NX,0)
        ix += ifelse(ixflag2,-NX,0)

        sign =  ifelse(ixflag1 || ixflag2,BC[1],1)
        #sign =  ifelse(ixflag2,BC[1],1)

        iyflag1 = (iy < 1)
        iyflag2 = (iy > NY)

        iy += ifelse(iyflag1,NY,0)
        iy += ifelse(iyflag2,-NY,0)

        sign = ifelse(iyflag1 || iyflag2,BC[2],1)
        #sign = ifelse(iyflag2,BC[2],1)

        izflag1 = (iz < 1)
        izflag2 = (iz > NZ)

        iz += ifelse(izflag1,NZ,0)
        iz += ifelse(izflag2,-NZ,0)

        sign = ifelse(izflag1 || izflag2,BC[3],1)
        #sign = ifelse(izflag2,BC[3],1)

        itflag1 = (it < 1)
        itflag2 = (it > NT)

        it += ifelse(itflag1,NT,0)
        it += ifelse(itflag2,-NT,0)

        sign = ifelse(itflag1 || itflag2,BC[4],1)
        #sign = ifelse(itflag2,BC[4],1)

        return ix,iy,iz,it,sign
    end

    """
c-------------------------------------------------c
c     Random number function for Gaussian  Noise
    with σ^2 = 1/2
c-------------------------------------------------c
    """
    function gauss_distribution_fermi!(x::FermionFields)
        NC = x.NC
        NX = x.NX
        NY = x.NY
        NZ = x.NZ
        NT = x.NT
        n6 = size(x.f)[6]
        σ = sqrt(1/2)

        for ialpha = 1:n6
            for it=1:NT
                for iz=1:NZ
                    for iy=1:NY
                        for ix=1:NX
                            for ic=1:NC

                                x[ic,ix,iy,iz,it,ialpha] = σ*randn()+im*σ*randn()
                            end
                        end
                    end
                end
            end
        end

        set_wing_fermi!(x)

        return
    end

    """
c-------------------------------------------------c
c     Random number function for Gaussian  Noise
    with σ^2 = 1/2
c-------------------------------------------------c
    """
    function gauss_distribution_fermi!(x::FermionFields,randomfunc,σ)
        NC = x.NC
        NX = x.NX
        NY = x.NY
        NZ = x.NZ
        NT = x.NT
        n6 = size(x.f)[6]
        #σ = sqrt(1/2)

        for mu = 1:n6
            for ic=1:NC
                for it=1:NT
                    for iz=1:NZ
                        for iy=1:NY
                            for ix=1:NX
                                v1 = sqrt(-log(randomfunc()+1e-10))
                                v2 = 2pi*randomfunc()

                                xr = v1*cos(v2)
                                xi = v1 * sin(v2)

                                x[ic,ix,iy,iz,it,mu] = σ*xr + σ*im*xi
                            end
                        end
                    end
                end
            end
        end

        set_wing_fermi!(x)

        return
    end

    function gauss_distribution_fermi!(x::FermionFields,randomfunc)
        σ = 1
        gauss_distribution_fermi!(x,randomfunc,σ)
    end

    function gauss_distribution_fermi_Z2!(x::FermionFields)
        NC = x.NC
        NX = x.NX
        NY = x.NY
        NZ = x.NZ
        NT = x.NT
        n6 = size(x.f)[6]
        #σ = sqrt(1/2)

        for mu = 1:n6
            for it=1:NT
                for iz=1:NZ
                    for iy=1:NY
                        for ix=1:NX
                            for ic=1:NC
                                x[ic,ix,iy,iz,it,mu] = rand([-1,1])
                            end
                        end
                    end
                end
            end
        end

        set_wing_fermi!(x)

        return
    end

    """
c-------------------------------------------------c
c     Random number function Z4  Noise
c     https://arxiv.org/pdf/1611.01193.pdf
c-------------------------------------------------c
    """
    function Z4_distribution_fermi!(x::FermionFields)
        NC = x.NC
        NX = x.NX
        NY = x.NY
        NZ = x.NZ
        NT = x.NT
        n6 = size(x.f)[6]
        θ = 0.0
        N::Int32 = 4
        Ninv = Float64(1/N)
        for ialpha = 1:n6
            for it=1:NT
                for iz=1:NZ
                    for iy=1:NY
                        for ix=1:NX
                            for ic=1:NC
                                θ = Float64(rand(0:N-1))*π*Ninv # r \in [0,π/4,2π/4,3π/4]
                                x[ic,ix,iy,iz,it,ialpha] = cos(θ)+im*sin(θ)
                            end
                        end
                    end
                end
            end
        end

        set_wing_fermi!(x)

        return
    end

end


"""
$(TYPEDEF)

Constrain only the yaw angle of a Pose2, generally used for Gyrocompass, Magnetometer, Dual-GNSS heading type measurements, or any other similar construct.
"""
mutable struct PartialPriorYawPose2{T} <: IIF.FunctorSingleton  where {T <: IIF.SamplableBelief}
    Z::T
    partial::Tuple{Int}
    PartialPriorYawPose2{T}() where T = new{T}()
    PartialPriorYawPose2{T}(x::T) where {T <: IIF.SamplableBelief}  = new{T}(x, (3,))
end
PartialPriorYawPose2(x::T) where {T <: IIF.SamplableBelief} = PartialPriorYawPose2{T}(x)

function getSample(p2::PartialPriorYawPose2, N::Int=1)
  return (reshape(rand(p2.Z,N),1,N), )
end

## Serialization support

"""
$(TYPEDEF)
"""
mutable struct PackedPartialPriorYawPose2 <: IIF.PackedInferenceType
    Z::String
    PackedPartialPriorYawPose2() = new()
    PackedPartialPriorYawPose2(x::T) where {T <: AbstractString}  = new(x)
end

function convert(::Type{PackedPartialPriorYawPose2}, d::PartialPriorYawPose2)
  PackedPartialPriorYawPose2(string(d.Z))
end
function convert(::Type{PartialPriorYawPose2}, d::PackedPartialPriorYawPose2)
  PartialPriorYawPose2(extractdistribution(d.Z))
end


struct Dummy end

const DUMMY = Dummy()

mutable struct Model{Optimizer}
    con::Sink{Field}
    obj::Union{Nothing,Expression}

    n::Int # num vars
    q::Int # num pars
    m::Int # num cons

    x::Vector{Float64}
    p::Vector{Float64}
    g::Vector{Float64}
    l::Vector{Float64}
    zl::Vector{Float64}
    zu::Vector{Float64}
    xl::Vector{Float64}
    xu::Vector{Float64}
    gl::Vector{Float64}
    gu::Vector{Float64}

    prob
    ext::Dict{Symbol,Any}
    opt::Dict{Symbol,Any}
end

struct ModelComponent{C <: Union{Variable,Parameter}}
    parent::Model
    inner::C
end

struct Constraint
    parent::Model
    index::Int
end

for (f,df,ddf) in f_nargs_1
    @eval $f(e1::T) where T <: ModelComponent = $f(inner(e1))
end
for (f,df1,df2,ddf1,ddf12,ddf22) in f_nargs_2
    @eval $f(e1::T1,e2::T2) where {T1 <: ModelComponent,T2 <: ModelComponent} = $f(inner(e1),e2)
    @eval $f(e1::T,e2) where T <: ModelComponent = $f(inner(e1),e2)
    @eval $f(e1,e2::T) where T <: ModelComponent = $f(e1,inner(e2))
end
for o in [:(==),:(>=),:(<=)]
    @eval $o(e1::T1,e2::T2) where {T1 <: ModelComponent, T2 <: ModelComponent} = $o(inner(e1),inner(e2))
    @eval $o(e1::T1,e2) where {T1 <: ModelComponent} = $o(inner(e1),e2)
    @eval $o(e1,e2::T2) where {T2 <: ModelComponent} = $o(e1,inner(e2))
end
add_sum(a::E,b::ModelComponent{C}) where {C,E<:ExpressionSum} = add_sum(a,b.inner)
add_sum(a::ModelComponent{C},b::E) where {C,E<:ExpressionSum} = add_sum(a.inner,b)
add_sum(a::ModelComponent{C},b::ModelComponent{C}) where C = add_sum(a.inner,b.inner)

parent(c) = c.parent
index(c::ModelComponent{C}) where C = index(inner(c))
convert(::Type{Expression},e::ModelComponent{C}) where C = inner(e)

getindex(m::Model,idx::Symbol) = m.ext[idx]
setindex!(m::Model,val,idx::Symbol) = setindex!(m.ext,val,idx)

default_optimizer() = @isdefined(DEFAULT_OPTIMIZER) ? DEFAULT_OPTIMIZER : error("DEFAULT_OPTIMIZER is not defined. To use Ipopt as a default optimizer, do: using Ipopt")
Model(Optimizer=default_optimizer();opt...) =Model{Optimizer}(
    Field(),Constant(0.),0,0,0,Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],Float64[],nothing,Dict{Symbol,Any}(),Dict{Symbol,Any}(name=>option for (name,option) in opt))

function variable(m::Model;lb=-Inf,ub=Inf,start=0.,name=nothing)
    m.n += 1
    push!(m.x,start)
    push!(m.xl,lb)
    push!(m.xu,ub)
    push!(m.zl,1.)
    push!(m.zu,1.)
    ModelComponent(m,Variable(m.n))
end

function parameter(m::Model,val=0.;name=nothing)
    m.q += 1
    push!(m.p,val)
    ModelComponent(m,Parameter(m.q))
end

function constraint(m::Model,e::E;lb=0.,ub=0.) where E <: Expression
    m.m += 1
    m.con[m.m] = e
    push!(m.l,0.)
    push!(m.g,0.)
    push!(m.gl,lb)
    push!(m.gu,ub)
    Constraint(m,m.m)
end

constraint(m,eq::Equality{E}) where {E<:Expression} = constraint(m,eq.e)
constraint(m,eq::Inequality{E}) where {E<:Expression} = constraint(m,eq.e;ub=Inf)

function objective(m::Model,e::E) where E <: Expression
    m.obj = e
    nothing
end

instantiate!(m::Model{Optimizer}) where Optimizer = (m.prob = Optimizer(m))
function optimize!(m::Model)
    m.prob == nothing && instantiate!(m)
    optimize!(m.prob)
end

value(e::ModelComponent{C}) where C = non_caching_eval(inner(e),parent(e).x,parent(e).p)
setvalue(e::ModelComponent{V},val) where V <: Variable = parent(e).x[index(e)] = val
setvalue(e::ModelComponent{P},val) where P <: Parameter= parent(e).p[index(e)] = val
set_lower_bound(e::ModelComponent{V},val) where V <: Variable = parent(e).xl[index(e)] = val
set_upper_bound(e::ModelComponent{V},val) where V <: Variable = parent(e).xu[index(e)] = val
lower_bound(e::ModelComponent{V}) where V <: Variable = parent(e).xl[index(e)]
upper_bound(e::ModelComponent{V}) where V <: Variable = parent(e).xu[index(e)]

value(e::Constraint) = parent(e).g[index(e)]
# value(e::Constraint) = non_caching_eval(parent(e).con[e.index],parent(e).x,parent(e).p)
setvalue(e::Constraint,val) = parent(e).g[index(e)] = val
set_lower_bound(e::Constraint,val) = parent(e).gl[index(e)] = val
set_upper_bound(e::Constraint,val) = parent(e).gu[index(e)] = val
lower_bound(e::Constraint) = parent(e).gl[index(e)]
upper_bound(e::Constraint) = parent(e).gu[index(e)]

dual(c::Constraint) = parent(c).l[index(c)]
objective_value(m::Model) = non_caching_eval(m.obj,m.x,m.p)

num_variables(m::Model) = m.n
num_constraints(m::Model) = m.m

function nlp_evaluator(obj,con)

    grad = Gradient(obj)
    jac,jac_sparsity = SparseJacobian(con)
    hess,hess_sparsity = SparseLagrangianHessian(obj,grad,inner(con),jac)

    _obj = @inline (x,p)->non_caching_eval(obj,x,p)
    _con = @inline (y,x,p)->non_caching_eval(con,y,x,p)
    _grad! = @inline function (y,x,p)
        y .= 0
        obj(x,p)
        non_caching_eval(grad,y,x,p)
    end
    _jac! = @inline function (J,x,p)
        J .= 0
        con(DUMMY,x,p)
        non_caching_eval(jac,J,x,p)
    end
    _hess! = @inline function (z,x,p,lag,sig)
        z .= 0
        obj(x,p)
        con(DUMMY,x,p)
        grad(DUMMY,x,p)
        jac(DUMMY,x,p)
        hess(z,x,p,lag,sig)
    end

    nnz_jac = length(jac_sparsity)
    nnz_hess = length(hess_sparsity)

    return _obj,_grad!,_con,_jac!,jac_sparsity,_hess!,hess_sparsity
end

# etc
getindex(::Dummy,key::Int) = 0.
getindex(::Tuple{Int,Dummy},key::Int) = 0.
setindex!(::Dummy,val,key) = nothing
setindex!(::Tuple{Int,Dummy},val,key) = nothing
# ndims(::Type{Dummy}) = 0
# ndims(::Type{T}) where T <: HessianSum = 0
# size(::Dummy) = 0
# copyto!(::Dummy,val) = nothing

@inline function fill_sparsity!(I,J,tuples)
    @simd for l in eachindex(tuples)
        (i,j) = tuples[l]
        @inbounds I[l] = i
        @inbounds J[l] = j
    end
end