using JLD
using Printf

scratch_dir = ARGS[1]

for set in ["set1", "set2", "set3", "set4"]
    power = zeros(2, length(-0.75:0.05:0.75))

    i = 0
    for del in -0.75:0.05:0.75
        i += 1
        nrep = 0
        for batch in 1:10
            try
                file = jldopen("$(scratch_dir)/res_$(set)_$(del)_$(batch).jld", "r")
                T1 = read(file, "T1")
                T2 = read(file, "T2")
                close(file)

                power[1, i] += sum(T1)
                power[2, i] += sum(T2)

                nrep += 1
            catch
                @warn "could not open $(set) $(del) $(batch)"
            end
        end
	    power[1, i] = power[1, i] / (nrep * 100)
	    power[2, i] = power[2, i] / (nrep * 100)
    end

    @save "power_$(set).jld" power
end


#****************************************************************************
#                     Molecular Dynamics Potentials (MDP)
#                            CESMIX-MIT Project
#
#  Contributing authors: Ngoc-Cuong Nguyen (cuongng@mit.edu, exapde@gmail.com)
#****************************************************************************

mutable struct DomainStruct

    boxlo::Array{Float64,2};
    boxhi::Array{Float64,2};
    boxtilt::Array{Float64,2};
    pbc::Array{Int64,2};
    bcs::Array{Int64,2};
    triclinic::Int64;

    DomainStruct() = new();
end

function setdomain(boxhi, boxtilt, pbc=[1 1 1], bcs=[1 1 1 1 1 1])

dom = DomainStruct();

reg.boxlo = [0.0 0.0 0.0];
dom.boxhi = boxhi;
dom.boxtilt = boxtilt;
dom.pbc = pbc;
dom.bcs = bcs;

if (dom.boxtilt[1] == 0 && dom.boxtilt[2] == 0 && dom.boxtilt[3] == 0)
    dom.triclinic = 0;
else
    dom.triclinic = 1;
end

return dom

end


module AIDA

export parse_aidaFile
using LightXML

abstract AIDA1D

type Cloud1D <: AIDA1D
	name::String
	title::String
	path::String
	conversionLowerEdge::Float32
	conversionUpperEdge::Float32
	conversionBins::Int32
	maxEntries::Int32
	data::Array{Float64,1}
end

type Histogram1D <: AIDA1D
	name::String
	title::String
	path::String
	xLeftEdges::Array{Float64,1}
	weights::Array{Float64,1}
	errors::Array{Float64,1}
end

function parse_histogram1d(xmlHistogram1d)
	attrs = attributes_dict(xmlHistogram1d)
	name = attrs["name"]
	title = attrs["title"]
	path = attrs["path"]
	axis = find_element(xmlHistogram1d, "axis")
	axis_attrs = attributes_dict(axis)
	nxBins = int(axis_attrs["numberOfBins"])
	xMin = float64(axis_attrs["min"])
	xMax = float64(axis_attrs["max"])
	weights = zeros(Float64, nxBins)
	errors = zeros(Float64, nxBins)
	data1d = find_element(xmlHistogram1d, "data1d")
	for e in child_elements(data1d)
		binNum = attribute(e, "binNum")
		if binNum == "OVERFLOW"
			continue
		end
		ibin = int(binNum)
		weights[ibin+1] = float64(attribute(e, "entries"))
		errors[ibin+1] = float64(attribute(e, "weightedRms"))
	end

	return Histogram1D(
	name,
	title,
	path,
	linspace(xMin, xMax, nxBins+1),
	weights,
	errors
	)
end

function parse_cloud1d(xmlCloud1d)
	attrs = attributes_dict(xmlCloud1d)
	n = attrs["name"]
	t = attrs["title"]
	p = attrs["path"]
	convUp = float64(attrs["conversionUpperEdge"])
	convLow = float64(attrs["conversionLowerEdge"])
	convBins = int32(attrs["conversionBins"])
	max = int32(attrs["maxEntries"])
	d = Float32[]
	entries1d = find_element(xmlCloud1d, "entries1d")
	if entries1d != nothing
		for e in child_elements(entries1d)
			push!(d, float64(attribute(e, "valueX", required=true)))
		end
	else
		hist1d = find_element(xmlCloud1d, "histogram1d")
		return parse_histogram1d(hist1d)
	end
	return Cloud1D(
	n,
	t,
	p,
	convLow,
	convUp,
	convBins,
	max,
	d
	)
end

function parse_aidaFile(filename::String)
	xdoc = parse_file(filename)
	xroot = root(xdoc)
	aida1dList = AIDA1D[]
	for cloud in get_elements_by_tagname(xroot, "cloud1d")
		push!(aida1dList, parse_cloud1d(cloud))
	end
	for cloud in get_elements_by_tagname(xroot, "histogram1d")
		push!(aida1dList, parse_histogram1d(cloud))
	end

	cloud1dList = Cloud1D[]
	histogram1dList = Histogram1D[]
	for a in aida1dList
		if typeof(a) == Cloud1D
			push!(cloud1dList, a)
		elseif typeof(a) == Histogram1D
			push!(histogram1dList, a)
		end
	end
	return cloud1dList, histogram1dList
end
end # module


using Base.Broadcast: broadcastable
using ChainRules
using ChainRulesCore
using ChainRulesTestUtils
using ChainRulesTestUtils: rand_tangent, _fdm
using Compat: hasproperty, only, cispi, eachcol
using FiniteDifferences
using LinearAlgebra
using LinearAlgebra.BLAS
using LinearAlgebra: dot
using Random
using StaticArrays
using Statistics
using Test

Random.seed!(1) # Set seed that all testsets should reset to.

function include_test(path)
    println("Testing $path:")  # print so TravisCI doesn't timeout due to no output
    @time include(path)  # show basic timing, (this will print a newline at end)
end

println("Testing ChainRules.jl")
@testset "ChainRules" begin
    include_test("test_helpers.jl")
    println()
    @testset "rulesets" begin
        @testset "Core" begin
            include_test("rulesets/Core/core.jl")
        end

        @testset "Base" begin
            include_test("rulesets/Base/base.jl")
            include_test("rulesets/Base/fastmath_able.jl")
            include_test("rulesets/Base/evalpoly.jl")
            include_test("rulesets/Base/array.jl")
            include_test("rulesets/Base/arraymath.jl")
            include_test("rulesets/Base/indexing.jl")
            include_test("rulesets/Base/mapreduce.jl")
            include_test("rulesets/Base/sort.jl")
        end
        println()

        @testset "Statistics" begin
            include_test("rulesets/Statistics/statistics.jl")
        end
        println()

        @testset "LinearAlgebra" begin
            include_test("rulesets/LinearAlgebra/dense.jl")
            include_test("rulesets/LinearAlgebra/norm.jl")
            include_test("rulesets/LinearAlgebra/matfun.jl")
            include_test("rulesets/LinearAlgebra/structured.jl")
            include_test("rulesets/LinearAlgebra/symmetric.jl")
            include_test("rulesets/LinearAlgebra/factorization.jl")
            include_test("rulesets/LinearAlgebra/blas.jl")
            include_test("rulesets/LinearAlgebra/lapack.jl")
        end
        println()

        @testset "Random" begin
            include_test("rulesets/Random/random.jl")
        end
        println()
    end
end


"""
Finalising function. It is called at the end of each computed jump for the user to alter the saving, plotting... procedure.
"""
function finalize_dummy(rate, xc, xd, p, t)
	nothing
end

"""
Function to pre-allocate arrays contening the result.
"""
function allocate_arrays(ti	,xc0, xd0, n_max; rejection = false, ind_save_c=-1:1, ind_save_d=-1:1)
	if ind_save_c[1] == -1
		ind_save_c = 1:length(xc0)
	end

	if ind_save_d[1] == -1
		ind_save_d = 1:length(xd0)
	end

	if rejection
		X0  = copy(xc0)
		Xc  = copy(xc0)
	else
		# for the CVH method, needs to enlarge the state space
		X0 = copy(xc0); push!(X0,ti)
		Xc = copy(xc0)
	end
	Xd	 = copy(xd0)

	# arrays for storing history, pre-allocate storage
	t_hist  = [ti]
	xc_hist = VectorOfArray([copy(xc0)[ind_save_c]])
	xd_hist = VectorOfArray([copy(xd0)[ind_save_d]])
	res_ode = zeros(2, length(X0))

	return X0, Xc, Xd, t_hist, xc_hist, xd_hist, res_ode, ind_save_d, ind_save_c
end

"""
Function copied from Gillespie.jl and StatsBase

This function is a substitute for `StatsBase.sample(wv::WeightVec)`, which avoids recomputing the sum and size of the weight vector, as well as a type conversion of the propensity vector. It takes the following arguments:
- **w** : an `Array{Float64,1}`, representing propensity function weights.
- **s** : the sum of `w`.
- **n** : the length of `w`.
"""
function pfsample(w::vec, s::Tc, n::Int64) where {Tc, vec <: AbstractVector{Tc}}
	t = rand() * s
	i = 1
	cw = w[1]
	while cw < t && i < n
		i += 1
		@inbounds cw += w[i]
	end
	return i
end

pfsample(rate) = pfsample(rate, sum(rate), length(rate))

"""
This type stores the output composed of:
- **time** : a `Vector` of `Float64`, containing the times of simulated events.
- **xc** : containing the simulated states for the continuous variable.
- **xd** : containing the simulated states for the continuous variable.
- **rates** : containing the rates used during the simulation
"""
struct PDMPResult{Tc <: Real, vectype_xc, vectype_xd}
	time::Vector{Tc}
	xc::vectype_xc
	xd::vectype_xd
	rates::Vector{Tc}
	save_positions::Tuple{Bool, Bool}
	njumps::Int64
	nrejected::Int64
end

PDMPResult(time, xchist, xdhist) = PDMPResult(time, xchist, xdhist, eltype(xchist)[], (false, false), length(time), 0)
PDMPResult(time, xchist, xdhist, rates, savepos) = PDMPResult(time, xchist, xdhist, rates, savepos, length(time), 0)

PDMPResult(pb::PDMPProblem, savepos = (false, false)) = PDMPResult(copy(pb.time), copy(pb.Xc), copy(pb.Xd), copy(pb.rate_hist), savepos, pb.simjptimes.njumps, pb.simjptimes.fictitous_jumps)


################################################################################
# InteratomicPotentials API default implementations
################################################################################

get_rcutoff(::AbstractPotential) = Inf
get_species(::AbstractPotential) = missing

potential_energy(s::AbstractSystem, p::AbstractPotential) = energy_and_force(s, p).e
force(s::AbstractSystem, p::AbstractPotential) = energy_and_force(s, p).f
virial(s::AbstractSystem, p::AbstractPotential) = sum(virial_stress(s, p)[1:3])


# defines structures for holding ASWING Solution data

struct StabilityDerivatives
    CXu::Float64
    CYu::Float64
    CLu::Float64
    CXb::Float64
    CYb::Float64
    CLb::Float64
    CXa::Float64
    CYa::Float64
    CLa::Float64
    Clu::Float64
    Cmu::Float64
    Cnu::Float64
    Clb::Float64
    Cmb::Float64
    Cnb::Float64
    Cla::Float64
    Cma::Float64
    Cna::Float64
    CXp::Float64
    CYp::Float64
    CLp::Float64
    CXq::Float64
    CYq::Float64
    CLq::Float64
    CXr::Float64
    CYr::Float64
    CLr::Float64
    Clp::Float64
    Cmp::Float64
    Cnp::Float64
    Clq::Float64
    Cmq::Float64
    Cnq::Float64
    Clr::Float64
    Cmr::Float64
    Cnr::Float64
    CXdF::Array{Float64,1}
    CYdF::Array{Float64,1}
    CLdF::Array{Float64,1}
    CldF::Array{Float64,1}
    CmdF::Array{Float64,1}
    CndF::Array{Float64,1}
    CXdP::Array{Float64,1}
    CYdP::Array{Float64,1}
    CLdP::Array{Float64,1}
    CldP::Array{Float64,1}
    CmdP::Array{Float64,1}
    CndP::Array{Float64,1}
    function StabilityDerivatives(f_vba::AbstractArray{<:Real,2},
        m_vba::AbstractArray{<:Real,2}, f_rot::AbstractArray{<:Real,2},
        m_rot::AbstractArray{<:Real,2}, f_flap::AbstractArray{<:Real,2},
        m_flap::AbstractArray{<:Real,2}, f_peng::AbstractArray{<:Real,2},
        m_peng::AbstractArray{<:Real,2})

        new(f_vba[1,1], f_vba[2,1], -f_vba[3,1],
        f_vba[1,2], f_vba[2,2], -f_vba[3,2],
        f_vba[1,3], f_vba[2,3], -f_vba[3,3],
        m_vba[1,1], m_vba[2,1], m_vba[3,1],
        m_vba[1,2], m_vba[2,2], m_vba[3,2],
        m_vba[1,3], m_vba[2,3], m_vba[3,3],
        f_rot[1,1], f_rot[2,1], -f_rot[3,1],
        f_rot[1,2], f_rot[2,2], -f_rot[3,2],
        f_rot[1,3], f_rot[2,3], -f_rot[3,3],
        m_rot[1,1], m_rot[2,1], m_rot[3,1],
        m_rot[1,2], m_rot[2,2], m_rot[3,2],
        m_rot[1,3], m_rot[2,3], m_rot[3,3],
        f_flap[1,:], f_flap[2,:], -f_flap[3,:],
        m_flap[1,:], m_flap[2,:], m_flap[3,:],
        f_peng[1,:], f_peng[2,:], -f_peng[3,:],
        m_peng[1,:], m_peng[2,:], m_peng[3,:])
    end
