#type definitions

mutable struct SampledData
    DF::DataFrame
    VAF::Array{Float64,1}
    counts::Array{Int64,1}
    depth::Array{Int64,1}
end

mutable struct AnalysedData
    DF::DataFrame
    VAF::Array{Float64,1}
end

mutable struct RsqObj
    metric::Float64
    mu::Float64
    pval::Float64
end

mutable struct MetricObj
    metric::Float64
    pval::Float64
end

mutable struct AllMetrics
    rsq::RsqObj
    area::MetricObj
    Dk::MetricObj
    meanD::MetricObj
end

mutable struct Simulation
  input::InputParameters
  output::SimResult
  sampleddata::SampledData
end

mutable struct SimulationM
  input::InputParametersM
  output::SimResultM
  sampleddata::SampledData
end

mutable struct SimulationStemCells
  input::InputParameters
  output::StemCellSimResult
  sampleddata::SampledData
end

###############################################################################

function sampledhist(AF::Array{Float64, 1}, cellnum::Int64; detectionlimit = 0.1, ploidy = 2.0, read_depth = 100.0, cellularity = 1.0)

    AF = AF./ploidy
    AF = AF .* cellularity
    filter!(x -> x > detectionlimit * cellnum, AF)
    samp_percent = read_depth/cellnum
    #depth = rand(Binomial(cellnum,samp_percent), length(AF))
    depth = rand(Poisson(read_depth), length(AF))
    samp_alleles = map((n, p) -> rand(Binomial(n, p)), depth, AF/cellnum)

    VAF = samp_alleles./depth

    #data for histogram
    x = 0.005:0.01:1.005
    y = fit(Histogram, VAF, x, closed=:right)
    DFhist = DataFrame(VAF = x[1:end-1], freq = y.weights)

    SampledData(DFhist, VAF, samp_alleles, depth)
end

function betabinom(p, n, ρ)
    μ = p * n
    shape1 = (μ / n) * ((1 / ρ) - 1)
    shape2 = n * shape1/μ - shape1
    return rand(Binomial(n, rand(Beta(shape1, shape2))))
end

function sampledhist(AF, cellnum, ρ; detectionlimit = 0.1, ploidy = 2.0, read_depth = 100.0, cellularity = 1.0)

    AF = AF./ploidy
    AF = AF .* cellularity
    filter!(x -> x > detectionlimit * cellnum, AF)
    samp_percent = read_depth/cellnum
    #depth = rand(Binomial(cellnum, samp_percent), length(AF))
    depth = rand(Poisson(read_depth), length(AF))
    samp_alleles = map((x, y) -> betabinom(x, y, ρ), AF/cellnum, depth)
    VAF = samp_alleles./depth

    #data for histogram
    x = 0.005:0.01:1.005
    y = fit(Histogram, VAF, x, closed=:right)
    DFhist = DataFrame(VAF = x[1:end-1], freq = y.weights)

    SampledData(DFhist, VAF, samp_alleles, depth)
end

function cumulativedist(sresult; fmin = 0.1, fmax = 0.3)

    VAF = sresult.sampleddata.VAF

    #calculate cumulative sum
    steps = fmax:-0.001:fmin
    cumsum = Array{Int64}(undef, 0)
    v = Array{Float64}(undef, 0)

    for i in steps
        push!(cumsum, sum(VAF .>= i))
        push!(v, i)
    end
    cumsum = cumsum .- cumsum[1]

    DF = DataFrame(cumsum = map(Float64, cumsum), v = v)
    DF[:invf] = 1 ./ DF[:v] .- 1 ./ fmax

    DF[:theory] = Mcdf(DF[:v], fmin, fmax)
    DF[:normalized] = DF[:cumsum] ./ maximum(DF[:cumsum])

    lmfit = fit(LinearModel, @formula(cumsum ~ invf + 0), DF)
    DF[:prediction] = predict(lmfit)

    return AnalysedData(DF, VAF)
end

function Mcdf(f,fmin,fmax)
    (1.0./f .- 1.0/fmax) ./ (1.0/fmin .- 1.0/fmax)
end

function rsq(AD, fmin, fmax, metricp)

    #fit constrained fit
    lmfit = fit(LinearModel, @formula(cumsum ~ invf + 0), AD.DF)
    #calculate R^2 value
    rsqval = 1 .- (sum(residuals(lmfit) .^ 2) ./ sum((AD.DF[:cumsum] .- 0) .^ 2))
    #extract coefficient for mutation rate
    mu = coef(lmfit)[1]
    #get pvalue
    pval = metricp[:pval][searchsortedlast(metricp[:invrsqmetric],1 - rsqval)]

    return RsqObj(rsqval, mu, pval)
end

function kolmogorovD(AD, fmin, fmax, metricp)

    xfiltered = AD.VAF[fmin .< AD.VAF .< fmax]
    n = length(xfiltered)
    cdfs = 1 .- map(x->Mcdf(x, fmin, fmax), sort(xfiltered))
    δp = maximum((1:n) / n - cdfs)
    δn = -minimum((0:n-1) / n - cdfs)
    δ = max(δn, δp)
    pval = metricp[:pval][searchsortedlast(metricp[:Dkmetric], δ)]

    MetricObj(δ, pval)
end

function Draw(DF1, DF2)
    maximum(abs(DF1[:cumsum] .- DF2[:cumsum]))
end

function meanDraw(DF1, DF2)
    #mean(abs(DF1[:cumsum] - DF2[:cumsum]))
    sqrt(sum((DF1[:cumsum] .- DF2[:cumsum]).^2))
end

function kolmogorovDmean(AD, fmin, fmax, metricp)
    D = mean(abs.(convert(Array, AD.DF[:theory]) .- convert(Array, AD.DF[:normalized])))
    pval = metricp[:pval][searchsortedlast(metricp[:meanDmetric], D)]
    return MetricObj(D, pval)
end

function trapz(x::Vector{T}, y::Vector{T}) where T <: Number
    local n = length(x)
    if (length(y) != n)
        error("Vectors 'x', 'y' must be of same length")
    end
    if n == 1; return 0.0; end
    r = 0.0
    for i in 2:n
        r += (x[i] .- x[i-1]) .* (y[i] .+ y[i-1])
    end
    r / 2.0
end

function areametric(AD, fmin, fmax, metricp)
    area = abs.(trapz(convert(Array, AD.DF[:invf]), convert(Array, AD.DF[:normalized])) - trapz(convert(Array, AD.DF[:invf]), convert(Array, AD.DF[:theory])))
    area = area / (1 / fmin - 1 / fmax)
    pval = metricp[:pval][searchsortedlast(metricp[:areametric], area)]
    return MetricObj(area, pval)
end

function areametricraw(AD, DFABC; fmin = 0.12, fmax = 0.8)
    area = abs.(trapz(convert(Array, AD.DF[:v]), convert(Array, AD.DF[:cumsum])) - trapz(convert(Array, AD.DF[:v]), convert(Array, DFABC[:cumsum])))
    area = area / (1 / fmin - 1 / fmax)
    return area
end

"""
    simulate(; <keyword arguments>)

Simulate a stochastic model of tumour growth with a single subclone introduced at a random time and with a random fitness advantage. Output return synthetic sequencing data.
...
## Arguments
- `read_depth = 200.0`: Mean read depth of the target data set
- `detectionlimit = 5/read_depth`: Ability to detect low frequency variants. Assumes 5 reads are needed to call a variant.
- `μ = 10.0`: Mutation rate per division per genome (this will timesed by ploidy for the mutation rate per cell). At each division a Poisson random variable with mean μ is sampled.
- `clonalmutations = 100.0`: Number of clonal mutations present in the first cell.
- `nclones = 1`: Number of subclones introduced
- `Nmax = 10^4`: Maximum population size.
- `ρ = 0.0`: Overdispersion parameter for beta-binomial model of sequencing data. ρ = 0.0 means model is binomial sampling
- `timefunction = exptime`: Function for KMC algorithm timestep. exptime returns an exponentially distributed random variable, if you would rather return the mean of the distribution define a function that returns 1. ie `returnone() = 1`.
- `ploidy = 2`: ploidy of the genome
- `d = 0.0`: Death rate of the thost population in the tumour
- `b = log(2)`: Birth rate of the population. Set to `log(2)` so that tumour doubles with each unit increase in t in the absence of cell death.
- `fixedmu = false`: If set to false number of mutations per division is fixed and not sampled from a poisson distribution.
...
"""
function simulate(; nclones = 1, ploidy = 2, read_depth = 100.0, detectionlimit = 5 ./ read_depth, clonalmutations = 100.0, μ = 10.0, d = 0.0, b = log(2), ρ = 0.0, Nmax = 10^4, s = repeat([1.0], inner = nclones), tevent = collect(1.0:0.5:100.0)[1:nclones], cellularity = 1.0, fixedmu = false, timefunction::Function = exptime, maxclonefreq = 200)

    nclones == length(s) || error("Number of clones is $(nclones), size of selection coefficient array is $(length(s)), these must be the same size ")
    nclones == length(tevent) || error("Number of clones is $(nclones), size of selection coefficient array is $(length(tevent)), these must be the same size ")
    IP = InputParameters(nclones,
    Nmax,
    detectionlimit,
    ploidy,
    read_depth,
    clonalmutations,
    s,
    (μ/2) * ploidy,
    b,
    d,
    tevent,
    ρ,
    cellularity,
    fixedmu,
    timefunction,
    maxclonefreq)

    #get simulation data
    simresult, IP = run1simulation(IP, 0.0, 1.0)

    #get sampled VAFs
    if IP.ρ > 0.0
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, IP.ρ, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    else
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    end

    return Simulation(IP, simresult, sampleddata)
end

"""
    simulate(minclonesize, maxclonesize; <keyword arguments>)

Return simulation with frequency of subclones >minclones & <maxclonesize.
"""
function simulate(minclonesize, maxclonesize; nclones = 1, ploidy = 2, read_depth = 100.0, detectionlimit = 5 ./read_depth, clonalmutations = 100.0, μ = 10.0, d = 0.0, b = log(2), ρ = 0.0, Nmax = 10^4, cellularity = 1.0, fixedmu = false, tmin = 6.0, tmax = 20.0, smin = 0.0, smax = 25.0, timefunction::Function = exptime, maxclonefreq = 200)

    correctnc = false
    IP, simresult = 0, 0

    while correctnc == false
      freq = sort(rand(Uniform(minclonesize, maxclonesize), nclones), rev = true)
      tevent = sort(rand(Uniform(tmin, log(Nmax)/log(2)), nclones))
      if nclones == 1
          s1 = selection(log(2), minclonesize, log(Nmax)/log(2), tevent[1])
          s2 = selection(log(2), maxclonesize, log(Nmax)/log(2), tevent[1])
          s = rand(Uniform(s1, 3*s2), nclones) #increasing this s2 makes the distribution of clone sizes more uniform
      elseif nclones == 2
          s1, s2 = selection2clone(log(2), freq[1], freq[2], log(Nmax)/log(2), tevent[1], tevent[2])
          sm = maximum([s1, s2])
          s = rand(Uniform(s1[1], 3*sm[1]), nclones)
      else
          s = rand(Uniform(smin, smax), nclones)
      end

      IP = InputParameters(nclones,
      Nmax,
      detectionlimit,
      ploidy,
      read_depth,
      clonalmutations,
      s,
      (μ/2) * ploidy, #divide by 2 because both daughter cells can accumulate mutations so this ensures the *per division* mutation rate is accurate
      b,
      d,
      tevent,
      ρ,
      cellularity,
      fixedmu,
      timefunction,
      maxclonefreq)

      simresult, IP = run1simulation(IP, minclonesize, maxclonesize)

      nctemp = IP.numclones
      if nctemp == nclones
          correctnc = true
      end
    end
    #println(i)
    #get sampled VAFs
    if IP.ρ > 0.0
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, IP.ρ, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    else
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    end

    return Simulation(IP, simresult, sampleddata)
end

