function run_investor_iteration(investor::Investor,
                                 active_projects::Vector{Project},
                                 iteration_year::Int64,
                                 yearly_horizon::Int64,
                                 simulation_years::Int64,
                                 start_year::Int64,
                                 capacity_forward_years::Int64,
                                 sys_data_dir::String,
                                 sys_UC::Union{Nothing, PSY.System},
                                 services::Vector{PSY.Service},
                                 solver::JuMP.MOI.OptimizerWithAttributes
                            )

    investor_dir = get_data_dir(investor)
    projects = get_projects(investor)

    active_projects_copy = deepcopy(active_projects)

    market_names = get_markets(investor)

    # Create empty market prices struct
    market_prices = MarketPrices()

    option_projects = get_options(investor)
    max_new_options = Dict(get_name(project) => 0 for project in option_projects)

    scenarios = get_scenario_data(get_forecast(investor))

    for scenario in scenarios
        scenario_name = get_name(scenario)
        output_file = joinpath(investor_dir, "expected_market_data", "$(scenario_name)_year_$(iteration_year).jld2")
        expected_data = FileIO.load(output_file)

        set_energy_price!(market_prices, scenario_name, expected_data["energy_price"])

        if in(:ReserveUp, market_names)
            set_reserveup_price!(market_prices, scenario_name, expected_data["reserve_up_price"])
        end

        if in(:ReserveDown, market_names)
            set_reservedown_price!(market_prices, scenario_name, expected_data["reserve_down_price"])
        end

        if in(:Capacity, market_names)
            set_capacity_price!(market_prices, scenario_name, expected_data["capacity_price"])
        end

        if in(:REC, market_names)
            set_rec_price!(market_prices, scenario_name, expected_data["rec_price"])
        end

        for project in projects
            if in(get_name(project), get_name.(active_projects_copy))
                update_capacity_factors!(project, scenario_name, expected_data["capacity_factors"])
            end
            if in(get_name(project), get_name.(active_projects_copy))
                update_capacity_accepted_perc!(project, scenario_name, expected_data["capacity_accepted_perc"])
            end

        end

         max_new_options = update_max_new_options!(max_new_options, expected_data["new_options"], option_projects)
    end

    set_market_prices!(investor, market_prices)

    retire_unprofitable!(investor,
                         sys_UC,
                         sys_data_dir,
                         iteration_year,
                         yearly_horizon,
                         simulation_years,
                         capacity_forward_years,
                         solver)

    make_investments!(investor,
                      max_new_options,
                      iteration_year,
                      yearly_horizon,
                      simulation_years,
                      capacity_forward_years,
                      solver)

    for (i, project) in enumerate(projects)
        start_construction!(projects,
                            i,
                            project,
                            iteration_year)

        finish_construction!(projects,
                            i,
                            project,
                            sys_UC,
                            services,
                            sys_data_dir,
                            iteration_year,
                            start_year)

        update_lifecycle!(project,
                          iteration_year,
                          simulation_years)
    end

    return
end


# Kmer Iterator
# =============
#
# Iterator over all k-mers in a biological sequence.
#
# This file is a part of BioJulia.
# License is MIT: https://github.com/BioJulia/Bio.jl/blob/master/LICENSE.md

# Iterate through every k-mer in a nucleotide sequence
immutable EachKmerIterator{T,K,S<:Sequence}
    seq::S
    step::Int
end

"""
    each(::Type{Kmer{T,k}}, seq::Sequence[, step=1])

Initialize an iterator over all k-mers in a sequence `seq` skipping ambiguous
nucleotides without changing the reading frame.

# Arguments
* `Kmer{T,k}`: k-mer type to enumerate.
* `seq`: a nucleotide sequence.
* `step=1`: the number of positions between iterated k-mers

# Examples
```
# iterate over DNA codons
for (pos, codon) in each(DNAKmer{3}, dna"ATCCTANAGNTACT", 3)
    @show pos, codon
end
```
"""
function each{T,K}(::Type{Kmer{T,K}}, seq::Sequence, step::Integer=1)
    if eltype(seq) ∉ (DNA, RNA)
        throw(ArgumentError("element type must be either DNA or RNA nucleotide"))
    elseif !(0 ≤ K ≤ 32)
        throw(ArgumentError("k-mer length must be between 0 and 32"))
    elseif step < 1
        throw(ArgumentError("step size must be positive"))
    end
    return EachKmerIterator{T,K,typeof(seq)}(seq, step)
end

eachkmer{A<:DNAAlphabet}(seq::BioSequence{A}, K::Integer, step::Integer=1) = each(DNAKmer{Int(K)}, seq, step)
eachkmer{A<:RNAAlphabet}(seq::BioSequence{A}, K::Integer, step::Integer=1) = each(RNAKmer{Int(K)}, seq, step)
eachkmer(seq::ReferenceSequence, K::Integer, step::Integer=1) = each(DNAKmer{Int(K)}, seq, step)

Base.eltype{T,k,S}(::Type{EachKmerIterator{T,k,S}}) = Tuple{Int,Kmer{T,k}}

function Base.iteratorsize{T,k,S}(::Type{EachKmerIterator{T,k,S}})
    return Base.SizeUnknown()
end

@inline function Base.start{T,K}(it::EachKmerIterator{T,K})
    nextn = find_next_ambiguous(it.seq, first(it.seq.part))
    pair = Nullable{Tuple{Int,Kmer{T,K}}}()
    pair, nextn = nextkmer(Kmer{T,K}, it.seq, 1, it.step, nextn, pair)
    return pair, nextn
end

@inline function Base.done{T,K}(::EachKmerIterator{T,K}, state)
    return isnull(state[1])
end

@inline function Base.next{T,K}(it::EachKmerIterator{T,K}, state)
    pair, nextn = state
    from, kmer = get(pair)
    return (from, kmer), nextkmer(Kmer{T,K}, it.seq, from + it.step, it.step, nextn, pair)
end

@inline function nextkmer{T,K}(::Type{Kmer{T,K}}, seq, i, step, nextn, kmer)
    # find a position from which we can extract at least K unambiguous nucleotides
    from = i
    while nextn > 0 && nextn - from < K
        from = nextn + 1
        # align `from` since it must be a multiple of `step`
        r = rem(from - 1, step)
        if r > 0
            from += step - r
        end
        nextn = find_next_ambiguous(seq, from)
    end
    if endof(seq) - from + 1 < K
        # no available kmer
        return Nullable{Tuple{Int,Kmer{T,K}}}(), nextn
    end

    newkmer = extract_kmer(Kmer{T,K}, seq, from, kmer)

    # update `nextn` for the next iteration if needed
    if nextn > 0 && nextn < from + step
        nextn = find_next_ambiguous(seq, from)
    end

    return Nullable((from, newkmer)), nextn
end

@inline function extract_kmer{K,T}(::Type{Kmer{T,K}}, seq, from, pair)
    if isnull(pair) || get(pair)[1] + K - 1 < from
        # the last kmer doesn't overlap the extracting one
        x = UInt64(0)
        for k in 1:K
            nt = inbounds_getindex(seq, from + k - 1)
            x = x << 2 | trailing_zeros(nt)
        end
    else
        pos, kmer = get(pair)
        # |<-K->|
        # -------
        #     -------
        # ^   ^
        # pos from
        n = from - pos
        from += K - n
        x = UInt64(kmer)
        for k in 1:n
            nt = inbounds_getindex(seq, from + k - 1)
            x = x << 2 | trailing_zeros(nt)
        end
    end
    return Kmer{T,K}(x)
end


## S2MktInt -----------------------------------------------------
function S2MktInt(ds2_mkt_int::Dict{Symbol, Any})
  shock_object = :CapMkt
  shock = ds2_mkt_int[:shock]
  spot_up_abs_min = ds2_mkt_int[:spot_up_abs_min]
  balance = DataFrame()
  scr = zeros(Float64, 2)
  scen_up = false
  return S2MktInt(shock_object,
                  shock,
                  spot_up_abs_min, balance, scr, scen_up)
end

function S2MktInt(param::ProjParam,
                  s2_balance::DataFrame,
                  ds2_mkt_int::Dict{Symbol, Any})
  p = deepcopy(param)
  mkt_int = S2MktInt(ds2_mkt_int)
  mkt_int.balance = deepcopy(s2_balance)
  for 𝑖𝑛𝑡_𝑡𝑦𝑝𝑒_𝑠𝑦𝑚𝑏 ∈ collect(keys(mkt_int.shock))
    append!(mkt_int.balance,
            s2bal(p, mkt_int,
                  (inv, s2_int) ->
                  mktintshock!(inv, s2_int, 𝑖𝑛𝑡_𝑡𝑦𝑝𝑒_𝑠𝑦𝑚𝑏),
                  𝑖𝑛𝑡_𝑡𝑦𝑝𝑒_𝑠𝑦𝑚𝑏))
  end
  scr!(mkt_int)
  return mkt_int
end

## S2MktEq ------------------------------------------------------
function S2MktEq(ds2_mkt_eq::Dict{Symbol, Any})
  shock_object = :CapMkt_AdjVal0
  eq_type = Dict{Symbol, Symbol}()
  balance = DataFrame()
  corr = ds2_mkt_eq[:corr]
  shock = ds2_mkt_eq[:shock]
  scr = zeros(Float64, 2)
  return S2MktEq(shock_object, eq_type, shock,
                 balance, corr, scr)
end

function S2MktEq(param::ProjParam,
                 s2_balance::DataFrame,
                 ds2_mkt_eq::Dict{Symbol, Any},
                 eq2type)
  p = deepcopy(param)
  mkt_eq = S2MktEq(ds2_mkt_eq)
  merge!(mkt_eq.eq2type, eq2type)
  mkt_eq.balance = deepcopy(s2_balance)
  for 𝑠𝑦𝑚𝑏 ∈ collect(keys(mkt_eq.shock))
    append!(mkt_eq.balance,
            s2bal(p, mkt_eq,
                  (invs, s2_eq) ->
                  mkteqshock!(invs, s2_eq, 𝑠𝑦𝑚𝑏),
                  𝑠𝑦𝑚𝑏))
  end
  scr!(mkt_eq)
  return mkt_eq
end

## S2Mkt --------------------------------------------------------
function S2Mkt(ds2_mkt::Dict{Symbol, Any})
  mds = Array{S2Module}(undef, 0)
  corr_up = ds2_mkt[:corr](ds2_mkt[:raw], ds2_mkt[:adj], :up)
  corr_down = ds2_mkt[:corr](ds2_mkt[:raw], ds2_mkt[:adj], :down)
  scr = zeros(Float64, 2)
  return S2Mkt(mds, corr_up, corr_down, scr)
end

function S2Mkt(param::ProjParam,
               s2_balance::DataFrame,
               eq2type::Dict,
               ds2_mkt_all::Dict)
  p = deepcopy(param)
  mkt = S2Mkt(ds2_mkt_all[:mkt])
  push!(mkt.mds, S2MktInt(p, s2_balance,
                 ds2_mkt_all[:mkt_int]))
  push!(mkt.mds, S2MktEq(p, s2_balance,
                 ds2_mkt_all[:mkt_eq], eq2type))
  push!(mkt.mds, S2MktProp(p, s2_balance))
  push!(mkt.mds, S2MktSpread(p, s2_balance))
  push!(mkt.mds, S2MktFx(p, s2_balance))
  push!(mkt.mds, S2MktConc(p, s2_balance))
  scen_up = false
  for 𝑖 ∈ 1:length(mkt.mds)
    if :scen_up ∈ fieldnames(typeof(mkt.mds[𝑖]))
      scen_up = mkt.mds[𝑖].scen_up
    end
  end
  corr = (scen_up ? mkt.corr_up : mkt.corr_down)
  mkt.scr = scr(mkt, corr)
  return mkt