end
eltype(::Type{StabilityDerivatives}) = Float64

# contains Solution information for an operating point
struct SolutionParameters
    linear_acceleration::Array{Float64,1}
    angular_acceleration::Array{Float64,1}
    velocity::Array{Float64,1}
    rotation_rate::Array{Float64,1}
    position_aircraft_origin::Array{Float64,1}
    phi::Float64
    theta::Float64
    psi::Float64
    flap_defl_ctrl_var::Array{Float64,1}
    eng_pwr_ctrl_var::Array{Float64,1}
    err_int_Vinf::Float64
    err_int_beta::Float64
    err_int_alpha::Float64
    err_int_phi::Float64
    err_int_theta::Float64
    err_int_psi::Float64
    err_int_ROTxyz::Array{Float64,1}
    err_int_VACxyz::Array{Float64,1}
    position_aircraft_cg::Array{Float64,1}
    freestream_speed::Float64
    freestream_sideslip_angle::Float64
    freestream_incidence_angle::Float64
    indicated_V::Float64
    indicated_beta::Float64
    indicated_alpha::Float64
    drag_force::Float64
    side_force::Float64
    lift_force::Float64
    induced_drag_coefficient::Float64
    drag_coefficient::Float64
    sideforce_coefficient::Float64
    lift_coefficient::Float64
    rolling_moment_coefficient::Float64
    pitching_moment_coefficient::Float64
    yawing_moment_coefficient::Float64
    span_efficiency::Float64
    aero_force::Array{Float64,1}
    aero_moment::Array{Float64,1}
    accel_reaction_force::Array{Float64,1}
    accel_reaction_moment::Array{Float64,1}
    ground_reaction_force::Array{Float64,1}
    ground_reaction_moment::Array{Float64,1}
    engine_force::Array{Float64,1}
    engine_moment::Array{Float64,1}
    trefftz_plane_aero_force::Array{Float64,1}
    trefftz_plane_aero_moment::Array{Float64,1}
    mach::Float64
    altitude::Float64
    density::Float64
    speed_of_sound::Float64
    viscosity::Float64
    gravitational_acceleration::Float64
    load_factor::Float64
    mass::Float64
end
eltype(::Type{SolutionParameters}) = Float64

# contains spanwise input variable information interpolated to Solution nodes
struct SolutionGeometry
    s::Array{Float64,1}
    x::Array{Float64,1}
    y::Array{Float64,1}
    z::Array{Float64,1}
    twist::Array{Float64,1}
    EIcc::Array{Float64,1}
    EInn::Array{Float64,1}
    EIcn::Array{Float64,1}
    EIcs::Array{Float64,1}
    EIsn::Array{Float64,1}
    GJ::Array{Float64,1}
    EA::Array{Float64,1}
    GKc::Array{Float64,1}
    GKn::Array{Float64,1}
    mgcc::Array{Float64,1}
    mgnn::Array{Float64,1}
    mg::Array{Float64,1}
    Ccg::Array{Float64,1}
    Ncg::Array{Float64,1}
    Dmgcc::Array{Float64,1}
    Dmgnn::Array{Float64,1}
    Dmg::Array{Float64,1}
    DCcg::Array{Float64,1}
    DNcg::Array{Float64,1}
    Cea::Array{Float64,1}
    Nea::Array{Float64,1}
    Cta::Array{Float64,1}
    Nta::Array{Float64,1}
    tdeps::Array{Float64,1}
    tdgam::Array{Float64,1}
    Cshell::Array{Float64,1}
    Nshell::Array{Float64,1}
    Atshell::Array{Float64,1}
    radius::Array{Float64,1}
    Cdf::Array{Float64,1}
    Cdp::Array{Float64,1}
    chord::Array{Float64,1}
    Xax::Array{Float64,1}
    alpha::Array{Float64,1}
    Cm::Array{Float64,1}
    CLmax::Array{Float64,1}
    CLmin::Array{Float64,1}
    dCLda::Array{Float64,1}
    dCLdF::Array{Float64,2}
    dCMdF::Array{Float64,2}
    dCDdF::Array{Float64,2}
end
eltype(::Type{SolutionGeometry}) = Float64

# contains spanwise output variable information at Solution nodes
struct SolutionSpanwise
    x::Array{Float64,1}
    y::Array{Float64,1}
    z::Array{Float64,1}
    phi::Array{Float64,1}
    theta::Array{Float64,1}
    psi::Array{Float64,1}
    Mc::Array{Float64,1}
    Ms::Array{Float64,1}
    Mn::Array{Float64,1}
    Fc::Array{Float64,1}
    Fs::Array{Float64,1}
    Fn::Array{Float64,1}
    ux::Array{Float64,1}
    uy::Array{Float64,1}
    uz::Array{Float64,1}
    wx::Array{Float64,1}
    wy::Array{Float64,1}
    wz::Array{Float64,1}
    max_extensional_strain::Array{Float64,1}
    shear_stress::Array{Float64,1}
    chord::Array{Float64,1}
    cl::Array{Float64,1}
    cm::Array{Float64,1}
    f_aero::Array{Float64,1}
    f_reac::Array{Float64,1}
    da_eff::Array{Float64,1} # aeroelastic twist
    da_flp::Array{Float64,1} # additional twist due to flaps
end
eltype(::Type{SolutionSpanwise}) = Float64

# contains aswing solution data
struct Solution
    converged::Bool
    pnt_param::SolutionParameters
    span_geom::Array{SolutionGeometry,1}
    span_param::Array{SolutionSpanwise,1}
end
eltype(::Type{Solution}) = Float64