"""
    simulate(minclonesize, maxclonesize, independentclones::Bool; <keyword arguments>)

Return simulation with frequency of subclones >minclones & <maxclonesize and specify whether subclones are independent or not (ie nested or not). Only applicable to >1 subclone.
"""
function simulate(minclonesize, maxclonesize, independentclones::Bool; nclones = 1, ploidy = 2, read_depth = 100.0, detectionlimit = 5 ./read_depth, clonalmutations = 100.0, μ = 10.0, d = 0.0, b = log(2), ρ = 0.0, Nmax = 10^4, cellularity = 1.0, fixedmu = false, tmin = 3.0, tmax = 20.0, smin = 0.0, smax = 25.0, timefunction::Function = exptime, maxclonefreq = 200)

  ct = 1
  x = 0.0

  while (ct >= 1)
    x = simulate(minclonesize, maxclonesize; nclones = nclones, ploidy = ploidy, read_depth = read_depth, detectionlimit = detectionlimit, clonalmutations = clonalmutations, μ = μ, d = d, b = b, ρ = ρ, Nmax = Nmax, cellularity = cellularity, fixedmu = fixedmu, tmin = tmin, tmax = tmax, smin = smin, smax = smax, timefunction = timefunction, maxclonefreq = maxclonefreq)
    ct = sum(x.output.clonetype)
  end

  return x
end

"""
    simulate(minclonesize, maxclonesize, mindiff; <keyword arguments>)

Return simulation with frequency of subclones >minclones & <maxclonesize and subclone frequency are at least mindiff apart.
"""
function simulate(minclonesize, maxclonesize, mindiff::Float64; nclones = 1, ploidy = 2, read_depth = 100.0, detectionlimit = 5 ./read_depth, clonalmutations = 100.0, μ = 10.0, d = 0.0, b = log(2), ρ = 0.0, Nmax = 10^4, cellularity = 1.0, fixedmu = false, tmin = 3.0, tmax = 20.0, smin = 0.0, smax = 25.0, timefunction::Function = exptime, maxclonefreq = maxclonefreq)

  nclones == 2 || error("nclones must be = 2 for this method as it is a function to simulate until we arrive at 2 clones that are greater than mindiff apart")

  ct = 1
  x = 0.0
  freqdiff = false

  while (freqdiff == false)
    x = simulate(minclonesize, maxclonesize; nclones = nclones, ploidy = ploidy, read_depth = read_depth, detectionlimit = detectionlimit, clonalmutations = clonalmutations, μ = μ, d = d, b = b, ρ = ρ, Nmax = Nmax, cellularity = cellularity, fixedmu = fixedmu, tmin = tmin, tmax = tmax, smin = smin, smax = smax, timefunction = timefunction, maxclonefreq = maxclonefreq)
    if abs(x.output.clonefreq[2] - x.output.clonefreq[1]) > mindiff
      freqdiff = true
    end
  end

  return x
end

"""
    simulate(minclonesize, maxclonesize, mindiff; <keyword arguments>)

Return simulation with frequency of subclones >minclones & <maxclonesize and subclone frequency are at least mindiff and have at least minmutations mutations.
"""
function simulate(minclonesize, maxclonesize, mindiff::Float64, minmutations::Int64; nclones = 1, ploidy = 2, read_depth = 100.0, detectionlimit = 5 ./read_depth, clonalmutations = 100.0, μ = 10.0, d = 0.0, b = log(2), ρ = 0.0, Nmax = 10^4, cellularity = 1.0, fixedmu = false, tmin = 3.0, tmax = 20.0, smin = 0.0, smax = 25.0, timefunction::Function = exptime, maxclonefreq = maxclonefreq)

  nclones == 2 || error("nclones must be = 2 for this method as it is a function to simulate until we arrive at 2 clones that are greater than mindiff apart")

  ct = 1
  x = 0.0
  savesim = false

  while (savesim == false)
    x = simulate(minclonesize, maxclonesize, mindiff; nclones = nclones, ploidy = ploidy, read_depth = read_depth, detectionlimit = detectionlimit, clonalmutations = clonalmutations, μ = μ, d = d, b = b, ρ = ρ, Nmax = Nmax, cellularity = cellularity, fixedmu = fixedmu, tmin = tmin, tmax = tmax, smin = smin, smax = smax, timefunction = timefunction, maxclonefreq = maxclonefreq)
    if sum(x.output.subclonemutations .> minmutations) == 2
      savesim = true
    end
  end

  return x
end

function getmetrics(AD, metricp; fmin = 0.1, fmax = 0.3)

    rsq1 = rsq(AD, fmin, fmax, metricp)
    Dk = kolmogorovD(AD, fmin, fmax, metricp)
    meanD = kolmogorovDmean(AD, fmin, fmax, metricp)
    area = areametric(AD, fmin, fmax, metricp)

    return(AllMetrics(rsq1, area, Dk, meanD))
end

"""
    simulatestemcells(; <keyword arguments>)

Simulate a stochastic model of tumour growth with a stem cell architecture. Output return synthetic sequencing data.
...
## Arguments
- `α = 0.1`: Symmetric division rate
- `maxdivisions = 5`: Maximum number of divisions of differentiated cells.
- `read_depth = 200.0`: Mean read depth of the target data set
- `detectionlimit = 5/read_depth`: Ability to detect low frequency variants. Assumes 5 reads are needed to call a variant.
- `μ = 10.0`: Mutation rate per division. At each division a Poisson random variable with mean μ is sampled.
- `clonalmutations = 100.0`: Number of clonal mutations present in the first cell.
- `Nmax = 10^4`: Maximum population size.
- `ρ = 0.0`: Overdispersion parameter for beta-binomial model of sequencing data. ρ = 0.0 means model is binomial sampling
- `timefunction = exptime`: Function for KMC algorithm timestep. exptime returns an exponentially distributed random variable, if you would rather return the mean of the distribution define a function that returns 1. ie `returnone() = 1`.
- `ploidy = 2`: ploidy of the genome
- `d = 0.0`: Death rate of the thost population in the tumour
- `b = log(2)`: Birth rate of the population. Set to `log(2)` so that tumour doubles with each unit increase in t in the absence of cell death.
- `fixedmu = false`: If set to false number of mutations per division is fixed and not sampled from a poisson distribution.
...
"""
function simulatestemcells(;ploidy = 2, α = 0.1, maxdivisions = 5, read_depth = 100.0, nclones = 0, detectionlimit = 5 ./read_depth, clonalmutations = 100.0, μ = 10.0, d = 0.0, b = log(2), ρ = 0.0, Nmax = 10^4, s = repeat([1.0], inner = nclones), tevent = collect(1.0:0.5:100.0)[1:nclones], cellularity = 1.0, fixedmu = false, timefunction::Function = exptime)

    nclones == length(s) || error("Number of clones is $(nclones), size of selection coefficient array is $(length(s)), these must be the same size ")

    nclones == length(tevent) || error("Number of clones is $(nclones), size of selection coefficient array is $(length(tevent)), these must be the same size ")

    IP = InputParameters(nclones,
    Nmax,
    detectionlimit,
    ploidy,
    read_depth,
    clonalmutations,
    s,
    μ,
    b,
    d,
    tevent,
    ρ,
    cellularity,
    fixedmu,
    timefunction)

    #get simulation data
    simresult = run1simulationstem(Nmax; α = α, maxdivisions = maxdivisions, d = d, μ = μ, clonalmutations = clonalmutations)

    #get sampled VAFs
    if IP.ρ > 0.0
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, IP.ρ, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    else
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    end

    return SimulationStemCells(IP, simresult, sampleddata)
end

function simulatedifferentmutations(;ploidy = 2, read_depth = 100.0, detectionlimit = 5 ./read_depth, clonalmutations = 100.0, μp = 10.0, μd = 0.001, μneg = 0.0, d = 0.0, b = log(2), ρ = 0.0, Nmax = 10^4, cellularity = 1.0, timefunction::Function = exptime, s = sfunc, fitnessfunc = nonmultiplicativefitness)

    IP = InputParametersM(
    Nmax,
    detectionlimit,
    ploidy,
    read_depth,
    clonalmutations,
    μp,
    μd,
    μneg,
    b,
    d,
    ρ,
    cellularity,
    s,
    timefunction,
    fitnessfunc)

    #get simulation data
    simresult, IP = run1simulation(IP)

    #get sampled VAFs
    if IP.ρ > 0.0
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, IP.ρ, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    else
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    end

    return SimulationM(IP, simresult, sampleddata)
end

function simulatedifferentmutationsmoran(tmax; ploidy = 2, read_depth = 100.0, detectionlimit = 5 ./read_depth, clonalmutations = 100.0, μp = 10.0, μd = 0.001, μneg = 0.0, d = 0.0, b = log(2), ρ = 0.0, Nmax = 10^4, cellularity = 1.0, timefunction::Function = exptime, s = sfunc, fitnessfunc = nonmultiplicativefitness)

    IP = InputParametersM(
    Nmax,
    detectionlimit,
    ploidy,
    read_depth,
    clonalmutations,
    μp,
    μd,
    μneg,
    b,
    d,
    ρ,
    cellularity,
    s,
    timefunction,
    nonmultiplicativefitness)

    #get simulation data
    simresult, IP = run1simulation(IP, tmax)

    #get sampled VAFs
    if IP.ρ > 0.0
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, IP.ρ, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    else
        sampleddata = sampledhist(simresult.trueVAF, IP.Nmax, detectionlimit = IP.detectionlimit, ploidy = IP.ploidy, read_depth = IP.read_depth, cellularity = IP.cellularity)
    end

    return SimulationM(IP, simresult, sampleddata)
end

function getsummary(inandout; sname = "", fmin = 0.1, fmax = 0.3)

  simresult = inandout.output
  IP = inandout.input
  sampleddata = inandout.sampleddata
  #get cumulativedistributions
  AD = cumulativedist(inandout, fmin = fmin, fmax = fmax)
  allmetrics = getmetrics(AD, metricp, fmin = fmin, fmax = fmax)
  fitout = rsq(AD, fmin, fmax, metricp)

  DF = DataFrame(
  sname = sname,
  Nmax = IP.Nmax,
  numclones = IP.numclones,
  Nevent = join(simresult.cloneN, ","),
  teventin = join(IP.tevent, ","),
  s = join(IP.selection, ","),
  clonefreq = join(simresult.clonefreq, ","),
  teventtrue = join(simresult.clonetime, ","),
  subclonemutations = join(simresult.subclonemutations, ","),
  clonetype = join(simresult.clonetype, ","),
  br = join(simresult.birthrates, ","),
  dr = join(simresult.deathrates, ","),
  mu = IP.μ,
  muout = fitout.mu,
  b = IP.b,
  d = IP.d,
  tend = simresult.tend,
  clonalmutations = IP.clonalmutations,
  area = allmetrics.area.metric,
  area_pval = allmetrics.area.pval,
  Dk = allmetrics.Dk.metric,
  Dk_pval = allmetrics.Dk.pval,
  meanD = allmetrics.meanD.metric,
  meanD_pval = allmetrics.meanD.pval,
  rsq = allmetrics.rsq.metric,
  rsq_pval = allmetrics.rsq.pval,
  fmin = fmin,
  fmax = fmax,
  detectionlimit = IP.detectionlimit,
  ploidy = IP.ploidy,
  read_depth = IP.read_depth,
  num_muts = AD.DF[:cumsum][end]
  )

  return DF

end

function getsummary(inandout::SimulationStemCells; sname = "", fmin = 0.1, fmax = 0.3)

  simresult = inandout.output
  IP = inandout.input
  sampleddata = inandout.sampleddata
  #get cumulativedistributions
  AD = cumulativedist(inandout, fmin = fmin, fmax = fmax)
  allmetrics = getmetrics(AD, metricp, fmin = fmin, fmax = fmax)
  fitout = rsq(AD, fmin, fmax, metricp)

  DF = DataFrame(
  sname = sname,
  Nmax = IP.Nmax,
  numclones = IP.numclones,
  mu = IP.μ,
  muout = fitout.mu,
  clonalmutations = IP.clonalmutations,
  area = allmetrics.area.metric,
  area_pval = allmetrics.area.pval,
  Dk = allmetrics.Dk.metric,
  Dk_pval = allmetrics.Dk.pval,
  meanD = allmetrics.meanD.metric,
  meanD_pval = allmetrics.meanD.pval,
  rsq = allmetrics.rsq.metric,
  rsq_pval = allmetrics.rsq.pval,
  fmin = fmin,
  fmax = fmax,
  detectionlimit = IP.detectionlimit,
  ploidy = IP.ploidy,
  read_depth = IP.read_depth,
  num_muts = AD.DF[:cumsum][end]
  )

  return DF