end

## S2Def1 -------------------------------------------------------
function S2Def1(ds2_def_1)
  tlgd = Array{Float64}(undef, 0)
  slgd = Array{Float64}(undef, 0)
  u = Array{Float64}(undef, 0,0)
  v = Array{Float64}(undef, 0)
  scr_par = Dict{Symbol, Vector{Float64}}()
  for 𝑖 ∈ 1:nrow(ds2_def_1[:scr_par])
    merge!(scr_par,
           Dict(ds2_def_1[:scr_par][𝑖, :range] =>
            [ds2_def_1[:scr_par][𝑖, :threshold_upper],
             ds2_def_1[:scr_par][𝑖, :multiplier]]))
  end
  scr = zeros(Float64, 2)
  return S2Def1(tlgd, slgd, u, v, scr_par, scr)
end

function S2Def1(param::ProjParam,
                ds2_def_1::Dict{Symbol, Any})
  p = deepcopy(param)
  def = S2Def1(ds2_def_1)
  cqs_vec = filter(x -> occursin("cqs", string(x)),
                   names(ds2_def_1[:prob]))
  prob = [ds2_def_1[:prob][1, 𝑐𝑞𝑠] for 𝑐𝑞𝑠 ∈ cqs_vec]
  def.tlgd = zeros(Float64, length(cqs_vec))
  def.slgd = zeros(Float64, length(cqs_vec))
  def.u = Array{Float64}(undef, length(cqs_vec), length(cqs_vec))
  def.v = Array{Float64}(undef, length(cqs_vec))

  def.v = 1.5 * prob .* (1 .- prob) ./ (2.5 .- prob)
  for 𝑖 ∈ 1:size(def.u,1), 𝑗 ∈ 1:1:size(def.u,2)
    def.u[𝑖,𝑗] =
      (1-prob[𝑖]) * prob[𝑖] * (1-prob[𝑗]) * prob[𝑗] /
      (1.25 * (prob[𝑖] + prob[𝑗]) - prob[𝑖] * prob[𝑗])
  end
  invs = InvPort(p.t_0, p.T, p.cap_mkt, p.invs_par...)
  for 𝑖 = 1:length(invs.igs[:IGCash].investments)
    j = indexin([invs.igs[:IGCash].investments[𝑖].cqs],
          Symbol.(cqs_vec))[1]
    lgd =
      invs.igs[:IGCash].investments[𝑖].lgd *
      invs.igs[:IGCash].investments[𝑖].mv_0
    def.tlgd[j] += lgd
    def.slgd[j] += lgd * lgd
  end
  scr!(def)
  return def
end

## S2Def --------------------------------------------------------
function S2Def(ds2_def)
  mds = Array{S2Module}(undef, 0)
  corr = ds2_def[:corr]
  scr = zeros(Float64, 2)
  return S2Def(mds, corr, scr)
end

function S2Def(param::ProjParam,
               s2_balance::DataFrame,
               ds2_def_all::Dict)
  p = deepcopy(param)
  def = S2Def(ds2_def_all[:def])
  push!(def.mds, S2Def1(p, ds2_def_all[:def_1]))
  push!(def.mds, S2Def2(p, s2_balance))
  def.scr = scr(def, def.corr)
  return def
end

## S2LifeBio (mortality risk, longevity risk, lapse risk---------
function S2LifeBio(ds2_bio::Dict{Symbol, Any})
  shock_object = :LiabIns
  if :shock_param in collect(keys(ds2_bio))
    shock_param = ds2_bio[:shock_param]
  else
    shock_param = Dict()
  end
  shock = ds2_bio[:shock]
  balance = DataFrame()
  mp_select = Dict{Symbol, Vector{Bool}}()
  scr = zeros(Float64, 2)
  return S2LifeBio(shock_object, shock,
                   shock_param,
                   balance, mp_select, scr)
end

function S2LifeBio(param::ProjParam,
                   s2_balance::DataFrame,
                   ds2_bio::Dict{Symbol, Any})
  p = deepcopy(param)
  bio = S2LifeBio(ds2_bio)
  bio.balance = deepcopy(s2_balance)
  selectmort!(p, bio)
  for 𝑠𝑦𝑚𝑏 ∈ collect(keys(bio.shock))
    append!(bio.balance,
            s2bal(p, bio,
                  (l_ins, wx) -> bioshock!(l_ins, wx, 𝑠𝑦𝑚𝑏),
                  𝑠𝑦𝑚𝑏))
  end
  scr!(bio)
  return bio
end

## S2LifeCost ---------------------------------------------------
function S2LifeCost(ds2_cost::Dict{Symbol, Any})
  shock_object = :InvPort_LiabIns
  shock = ds2_cost[:shock]
  if :shock_param in collect(keys(ds2_cost))
    shock_param = ds2_cost[:shock_param]
  else
    shock_param = Dict()
  end
  balance = DataFrame()
  scr = zeros(Float64, 2)
  return S2LifeCost(shock_object, shock, shock_param,
                    balance, scr)
end

function S2LifeCost(param::ProjParam,
                    s2_balance::DataFrame,
                    ds2_cost::Dict{Symbol, Any})
  p = deepcopy(param)
  cost = S2LifeCost(ds2_cost)
  cost.balance = deepcopy(s2_balance)
  for 𝑠𝑦𝑚𝑏 ∈ collect(keys(cost.shock))
    append!(cost.balance,
            s2bal(p, cost, (invs, l_ins, cst) ->
                  costshock!(invs, l_ins, cst), 𝑠𝑦𝑚𝑏))
  end
  scr!(cost)
  return cost
end

## S2Life -------------------------------------------------------
function S2Life(ds2_life::Dict{Symbol, Any})
  mds = Array{S2Module}(undef, 0)
  corr = ds2_life[:corr]
  scr = zeros(Float64, 2)
  return S2Life(mds, corr, scr)
end

function S2Life(param::ProjParam,
                s2_balance::DataFrame,
                d_life::Dict)
  p = deepcopy(param)
  life = S2Life(d_life[:life])
  push!(life.mds, S2LifeBio(p, s2_balance, d_life[:life_qx]))
  push!(life.mds, S2LifeBio(p, s2_balance, d_life[:life_px]))
  push!(life.mds, S2LifeMorb(p, s2_balance))
  push!(life.mds, S2LifeBio(p, s2_balance, d_life[:life_sx]))
  push!(life.mds, S2LifeCost(p, s2_balance, d_life[:life_cost]))
  push!(life.mds, S2LifeRevision(p, s2_balance))
  push!(life.mds, S2LifeBio(p, s2_balance, d_life[:life_cat]))
  life.scr = scr(life, life.corr)
  return life
end

## S2Op ---------------------------------------------------------
S2Op(ds2_op::Dict{Symbol, Float64}, s2_op::Dict) =
  S2Op(ds2_op,
       s2_op[:prem_earned],
       s2_op[:prem_earned_prev],
       s2_op[:tp], 0, 0, s2_op[:cost_ul], 0.0)

## S2 -----------------------------------------------------------
function   S2(ds2_op, s2_op)
  mds = Array{S2Module}(undef, 0)
  balance = DataFrame()
  corr = zeros(Float64, 5, 5)
  bscr = zeros(Float64, 2)
  adj_tp = 0.0
  adj_dt = 0.0
  op = S2Op(ds2_op, s2_op)
  scr = 0.0
  S2(mds, balance, corr, bscr,
     adj_tp, adj_dt, op, scr, 0, 0, 0, 0, 0)
end

function  S2(param::ProjParam,
             eq2type::Dict{Symbol, Symbol},
             ds2_mkt_all::Dict,
             ds2_def_all::Dict,
             ds2_life_all::Dict,
             s2_op::Dict,
             ds2_op::Dict{Symbol, Float64},
             ds2::Dict{Symbol, Any})
  p = deepcopy(param)
  s2 = S2(ds2_op, s2_op)
  s2.corr = ds2[:corr]
  s2.coc = ds2[:coc]
  s2.balance = s2bal(p)
  s2.op.tp = s2.balance[1, :tpg] + s2.balance[1, :bonus]
  push!(s2.mds, S2Mkt(p, s2.balance, eq2type, ds2_mkt_all))
  push!(s2.mds, S2Def(p, s2.balance, ds2_def_all))
  push!(s2.mds, S2Life(p, s2.balance, ds2_life_all))
  push!(s2.mds, S2Health(p, s2.balance))
  push!(s2.mds, S2NonLife(p, s2.balance))
  scr!(s2, p.tax_credit_0)
  s2.liabs_mod = s2.balance[1,:tpg] +
                 s2.balance[1,:bonus] +
                 s2.balance[1,:cost_prov]
  s2.invest_mod =  s2.balance[1,:invest]
  s2.risk_margin = s2riskmargin(p, s2.scr, s2.coc)
  s2.scr_ratio =
    (s2.invest_mod - s2.liabs_mod - s2.risk_margin)/ s2.scr
  return s2
end


# SPD-License-Identifier: MIT

using ArgParse
using JLD2
using MarkovModels
using TOML

function parse_commandline()
    s = ArgParseSettings()
    @add_arg_table s begin
        "topology"
            required = true
            help = "hmm topology in TOML format"
        "units"
            required = true
            help = "list of units with their categories"
        "hmms"
            required = true
            help = "output hmms in BSON format"
    end
    s.description = """
    Build a set of HMM. The topology file should be formatted as:

    states = [

    \ua0\ua0{ id = 1, initweight = 1.0, finalweight = 0.0 },

    \ua0\ua0...

    ]

    links = [

    \ua0\ua0{ src = 1, dest = 2, weight = 0.5 },

    \ua0\ua0...

    ]

    tiestates = false | true

    """
    parse_args(s)
end

function loadunits(file)
    units, categories = [], []
    open(file, "r") do f
        for line in eachline(f)
            tokens = split(line)
            push!(units, tokens[1])
            push!(categories, tokens[2:end])
        end
    end
    units, categories
end

function get_unit_topo(topo, category)
    i = 1
    while i <= length(category) && category[i] ∈ keys(topo)
        topo = topo[category[i]]
        i += 1
    end
    topo
end

function makehmm!(pdfid_mapping, unit, topo, pdfid)
    SF = LogSemifield{Float32}
    fsm = VectorFSM{SF}()

    states = Dict()
    for (i, state) in enumerate(topo["states"])
        initweight = SF(log(state["initweight"]))
        finalweight = SF(log(state["finalweight"]))
        s = addstate!(fsm, i; initweight, finalweight)
        states[i] = s
        pdfid_mapping[(unit, i)] = pdfid
        pdfid += 1
    end

    for arc in topo["arcs"]
        addarc!(fsm, states[arc["src"]], states[arc["dest"]],
                SF(log(arc["weight"])))
    end

    fsm |> renormalize, pdfid
end