# converts ASWING output arrays to Solution struct
function getsolution(ipnt1::Integer=1, ipnt2::Integer=ASWING.NPOINT[1])

    sol = Array{Solution,1}(undef, max(ipnt2-ipnt1+1,0))
    for ipnt = ipnt1:ipnt2
        # Process converged boolean
        converged = ASWING.LCONV[ipnt]
        # Process operating point parameters
        linear_acceleration = ASWING.PARAM[KPVACX:KPVACZ,ipnt]
        angular_acceleration = ASWING.PARAM[KPRACX:KPRACZ,ipnt]
        velocity = ASWING.PARAM[KPVELX:KPVELZ,ipnt]
        rotation_rate = ASWING.PARAM[KPROTX:KPROTZ,ipnt]
        position_aircraft_origin = ASWING.PARAM[KPPOSX:KPPOSZ,ipnt]
        phi = ASWING.PARAM[KPBANK,ipnt]
        theta = ASWING.PARAM[KPELEV,ipnt]
        psi = ASWING.PARAM[KPHEAD,ipnt]
        flap_defl_ctrl_var = ASWING.PARAM[KPFLP1:KPFLP1+NFLPX-1,ipnt]
        eng_pwr_ctrl_var = ASWING.PARAM[KPENG1:KPENG1+NENGX-1,ipnt]
        err_int_Vinf = ASWING.PARAM[KPVIDT,ipnt]
        err_int_beta = ASWING.PARAM[KPBEDT,ipnt]
        err_int_alpha =ASWING.PARAM[KPALDT,ipnt]
        err_int_phi = ASWING.PARAM[KPBADT,ipnt]
        err_int_theta = ASWING.PARAM[KPELDT,ipnt]
        err_int_psi = ASWING.PARAM[KPHEDT,ipnt]
        err_int_ROTxyz = ASWING.PARAM[KPWDTX:KPWDTZ,ipnt]
        err_int_VACxyz = ASWING.PARAM[KPADTX:KPADTZ,ipnt]
        position_aircraft_cg = ASWING.PARAM[KPRCGX:KPRCGZ,ipnt]
        freestream_speed = ASWING.PARAM[KPVINF,ipnt]
        freestream_sideslip_angle = ASWING.PARAM[KPBETA,ipnt]
        freestream_incidence_angle = ASWING.PARAM[KPALFA,ipnt]
        indicated_V = ASWING.PARAM[KPVIAS,ipnt]
        indicated_beta = ASWING.PARAM[KPBREF,ipnt]
        indicated_alpha = ASWING.PARAM[KPAREF,ipnt]
        drag_force = ASWING.PARAM[KPDRAG,ipnt]
        side_force = ASWING.PARAM[KPSIDE,ipnt]
        lift_force = ASWING.PARAM[KPLIFT,ipnt]
        induced_drag_coefficient = ASWING.PARAM[KPCDIN,ipnt]
        drag_coefficient = ASWING.PARAM[KPCDRG,ipnt]
        sideforce_coefficient = ASWING.PARAM[KPCSID,ipnt]
        lift_coefficient = ASWING.PARAM[KPCLIF,ipnt]
        rolling_moment_coefficient = ASWING.PARAM[KPCROL,ipnt]
        pitching_moment_coefficient = ASWING.PARAM[KPCMOM,ipnt]
        yawing_moment_coefficient = ASWING.PARAM[KPCYAW,ipnt]
        span_efficiency = ASWING.PARAM[KPSEFF,ipnt]
        aero_force = ASWING.PARAM[KPAFOX:KPAFOZ,ipnt]
        aero_moment = ASWING.PARAM[KPAMOX:KPAMOZ,ipnt]
        accel_reaction_force = ASWING.PARAM[KPRFOX:KPRFOZ,ipnt]
        accel_reaction_moment = ASWING.PARAM[KPRMOX:KPRMOZ,ipnt]
        ground_reaction_force = ASWING.PARAM[KPGFOX:KPGFOZ,ipnt]
        ground_reaction_moment = ASWING.PARAM[KPGMOX:KPGMOZ,ipnt]
        engine_force = ASWING.PARAM[KPEFOX:KPEFOZ,ipnt]
        engine_moment = ASWING.PARAM[KPEMOX:KPEMOZ,ipnt]
        trefftz_plane_aero_force = ASWING.PARAM[KPTFOX:KPTFOZ,ipnt]
        trefftz_plane_aero_moment = ASWING.PARAM[KPTMOX:KPTMOZ,ipnt]
        mach = ASWING.PARAM[KPMACH,ipnt]
        altitude = ASWING.PARAM[KPALTK,ipnt]
        density = ASWING.PARAM[KPDENS,ipnt]
        speed_of_sound = ASWING.PARAM[KPVSOU,ipnt]
        viscosity = ASWING.PARAM[KPVISC,ipnt]
        gravitational_acceleration = ASWING.PARAM[KPGRAV,ipnt]
        load_factor = ASWING.PARAM[KPLOAD,ipnt]
        pnt_param = SolutionParameters(linear_acceleration,angular_acceleration,velocity,rotation_rate,
        position_aircraft_origin,phi,theta,psi,flap_defl_ctrl_var,eng_pwr_ctrl_var,
        err_int_Vinf,err_int_beta,err_int_alpha,err_int_phi,err_int_theta,err_int_psi,
        err_int_ROTxyz,err_int_VACxyz,position_aircraft_cg,freestream_speed,
        freestream_sideslip_angle,freestream_incidence_angle,indicated_V,
        indicated_beta,indicated_alpha,drag_force,side_force,lift_force,
        induced_drag_coefficient,drag_coefficient,sideforce_coefficient,
        lift_coefficient,rolling_moment_coefficient,pitching_moment_coefficient,
        yawing_moment_coefficient,span_efficiency,aero_force,aero_moment,
        accel_reaction_force,accel_reaction_moment,ground_reaction_force,
        ground_reaction_moment,engine_force,engine_moment,trefftz_plane_aero_force,
        trefftz_plane_aero_moment,mach,altitude,density,speed_of_sound,viscosity,
        gravitational_acceleration,load_factor,ASWING.MASS[1])
        #Process spanwise geometry and Solution
        span_geom = Array{SolutionGeometry,1}(undef, ASWING.NBEAM[1])
        span_sol = Array{SolutionSpanwise,1}(undef, ASWING.NBEAM[1])
        for ibeam = 1:ASWING.NBEAM[1]
            i1 = convert(Int,ASWING.IFRST[ibeam])
            i2 = convert(Int,ASWING.ILAST[ibeam])
            # spanwise geometry
            sgeom = ASWING.S[i1:i2]
            x = ASWING.QPAR[i1:i2,JXA]
            y = ASWING.QPAR[i1:i2,JYA]
            z = ASWING.QPAR[i1:i2,JZA]
            twist = ASWING.QPAR[i1:i2,JTW]
            EIcc = ASWING.QPAR[i1:i2,JECC]
            EInn = ASWING.QPAR[i1:i2,JENN]
            EIcn = ASWING.QPAR[i1:i2,JECN]
            EIcs = ASWING.QPAR[i1:i2,JECS]
            EIsn = ASWING.QPAR[i1:i2,JESN]
            GJ = ASWING.QPAR[i1:i2,JGJ]
            EA = ASWING.QPAR[i1:i2,JEA]
            GKc = ASWING.QPAR[i1:i2,JGKC]
            GKn = ASWING.QPAR[i1:i2,JGKN]
            mgcc = ASWING.QPAR[i1:i2,JMCC1]
            mgnn = ASWING.QPAR[i1:i2,JMNN1]
            mg = ASWING.QPAR[i1:i2,JMG1]
            Ccg = ASWING.QPAR[i1:i2,JCCG1]
            Ncg = ASWING.QPAR[i1:i2,JNCG1]
            Dmgcc = ASWING.QPAR[i1:i2,JMCC2]
            Dmgnn = ASWING.QPAR[i1:i2,JMNN2]
            Dmg = ASWING.QPAR[i1:i2,JMG2]
            DCcg = ASWING.QPAR[i1:i2,JCCG2]
            DNcg = ASWING.QPAR[i1:i2,JNCG2]
            Cea = ASWING.QPAR[i1:i2,JCEA]
            Nea = ASWING.QPAR[i1:i2,JNEA]
            Cta = ASWING.QPAR[i1:i2,JCTA]
            Nta = ASWING.QPAR[i1:i2,JNTA]
            tdeps = ASWING.QPAR[i1:i2,JTDE]
            tdgam = ASWING.QPAR[i1:i2,JTDG]
            Cshell = ASWING.QPAR[i1:i2,JCSH]
            Nshell = ASWING.QPAR[i1:i2,JNSH]
            Atshell = ASWING.QPAR[i1:i2,JASH]
            radius = ASWING.QPAR[i1:i2,JRAD]
            Cdf = ASWING.QPAR[i1:i2,JCDF]
            Cdp = ASWING.QPAR[i1:i2,JCDP]
            chord = ASWING.QPAR[i1:i2,JCH]
            Xax = ASWING.QPAR[i1:i2,JXAX]
            alpha = ASWING.QPAR[i1:i2,JAL]
            Cm = ASWING.QPAR[i1:i2,JCM]
            CLmax = ASWING.QPAR[i1:i2,JCMX]
            CLmin = ASWING.QPAR[i1:i2,JCMN]
            dCLda = ASWING.QPAR[i1:i2,JDCL]
            dCLdF = ASWING.QPAR[i1:i2,JCLF1:JCLF1+NFLPX-1]
            dCMdF = ASWING.QPAR[i1:i2,JCMF1:JCMF1+NFLPX-1]
            dCDdF = ASWING.QPAR[i1:i2,JCDF1:JCDF1+NFLPX-1]
            span_geom[ibeam] = SolutionGeometry(sgeom,x,y,z,twist,EIcc,EInn,EIcn,EIcs,EIsn,GJ,
            EA,GKc,GKn,mgcc,mgnn,mg,Ccg,Ncg,Dmgcc,Dmgnn,Dmg,DCcg,DNcg,Cea,Nea,Cta,Nta,
            tdeps,tdgam,Cshell,Nshell,Atshell,radius,Cdf,Cdp,chord,Xax,alpha,Cm,CLmax,
            CLmin,dCLda,dCLdF, dCMdF, dCDdF)
            # spanwise Solution
            x=ASWING.QPNT[i1:i2,1,ipnt]
            y=ASWING.QPNT[i1:i2,2,ipnt]
            z=ASWING.QPNT[i1:i2,3,ipnt]
            psi=ASWING.QPNT[i1:i2,4,ipnt]
            theta=ASWING.QPNT[i1:i2,5,ipnt]
            phi=ASWING.QPNT[i1:i2,6,ipnt]
            Mc=ASWING.QPNT[i1:i2,7,ipnt]
            Ms=ASWING.QPNT[i1:i2,8,ipnt]
            Mn=ASWING.QPNT[i1:i2,9,ipnt]
            Fc=ASWING.QPNT[i1:i2,10,ipnt]
            Fs=ASWING.QPNT[i1:i2,11,ipnt]
            Fn=ASWING.QPNT[i1:i2,12,ipnt]
            ux=ASWING.QPNT[i1:i2,13,ipnt]
            uy=ASWING.QPNT[i1:i2,14,ipnt]
            uz=ASWING.QPNT[i1:i2,15,ipnt]
            wx=ASWING.QPNT[i1:i2,16,ipnt]
            wy=ASWING.QPNT[i1:i2,17,ipnt]
            wz=ASWING.QPNT[i1:i2,18,ipnt]
            max_extensional_strain=ASWING.QPNT[i1:i2,19,ipnt]
            shear_stress=ASWING.QPNT[i1:i2,20,ipnt]
            chord=ASWING.QPNT[i1:(i2-1),21,ipnt]
            cl=ASWING.QPNT[i1:(i2-1),22,ipnt]
            cm=ASWING.QPNT[i1:(i2-1),23,ipnt]
            f_aero=ASWING.QPNT[i1:(i2-1),24,ipnt]
            f_reac=ASWING.QPNT[i1:(i2-1),25,ipnt]
            da_eff=ASWING.QPNT[i1:(i2-1),26,ipnt]
            da_flp=ASWING.QPNT[i1:(i2-1),27,ipnt]
            span_sol[ibeam] = SolutionSpanwise(x,y,z,psi,theta,phi,Mc,Ms,Mn,Fc,Fs,Fn,
            ux,uy,uz,wx,wy,wz,max_extensional_strain,shear_stress,chord,cl,cm,f_aero,
            f_reac,da_eff,da_flp)
        end

        # Create combined Solution
        sol[ipnt] = Solution(converged,pnt_param,span_geom,span_sol)
    end
    return sol
end


using TuringMachine

input_file = "example_input_1.txt"
program_file = "example_program_1.txt"

input = load_input(input_file)
program, init_state, halt_state = load_program(program_file)

state, tape_left, tape_right = set_up(init_state, input)

# run simulation step-by-step until we reach a halting state of 'accept' or 'reject'
while true
    try
        global state, tape_left, tape_right = simulate(state, program, tape_left, tape_right)
        println(state)
        state ==("qAccept") && break
        state ==("qReject") && break
    catch err
        println(err) && break
    end
end


abstract type PositionRecord end

mutable struct FullJob
    history::Vector{PositionRecord}
end

struct InTransit <: PositionRecord
    time::Float64 #time transit started
    end_time::Float64 #time transit will end
end

struct InQueue <: PositionRecord
    time::Float64 #time entered queue
    queue::Int #queue index
end

struct LeaveSystem <: PositionRecord
    time::Float64 #time left system
end

FullJob() = FullJob([])
FullJob(entry::PositionRecord) = FullJob([entry])

""" Returns the time the job entered the system """
entry_time(job::FullJob)::Float64 = first(job.history).time

""" Returns the time the job exited the system or time of last event if still in the system """
exit_time(job::FullJob)::Float64 = last(job.history).time

""" Returns the duration of time the job has spent in the system """
duration(job::FullJob)::Float64 = exit_time(job) - entry_time(job)

function push!(job::FullJob, pos::PositionRecord)
    push!(job.history, pos)
end

isless(p1::PositionRecord, p2::PositionRecord) = p1.time < p2.time
isless(p1::InTransit, p2::InTransit) = p1.end_time < p2.end_time
isless(j1::FullJob, j2::FullJob) = last(j1.history) < last(j2.history)

function show(io::IO, job::FullJob)
    print(io, "FullJob(Entry: $(entry_time(job)), Pos: $(last(job.history)), Duration: $(duration(job)))")
end

function show(io::IO, pos::InTransit)
    print(io, "(T: $(round(pos.time, digits = 4)))")
end

function show(io::IO, pos::InQueue)
    print(io, "(Q$(pos.queue): $(round(pos.time, digits = 4)))")
end

function show(io::IO, pos::LeaveSystem)
    print(io, "(E: $(round(pos.time, digits = 4)))")
end

function show(io::IO, history::Vector{PositionRecord})
    length(history) == 0 && print(io, "[]") && return
    print(io, "[$(first(history))")
    for i in 2:length(history)
        print(io, "-> $(history[i])")
    end
    print(io, "]")
end


@testset "sort.jl" begin
    @testset "sort" begin
        a = rand(10)
        test_rrule(sort, a)
        test_rrule(sort, a; fkwargs=(;rev=true))
    end
    @testset "partialsort" begin
        a = rand(10)
        test_rrule(partialsort, a, 4)
        test_rrule(partialsort, a, 3:5)
        test_rrule(partialsort, a, 1:2:6)

        test_rrule(partialsort, a, 4, fkwargs=(;rev=true))
    end

    @testset "sortslices" begin
        test_rrule(sortslices, rand(3,4); fkwargs=(; dims=2))
        test_rrule(sortslices, rand(5,4); fkwargs=(; dims=1, rev=true, by=last))
        test_rrule(sortslices, rand(3,4,5); fkwargs=(; dims=3, by=sum), check_inferred=false)

        @test_throws Exception sortslices(Diagonal(1:3), dims=1)
    end

    @testset "unique" begin
        # Trivial case, all unique:
        test_rrule(unique, rand(5))
        test_rrule(unique, rand(3,4))
        test_rrule(unique, rand(3,4); fkwargs=(; dims=2))

        # Not all unique:
        @test rrule(unique, [1,1,2,3])[1] == [1,2,3]
        @test rrule(unique, [1,1,2,3])[2]([10,20,30]) == (NoTangent(), [10, 0, 20, 30])

        @test rrule(unique, [1 2; 1 4])[1] == [1,2,4]
        @test rrule(unique, [1 2; 1 4])[2]([10,20,30]) == (NoTangent(), [10 20; 0 30])

        @test rrule(unique, [1 2 1 2; 1 2 1 4], dims=2)[1] == [1 2 2; 1 2 4]
        @test rrule(unique, [1 2 1 2; 1 2 1 4], dims=2)[2]([10 20 30; 40 50 60])[2] == [10 20 0 30; 40 50 0 60]

        @test rrule(unique, Diagonal([1,2,3]))[1] == [1,0,2,3]
        @test rrule(unique, Diagonal([1,2,3]))[2]([10 20 30 40])[2] == [10.0 0.0 0.0; 0.0 30.0 0.0; 0.0 0.0 40.0]
        @test rrule(unique, Diagonal([1,2,3]))[2]([10 20 30 40])[2] isa Diagonal
    end
end


################################################################################
#
#	CONCRETE TYPE
#
#   Site{L,D} <: AbstractSite{L,D}
#   --> L is the label type
#   --> D is the dimension of embedding space
#
#   FILE CONTAINS
#       - concrete struct definition
#       - interface implementation
#
################################################################################

################################################################################
#
#   CONCRETE STRUCT DEFINITION
#
################################################################################
mutable struct Site{L,D} <: AbstractSite{L,D}

	# point
	point	:: Vector{Float64}

	# label
	label	:: L

end

# export the concrete type
export Site

################################################################################
#
#	IMPLEMENTATION OF INTERFACE FOR CONCRETE SITE TYPE
#	(functions that had to be overwritten by concrete type)
#
################################################################################

# default constructor interface
# used for creation of new sites
function newSite(
            :: Type{Site{L,D}},
            point   :: Vector{<:Real},
            label   :: L
        ) :: Site{L,D} where {L,D}

    # check if correct dimension is given
    @assert length(point) == D

    # return a new object
    return Site{L,D}(point, label)
end

# get label
function label(
            s :: Site{L,D}
        ) :: L where {L,D}

    # return the label
    return s.label
end
# set label
function label!(
            s :: Site{L,D},
            l :: L
        ) where {L,D}

    # return the label
    s.label = l
end

# get point
function point(
            s :: Site{L,D}
        ) :: Vector{Float64} where {L,D}

    # return the point
    return s.point
end
# set point
function point!(
            s :: Site{L,D},
            p :: Vector{<:Real}
        ) where {L,D}

    # return the point
    s.point = p