end


#=

# 420: Discontinuous Quantities
 ([source code](SOURCE_URL))

Test  jumping species and quantity handling

=#
module Example420_DiscontinuousQuantities
using Printf
using VoronoiFVM
using SparseArrays
using ExtendableGrids
using GridVisualize
using LinearAlgebra

function main(;N=5, Plotter=nothing,unknown_storage=:sparse)
    XX=collect(0:0.1:1)
    xcoord=XX
    for i=1:N-1
	xcoord=glue(xcoord,XX.+i)
    end
    grid2=simplexgrid(xcoord)
    for i=1:N
	cellmask!(grid2,[i-1],[i],i)
    end
    for i=1:N-1
	bfacemask!(grid2,[i],[i],i+2)
    end

    params=zeros(2,num_cellregions(grid2))
    for i=1:num_cellregions(grid2)
        params[1,i]=i
        params[2,i]=10*i
    end

    system=VoronoiFVM.System(grid2,unknown_storage=unknown_storage)

    ## First, we introduce a continuous quantity which we name "cspec". Note that the "species number" can be assigned automatically if not given explicitely.
    cspec=ContinuousQuantity(system,1:N;ispec=1,id=1)

    ## A discontinuous quantity can be introduced as well. by default, each reagion gets a new species number. This can be overwritten by the user.
    dspec=DiscontinuousQuantity(system,1:N; regionspec=[2+i%2 for i=1:N],id=2)

    # check 1D array acces with quantities
    carrierList = [cspec dspec]
    numberCarriers = length(carrierList)

    params2=zeros(1, numberCarriers)

    for icc ∈ carrierList
        params2[icc] = 2
    end

    for i=1:numberCarriers
        @assert params2[i] == 2
    end

    # check 2D array acces with quantities
    for i=1:num_cellregions(grid2)
        @assert params[cspec,i] == i
        @assert params[dspec,i] == 10*i
    end

    for i=1:num_cellregions(grid2)
        params[cspec,i] = -i
        params[dspec,i] = -10*i
    end

    for i=1:num_cellregions(grid2)
        @assert params[1,i] == -i
        @assert params[2,i] == -10*i
    end

    ##For both quantities, we define simple diffusion fluxes:

    function flux(f,u,edge)
	f[dspec]=u[dspec,1]-u[dspec,2]
	f[cspec]=u[cspec,1]-u[cspec,2]
    end

    d1=1
    q1=0.2

    function breaction(f,u,bnode)

        # left outer boundary value for dspec
        if bnode.region == 1
            f[dspec] = u[dspec] + 0.5
        end

        ## Define a thin layer inteface condition for `dspec` and an interface source for `cspec`.
        if bnode.region>2
            react=(u[dspec,1]-u[dspec,2])/d1
            f[dspec,1]= react
            f[dspec,2]= -react
            f[cspec]=-q1*u[cspec]
        end
    end

    physics!(system,VoronoiFVM.Physics(
        flux=flux,
        breaction=breaction
    ))

    ## Set boundary conditions
    boundary_dirichlet!(system,dspec,2,0.1)
    boundary_dirichlet!(system,cspec,1,0.1)
    boundary_dirichlet!(system,cspec,2,1.0)
    subgrids=VoronoiFVM.subgrids(dspec,system)

    U=solve(unknowns(system,inival=0),system)

    dvws=VoronoiFVM.views(U,dspec,subgrids,system)
    cvws=VoronoiFVM.views(U,cspec,subgrids,system)
    vis=GridVisualizer(resolution=(600,300),Plotter=Plotter)
    for i=1:length(dvws)
	scalarplot!(vis,subgrids[i],dvws[i],flimits=(-0.5,1.5),clear=false,color=:red)
	scalarplot!(vis,subgrids[i],cvws[i],flimits=(-0.5,1.5),clear=false,color=:green)
    end
    reveal(vis)
    norm(system,U,2)
end

function test()
    testval=7.812799873197911
    main(unknown_storage=:sparse) ≈ testval &&
        main(unknown_storage=:dense) ≈ testval
end

end


using MDPTools, NestedFixedPoint
using DataFrames

# 1 Data
df = readtable(Pkg.dir("StructuralBase"*"/data/data1234.csv"))
n = 90
omax = 450000
Xval = linspace(0, omax/1000, n)
x = ceil(df[:odo]*n/omax)
data = DataFrame()
data[:a] = df[:replace]
data[:x] = x
data[:dx] = x[1:end]-[0;x[1:end-1]]
for i = eachindex(data[:dx])
    data[:dx][i] = data[:dx][i]*(1-data[:a][i])+data[:x][i]*data[:a][i]
end
remove_first_row_index=df[:id]-[0;df[:id][1:end-1]]
data=data[remove_first_row_index .== 0,:]
data[:a]=[data[:a][2:end];0]
maxi = Int64(maximum(data[:dx]))

# 2 Model
#function transition(p)

p=zeros(maxi+1);
for i = 1:1:maxi+1
    p[i]=sum(data[:dx] .== i-1)/length(data[:dx])
end

F = zeros(n, n)
for i = 1:n-length(p)+1
    F[i,i:i+length(p)-1] = p
end
for i=1:length(p)-1
    F[end-length(p)+i+1,end-length(p)+i+1:end]=[p[1:end-i-1];sum(p[end-i:end])]
end
F = [F, F[ones(Int64, n),:]]

S = State(Xval, F)

Z1 = [zeros(n) -0.001*Xval]
Z2 = [-ones(n) zeros(n)]

U = LinearUtility([Z1, Z2], 0.9999, [11., 3.])

a = vec(round(Int64,data[:a]))
x = vec(round(Int64,data[:x]))

D = Data(a, a+1, x)
U.Θ*=0
rust_nfxp = fit_nfxp(U, S, D; verbose = false);


using ForwardDiff
using ForwardDiff: Dual

seed(x::Real, ::Val) = Dual(x, true)

function seed(x, ::Val{N}, offset = 0) where N
  map(x, reshape(1:length(x), size(x))) do x, i
    Dual(x, ntuple(j -> j+offset == i, Val(N)))
  end
end

extract(x::ForwardDiff.Dual) = x.value, [x.partials...]

function extract(xs::AbstractArray{ForwardDiff.Dual{T,V,N}}) where {T,V,N}
  J = similar(xs, V, N, length(xs))
  for i = 1:length(xs), j = 1:N
    J[j, i] = xs[i].partials.values[j]
  end
  return map(x -> x.value, xs), J
end

function forward_jacobian(f, x, ::Val{N}) where N
  y, _J = extract(f(seed(x, Val(N))))
  J = similar(_J, length(x), length(y))
  J[1:N,:] = _J
  offset = 0
  while offset + N < length(x)
    offset += N
    _, _J = extract(f(seed(x, Val(N), offset)))
    range = (1+offset):min(N+offset,length(x))
    J[range,:] = @view _J[range.-offset,:]
  end
  return y, J
end

function forward_jacobian(f, x)
  if length(x) < ForwardDiff.DEFAULT_CHUNK_THRESHOLD
    forward_jacobian(f, x, Val(length(x)))
  else
    forward_jacobian(f, x, Val(ForwardDiff.DEFAULT_CHUNK_THRESHOLD))
  end
end

vec_scalar(x) = vec(x)
vec_scalar(x::Real) = [x]
reshape_scalar(x, y) = reshape(y, size(x))
reshape_scalar(x::Real, y) = y[]

forwarddiff(f, x) = f(x)

@adjoint function forwarddiff(f, x)
  y, J = forward_jacobian(f, x)
  return y, ȳ -> (nothing, reshape_scalar(x, J*vec_scalar(ȳ)))
end


using QuantEcon
import QuantEcon: simulate

mutable struct Model{TF <: AbstractFloat,
                    TM <: AbstractMatrix{TF},
                    TV <: AbstractVector{TF}}
    β::TF
    Π::TM
    G::TV
    Θ::TV
    transfers::Bool
    U::Function
    Uc::Function
    Ucc::Function
    Un::Function
    Unn::Function
    n_less_than_one::Bool
end


using SolitonDistribution
using StatsBase, Statistics, Distributions
using Test

@testset "SolitonDistribution.jl" begin
    K, M, δ, atol = 100, 60, 0.2, 0
    Ω = Soliton(K, M, δ, atol)
    @test pdf(Ω, M) > pdf(Ω, M-1)
    @test pdf(Ω, M) > pdf(Ω, M+1)
    @test cumsum(pdf.(Ω, 1:K)) ≈ cdf.(Ω, 1:K)
    @test cdf(Ω, 0) ≈ 0
    @test cdf(Ω, K) ≈ 1
    @test mean(Ω) ≈ 7.448826535558562
    @test var(Ω) ≈ 178.91717915136957
    @test minimum(Ω) == 1
    @test maximum(Ω) == K
    @test quantile(Ω, 0) == 1
    @test quantile(Ω, 1) == K
    @test insupport(Ω, 1) && insupport(Ω, 2) && insupport(Ω, K)
    @test !insupport(Ω, 0) && !insupport(Ω, 2.1) && !insupport(Ω, K + 1)

    K, M, δ, atol = 100, 60, 0.2, 1e-3
    Ω = Soliton(K, M, δ, atol)
    ds = [d for d in 1:K if pdf(Ω, d) > 0]
    @test all(pdf.(Ω, ds) .> atol)
    @test cdf(Ω, 0) ≈ 0
    @test cdf(Ω, K) ≈ 1
    @test minimum(Ω) == 1
    @test maximum(Ω) == K
    @test quantile(Ω, 0) == 1
    @test quantile(Ω, 1) == M
end


function calculate_transcriptional_control_variable(W::Array{Float64,1}, binding_rate::Float64, order_param::Float64, frac_bound::Float64)::Float64

    # initialize
    v_variable = 0.0
    v_num = 0.0
    Z = 0.0

    # define states involved in activity
    all_states = [0,1,2]
    activity_states = [1,2]
    f_dependent_states = [2]

    # rename parameters
    ni = order_param
    Ki = binding_rate
    x = frac_bound

    # v-variable function
    for (index, value) ∈ enumerate(all_states)
        Wf = W[index]

        if value ∈ f_dependent_states
            Wf *= (x/Ki)^ni / (1 + (x/Ki)^ni)
        end

        if value ∈ activity_states
            v_num += Wf
        end

        Z += Wf
    end

    v_variable = v_num / Z

    # return
    return v_variable
end


using StanSample, Test

#set_cmdstan_home!(homedir() * "/Projects/StanSupport/cmdstan_stanc3")

ProjDir = @__DIR__
cd(ProjDir) # do

  bernoulli_model = "
    functions{
      #include model_specific_funcs.stan
      #include shared_funcs.stan // a comment
      //#include shared_funcs.stan // a comment
    }
    data {
      int<lower=1> N;
      int<lower=0,upper=1> y[N];
    }
    parameters {
      real<lower=0,upper=1> theta;
    }
    model {
      model_specific_function();
      theta ~ beta(my_function(),1);
      y ~ bernoulli(theta);
    }
  ";

  sm = SampleModel("bernoulli", bernoulli_model)
  observeddata = Dict("N" => 10, "y" => [0, 1, 0, 1, 0, 0, 0, 0, 0, 1])
  rc = stan_sample(sm; data=observeddata)

  if success(rc)
    samples = read_samples(sm, :array)
    @test sum(samples)/length(samples) ≈ 0.33 rtol=0.05
  end

#end


export quadratic_estimate