function main(args)
    topo = TOML.parsefile(args["topology"])

    units, categories = loadunits(args["units"])
    hmms = Dict()
    pdfid_mapping = Dict()
    pdfid = 1
    for (unit, category) in zip(units, categories)
        unit_topo = get_unit_topo(topo, category)
        hmms[unit], pdfid = makehmm!(pdfid_mapping, unit, topo, pdfid)
    end

    data = Dict(
        "units" => hmms,
        "pdfid_mapping" => pdfid_mapping
    )
    save(args["hmms"], data)
end

args = parse_commandline()
main(args)


include("evaluate_nh/eval.jl")

# optimize with simulated annealing
function siman!(
    ata_model::AbstractModel;
    starting_design = Matrix{Float64}(undef, 0, 0),
    results_folder = "results",
    start_temp = 0.1,
    geom_temp = 0.1,
    max_time = 1000.0,
    max_conv = 2,
    feas_nh = 0,
    opt_nh = 5,
    n_item_sample = 1000,
    n_test_sample = 2,
    opt_feas = 0.0,
    n_fill = 1,
    verbosity = 2,
    kwargs...,
)
    if !isdir(results_folder)
        mkdir(results_folder)
    else
        success!(
            ata_model,
            string(
                "There is already a folder with this name, files in ",
                results_folder,
                " will be overwritten.",
            ),
        )
    end

    ##
    nNH = feas_nh + opt_nh
    opt_nh = feas_nh + 1
    if feas_nh == 0
        fF = false
    else
        fF = true
    end
    T = ata_model.settings.T
    n_fs = ata_model.settings.n_fs
    n_items = ata_model.settings.n_items
    fs_counts = ata_model.settings.fs.counts * ones(Float64, T)'
    iu_max⁺ = 0
    iu_min⁺ = 0
    start_time = copy(time())
    nacc = 0 # total accepted trials
    t = copy(start_temp) # temperature - will initially rise or fall to cover parameter space. Then it will fall
    finish = 0 # convergence indicator 0 (failure), 1 (normal success), or 2 (convergence but near bounds)
    f_evals = 0
    hline = " = "^80
    f_star = typemax(Float64) * ones(2)
    NH = 1

    # starting design check
    if size(starting_design, 1) > 0
        if (
            size(starting_design, 1) != n_fs ||
            size(starting_design, 2) != ata_model.settings.T
        )
            error!(ata_model, "Starting design must be of size: (n_items x T).")
            return nothing
        end
        if any((ata_model.output.design .!= 0.0) .& (ata_model.output.design .!= 1.0))
            error!(ata_model, "Starting design must contain only 1 or 0.")
            return nothing
        end
        x₀ = Float64.(starting_design)
    else
        x₀ = zeros(Float64, n_fs, ata_model.settings.T)
    end

    NH⁺ = Neighbourhood(x₀, Inf, zeros(T), 1e6 * ones(T), zeros(T), zero(Float64))
    bestNH = 1
    if size(x₀, 1) == 0
        NH⁺.x = zeros(Float64, n_fs, T)
    end
    # compute f
    if ata_model.settings.to_apply[1]
        iu_max = sum(NH⁺.x, dims = 2)[:, 1] - ata_model.settings.iu.max
        iu_max = sum_pos(iu_max)
    else
        iu_max = 0
    end
    if ata_model.settings.to_apply[2]
        iu_min = -sum(NH⁺.x, dims = 2)[:, 1] + ata_model.settings.iu.min
        iu_min = sum_pos(iu_min)
    else
        iu_min = 0
    end
    NH⁺.iu = iu_max + iu_min
    t = copy(start_temp)
    for v2 = 1:T
        NH⁺.infeas[v2], x_Iₜ = check_feas(
            ata_model.settings.fs,
            ata_model.constraints[v2],
            NH⁺.x[:, v2],
            n_fs,
            n_items,
        )
        if ata_model.settings.to_apply[3]
            NH⁺.ol = eval_overlap(NH⁺.x, fs_counts, ata_model.settings.ol_max, T, NH⁺.ol)
        end
        if fF == false
            if ata_model.obj.name in
               ["maximin", "minimax", "cc_maximin", "soyster_maximin", "de_jong_maximin"]
                NH⁺.obj[v2] = eval_TIFₜ(x_Iₜ, ata_model.obj.cores[v2])
            elseif ata_model.obj.name == "custom"
                NH⁺.obj = ata_model.obj.fun(x_Iₜ, ata_model.obj.args)
            end
        end
    end
    NH⁺.f = _comp_f(NH⁺, opt_feas)
    if sum(NH⁺.x) <= 1.0
        f⁺ = Inf
    else
        f⁺ = copy(NH⁺.f)
    end
    println("Starting solution: ")
    print_neighbourhood(NH⁺)
    if (Distributed.nprocs() > 1)
        workers = collect(1:(Distributed.nprocs()-1))
    else
        workers = [one(Int64)]
    end
    NHs = copy(workers)
    ata_model.output.neighbourhoods = [Neighbourhood() for n = 1:NHs[end]]
    for nh = 1:NHs[end]
        ata_model.output.neighbourhoods[nh] =
            _mycopy(NH⁺, ata_model.output.neighbourhoods[nh])
    end
    round = 1
    while finish == 0
        #for each proc in nprocs analyse nh
        Distributed.@sync Distributed.@distributed for p in workers
            nh_tot = (NHs[end] * (round - 1)) + NHs[p]
            # println("f was ", ata_model.output.neighbourhoods[NHs[p]].f)
            NH_proc = Neighbourhood()
            NH_proc = _mycopy(ata_model.output.neighbourhoods[NHs[p]], NH_proc)
            if round > 1
                #fill-up phase just in first round (first n_procs neighbourhoods)
                n_fill = 0
            end
            NH_proc = analyse_NH(
                NH_proc,
                ata_model;
                fF = fF,
                n_fill = n_fill,
                opt_feas = opt_feas,
                max_conv = max_conv,
                start_temp = t,
                geom_temp = geom_temp,
                n_item_sample = n_item_sample,
                n_test_sample = n_test_sample,
                verbosity = verbosity,
                start_time = start_time,
                max_time = max_time,
            )
            open(string(results_folder, "/neigh_", nh_tot, "_x.csv"), "w") do io
                return DelimitedFiles.writedlm(io, NH_proc.x)
            end
            open(string(results_folder, "/neigh_", nh_tot, "_infeas.csv"), "w") do io
                return DelimitedFiles.writedlm(io, NH_proc.infeas)
            end
            open(string(results_folder, "/neigh_", nh_tot, "_obj.csv"), "w") do io
                return DelimitedFiles.writedlm(io, NH_proc.obj)
            end
            open(string(results_folder, "/neigh_", nh_tot, "_ol.csv"), "w") do io
                return DelimitedFiles.writedlm(io, NH_proc.ol)
            end
            open(string(results_folder, "/neigh_", nh_tot, "_iu.csv"), "w") do io
                return DelimitedFiles.writedlm(io, NH_proc.iu)
            end
            open(string(results_folder, "/neigh_", nh_tot, "_f.csv"), "w") do io
                return DelimitedFiles.writedlm(io, NH_proc.f)
            end
            println("\nNeighbourhood ", nh_tot, " fully explored, increase temperature")
        end
        #save last nprocs nhs in text files
        for p in workers
            nh_tot = (NHs[end] * (round - 1)) + NHs[p]
            ata_model.output.neighbourhoods[NHs[p]].x = DelimitedFiles.readdlm(
                string(results_folder, "/neigh_", nh_tot, "_x.csv"),
                '\t',
            )
            ata_model.output.neighbourhoods[NHs[p]].obj = DelimitedFiles.readdlm(
                string(results_folder, "/neigh_", nh_tot, "_obj.csv"),
                '\t',
            )[
                :,
                1,
            ]
            ata_model.output.neighbourhoods[NHs[p]].infeas = DelimitedFiles.readdlm(
                string(results_folder, "/neigh_", nh_tot, "_infeas.csv"),
                '\t',
            )[
                :,
                1,
            ]
            ata_model.output.neighbourhoods[NHs[p]].ol = DelimitedFiles.readdlm(
                string(results_folder, "/neigh_", nh_tot, "_ol.csv"),
                '\t',
            )[
                :,
                1,
            ]
            ata_model.output.neighbourhoods[NHs[p]].iu = DelimitedFiles.readdlm(
                string(results_folder, "/neigh_", nh_tot, "_iu.csv"),
                '\t',
            )[
                1,
                1,
            ]
            ata_model.output.neighbourhoods[NHs[p]].f = DelimitedFiles.readdlm(
                string(results_folder, "/neigh_", nh_tot, "_f.csv"),
                '\t',
            )[
                1,
                1,
            ]
        end
        # NH+= 1
        # find best nh
        f_loc, nh_loc = findmin([ata_model.output.neighbourhoods[n].f for n = 1:NHs[end]])
        # copy first best result as benchmark (avoid to keep empty design as best solution in large scale models)
        if f_loc <= f⁺
            bestNH = (NHs[end] * (round - 1)) + nh_loc
            f⁺ = copy(f_loc)
            NH⁺ = _mycopy(ata_model.output.neighbourhoods[nh_loc], NH⁺)
        end
        # NHs = NHs.+Distributed.nprocs()
        if NHs[end] * round + 1 > feas_nh
            fF = false
        end
        if NHs[end] * round + 1 > nNH
            finish = 1
        else
            # perturbate the solution and go to warmup
        end
        round += 1
        # f_evals+= f_evals_NH
        # if f_evals >= max_evals
        # 	finish = 2
        # end
        if verbosity >= 1
            println(hline)
            println("results")
            if (finish == 1)
                println(" == > Maximum number of neighbourhoods explored <== ")
                # elseif (finish == 2) #time max reached
                # 	println(" == > Maximum number of evaluations reached <== ")
            elseif (finish == 3) # evals max reached
                println(" == > Maximum time reached <== ")
            end
            Printf.@printf("\n obj. value:	%5.1f", NH⁺.f)
            Printf.@printf("\n Optimality:	%5.1f", minimum(NH⁺.obj))
            Printf.@printf("\n Infeasibility:	%5.1f", sum(NH⁺.infeas + NH⁺.ol) + NH⁺.iu)
            Printf.@printf("\n Elapsed Time:	%5.1f", time() - start_time)
            Printf.@printf("\n Best Neighbourhood:	%5d", bestNH)
            println("\n")
            println(hline)
        end
        if time() - start_time >= max_time
            ata_model.output.elapsed_time = time() - start_time
            finish = 3
        end
        if finish > 0
            # f⁺, NH = findmin([ata_model.output.neighbourhoods[n].f for n = 1:nNH])
            # x⁺ = ata_model.output.neighbourhoods[NH].x
            # infeas⁺ = ata_model.output.neighbourhoods[NH].infeas+ata_model.output.neighbourhoods[NH].ol
            # iu⁺ = ata_model.output.neighbourhoods[NH].iu
            # TIF⁺ = ata_model.output.neighbourhoods[NH].obj
            ata_model.output.design = NH⁺.x
            ata_model.output.feas = NH⁺.infeas + NH⁺.ol
            ata_model.output.f = NH⁺.f
            ata_model.output.TIF = NH⁺.obj
        end
        # j = 1
        # for v = 1:ata_model.settings.T
        # 	for i = 1:ata_model.settings.n_fs
        # 		if NH⁺.x[i, v] == one(Float64)
        # 			if j == 1
        # 				NH⁺.x[i, v] = zero(Float64)
        # 				j+= 1
        # 			else
        # 				j - = 1
        # 			end
        # 		end
        # 	end
        # end
    end # end of finish
    JLD2.@save string(results_folder, "/ata_model.jld2") ata_model
    DelimitedFiles.writedlm(
        string(results_folder, "/design.csv"),
        reshape(ata_model.output.design, n_fs, T),
    )
    open(string(results_folder, "/results_ata.txt"), "w") do io
        write(io, "tests")
        write(io, "\r\n")
        DelimitedFiles.writedlm(io, collect(1:T)', ", ")
        write(io, "\r\n")
        write(io, "f⁺")
        write(io, "\r\n")
        DelimitedFiles.writedlm(io, ata_model.output.f)
        write(io, "\r\n")
        write(io, "infeasibility")
        write(io, "\r\n")
        DelimitedFiles.writedlm(io, ata_model.output.feas', ", ")
        write(io, "\r\n")
        write(io, "Item use")
        write(io, "\r\n")
        write(io, string(NH⁺.iu))
        write(io, "\r\n")
        write(io, "TIF")
        write(io, "\r\n")
        DelimitedFiles.writedlm(io, ata_model.output.TIF', ", ")
        write(io, "\r\n")
        write(io, "elapsed Time for optimization")
        write(io, "\r\n")
        write(io, string(ata_model.output.elapsed_time))
        return write(io, "\r\n")
    end
    return nothing
end


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

vjp_algorithm(d::AbstractDensity) = ForwardDiffAD()
vjp_algorithm(d::AbstractPosteriorDensity) = vjp_algorithm(getlikelihood(d))
vjp_algorithm(d::DensityWithShape) = vjp_algorithm(parent(d))
vjp_algorithm(d::TransformedDensity) = vjp_algorithm(parent(d))
vjp_algorithm(d::RenormalizedDensity) = vjp_algorithm(parent(d))

vjp_algorithm(f::Union{LogDensityOf,NegLogDensityOf}) = vjp_algorithm(f.density)

jvp_algorithm(d::AbstractDensity) = ForwardDiffAD()
jvp_algorithm(d::AbstractPosteriorDensity) = jvp_algorithm(getprior(d))
jvp_algorithm(d::DensityWithShape) = jvp_algorithm(parent(d))
jvp_algorithm(d::TransformedDensity) = jvp_algorithm(parent(d))
jvp_algorithm(d::RenormalizedDensity) = jvp_algorithm(parent(d))

jvp_algorithm(f::Union{LogDensityOf,NegLogDensityOf}) = jvp_algorithm(f.density)

struct DensityWithDiff{JVP<:DifferentiationAlgorithm,D<:AbstractDensity,VJP<:DifferentiationAlgorithm} <: AbstractDensity
    vjpalg::VJP
    density::D
    jvpalg::JVP
end

@inline Base.parent(density::DensityWithDiff) = density.density

vjp_algorithm(density::DensityWithDiff) = density.vjpalg
jvp_algorithm(density::DensityWithDiff) = density.jvpalg

Base.:(|)(vjpalg::DifferentiationAlgorithm, density::AbstractDensity) = DensityWithDiff(vjpalg, density, jvp_algorithm(density))
Base.:(|)(vjpalg::DifferentiationAlgorithm, density::DensityWithDiff) = DensityWithDiff(vjpalg, parent(density), jvp_algorithm(density))

Base.:(|)(density::AbstractDensity, jvpalg::DifferentiationAlgorithm) = DensityWithDiff(vjp_algorithm(density), density, jvpalg)
Base.:(|)(density::DensityWithDiff, jvpalg::DifferentiationAlgorithm) = DensityWithDiff(vjp_algorithm(density), parent(density), jvpalg)

function Base.show(io::IO, density::DensityWithDiff)
    vjpalg = vjp_algorithm(density)
    jvpalg = jvp_algorithm(density)
    parent_vjpalg = vjp_algorithm(parent(density))
    parent_jvpalg = jvp_algorithm(parent(density))
    if vjpalg != parent_vjpalg || jvpalg == parent_jvpalg
        Base.show(io, vjpalg)
        print(io, "|")
    end
    Base.show(io, parent(density))
    if jvpalg != parent_jvpalg
        print(io, "|")
        Base.show(io, jvpalg)
    end
end

var_bounds(density::DensityWithDiff) = var_bounds(density.density)

ValueShapes.varshape(density::DensityWithDiff) = varshape(density.density)

ValueShapes.unshaped(density::DensityWithDiff) = DensityWithDiff(density.vjpalg, unshaped(density.density), density.jvpalg)

@inline function eval_logval_unchecked(density::DensityWithDiff, v::Any)
    eval_logval_unchecked(density.density, v)
end

@inline function eval_logval(density::DensityWithDiff, v::Any, T::Type{<:Real})
    eval_logval(density.density, v, T)
end

@inline get_initsrc_from_target(target::DensityWithDiff) = get_initsrc_from_target(target.density)

function bat_transform_impl(target::NoDensityTransform, density::DensityWithDiff, algorithm::DensityIdentityTransform)
    (result = density, trafo = IdentityVT(varshape(density)))
end

function bat_transform_impl(target::AbstractDensityTransformTarget, density::DensityWithDiff, algorithm::TransformAlgorithm)
    transformed_density, trafo = bat_transform_impl(target, parent(density), algorithm)
    (result = DensityWithDiff(vjp_algorithm(density), transformed_density, jvp_algorithm(density)), trafo = trafo)
end


# License for this file: MIT (expat)
# Copyright 2017-2018, DLR Institute of System Dynamics and Control
#
# This file is part of module
#   ModiaMath.SimulationEngine(ModiaMath/SimulationEngine/_module.jl)
#

mutable struct IntegratorData
    model::ModiaMath.AbstractSimulationModel    # Model specific data structure
    simulationState::ModiaMath.SimulationState
    statistics::ModiaMath.SimulationStatistics   # Simulation statistics
    z::Vector{Float64}          # Vector of event indicators (zero crossings). If one of z[i] passes
                                # zero, that is beforeEvent(z[i])*z[i] < 0, an event is triggered
                                # (allocated during instanciation according to nz).
    zDir::Vector{Cint}          # zDir[i] =  0: Root is reported for both crossing directions
                                #         =  1: Root is reported when crossing from negative to positive direction
                                #         = -1: Root is reported when crossing from positive to negative direction

    eweight::Sundials.N_Vector   # Weight vector
    last_t::Float64              # Time instant of the last call to idasol_f (residue function)
    last_norm_y::Float64         # Norm of y of the last call to idasol_f (residue function)
    hcur::MVector{1,Cdouble}     # Current step size argument for IDAGetCurrentStep
    order::MVector{1,Cint}       # Current order for IDAGetCurrentOrder
    r::Vector{Sundials.realtype} # Residues vector used in idasol_f function
    ida_mem::Ptr{Nothing}        # IDA pointer (to access all IDAgetXXX functions)

    y::Vector{Sundials.realtype}    # Julia vector of y wrapping y_N_Vector vector
    yp::Vector{Sundials.realtype}   # Julia vector of yp wrapping yp_N_Vector vector
    fulljac::Union{Matrix{Sundials.realtype}, Nothing}  # Julia matrix of fulljac wrapping fulljac_N_xxx

    function IntegratorData(model::ModiaMath.AbstractSimulationModel, simulationState::ModiaMath.SimulationState)
        ny = simulationState.nx
        nz = simulationState.nz
        new(model, simulationState, simulationState.statistics,
          ones(nz), fill(0, 0), Sundials.N_VNew_Serial(ny), typemin(Float64), 0.0,
          MVector{1,Cdouble}(0.0), MVector{1,Cint}(0), zeros(ny))
    end
end

include("sundials_ida_UserData.jl")

function updateIDAstatistics!(mem, stat::ModiaMath.SimulationStatistics)
    info = Clong[0]
    Sundials.IDAGetNumSteps(mem, info)
    stat.nSteps        += info[1]

    stat.sparseSolver ? Sundials.IDASlsGetNumJacEvals(mem, info) : Sundials.IDADlsGetNumJacEvals(mem, info)
    stat.nJac          += info[1]

    Sundials.IDAGetNumErrTestFails(mem, info)
    stat.nErrTestFails += info[1]

    h0 = [0.0]
    Sundials.IDAGetActualInitStep(mem, h0)
    stat.h0 = min(stat.h0, h0[1])
end

"""
    result = closeTimePoints(t1,t2,epsilon)

The function returns true if the two time points t1 and t2 are close together (with respect to epsilon)
"""
closeTimePoints(t1::Float64, t2::Float64, epsilon::Float64) = abs(t1 - t2) / max(abs(t2), 10 * epsilon) <= 100 * epsilon

"""
    ModiaMath.simulate!(simulationModel; log=false, startTime=NaN, stopTime=NaN,
                                         tolerance=NaN, interval=NaN,
                                         maxStepSize=NaN, maxNumberOfSteps=missing)

Simulates a DAE `simulationModel` that is defined with package `Modia`, package `Modia3D` or
with the `ModiaMath.@component` macro. The DAE is mathematically described as
implicit, index 1 DAE with events (containing an ODE or a semi-explicit
index 1 DAE as special cases). For details see [`ModiaMath.StructureOfDAE`](@ref).

During continuous integration, the DAE is solved with the
[Sundials](https://computation.llnl.gov/projects/sundials) IDA solver
(accessed via the Julia [Sundials](https://github.com/JuliaDiffEq/Sundials.jl) interface package).

Input arguments

- `simulationModel::ModiaMath.AbstractSimulationModel`: Model struct (generated with Modia, Modia3D or `ModiaMath.@component`).
- `log::Bool`: = true, if logging is enabled, otherwise it is disabled.
- `startTime::Float64`: Start time of the simulation in [s].
                        If startTime=NaN, the default startTime is used that is defined by the `simulationModel`.
- `stopTime::Float64`: Stop time of the simulation in [s].
                       If stopTime=NaN, the default stopTime is used that is defined by the `simulationModel`.
- `tolerance::Float64`: The relative tolerance for the integration.
  The absolute tolerance is computed as `0.1*tolerance*nominal(variable)` where
  `nominal(variable)` is the nominal value of the variable.
   If tolerance=NaN, the default tolerance is used that is defined by the `simulationModel`.
- `interval::Float64`: Output interval for results in [s]. If events occur, the event time instants are
  additionally added to the result.
  If interval=NaN, the default interval is used that is defined by the `simulationModel`.
- `maxStepSize::Float64`: Maximum absolute value of the step size.
  If `maxStepSize=NaN`, the default maximum step size of the integrator is used.
- `maxNumberOfSteps::Union{Int,Missing}`: Maximum number of
  steps to be taken by the solver in its attempt to reach the next output time.
  If `maxNumberOfSteps=missing`, the default value of the integrator is used (= 500).
"""
function simulate!(model::ModiaMath.AbstractSimulationModel;
                   tolerance=NaN,
                   startTime=NaN,
                   stopTime=NaN,
                   interval=NaN,
                   maxStepSize=NaN,
                   maxNumberOfSteps=missing,
                   log::Bool=false,
                   KLUorderingChoice::Int=1)

    use_fulljac = false

    sim           = model.simulationState
    sim.model     = model
    sim.tolerance = isnan(tolerance) ? sim.defaultTolerance : convert(Float64, tolerance)
    sim.startTime = isnan(startTime) ? sim.defaultStartTime : convert(Float64, startTime)
    sim.stopTime  = isnan(stopTime)  ? sim.defaultStopTime  : convert(Float64, stopTime)
    sim.interval  = isnan(interval)  ? (isnan(sim.defaultInterval) ? (sim.stopTime - sim.startTime)/500.0 : sim.defaultInterval)  : convert(Float64, interval)
    sim.time      = sim.startTime
    @assert(sim.stopTime >= sim.startTime)
    @assert(sim.interval >= 0.0)
    @assert(sim.tolerance > 0.0)

    stopTime2::Float64  = sim.stopTime
    startTime2::Float64 = sim.startTime
    tolerance::Float64  = sim.tolerance
    interval::Float64   = sim.interval
    logger              = sim.logger
    ModiaMath.setLog!(logger, log)

    if ModiaMath.isLogInfos(logger)
        println("... ModiaMath.simulate! (version ", ModiaMath.Version, " ", ModiaMath.Date, ") to simulate model: ", sim.name)
    end

    # Start timing measure
    cpuStart::UInt64 = time_ns()
    cpuLast::UInt64  = cpuStart
    cpuStartIntegration::UInt64 = cpuStart

    # Init variables
    nt::Int           = round(Int64, 1 + (stopTime2 - startTime2) / interval)
    t0::Float64       = startTime2
    tEnd::Float64     = stopTime2
    tReached::Float64 = t0
    epsilon::Float64  = eps()

    # Initialize simulation model and store result after initialization
    statistics = sim.statistics
    ModiaMath.reInitializeStatistics!(statistics, t0, stopTime2, interval, tolerance)

    if ModiaMath.isLogInfos(logger)
        println("      Initialization at time = ", t0, " s")
    end

    init = DAE.initialize!(model, sim, t0, nt, tolerance)
    @assert(length(init.y0)  == sim.nx)
    @assert(length(init.yp0) == sim.nx)
    @assert(tolerance > 0.0)

    if init.terminate || tEnd <= t0
        statistics.h0   = 0.0
        statistics.hMin = 0.0
        @goto TerminateSimulation
    end

    if ModiaMath.isLogInfos(logger)
        println("      Simulation started")
    end

    maxTime::Float64          = init.maxTime
    nextEventTime::Float64    = init.nextEventTime
    integrateToEvent::Bool = init.integrateToEvent

    if nextEventTime < t0
        nextEventTime = typemax(Float64)
    end

    # Initialize auxiliary variables
    simModel = IntegratorData(model, sim)
    stateEvent::Bool = false
    timeEvent::Bool  = false
    flag::Cint = 0
    ny = sim.nx
    nz = sim.nz

    y  = copy(init.y0)
    yp = copy(init.yp0)
    simModel.y  = y
    simModel.yp = yp
    if use_fulljac
        fulljac = zeros(sim.nx, sim.nx)
        simModel.fulljac = fulljac
    else
        simModel.fulljac = nothing
    end

    eventInfo = DAE.EventInfo()
    tret    = [0.0]
    jc::Int = 0  # communication point counter
    if ModiaMath.isLogInfos(logger) && nz > 0
        rootInfo = fill(Cint(0), nz)
    end

    # Create IDA
    mem = Sundials.IDACreate()
    if mem == C_NULL
        error("ModiaMath.simulate: Failed to allocate IDA solver object")
    end
    simModel.ida_mem = mem

    # Allocate N_Vector storage for y and yp
    y_N_Vector  = Sundials.N_VMake_Serial(ny, pointer(y))
    yp_N_Vector = Sundials.N_VMake_Serial(ny, pointer(yp))

    # Run IDA
    try
        # Set error handler function
        Sundials.IDASetErrHandlerFn(mem, old_cfunction(idasol_ErrHandlerFn, Nothing, Tuple{Cint,Cstring,Cstring,Cstring,Ref{typeof(IntegratorData)}}), pointer_from_objref(simModel))

        # Initialize IDA
        Sundials.IDAInit(mem, old_cfunction(idasol_f, Cint, Tuple{Sundials.realtype,Sundials.N_Vector,Sundials.N_Vector,Sundials.N_Vector,Ref{typeof(IntegratorData)}}), t0, y_N_Vector, yp_N_Vector)
        if use_fulljac
            # IDADlsJacFn
            Sundials.IDADlsSetJacFn(mem, old_cfunction(idasol_fulljac, Cint, Tuple{Sundials.realtype,Sundials.realtype,Sundials.N_Vector,Sundials.N_Vector,
                                                       Sundials.N_Vector,Sundials.SUNMatrix, Ref{typeof(IntegratorData)},Sundials.N_Vector,Sundials.N_Vector,Sundials.N_Vector}))
        end

        #IDASStolerances(mem, tolerance, 0.1*tolerance)
        tolAbs = 0.1 * tolerance * init.y_nominal
        for i in 1:ny
            if !init.y_errorControl[i]
                tolAbs[i] = 1e5 * init.y_nominal[i]   # switch tolerance control off
            end
        end

        Sundials.IDASVtolerances(mem, tolerance, tolAbs)
        Sundials.IDASetUserData(mem, simModel)

        if !isnan(maxStepSize)
            Sundials.IDASetMaxStep(mem, maxStepSize)
            sim.maxStepSize = maxStepSize
        elseif !isnan(sim.defaultMaxStepSize)
            Sundials.IDASetMaxStep(mem, sim.defaultMaxStepSize)
            sim.maxStepSize = sim.defaultMaxStepSize
        end

        if typeof(maxNumberOfSteps) != Missing
            Sundials.IDASetMaxNumSteps(mem, maxNumberOfSteps)
            sim.maxNumberOfSteps = maxNumberOfSteps
        elseif typeof(sim.defaultMaxNumberOfSteps) != Missing
            Sundials.IDASetMaxNumSteps(mem, sim.defaultMaxNumberOfSteps)
            sim.maxNumberOfSteps = sim.defaultMaxNumberOfSteps
        end

        # Sundials.IDASetId(mem, y_states_nvector)
        #if sim.sparse
        #   nnz_jac = nnz(sim.jac)
        #   Sundials.IDAKLU(mem, ny, nnz_jac)
        #   Sundials.IDAKLUSetOrdering(mem, KLUorderingChoice)
        #   Sundials.IDASlsSetSparseJacFn(mem);
        #else
        A  = Sundials.SUNDenseMatrix(ny, ny)
        LS = Sundials.SUNDenseLinearSolver(y, A)
        Sundials.IDADlsSetLinearSolver(mem, LS, A)
        #end

        # Initialize zero crossing function, if required
        hasZeroCross = nz > 0
        if hasZeroCross
            Sundials.IDARootInit(mem, nz, old_cfunction(idasol_g, Cint, Tuple{Sundials.realtype,Sundials.N_Vector,Sundials.N_Vector,Ptr{Sundials.realtype},Ref{typeof(IntegratorData)}}))
            Sundials.IDASetRootDirection(mem, sim.zDir)
        end

        # Initialize event variables
        tStop::Float64 = tEnd
        tNext = t0
        tc    = t0
        nTimeEvents = 0
        restart     = ModiaMath.NoRestart

        # Check for event at time = t0. If yes, trigger event and store result after event
        if closeTimePoints(t0, nextEventTime, epsilon)
            tReached = t0
            if ModiaMath.isLogEvents(logger)
                println("\n      Time event at time = $tReached s")
            end

            DAE.reset!(eventInfo)
            DAE.processEvent!(model, sim, tReached, y, yp, eventInfo)
            restart          = eventInfo.restart
            maxTime          = eventInfo.maxTime
            nextEventTime    = eventInfo.nextEventTime
            integrateToEvent = eventInfo.integrateToEvent

            statistics.nTimeEvents += 1
            if ModiaMath.isLogEvents(logger)
                println("        restart = ", restart)
            end

            # Reinitialize IDA if required by model
            if restart == ModiaMath.Restart
                statistics.nRestartEvents += 1
                Sundials.__IDAReInit(mem, tReached, y_N_Vector, yp_N_Vector);
            end
        end

        # Loop over communication points
        cpuStartIntegration = time_ns()

        while tReached < tEnd && restart != ModiaMath.Terminate  #------------------ while --------------
            if tReached >= tc
                # Last communication point reached -> increment communication point
                jc += 1
                tc = startTime2 + jc * interval
            end

            if closeTimePoints(nextEventTime, tc, epsilon)
                nextEventTime = tc
            end

            if nextEventTime <= tc # next tStop is a time event
                event = true
                tNext = nextEventTime
                tStop = integrateToEvent ? nextEventTime : min(tEnd, maxTime)
            else        # next tStop is a communication point (no event)
                event = false
                tNext = tc
                tStop = min(tEnd, maxTime)
            end

            # Integrate in direction of tStop
            #println("... tReached = ", tReached, ", tNext = ", tNext, ", tStop = ", tStop,
            #        ", y = ", y[1], ", yp = ", yp[1])
            Sundials.IDASetStopTime(mem, tStop)
            #flag = Sundials.IDASolve(mem, tNext, tret, simModel.y, simModel.yp, Sundials.IDA_NORMAL)
            flag = Sundials.__IDASolve(mem, tNext, tret, y_N_Vector, yp_N_Vector, Sundials.IDA_NORMAL)
            tReached = tret[1]
            #println("    tReached = ", tReached)

            stateEvent = flag == Sundials.IDA_ROOT_RETURN
            timeEvent  = event && closeTimePoints(tNext, tReached, epsilon)
            isEvent    = timeEvent || stateEvent

            # Store result point at tReached
            DAE.computeAndStoreResult!(model, sim, tReached, y, yp)

            # If event at tReached, handle the event
            if isEvent
                if ModiaMath.isLogEvents(logger)
                    if timeEvent && stateEvent
                        print("\n      Time and state (zero-crossing) event at time = $tReached s")
                    elseif timeEvent
                        println("\n      Time event at time = $tReached s")
                    elseif stateEvent
                        print("\n      State event (zero-crossing) at time = $tReached s")
                    end

                    if stateEvent
                       # Print information about the root
                        Sundials.IDAGetRootInfo(mem, rootInfo)
                        print(" (")
                        firstRoot = true
                        for iroot in eachindex(rootInfo)
                            if rootInfo[iroot] != 0
                                if firstRoot
                                    firstRoot = false
                                else
                                    print(", ")
                                end
                                print("z[", iroot, "]", rootInfo[iroot] > 0 ? " > 0" : " < 0")
                            end
                        end
                        print(")\n")
                    end
                end

                # Trigger event
                DAE.reset!(eventInfo)
                DAE.processEvent!(model, sim, tReached, y, yp, eventInfo)
                restart          = eventInfo.restart
                maxTime          = eventInfo.maxTime
                nextEventTime    = eventInfo.nextEventTime
                integrateToEvent = eventInfo.integrateToEvent

                if timeEvent
                    statistics.nTimeEvents += 1
                end

                if stateEvent
                    statistics.nStateEvents += 1
                end

                if restart == ModiaMath.Restart || restart == ModiaMath.FullRestart
                    statistics.nRestartEvents += 1
                end

                if ModiaMath.isLogEvents(logger)
                    println("        restart = ", restart)
                end

                # Handle restart flag
                if restart == ModiaMath.Restart
                    # Restart (dimensions do not change)
                    updateIDAstatistics!(mem, statistics)
                    Sundials.__IDAReInit(mem, tReached, y_N_Vector, yp_N_Vector);
                elseif restart == ModiaMath.FullRestart
                    error("FullRestart not yet supported")
                end
            end

            # print every 5 s the time in order that the user sees the progress
            if ModiaMath.isLogProgress(logger)
                cpuNew = time_ns()
                if (cpuNew - cpuLast) * 1e-9 > 5.0
                    cpuLast = cpuNew
                    @printf("      progress: integrated up to time = %.2g s\n", tReached)
                end
            end
        end #---------------------- end while ------------------------------------

        # Finalize statistics for simulation run
        updateIDAstatistics!(mem, statistics)
    finally
        # Free allocated memory
        Sundials.__N_VDestroy_Serial(y_N_Vector)
        Sundials.__N_VDestroy_Serial(yp_N_Vector)
        Sundials.IDAFree([mem])
    end

    @label TerminateSimulation
    if ModiaMath.isLogInfos(logger)
        println("\n      Simulation is terminated at time = ", tEnd, " s")
    end

    result = DAE.terminate!(model, sim, tReached, y, yp)
    statistics.cpuTimeInitialization = (cpuStartIntegration - cpuStart) * 1e-9
    statistics.cpuTimeIntegration    = (time_ns() - cpuStartIntegration) * 1e-9

    if ModiaMath.isLogStatistics(logger)
        println("\n      Statistics (get help with ?ModiaMath.SimulationStatistics):")
        display(statistics)
    end
    return result
end


"""
Data structure for holding a routing in a path-based manner.
"""
struct Routing
    data::RoutingData
    path_flows::Dict{Edge{Int}, Dict{Int, Float64}} # Demand -> path -> flow.
    edge_flows::Dict{Edge{Int}, Dict{Edge{Int}, Float64}} # Demand -> edge -> flow.
    demands::Vector{Edge} # List of demand; the only goal of this vector is to fix
    # the order of the demands (i.e. have demand IDs).
    paths::Vector{Vector{Edge}} # A path is a list of edges. This vector stores
    # a mapping from integers (indices in the vector) to paths.

    function Routing(data::RoutingData, routing::AbstractMatrix{Float64})
        if data.model_type.type == FlowFormulation()
            # If the model is not path-based, translate it.
            path_flows, edge_flows, paths = flow_routing_to_path(data, routing)
            return new(data, path_flows, edge_flows, collect(demands(data)), paths)
        else
            @assert data.model_type.type == PathFormulation()
            # Otherwise, much simpler transformation: the solution is already written in terms of paths.
            path_flows, edge_flows = path_routing_to_flow(data, routing)
            return new(data, path_flows, edge_flows, collect(demands(data)), data.paths_edges)
        end
    end
end

function routing_to_matrix(r::Routing)
    routing_matrix = zeros(length(r.demands), length(r.paths))
    for demand_id in 1:length(r.demands)
        demand = r.demands[demand_id]
        for (path, weight) in r.path_flows[demand]
            routing_matrix[demand_id, path] = weight
        end
    end
    return routing_matrix
end

function total_flow_in_edge(r::Routing, e::Edge, dm)
    # dm is an optimisation variable, as the routing is fixed.
    return total_flow_in_edge(r, e, dm, r.data.model_type.type)
end

function total_flow_in_edge(r::Routing, e::Edge, dm, ::FormulationType; ε::Float64=1.e-3)
    # dm is an optimisation variable, as the routing is fixed.
    # Works for both flow-based and path-based formulations.
    # Cannot use sum(), as the collection is sometimes empty.
    flow = 0.0
    for d in demands(r.data)
        if e in keys(r.edge_flows[d]) && abs(r.edge_flows[d][e]) > ε
            flow += r.edge_flows[d][e] * dm[d]
        end
    end
    return flow
end

"""
Data structure holding results of the computations for a routing.
All information from intermediate iterations are kept within this object.

  * `data`: a pointer to the `RoutingData` object that was used for the
    computations.
  * `result`: an `MOI.TerminationStatusCode` indicating the result of the
    optimiser. This is usually `MOI.OPTIMAL`, but other codes indicate why
    the solver stopped (`MOI.SLOW_PROGRESS`, `MOI.INFEASIBLE`, etc.).
    This code is not supposed to be the result of the last iteration, but
    a summary of the termination of the process: for instance, if the last
    iteration gives an `MOI.INFEASIBLE` code at the last iteration due to
    numerical errors, the whole process may show `MOI.SLOW_PROGRESS`.
  * `n_iter`: the number of iterations (the exact meaning of this field depends
    on the algorithm). Iterations are supposed to be spent in this package,
    improving/finding a network routing; this should not be a copy of a field
    from the underlying solver (e.g., simplex iterations).
    (This field is computed and not explicitly stored in the object.)
  * `n_matrices`: the number of traffic matrices added into the formulation. It
    may be equal to the number of iterations, depending on the algorithm.
    (This field is computed and not explicitly stored in the object.)
  * `n_cuts`: the number of added constraints.
  * `n_columns`: the number of added columns, for column-generation based
    algorithms. Its value should be 0 for other algorithms.

Three different timings are measured for the whole execution (always in
milliseconds):

  * `time_precompute_ms`: time to prepare the data structures before the actual
    computations. For instance, it may account for detection of loops or
    unroutable demands; for column-generation algorithms, it includes the time
    to generate the first few paths (i.e. before any pricing process can take
    place). This parameter takes precedence on the one from the given
	`RoutingData` object (i.e. it may include more operations).
  * `time_create_master_model_ms`: time to create the mathematical formulation
    of the master problem (or the only problem, if the algorithm is not
	iterative). In particular, this does not include instanciation of
	subproblems.
  * `total_time_solve_ms`: total time spent in solving.
    (This field is computed and not explicitly stored in the object.)
  * `time_solve_ms`: time to compute the routing, one value per iteration.
  * `time_intermediate_export_ms`: time to export the intermediate results,
    when requested, one value per iteration where there is export.
  * `time_final_export_ms`: time to export the final solution, when requested.
  * `total_time_export_ms`: total time spent in exporting.
    (This field is computed and not explicitly stored in the object.)
  * `total_time_ms`: total time spent in the whole process (precomputing,
    solving, exporting).
    (This field is computed and not explicitly stored in the object.)

The following vectors contain information about the execution of the algorithm.
Not all vectors have as many entries as iterations, even though it is expected
to be the most likely case.

  * `objectives`: the value of the objective function that is being optimised,
    at most one per iteration.
  * `matrices`: the demand matrices generated during the execution, when their
    output is requested. It may contain a single matrix if the algorithm does
    not generate new matrices during its execution. There may be no such matrix
    at the last iteration. There may be several matrices for some iterations.
  * `routings`: the various routings generated during the execution. There must
    be at most one routing per iteration, except when requested.
  * `master_model`: the final optimisation model, with all generated cuts and
    columns. Solving it should give the same solution as `routings[end]`.
"""
struct RoutingSolution
    data::RoutingData
    result::MOI.TerminationStatusCode
    n_cuts::Int
    n_columns::Int

    time_precompute_ms::Float64
	time_create_master_model_ms::Float64
    time_solve_ms::Dict{Int, Float64}
    time_intermediate_export_ms::Dict{Int, Float64}
    time_final_export_ms::Float64

    objectives::Dict{Int, Float64}
    matrices::Dict{Int, Vector{Dict{Edge{Int}, Float64}}}
    routings::Dict{Int, Routing}
    master_model::RoutingModel
end

function Base.getproperty(obj::RoutingSolution, sym::Symbol)
    if sym === :n_matrices
        return length(obj.matrices)
    elseif sym === :n_iter
        return length(obj.time_solve_ms)
    elseif sym === :total_time_solve_ms
        return sum(values(obj.time_solve_ms))
    elseif sym === :total_time_export_ms
        return sum(values(obj.time_intermediate_export_ms)) + obj.time_final_export_ms
    elseif sym === :total_time_ms
        return obj.time_precompute_ms + obj.time_create_master_model_ms + sum(values(obj.time_solve_ms)) + sum(values(obj.time_intermediate_export_ms)) + obj.time_final_export_ms
    end
    return getfield(obj, sym)
end

_parse_routingsolution_input(x::T) where T = Dict{Int, T}(1 => x)
_parse_routingsolution_input(x::Vector{T}) where T = Dict{Int, T}(i => x[i] for i in 1:length(x))
_parse_routingsolution_input(x::Dict{Int, T}) where T = x

_parse_routingsolution_matrices(x::Dict{Edge{Int}, Float64}) = length(x) == 0 ? Dict{Int, Vector{Dict{Edge{Int}, Float64}}}() : Dict(1 => [x])
_parse_routingsolution_matrices(x::Vector{Dict{Edge{Int}, Float64}}) = length(x) == 0 ? Dict{Int, Vector{Dict{Edge{Int}, Float64}}}() : Dict(1 => x)
_parse_routingsolution_matrices(x::Dict{Int, Vector{Dict{Edge{Int}, Float64}}}) = x

function RoutingSolution(data::RoutingData;
                         result::MOI.TerminationStatusCode=MOI.OPTIMAL,
                         n_cuts::Int=0,
                         n_columns::Int=0,
                         time_precompute_ms::Float64=0.0,
						 time_create_master_model_ms::Float64=0.0,
						 time_solve_ms::Union{Float64, Vector{Float64}, Dict{Int, Float64}}=0.0,
						 time_intermediate_export_ms::Union{Float64, Vector{Float64}, Dict{Int, Float64}}=Dict{Int, Float64}(),
						 time_final_export_ms::Float64=0.0,
						 objectives::Union{Float64, Vector{Float64}, Dict{Int, Float64}}=error("Missing parameter `objectives` when building a `RoutingSolution` object"),
						 matrices::Union{Dict{Edge{Int}, Float64}, Vector{Dict{Edge{Int}, Float64}}, Dict{Int, Vector{Dict{Edge{Int}, Float64}}}}=Dict{Edge{Int}, Float64}(),
						 routings::Union{Routing, Vector{Routing}, Dict{Int, Routing}}=error("Missing parameter `routings` when building a `RoutingSolution` object"),
						 master_model::RoutingModel=error("Missing parameter `master_model` when building a `RoutingSolution` object")
						)
    return RoutingSolution(data,
	                       result,
						   n_cuts,
						   n_columns,
	                       time_precompute_ms,
						   time_create_master_model_ms,
						   _parse_routingsolution_input(time_solve_ms),
						   _parse_routingsolution_input(time_intermediate_export_ms),
						   time_final_export_ms,
						   _parse_routingsolution_input(objectives),
						   _parse_routingsolution_matrices(matrices),
						   _parse_routingsolution_input(routings),
						   master_model)
end

function flow_routing_to_path(data::RoutingData, routing::AbstractMatrix{Float64}; demand=nothing, ε::Float64=1.e-5)
    if data.model_type.type != FlowFormulation()
        error("This function can only be used for flow-based formulations; it makes no sense for formulation type $(data.model_type)")
    end

    path_flows = Dict{Edge{Int}, Dict{Int, Float64}}() # demand -> path index -> flow
    edge_flows = Dict{Edge{Int}, Dict{Edge{Int}, Float64}}() # demand -> edge -> flow
    paths = Vector{Edge}[] # list of paths (indexes match those of path_flows)

    for d in demands(data)
        # Prepare to store the results for this demand.
        path_flows[d] = Dict{Int, Float64}()
        edge_flows[d] = Dict{Edge{Int}, Float64}()

        # Copy the flows into a new graph (with only those edges).
        # Don't use a weighted graph: removing edges does not always work
        # as expected.
        ug = MetaGraph(nv(graph(data)))
        for e in edges(data)
            if has_edge(ug, src(e), dst(e))
                continue
            end

            flow_a = try; routing[d, e]; catch; 0.0; end
            flow_b = try; routing[d, reverse(e)]; catch; 0.0; end
            flow = abs(flow_a - flow_b)
            if flow > ε
                add_edge!(ug, src(e), dst(e), :weight, flow)
            end
        end

        # Compute paths and remove the corresponding flow as long as there is
        # something in there.
        weight_e(e) = get_prop(ug, e, :weight)
        while ne(ug) > 0
            # Get a path arbitrarily (easiest to compute: shortest one)
            sp = desopo_pape_shortest_paths(ug, src(d))
            path_vertices = enumerate_paths(sp, dst(d))
            path = map(Edge, zip(path_vertices[1:end-1], path_vertices[2:end]))
            if length(path) == 0
                break
            end
            path_weight = minimum(weight_e(e) for e in path)

            # Update the data structures with this new path.
            push!(paths, path)
            path_idx = length(paths)
            path_flows[d][path_idx] = path_weight
            for e in path # TODO: DataStructures.jl's DefaultDict? Potential problems when returning this instead of a Dict?
                if ! haskey(path_flows[d], e)
                    edge_flows[d][e] = 0.0
                end
                edge_flows[d][e] += path_weight
            end

            # Remove the path from the graph.
            for e in path
                new_weight = weight_e(e) - path_weight
                if new_weight > ε
                    set_prop!(ug, e, :weight, new_weight)
                else
                    rem_edge!(ug, e)
                end
            end
        end
    end

    return path_flows, edge_flows, paths
end

function path_routing_to_flow(data::RoutingData, routing::AbstractMatrix{Float64}; ε::Float64=1.e-5)
    if data.model_type.type != PathFormulation()
        error("This function can only be used for path-based formulations; it makes no sense for formulation type $(data.model_type)")
    end

    # Sum over all paths that cross a given edge to get the total weight of each edge, demand per demand.
    path_flows = Dict{Edge{Int}, Dict{Int, Float64}}()

    for k in eachindex(routing)
        demand = k[1]
        path = k[2]

        if ! (demand in keys(path_flows))
            path_flows[demand] = Dict{Int, Float64}()
        end

        if abs(routing[k]) > ε
            path_flows[demand][path] = routing[k]
        end
    end

    # Sum the flows over the demands.
    edge_flows = Dict{Edge{Int}, Dict{Edge{Int}, Float64}}()
    for demand in demands(data)
        edge_flows[demand] = Dict{Edge{Int}, Float64}()
        for edge in edges(data)
            total_weight = zero(Float64)
            for p_id in find_path_ids_with_edge(data, edge)
                if data.path_id_to_demand[p_id] == demand
                    total_weight += routing[demand, p_id]
                end
            end

            if abs(total_weight) > ε
                edge_flows[demand][edge] = total_weight
            end
        end
    end

    return path_flows, edge_flows
end


for (func, typ) in [(:clblasSsyr2k, cl.CL_float),
                    (:clblasDsyr2k, cl.CL_double),
                    (:clblasCsyr2k, CL_float2),
                    (:clblasZsyr2k, CL_double2)]

    @eval @blasfun $func(order::clblasOrder, uplo::clblasUplo,
                              transAB::clblasTranspose,
                              N::Csize_t, K::Csize_t,
                              alpha::$typ,
                              A::cl.CL_mem, offA::Csize_t, lda::Csize_t,
                              B::cl.CL_mem, offB::Csize_t, ldb::Csize_t,
                              beta::$typ,
                              C::cl.CL_mem, offC::Csize_t, ldc::Csize_t,
                              n_queues::cl.CL_uint,
                              queues::Ptr{cl.CL_command_queue},
                              n_events_in_wait_list::cl.CL_uint,
                              event_wait_list::Ptr{cl.CL_event},
                              events::Ptr{cl.CL_event})

    @eval @blasfun2 $func(order::clblasOrder, uplo::clblasUplo,
                               transAB::clblasTranspose,
                               N::Csize_t, K::Csize_t,
                               alpha::$typ,
                               A::cl.CL_mem, offA::Csize_t, lda::Csize_t,
                               B::cl.CL_mem, offB::Csize_t, ldb::Csize_t,
                               beta::$typ,
                               C::cl.CL_mem, offC::Csize_t, ldc::Csize_t)

end


abstract type AbstractXAIMethod end
# All analyzers are implemented such that they return an array of explanations:
#   (method::AbstractXAIMethod)(input, ns::AbstractNeuronSelector)::Vector{Explanation}

const BATCHDIM_MISSING = ArgumentError(
    """The input is a 1D vector and therefore missing the required batch dimension.
    Call analyze with the keyword argument add_batch_dim=false."""
)

"""
    analyze(input, method)
    analyze(input, method, neuron_selection)

Return raw classifier output and explanation.
If `neuron_selection` is specified, the explanation will be calculated for that neuron.
Otherwise, the output neuron with the highest activation is automatically chosen.

## Keyword arguments
- `add_batch_dim`: add batch dimension to the input without allocating. Default is `false`.
"""
function analyze(
    input::AbstractArray{<:Real},
    method::AbstractXAIMethod,
    neuron_selection::Union{Integer,Tuple{<:Integer}};
    kwargs...,
)
    return _analyze(input, method, IndexSelector(neuron_selection); kwargs...)
end

function analyze(input::AbstractArray{<:Real}, method::AbstractXAIMethod; kwargs...)
    return _analyze(input, method, MaxActivationSelector(); kwargs...)
end

function (method::AbstractXAIMethod)(
    input::AbstractArray{<:Real},
    neuron_selection::Union{Integer,Tuple{<:Integer}};
    kwargs...,
)
    return _analyze(input, method, IndexSelector(neuron_selection); kwargs...)
end
function (method::AbstractXAIMethod)(input::AbstractArray{<:Real}; kwargs...)
    return _analyze(input, method, MaxActivationSelector(); kwargs...)
end

# lower-level call to method
function _analyze(
    input::AbstractArray{T,N},
    method::AbstractXAIMethod,
    sel::AbstractNeuronSelector;
    add_batch_dim::Bool=false,
    kwargs...,
) where {T<:Real,N}
    if add_batch_dim
        return method(batch_dim_view(input), sel; kwargs...)
    end
    N < 2 && throw(BATCHDIM_MISSING)
    return method(input, sel; kwargs...)
end

# for convenience, the anaylyzer can be called directly

# Explanations and outputs are returned in a wrapper.
# Metadata such as the analyzer allows dispatching on functions like `heatmap`.
struct Explanation{A,O,I,L}
    attribution::A
    output::O
    neuron_selection::I
    analyzer::Symbol
    layerwise_relevances::L
end


const EXPONENTS = [
    1e0,   1e1,   1e2,   1e3,   1e4,   1e5,   1e6,   1e7,   1e8,    1e9,
    1e10,  1e11,  1e12,  1e13,  1e14,  1e15,  1e16,  1e17,  1e18,  1e19,
    1e20,  1e21,  1e22,  1e23,  1e24,  1e25,  1e26,  1e27,  1e28,  1e29,
    1e30,  1e31,  1e32,  1e33,  1e34,  1e35,  1e36,  1e37,  1e38,  1e39,
    1e40,  1e41,  1e42,  1e43,  1e44,  1e45,  1e46,  1e47,  1e48,  1e49,
    1e50,  1e51,  1e52,  1e53,  1e54,  1e55,  1e56,  1e57,  1e58,  1e59,
    1e60,  1e61,  1e62,  1e63,  1e64,  1e65,  1e66,  1e67,  1e68,  1e69,
    1e70,  1e71,  1e72,  1e73,  1e74,  1e75,  1e76,  1e77,  1e78,  1e79,
    1e80,  1e81,  1e82,  1e83,  1e84,  1e85,  1e86,  1e87,  1e88,  1e89,
    1e90,  1e91,  1e92,  1e93,  1e94,  1e95,  1e96,  1e97,  1e98,  1e99,
    1e100, 1e101, 1e102, 1e103, 1e104, 1e105, 1e106, 1e107, 1e108, 1e109,
    1e110, 1e111, 1e112, 1e113, 1e114, 1e115, 1e116, 1e117, 1e118, 1e119,
    1e120, 1e121, 1e122, 1e123, 1e124, 1e125, 1e126, 1e127, 1e128, 1e129,
    1e130, 1e131, 1e132, 1e133, 1e134, 1e135, 1e136, 1e137, 1e138, 1e139,
    1e140, 1e141, 1e142, 1e143, 1e144, 1e145, 1e146, 1e147, 1e148, 1e149,
    1e150, 1e151, 1e152, 1e153, 1e154, 1e155, 1e156, 1e157, 1e158, 1e159,
    1e160, 1e161, 1e162, 1e163, 1e164, 1e165, 1e166, 1e167, 1e168, 1e169,
    1e170, 1e171, 1e172, 1e173, 1e174, 1e175, 1e176, 1e177, 1e178, 1e179,
    1e180, 1e181, 1e182, 1e183, 1e184, 1e185, 1e186, 1e187, 1e188, 1e189,
    1e190, 1e191, 1e192, 1e193, 1e194, 1e195, 1e196, 1e197, 1e198, 1e199,
    1e200, 1e201, 1e202, 1e203, 1e204, 1e205, 1e206, 1e207, 1e208, 1e209,
    1e210, 1e211, 1e212, 1e213, 1e214, 1e215, 1e216, 1e217, 1e218, 1e219,
    1e220, 1e221, 1e222, 1e223, 1e224, 1e225, 1e226, 1e227, 1e228, 1e229,
    1e230, 1e231, 1e232, 1e233, 1e234, 1e235, 1e236, 1e237, 1e238, 1e239,
    1e240, 1e241, 1e242, 1e243, 1e244, 1e245, 1e246, 1e247, 1e248, 1e249,
    1e250, 1e251, 1e252, 1e253, 1e254, 1e255, 1e256, 1e257, 1e258, 1e259,
    1e260, 1e261, 1e262, 1e263, 1e264, 1e265, 1e266, 1e267, 1e268, 1e269,
    1e270, 1e271, 1e272, 1e273, 1e274, 1e275, 1e276, 1e277, 1e278, 1e279,
    1e280, 1e281, 1e282, 1e283, 1e284, 1e285, 1e286, 1e287, 1e288, 1e289,
    1e290, 1e291, 1e292, 1e293, 1e294, 1e295, 1e296, 1e297, 1e298, 1e299,
    1e300, 1e301, 1e302, 1e303, 1e304, 1e305, 1e306, 1e307, 1e308,
]

pow10(exp) = (@inbounds v = EXPONENTS[exp+1]; return v)

maxexponent(::Type{Int16}) = 4
maxexponent(::Type{Int32}) = 38
maxexponent(::Type{Int64}) = 308

minexponent(::Type{Int16}) = -5
minexponent(::Type{Int32}) = -38
minexponent(::Type{Int64}) = -308

inttype(::Type{Float16}) = Int16
inttype(::Type{Float32}) = Int32
inttype(::Type{Float64}) = Int64

const BIGN = UInt8('N')
const LITTLEN = UInt8('n')
const BIGA = UInt8('A')
const LITTLEA = UInt8('a')
const BIGI = UInt8('I')
const LITTLEI = UInt8('i')
const BIGF = UInt8('F')
const LITTLEF = UInt8('f')
const BIGT = UInt8('T')
const LITTLET = UInt8('t')
const BIGY = UInt8('Y')
const LITTLEY = UInt8('y')
const BIGE = UInt8('E')
const LITTLEE = UInt8('e')

ParsingException(::Type{<:AbstractFloat}, exp::Signed, row, col) = ParsingException("error parsing a `$T` value on column $col, row $row; exponent out of range: $exp")

function scale(exp, v::T, frac, row, col) where T
    if exp >= 0
        max_exp = maxexponent(T)
        exp > max_exp && throw(ParsingException(T, exp, row, col))
        if exp > 15
            return Float64(Base.TwicePrecision{Float64}(v) * Base.TwicePrecision{Float64}(pow10(exp)))
        else
            return v * pow10(exp)
        end
    else
        min_exp = minexponent(T)
        if exp < min_exp
            -exp + min_exp > -min_exp && throw(ParsingException(T, exp, row, col))
            return Float64(Base.TwicePrecision{Float64}(v) / Base.TwicePrecision{Float64}(pow10(-exp + min_exp)))
        else
            if exp > 15
                return Float64(Base.TwicePrecision{Float64}(v) / Base.TwicePrecision{Float64}(pow10(-exp)))
            else
                return v / pow10(-exp)
            end
        end
    end
end

function parsefield(io::IO, ::Type{T}, opt::CSV.Options, row, col, state, ifmissing::Function) where {T <: Union{Float16, Float32, Float64}}
    mark(io)
    @checkmissingstart()
    minussign = plussign = false
    if b == MINUS # check for leading '-' or '+'
        minussign = true
        c = peekbyte(io)
        if (NEG_ONE < c < TEN) || c == opt.decimal
            b = readbyte(io)
        end
    elseif b == PLUS
        plussign = true
        c = peekbyte(io)
        if (NEG_ONE < c < TEN) || c == opt.decimal
            b = readbyte(io)
        end
    end
    # float digit parsing
    iT = inttype(T)
    v = zero(iT)
    parseddigits = false
    while NEG_ONE < b < TEN
        parseddigits = true
        # process digits
        v *= iT(10)
        v += iT(b - ZERO)
        eof(io) && (state[] = EOF; result = T(v); @goto done)
        b = readbyte(io)
    end
    # if we didn't get any digits and character isn't leading dot, check for NaN/Inf
    if !parseddigits && b != opt.decimal
        if minussign || plussign # skip sign character, if any
            eof(io) && @goto checkmissingend
            b = readbyte(io)
        end
        if b == LITTLEN || b == BIGN
            eof(io) && @goto checkmissingend
            b = readbyte(io)
            (!(b == LITTLEA || b == BIGA) || eof(io)) && (reset(io); b = readbyte(io); @goto checkmissingend)
            b = readbyte(io)
            !(b == LITTLEN || b == BIGN) && (reset(io); b = readbyte(io); @goto checkmissingend)
            result = T(NaN)
            eof(io) && (state[] = EOF; @goto done)
            b = readbyte(io)
            @goto checkdone
        elseif b == LITTLEI || b == BIGI
            eof(io) && @goto checkmissingend
            b = readbyte(io)
            (!(b == LITTLEN || b == BIGN) || eof(io)) && (reset(io); b = readbyte(io); @goto checkmissingend)
            b = readbyte(io)
            !(b == LITTLEF || b == BIGF) && (reset(io); b = readbyte(io); @goto checkmissingend)
            result = T(Inf)
            eof(io) && (state[] = EOF; @goto done)
            b = readbyte(io)
            if b == LITTLEI || b == BIGI
                # read the rest of INFINITY
                eof(io) && (state[] = EOF; @goto done)
                b = readbyte(io)
                b == LITTLEN || b == BIGN || @goto checkdone
                eof(io) && (state[] = EOF; @goto done)
                b = readbyte(io)
                b == LITTLEI || b == BIGI || @goto checkdone
                eof(io) && (state[] = EOF; @goto done)
                b = readbyte(io)
                b == LITTLET || b == BIGT || @goto checkdone
                eof(io) && (state[] = EOF; @goto done)
                b = readbyte(io)
                b == LITTLEY || b == BIGY || @goto checkdone
                eof(io) && (state[] = EOF; @goto done)
                b = readbyte(io)
            end
            @goto checkdone
        else
            @goto checkmissingend
        end
    end
    # parse fractional part
    frac = 0
    result = T(v)
    if b == opt.decimal
        eof(io) && (state[] = EOF; parseddigits ? @goto(done) : @goto(error))
        b = readbyte(io)
    elseif b == LITTLEE || b == BIGE
        @goto parseexp
    else
        @goto checkdone
    end

    while NEG_ONE < b < TEN
        frac += 1
        # process digits
        v *= iT(10)
        v += iT(b - ZERO)
        eof(io) && (state[] = EOF; result = scale(-frac, v, 0, row, col); @goto done)
        b = readbyte(io)
    end
    # parse potential exp
    if b == LITTLEE || b == BIGE
        @label parseexp
        eof(io) && (state[] = EOF; result = scale(-frac, v, 0, row, col); @goto done)
        b = readbyte(io)
        exp = zero(iT)
        negativeexp = false
        if b == MINUS
            negativeexp = true
            b = readbyte(io)
        elseif b == PLUS
            b = readbyte(io)
        end
        parseddigits = false
        while NEG_ONE < b < TEN
            parseddigits = true
            # process digits
            exp *= iT(10)
            exp += iT(b - ZERO)
            eof(io) && (state[] = EOF; result = scale(ifelse(negativeexp, -exp, exp) - frac, v, frac, row, col); @goto done)
            b = readbyte(io)
        end
        result = parseddigits ? scale(ifelse(negativeexp, -exp, exp) - frac, v, frac, row, col) : scale(-frac, v, 0, row, col)
    else
        result = scale(-frac, v, 0, row, col)
    end

    @label checkdone
    @checkdone(done)
    @goto checkmissingend

    @label checkmissingend
    @checkmissingend()
    @goto error

    @label done
    return T(ifelse(minussign, -result, result))

    @label missing
    return ifmissing(row, col)

    @label error
    throw(ParsingException(T, b, row, col))
end

# (-73.99378204345703, -73.99378204345702)
#  (-73.95227813720703, -73.95227813720702)
#  (-73.98616027832031, -73.98616027832033)
#  (-74.00163269042969, -74.00163269042967)
#  (-73.96940612792969, -73.9694061279297)
#  (-73.96797943115234, -73.96797943115236)
#  (-73.95426940917969, -73.95426940917967)
#  (-73.97286224365234, -73.97286224365236)
#  (-73.99149322509766, -73.99149322509767)
#  (-73.97639465332031, -73.97639465332033)
#  (-73.97297668457031, -73.97297668457033)
#  (-73.98991394042969, -73.98991394042967)
#  (-73.98424530029297, -73.98424530029298)
#  (-73.97872161865234, -73.97872161865236)
#  (-73.99348449707031, -73.99348449707033)
#  (-73.96598815917969, -73.96598815917967)
#  (-73.98743438720703, -73.98743438720702)


using Base: @kwdef
import Base: show, getproperty

include("convenience_functions.jl")
# aliases
Maybe(T)=Union{T,Nothing}
const F64orFunc = Union{Float64,Function}

### PRIMITIVE VARIABLES ###
@kwdef struct PrimitiveVariables{Nvars}
    names::Union{Symbol,NTuple{Nvars,Symbol}}
    interpolations::Union{Interpolation,NTuple{Nvars,Interpolation}}
    function PrimitiveVariables{Nvars}(names,ips) where {Nvars}
        (typeof(names) == Symbol) && (@assert Nvars == 1)
        (typeof(names) == Tuple) && (@assert Nvars == length(names) == length(ips))
        return new(names,ips)
    end
end

refshape(::Type{Cell{1,N,2}}) where{N} = RefCube
refshape(::Type{Cell{2,N,4}}) where{N} = RefCube
refshape(::Type{Cell{3,N,6}}) where{N} = RefCube
refshape(::Type{Cell{2,N,3}}) where{N} = RefTetrahedron
refshape(::Type{Cell{3,N,4}}) where{N} = RefTetrahedron

function PrimitiveVariables{NV}(names,ip_order,el_geom::Type{Cell{dim,N,M}}) where {NV,dim,N,M}
    NV == 1 && return PrimitiveVariables{NV}(names, (Lagrange{dim,refshape(el_geom),ip_order[1]}(),))
    NV == 2 && return PrimitiveVariables{NV}(names, (Lagrange{dim,refshape(el_geom),ip_order[1]}(),
                                          Lagrange{dim,refshape(el_geom),ip_order[2]}()) )
    @error "More than two primitive variables (:u and :p or :D) not implemented yet"
end

# function Variables(names::Symbol,ips::Interpolation)
#     Variables{1}(names,ips)
# end
# function Variables(names::NTuple{N,Symbol},ips::NTuple{N,Interpolation}) where {N}
#     Variables{N}(names,ips)
# end

### BOUNDARY CONDITIONS ###

@kwdef struct BoundaryConditions
    dirichlet::Dict
    neumann::Maybe(Dict)
end
BoundaryConditions(::Nothing,::Dict) = @error "A minimal set of Dirichlet boundary conditions must be provided in order to prevent rigid motion of the material"

### BODY FORCES ###
struct BodyForces{T}
    components::T
    function BodyForces(c::T) where {T}
        if T <: AbstractVector
            dim = length(c)
            if T <: Vector
                c = Vec{dim}(c)
                new{typeof(c)}(c)
            elseif T <: Tensor
                return new{T}(c)
            end
        elseif T <: Function
            return new{T}(c)
        else
            @error "wrong field type. Must be an AbstractVector"
        end
    end
end

# other types :

include("material_behaviors_types.jl")
include("rheology_type.jl")
include("clock_type.jl")
include("solvers_types.jl")
include("material_state_types.jl")
include("output_writer_types.jl")
include("model_type.jl")

## TODO : Check whether a material parameters type is needed
### MATERIAL PARAMETERS ###

# struct MaterialProperties{T}
#     mp :: Vector{Rheology}
#     function MaterialProperties(mp)
#         T = eltype(mp)
#         new{T}(mp)
#     end
# end
# MaterialProperties{T}(mp) where T = MaterialProperties(mp)
# function Base.getproperty(MP::MaterialProperties,sym::Symbol)
#     if sym == :mp
#         Core.getfield(MP::MaterialProperties,sym::Symbol)
#     else
#         Core.getproperty(Core.getfield(MP,:mp),sym)
#     end
# end
#
# Base.setproperty!(MP::MaterialProperties,prop::Symbol,val) = Core.setproperty!(Core.getfield(MP,:mp),prop,val)
#
# function Base.propertynames(MP::MaterialProperties)
#     properties = Set{Symbol}()
#     for fields in fieldMP.mp
#         push!(properties, propertynames())
#     end
#
# end