end

function saveSites(
        ss :: Vector{S},
        fn :: AbstractString,
        group :: AbstractString = "sites"
        ;
        append :: Bool = false
    ) where {LS,D,S<:Site{LS,D}}

    # determine the mode based on if one wants to append stuff
    if append
        mode = "r+"
    else
        mode = "w"
    end

    # open the file in mode
    h5open(fn, mode) do ucfile
        # create the group in which the bonds are saved
        group_sites = g_create(ucfile, group)
        # save the parameters
        attrs(group_sites)["D"] = Int64(D)
        attrs(group_sites)["L"] = string(LS)
        # save all Positions (D dimensions)
        if 0 < Int64(D)
            for n in 1:Int64(D)
                group_sites["point_$(n)"] = Float64[point(s)[n] for s in ss]
            end
        end
        # save all labels
        if LS <: Number
            group_sites["label"] = [label(s) for s in ss]
        else
            group_sites["label"] = String[string(label(s)) for s in ss]
        end
    end

    # return nothing
    return nothing
end

function loadSites(
        ::Type{SI},
        fn :: AbstractString,
        group :: AbstractString = "sites"
    ) where {LSI,DI,SI<:Union{Site{LSI,DI},Site}}

    # read attribute data
    attr_data = h5readattr(fn, group)
    # determine D and L based on this
    D  = attr_data["D"]
    LS = Meta.eval(Meta.parse(attr_data["L"]))

    # load all remaining data
    st_label = h5read(fn, group*"/label")

    if D == 0
        st_point = [Float64[] for i in 1:length(st_label)]
    else
        st_point_parts = [
            h5read(fn, group*"/point_"*string(j)) for j in 1:D
        ]
        st_point = Vector{Float64}[Float64[st_point_parts[j][i] for j in 1:D] for i in 1:length(st_label)]
    end

    # create list of sites
    ss = Site{LS,D}[
        newSite(Site{LS,D}, st_point[i], LS(st_label[i]))
        for i in 1:length(st_label)
    ]

    # return nothing
    return ss
end

# convinience function for standard type
function loadSites(
        fn :: AbstractString,
        group :: AbstractString = "sites"
    )

    return loadSites(Site, fn, group)
end


functor(T, x) = (), _ -> x
functor(x) = functor(typeof(x), x)

functor(::Type{<:Tuple}, x) = x, y -> y
functor(::Type{<:NamedTuple}, x) = x, y -> y

functor(::Type{<:AbstractArray}, x) = x, y -> y
functor(::Type{<:AbstractArray{<:Number}}, x) = (), _ -> x

function makefunctor(m::Module, T, fs = fieldnames(T))
  @eval m begin
    $Functors.functor(::Type{<:$T}, x) = ($([:($f=x.$f) for f in fs]...),), y -> $T(y...)
  end
end

function functorm(T, fs = nothing)
  fs == nothing || isexpr(fs, :tuple) || error("@functor T (a, b)")
  fs = fs == nothing ? [] : [:($(map(QuoteNode, fs.args)...),)]
  :(makefunctor(@__MODULE__, $(esc(T)), $(fs...)))
end

macro functor(args...)
  functorm(args...)
end

isleaf(x) = functor(x)[1] === ()

function fmap1(f, x)
  func, re = functor(x)
  re(map(f, func))
end

function fmap(f, x; cache = IdDict())
  haskey(cache, x) && return cache[x]
  cache[x] = isleaf(x) ? f(x) : fmap1(x -> fmap(f, x, cache = cache), x)
end


##########################################################################
# Timing for list of methods. loop_over works like in simulate.
function timing(network, simulation_params; loop_over::Symbol=:precoding_methods)
    println("-- timing on $network.")
    Lumberjack.info("Starting timing.", @compat Dict(:network => network, :simulation_params => simulation_params))

    # Get assignment method if needed
    assignment_method() = nothing
    if loop_over == :precoding_methods
        if haskey(simulation_params, "assignment_methods")
            if length(simulation_params["assignment_methods"]) > 1
                Lumberjack.warn("Looping over precoding methods: will only use first assignment method provided.")
            end
            assignment_method(channel, network) = simulation_params["assignment_methods"][1](channel, network)
        else
            assignment_method(channel, network) = IDCellAssignment!(channel, network)
        end
    end

    # Set initial aux params
    set_initial_aux_params!(simulation_params, network)

    # Ensure that we are not storing all intermediate iterations.
    set_aux_precoding_param!(network, :final_iteration, "output_protocol")

    # Get channel realization to work on
    draw_user_drop!(network)
    channel = draw_channel(network)

    if loop_over == :precoding_methods
        # Perform assignment
        assignment_method(channel, network)

        # Make sure things are JITed
        for method in simulation_params["precoding_methods"]
            method(channel, network)
        end

        # Run performance test
        for method in simulation_params["precoding_methods"]
            println("--- Testing performance of ", stringify_method(method))
            @time for i = 1:simulation_params["Ntest"]; method(channel, network); end
        end
    elseif loop_over == :assignment_methods
        # Make sure things are JITed
        for method in simulation_params["assignment_methods"]
            method(channel, network)
        end

        # Run performance test
        for method in simulation_params["assignment_methods"]
            println("--- Testing performance of ", stringify_method(method))
            @time for i = 1:simulation_params["Ntest"]; method(channel, network); end
        end
    end
end


"""
    SplitChannels <: Augmentor.Operation

Description
--------------

Splits out the color channels of the given image using the
function `ImageCore.channelview`. This will effectively create a
new array dimension for the colors in the front. In contrast to
`ImageCore.channelview` it will also result in a new dimension
for gray images.

This operation is mainly useful at the end of a pipeline in
combination with [`PermuteDims`](@ref) in order to prepare the
image for the training algorithm, which often requires the color
channels to be separate.

Usage
--------------

    SplitChannels()

See also
--------------

[`PermuteDims`](@ref), [`CombineChannels`](@ref), [`augment`](@ref)

Examples
--------------

```julia-repl
julia> using Augmentor

julia> img = testpattern()
300×400 Array{RGBA{N0f8},2}:
[...]

julia> augment(img, SplitChannels())
4×300×400 Array{N0f8,3}:
[...]

julia> augment(img, SplitChannels() |> PermuteDims(3,2,1))
400×300×4 Array{N0f8,3}:
[...]
```
"""
struct SplitChannels <: Operation end

@inline supports_eager(::Type{SplitChannels}) = false
@inline supports_lazy(::Type{SplitChannels}) = true

function applylazy(op::SplitChannels, img::AbstractArray{<:Colorant}, param)
    plain_indices(channelview(img))
end

function applylazy(op::SplitChannels, img::AbstractArray{<:AbstractGray}, param)
    ns = (1, map(length, indices(img))...)
    reshape(channelview(img), ns)
end

function showconstruction(io::IO, op::SplitChannels)
    print(io, typeof(op).name.name, "()")
end

function Base.show(io::IO, op::SplitChannels)
    if get(io, :compact, false)
        print(io, "Split colorant into its color channels")
    else
        print(io, "Augmentor.")
        showconstruction(io, op)
    end
end

# --------------------------------------------------------------------

"""
    CombineChannels <: Augmentor.Operation

Description
--------------

Combines the first dimension of a given array into a colorant of
type `colortype` using the function `ImageCore.colorview`. The
main difference is that a separate color channel is also expected
for Gray images.

The shape of the input image has to be appropriate for the given
`colortype`, which also means that the separated color channel
has to be the first dimension of the array. See
[`PermuteDims`](@ref) if that is not the case.

Usage
--------------

    CombineChannels(colortype)

Arguments
--------------

- **`colortype`** : The color type of the resulting image. Must
    be a subtype of `ColorTypes.Colorant` and match the color
    channel of the given image.

See also
--------------

[`SplitChannels`](@ref), [`PermuteDims`](@ref), [`augment`](@ref)

Examples
--------------

```julia-repl
julia> using Augmentor, Colors

julia> A = rand(3, 10, 10) # three color channels
3×10×10 Array{Float64,3}:
[...]

julia> augment(A, CombineChannels(RGB))
10×10 Array{RGB{Float64},2}:
[...]

julia> B = rand(1, 10, 10) # singleton color channel
1×10×10 Array{Float64,3}:
[...]

julia> augment(B, CombineChannels(Gray))
10×10 Array{Gray{Float64},2}:
[...]
```
"""
struct CombineChannels{T<:Colorant} <: Operation
    colortype::Type{T}
end

@inline supports_eager(::Type{<:CombineChannels}) = false
@inline supports_lazy(::Type{<:CombineChannels}) = true

function applylazy(op::CombineChannels, img::AbstractArray{<:Number}, param)
    colorview(op.colortype, plain_indices(img))
end

function applylazy(op::CombineChannels{<:AbstractGray}, img::AbstractArray{<:Number}, param)
    length(indices(img,1)) == 1 || throw(ArgumentError("The given image must have a singleton colorchannel in the first dimension in order to combine the channels to a AbstractGray colorant"))
    ns = Base.tail(map(length, indices(img)))
    colorview(op.colortype, reshape(img, ns))
end

function showconstruction(io::IO, op::CombineChannels)
    print(io, typeof(op).name.name, '(')
    _showcolor(io, op.colortype)
    print(io, ')')
end

function Base.show(io::IO, op::CombineChannels)
    if get(io, :compact, false)
        print(io, "Combine color channels into colorant ")
        _showcolor(io, op.colortype)
    else
        print(io, "Augmentor.")
        showconstruction(io, op)
    end
end


#=
This file is auto-generated. Do not edit.
=#
"""
    mutable struct VirtualInertia <: ActivePowerControl
        Ta::Float64
        kd::Float64
        kω::Float64
        ωb::Float64
        ext::Dict{String, Any}
        states::Vector{Symbol}
        n_states::Int64
        internal::InfrastructureSystemsInternal
    end

Parameters of a Virtual Inertia with SRF using VSM for active power controller

# Arguments
- `Ta::Float64`: VSM inertia constant, validation range: (0, nothing)
- `kd::Float64`: VSM damping constant, validation range: (0, nothing)
- `kω::Float64`: frequency droop gain, validation range: (0, nothing)
- `ωb::Float64`: rated angular frequency, validation range: (0, nothing)
- `ext::Dict{String, Any}`
- `states::Vector{Symbol}`
- `n_states::Int64`
- `internal::InfrastructureSystemsInternal`: power system internal reference, do not modify
"""
mutable struct VirtualInertia <: ActivePowerControl
    "VSM inertia constant"
    Ta::Float64
    "VSM damping constant"
    kd::Float64
    "frequency droop gain"
    kω::Float64
    "rated angular frequency"
    ωb::Float64
    ext::Dict{String, Any}
    states::Vector{Symbol}
    n_states::Int64
    "power system internal reference, do not modify"
    internal::InfrastructureSystemsInternal
end

function VirtualInertia(Ta, kd, kω, ωb, ext=Dict{String, Any}(), )
    VirtualInertia(Ta, kd, kω, ωb, ext, [:δω_vsm, :δθ_vsm], 2, InfrastructureSystemsInternal(), )
end

function VirtualInertia(; Ta, kd, kω, ωb, ext=Dict{String, Any}(), )
    VirtualInertia(Ta, kd, kω, ωb, ext, )
end

# Constructor for demo purposes; non-functional.
function VirtualInertia(::Nothing)
    VirtualInertia(;
        Ta=0,
        kd=0,
        kω=0,
        ωb=0,
        ext=Dict{String, Any}(),
    )
end

"""Get VirtualInertia Ta."""
get_Ta(value::VirtualInertia) = value.Ta
"""Get VirtualInertia kd."""
get_kd(value::VirtualInertia) = value.kd
"""Get VirtualInertia kω."""
get_kω(value::VirtualInertia) = value.kω
"""Get VirtualInertia ωb."""
get_ωb(value::VirtualInertia) = value.ωb
"""Get VirtualInertia ext."""
get_ext(value::VirtualInertia) = value.ext
"""Get VirtualInertia states."""
get_states(value::VirtualInertia) = value.states
"""Get VirtualInertia n_states."""
get_n_states(value::VirtualInertia) = value.n_states
"""Get VirtualInertia internal."""
get_internal(value::VirtualInertia) = value.internal