"""

    quadratic_estimate(ds::DataSet, which; wiener_filtered=true)
    quadratic_estimate((ds₁::DataSet, ds₂::DataSet), which; wiener_filtered=true)

Compute the quadratic estimate of `ϕ` given data.

The `ds` or `(ds₁,ds₂)` tuple contain the DataSet object(s) which house the
data and covariances used in the estimate. Note that only the Fourier-diagonal
approximations for the beam, mask, and noise, i.e. `B̂`, `M̂`, and
`Cn̂`, are accounted for. To account full operators (if they are not actually
Fourier-diagonal), you should compute the impact using Monte Carlo.

If a tuple is passed in, the result will come from correlating the data from
`ds₁` with that from `ds₂`.

An optional keyword argument `AL` can be passed in case the QE normalization
was already computed, in which case it won't be recomputed during the
calculation.

Returns a named tuple of `(;ϕqe, AL, Nϕ)` where `ϕqe` is the (possibly Wiener
filtered, depending on `wiener_filtered` option) quadratic estimate, `AL` is the
normalization (which is already applied to ϕqe, it does not need to be applied
again), and `Nϕ` is the analytic N⁰ noise bias (Nϕ==AL if using unlensed
weights, currently only Nϕ==AL is always returned, no matter the weights)
"""
function quadratic_estimate(
    (ds₁,ds₂) :: NTuple{2,DataSet},
    which = nothing;
    wiener_filtered = true,
    AL = nothing,
    weights = :unlensed
)
    @assert weights in (:lensed, :unlensed) "weights should be :lensed or :unlensed"
    if isnothing(which)
        which = ds₁.d isa FlatS0 ? :TT : :EB
    end
    @assert (which in [:TT, :EE, :EB]) "which='$which' not implemented"
    @assert (ds₁.Cf===ds₂.Cf && ds₁.Cf̃===ds₂.Cf̃ && ds₁.Cn̂===ds₂.Cn̂ && ds₁.Cϕ===ds₂.Cϕ && ds₁.B̂===ds₂.B̂) "operators in `ds₁` and `ds₂` should be the same"
    @unpack Cf, Cf̃, Cn̂, Cϕ, B̂, M̂ = ds₁()
    pol = which == :TT ? :I : :P
    quadratic_estimate(Val(which), (ds₁.d[pol], ds₂.d[pol]), Cf[pol], Cf̃[pol], Cn̂[pol], Cϕ, (M̂*B̂)[pol], wiener_filtered, weights, AL)
end

quadratic_estimate(ds::DataSet, args...; kwargs...) = quadratic_estimate((ds,ds), args...; kwargs...)

@doc doc"""

    QE_leg(C::Diagonal, inds...)

The quadratic estimate and normalization expressions all consist of
terms involving products of two "legs", each leg which look like:

    C * l[i] * l̂[j] * l̂[k] * ...

where C is some field or diagonal covariance, l[i] is the Fourier
wave-vector in direction i (for i=1:2), and l̂[i] = l[i]/‖l‖. For
example, there's a leg in the EB estimator that looks like:

    (CE * (CẼ+Cn) \ d[:E])) * l[i] * l̂[j] * l̂[k]

The function `QE_leg` computes quatities like these, e.g. the above
would be given by:

    QE_leg((CE * (CẼ+Cn) \ d[:E])), [i], j, k)

(where note that specifying whether its the Fourier wave-vector l
instead of the unit-vector l̂ is done by putting that index in
brackets).

Additionally, all of these terms are symmetric in their indices, i.e.
in `(i,j,k)` in this case. The `QE_leg` function is smart about this,
and is memoized so that each unique set of indices is only computed
once. This leads to a pretty drastic speedup for terms with many
indices like those that arize in the EE and EB normalizations, and
lets us write code which is both clear and fast without having to
think too hard about these symmetries.

"""
QE_leg(C::Diagonal, inds...) = QE_leg(C.diag, inds...)
function QE_leg(C::Field, inds...)
    n = count((x->x isa Int),inds)
    p₁,p₂ = (count(==(i), first.(inds)) for i=1:2)
    QE_leg(C, (n, p₁, p₂))
end
@memoize function QE_leg(C::Field, (n, p₁, p₂)::Tuple)
    Map(@. nan2zero($Ð(C) * ∇[1].diag^p₁ * ∇[2].diag^p₂ / sqrt(∇².diag)^n))
end
ϵ(x...) = levicivita([x...])
inds(D) = collect(product(repeated(1:2,D)...))[:]

function quadratic_estimate(::Val{:TT}, (d₁,d₂)::NTuple{2,FlatS0}, Cf, Cf̃, Cn, Cϕ, TF, wiener_filtered, weights, AL=nothing)

    ΣTtot = TF^2 * Cf̃ + Cn
    CT = (weights==:unlensed) ? Cf : Cf̃

    # unnormalized estimate
    ϕqe_unnormalized = @subst -sum(∇[i] * Fourier(QE_leg($(ΣTtot\(TF*d₁))) * QE_leg($(CT*(ΣTtot\(TF*d₂))), [i])) for i=1:2)

    # normalization
    if AL == nothing
        AL = @subst begin
            A(i,j) = (
                QE_leg($(TF^2 * CT^2 / ΣTtot), [i], [j]) * QE_leg($(TF^2      / ΣTtot)     )
              + QE_leg($(TF^2 * CT   / ΣTtot), [i]     ) * QE_leg($(TF^2 * CT / ΣTtot), [j])
            )
            pinv(Diagonal(sum(∇[i].diag .* ∇[j].diag .* Fourier(A(i,j)) for (i,j) in inds(2))))
        end
    end
    Nϕ = AL # true only for unlensed weights

    Memoization.empty_cache!(QE_leg)

    ϕqe = (wiener_filtered ? (Cϕ*pinv(Cϕ+Nϕ)) : 1) * (AL*ϕqe_unnormalized)
    (;ϕqe, AL, Nϕ)

end

function quadratic_estimate(::Val{:EE}, (d₁,d₂)::NTuple{2,FlatS2}, Cf, Cf̃, Cn, Cϕ, TF, wiener_filtered, weights, AL=nothing)

    TF² = TF[:E]^2
    ΣEtot = TF² * Cf̃[:E] + Cn[:E]
    CE = ((weights==:unlensed) ? Cf : Cf̃)[:E]

    # unnormalized estimate
    ϕqe_unnormalized = @subst begin
        I(i) = -(
            2sum(QE_leg($(CE * (ΣEtot \ (TF*d₁)[:E])), [i], j, k) * QE_leg($((ΣEtot \ (TF*d₂)[:E])), j, k) for (j,k) in inds(2))
               - QE_leg($(CE * (ΣEtot \ (TF*d₁)[:E])), [i]      ) * QE_leg($((ΣEtot \ (TF*d₂)[:E]))      )
        )
        sum(∇[i] * Fourier(I(i)) for i=1:2)
    end

    # normalization
    if AL == nothing
        AL = @subst begin
            A1(i,j) = -4 * sum( ϵ(m,p,3) * ϵ(n,q,3) * (
                  QE_leg($(TF² * CE^2 / ΣEtot), [i], [j], k, l, m, n) .* QE_leg($(TF²      / ΣEtot),      k, l, p, q)
                + QE_leg($(TF² * CE   / ΣEtot), [i],      k, l, m, n) .* QE_leg($(TF² * CE / ΣEtot), [j], k, l, p, q))
                for (k,l,m,n,p,q) in inds(6)
            )
            A2(i,j) = (
                  QE_leg($(TF² * CE^2 / ΣEtot), [i], [j]) .* QE_leg($(TF²      / ΣEtot)     )
                + QE_leg($(TF² * CE   / ΣEtot), [i]     ) .* QE_leg($(TF² * CE / ΣEtot), [j])
            )
            pinv(Diagonal(sum(∇[i].diag .* ∇[j].diag .* Fourier(A1(i,j) + A2(i,j)) for (i,j) in inds(2))))
        end
    end
    Nϕ = AL # true only for unlensed weights

    Memoization.empty_cache!(QE_leg)

    ϕqe = (wiener_filtered ? (Cϕ*pinv(Cϕ+Nϕ)) : 1) * (AL*ϕqe_unnormalized)
    (;ϕqe, AL, Nϕ)

end

function quadratic_estimate(::Val{:EB}, (d₁,d₂)::NTuple{2,FlatS2}, Cf, Cf̃, Cn, Cϕ, TF, wiener_filtered, weights, AL=nothing; zeroB=false)

    CE, CB = getindex.(Ref((weights==:unlensed) ? Cf : Cf̃),(:E,:B))
    TF²E, TF²B = TF[:E]^2, TF[:B]^2
    ΣEtot = TF²E * Cf̃[:E] + Cn[:E]
    ΣBtot = TF²B * Cf̃[:B] + Cn[:B]

    # unnormalized estimate
    ϕqe_unnormalized = @subst begin
        I(i) = 2 * sum(  ϵ(k,l,3) * (
                           QE_leg($(CE * (ΣEtot \ (TF*d₁)[:E])), [i], j, k) * QE_leg($(     (ΣBtot \ (TF*d₂)[:B])),      j, l)
            - (zeroB ? 0 : QE_leg($(      ΣEtot \ (TF*d₁)[:E]),       j, k) * QE_leg($(CB * (ΣBtot \ (TF*d₂)[:B])), [i], j, l)))
            for (j,k,l) in inds(3)
        )
        sum(∇[i] * Fourier(I(i)) for i=1:2)
    end

    # normalization
    if AL == nothing
        AL = @subst begin
            A(i,j) = 4 * sum( ϵ(m,p,3) * ϵ(n,q,3) * (
                                 QE_leg($(TF²E * CE^2 / ΣEtot), [i], [j], k, l, m, n) * QE_leg($(TF²B        / ΣBtot),           k, l, p, q)
                + (zeroB ? 0 : -2QE_leg($(TF²E * CE   / ΣEtot), [i],      k, l, m, n) * QE_leg($(TF²B * CB   / ΣBtot),      [j], k, l, p, q))
                + (zeroB ? 0 :   QE_leg($(TF²E        / ΣEtot),           k, l, m, n) * QE_leg($(TF²B * CB^2 / ΣBtot), [i], [j], k, l, p, q)))
                for (k,l,m,n,p,q) in inds(6)
            )
            pinv(Diagonal(sum(∇[i].diag .* ∇[j].diag .* Fourier(A(i,j)) for (i,j) in inds(2))))
        end
    end
    Nϕ = AL # true only for unlensed weights

    Memoization.empty_cache!(QE_leg)

    ϕqe = (wiener_filtered ? (Cϕ*pinv(Cϕ+Nϕ)) : 1) * (AL * ϕqe_unnormalized)
    (;ϕqe, AL, Nϕ)

end


using RandomMatrices
using Base.Test

#Test far outside support
#Tracy-Widom has support on all x>0, but the integration won't pick it up
@test pdf(TracyWidom, -10) == pdf(TracyWidom, 10) == 0
@test cdf(TracyWidom, -10) == 0
@test cdf(TracyWidom, 10) == 1

if isdefined(:ODE) && isa(ODE, Module)
    t = rand()
    @test pdf(TracyWidom, t) > 0
    @test 0 < cdf(TracyWidom, t) < 1
end

@test isfinite(rand(TracyWidom, 10))
@test isfinite(rand(TracyWidom, 100))


import DelimitedFiles

data = DelimitedFiles.readdlm("data/day_one.txt", ',', Int)
complements = 2020 .- data

println(prod(intersect(complements, data)))

for j in data
    complements = 2020 - j .- data
    matches = intersect(complements, data)
    if length(matches) > 0
        println(j * prod(matches))
        break
    end
end


# HOME_DIR = dirname(@__DIR__)
# PACKAGE_DIR = dirname(HOME_DIR)
# pushfirst!(LOAD_PATH, PACKAGE_DIR)
using MPCHelper
using LazySets

a = HPolytope([eye(2); -eye(2)], [2,2,2,0.0])
b = HPolytope([eye(2); -eye(2)], [0.5,0.5,0.5,0.3])
c = a-b
get_support(a,2)
get_support(c,2)


using AutoHOOT
using Test

const ad = AutoHOOT.autodiff
const T = AutoHOOT.backend

T.set_backend("numpy")

@testset "test einsum" begin
    x2 = ad.Variable(name = "x2", shape = [3, 2])
    x3 = ad.Variable(name = "x3", shape = [2, 3])
    matmul = ad.einsum("ik,kj->ij", x2, x3)
    y = ad.sum(matmul)

    grad_x2, grad_x3 = ad.gradients(y, [x2, x3])

    executor = ad.Executor([y, grad_x2, grad_x3])
    x2_val = T.tensor([[1, 2], [3, 4], [5, 6]])  # 3x2
    x3_val = T.tensor([[7, 8, 9], [10, 11, 12]])  # 2x3

    y_val, grad_x2_val, grad_x3_val =
        executor.run(feed_dict = Dict(x2 => x2_val, x3 => x3_val))

    expected_grad_sum = T.ones_like(T.dot(x2_val, x3_val))
    expected_yval = T.sum(T.dot(x2_val, x3_val))
    expected_grad_x2_val = T.dot(expected_grad_sum, T.transpose(x3_val))
    expected_grad_x3_val = T.dot(T.transpose(x2_val), expected_grad_sum)

    @test T.array_equal(y_val, expected_yval)
    @test T.array_equal(grad_x2_val, expected_grad_x2_val)
    @test T.array_equal(grad_x3_val, expected_grad_x3_val)
end

@testset "test vjps" begin
    x = ad.Variable(name = "x", shape = [2])
    A = ad.Variable(name = "A", shape = [3, 2])
    v = ad.Variable(name = "v", shape = [3])
    y = ad.einsum("ab, b->a", A, x)

    transposed_vjp_x, = ad.transposed_vjps(y, [x], v)

    executor = ad.Executor([y, transposed_vjp_x])
    x_val = T.tensor([1.0, 2.0])  # 1x3
    A_val = T.tensor([[1.0, 2.0], [3.0, 4.0], [5, 6]])
    v_val = T.tensor([1.0, 2.0, 3.0])

    y_val, transposed_vjp_x_val =
        executor.run(feed_dict = Dict(x => x_val, A => A_val, v => v_val))

    expected_yval = T.einsum("ab, b->a", A_val, x_val)
    expected_transposed_vjp_x_val = T.einsum("b, ba->a", v_val, A_val)

    @test T.array_equal(y_val, expected_yval)
    @test T.array_equal(transposed_vjp_x_val, expected_transposed_vjp_x_val)
end

@testset "test inner product hvp" begin
    x = ad.Variable(name = "x", shape = [3, 1])
    v = ad.Variable(name = "v", shape = [3, 1])
    y = ad.sum(ad.einsum("ab,bc->ac", ad.transpose(x), x))

    grad_x, = ad.gradients(y, [x])
    Hv, = ad.hvp(output_node = y, node_list = [x], vector_list = [v])

    executor = ad.Executor([y, grad_x, Hv])
    x_val = T.tensor([[1.0], [2.0], [3]])  # 3x1
    v_val = T.tensor([[1.0], [2.0], [3]])  # 3x1
    y_val, grad_x_val, Hv_val = executor.run(feed_dict = Dict(x => x_val, v => v_val))

    expected_yval = T.sum(T.dot(T.transpose(x_val), x_val))
    expected_grad_x_val = 2 * x_val
    expected_hv_val = 2 * v_val

    @test T.array_equal(y_val, expected_yval)
    @test T.array_equal(grad_x_val, expected_grad_x_val)
    @test T.array_equal(Hv_val, expected_hv_val)
end


using Hadamard
using Base.Test

H8 = [   1     1     1     1     1     1     1     1
         1     1     1     1    -1    -1    -1    -1
         1     1    -1    -1    -1    -1     1     1
         1     1    -1    -1     1     1    -1    -1
         1    -1    -1     1     1    -1    -1     1
         1    -1    -1     1    -1     1     1    -1
         1    -1     1    -1    -1     1    -1     1
         1    -1     1    -1     1    -1     1    -1  ]

H8n = [  1     1     1     1     1     1     1     1
         1    -1     1    -1     1    -1     1    -1
         1     1    -1    -1     1     1    -1    -1
         1    -1    -1     1     1    -1    -1     1
         1     1     1     1    -1    -1    -1    -1
         1    -1     1    -1    -1     1    -1     1
         1     1    -1    -1    -1    -1     1     1
         1    -1    -1     1    -1     1     1    -1  ]

H8d = [  1     1     1     1     1     1     1     1
         1     1     1     1    -1    -1    -1    -1
         1     1    -1    -1     1     1    -1    -1
         1     1    -1    -1    -1    -1     1     1
         1    -1     1    -1     1    -1     1    -1
         1    -1     1    -1    -1     1    -1     1
         1    -1    -1     1     1    -1    -1     1
         1    -1    -1     1    -1     1     1    -1  ]

ieye(n) = eye(Int8, n)

@test ifwht(eye(8),1) == H8
@test ifwht(eye(8),2)' == H8
@test ifwht_natural(eye(8),1) == H8n
@test ifwht_natural(eye(8),2)' == H8n
@test ifwht_dyadic(eye(8),1) == H8d
@test ifwht_dyadic(eye(8),2)' == H8d

@test ifwht(ieye(8),1) == H8
@test ifwht(ieye(8),2)' == H8
@test ifwht_natural(ieye(8),1) == H8n
@test ifwht_natural(ieye(8),2)' == H8n
@test ifwht_dyadic(ieye(8),1) == H8d
@test ifwht_dyadic(ieye(8),2)' == H8d

H32 = [  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1
 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1
 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 -1 -1 -1 -1
 1 1 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 1 1
 1 1 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1
 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1
 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1
 1 1 -1 -1 -1 -1 1 1 -1 -1 1 1 1 1 -1 -1 -1 -1 1 1 1 1 -1 -1 1 1 -1 -1 -1 -1 1 1
 1 1 -1 -1 -1 -1 1 1 -1 -1 1 1 1 1 -1 -1 1 1 -1 -1 -1 -1 1 1 -1 -1 1 1 1 1 -1 -1
 1 1 -1 -1 1 1 -1 -1 -1 -1 1 1 -1 -1 1 1 1 1 -1 -1 1 1 -1 -1 -1 -1 1 1 -1 -1 1 1
 1 1 -1 -1 1 1 -1 -1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 1 1 -1 -1 1 1 -1 -1
 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1
 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1
 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1
 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1
 1 -1 -1 1 1 -1 -1 1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 -1 1 1 -1 1 -1 -1 1 1 -1 -1 1
 1 -1 -1 1 1 -1 -1 1 -1 1 1 -1 -1 1 1 -1 1 -1 -1 1 1 -1 -1 1 -1 1 1 -1 -1 1 1 -1
 1 -1 -1 1 -1 1 1 -1 -1 1 1 -1 1 -1 -1 1 1 -1 -1 1 -1 1 1 -1 -1 1 1 -1 1 -1 -1 1
 1 -1 -1 1 -1 1 1 -1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 1 -1 -1 1 -1 1 1 -1
 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1
 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1
 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1
 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1
 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1 1 -1 1 -1 -1 1 -1 1 1 -1 1 -1 1 -1 1 -1 -1 1 -1 1
 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1 1 -1 1 -1 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1 1 -1 1 -1
 1 -1 1 -1 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1 1 -1 1 -1 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1
 1 -1 1 -1 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 1 -1 1 -1 1 -1 1 -1
 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1
 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 ]

@test ifwht(eye(32),1) == H32
@test ifwht(eye(32),2)' == H32

@test ifwht(ieye(32),1) == H32
@test ifwht(ieye(32),2)' == H32

X = reshape(sin([1:1024*32;]), 1024,32);
norminf(A) = maximum(abs(A))