function initialize!(ips::AbstractInteriorPointSolver)
    # initializing slack variables
    @trace(ips.logger,"Initializing slack variables.")
    cons!(ips.nlp,get_x0(ips.nlp),view(ips.c,1:get_ncon(ips.nlp)))
    ips.cnt.con_cnt += 1
    ips.x_slk.=ips.c_slk

    # Initialization
    @trace(ips.logger,"Initializing primal and bound duals.")
    ips.zl_r.=1.0
    ips.zu_r.=1.0
    ips.xl_r.-= max.(1,abs.(ips.xl_r)).*ips.opt.tol
    ips.xu_r.+= max.(1,abs.(ips.xu_r)).*ips.opt.tol
    initialize_variables!(ips.x,ips.xl,ips.xu,ips.opt.bound_push,ips.opt.bound_fac)

    # Automatic scaling (constraints)
    @trace(ips.logger,"Computing constraint scaling.")
    eval_jac_wrapper!(ips, ips.kkt, ips.x)
    compress_jacobian!(ips.kkt)
    if (ips.m > 0) && ips.opt.nlp_scaling
        jac = get_raw_jacobian(ips.kkt)
        scale_constraints!(ips.nlp, ips.con_scale, jac; max_gradient=ips.opt.nlp_scaling_max_gradient)
        set_jacobian_scaling!(ips.kkt, ips.con_scale)
        ips.l./=ips.con_scale
    end
    compress_jacobian!(ips.kkt)

    # Automatic scaling (objective)
    eval_grad_f_wrapper!(ips, ips.f,ips.x)
    @trace(ips.logger,"Computing objective scaling.")
    if ips.opt.nlp_scaling
        ips.obj_scale[] = scale_objective(ips.nlp, ips.f; max_gradient=ips.opt.nlp_scaling_max_gradient)
        ips.f.*=ips.obj_scale[]
    end

    # Initialize dual variables
    @trace(ips.logger,"Initializing constraint duals.")
    if !ips.opt.dual_initialized
        set_initial_rhs!(ips, ips.kkt)
        initialize!(ips.kkt)
        factorize_wrapper!(ips)
        solve_refine_wrapper!(ips,ips.d,ips.p)
        if norm(dual(ips.d), Inf) > ips.opt.constr_mult_init_max
            fill!(ips.l, 0.0)
        else
            copyto!(ips.l, dual(ips.d))
        end
    end

    # Initializing
    ips.obj_val = eval_f_wrapper(ips, ips.x)
    eval_cons_wrapper!(ips, ips.c, ips.x)
    eval_lag_hess_wrapper!(ips, ips.kkt, ips.x, ips.l)

    theta = get_theta(ips.c)
    ips.theta_max=1e4*max(1,theta)
    ips.theta_min=1e-4*max(1,theta)
    ips.mu=ips.opt.mu_init
    ips.tau=max(ips.opt.tau_min,1-ips.opt.mu_init)
    ips.filter = [(ips.theta_max,-Inf)]

    return REGULAR
end

function reinitialize!(ips::AbstractInteriorPointSolver)
    view(ips.x,1:get_nvar(ips.nlp)) .= get_x0(ips.nlp)

    ips.obj_val = eval_f_wrapper(ips, ips.x)
    eval_grad_f_wrapper!(ips, ips.f, ips.x)
    eval_cons_wrapper!(ips, ips.c, ips.x)
    eval_jac_wrapper!(ips, ips.kkt, ips.x)
    eval_lag_hess_wrapper!(ips, ips.kkt, ips.x, ips.l)

    theta = get_theta(ips.c)
    ips.theta_max=1e4*max(1,theta)
    ips.theta_min=1e-4*max(1,theta)
    ips.mu=ips.opt.mu_init
    ips.tau=max(ips.opt.tau_min,1-ips.opt.mu_init)
    ips.filter = [(ips.theta_max,-Inf)]

    return REGULAR
end

# major loops ---------------------------------------------------------
function optimize!(ips::AbstractInteriorPointSolver)
    try
        if ips.status == INITIAL
            @notice(ips.logger,"This is $(introduce()), running with $(introduce(ips.linear_solver))\n")
            print_init(ips)
            ips.status = initialize!(ips)
        else # resolving the problem
            ips.status = reinitialize!(ips)
        end

        while ips.status >= REGULAR
            ips.status == REGULAR && (ips.status = regular!(ips))
            ips.status == RESTORE && (ips.status = restore!(ips))
            ips.status == ROBUST && (ips.status = robust!(ips))
        end
    catch e
        if e isa InvalidNumberException
            ips.status=INVALID_NUMBER_DETECTED
        elseif e isa NotEnoughDegreesOfFreedomException
            ips.status=NOT_ENOUGH_DEGREES_OF_FREEDOM
        elseif e isa LinearSolverException
            ips.status=ERROR_IN_STEP_COMPUTATION;
            ips.opt.rethrow_error && rethrow(e)
        elseif e isa InterruptException
            ips.status=USER_REQUESTED_STOP
            ips.opt.rethrow_error && rethrow(e)
        else
            ips.status=INTERNAL_ERROR
            ips.opt.rethrow_error && rethrow(e)
        end
    finally
        ips.cnt.total_time = time() - ips.cnt.start_time
        !(ips.status < SOLVE_SUCCEEDED) && (print_summary_1(ips);print_summary_2(ips))
        # Unscale once the summary has been printed out
        unscale!(ips)
        @notice(ips.logger,"EXIT: $(STATUS_OUTPUT_DICT[ips.status])")
        ips.opt.disable_garbage_collector &&
            (GC.enable(true); @warn(ips.logger,"Julia garbage collector is turned back on"))
        finalize(ips.logger)
    end
    return MadNLPExecutionStats(ips)
end

function unscale!(ips::AbstractInteriorPointSolver)
    ips.obj_val/=ips.obj_scale[]
    ips.c ./= ips.con_scale
    ips.c .-= ips.rhs
    ips.c_slk .+= ips.x_slk
end

function regular!(ips::AbstractInteriorPointSolver)
    while true
        if (ips.cnt.k!=0 && !ips.opt.jacobian_constant)
            eval_jac_wrapper!(ips, ips.kkt, ips.x)
        end
        jtprod!(ips.jacl, ips.kkt, ips.l)
        fixed_variable_treatment_vec!(ips.jacl,ips.ind_fixed)
        fixed_variable_treatment_z!(ips.zl,ips.zu,ips.f,ips.jacl,ips.ind_fixed)

        sd = get_sd(ips.l,ips.zl_r,ips.zu_r,ips.opt.s_max)
        sc = get_sc(ips.zl_r,ips.zu_r,ips.opt.s_max)

        ips.inf_pr = get_inf_pr(ips.c)
        ips.inf_du = get_inf_du(ips.f,ips.zl,ips.zu,ips.jacl,sd)
        ips.inf_compl = get_inf_compl(ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,0.,sc)
        inf_compl_mu = get_inf_compl(ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,ips.mu,sc)

        print_iter(ips)

        # evaluate termination criteria
        @trace(ips.logger,"Evaluating termination criteria.")
        max(ips.inf_pr,ips.inf_du,ips.inf_compl) <= ips.opt.tol && return SOLVE_SUCCEEDED
        max(ips.inf_pr,ips.inf_du,ips.inf_compl) <= ips.opt.acceptable_tol ?
            (ips.cnt.acceptable_cnt < ips.opt.acceptable_iter ?
            ips.cnt.acceptable_cnt+=1 : return SOLVED_TO_ACCEPTABLE_LEVEL) : (ips.cnt.acceptable_cnt = 0)
        max(ips.inf_pr,ips.inf_du,ips.inf_compl) >= ips.opt.diverging_iterates_tol && return DIVERGING_ITERATES
        ips.cnt.k>=ips.opt.max_iter && return MAXIMUM_ITERATIONS_EXCEEDED
        time()-ips.cnt.start_time>=ips.opt.max_wall_time && return MAXIMUM_WALLTIME_EXCEEDED

        # update the barrier parameter
        @trace(ips.logger,"Updating the barrier parameter.")
        while ips.mu != max(ips.opt.mu_min,ips.opt.tol/10) &&
            max(ips.inf_pr,ips.inf_du,inf_compl_mu) <= ips.opt.barrier_tol_factor*ips.mu
            mu_new = get_mu(ips.mu,ips.opt.mu_min,
                            ips.opt.mu_linear_decrease_factor,ips.opt.mu_superlinear_decrease_power,ips.opt.tol)
            inf_compl_mu = get_inf_compl(ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,ips.mu,sc)
            ips.tau= get_tau(ips.mu,ips.opt.tau_min)
            ips.mu = mu_new
            empty!(ips.filter)
            push!(ips.filter,(ips.theta_max,-Inf))
        end

        # compute the newton step
        @trace(ips.logger,"Computing the newton step.")
        if (ips.cnt.k!=0 && !ips.opt.hessian_constant)
            eval_lag_hess_wrapper!(ips, ips.kkt, ips.x, ips.l)
        end

        set_aug_diagonal!(ips.kkt,ips)
        set_aug_rhs!(ips, ips.kkt, ips.c)
        if ips.opt.inertia_correction_method == INERTIA_FREE
            set_aug_rhs_ifr!(ips, ips.kkt)
        end
        dual_inf_perturbation!(primal(ips.p),ips.ind_llb,ips.ind_uub,ips.mu,ips.opt.kappa_d)

        # start inertia conrrection
        @trace(ips.logger,"Solving primal-dual system.")
        if ips.opt.inertia_correction_method == INERTIA_FREE
            inertia_free_reg(ips) || return ROBUST
        elseif ips.opt.inertia_correction_method == INERTIA_BASED
            inertia_based_reg(ips) || return ROBUST
        end

        finish_aug_solve!(ips, ips.kkt, ips.mu)

        # filter start
        @trace(ips.logger,"Backtracking line search initiated.")
        theta = get_theta(ips.c)
        varphi= get_varphi(ips.obj_val,ips.x_lr,ips.xl_r,ips.xu_r,ips.x_ur,ips.mu)
        varphi_d = get_varphi_d(ips.f,ips.x,ips.xl,ips.xu,primal(ips.d),ips.mu)

        alpha_max = get_alpha_max(ips.x,ips.xl,ips.xu,primal(ips.d),ips.tau)
        ips.alpha_z = get_alpha_z(ips.zl_r,ips.zu_r,dual_lb(ips.d),dual_ub(ips.d),ips.tau)
        alpha_min = get_alpha_min(theta,varphi_d,ips.theta_min,ips.opt.gamma_theta,ips.opt.gamma_phi,
                                  ips.opt.alpha_min_frac,ips.opt.delta,ips.opt.s_theta,ips.opt.s_phi)
        ips.cnt.l = 1
        ips.alpha = alpha_max
        varphi_trial= 0.
            theta_trial = 0.
            small_search_norm = get_rel_search_norm(ips.x, primal(ips.d)) < 10*eps(Float64)
        switching_condition = is_switching(varphi_d,ips.alpha,ips.opt.s_phi,ips.opt.delta,2.,ips.opt.s_theta)
        armijo_condition = false
        while true
            copyto!(ips.x_trial, ips.x)
            axpy!(ips.alpha, primal(ips.d), ips.x_trial)

            ips.obj_val_trial = eval_f_wrapper(ips, ips.x_trial)
            eval_cons_wrapper!(ips, ips.c_trial, ips.x_trial)

            theta_trial = get_theta(ips.c_trial)
            varphi_trial= get_varphi(ips.obj_val_trial,ips.x_trial_lr,ips.xl_r,ips.xu_r,ips.x_trial_ur,ips.mu)
            armijo_condition = is_armijo(varphi_trial,varphi,ips.opt.eta_phi,ips.alpha,varphi_d)

            small_search_norm && break

            ips.ftype = get_ftype(
                ips.filter,theta,theta_trial,varphi,varphi_trial,switching_condition,armijo_condition,
                ips.theta_min,ips.opt.obj_max_inc,ips.opt.gamma_theta,ips.opt.gamma_phi,
                has_constraints(ips))
            ips.ftype in ["f","h"] && (@trace(ips.logger,"Step accepted with type $(ips.ftype)"); break)

            ips.cnt.l==1 && theta_trial>=theta && second_order_correction(
                ips,alpha_max,theta,varphi,theta_trial,varphi_d,switching_condition) && break

            ips.alpha /= 2
            ips.cnt.l += 1
            if ips.alpha < alpha_min
                @debug(ips.logger,
                       "Cannot find an acceptable step at iteration $(ips.cnt.k). Switching to restoration phase.")
                ips.cnt.k+=1
                return RESTORE
            else
                @trace(ips.logger,"Step rejected; proceed with the next trial step.")
                ips.alpha * norm(primal(ips.d)) < eps(Float64)*10 &&
                    return ips.cnt.acceptable_cnt >0 ?
                    SOLVED_TO_ACCEPTABLE_LEVEL : SEARCH_DIRECTION_BECOMES_TOO_SMALL
            end
        end

        @trace(ips.logger,"Updating primal-dual variables.")
        ips.x.=ips.x_trial
        ips.c.=ips.c_trial
        ips.obj_val=ips.obj_val_trial
        adjusted = adjust_boundary!(ips.x_lr,ips.xl_r,ips.x_ur,ips.xu_r,ips.mu)
        adjusted > 0 &&
            @warn(ips.logger,"In iteration $(ips.cnt.k), $adjusted Slack too small, adjusting variable bound")

        axpy!(ips.alpha,dual(ips.d),ips.l)
        axpy!(ips.alpha_z, dual_lb(ips.d), ips.zl_r)
        axpy!(ips.alpha_z, dual_ub(ips.d), ips.zu_r)
        reset_bound_dual!(ips.zl,ips.x,ips.xl,ips.mu,ips.opt.kappa_sigma)
        reset_bound_dual!(ips.zu,ips.xu,ips.x,ips.mu,ips.opt.kappa_sigma)
        eval_grad_f_wrapper!(ips, ips.f,ips.x)

        if !switching_condition || !armijo_condition
            @trace(ips.logger,"Augmenting filter.")
            augment_filter!(ips.filter,theta_trial,varphi_trial,ips.opt.gamma_theta)
        end

        ips.cnt.k+=1
        @trace(ips.logger,"Proceeding to the next interior point iteration.")
    end