for f in (:fwht, :fwht_natural, :fwht_dyadic)
    fi = Symbol(string("i", f))
    @eval begin
        @test norminf(X - $fi($f(X))) < 1e-14
        @test norminf(X - $fi($f(X,1),1)) < 1e-14
        @test norminf(X - $fi($f(X,2),2)) < 1e-14
        @test norminf($f($f(X,1),2) - $f(X)) < 1e-14
        @test norminf($f(X',1)' - $f(X,2)) < 1e-14
    end
end

@test hadamard(8) == H8n
@test ifwht_natural(eye(32), 1) == hadamard(32)
@test ifwht_natural(eye(2), 1) == hadamard(2)

print("Checking unitarity of hadamard(n): ")
for i = 4:4:1000
    H = try
        convert(Matrix{Int}, hadamard(i))
    catch
        Int[]
    end
    if !isempty(H)
        print(i, ", ")
        @test norm(H'*H - size(H,1)*eye(H), 1) == 0
    end
end
println(".\nSuccess!")


@symbol_func function h_CS(cur_reactor::AbstractReactor)
  cur_h = cur_reactor.kappa_95 * a(cur_reactor)

  cur_h += b(cur_reactor)

  cur_h += c(cur_reactor)

  cur_h *= 2

  cur_h
end


#solve u_t + div(v * u) - div(D * grad(u)) - Q = 0 on an unstructured mesh using an upwind discretization of the velocity term (or downwind if use_upwind=false)
@NonlinearEquations.equations exclude=(areasoverlengths, volumes, dirichletnodes, coords) function transport(u, vxs, vys, vzs, Ds, neighbors, areasoverlengths, dirichletnodes, dirichletus, Qs, volumes, coords; use_upwind=true)
	@assert size(coords, 1) == 3#it is assumed that this is a 3d problem
	NonlinearEquations.setnumequations(length(u))
	for i = 1:length(u)
		if i in dirichletnodes
			NonlinearEquations.addterm(i, u[i] - dirichletus[i])
		else
			NonlinearEquations.addterm(i, Qs[i] / (volumes[i]))
		end
	end
	delta = zeros(eltype(coords), size(coords, 1))
	for (i, (node_a, node_b)) in enumerate(neighbors)
		for (node1, node2) in [(node_a, node_b), (node_b, node_a)]
			#add the advection term
			delta .= coords[:, node1] .- coords[:, node2]
			is_upwind = ((vxs[i] * delta[1] + vys[i] * delta[2] + vzs[i] * delta[3]) > 0)
			if (is_upwind && use_upwind) || (!is_upwind && !use_upwind)#!use_upwind basically means use_downwind and !is_upwind basically means is_downwind
				if !(node1 in dirichletnodes) && !(node2 in dirichletnodes)
					NonlinearEquations.addterm(node1, -(vxs[i] * delta[1] + vys[i] * delta[2] + vzs[i] * delta[3]) * (u[node1] - u[node2]) * areasoverlengths[i] / (volumes[node1]))
				elseif !(node1 in dirichletnodes) && node2 in dirichletnodes
					NonlinearEquations.addterm(node1, -(vxs[i] * delta[1] + vys[i] * delta[2] + vzs[i] * delta[3]) * (u[node1] - dirichletus[node2]) * areasoverlengths[i] / (volumes[node1]))
				end
			end
			#add the diffusion term
			if !(node1 in dirichletnodes) && !(node2 in dirichletnodes)
				NonlinearEquations.addterm(node1, Ds[i] * (u[node2] - u[node1]) * areasoverlengths[i] / (volumes[node1]))
			elseif !(node1 in dirichletnodes) && node2 in dirichletnodes
				NonlinearEquations.addterm(node1, Ds[i] * (dirichletus[node2] - u[node1]) * areasoverlengths[i] / (volumes[node1]))
			end
		end
	end
end


# ===============================================================
# Discretize using the correction hull of the matrix exponential
# ===============================================================

"""
    CorrectionHull{EM} <: AbstractApproximationModel

Discretization using the correction hull of the matrix exponential.

### Fields

- `exp`   -- exponentiation method
- `order` -- order of the Taylor series expansion of the matrix exponential

### Algorithm

For the homogeneous case, this method implements the transformation:

```math
Ω_0 = CH(X_0, e^{Aδ}  X_0) ⊕ FX_0
```
where ``F`` is the correction (interval) matrix.

For the inhomogeneous case, ``x' = Ax + u``,  ``x ∈ X, u ∈ U``, implements
``Ω_0 = CH(X_0, exp(Aδ)  X0) ⊕ FX0`` where ``F`` is the correction (interval) matrix.

In both cases, if ``A`` is an interval matrix, the exponential is overapproximated
using methods from `IntervalMatrices.jl`.
"""
struct CorrectionHull{EM} <: AbstractApproximationModel
   order::Int
   exp::EM
end

# convenience constructor using symbols
function CorrectionHull(; order::Int=10, exp=IntervalExpAlg(order))
    return CorrectionHull(order, _alias(exp))
end

function Base.show(io::IO, alg::CorrectionHull)
    print(io, "`CorrectionHull` approximation model with: \n")
    print(io, "    - exponentiation method: $(alg.exp) \n")
    print(io, "    - order: $(alg.order)\n")
end

Base.show(io::IO, m::MIME"text/plain", alg::CorrectionHull) = print(io, alg)

# -----------------------------------------------------------------
# Correction hull: homogeneous case x' = Ax, x in X
# -----------------------------------------------------------------

function discretize(ivp::IVP{<:CLCS, <:LazySet}, δ, alg::CorrectionHull)
    A = state_matrix(ivp)
    X0 = initial_state(ivp)
    X = stateset(ivp)

    # compute exp(A*δ) * X0
    Φ = _exp(A, δ, alg.exp)

    # compute Ω0 = CH(X0, ΦX0) ⊕ FX0
    Ω0 = _discretize_chull(A, Φ, X0, δ, alg)

    Sdis = ConstrainedLinearDiscreteSystem(Φ, X)
    return InitialValueProblem(Sdis, Ω0)
end

function _discretize_chull(A, Φ::IntervalMatrix, X0, δ, alg, P=nothing)
    X0z = _convert_or_overapproximate(X0, Zonotope)
    Y = _overapproximate(Φ * X0z, Zonotope)
    if !isnothing(P)
        Y = minkowski_sum(Y, P)
    end

    H = overapproximate(CH(X0z, Y), Zonotope)
    F = correction_hull(A, δ, alg.order)
    R = _overapproximate(F * X0z, Zonotope)
    Ω0 = minkowski_sum(H, R)
    return Ω0
end

# F(δ) without the E(δ) correction term
function _correction_hull_without_E(A, δ, p)
    timeint(δ, i) = interval((i^(-i / (i-1)) - i^(-1 / (i-1))) * δ^i, 0)
    F = sum(map(x -> timeint(δ, i) * x, A^i / factorial(i)) for i in 2:p)
    return IntervalMatrix(F)
end

function _discretize_chull(A, Φ::AbstractMatrix, X0, δ, alg)
    X0z = _convert_or_overapproximate(X0, Zonotope)
    Y = linear_map(Φ, X0z)

    H = overapproximate(CH(X0z, Y), Zonotope)
    F = _correction_hull_without_E(A, δ, alg.order)
    R = _overapproximate(F * X0z, Zonotope)

    Ω0 = minkowski_sum(H, R)
    return Ω0
end

# -----------------------------------------------------------------
# Correction hull: inhomogeneous case x' = Ax + u, x in X, u ∈ U
# -----------------------------------------------------------------
function discretize(ivp::IVP{<:CLCCS, <:LazySet}, δ, alg::CorrectionHull)
    A = state_matrix(ivp)
    X0 = initial_state(ivp)
    X = stateset(ivp)
    U = next_set(inputset(ivp), 1) # inputset(ivp)

    # here U is an interval matrix map of a lazyset, TODO refactor / dispatch
    if isa(U, LinearMap)
        Uz = _convert_or_overapproximate(Zonotope, LazySets.set(U))
        B = matrix(U) # TODO remove this case, since preprocessing normalize(ivp) would make B the identity
        Uz = isinterval(B) ? _overapproximate(B * Uz, Zonotope) : linear_map(B, Uz)
    else # LazySet
        Uz = _convert_or_overapproximate(Zonotope, U)
    end

    Φ = _exp(A, δ, alg.exp)

    A_interval = _interval_matrix(A)

    origin_not_contained_in_U = zeros(dim(U)) ∉ Uz

    if origin_not_contained_in_U
        # shift U to origin
        u = center(Uz)
        Uz = Zonotope(zeros(dim(U)), genmat(Uz))
    end

    # note: this evaluates Φ₁(A, δ) using Taylor expansion of the interval matrix
    Cδ = _Cδ(A_interval, δ, alg.order)

    if origin_not_contained_in_U
        # compute C(δ) * u
        Pu = _convert_intervals_to_box_with_vectors(Cδ * u)
    else
        Pu = nothing
    end

    Ω0 = _discretize_chull(A_interval, Φ, X0, δ, alg, Pu)

    if origin_not_contained_in_U
        # apply another correction hull for the shifted U
        F = input_correction(A_interval, δ, alg.order)
        Fu = _convert_intervals_to_box_with_vectors(F * u)
        Ω0 = minkowski_sum(Ω0, Fu)
    end
    # Ω0 = _apply_setops(Ω0, alg.setops) # TODO requires to add `setops` field to the struct

    # compute C(δ) * U
    Ud = _overapproximate(Cδ * Uz, Zonotope)

    # add inputs to Ω0
    Ω0 = minkowski_sum(Ω0, Ud)

    B = Matrix(IdentityMultiple(one(eltype(A)), size(A, 1)))
    Sdis = ConstrainedLinearControlDiscreteSystem(Φ, B, X, Ud)
    return InitialValueProblem(Sdis, Ω0)
end

# convert a vector of intervals to a hyperrectangle with normal vector type
function _convert_intervals_to_box_with_vectors(intervals)
    # convert to Hyperrectangle
    H = convert(Hyperrectangle, IntervalBox(intervals))
    # remove static vectors (they scale poorly)
    H = Hyperrectangle(Vector(center(H)), Vector(radius_hyperrectangle(H)))
    return H
end


"""Bitmap Cubical Complex

A bitmap cubical complex is a structured complex representation of the collection of cubes.
The cubes store filtration values, thus this the cubical complexes is a filtered cubical complex.
"""
mutable struct BitmapComplex{T<:Real} <: AbstractComplex
    dims::Vector{Int}
    mult::Vector{Int}
    cells::AbstractVector{T} # cubes encoded in contiguous array
end

function BitmapComplex(::Type{T}, sizes::Vector{Int}=zeros(Int,0)) where {T<:Real}
    m = length(sizes) == 0 ? 0 : 1
    mlt = Int[]
    for s in sizes
        push!(mlt, m)
        m *= 2*s+1
    end
    BitmapComplex{T}(sizes, mlt, fill(typemax(T), m))
end
BitmapComplex() = BitmapComplex(Float64)

function BitmapComplex(sizes::Vector{Int}, cells::AbstractVector{T}) where {T<:Real}
    cplx = BitmapComplex(T, sizes)
    process = topcubes(cplx)
    for (i,v) in zip(process, cells)
        cplx.cells[i] = v
    end
    idx = BitVector(zeros(length(cplx.cells)))
    while length(process) > 0
        i = popfirst!(process)
        for j in faces(cplx, i)
            if cplx.cells[j] > cplx.cells[i]
                cplx.cells[j] = cplx.cells[i]
            end
            if !idx[j]
                push!(process, j)
                idx[j] = true
            end
        end
    end
    return cplx
end

BitmapComplex(img::AbstractMatrix{T}) where {T<:Real} = BitmapComplex([size(img)...], vec(rotr90(img)))

function topcubes(cplx::BitmapComplex{T}) where {T<:Real}
    idxs = Int[]
    pos=ones(Int, dim(cplx))
    while pos[1] <= cplx.dims[1]
        idx = cellindex(cplx, pos...)
        push!(idxs, idx)
        # println("pos: $pos, idx: $idx")
        pos[1]+=1
        i = findfirst(x->x>0, pos .- cplx.dims)
        if i !== nothing
            pos[i]=1
            pos[i+1]+=1
        end
        last(pos) > last(cplx.dims) && break
    end
    return idxs
end

cellindex(cplx::BitmapComplex, idxs...) = sum((2*i-1)*m for (i, m) in zip(idxs, cplx.mult))+1

function celldim(cplx::BitmapComplex, ci::Int)
    d = 0
    ci-=1
    for m in reverse(cplx.mult)
        pos = div(ci, m)
        if (pos % 2 == 1)
            d+=1
        end
        ci = mod(ci, m)
    end
    return d
end

function faces(cplx::BitmapComplex, ci::IX) where {IX<:Integer}
    idxs = IX[]
    d = 0
    c=ci-1
    for m in reverse(cplx.mult)
        pos = div(c, m)
        if (pos % 2 == 1)
            ms = d % 2 == 1 ? m : -m
            push!(idxs, ci + ms)
            push!(idxs, ci - ms)
            d+=1
        end
        c = mod(c, m)
    end
    return idxs
end

function cofaces(cplx::BitmapComplex, ci::IX) where {IX<:Integer}
    ret = IX[]
    c=ci-1
    p = foldl((p,m)-> (push!(p[1], div(p[2],m)+1), mod(p[2],m)),
              reverse(cplx.mult), init=(Int[],c)) |> first
    d = dim(cplx)+1
    for (i,m) in enumerate(reverse(cplx.mult))
        if div(c, m) % 2 == 0
            if ci > m+1 && p[i] != 1
                push!(ret, ci - m)
            end
            if (ci + m < length(cplx)) && (p[i] != 2 * cplx.dims[d-i]+1)
                push!(ret, ci + m)
            end
        end
        c = mod(c, m)
    end
    return ret
end

show(io::IO, cplx::BitmapComplex) = print(io, "BitmapComplex($(size(cplx)))")
eltype(cplx::BitmapComplex{T}) where {T} = T
dim(cplx::BitmapComplex) = length(cplx.dims)
length(cplx::BitmapComplex) = length(cplx.cells)
function size(cplx::BitmapComplex)
    ii = [celldim(cplx, i) for i in 1:length(cplx)]
    ntuple(d->count(i->i==d-1, ii), dim(cplx)+1)
end

# Filtration

function filtration(cplx::BitmapComplex{T}) where {T<:Real}
    U = promote_type(Float32, T)
    idx = Vector{Tuple{Int,UInt,U}}()
    i = 1
    for (ci,cv) in enumerate(cplx.cells)
        d = celldim(cplx, ci)
        push!(idx, (d, ci, cv))
    end
    sort!(idx, by=x->(x[3], x[1])) # sort by dimension & filtration value
    return Filtration(cplx, idx, U(Inf))
end

# Miscellaneous

function read(io::IO, ::Type{BitmapComplex}, ::Type{T}) where {T<:Real}
    d = parse(Int, readline(io))
    dims = Int[]
    for i in 1:d
        push!(dims, parse(Int, readline(io)))
    end
    n  = prod(dims)
    data = zeros(T, n)
    for i in 1:n
        data[i] =  parse(T, readline(io))
    end
    return BitmapComplex(dims, data)
end


# This file implements the adaptive SIS model from :
#
#      Gross, Dommar, Blasius: "Epidemic dynamics on an adaptive network"
#      Physical Review Letters 96, 208701, 2006
#      doi: 10.1103/PhysRevLett.96.208701

using Fastnet

const S=1                   # Node state 1: Susceptible node
const I=2                   # Node state 2: Infected node
const SI=1                  # Link state 1: Susceptible-Infected link

p=0.004                     # Infection rate (per SI-link)
r=0.002                     # Recovery rate (per I-node)
w=0.2                       # Rewiring rate (per SI-link)

SI_link=LinkType(S,I)     # This describes what we mean by SI-link

# Lets make a network of 100k nodes and 400k links, 2 node states, that keeps track of SI links
net=FastNet(100000,1000000,2,[SI_link]; nodealias=["S","I"], linkalias=["SI"])

randomgraph!(net)           # Initialize as ER-random graph (all nodes will be in state 1: S)

for i=1:96000               # Infect 20 nodes at random
    node=randomnode(net,S)
    nodestate!(net,node,I)
end

function rates!(rates,t)    # This functins computes the rates of processes
    infected=countnodes_f(net,I)        # count the infected nodes
    activelinks=countlinks_f(net,SI)    # count the SI links
    infrate=p*activelinks               # compute total infection rate
    recrate=r*infected                  # compute total recovery rate
    rewrate=w*activelinks               # compute total rewiring rate
    rates[1]=infrate                    # Return the values by filling the rates array
    rates[2]=recrate
    rates[3]=rewrate
    nothing
end

function recovery!()        # This is what we do when the recovery process is triggered
    inode=randomnode_f(net,I)                   # Find a random infected node
    nodestate_f!(net,inode,S)                   # Set the state of the node to susceptible
end

function infection!()       # This is what we do when the infection process is triggered
    alink=randomlink_f(net,SI)                   # Find a random SI link
    nodestate_f!(net,linksrc_f(net,alink),I)     # Set both endpoints of the link to infected
    nodestate_f!(net,linkdst_f(net,alink),I)
end

function rewire!()         # This is what we do  when
    lnk=randomlink_f(net,SI)                    # Find a random SI link
    src=linksrc_f(net,lnk)
    dst=linkdst_f(net,lnk)
    sus=randomnode_f(net,S)
    destroylink_f!(net,lnk)
    if nodestate_f(net,src)==S
        makelink_f!(net,src,sus)
    else
        makelink_f!(net,sus,dst)
    end
end

sim=FastSim(net,rates!,[infection!,recovery!,rewire!])   # initialize the simulation

@time runsim!(sim,500,20)                       # Run for 60 timeunits (reporting every 5)


function fprop!(::RelaxInterval, vt::Variable, g::DAT, b::IntervalCache{T}, k) where T<:Real
    i = first_index(g, k)
    l = lbd(b, i)
    u = ubd(b, i)
    b[k] = (l == u) ? Interval(l) : Interval(l, u)
    nothing
end

function fprop!(t::RelaxInterval, ex::Subexpression, g::DAT, b::IntervalCache{T}, k) where T<:Real
    b[k] = subexpression_set(t, b, first_index(g, k))
end

for (F, f) in ((DIV, :/), (ARH, :arh), (POW, :^))
    @eval fprop!(t::RelaxInterval, v::Val{$F}, g::DAT, b::IntervalCache{T}, k) where T<:Real = (b[k] = ($f)(set(t, b, child(g, 1, k)), set(b, child(g, 2, k))); nothing)
end

function fprop!(t::RelaxInterval, v::Val{MINUS}, g::DAT, b::IntervalCache{T}, k) where T<:Real
    x = child(g, 1, k)
    b[k] = is_binary(g, k) ? (set(t, b, x) - set(t, b, child(g, 2, k))) : - set(t, b, x)
end

for (F, f) in ((PLUS, :sum), (MIN, :minimum), (MAX, :maximum), (MULT, :prod))
    @eval fprop!(t::Interval, v::Val{$F}, g::DAT, b::IntervalCache{T}, k) where T<:Real = (b[k] = ($f)(i -> set(t, b, i), children(g, k)); nothing)
end

function fprop!(t::RelaxInterval, v::Val{USER}, g::DAT, b::IntervalCache{T}, k) where T<:Real
    f = user_univariate_operator(g, first_index(g, k))
    b[k] = f(set(t, b, child(g, 1, k)))
end

function fprop!(t::RelaxInterval, v::Val{USERN}, g::DAT, b::IntervalCache{T}, k) where T<:Real
    mv = user_multivariate_operator(g, first_index(g, k))
    set_input = set_input(t, b, arity(g, k))
    for c in children(g, k)
        set_input[i] = set(t, b, c)
    end
    b[k] = MOI.eval_objective(mv, set_input)::Interval{Float64}
end

for ft in UNIVARIATE_ATOM_TYPES
    f = UNIVARIATE_ATOM_DICT[ft]
    (f == :user || f == :+ || f == :-) && continue
    @eval function fprop!(t::RelaxInterval, v::Val{$ft}, g::DAT, b::IntervalCache{T}, k) where T<:Real
        x = child(g, 1, k)
        b[k] = ($f)(set(t, b, x))
    end
end

for (F, f) in ((LOWER_BND, :lower_bnd), (UPPER_BND, :upper_bnd))
    @eval function fprop!(t::RelaxInterval, v::Val{$F}, g::DAT, b::IntervalCache{T}, k) where T<:Real
        y = child(g, 2, k)
        if is_num(t, b, y)
            z = set(t, b, child(g, 1, k))
            b[k] = ($f)(z, num(t, b, y))
        end
        nothing
    end
end

function fprop!(t::RelaxInterval, v::Val{BND}, g::DAT, b::IntervalCache{T}, k) where T<:Real
    y = child(g, 2, k)
    r = child(g, 3, k)
    if is_num(t, b, y) && is_num(t, b, r)
        z = set(t, b, child(g, 1, k))
        b[k] = bnd(z, num(t, b, y), num(t, b, r))
    end
    nothing
end


#
# Restricted Two-body problem propagation
#

"""
Create's an ODEProblem for a `R2BP` orbit.
"""
function SciMLBase.ODEProblem(orbit::R2BPOrbit, Δt::Number)
    u  = state(orbit)
    Δt = eltype(state(orbit))(Δt)
    ts = Δt isa Unitful.Time ? (zero(ustrip(timeunit(orbit), Δt)), ustrip(timeunit(orbit), Δt)) : (zero(Δt), Δt)
    p  = system(orbit)
    return ODEProblem(R2BPFunction(), u, ts, p)
end

"""
Create's an ODEProblem for a `R2BP` orbit.
"""
function SciMLBase.ODEProblem(orbit::CR3BPOrbit, Δt::Real)
    u  = state(orbit)
    Δt = eltype(state(orbit))(Δt)
    ts = (zero(Δt), Δt)
    p  = system(orbit)
    f  = u isa CartesianStateWithSTM ? CR3BPFunction(; stm=true) : CR3BPFunction()
    return ODEProblem(f, u, ts, p)
end

"""
Propagates an orbit forward or backward in time.
Use `algorithm` to set the desired numerical integration
algorithm, e.g. `algorithm = Tsit5()`. All other `kwargs`
are passed directly to `DifferentialEquations.solve`.
"""
function propagate(orbit::Orbit, Δt::Number; algorithm=nothing, kwargs...)
    defaults = (; reltol=1e-14, abstol=1e-14)
    options  = merge(defaults, kwargs)
    problem  = ODEProblem(orbit, Δt)

    if isnothing(algorithm)
        solution = solve(problem; options...)
    else
        solution =  solve(problem, algorithm; options...)
    end

    return Trajectory{frame(orbit), typeof(solution.prob.u0), typeof(solution.prob.p), typeof(epoch(orbit)), typeof(solution)}(epoch(orbit), solution)
end


# case with input and without invariant
function reach_inhomog_ASB07!(F::Vector{ReachSet{N, Zonotope{N, VN, MN}}},
                              Ω0::Zonotope{N, VN, MN},
                              Φ::AbstractMatrix,
                              NSTEPS::Integer,
                              δ::N,
                              max_order::Integer,
                              X::Universe,
                              U::Zonotope,
                              recursive::Val{true},
                              reduction_method::AbstractReductionMethod,
                              Δt0::TN) where {N, TN, VN, MN}
    # initial reach set
    Δt = (zero(N) .. δ) + Δt0
    @inbounds F[1] = ReachSet(Ω0, Δt)

    # input sequence
    Wk₊ = copy(U)

    # split the interval matrix into center and radius
    Φc, Φs = _split(Φ)

    k = 2
    @inbounds while k <= NSTEPS
        Zₖ₋₁ = set(F[k-1])
        cₖ₋₁ = Zₖ₋₁.center
        Gₖ₋₁ = Zₖ₋₁.generators

        Zₖ = _overapproximate_interval_linear_map(Φc, Φs, cₖ₋₁, Gₖ₋₁)
        Zₖ = minkowski_sum(Wk₊, Zₖ)
        Zₖʳ = _reduce_order(Zₖ, max_order, reduction_method)

        Δt += δ
        F[k] = ReachSet(Zₖʳ, Δt)
        k += 1
        Wk₊ = _overapproximate_interval_linear_map(Φc, Φs, Wk₊.center, Wk₊.generators)
        Wk₊ = _reduce_order(Wk₊, max_order, reduction_method)
    end
    return F
end

# case with input and with invariant
function reach_inhomog_ASB07!(F::Vector{ReachSet{N, Zonotope{N, VN, MN}}},
                              Ω0::Zonotope{N, VN, MN},
                              Φ::AbstractMatrix,
                              NSTEPS::Integer,
                              δ::N,
                              max_order::Integer,
                              X::LazySet,
                              U::Zonotope,
                              recursive::Val{true},
                              reduction_method::AbstractReductionMethod,
                              Δt0::TN) where {N, TN, VN, MN}
    # initial reach set
    Δt = (zero(N) .. δ) + Δt0
    @inbounds F[1] = ReachSet(Ω0, Δt)

    # input sequence
    Wk₊ = copy(U)

    # split the interval matrix into center and radius
    Φc, Φs = _split(Φ)

    k = 2
    @inbounds while k <= NSTEPS
        Zₖ₋₁ = set(F[k-1])
        cₖ₋₁ = Zₖ₋₁.center
        Gₖ₋₁ = Zₖ₋₁.generators

        Zₖ = _overapproximate_interval_linear_map(Φc, Φs, cₖ₋₁, Gₖ₋₁)
        Zₖ = minkowski_sum(Wk₊, Zₖ)
        Zₖʳ = _reduce_order(Zₖ, max_order, reduction_method)
        _is_intersection_empty(X, Zₖʳ) && break

        Δt += δ
        F[k] = ReachSet(Zₖʳ, Δt)
        k += 1

        Wk₊ = _overapproximate_interval_linear_map(Φc, Φs, Wk₊.center, Wk₊.generators)
        Wk₊ = _reduce_order(Wk₊, max_order, reduction_method)
    end
    if k < NSTEPS + 1
        resize!(F, k-1)
    end
    return F
end


using FixedPointAcceleration
using Test

func(x) = cos.(x)
Inputs = 1.1
fp_simple   = fixed_point(func, Inputs; Algorithm = :Simple)
fp_simple.Convergence_ < 1e-10
fp_anderson = fixed_point(func, Inputs; Algorithm = :Anderson)
fp_anderson.Convergence_ < 1e-10
fp_aitken   = fixed_point(func, Inputs; Algorithm = :Aitken)
fp_aitken.Convergence_ < 1e-10
fp_newton   = fixed_point(func, Inputs; Algorithm = :Newton)
fp_newton.Convergence_ < 1e-10
fp_VEA      = fixed_point(func, Inputs; Algorithm = :VEA)
fp_VEA.Convergence_ < 1e-10
fp_SEA      = fixed_point(func, Inputs; Algorithm = :SEA)
fp_SEA.Convergence_ < 1e-10
fp_MPE      = fixed_point(func, Inputs; Algorithm = :MPE)
fp_MPE.Convergence_ < 1e-10
fp_RRE      = fixed_point(func, Inputs; Algorithm = :RRE)
fp_RRE.Convergence_ < 1e-10

# Testing input of inputs without outputs.
a = @test_logs (:warn,"If you do not give outputs to the function then you can only give one vector of inputs (in a 2d array) to the fixed_pointFunction. So for a function that takes an N dimensional array you should input a Array{Float64}(N,1) array.  As you have input an array of size Array{Float64}(N,k) with k > 1 we have discarded everything but the last column to turn it into a Array{Float64}(N,1) array.\n") fixed_point(func, fp_RRE.Inputs_; Algorithm = :RRE)
a.Convergence_ < 1e-10 # If the above line throws then that is a test failure.
# Testing input of Outputs with different shape to inputs.
b = @test_logs (:warn,"If you input a matrix of outputs as well as a matrix of inputs then inputs and outputs must be the same shape. As they differ in this case the last column of the inputs matrix has been taken as the starting point and everything else discarded.") fixed_point(func, fp_RRE.Inputs_; Outputs = fp_RRE.Outputs_[:,2:3], Algorithm = :RRE)
b.Convergence_ < 1e-10 # If the above line throws then that is a test failure.


using CompScienceMeshes, BEAST
using LinearAlgebra
using Profile
using TimerOutputs
using StaticArrays

function tau(x::SVector{U,T}) where {U,T}
    1.0-1.0/5.0
end

ntrc = X->ntrace(X,y)

T = tetmeshsphere(1.0,0.2)
X = nedelecd3d(T)
y = boundary(T)
@show numfunctions(X)

ϵ, μ, ω = 1.0, 1.0, 1.0; κ, η = ω * √(ϵ*μ), √(μ/ϵ)
ϵ_r =5.0
χ = tau
K, I, B = VIE.singlelayer(wavenumber=κ, tau=χ), Identity(), VIE.boundary(wavenumber=κ, tau=χ)
E = VIE.planewave(direction=ẑ, polarization=x̂, wavenumber=κ)
H = -1/(im*κ*η)*curl(E)

@hilbertspace j
@hilbertspace k
α = 1.0/ϵ_r
eq = @varform α*I[j,k]-K[j,k]-B[ntrc(j),k] == E[j]

dbvie = @discretise eq j∈X k∈X
u_m = solve(dbvie)
#=
#postprocessing
Φ, Θ = [0.0], range(0,stop=2π,length=100)
pts = [point(cos(ϕ)*sin(θ), sin(ϕ)*sin(θ), cos(θ)) for θ in Θ for ϕ in Φ]
ffd = potential(VIE.farfield(wavenumber=κ, tau=χ), pts, u, X)
ff = im*κ*ffd

using Plots

#Farfield
plot(xlabel="theta")
plot!(Θ, norm.(ff), label="far field", title="D-VIE")
=#
using Plots
#NearField
Z = range(-1,1,length=100)
Y = range(-1,1,length=100)
nfpoints = [point(0,y,z) for z in Z, y in Y]

Enear = BEAST.grideval(nfpoints,α.*u_m,X)
Enear = reshape(Enear,100,100)

contour(real.(getindex.(Enear,1)))
heatmap(Z, Y,  real.(getindex.(Enear,1)))

plot!(Y[2:99],real.(getindex.(Enear[2:99,50],1)),label="D-VIE (simplex)", linestyle=:dash, linecolor=:darkorange4)
#plot!(Y[2:99],real.(getindex.(Enear_simplex[2:99,50],1)),label="D-VIE (simplex)", linestyle=:solid, linecolor=:darkorange3)

#=
import Cairo
using DataFrames, Gadfly, RDatasets
D = dataset("datasets","HairEyeColor")
palette = ["skyblue","skyblue3"]

D = DataFrame(
   Type=["D-VIE \n (1,190 Tets.)","D-VIE \n (1,190 Tets.)","D-VIE \n (1,190 Tets.)","D-VIE \n (1,190 Tets.)","E-VIE \n (1,190 Tets.)","E-VIE \n (1,190 Tets.)","E-VIE \n (1,190 Tets.)","E-VIE \n (1,190 Tets.)","D-VIE \n (2,706 Tets.)","D-VIE \n (2,706 Tets.)","D-VIE \n (2,706 Tets.)","D-VIE \n (2,706 Tets.)","E-VIE \n (2,706 Tets.)","E-VIE \n (2,706 Tets.)","E-VIE \n (2,706 Tets.)","E-VIE \n (2,706 Tets.)"],
   Quad=["Simplex","Simplex","Classic","Classic","Simplex","Simplex","Classic","Classic","Simplex","Simplex","Classic","Classic","Simplex","Simplex","Classic","Classic"],
   Sing=["Regular","Singular","Regular","Singular","Regular","Singular","Regular","Singular","Regular","Singular","Regular","Singular","Regular","Singular","Regular","Singular"],
   Time=[24,4.77,23.1,27.9,41.9,8.49,41.5,45.7,125,13.1,125,68.6,222,20.5,222,115],
)

p = Gadfly.plot(D, x=:Quad, y=:Time, color=:Sing, xgroup=:Type,
    Geom.subplot_grid(Geom.bar(position=:stack)),
    Scale.color_discrete_manual(palette...),
    Guide.xlabel(""),Guide.ylabel("Assembly time [s]"), Guide.colorkey("Interaction"),Theme(major_label_font="Times",minor_label_font="Times",key_title_font="Times", key_label_font="Times",background_color=color("white")))

# draw(PNG("haireyecolor.png", 6.6inch, 4inch), p)
draw(PNG(6.6inch, 4inch), p)
=#


# Script to run a test case for understanding how M scales with β #

# Add paths
loc_gm_package = "../src/"
if !(loc_gm_package in LOAD_PATH)
    push!(LOAD_PATH, "../src/")
end

# Required packages
using LinearAlgebra

using GraphicalModelLearning
using LightGraphs

# Function definitions
using Printf, SparseArrays
include("beta_scaling.jl")

using DelimitedFiles

# parameters
FLAG_create_struct_gm = false
FLAG_regular_random_gm = true
FLAG_lattice_gm = false
sampling_regime = M_regime()
FLAG_weak_impurity = false
learning_algo = NLP()

if FLAG_lattice_gm
    # text file to save adjacency matrix in
    file_adj_matrix_gm = "adj_matrix_ferro_lattice_gm_A_T.txt"
    # name of picture to save graphical model in
    file_plot_gm = "ferro_lattice_gm_A_T.eps"
    # File to save final results
    file_M_opt_gm = "M_opt_T_Regime_Ferro_Lattice_A.txt"
elseif FLAG_regular_random_gm
    # text file to save adjacency matrix in
    #file_adj_matrix_gm = "adj_matrix_ferro_random_regular_gm_B_M.txt"
    file_adj_matrix_gm = "spin_glass_random_regular_adj_matrix.txt"
    # name of picture to save graphical model in
    file_plot_gm = "ferro_random_regular_gm_B_M.eps"
    # File to save final results
    file_M_opt_gm = "M_opt_RR_FigD_M_RISE_reg_sensitivity_beta_1_5.txt"
end

# Simple test
N = 16
d = 3
α = 0.4
β = 1.5

# Create and plot the initial graphical graphical model
adj_matrix = readdlm(file_adj_matrix_gm)
struct_adj_matrix = sign.(abs.(adj_matrix))

## Start the complexity studies
τ = α/2
L = 45
M_factor = 0.05
M_guess = 1000

# Different values of regularization coefficient to be tested
c_array = vcat([0.02,0.04],[0.05 + 0.05*i for i=0:9],[0.5 + 0.1*i for i=1:15])
M_opt = Array{Int64,1}(undef,length(c_array))

# To prevent rewriting
adj_matrix_orig = deepcopy(adj_matrix)

println(β)
for i = 1:length(c_array)
    # define β
    c = copy(c_array[i])
    println(c)
    println(M_guess)

    # Change the
    adj_matrix = replace_beta_gm(adj_matrix_orig, α, β)

    # Get the optimum number of samples
    M_opt[i] = get_M_opt_glauber_dynamics_regularization(adj_matrix, RISE(c, true), learning_algo, sampling_regime, τ, L, M_guess, M_factor)

    # Update the guess of M_opt
    @printf("beta=%f, M_opt=%d \n", β, M_opt[i])
end

writedlm(file_M_opt_gm,M_opt)


isleap(yr::Integer) = yr % 4 == 0 && (yr < 1582 || yr % 400 == 0 || yr % 100 != 0)

@assert all(isleap, [2400, 2012, 2000, 1600, 1500, 1400])
@assert !any(isleap, [2100, 2014, 1900, 1800, 1700, 1499])


using IntervalArithmetic, IntervalOptimisation
using Test
using IntervalOptimisation: numeric_type

@testset "IntervalOptimisation tests" begin
    @testset "numeric_type" begin
        x = -10..10
        big_x = big(x)
        @test numeric_type(x) == Float64
        @test numeric_type(big_x) == BigFloat
        @test numeric_type(IntervalBox(x, x)) == Float64
        @test numeric_type(IntervalBox(big_x, big_x)) == BigFloat
    end

    @testset "Minimise in 1D using default data structure i.e HeapedVector" begin
        global_min, minimisers = minimise(x->x, -10..10)
        @test global_min ⊆ -10 .. -9.999
        @test length(minimisers) == 1
        @test minimisers[1] ⊆ -10 .. -9.999

        # same but with maximise
        global_max, maximisers = maximise(x->x, -10..10)
        @test global_max ⊆ 9.999 .. 10
        @test length(maximisers) == 1
        @test maximisers[1] ⊆ 9.999 .. 10

        # same but with BigFloats
        global_min, minimisers = minimise(x->x, -big(10.0)..big(10.0))
        @test global_min ⊆ -10 .. -9.999
        @test length(minimisers) == 1
        @test minimisers[1] ⊆ -10 .. -9.999

        global_min, minimisers = minimise(x->x^2, -10..11, tol = 1e-10)
        @test global_min ⊆ 0..1e-20
        @test length(minimisers) == 1
        @test minimisers[1] ⊆ -0.1..0.1

        global_min, minimisers = minimise(x->(x^2-2)^2, -10..11)
        @test global_min ⊆ 0..1e-7
        @test length(minimisers) == 2
        @test sqrt(2) ∈ minimisers[2]
    end

    for Structure in (SortedVector, HeapedVector)

        @testset "Minimise in 1D using SortedVector" begin
            global_min, minimisers = minimise(x->x, -10..10, structure = Structure)
            @test global_min ⊆ -10 .. -9.999
            @test length(minimisers) == 1
            @test minimisers[1] ⊆ -10 .. -9.999

            # same but with maximise
            global_max, maximisers = maximise(x->x, -10..10, structure = Structure)
            @test global_max ⊆ 9.999 .. 10
            @test length(maximisers) == 1
            @test maximisers[1] ⊆ 9.999 .. 10

            # same but with BigFloats
            global_min, minimisers = minimise(x->x, -big(10.0)..big(10.0), structure = Structure)
            @test global_min ⊆ -10 .. -9.999
            @test length(minimisers) == 1
            @test minimisers[1] ⊆ -10 .. -9.999

            global_min, minimisers = minimise(x->x^2, -10..11, tol=1e-10, structure = Structure)
            @test global_min ⊆ 0..1e-20
            @test length(minimisers) == 1
            @test minimisers[1] ⊆ -0.1..0.1

            global_min, minimisers = minimise(x->(x^2-2)^2, -10..11, structure = Structure)
            @test global_min ⊆ 0..1e-7
            @test length(minimisers) == 2
            @test sqrt(2) ∈ max(minimisers[1], minimisers[2])
        end

        @testset "Discontinuous function in 1D" begin

            H(x) = (sign(x) + 1) / 2   # Heaviside function except at 0, where H(0) = 0.5
            global_min, minimisers = minimise(x -> abs(x) + H(x) - 1, -10..11, tol=1e-5, structure = Structure)
            @test global_min ⊆ -1 .. -0.9999
            @test length(minimisers) == 1
            @test 0 ∈ minimisers[1]
            @test diam(minimisers[1]) <= 1e-5
        end

        @testset "Smooth function in 2D" begin
            global_min, minimisers = minimise( X -> ( (x,y) = X; x^2 + y^2 ), (-10..10) × (-10..10), structure = Structure )
            @test global_min ⊆ 0..1e-7
            @test all(X ⊆ (-1e-3..1e3) × (-1e-3..1e-3) for X in minimisers)

            # same but with maximise
            global_max, maximisers = maximise( X -> ( (x,y) = X; x^2 + y^2 ), (-10..10) × (-10..10), structure = Structure )
            @test global_max ⊆ 199.9..200
            m = (9.99..10)
            @test all(X ⊆ m × m || X ⊆ -m × m || X ⊆ m × -m || X ⊆ -m × -m for X in maximisers)

            # same but with BigFloats
            global_min, minimisers = minimise( X -> ( (x,y) = X; x^2 + y^2 ), (-big(10.0)..big(10.0)) × (-big(10.0)..big(10.0)), structure = Structure )
            @test global_min ⊆ 0..1e-7
            @test all(X ⊆ big(-1e-3..1e3) × big(-1e-3..1e-3) for X in minimisers)

        end

        @testset "Non smooth function in 2D" begin
            global_min, minimisers = minimise( X -> ( (x,y) = X; abs(x-y) + abs(x) ), (-1..1) × (-1..1), structure = Structure )
            @test global_min == 0..0 # it must return the exact minimum (if mid is used to find the first global_min)
            @test all(X ⊆ (-2e-3.. 2e-3) × (-2e-3..2e-3) for X in minimisers)
        end

    end

end