end

function restore!(ips::AbstractInteriorPointSolver)
    ips.del_w=0
    primal(ips._w1) .= ips.x # backup the previous primal iterate
    dual(ips._w1) .= ips.l # backup the previous primal iterate
    dual(ips._w2) .= ips.c # backup the previous primal iterate

    F = get_F(ips.c,ips.f,ips.zl,ips.zu,ips.jacl,ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,ips.mu)
    ips.cnt.t = 0
    ips.alpha_z = 0.
    ips.ftype = "R"

    while true
        ips.alpha = min(get_alpha_max(ips.x,ips.xl,ips.xu,primal(ips.d),ips.tau),
                        get_alpha_z(ips.zl_r,ips.zu_r,dual_lb(ips.d),dual_ub(ips.d),ips.tau))

        ips.x .+= ips.alpha.* primal(ips.d)
        ips.l .+= ips.alpha.* dual(ips.d)
        ips.zl_r.+=ips.alpha.* dual_lb(ips.d)
        ips.zu_r.+=ips.alpha.* dual_ub(ips.d)

        eval_cons_wrapper!(ips,ips.c,ips.x)
        eval_grad_f_wrapper!(ips,ips.f,ips.x)
        ips.obj_val = eval_f_wrapper(ips,ips.x)

        !ips.opt.jacobian_constant && eval_jac_wrapper!(ips,ips.kkt,ips.x)
        jtprod!(ips.jacl,ips.kkt,ips.l)

        F_trial = get_F(
            ips.c,ips.f,ips.zl,ips.zu,ips.jacl,ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,ips.mu)
        if F_trial > ips.opt.soft_resto_pderror_reduction_factor*F
            ips.x .= primal(ips._w1)
            ips.l .= dual(ips._w1)
            ips.c .= dual(ips._w2) # backup the previous primal iterate
            return ROBUST
        end

        adjusted = adjust_boundary!(ips.x_lr,ips.xl_r,ips.x_ur,ips.xu_r,ips.mu)
        adjusted > 0 &&
            @warn(ips.logger,"In iteration $(ips.cnt.k), $adjusted Slack too small, adjusting variable bound")

        F = F_trial

        theta = get_theta(ips.c)
        varphi= get_varphi(ips.obj_val,ips.x_lr,ips.xl_r,ips.xu_r,ips.x_ur,ips.mu)

        ips.cnt.k+=1

        is_filter_acceptable(ips.filter,theta,varphi) ? (return REGULAR) : (ips.cnt.t+=1)
        ips.cnt.k>=ips.opt.max_iter && return MAXIMUM_ITERATIONS_EXCEEDED
        time()-ips.cnt.start_time>=ips.opt.max_wall_time && return MAXIMUM_WALLTIME_EXCEEDED

        sd = get_sd(ips.l,ips.zl_r,ips.zu_r,ips.opt.s_max)
        sc = get_sc(ips.zl_r,ips.zu_r,ips.opt.s_max)
        ips.inf_pr = get_inf_pr(ips.c)
        ips.inf_du = get_inf_du(ips.f,ips.zl,ips.zu,ips.jacl,sd)

        ips.inf_compl = get_inf_compl(ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,0.,sc)
        inf_compl_mu = get_inf_compl(ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,ips.mu,sc)
        print_iter(ips)

        !ips.opt.hessian_constant && eval_lag_hess_wrapper!(ips,ips.kkt,ips.x,ips.l)
        set_aug_diagonal!(ips.kkt,ips)
        set_aug_rhs!(ips, ips.kkt, ips.c)

        dual_inf_perturbation!(primal(ips.p),ips.ind_llb,ips.ind_uub,ips.mu,ips.opt.kappa_d)
        factorize_wrapper!(ips)
        solve_refine_wrapper!(ips,ips.d,ips.p)
        finish_aug_solve!(ips, ips.kkt, ips.mu)

        ips.ftype = "f"
    end
end

function robust!(ips::InteriorPointSolver)
    initialize_robust_restorer!(ips)
    RR = ips.RR
    while true
        if !ips.opt.jacobian_constant
            eval_jac_wrapper!(ips, ips.kkt, ips.x)
        end
        jtprod!(ips.jacl, ips.kkt, ips.l)
        fixed_variable_treatment_vec!(ips.jacl,ips.ind_fixed)
        fixed_variable_treatment_z!(ips.zl,ips.zu,ips.f,ips.jacl,ips.ind_fixed)

        # evaluate termination criteria
        @trace(ips.logger,"Evaluating restoration phase termination criteria.")
        sd = get_sd(ips.l,ips.zl_r,ips.zu_r,ips.opt.s_max)
        sc = get_sc(ips.zl_r,ips.zu_r,ips.opt.s_max)
        ips.inf_pr = get_inf_pr(ips.c)
        ips.inf_du = get_inf_du(ips.f,ips.zl,ips.zu,ips.jacl,sd)
        ips.inf_compl = get_inf_compl(ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,0.,sc)

        # Robust restoration phase error
        RR.inf_pr_R = get_inf_pr_R(ips.c,RR.pp,RR.nn)
        RR.inf_du_R = get_inf_du_R(RR.f_R,ips.l,ips.zl,ips.zu,ips.jacl,RR.zp,RR.zn,ips.opt.rho,sd)
        RR.inf_compl_R = get_inf_compl_R(
            ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,RR.pp,RR.zp,RR.nn,RR.zn,0.,sc)
        inf_compl_mu_R = get_inf_compl_R(
            ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,RR.pp,RR.zp,RR.nn,RR.zn,RR.mu_R,sc)

        print_iter(ips;is_resto=true)

        max(RR.inf_pr_R,RR.inf_du_R,RR.inf_compl_R) <= ips.opt.tol && return INFEASIBLE_PROBLEM_DETECTED
        ips.cnt.k>=ips.opt.max_iter && return MAXIMUM_ITERATIONS_EXCEEDED
        time()-ips.cnt.start_time>=ips.opt.max_wall_time && return MAXIMUM_WALLTIME_EXCEEDED

        # update the barrier parameter
        @trace(ips.logger,"Updating restoration phase barrier parameter.")
        while RR.mu_R != ips.opt.mu_min*100 &&
            max(RR.inf_pr_R,RR.inf_du_R,inf_compl_mu_R) <= ips.opt.barrier_tol_factor*RR.mu_R
            RR.mu_R = get_mu(RR.mu_R,ips.opt.mu_min,
                            ips.opt.mu_linear_decrease_factor,ips.opt.mu_superlinear_decrease_power,ips.opt.tol)
            inf_compl_mu_R = get_inf_compl_R(
                ips.x_lr,ips.xl_r,ips.zl_r,ips.xu_r,ips.x_ur,ips.zu_r,RR.pp,RR.zp,RR.nn,RR.zn,RR.mu_R,sc)
            RR.tau_R= max(ips.opt.tau_min,1-RR.mu_R)
            RR.zeta = sqrt(RR.mu_R)

            empty!(RR.filter)
            push!(RR.filter,(ips.theta_max,-Inf))
        end

        # compute the newton step
        if !ips.opt.hessian_constant
            eval_lag_hess_wrapper!(ips, ips.kkt, ips.x, ips.l; is_resto=true)
        end
        set_aug_RR!(ips.kkt, ips, RR)
        set_aug_rhs_RR!(ips, ips.kkt, RR, ips.opt.rho)

        # without inertia correction,
        @trace(ips.logger,"Solving restoration phase primal-dual system.")
        factorize_wrapper!(ips)
        solve_refine_wrapper!(ips,ips.d,ips.p)

        finish_aug_solve!(ips, ips.kkt, RR.mu_R)
        finish_aug_solve_RR!(RR.dpp,RR.dnn,RR.dzp,RR.dzn,ips.l,dual(ips.d),RR.pp,RR.nn,RR.zp,RR.zn,RR.mu_R,ips.opt.rho)

        theta_R = get_theta_R(ips.c,RR.pp,RR.nn)
        varphi_R = get_varphi_R(RR.obj_val_R,ips.x_lr,ips.xl_r,ips.xu_r,ips.x_ur,RR.pp,RR.nn,RR.mu_R)
        varphi_d_R = get_varphi_d_R(RR.f_R,ips.x,ips.xl,ips.xu,primal(ips.d),RR.pp,RR.nn,RR.dpp,RR.dnn,RR.mu_R,ips.opt.rho)

        # set alpha_min
        alpha_max = get_alpha_max_R(ips.x,ips.xl,ips.xu,primal(ips.d),RR.pp,RR.dpp,RR.nn,RR.dnn,RR.tau_R)
        ips.alpha_z = get_alpha_z_R(ips.zl_r,ips.zu_r,dual_lb(ips.d),dual_ub(ips.d),RR.zp,RR.dzp,RR.zn,RR.dzn,RR.tau_R)
        alpha_min = get_alpha_min(theta_R,varphi_d_R,ips.theta_min,ips.opt.gamma_theta,ips.opt.gamma_phi,
                                  ips.opt.alpha_min_frac,ips.opt.delta,ips.opt.s_theta,ips.opt.s_phi)

        # filter start
        @trace(ips.logger,"Backtracking line search initiated.")
        ips.alpha = alpha_max
        ips.cnt.l = 1
        theta_R_trial = 0.
        varphi_R_trial = 0.
        small_search_norm = get_rel_search_norm(ips.x, primal(ips.d)) < 10*eps(Float64)
        switching_condition = is_switching(varphi_d_R,ips.alpha,ips.opt.s_phi,ips.opt.delta,theta_R,ips.opt.s_theta)
        armijo_condition = false

        while true
            copyto!(ips.x_trial,ips.x)
            copyto!(RR.pp_trial,RR.pp)
            copyto!(RR.nn_trial,RR.nn)
            axpy!(ips.alpha,primal(ips.d),ips.x_trial)
            axpy!(ips.alpha,RR.dpp,RR.pp_trial)
            axpy!(ips.alpha,RR.dnn,RR.nn_trial)

            RR.obj_val_R_trial = get_obj_val_R(
                RR.pp_trial,RR.nn_trial,RR.D_R,ips.x_trial,RR.x_ref,ips.opt.rho,RR.zeta)
            eval_cons_wrapper!(ips, ips.c_trial, ips.x_trial)
            theta_R_trial  = get_theta_R(ips.c_trial,RR.pp_trial,RR.nn_trial)
            varphi_R_trial = get_varphi_R(
                RR.obj_val_R_trial,ips.x_trial_lr,ips.xl_r,ips.xu_r,ips.x_trial_ur,RR.pp_trial,RR.nn_trial,RR.mu_R)

            armijo_condition = is_armijo(varphi_R_trial,varphi_R,0.,ips.alpha,varphi_d_R) #####

            small_search_norm && break
            ips.ftype = get_ftype(
                RR.filter,theta_R,theta_R_trial,varphi_R,varphi_R_trial,
                switching_condition,armijo_condition,
                ips.theta_min,ips.opt.obj_max_inc,ips.opt.gamma_theta,ips.opt.gamma_phi,
                has_constraints(ips))
            ips.ftype in ["f","h"] && (@trace(ips.logger,"Step accepted with type $(ips.ftype)"); break)

            ips.alpha /= 2
            ips.cnt.l += 1
            if ips.alpha < alpha_min
                @debug(ips.logger,"Restoration phase cannot find an acceptable step at iteration $(ips.cnt.k).")
                return RESTORATION_FAILED
            else
                @trace(ips.logger,"Step rejected; proceed with the next trial step.")
                ips.alpha < eps(Float64)*10 && return ips.cnt.acceptable_cnt >0 ?
                    SOLVED_TO_ACCEPTABLE_LEVEL : SEARCH_DIRECTION_BECOMES_TOO_SMALL
            end
        end

        @trace(ips.logger,"Updating primal-dual variables.")
        ips.x.=ips.x_trial
        ips.c.=ips.c_trial
        RR.pp.=RR.pp_trial
        RR.nn.=RR.nn_trial

        RR.obj_val_R=RR.obj_val_R_trial
        RR.f_R .= RR.zeta.*RR.D_R.^2 .*(ips.x.-RR.x_ref)

        axpy!(ips.alpha, dual(ips.d), ips.l)
        axpy!(ips.alpha_z, dual_lb(ips.d),ips.zl_r)
        axpy!(ips.alpha_z, dual_ub(ips.d),ips.zu_r)
        axpy!(ips.alpha_z, RR.dzp,RR.zp)
        axpy!(ips.alpha_z, RR.dzn,RR.zn)

        reset_bound_dual!(ips.zl,ips.x,ips.xl,RR.mu_R,ips.opt.kappa_sigma)
        reset_bound_dual!(ips.zu,ips.xu,ips.x,RR.mu_R,ips.opt.kappa_sigma)
        reset_bound_dual!(RR.zp,RR.pp,RR.mu_R,ips.opt.kappa_sigma)
        reset_bound_dual!(RR.zn,RR.nn,RR.mu_R,ips.opt.kappa_sigma)

        adjusted = adjust_boundary!(ips.x_lr,ips.xl_r,ips.x_ur,ips.xu_r,ips.mu)
        adjusted > 0 &&
            @warn(ips.logger,"In iteration $(ips.cnt.k), $adjusted Slack too small, adjusting variable bound")

        if !switching_condition || !armijo_condition
            @trace(ips.logger,"Augmenting restoration phase filter.")
            augment_filter!(RR.filter,theta_R_trial,varphi_R_trial,ips.opt.gamma_theta)
        end

        # check if going back to regular phase
        @trace(ips.logger,"Checking if going back to regular phase.")
        ips.obj_val = eval_f_wrapper(ips, ips.x)
        eval_grad_f_wrapper!(ips, ips.f, ips.x)
        theta = get_theta(ips.c)
        varphi= get_varphi(ips.obj_val,ips.x_lr,ips.xl_r,ips.xu_r,ips.x_ur,ips.mu)

        if is_filter_acceptable(ips.filter,theta,varphi) &&
            theta <= ips.opt.required_infeasibility_reduction * RR.theta_ref

            @trace(ips.logger,"Going back to the regular phase.")
            ips.zl_r.=1
            ips.zu_r.=1

            set_initial_rhs!(ips, ips.kkt)
            initialize!(ips.kkt)

            factorize_wrapper!(ips)
            solve_refine_wrapper!(ips,ips.d,ips.p)
            if norm(dual(ips.d), Inf)>ips.opt.constr_mult_init_max
                fill!(ips.l, 0.0)
            else
                copyto!(ips.l, dual(ips.d))
            end
            ips.cnt.k+=1

            return REGULAR
        end

        ips.cnt.k>=ips.opt.max_iter && return MAXIMUM_ITERATIONS_EXCEEDED
        time()-ips.cnt.start_time>=ips.opt.max_wall_time && return MAXIMUM_WALLTIME_EXCEEDED

        @trace(ips.logger,"Proceeding to the next restoration phase iteration.")
        ips.cnt.k+=1
        ips.cnt.t+=1
    end
end

function inertia_based_reg(ips::InteriorPointSolver)
    @trace(ips.logger,"Inertia-based regularization started.")

    factorize_wrapper!(ips)
    num_pos,num_zero,num_neg = inertia(ips.linear_solver)
    solve_status = num_zero!= 0 ? false : solve_refine_wrapper!(ips,ips.d,ips.p)

    n_trial = 0
    ips.del_w = del_w_prev = 0.0
    while !is_inertia_correct(ips.kkt, num_pos, num_zero, num_neg) || !solve_status
        @debug(ips.logger,"Primal-dual perturbed.")
        if ips.del_w == 0.0
            ips.del_w = ips.del_w_last==0. ? ips.opt.first_hessian_perturbation :
                max(ips.opt.min_hessian_perturbation,ips.opt.perturb_dec_fact*ips.del_w_last)
        else
            ips.del_w*= ips.del_w_last==0. ? ips.opt.perturb_inc_fact_first : ips.opt.perturb_inc_fact
            if ips.del_w>ips.opt.max_hessian_perturbation ips.cnt.k+=1
                @debug(ips.logger,"Primal regularization is too big. Switching to restoration phase.")
                return false
            end
        end
        ips.del_c = (num_zero == 0 || !solve_status) ?
            ips.opt.jacobian_regularization_value * ips.mu^(ips.opt.jacobian_regularization_exponent) : 0.
        regularize_diagonal!(ips.kkt, ips.del_w - del_w_prev, ips.del_c)
        del_w_prev = ips.del_w

        factorize_wrapper!(ips)
        num_pos,num_zero,num_neg = inertia(ips.linear_solver)
        solve_status = num_zero!= 0 ? false : solve_refine_wrapper!(ips,ips.d,ips.p)
        n_trial += 1
    end
    ips.del_w != 0 && (ips.del_w_last = ips.del_w)

    return true
end

function inertia_free_reg(ips::InteriorPointSolver)

    @trace(ips.logger,"Inertia-free regularization started.")
    p0 = ips._w1
    d0 = ips._w2
    t = primal(ips._w3)
    n = primal(ips._w2)
    wx= primal(ips._w4)
    fill!(dual(ips._w3), 0)

    g = ips.x_trial # just to avoid new allocation
    g .= ips.f.-ips.mu./(ips.x.-ips.xl).+ips.mu./(ips.xu.-ips.x).+ips.jacl

    fixed_variable_treatment_vec!(primal(ips._w1), ips.ind_fixed)
    fixed_variable_treatment_vec!(primal(ips.p),   ips.ind_fixed)
    fixed_variable_treatment_vec!(g, ips.ind_fixed)

    factorize_wrapper!(ips)
    solve_status = (solve_refine_wrapper!(ips,d0,p0) && solve_refine_wrapper!(ips,ips.d,ips.p))
    t .= primal(ips.d) .- n
    mul!(ips._w4, ips.kkt, ips._w3) # prepartation for curv_test
    n_trial = 0
    ips.del_w = del_w_prev = 0.

    while !curv_test(t,n,g,wx,ips.opt.inertia_free_tol)  || !solve_status
        @debug(ips.logger,"Primal-dual perturbed.")
        if n_trial == 0
            ips.del_w = ips.del_w_last==.0 ? ips.opt.first_hessian_perturbation :
                max(ips.opt.min_hessian_perturbation,ips.opt.perturb_dec_fact*ips.del_w_last)
        else
            ips.del_w*= ips.del_w_last==.0 ? ips.opt.perturb_inc_fact_first : ips.opt.perturb_inc_fact
            if ips.del_w>ips.opt.max_hessian_perturbation ips.cnt.k+=1
                @debug(ips.logger,"Primal regularization is too big. Switching to restoration phase.")
                return false
            end
        end
        ips.del_c = !solve_status ?
            ips.opt.jacobian_regularization_value * ips.mu^(ips.opt.jacobian_regularization_exponent) : 0.
        regularize_diagonal!(ips.kkt, ips.del_w - del_w_prev, ips.del_c)
        del_w_prev = ips.del_w

        factorize_wrapper!(ips)
        solve_status = (solve_refine_wrapper!(ips,d0,p0) && solve_refine_wrapper!(ips,ips.d,ips.p))
        t .= primal(ips.d) .- n
        mul!(ips._w4, ips.kkt, ips._w3) # prepartation for curv_test
        n_trial += 1
    end

    ips.del_w != 0 && (ips.del_w_last = ips.del_w)
    return true
end

curv_test(t,n,g,wx,inertia_free_tol) = dot(wx,t) + max(dot(wx,n)-dot(g,n),0) - inertia_free_tol*dot(t,t) >=0

function second_order_correction(ips::AbstractInteriorPointSolver,alpha_max::Float64,theta::Float64,varphi::Float64,
                                 theta_trial::Float64,varphi_d::Float64,switching_condition::Bool)
    @trace(ips.logger,"Second-order correction started.")

    dual(ips._w1) .= alpha_max .* ips.c .+ ips.c_trial
    theta_soc_old = theta_trial
    for p=1:ips.opt.max_soc
        # compute second order correction
        set_aug_rhs!(ips, ips.kkt, dual(ips._w1))
        dual_inf_perturbation!(primal(ips.p),ips.ind_llb,ips.ind_uub,ips.mu,ips.opt.kappa_d)
        solve_refine_wrapper!(ips,ips._w1,ips.p)
        alpha_soc = get_alpha_max(ips.x,ips.xl,ips.xu,primal(ips._w1),ips.tau)

        ips.x_trial .= ips.x .+ alpha_soc .* primal(ips._w1)
        eval_cons_wrapper!(ips, ips.c_trial,ips.x_trial)
        ips.obj_val_trial = eval_f_wrapper(ips, ips.x_trial)

        theta_soc = get_theta(ips.c_trial)
        varphi_soc= get_varphi(ips.obj_val_trial,ips.x_trial_lr,ips.xl_r,ips.xu_r,ips.x_trial_ur,ips.mu)

        !is_filter_acceptable(ips.filter,theta_soc,varphi_soc) && break

        if theta <=ips.theta_min && switching_condition
            # Case I
            if is_armijo(varphi_soc,varphi,ips.opt.eta_phi,ips.alpha,varphi_d)
                @trace(ips.logger,"Step in second order correction accepted by armijo condition.")
                ips.ftype = "F"
                ips.alpha=alpha_soc
                return true
            end
        else
            # Case II
            if is_sufficient_progress(theta_soc,theta,ips.opt.gamma_theta,varphi_soc,varphi,ips.opt.gamma_phi,has_constraints(ips))
                @trace(ips.logger,"Step in second order correction accepted by sufficient progress.")
                ips.ftype = "H"
                ips.alpha=alpha_soc
                return true
            end
        end

        theta_soc>ips.opt.kappa_soc*theta_soc_old && break
        theta_soc_old = theta_soc
    end
    @trace(ips.logger,"Second-order correction terminated.")

    return false
end


# Index types

"""
    ConstraintIndex{F, S}

A type-safe wrapper for `Int64` for use in referencing `F`-in-`S` constraints in
a model.
The parameter `F` is the type of the function in the constraint, and the
parameter `S` is the type of set in the constraint. To allow for deletion,
indices need not be consecutive. Indices within a constraint type (i.e. `F`-in-`S`)
must be unique, but non-unique indices across different constraint types are allowed.
If `F` is [`SingleVariable`](@ref) then the index is equal to the index of the
variable. That is for an `index::ConstraintIndex{SingleVariable}`, we always
have
```julia
index.value == MOI.get(model, MOI.ConstraintFunction(), index).variable.value
```
"""
struct ConstraintIndex{F, S}
    value::Int64
end

"""
    VariableIndex

A type-safe wrapper for `Int64` for use in referencing variables in a model.
To allow for deletion, indices need not be consecutive.
"""
struct VariableIndex
    value::Int64
end

# The default hash is slow. It's important for the performance of dictionaries
# of VariableIndices to define our own.
# https://github.com/JuliaLang/julia/issues/10208
Base.hash(v::VariableIndex, h::UInt) = hash(v.value, h)

# No need to define isequal because the default matches our implementation of
# hash.

const Index = Union{ConstraintIndex,VariableIndex}

"""
    struct InvalidIndex{IndexType<:Index} <: Exception
        index::IndexType
    end

An error indicating that the index `index` is invalid.
"""
struct InvalidIndex{IndexType<:Index} <: Exception
    index::IndexType
end

function Base.showerror(io::IO, err::InvalidIndex)
    print("The index $(err.index) is invalid. Note that an index becomes invalid after it has been deleted.")
end

"""
    is_valid(model::ModelLike, index::Index)::Bool

Return a `Bool` indicating whether this index refers to a valid object in the model `model`.
"""
is_valid(model::ModelLike, ref::Index) = false

"""
    throw_if_not_valid(model::ModelLike, index::Index)

Throw an `InvalidIndex(index)` error if `MOI.is_valid(model, index)` returns
`false`.
"""
function throw_if_not_valid(model::ModelLike, index::Index)
    if !is_valid(model, index)
        throw(InvalidIndex(index))
    end
end

"""
    struct DeleteNotAllowed{IndexType <: Index} <: NotAllowedError
        index::IndexType
        message::String
    end

An error indicating that the index `index` cannot be deleted.
"""
struct DeleteNotAllowed{IndexType <: Index} <: NotAllowedError
    index::IndexType
    message::String
end
DeleteNotAllowed(index::Index) = DeleteNotAllowed(index, "")

function operation_name(err::DeleteNotAllowed)
    return "Deleting the index $(err.index)"
end

"""
    delete(model::ModelLike, index::Index)

Delete the referenced object from the model. Throw [`DeleteNotAllowed`](@ref) if
if `index` cannot be deleted.

The following modifications also take effect if `Index` is [`VariableIndex`](@ref):
* If `index` used in the objective function, it is removed from the function,
  i.e., it is substituted for zero.
* For each `func`-in-`set` constraint of the model:
  - If `func isa SingleVariable` and `func.variable == index` then the
    constraint is deleted.
  - If `func isa VectorOfVariables` and `index in func.variable` then
    * if `length(func.variable) == 1` is one, the constraint is deleted;
    * if `length(func.variable) > 1` and `supports_dimension_update(set)` then
      then the variable is removed from `func` and `set` is replaced by
      `update_dimension(set, MOI.dimension(set) - 1)`.
    * Otherwise, a [`DeleteNotAllowed`](@ref) error is thrown.
  - Otherwise, the variable is removed from `func`, i.e., it is substituted for
    zero.
"""
delete(model::ModelLike, index::Index) = throw(DeleteNotAllowed(index))

"""
    delete{R}(model::ModelLike, indices::Vector{R<:Index})

Delete the referenced objects in the vector `indices` from the model.
It may be assumed that `R` is a concrete type.
"""
function delete(model::ModelLike, indices::Vector{<:Index})
    for index in indices
        delete(model, index)
    end
end


module nanoJulia
using Statistics, FASTX, DataFrames, Printf, Formatting, BioAlignments, XAM, Plots, HDF5, CSV, BioSequences
export nanoread, generateStatSummary, plotReadLen2QualScatter,
       plotReadLen2QualHistogram2D, readFast5, readFastq, readBAM,
       plotReadQual2IdentScatter, plotReadQual2IdentHistogram2D,
       plotReadLenDist, plotSquiggle, readSeqSummary, calculate_GC_content

include("datatype.jl")
include("utilities.jl")
include("record_handler.jl")
include("nanoread.jl")
include("info_handler.jl")
include("statistics_handler.jl")

"""
    readFast5(f5file, basecall_group)

Read and parse a fast5 file to extract basecalled fastq information.
The extracted fastq info will be parsed, then the required information
of each read will be extracted, calculated, and exported as a data frame.
The `basecall_group` could be determined simply by using `h5dump` on a
fast5 file. For example, you can type `h5dump -n single_read.fast5` in
terminal, and the content of this fast5 will be shown like this:
```
HDF5 "fff07ce7-f378-47ce-a71a-488d9a1a9af8.fast5" {
FILE_CONTENTS {
 group      /
 group      /Analyses
 group      /Analyses/Basecall_1D_000
 group      /Analyses/Basecall_1D_000/Summary
 group      /Analyses/Basecall_1D_000/Summary/basecall_1d_template
 group      /Analyses/Basecall_1D_001
 group      /Analyses/Basecall_1D_001/BaseCalled_template
 dataset    /Analyses/Basecall_1D_001/BaseCalled_template/Fastq
 dataset    /Analyses/Basecall_1D_001/BaseCalled_template/Move
 dataset    /Analyses/Basecall_1D_001/BaseCalled_template/Trace
 group      /Analyses/Basecall_1D_001/Summary
 group      /Analyses/Basecall_1D_001/Summary/basecall_1d_template
 group      /Analyses/Segmentation_000
 group      /Analyses/Segmentation_000/Summary
 group      /Analyses/Segmentation_000/Summary/segmentation
 group      /Analyses/Segmentation_001
 group      /Analyses/Segmentation_001/Summary
 group      /Analyses/Segmentation_001/Summary/segmentation
 group      /Raw
 group      /Raw/Reads
 group      /Raw/Reads/Read_1950
 dataset    /Raw/Reads/Read_1950/Signal
 group      /UniqueGlobalKey
 group      /UniqueGlobalKey/channel_id
 group      /UniqueGlobalKey/context_tags
 group      /UniqueGlobalKey/tracking_id
 }
}
```
You can observe this output, and notice that `Fastq` locates in the directory
`/Analyses/Basecall_1D_001/BaseCalled_template`. Therefore, the `basecall_group`
sould be set as `Basecall_1D_001` not `Basecall_1D_000`.

# Examples:
1. Read a single-read fast5.
```
julia> readFast5("fff07ce7-f378-47ce-a71a-488d9a1a9af8.fast5", "Basecall_1D_001")
1×3 DataFrame
 Row │ quality  length  gc_content
     │ Float64  Int64   Float64
─────┼─────────────────────────────
   1 │ 5.05611    1326      0.4759
```

2. Read a multi-reads fast5.
julia> readFast5("FAM93744_fail_6a4bbe82_1.fast5", "Basecall_1D_000")
4000×3 DataFrame
  Row │ quality  length  gc_content
      │ Float64  Int64   Float64
──────┼─────────────────────────────
    1 │     3.7     255      0.4314
    2 │     4.5     331      0.3897
  ⋮   │    ⋮       ⋮         ⋮
 3999 │     6.6     849      0.3604
 4000 │     4.2     164      0.5915
                   3996 rows omitted
"""
function readFast5(fast5file::String, basecall_group::String)::DataFrames.DataFrame
	h5open(fast5file) do f5
        file_version = read(attrs(f5), "file_version")
        extracted_info = nanoread(f5, file_version, basecall_group)
        return extracted_info
	end
end

"""
    readFastq(fastqfile)

Read and parse a fastq file to extract and calculate read length, read quality,
and gc-content, and finally export those information as a data frame.

# Example:

```
julia> readFastq("FAO34954_pass_b9e3d78f_0.fastq")
4000×3 DataFrame
  Row │ quality  length  gc_content
      │ Float64  Int64   Float64
──────┼─────────────────────────────
    1 │     7.5    1571      0.578
    2 │     9.8    1408      0.571
    3 │    10.5     777      0.4041
    4 │     8.6     301      0.4983
    5 │    11.5     318      0.3962
    6 │    10.6     509      0.5108
  ⋮   │    ⋮       ⋮         ⋮
 3996 │    10.6     616      0.5747
 3997 │    11.7     837      0.5388
 3998 │     8.0     403      0.4367
 3999 │    10.7     481      0.5717
 4000 │     7.8     223      0.4888
                   3989 rows omitted
```
"""
function readFastq(fastqfile::String)::DataFrames.DataFrame
    open(FASTQ.Reader, fastqfile) do fastq
        extracted_info = nanoread(fastq)
        return extracted_info
    end
end

"""
    readBAM(bamfile)

Read and parse a sorted and indexed BAM file to extract and calculate read length,
read quality, alignment identity and gc-content, and finally export those information
as a data frame.

# Example:

```
julia> readBAM("map_srt.bam")
5839×4 DataFrame
  Row │ quality  length  identity  gc_content
      │ Float64  Int64   Float64   Float64
──────┼───────────────────────────────────────
    1 │    12.9     119      94.5      0.4034
    2 │    16.1     120      96.7      0.4
    3 │    15.3     145      95.2      0.3793
    4 │    10.7     153      92.1      0.4183
    5 │     9.0     146      93.0      0.4247
    6 │    12.2     147      90.2      0.415
  ⋮   │    ⋮       ⋮        ⋮          ⋮
 5835 │     9.1     296      89.6      0.3716
 5836 │    12.1     283      94.7      0.3958
 5837 │    10.3     341      94.0      0.3871
 5838 │    11.0     244      93.5      0.4016
 5839 │    12.6     157      96.8      0.4076
                             5828 rows omitted
```
"""
function readBAM(bamfile::String)::DataFrames.DataFrame
    open(BAM.Reader, bamfile) do bam
        extracted_info = nanoread(bam)
        return extracted_info
    end
end

"""
    readSeqSummary(sequencing_summary_file)

Read and parse a sequencing summary file to extract and calculate read length and quality, and finally export those information as a data frame.

# Example:

```
julia> readSeqSummary("sequencing_summary_FAO34954_b9e3d78f.txt")
176498×2 DataFrame
    Row │ quality  length
        │ Float64  Int64
────────┼─────────────────
      1 │     7.2    1571
      2 │     9.3    1408
      3 │    10.1     777
      4 │     8.4     301
      5 │    11.0     318
      6 │    10.3     509
   ⋮    │    ⋮       ⋮
 176494 │    10.1     667
       176483 rows omitted
```
"""
function readSeqSummary(seqsummaryfile::String)::DataFrames.DataFrame
    extracted_info = CSV.File(seqsummaryfile) |> DataFrame |> nanoread
    return extracted_info
end

end


module IndirectArrays

export IndirectArray

"""
    IndirectArray(index, values)

creates an array `A` where the values are looked up in the value table,
`values`, using the `index`.  Concretely, `A[i...] =
values[index[i...]]`.

`values` can be an `AbstractVector` (useful when the values in `index` are
contiguous) or an `AbstractDict` (useful when they are not).
When there are not very many such distinct values, "frozen" LittleDicts from
[OrderedCollections](https://github.com/JuliaCollections/OrderedCollections.jl)
are recommended for performance.
"""
struct IndirectArray{T,N,I,A<:AbstractArray{I,N},V<:Union{AbstractVector{T},AbstractDict{I,T}}} <: AbstractArray{T,N}
    index::A
    values::V

    @inline function IndirectArray{T,N,I,A,V}(index, values) where {T,N,I,A,V}
        # The typical logic for testing bounds and then using
        # @inbounds will not check whether index is inbounds for
        # values. So we had better check this on construction.
        if isa(values, AbstractVector)
            I <: Integer || error("with a vector `values`, the index must have integer eltype")
            @boundscheck checkbounds(values, index)
        else
            @boundscheck begin
                uindex = unique(index)
                all(idx -> haskey(values, idx), index) || Base.throw_boundserror(index, values)
            end
        end
        new{T,N,I,A,V}(index, values)
    end
end
const IndirectArrayVec{T,N,I,A,V<:AbstractVector} = IndirectArray{T,N,I,A,V}

Base.@propagate_inbounds IndirectArray(index::AbstractArray{<:Integer,N}, values::AbstractVector{T}) where {T,N} =
    IndirectArray{T,N,eltype(index),typeof(index),typeof(values)}(index, values)
Base.@propagate_inbounds IndirectArray(index::AbstractArray{I,N}, values::AbstractDict{I,T}) where {I,T,N} =
    IndirectArray{T,N,I,typeof(index),typeof(values)}(index, values)

function IndirectArray{T}(A::AbstractArray) where {T}
    values = unique(A)
    index = convert(Array{T}, indexin(A, values))
    return IndirectArray(index, values)
end
IndirectArray(A::AbstractArray) = IndirectArray{UInt8}(A)

Base.size(A::IndirectArray) = size(A.index)
Base.axes(A::IndirectArray) = axes(A.index)
Base.IndexStyle(::Type{IndirectArray{T,N,I,A,V}}) where {T,N,I,A,V} = IndexStyle(A)

function Base.copy(A::IndirectArray)
    @inbounds ret = IndirectArray(copy(A.index), copy(A.values))
    return ret
end

@inline function Base.getindex(A::IndirectArray, i::Int)
    @boundscheck checkbounds(A.index, i)
    @inbounds idx = A.index[i]
    @inbounds ret = A.values[idx]
    ret
end

@inline function Base.getindex(A::IndirectArray{T,N}, I::Vararg{Int,N}) where {T,N}
    @boundscheck checkbounds(A.index, I...)
    @inbounds idx = A.index[I...]
    @inbounds ret = A.values[idx]
    ret
end

@inline function Base.setindex!(A::IndirectArrayVec, x, i::Int)
    @boundscheck checkbounds(A.index, i)
    idx = findfirst(isequal(x), A.values)
    if idx === nothing
        push!(A.values, x)
        A.index[i] = length(A.values)
    else
        A.index[i] = idx
    end
    return A
end

@inline function Base.push!(A::IndirectArrayVec{T,1} where T, x)
    idx = findfirst(isequal(x), A.values)
    if idx === nothing
        push!(A.values, x)
        push!(A.index, length(A.values))
    else
        push!(A.index, idx)
    end
    return A
end

function Base.append!(A::IndirectArrayVec{T,1}, B::IndirectArray{T,1}) where T
    if A.values == B.values
        append!(A.index, B.index)
    else # pretty inefficient but let's get something going
        for b in B
            push!(A, b)
        end
    end
    return A
end

function Base.append!(A::IndirectArrayVec{<:Any,1}, B::AbstractVector)
    for b in B
        push!(A, b)
    end
    return A
end

end # module