################################################################################
#
#  Generic group given by multiplication table
#
################################################################################

################################################################################
#
#  Types
#
################################################################################

# The elements are just 1..n and i * j = G.mult_table[i, j]
mutable struct GrpGen
  identity::Int
  order::Int
  mult_table::Array{Int, 2}
  gens::Array{Int, 1}
  isabelian::Bool
  iscyclic::Bool
  issolvable::Int
  isnilpotent::Int

  function GrpGen(M::Array{Int, 2})
    z = new()
    z.mult_table = M
    z.identity = find_identity(z)
    z.order = size(M, 1)
    z.issolvable = 0
    z.isnilpotent = 0
    # Because it is cheap, let us check some properties
    n = z.order
    z.isabelian = defines_abelian_group(M)
    e = eulerphi(n)
    # There are e generators in case it is cyclic
    # If I find n - e + 1 elements of the wrong order, we are done
    z.iscyclic = false
    for i in 1:(n - e + 1)
      if order(z[i]) == n
        z.iscyclic = true
        z.gens = Int[i]
        break
      end
    end

    return z
  end
end

struct GrpGenElem
  group::GrpGen
  i::Int
end

################################################################################
#
#  Compute generic group from anything
#
################################################################################

function generic_group(G, op)
  Gen = GrpGen(_multiplication_table(G, op))
  GentoG = Dict{GrpGenElem, eltype(G)}(Gen[i] => G[i] for i in 1:length(G))
  GtoGen = Dict{eltype(G), GrpGenElem}(G[i] => Gen[i] for i in 1:length(G))
  return Gen, GtoGen, GentoG
end

################################################################################
#
#  Basic functionality
#
################################################################################

elem_type(::Type{GrpGen}) = GrpGenElem

elem_type(::GrpGen) = GrpGenElem

Base.hash(G::GrpGenElem, h::UInt) = Base.hash(G.i, h)

function Base.deepcopy_internal(g::GrpGenElem, dict::IdDict)
  return GrpGenElem(g.group, g.i)
end

################################################################################
#
#  Parent
#
################################################################################

function parent(g::GrpGenElem)
  return g.group
end

################################################################################
#
#  Construct the ith element
#
################################################################################

function getindex(G::GrpGen, i::Int)
  return GrpGenElem(G, i)
end

################################################################################
#
#  Equality
#
################################################################################

function ==(g::GrpGenElem, h::GrpGenElem)
  return parent(g) === parent(h) && g.i == h.i
end

################################################################################
#
#  Order
#
################################################################################

function order(G::GrpGen)
  return size(G.mult_table, 1)
end

length(G::GrpGen) = order(G)

################################################################################
#
#  Order of an element
#
################################################################################

function order(g::GrpGenElem)
  k = 2
  h = g * g
  while h != g
    h = g * h
    k = k + 1
  end
  return k - 1
end

################################################################################
#
#  Identity
#
################################################################################

function find_identity(G::GrpGen)
  return _find_identity(G.mult_table)
end

function _find_identity(m::Array{Int, 2})
  return find_identity([1], (i, j) -> m[i, j])
end

function id(G::GrpGen)
  return GrpGenElem(G, G.identity)
end

################################################################################
#
#  Multiplication
#
################################################################################

function *(g::GrpGenElem, h::GrpGenElem)
  G = parent(g)
  return GrpGenElem(G, G.mult_table[g.i, h.i])
end

################################################################################
#
#  Inverse
#
################################################################################

function inv(g::GrpGenElem)
  G = parent(g)
  m = G.mult_table
  i = g.i
  l = size(m, 1)
  ide = id(G)
  for j in 1:l
    if m[i, j] == G.identity
      return GrpGenElem(G, j)
    end
  end
  error("Not possible")
end

################################################################################
#
#  String I/O
#
################################################################################

function Base.show(io::IO, G::GrpGen)
  print(io, "Generic group of order ", order(G), " with multiplication table")
end

function Base.show(io::IO, g::GrpGenElem)
  print(io, "($(g.i))\n")
end

################################################################################
#
#  Iterator interface
#
################################################################################

Base.eltype(::Type{GrpGen}) = GrpGenElem

function Base.iterate(G::GrpGen, state::Int = 1)
  if state > G.order
    return nothing
  end

  return G[state], state + 1
end

################################################################################
#
#  Generators
#
################################################################################

function gens(G::GrpGen)
  if isdefined(G, :gens)
    return [G[i] for i in G.gens]
  else
    S = small_generating_set(collect(G), *, id(G))
    G.gens = [g.i for g in S]
    return S
  end
end

################################################################################
#
#  Cyclic subgroups
#
################################################################################

# Check if H is invariant under conjugaction by the generators of the group.
function _isnormal(H::Vector{GrpGenElem})
  S = gens(parent(H[1]))
  for s in S
    for h in H
      if !(inv(s) * h * s in H)
        return false
      end
    end
  end

  return true
end

# Check if the cyclic group H with generator gen is invariant under
# conjugaction by the generators of the group.
function _isnormal(H::Vector{GrpGenElem}, gen::GrpGenElem)
  S = gens(parent(H[1]))
  for s in S
    if !(inv(s) * gen * s in H)
      return false
    end
  end

  return true
end

# Compute the cyclic subgroups naively.
# TODO: Improve this
function _cyclic_subgroups(G::GrpGen; normal::Bool = false)
  res = Vector{GrpGenElem}[]
  res_elem = GrpGenElem[]
  idG = id(G)
  for g in G
    S = closure([g], *, idG)
    if normal && !_isnormal(S, g)
      continue
    end

    h = first(sort!([ s for s in S if order(s) == length(S)], by = x -> x.i))
    if h in res_elem
      continue
    else
      sort!(S, by = x -> x.i)
      @assert !(S in res)
      push!(res, S)
      push!(res_elem, h)
    end
  end

  return res, res_elem
end

################################################################################
#
#  Subgroup algorithm
#
################################################################################

# Iteratively built up subgroups from cyclic groups.
# Any subgroup is of the form <C_1,...C_k>, where k are cyclic subgroups.
function _subgroups_all(G::GrpGen; normal::Bool = false)
  res = Vector{GrpGenElem}[]
  res_gens = Vector{GrpGenElem}[]
  cur_grps, Cgen = _cyclic_subgroups(G)
  cur = Vector{GrpGenElem}[GrpGenElem[g] for g in Cgen]
  old = cur
  ngenext = Vector{GrpGenElem}[]
  while true
    new = Vector{GrpGenElem}[]
    for c in old
      for cy in Cgen
        n = push!(copy(c), cy)
        S = sort!(closure(n, *), by = x -> x.i)
        sort!(n, by = x -> x.i)
        if !(S in cur_grps)
          push!(new, n)
          push!(cur_grps, S)
        end
      end
    end

    if length(new) == 0
      break
    else
      append!(cur, new)
    end
    old = new
  end
  if normal
    return [cur_grps[i] for i in 1:length(cur_grps) if _isnormal(cur_grps[i])]
  else
    return cur_grps
  end
end

function subgroups(G::GrpGen; order::Int = 0,
                              index::Int = 0,
                              normal::Bool = false,
                              conjugacy_classes::Bool = false)
  H = _subgroups_all(G, normal = normal)
  if order > 0
    HH = Vector{Vector{GrpGenElem}}[h for h in H if length(h) == order]
  elseif index > 0
    HH = [Vector{Vector{GrpGenElem}}h for h in H if divexact(length(G), length(h)) == index]
  else
    HH = H
  end

  if conjugacy_classes
    HHH = Vector{Vector{GrpGenElem}}()
    for S in HH
      new = true
      for g in G
        Sg = sort!(GrpGenElem[g * s * inv(g) for s in S], by = x -> x.i)
        if any(isequal(Sg), HHH)
          new = false
          break
        end
      end
      if new
        push!(HHH, S)
      end
    end
    HH = HHH
  end

  res = Vector{Tuple{GrpGen, GrpGenToGrpGenMor}}(undef, length(HH))
  for i in 1:length(HH)
    res[i] = subgroup(G, HH[i])
  end
  return res
end

# Assume that H is a subgroup of G, compute a generic group and an embedding.
function subgroup(G::GrpGen, H::Vector{GrpGenElem})
  Hgen, = generic_group(H, *)
  m = GrpGenToGrpGenMor(Hgen, G, H)
  return Hgen, m
end

################################################################################
#
#  Predicates
#
################################################################################

function isabelian(G::GrpGen)
  return G.isabelian
end

function defines_abelian_group(m)
  return issymmetric(m)
end

function iscyclic(G::GrpGen)
  return G.isscyclic
end

################################################################################
#
#  Morphism type
#
################################################################################

mutable struct GrpGenToGrpGenMor <: Map{GrpGen, GrpGen, HeckeMap, GrpGen}

  domain::GrpGen
  codomain::GrpGen
  img::Vector{GrpGenElem}
  preimg::Vector{GrpGenElem}

  function GrpGenToGrpGenMor(G::GrpGen, H::GrpGen, image::Vector{GrpGenElem})
    z = new()
    z.domain = G
    z.codomain = H
    z.img = image
    return z
  end
end

function image(f::GrpGenToGrpGenMor, g::GrpGenElem)
  return f.img[g.i]
end

function Base.show(io::IO, f::GrpGenToGrpGenMor)
  println(io, "Morphism from group\n", f.domain, " to\n", f.codomain)
end

domain(f::GrpGenToGrpGenMor) = f.domain

codomain(f::GrpGenToGrpGenMor) = f.codomain

################################################################################
#
#  Normalizer
#
################################################################################

function normalizer(G::GrpGen, mH::GrpGenToGrpGenMor)
  C = left_cosets(G, mH)
  H = GrpGenElem[mH(h) for h in domain(mH)]
  ge = GrpGenElem[mH(h) for h in gens(domain(mH))]
  norm = GrpGenElem[]
  for c in C
    isnorm = true
    for h in ge
      if !(inv(c) * h * c in H)
        isnorm = false
        break
      end
    end
    if isnorm
      push!(norm, c)
    end
  end
  append!(H, norm)
  unique!(H)
  H = closure(H, *)
  return subgroup(G, H)
end

function left_cosets(G::GrpGen, mH::GrpGenToGrpGenMor)
  H = GrpGenElem[mH(h) for h in domain(mH)]
  GG = collect(G)
  cosets = GrpGenElem[id(G)]
  setdiff!(GG, H)
  while !isempty(GG)
    h = pop!(GG)
    push!(cosets, h)
    setdiff!(GG, (h * hh for hh in H))
  end
  return cosets
end

function right_cosets(G::GrpGen, mH::GrpGenToGrpGenMor)
  H = GrpGenElem[mH(h) for h in domain(mH)]
  GG = collect(G)
  cosets = GrpGenElem[id(G)]
  setdiff!(GG, H)
  while !isempty(GG)
    h = pop!(GG)
    push!(cosets, h)
    setdiff!(GG, (hh * h for hh in H))
  end
  return cosets
end

###############################################################################
#
#  NfToNfMor closure
#
###############################################################################

function closure(S::Vector{NfToNfMor}, final_order::Int = -1)

  K = domain(S[1])
  d = numerator(discriminant(K.pol))
  p = 11
  while mod(d, p) == 0
    p = next_prime(p)
  end
  R = GF(p, cached = false)
  Rx, x = PolynomialRing(R, "x", cached = false)
  fmod = Rx(K.pol)

  t = length(S)
  order = 1
  elements = [id_hom(K)]
  pols = gfp_poly[x]
  gpol = Rx(S[1].prim_img)
  if gpol != x
    push!(pols, gpol)
    push!(elements, S[1])
    order += 1

    gpol = compose_mod(gpol, pols[2], fmod)

    while gpol != x
      order = order +1
      push!(elements, S[1]*elements[end])
      push!(pols, gpol)
      gpol = compose_mod(gpol, pols[2], fmod)
    end
  end
  if order == final_order
    return elements
  end

  for i in 2:t
    if !(S[i] in elements)
      pi = Rx(S[i].prim_img)
      previous_order = order
      order = order + 1
      push!(elements, S[i])
      push!(pols, Rx(S[i].prim_img))
      for j in 2:previous_order
        order = order + 1
        push!(pols, compose_mod(pols[j], pi, fmod))
        push!(elements, elements[j]*S[i])
      end
      if order == final_order
        return elements
      end
      rep_pos = previous_order + 1
      while rep_pos <= order
        for k in 1:i
          s = S[k]
          po = Rx(s.prim_img)
          att = compose_mod(pols[rep_pos], po, fmod)
          if !(att in pols)
            elt = elements[rep_pos]*s
            order = order + 1
            push!(elements, elt)
            push!(pols, att)
            for j in 2:previous_order
              order = order + 1
              push!(pols, compose_mod(pols[j], att, fmod))
              push!(elements, elements[j] *elt)
            end
            if order == final_order
              return elements
            end
          end
        end
        rep_pos = rep_pos + previous_order
      end
    end
  end
  return elements
end

function small_generating_set(Aut::Array{NfToNfMor, 1})
  K = domain(Aut[1])
  a = gen(K)
  Identity = Aut[1]
  for i in 1:length(Aut)
    Au = Aut[i]
    if Au.prim_img == a
      Identity = Aut[i]
      break
    end
  end
  return small_generating_set(Aut, *, Identity)
end

################################################################################
#
#  Automorphisms
#
################################################################################

# TODO: Cache the orders of the generators of the small_groups.
#       Do not recompute it here.
function find_small_group(G::GrpGen)
  l = order(G)

  elements_by_orders = Dict{Int, Array{GrpGenElem, 1}}()

  for i in 1:l
    g = G[i]
    o = order(g)
    if haskey(elements_by_orders, o)
      push!(elements_by_orders[o], g)
    else
      elements_by_orders[o] = [g]
    end
  end

  candidates = Int[]

  local ordershape

  for j in 1:length(small_groups_1_63[order(G)])
    ordershape = small_groups_1_63[order(G)][j][4]

    candidate = true
    for (o, no) in ordershape
      if !haskey(elements_by_orders, o)
        candidate = false
        break
      else
        elts = elements_by_orders[o]
        if length(elts) != no
          candidate = false
          break
        end
      end
     end

     if candidate
        push!(candidates, j)
     end
  end

  @assert length(candidates) > 0
  println("Candidate groups are $candidates")

  sort!(candidates, rev = true)

  idG = id(G)

  for j in candidates
    H = small_groups_1_63[order(G)][j]

    elbyord = [elements_by_orders[order(o)] for o in H[1]]

    it = Iterators.product(elbyord...)

    words = H[2]

    for poss in it
      is_hom = true
      for w in words
        if eval_word(collect(poss), w) != idG
          is_hom = false
          break
        end
      end

      if is_hom
        if length(closure(collect(poss), *, idG)) == order(G)
          return order(G), j
        end
      end
    end
  end
  throw(error("Could not identify group"))
end

function eval_word(S, w::Vector{Int})
  g = id(parent(S[1]))
  for i in 1:length(w)
    if w[i] > 0
      g = g * S[w[i]]
    else
      g = g * inv(S[-w[i]])
    end
  end
  return g
end

function automorphisms(i, j)
  Gdata = small_groups_1_63[i][j]
  P = PermGroup(i)
  G, _  = generic_group(closure([P(p) for p in Gdata[1]], *), *)

  l = order(G)

  elements_by_orders = Dict{Int, Array{GrpGenElem, 1}}()

  for i in 1:l
    g = G[i]
    o = order(g)
    if haskey(elements_by_orders, o)
      push!(elements_by_orders[o], g)
    else
      elements_by_orders[o] = [g]
    end
  end

  elbyord = [elements_by_orders[order(o)] for o in Gdata[1]]

  it = Iterators.product(elbyord...)

  words::Vector{Vector{Int}} = Gdata[2]

  idG = id(G)

  auts = _aut_group(it, words, idG, order(G))::Vector{Vector{GrpGenElem}}

  return auts
end

@noinline function _aut_group(it, words, idG, n)
  auts = Vector{GrpGenElem}[]
  for poss in it
    is_hom = true
    for w in words
      if eval_word(poss, w) != idG
        is_hom = false
        break
      end
    end

    if is_hom
      cposs = collect(poss)
      if length(closure(cposs, *, idG)) == n
        push!(auts, cposs)
      end
    end
  end

  return auts
end


using MathOptInterface, GalacticOptim, Optim, Test, Random, Flux, ForwardDiff, Zygote
using Nonconvex
Nonconvex.@load MMA
Nonconvex.@load Ipopt
Nonconvex.@load NLopt
Nonconvex.@load BayesOpt
Nonconvex.@load Juniper
Nonconvex.@load Pavito
Nonconvex.@load Hyperopt
Nonconvex.@load MTS

rosenbrock(x, p) =  (p[1] - x[1])^2 + p[2] * (x[2] - x[1]^2)^2
x0 = zeros(2)
_p  = [1.0, 100.0]

f = OptimizationFunction(rosenbrock, GalacticOptim.AutoForwardDiff())
l1 = rosenbrock(x0, _p)
prob = OptimizationProblem(f, x0, _p)
Random.seed!(1234)
sol = solve(prob, SimulatedAnnealing())
@test 10*sol.minimum < l1

Random.seed!(1234)
prob = OptimizationProblem(f, x0, _p, lb=[-1.0, -1.0], ub=[0.8, 0.8])
sol = solve(prob, SAMIN())
@test 10*sol.minimum < l1

using CMAEvolutionStrategy
sol = solve(prob, CMAEvolutionStrategyOpt())
@test 10*sol.minimum < l1

rosenbrock(x, p) =  (1 - x[1])^2 + 100 * (x[2] - x[1]^2)^2
prob = OptimizationProblem(rosenbrock, x0)
sol = solve(prob, Optim.NelderMead())
@test 10*sol.minimum < l1

cons= (x,p) -> [x[1]^2 + x[2]^2]
optprob = OptimizationFunction(rosenbrock, GalacticOptim.AutoForwardDiff();cons= cons)

prob = OptimizationProblem(optprob, x0)

sol = solve(prob, Flux.ADAM(0.1), maxiters = 1000)
@test 10*sol.minimum < l1

sol = solve(prob, Optim.BFGS())
@test 10*sol.minimum < l1

sol = solve(prob, Optim.Newton())
@test 10*sol.minimum < l1

sol = solve(prob, Optim.KrylovTrustRegion())
@test 10*sol.minimum < l1

prob = OptimizationProblem(optprob, x0, lcons = [-Inf], ucons = [Inf])
sol = solve(prob, IPNewton())
@test 10*sol.minimum < l1

prob = OptimizationProblem(optprob, x0, lcons = [-5.0], ucons = [10.0])
sol = solve(prob, IPNewton())
@test 10*sol.minimum < l1

prob = OptimizationProblem(optprob, x0, lcons = [-Inf], ucons = [Inf], lb = [-500.0,-500.0], ub=[50.0,50.0])
sol = solve(prob, IPNewton())
@test sol.minimum < l1

function con2_c(x,p)
    [x[1]^2 + x[2]^2, x[2]*sin(x[1])-x[1]]
end

optprob = OptimizationFunction(rosenbrock, GalacticOptim.AutoForwardDiff();cons= con2_c)
prob = OptimizationProblem(optprob, x0, lcons = [-Inf,-Inf], ucons = [Inf,Inf])
sol = solve(prob, IPNewton())
@test 10*sol.minimum < l1

cons_circ = (x,p) -> [x[1]^2 + x[2]^2]
optprob = OptimizationFunction(rosenbrock, GalacticOptim.AutoForwardDiff();cons= cons_circ)
prob = OptimizationProblem(optprob, x0, lcons = [-Inf], ucons = [0.25^2])
sol = solve(prob, IPNewton())
@test sqrt(cons(sol.u,nothing)[1]) ≈ 0.25 rtol = 1e-6

optprob = OptimizationFunction(rosenbrock, GalacticOptim.AutoZygote())

prob = OptimizationProblem(optprob, x0)
sol = solve(prob, Flux.ADAM(), maxiters = 1000, progress = false)
@test 10*sol.minimum < l1

prob = OptimizationProblem(optprob, x0, lb=[-1.0, -1.0], ub=[0.8, 0.8])
sol = solve(prob, Optim.Fminbox())
@test 10*sol.minimum < l1

Random.seed!(1234)
prob = OptimizationProblem(optprob, x0, lb=[-1.0, -1.0], ub=[0.8, 0.8])
sol = solve(prob, Optim.SAMIN())
@test 10*sol.minimum < l1

optprob = OptimizationFunction((x,p) -> -rosenbrock(x,p), GalacticOptim.AutoZygote())
prob = OptimizationProblem(optprob, x0; sense = GalacticOptim.MaxSense)

import Ipopt
sol = solve(prob, Ipopt.Optimizer())
@test 10*sol.minimum < l1

optprob = OptimizationFunction(rosenbrock, GalacticOptim.AutoZygote())
prob = OptimizationProblem(optprob, x0, _p; sense = GalacticOptim.MinSense)

sol = solve(prob, Ipopt.Optimizer())
@test 10*sol.minimum < l1

sol = solve(prob, GalacticOptim.MOI.OptimizerWithAttributes(Ipopt.Optimizer, "max_cpu_time" => 60.0))
@test 10*sol.minimum < l1

import NLopt
prob = OptimizationProblem(optprob, x0)
sol = solve(prob, NLopt.Opt(:LN_BOBYQA, 2))
@test 10*sol.minimum < l1

sol = solve(prob, GalacticOptim.MOI.OptimizerWithAttributes(NLopt.Optimizer, "algorithm" => :LN_BOBYQA))
@test 10*sol.minimum < l1

sol = solve(prob, NLopt.Opt(:LD_LBFGS, 2))
@test 10*sol.minimum < l1

sol = solve(prob, GalacticOptim.MOI.OptimizerWithAttributes(NLopt.Optimizer, "algorithm" => :LD_LBFGS))
@test 10*sol.minimum < l1

prob = OptimizationProblem(optprob, x0, lb=[-1.0, -1.0], ub=[0.8, 0.8])
sol = solve(prob, NLopt.Opt(:LD_LBFGS, 2))
@test 10*sol.minimum < l1

sol = solve(prob, GalacticOptim.MOI.OptimizerWithAttributes(NLopt.Optimizer, "algorithm" => :LD_LBFGS))
@test 10*sol.minimum < l1

sol = solve(prob, NLopt.Opt(:G_MLSL_LDS, 2), local_method = NLopt.Opt(:LD_LBFGS, 2), maxiters=10000)
@test 10*sol.minimum < l1

prob = OptimizationProblem(optprob, x0)
sol = solve(prob, NLopt.LN_BOBYQA())
@test 10*sol.minimum < l1

sol = solve(prob, NLopt.LD_LBFGS())
@test 10*sol.minimum < l1

prob = OptimizationProblem(optprob, x0, lb=[-1.0, -1.0], ub=[0.8, 0.8])
sol = solve(prob, NLopt.LD_LBFGS())
@test 10*sol.minimum < l1

sol = solve(prob, NLopt.G_MLSL_LDS(), local_method = NLopt.LD_LBFGS(), local_maxiters=10000, maxiters=10000, population=10)
@test 10*sol.minimum < l1

# using MultistartOptimization
# sol = solve(prob, MultistartOptimization.TikTak(100), local_method = NLopt.LD_LBFGS())
# @test 10*sol.minimum < l1

# using QuadDIRECT
# sol = solve(prob, QuadDirect(); splits = ([-0.5, 0.0, 0.5],[-0.5, 0.0, 0.5]))
# @test 10*sol.minimum < l1

using Evolutionary
prob = GalacticOptim.OptimizationProblem(optprob, x0)
sol = solve(prob, CMAES(μ =40 , λ = 100),abstol=1e-15)
@test 10*sol.minimum < l1

using BlackBoxOptim
prob = GalacticOptim.OptimizationProblem(optprob, x0, lb=[-1.0, -1.0], ub=[0.8, 0.8])
sol = solve(prob, BBO_adaptive_de_rand_1_bin_radiuslimited())
@test 10*sol.minimum < l1

using Metaheuristics
prob = GalacticOptim.OptimizationProblem(f, x0, lb=[-1.0, -1.0], ub=[1.5, 1.5])
sol = solve(prob, ECA())
@test 10*sol.minimum < l1

sol = solve(prob, Metaheuristics.DE())
@test 10*sol.minimum < l1

sol = solve(prob, PSO())
@test 10*sol.minimum < l1

sol = solve(prob, ABC())
@test 10*sol.minimum < l1

sol = solve(prob, CGSA())
@test 10*sol.minimum < l1

sol = solve(prob, SA())
@test 10*sol.minimum < l1

sol = solve(prob, WOA())
@test 10*sol.minimum < l1

sol = solve(prob, ECA())
@test 10*sol.minimum < l1

sol = solve(prob, Metaheuristics.DE(), use_initial=true)
@test 10*sol.minimum < l1

sol = solve(prob, PSO(), use_initial=true)
@test 10*sol.minimum < l1

sol = solve(prob, ABC(), use_initial=true)
@test 10*sol.minimum < l1

sol = solve(prob, CGSA(), use_initial=true)
@test 10*sol.minimum < l1

sol = solve(prob, SA(), use_initial=true)
@test 10*sol.minimum < l1

sol = solve(prob, WOA(), use_initial=true)
@test 10*sol.minimum < l1

using ModelingToolkit
rosenbrock(x, p) = (p[1] - x[1])^2 + p[2] * (x[2] - x[1]^2)^2
x0 = zeros(2)
_p = [1.0, 100.0]

f = OptimizationFunction(rosenbrock,ModelingToolkit.AutoModelingToolkit(),x0,_p,grad=true,hess=true)
prob = OptimizationProblem(f,x0,_p)
sol = solve(prob,Optim.Newton())

### Nonconvex test
prob = GalacticOptim.OptimizationProblem(f, x0, _p, lb = [-1.0,-1.0], ub = [1.5,1.5])

sol = solve(prob, MMA02())
@test 10*sol.minimum < l1

sol = solve(prob, GCMMA())
@test 10*sol.minimum < l1

sol = solve(prob, IpoptAlg())
@test 10*sol.minimum < l1

sol = solve(prob, NLoptAlg(:LN_NELDERMEAD))
@test 10*sol.minimum < l1

sol = solve(prob, NLoptAlg(:LD_LBFGS))
@test 10*sol.minimum < l1

sol = solve(prob, MTSAlg())
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)))
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=LHSampler())
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=CLHSampler())
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=GPSampler())
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()))
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=LHSampler())
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=CLHSampler())
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=GPSampler())
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)))
@test 10*sol.minimum < l1

sol = solve(prob, MMA02(), maxiters=1000)
@test 10*sol.minimum < l1

sol = solve(prob, GCMMA(), maxiters=1000)
@test 10*sol.minimum < l1

sol = solve(prob, IpoptAlg(), maxiters=1000)
@test 10*sol.minimum < l1

sol = solve(prob, NLoptAlg(:LN_NELDERMEAD), maxiters=1000)
@test 10*sol.minimum < l1

sol = solve(prob, NLoptAlg(:LD_LBFGS), maxiters=1000)
@test 10*sol.minimum < l1

sol = solve(prob, MTSAlg(), maxiters=1000)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=LHSampler(), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=CLHSampler(), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=GPSampler(), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=LHSampler(), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=CLHSampler(), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=GPSampler(), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()), maxiters=1000)
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()), maxiters=1000)
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)), maxiters=100)
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)))
@test 10*sol.minimum < l1

### suboptions
sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=LHSampler(), sub_options=(;maxeval=100))
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=CLHSampler(), sub_options=(;maxeval=100))
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(NLoptAlg(:LN_NELDERMEAD)), sampler=GPSampler(), sub_options=(;maxeval=100))
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sub_options=(;max_iter=100))
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=LHSampler(), sub_options=(;max_iter=100))
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=CLHSampler(), sub_options=(;max_iter=100))
@test 10*sol.minimum < l1

sol = solve(prob, HyperoptAlg(IpoptAlg()), sampler=GPSampler(), sub_options=(;max_iter=100))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()), sub_options=(;max_iter=100))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()), sub_options=(;max_iter=100))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)), sub_options=(;maxeval=100))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)), sub_options=(;maxeval=100))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()), sub_options=(;max_iter=100))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(IpoptAlg()), sub_options=(;max_iter=100))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)), sub_options=(;maxeval=100))
@test 10*sol.minimum < l1

sol = solve(prob, BayesOptAlg(NLoptAlg(:LN_NELDERMEAD)), sub_options=(;maxeval=100))
@test 10*sol.minimum < l1


module LogArrayFunctions

export LogArray
type LogArray <: AbstractArray
    content
    function LogArray(content)
        new(content)
    end
end

import Base.display
function display(L::LogArray)
    return display(L.content)
end

import Base.size
function size(A::LogArray,I::Int64)
    return size(A.content,I)
end

function size(A::LogArray)
    return size(A.content)
end

import Base.permutedims
function permutedims(A::LogArray,I::Array{Int64,1})
    return permutedims(A.content,I)
end

function *(A::LogArray,B::LogArray)
    logprefactor=maximum([maximum(A.content[:]) maximum(B.content[:])]);
    AA=exp.(A.content-logprefactor)
    BB=exp.(B.content-logprefactor)
    L=LogArray(1);
    L.content=2*logprefactor+log.(AA*BB)
    return L
end

function +(A::LogArray,B::LogArray)
    logprefactor=maximum([maximum(A.content[:]) maximum(B.content[:])]);
    AA=exp.(A.content-logprefactor)
    BB=exp.(B.content-logprefactor)
    L=LogArray(1);
    L.content=logprefactor+log.(AA+BB)
    return L
end

import Base.sum
function sum(A::LogArray,I...)
    logprefactor=maximum(A.content[:])
    AA=exp.(A.content-logprefactor)
    L=LogArray(1)
    L.content=logprefactor+log.(sum(AA,I...))
    return L
end

import Base.ndims
function ndims(A::LogArray)
    return ndims(A.content)
end

import Base.getindex
function getindex(A::LogArray, I...)
    tmp=getindex(A.content,I...)
    return LogArray(tmp)
end

import Base.transpose
function transpose(A::LogArray)
    return LogArray((A.content)')
end

function .*(A::LogArray,B::LogArray)
    logprefactor=maximum([maximum(A.content[:]) maximum(B.content[:])]);
    AA=exp.(A.content-logprefactor)
    BB=exp.(B.content-logprefactor)
    L=LogArray(1)
    L.content=2*logprefactor+log.(AA.*BB)
    return L
end

end #end module


### A Pluto.jl notebook ###
# v0.17.1

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
        el
    end
end

# ╔═╡ 56807afc-164e-11ec-2cf4-354654176242
begin
	using Pkg
	Pkg.activate("../../")
	using Revise
end

# ╔═╡ 8a11c2b7-b6e5-4c26-9e06-ad3ab958db9c
using TestImages, ImageShow, Colors, ImgProcMic, Statistics, PlutoTest, PlutoUI

# ╔═╡ 3ccbf4fb-0707-4ce8-83d8-b1af555f3fe1
using PoissonRandom

# ╔═╡ 62c6e2f0-0007-40ec-81c5-61cda80f59e5
using ImageFiltering, IndexFunArrays

# ╔═╡ 31cc54e9-d47b-4df7-a785-dad0120e8cdb
md"## Activate our environment"

# ╔═╡ d8c83f2c-76ea-42b9-bd39-7fa5cafcd4d3
md"# Homework 01"

# ╔═╡ 334ebdc5-dc11-4e83-ad12-a1961420a97b
begin
	img = Float64.(testimage("mandril_gray"))
	gray_show(img)
end

# ╔═╡ 2122c12f-0832-4f19-9a77-3047d5311cba
md"## 1. Add Noise To Images

This homework is dedicated to introduce into the basics of Julia.
As examples, we want to apply and remove noise from images
"

# ╔═╡ a7d7a415-7464-43fb-a2bf-8e3f2d999c32
md"## Task 1.1  - Gaussian Noise
In the first step, we want to add Gaussian noise with a certain
standard deviation $\sigma$. Try to get yourself used to the Julia documentation
and check out whether such a function exists already and how you can apply it
to your image/array.

Codewords: **Julia, random, random normal distribution**
"

# ╔═╡ de104b0a-28a4-42de-ae8d-fa857a8f32e0
"""
	add_gauss_noise(img, σ=1)

This function adds normal distributed noise to `img`.
`σ` is an optional argument
"""
function add_gauss_noise(img, σ=one(img))
	# todo
	return similar(img)
end

# ╔═╡ f89bc1e4-d406-4550-988b-71496b64035a
md"
Now we want to use a for loop inside this function.
Try to find out how to write for loops to iterate through an array.
"

# ╔═╡ c0b5ff08-7342-4c25-9791-be3a4918c400
"""
	add_gauss_noise_fl!(img, σ=1)

This function adds normal distributed noise to `img`.
`σ` is an optional argument.
This function is memory efficient by using for loops.
`!` means that the input (`img`) is modified.
Therefore, don't return a new array but instead modify the existing one!
The bang (!) is a convention in Julia that a function modifies the input.
"""
function add_gauss_noise_fl!(img, σ=1)
	# todo
	return img
end

# ╔═╡ d56bc500-f5cf-40f3-8c0b-b4ae26a58367
md"### Tests 1.1
Don't modify but rather take those tests as input whether you are correct.
Green is excellent, red not :(
"

# ╔═╡ 1116455e-4698-489a-add2-4e1d8ecbb546
gray_show(add_gauss_noise(img, 0.15), set_one=true)

# ╔═╡ 8e343a3d-ab0f-45a1-9161-21fec07755c9
md"###### Type Stability of your functions.
That means that the output type is identical to the input type"

# ╔═╡ d865dddc-6d32-416b-ba12-8f6b3870e771
PlutoTest.@test Float32 == eltype(add_gauss_noise(ones(Float32, (512, 512)), 0.3f0))

# ╔═╡ 5c9a8c7b-e9c4-4177-86bb-b69113fce144
PlutoTest.@test Float32 == eltype(add_gauss_noise_fl!(ones(Float32, (512, 512)), 0.3))

# ╔═╡ cc7bbb55-b8dd-454b-b170-45137ab0c0b5
md"###### Check wether mean and standard deviation are correct"

# ╔═╡ c997c248-fba8-4ed8-ab75-179d248e6b83
PlutoTest.@test ≈(0.3, std(add_gauss_noise(ones((512, 512)), 0.3)), rtol=0.1)

# ╔═╡ 07bc00b7-81ad-4645-8fbe-8545ccdfe7e4
PlutoTest.@test ≈(0.3, std(add_gauss_noise_fl!(ones((512, 512)), 0.3)), rtol=0.1)

# ╔═╡ d317ed1a-1867-4293-8db4-51352212929c
PlutoTest.@test ≈(1, mean(add_gauss_noise(ones((512, 512)), 0.3)), rtol=0.1)

# ╔═╡ e69a8b77-833a-4498-8bc9-e4c970f6529d
md"## Task 1.2 - Poisson Noise
In microscopy and low light imaging situation, the dominant noise term is usually Poisson noise which we want to simulate here.

For adding Poisson noise we use: [PoissonRandom.jl](https://github.com/SciML/PoissonRandom.jl)

Read the documentation of this package how to generate Poisson Random numbers
"

# ╔═╡ 1e4b32cb-00ea-439f-900e-abadc850be95
"""
	add_poisson_noise!(img, scale_to=nothing)

This function adds poisson distributed noise to `img`.
Before adding noise, it scales the maximum value to `scale_to` and
divides by it afterwards.
With that we can set the number of events (like a photon count)

If `isnothing(scale_to) == true`, we don't modify/scale the array.

`!` means that the input is modified.
"""
function add_poisson_noise!(img, scale_to=nothing)
	# todo
	return img
end

# ╔═╡ 6cf8ff2a-4255-4c53-9376-5a0d2d372569
add_poisson_noise!(10 .* ones(Float32, (3, 3)))

# ╔═╡ e3fcd381-9652-4e1d-8585-a91f87b73ce5
md"### Test 1.2 - Poisson Noise

You probably encounter errors for Float32 types errors, try to put a `Float64(some_part)` at the right place.

Works like this:
* `typeof(1f0)` = $(typeof(1f0))
* `typeof(Float64(1f0))` = $(typeof(Float64(1f0)))

"

# ╔═╡ f3efcfcf-4516-43d2-9375-d8863b4b6f5b
gray_show(add_poisson_noise!(100 .* img), set_one=true)

# ╔═╡ 00c72e52-09fa-413b-acfd-83c760d42d7f
PlutoTest.@test Float32 == eltype(add_poisson_noise!(100 .* ones(Float32, (512, 512)), 0.3f0))

# ╔═╡ 5b68030f-557e-449e-8baf-cf7ae65e4425
mean(add_poisson_noise!(150 .* ones(Float64, (512, 512))))

# ╔═╡ 78a39a77-bc9d-427a-a113-4e64b0c9d311
PlutoTest.@test ≈(150, mean(add_poisson_noise!(150 .* ones(Float64, (512, 512)))), rtol=0.05)

# ╔═╡ 41118b3c-3e49-4fc7-8c80-7906b7cf91fe
PlutoTest.@test ≈(√(150), std(add_poisson_noise!(150 * ones(Float32, (512, 512)))), rtol=0.05)

# ╔═╡ f8be3a4b-d8f0-40cb-869e-81c17e58327a
md"###### Consider those tests as bonus"

# ╔═╡ 65a8981d-890b-4590-a913-6890ecf3817d
PlutoTest.@test ≈(150, 150 * mean(add_poisson_noise!(ones(Float32, (512, 512)), 150)), rtol=0.05)

# ╔═╡ 272c8afb-667f-49c7-af48-2feb1b9d06f7
PlutoTest.@test ≈(√(150), 150 * std(add_poisson_noise!(ones(Float32, (512, 512)), 150)), rtol=0.05)

# ╔═╡ 59482e49-d784-4303-9487-bf37f6a0462e
md"## 1.3 Hot Pixels

Another issues are hot pixels which show maximum value. This can be due to damaged pixels or some other noise (radioactivity, ...). Often this is called Salt (because white) noise

"

# ╔═╡ 6e050fc2-4db2-4e6e-abd4-2812b47c070f
"""
	add_hot_pixels(img, probability=0.1; max_value=one(eltype(img)))

Add randomly hot pixels. The probability for each pixel to be hot,
should be specified by `probability`.
`max_value` is a keyword argument which is the value the _hot_ pixel will have.
"""
function add_hot_pixels!(img, probability=0.1; max_value=one(eltype(img)))
	# todo
	return img
end

# ╔═╡ 22985061-9740-45e9-af56-e0787dadba7b
gray_show(add_hot_pixels!(copy(img)))

# ╔═╡ 42f92a8c-54bb-4033-9ef2-f5ffb78f4f3a
md"### 1.3 Test"

# ╔═╡ a9ad9d52-53c9-44ce-8edc-af8d7bfdc81f
PlutoTest.@test sum(map(x -> x ≈ 2, add_hot_pixels!(copy(img), 0.5, max_value=2))) ≈ length(img) .* 0.5 rtol=0.05

# ╔═╡ 373a3e39-fd8c-4cea-88bb-2b9c3734f14f
md"## 2. Remove Noise From Images"

# ╔═╡ 2c274829-ef33-4f34-9ba0-63d3b8cd7343
md"## 2.1 Remove Noise with Gaussian Blur Kernel

One option to remove noise is, is to blur the image with a Gaussian kernel.
We can convolve a small gaussian (odd sized) Kernel over the array.
For that we are using `ImageFiltering.imfilter` for the convolution.

To create a Gaussian shaped function, checkout `IndexFunArrays.normal`.
You probably want to normalize the sum of it again
"

# ╔═╡ 52ebebfc-940b-47e2-a059-40ec996f2f98
begin
	img_g = add_gauss_noise(img, 0.1)
	img_p = add_poisson_noise!(100 .* copy(img)) ./ 100
	img_h = add_hot_pixels!(copy(img))
end;

# ╔═╡ 2d6f25a5-b902-4e46-ac3f-cf452f525912
gray_show([img_g img_p img_h])

# ╔═╡ 75aa8212-8ba3-4fba-b8b2-af2dd9c82adf
# check out the help
imfilter

# ╔═╡ 6acf7ba6-445d-4ac0-a83f-6916318c783c
function gaussian_noise_remove(arr; kernel_size=(3,3), σ=1)
	# todo
	return similar(arr)
end

# ╔═╡ 4f683117-9a09-4837-b0af-5818ac2e62fd
md"σ = $(@bind σ Slider(0.01:0.1:10, show_value=true))"

# ╔═╡ 01203c2c-4e67-492c-9d9c-dc84a84afe15
md"kernel size = $(@bind ks Slider(3:2:21, show_value=true))"

# ╔═╡ daae9a7d-99f4-4d34-a7fd-886fa754d59b
begin
	img_p_gauss = gaussian_noise_remove(img_p, kernel_size=(ks,ks), σ=σ);
	img_g_gauss = gaussian_noise_remove(img_g, kernel_size=(ks,ks), σ=σ);
	img_h_gauss = gaussian_noise_remove(img_h, kernel_size=(ks,ks), σ=σ);
end;

# ╔═╡ 92d43328-29a2-45b2-ae71-e169dec02316
gray_show([img_p_gauss img_g_gauss img_h_gauss])

# ╔═╡ 512486eb-1919-446a-9f24-e9713546d237
md"### 2.1 Test"

# ╔═╡ 9b83d92d-f838-4329-b0b1-62bf7a18f123
arr_rand = add_gauss_noise(ones((500, 500)), 0.2);

# ╔═╡ bbbed1b7-a406-44a7-8c77-959a3eba0197
PlutoTest.@test std(gaussian_noise_remove(arr_rand, kernel_size=(8,8), σ=2)) < 0.05

# ╔═╡ 911c2e1e-f04a-4efa-9aff-7f4a424011ae
PlutoTest.@test sum(abs2, img_g .- img) > sum(abs2, img_g_gauss .- img)

# ╔═╡ 0f6ff33f-e50a-4317-a240-3b2f52de7a97
md"## 2.2 Noise Removal with Median Filter
The median filter slides with an quadratic shaped box over the arrays and
always takes the median value of this array.

"

# ╔═╡ 7bc0f845-d5d4-4192-be1d-b97750e95761
function median_noise_remove!(arr; kernel_size=(5,5))
	# todo
	return similar(arr)
end

# ╔═╡ ac6e7ea7-c277-4180-a809-a37531ab4f11
md"kernel\_size\_2 = $(@bind ks_2 Slider(3:2:9, show_value=true))"

# ╔═╡ 8208b936-ad5d-45a0-96d1-04fd131f1e29
begin
	img_p_median = median_noise_remove!(copy(img_p), kernel_size=(ks_2,ks_2));
	img_g_median = median_noise_remove!(copy(img_g), kernel_size=(ks_2,ks_2));
	img_h_median = median_noise_remove!(copy(img_h), kernel_size=(ks_2,ks_2));
end;

# ╔═╡ 06af0b94-f4e1-4114-b565-730b87567995
gray_show([img_p_median img_g_median img_h_median])

# ╔═╡ 707a3be6-ac83-421d-a8e1-31d75444aeef
md"### 2.2 Test"

# ╔═╡ 07f5e6da-7868-4e22-90aa-7a81c2b41ab3
PlutoTest.@test sum(abs2, img_p .- img) > sum(abs2, img_p_median .- img)

# ╔═╡ 3dbe2b38-8609-41e5-b3b9-d62da394b89d
PlutoTest.@test sum(abs2, img_g .- img) > sum(abs2, img_g_median .- img)

# ╔═╡ 867d5676-8dfc-4bb5-b555-73aa767f7e9e
md"## 3 Final Images"

# ╔═╡ 3605fda7-078e-4bff-9e33-91c4025e775a
gray_show([img_g img_p img_h])

# ╔═╡ c09b3470-95bd-4f49-b64b-26459c95ab19
gray_show([median_noise_remove!(copy(img_g)) median_noise_remove!(copy(img_p)) median_noise_remove!(copy(img_h))])

# ╔═╡ Cell order:
# ╟─31cc54e9-d47b-4df7-a785-dad0120e8cdb
# ╠═56807afc-164e-11ec-2cf4-354654176242
# ╠═8a11c2b7-b6e5-4c26-9e06-ad3ab958db9c
# ╠═d8c83f2c-76ea-42b9-bd39-7fa5cafcd4d3
# ╠═334ebdc5-dc11-4e83-ad12-a1961420a97b
# ╟─2122c12f-0832-4f19-9a77-3047d5311cba
# ╟─a7d7a415-7464-43fb-a2bf-8e3f2d999c32
# ╠═de104b0a-28a4-42de-ae8d-fa857a8f32e0
# ╟─f89bc1e4-d406-4550-988b-71496b64035a
# ╠═c0b5ff08-7342-4c25-9791-be3a4918c400
# ╟─d56bc500-f5cf-40f3-8c0b-b4ae26a58367
# ╠═1116455e-4698-489a-add2-4e1d8ecbb546
# ╟─8e343a3d-ab0f-45a1-9161-21fec07755c9
# ╠═d865dddc-6d32-416b-ba12-8f6b3870e771
# ╠═5c9a8c7b-e9c4-4177-86bb-b69113fce144
# ╟─cc7bbb55-b8dd-454b-b170-45137ab0c0b5
# ╠═c997c248-fba8-4ed8-ab75-179d248e6b83
# ╠═07bc00b7-81ad-4645-8fbe-8545ccdfe7e4
# ╠═d317ed1a-1867-4293-8db4-51352212929c
# ╟─e69a8b77-833a-4498-8bc9-e4c970f6529d
# ╠═3ccbf4fb-0707-4ce8-83d8-b1af555f3fe1
# ╠═1e4b32cb-00ea-439f-900e-abadc850be95
# ╠═6cf8ff2a-4255-4c53-9376-5a0d2d372569
# ╟─e3fcd381-9652-4e1d-8585-a91f87b73ce5
# ╠═f3efcfcf-4516-43d2-9375-d8863b4b6f5b
# ╠═00c72e52-09fa-413b-acfd-83c760d42d7f
# ╠═5b68030f-557e-449e-8baf-cf7ae65e4425
# ╠═78a39a77-bc9d-427a-a113-4e64b0c9d311
# ╠═41118b3c-3e49-4fc7-8c80-7906b7cf91fe
# ╠═f8be3a4b-d8f0-40cb-869e-81c17e58327a
# ╠═65a8981d-890b-4590-a913-6890ecf3817d
# ╠═272c8afb-667f-49c7-af48-2feb1b9d06f7
# ╟─59482e49-d784-4303-9487-bf37f6a0462e
# ╠═6e050fc2-4db2-4e6e-abd4-2812b47c070f
# ╠═22985061-9740-45e9-af56-e0787dadba7b
# ╟─42f92a8c-54bb-4033-9ef2-f5ffb78f4f3a
# ╠═a9ad9d52-53c9-44ce-8edc-af8d7bfdc81f
# ╟─373a3e39-fd8c-4cea-88bb-2b9c3734f14f
# ╟─2c274829-ef33-4f34-9ba0-63d3b8cd7343
# ╠═52ebebfc-940b-47e2-a059-40ec996f2f98
# ╠═2d6f25a5-b902-4e46-ac3f-cf452f525912
# ╠═62c6e2f0-0007-40ec-81c5-61cda80f59e5
# ╠═75aa8212-8ba3-4fba-b8b2-af2dd9c82adf
# ╠═6acf7ba6-445d-4ac0-a83f-6916318c783c
# ╟─4f683117-9a09-4837-b0af-5818ac2e62fd
# ╟─01203c2c-4e67-492c-9d9c-dc84a84afe15
# ╠═daae9a7d-99f4-4d34-a7fd-886fa754d59b
# ╠═92d43328-29a2-45b2-ae71-e169dec02316
# ╟─512486eb-1919-446a-9f24-e9713546d237
# ╠═9b83d92d-f838-4329-b0b1-62bf7a18f123
# ╠═bbbed1b7-a406-44a7-8c77-959a3eba0197
# ╠═911c2e1e-f04a-4efa-9aff-7f4a424011ae
# ╟─0f6ff33f-e50a-4317-a240-3b2f52de7a97
# ╠═7bc0f845-d5d4-4192-be1d-b97750e95761
# ╟─ac6e7ea7-c277-4180-a809-a37531ab4f11
# ╠═8208b936-ad5d-45a0-96d1-04fd131f1e29
# ╠═06af0b94-f4e1-4114-b565-730b87567995
# ╟─707a3be6-ac83-421d-a8e1-31d75444aeef
# ╠═07f5e6da-7868-4e22-90aa-7a81c2b41ab3
# ╠═3dbe2b38-8609-41e5-b3b9-d62da394b89d
# ╟─867d5676-8dfc-4bb5-b555-73aa767f7e9e
# ╠═3605fda7-078e-4bff-9e33-91c4025e775a
# ╠═c09b3470-95bd-4f49-b64b-26459c95ab19


module Make_AEM_Synth
using PyPlot, GP, Random, UseGA_AEM, TransD_GP

function geomprogdepth(n, dy, c)
    dy*(1.0-c^n)/(1-c)
end

function getn(z, dy, c)
    log(1 - z/dy*(1-c))/log(c)
end

function main()
    n, dz, extendfrac  = 52, 1.1, 1.05
    znrange            = 1.0:n
    zboundaries        = geomprogdepth.(znrange, dz, extendfrac)
    thickness          = [zboundaries[1]; diff(zboundaries)[1:end-1]]
    zall               = [zboundaries[1]/2; 0.5*(zboundaries[1:end-1] + zboundaries[2:end])]
    znall              = getn.(zall, dz, extendfrac)

    figure()
    plot(znall, zall)
    xlabel("depth index")
    ylabel("depth associated")
    grid()
    nicenup(gcf())

    f, ax = plt.subplots(1, 2, figsize=(10,5))
    ax[1].stem(zboundaries[1:end-1], zboundaries[1:end-1], markerfmt="")
    ax[1].stem(zall, zall, "k--", markerfmt=" ")
    ax[1].set_xlabel("depth m")
    ax[1].set_ylabel("depth m")

    ax[2].stem(znrange[1:end-1], znrange[1:end-1], markerfmt="")
    ax[2].stem(znall, znall, "k--", markerfmt=" ")
    ax[2].set_ylabel("depth index")
    ax[2].set_xlabel("depth index")
    nicenup(gcf())

    f, ax = plt.subplots(1, 2, figsize=(10,5), sharey=true)
    ax[1].stem(zall[1:end-1],thickness, "k--", markerfmt=" ")
    ax[1].set_ylabel("thickness m")
    ax[1].yaxis.grid(which="major")
    ax[2].stem(znall[1:end-1],thickness, "k--", markerfmt=" ")
    ax[2].set_xlabel("depth index")
    ax[2].yaxis.grid(which="major")
    nicenup(f)

    λ, δ = [2.0], 0.01
end

function nicenup(g::PyPlot.Figure;fsize=14)
    for ax in gcf().axes
        ax.tick_params("both",labelsize=fsize)
        ax.xaxis.label.set_fontsize(fsize)
        ax.yaxis.label.set_fontsize(fsize)
        ax.title.set_fontsize(fsize)
        if typeof(ax.get_legend_handles_labels()[1]) != Array{Any,1}
            ax.legend(loc="best", fontsize=fsize)
        end
    end
    g.tight_layout()
end

end


@testset "common_covmat_ops" begin
    # Supporting utility functions.
    @testset "_cholesky" begin
        D = Diagonal(Fill(5.0, 10))
        @test AbstractGPs._cholesky(D).U ≈ AbstractGPs._cholesky(collect(D)).U
    end

    @testset "_symmetric" begin
        @testset "Matrix" begin
            X = randn(5, 5)
            @test AbstractGPs._symmetric(X) isa Symmetric
            @test collect(AbstractGPs._symmetric(X)) == collect(Symmetric(X))
        end
        @testset "Diagonal" begin
            X = Diagonal(randn(5))
            @test AbstractGPs._symmetric(X) isa Diagonal
            @test collect(AbstractGPs._symmetric(X)) == collect(Symmetric(X))
        end
    end

    @testset "update_chol" begin
        X = rand(5)
        y = rand(5)

        k = SqExponentialKernel()

        C = kernelmatrix(k, X, X)
        U = cholesky(C).U
        C11 = kernelmatrix(k, X[1:3], X[1:3])
        chol1 = cholesky(C11)
        C22 = kernelmatrix(k, X[4:5], X[4:5])
        C12 = kernelmatrix(k, X[1:3], X[4:5])
        chol = AbstractGPs.update_chol(chol1, C12, C22)

        @test U ≈ chol.U atol = 1e-5
        @test chol1.U ≈ chol.U[1:3, 1:3] atol = 1e-5
    end

    # Set up some matrices and factorisations.
    rng, N, N′, P, Q = MersenneTwister(123456), 5, 3, 6, 2
    B = randn(rng, N, N)
    A_ = B' * B + UniformScaling(1e-6)
    A = cholesky(A_)
    x, y, z = randn(rng, N), randn(rng, N′), randn(rng, N)
    X, Y = randn(rng, N, P), randn(rng, N, Q)
    Z = randn(rng, N, P)
    A_1_ = exp(randn(rng))
    A_1 = cholesky(A_1_)

    # Specialised matrix operations.
    @test Xt_A_X(A, x) isa Real
    @test Xt_A_X(A, x) ≈ x' * A_ * x

    @test Xt_A_X(A_1, x') isa Symmetric
    @test Xt_A_X(A_1, x') ≈ x * A_1_ * x'

    @test Xt_A_X(A, X) isa Symmetric
    @test Xt_A_X(A, X) ≈ X' * A_ * X

    @test Xt_A_Y(x, A, z) isa Real
    @test Xt_A_Y(x, A, z) ≈ x' * A_ * z

    @test Xt_A_Y(x', A_1, y') isa Matrix
    @test Xt_A_Y(x', A_1, y') ≈ x * A_1_ * y'

    @test Xt_A_Y(X, A, Y) isa Matrix
    @test Xt_A_Y(X, A, Y) ≈ X' * A_ * Y

    @test Xt_invA_X(A, x) isa Real
    @test Xt_invA_X(A, x) ≈ x' * (A \ x)

    @test Xt_invA_X(A, X) isa Symmetric
    @test Xt_invA_X(A, X) ≈ X' * (A \ X)

    @test Xt_invA_Y(X, A, Y) isa Matrix
    @test Xt_invA_Y(X, A, Y) ≈ X' * (A \ Y)

    @test diag_At_A(x) ≈ [x'x]
    @test diag_At_A(X) ≈ diag(X'X)

    @test diag_At_B(x, z) ≈ [x'z]
    @test diag_At_B(X, Z) ≈ diag(X'Z)

    @test diag_Xt_A_X(A, x) ≈ [Xt_A_X(A, x)]
    @test diag_Xt_A_X(A, X) ≈ diag(Xt_A_X(A, X))

    @test diag_Xt_A_Y(x, A, z) ≈ [x' * A_ * z]
    @test diag_Xt_A_Y(X, A, Z) ≈ diag(Xt_A_Y(X, A, Z))

    @test diag_Xt_invA_X(A, X) ≈ diag(Xt_invA_X(A, X))

    @test diag_Xt_invA_Y(X, A, Z) ≈ diag(Xt_invA_Y(X, A, Z))

    @test Xtinv_A_Xinv(A, A) isa Symmetric
    @test Xtinv_A_Xinv(A, A) ≈ A \ (A \ A_)'

    @testset "tr_At_A" begin
        rng, P = MersenneTwister(123456), 11
        X = randn(rng, P, P)

        @test tr_At_A(X) ≈ tr(X'X)
        adjoint_test(tr_At_A, randn(rng), X)
    end
end


using Lathe.stats: Distribution
# ---- Rescalar (Standard Deviation) ---
"""
    ## Rescalar
    ### Description
      Rescales an array.\n
      --------------------\n
    ### Input
      Rescaler(x)\n
      --------------------\n
      #### Positional Arguments
      Array{Any} - x:: Array for which the original scaler should be based
      off of.\n
      --------------------\n
     ### Output
     scalar :: A Lathe Preprocesser object.
     ---------------------\n
     ### Functions
     Preprocesser.predict(xt) :: Applies the scaler to xt.\n
     ---------------------\n
     ### Data
     min :: The minimum value in the array.\n
     max :: The maximum value in the array.
       """
mutable struct Rescaler{P} <: Scaler
    predict::P
    min::Float64
    max::Float64
    function Rescalar(x::Array)
        a = minimum(x)
        b = maximum(x)
        predict(x::Array) = [i = (i-min) / (max - min) for i in x]
        predict(df::DataFrame, symb::Symbol) = [i = (i-min) / (max - min) for i in df[!, symb]]
        P = typeof(predict)
        return new{P}(predict, a, b)
    end
end
# ---- Arbitrary Rescalar ----
"""
    ## Arbitrary Rescaler
    ### Description
      Arbitrarily rescales an array.\n
      --------------------\n
    ### Input
      ArbitraryRescaler(x)\n
      --------------------\n
      #### Positional Arguments
      Array{Any} - x:: Array for which the original scaler should be based
      off of.\n
      --------------------\n
     ### Output
     scalar :: A Lathe Preprocesser object.
     ---------------------\n
     ### Functions
     Preprocesser.predict(xt) :: Applies the scaler to xt.\n
     ---------------------\n
     ### Data
     a :: The minimum value in the array.\n
     b :: The maximum value in the array.
       """
mutable struct ArbitraryRescaler{P} <: Scaler
    predict::P
    A::Float64
    B::Float64
    function ArbitraryRescaler(array::Array)
        a = minimum(array)
        b = maximum(array)
        predict(x::Array) = [i = a + ((i-a*i)*(b-a)) / (b-a) for i in x]
        predict(df::DataFrame, symb::Symbol) = [i = a + ((i-a*i)*(b-a)) / (b-a) for i in df[!, symb]]
        P = typeof(predict)
        return new{P}(predict, a, b)
    end
end
# ---- Mean Normalization ----
"""
    ## Mean Normalizer
    ### Description
      Normalizes an array using the mean of the data.\n
      --------------------\n
    ### Input
      ArbitraryRescaler(x)\n
      --------------------\n
      #### Positional Arguments
      Array{Any} - x:: Array for which the original scaler should be based
      off of.\n
      --------------------\n
     ### Output
     scalar :: A Lathe Preprocesser object.
     ---------------------\n
     ### Functions
     Preprocesser.predict(xt) :: Applies the scaler to xt.\n
     ---------------------\n
     ### Data
     a :: The minimum value in the array.\n
     b :: The maximum value in the array.\n
     avg :: The mean of the array.
       """
mutable struct MeanScaler{P} <: Scaler
    predict::P
    avg::Float64
    a::Float64
    b::Float64
    function MeanScaler(array::Array)
        avg = mean(array)
        a = minimum(array)
        b = maximum(array)
        predict(array::Array) = [i = (i-avg) / (b-a) for i in array]
        predict(df::DataFrame, symb::Symbol) = [i = (i-avg) / (b-a) for i in df[!, symb]]
        P = typeof(predict)
        return new{P}(predict, avg, a, b)
    end
end
# ---- Z Normalization ----
"""
    ## Standard Scaler
    ### Description
      Normalizes an array using the z (Normal) distribution.\n
      --------------------\n
    ### Input
      StandardScaler(x)\n
      --------------------\n
      #### Positional Arguments
      Array{Any} - x:: Array for which the original scaler should be based
      off of.\n
      --------------------\n
     ### Output
     scalar :: A Lathe Preprocesser object.
     ---------------------\n
     ### Functions
     Preprocesser.predict(xt) :: Applies the scaler to xt.\n
     ---------------------\n
     ### Data
     dist  :: Returns the normal distribution object for which this scaler uses.
       """
mutable struct StandardScaler{dist, predict} <: Scaler
    dist::Distribution
    predict::predict
    function StandardScaler(x::Array)
        dist = NormalDist(x)
        predict(xt::Array) = dist.apply(xt)
        predict(df::DataFrame, symb::Symbol) = dist.apply(df[!, symb])
        D, P =  typeof(dist), typeof(predict)
        return new{D, P}(dist, predict)
    end
end

mutable struct QuantileTransformer{P} <: Transformer
    predict::P
    dist::Distribution
    norm::Distribution
    function QuantileTransformer(array::Array)
        normalized = NormalDist(array).apply(array)
        dist = UniformDist(normalized)
        predict(xt::Array) = dist.cdf(xt)
        predict(df::DataFrame, symb::Symbol) = dist.cdf(df[!, symb])
        P = typeof(predict)
        return new{P}(predict, dist, norm)
    end
end


@testset "OccamNet Flux API" begin
    Random.seed!(1223)
    # Generate a multivariate function for OccamNet
    X = rand(2,10)
    f(x) = [sin(π*x[2]+x[1]); exp(x[2])]
    Y = hcat(map(f, eachcol(X))...)

    net = OccamNet(2, 2, 3, Function[sin, +, *, exp], skip = true, constants = Float64[π])
    initial_loss = sum([sum(abs2, net(X)-Y) for i in 1:100]) / 100
    Flux.train!(net, X, Y, ADAM(1e-2), 1000, routes = 100, nbest = 3)
    final_loss = sum([sum(abs2, net(X)-Y) for i in 1:100]) / 100
    @test initial_loss >= final_loss

    # Set the temperature low to get the best
    @test_nowarn set_temp!(net, 0.01)
    @variables x[1:2]
    x = Symbolics.scalarize(x)
    # Get the best route
    @test_nowarn rand(net)
    route = rand(net)
    @test all(probability(net, route) .> 0.9)
    # Use simplify for ordering
    eqs = simplify.(net(x, route))
    @test isequal(eqs, simplify.(Num[sin(π*x[2]+x[1]); exp(x[2])]))
end

@testset "OccamNet Solve API" begin

    sralg = OccamSR(layers = 3)
    X = rand(2,20)
    Y = permutedims(sin.(π*X[1,:]+X[2,:]))

    ddprob = DirectDataDrivenProblem(X, Y)

    res = solve(ddprob, sralg, ADAM(1e-2), max_iter = 1000, progress = false, routes = 100, temperature = 1.0)

    basis = result(res)
    m = metrics(res)
    @test m.Probability > 0.7
    @test m.Error < eps()
    @test m.AICC == Inf
end


#Copyright (c) 2015 Rex Computing and Isaac Yonemoto

#see LICENSE.txt

#this work was supported in part by DARPA Contract D15PC00135

#test-warlpiri-multiplication.jl

###############################################################################
##COMPLETE MULTIPLICATION TABLE

#column key
#none_ pfew_ pone_ psome ptwo_ pmany pall_ junk_ nnone n_few n_one nsome n_two nmany n_all
wmult = Utype[
#none_ * line
 none_ none_ none_ none_ none_ none_ junk_ junk_ none_ none_ none_ none_ none_ none_ junk_;
#pfew_ * line
 none_ pfew_ pfew_ pf_ps pf_ps pf_pm pall_ junk_ none_ n_few n_few ns_nf ns_nf nm_nf n_all;
#pone_ * line
 none_ pfew_ pone_ psome ptwo_ pmany pall_ junk_ none_ n_few n_one nsome n_two nmany n_all;
#psome * line
 none_ pf_ps psome ps_pm pmany pmany pall_ junk_ none_ ns_nf nsome nm_ns nmany nmany n_all;
#ptwo_ * line
 none_ pf_ps ptwo_ pmany pmany pmany pall_ junk_ none_ ns_nf n_two nmany nmany nmany n_all;
#pmany * line
 none_ pf_pm pmany pmany pmany pmany pall_ junk_ none_ nm_nf nmany nmany nmany nmany n_all;
#pall_ * line
 junk_ pall_ pall_ pall_ pall_ pall_ pall_ junk_ junk_ n_all n_all n_all n_all n_all n_all;
#junk_ * line
 junk_ junk_ junk_ junk_ junk_ junk_ junk_ junk_ junk_ junk_ junk_ junk_ junk_ junk_ junk_;
#nnone * line
 none_ none_ none_ none_ none_ none_ junk_ junk_ none_ none_ none_ none_ none_ none_ junk_;
#n_few * line
 none_ n_few n_few ns_nf ns_nf nm_nf n_all junk_ none_ pfew_ pfew_ pf_ps pf_ps pf_pm pall_;
#n_one * line
 none_ n_few n_one nsome n_two nmany n_all junk_ none_ pfew_ pone_ psome ptwo_ pmany pall_;
#nsome * line
 none_ ns_nf nsome nm_ns nmany nmany n_all junk_ none_ pf_ps psome ps_pm pmany pmany pall_;
#n_two * line
 none_ ns_nf n_two nmany nmany nmany n_all junk_ none_ pf_ps ptwo_ pmany pmany pmany pall_;
#nmany * line
 none_ nm_nf nmany nmany nmany nmany n_all junk_ none_ pf_pm pmany pmany pmany pmany pall_;
#n_all * line
 junk_ n_all n_all n_all n_all n_all n_all junk_ junk_ pall_ pall_ pall_ pall_ pall_ pall_
]

testop(*, wmult)


## Flow algorithms: maxflow, multirouteflow, connectivity

# Classical max-flow algorithm
function edmondsKarp(g::Network)
  flows = zeros(length(g.links))
  flow = 0.0

  while true
    u = g.source
    p, x = bfs(g,:Path,flows)
    if x == 0
      break
    end
    for v in p[2:end]
      i = findArc(g,u,v)
      rev = (g.links[i]).sym
      flows[i] = flows[i] + x
      flows[rev] = flows[rev] - x
      u = v
    end
    flow = flow + x
  end
  return (flow,flows)
end

#Connectivity
function connectivity(g::Network)
  function unweight(a::Arc)
    if a.cap == 0.
      return a
    else
      return Arc(a.sym,a.head,1.)
    end
  end
  arcs = map(unweight,g.links)
  gaux = Network(g.source,g.target,g.tails,arcs)
  λ, flows = edmondsKarp(gaux)
  return λ
end

# Multirouteflow
function kishimoto(g0::Network,k::Int)
  #Init
  i = 1
  flow,flows = edmondsKarp(g0)
  p = flow / k
  function restrict(a::Arc)
    Arc(a.sym,a.head,min(p,a.cap))
  end
  arcs = map(restrict,g0.links)
  g = Network(g0.source,g0.target,g0.tails,arcs)
  flow,flows = edmondsKarp(g)

  #Loop
  while flow < k * p
    p = (flow - i * p)/(k - i)
    i = i + 1
    g.links = map(restrict,g0.links)
    flow,flows = edmondsKarp(g)
  end
  #End
  return flow,flows
end


"""Vector geometry."""

mutable struct Vector
    x
    y
end

Vector() = Vector(0.0, 0.0)

import Base.+, Base.-, Base.*, Base./, Base.==
add(a::Vector, b::Vector) = Vector(a.x + b.x, a.y + b.y)
sub(a::Vector, b::Vector) = Vector(a.x - b.x, a.y - b.y)
mult(a::Vector, scalar) = Vector(a.x * scalar, a.y * scalar)
div(a::Vector, scalar) = Vector(a.x / scalar, a.y / scalar)
neg(v::Vector) = mult(v, -1)
(+)(a::Vector, b::Vector) = add(a, b)
(-)(a::Vector, b::Vector) = sub(a, b)
(*)(a::Vector, scalar) = mult(a, scalar)
(/)(a::Vector, scalar) = div(a, scalar)
(-)(v::Vector) = neg(v)
function (==)(a::Vector, b::Vector)
    a.x == b.x && a.y == b.y && return true
    return false
end

function add!(a::Vector, b::Vector)
    a.x += b.x
    a.y += b.y
    return a
end

function sub!(a::Vector, b::Vector)
    a.x -= b.x
    a.y -= b.y
    return a
end

function mult!(v::Vector, scalar)
    v.x *= scalar
    v.y *= scalar
    return v
end

function div!(v::Vector, scalar)
    v.x /= scalar
    v.y /= scalar
    return v
end

magnitude(v::Vector) = sqrt(v.x * v.x + v.y * v.y)

function normalize!(v::Vector)
    m = magnitude(v)
    if m == 0
        v.x = 0.0
        v.y = 0.0
    else
        v.x /= m
        v.y /= m
    end
    return v
end

dot(a::Vector, b::Vector) = a.x * b.x + a.y * b.y
det(a::Vector, b::Vector) = a.x * b.y - a.y * b.x
cross(v::Vector) = Vector(-v.x, v.y)

function rotate!(v::Vector, angle, point)
    angle == 0. && return
    dx = v.x - point.x
    dy = v.y - point.y
    v.x = point.x + (dx * cos(angle) - dy * sin(angle))
    v.y = point.y + (dx * sin(angle) + dy * cos(angle))
    return v
end

rotate!(v::Vector, angle) = rotate!(v, angle, Vector(0., 0.))

import Base.isapprox
function isapprox(a::Vector, b::Vector; atol = 1e-16)
    isapprox(a.x, b.x; atol = atol) && isapprox(a.y, b.y; atol = atol) && return true
    return false
end


include("../src/vm_basis.jl")

function test_dim_Sk()
   print("dimensions.Sk...")

   for k in [-2, 0, 2, 3, 4, 6, 8, 10, 14]
      @test dim_Sk(k) == 0
   end

   for k in [12, 16, 18, 20, 22, 26]
      @test dim_Sk(k) == 1
   end

   @test dim_Sk(24) == 2

   println("PASS")
end

function test_dim_Mk()
   print("dimensions.Mk...")

   for k in [-2, 2, 3]
      @test dim_Mk(k) == 0
   end

   for k in [0, 4, 6, 8, 10, 14]
      @test dim_Mk(k) == 1
   end

   for k in [12, 16, 18, 20, 22, 26]
      @test dim_Mk(k) == 2
   end

   @test dim_Mk(24) == 3

   println("PASS")
end

function test_dimensions()
   test_dim_Sk()
   test_dim_Mk()

   println("")
end


__precompile__()

module LogProbs

import Base: show, log, float, rand, isapprox
import Base: ==, hash
import Base: zero, one, +, -, *, /, isless

using StatsFuns: logaddexp, log1mexp

export LogProb, information

"""
    LogProb(x)

Create a container of a positive real number `x`
for convenient calculations with logspace probabilities

see https://en.wikipedia.org/wiki/Log_probability
"""
struct LogProb <: Number
    log :: Float64
    LogProb(number; islog::Bool=false) = islog ? new(number) : new(log(number))
end

show(io::IO, x::LogProb)  = print(io, "LogProb($(float(x)))")

==(x::LogProb,y::LogProb) = x.log == y.log
hash(x::LogProb)          = hash(LogProb, hash(x.log))

rand(::Type{LogProb})     = LogProb(rand())

float(x::LogProb)         = exp(x.log)
log(x::LogProb)           = x.log

"""
    information(p::LogProb)

Calculate the Shannon information content of `p` in bits
"""
information(x::LogProb)   = - log(x) / log(2)

one( ::Type{LogProb})     = LogProb(1)
zero(::Type{LogProb})     = LogProb(0)

*(x::LogProb, y::LogProb) = LogProb(x.log + y.log, islog=true)
/(x::LogProb, y::LogProb) = LogProb(x.log - y.log, islog=true)
+(x::LogProb, y::LogProb) = LogProb(logaddexp(x.log, y.log), islog=true)
-(x::LogProb, y::LogProb) = LogProb(x.log + log1mexp(y.log-x.log), islog=true)

isless(  x::LogProb, y::LogProb) = isless(  x.log, y.log)
isapprox(x::LogProb, y::LogProb) = isapprox(x.log, y.log)

end # Module


# s^n = sum(a.^n) + error
struct Solution{T<:Integer,N}
    s :: T
    n :: Int
    a :: NTuple{N,T}
    function Solution(s::T,n::Int,a,AZsPC::Bool=true) where {T<:Integer}
        N = length(a)
        @assert N<s
        if AZsPC
            @assert N>1
            @assert n>15
            @assert n<41
        end
        for x in a
            @assert x<s
        end
        sorted_a=sort([a...])
        @assert allunique(sorted_a)
        new{T,N}(s,n,Tuple(sorted_a))
    end
end
Base.show(io::IO, x::Solution) = print(io,string(x))
function Base.string(sol::Solution,witherror::Bool= false)
    to_string((sol.s,sol.n,sol.a),witherror)
end
function to_string(x,witherror::Bool=false)
    s,n,a = x
    out = string(s,"^",n,"=>{",a[1])
    for value in a[2:end]
        out = out*string(",",value)
    end
    out = out*"}"
    if witherror
        out = out*",e="*string(err(s,n,a))
    end
    out
end
function err(sol::Solution, acc=BigInt(0))
    err((sol.s,sol.n,sol.a),acc)
end
function err(x, acc=BigInt(0))
    s,n,a = x
    acc = acc + BigInt(s)^n
    for ak in a
        acc = acc - BigInt(ak)^n
    end
    acc
end


convert_to_int_vector(line) = map(x -> parse(Int64, x), split(line, ","))

function read_data(lines)
    function read_interval(s)
        a, b = map(x -> parse(Int64, x), split(s, "-"))
        (a=a, b=b)
    end

    ranges = Dict{String, Set{NamedTuple{(:a, :b), Tuple{Int64, Int64}}}}()
    ix1, ix2 = findall(isempty, lines)
    for line in lines[1:ix1-1]
        label, rest = split(line, ": ")
        intervals = split(rest, " or ")
        ranges[label] = Set(map(read_interval, intervals))
    end

    own_ticket = convert_to_int_vector(lines[ix1+2])

    tickets = Vector{Int64}[]
    for line in lines[ix2+2:end]
        push!(tickets, convert_to_int_vector(line))
    end

    (ranges, own_ticket, tickets)
end

item_in_intervals(item, intervals) = any(a <= item <= b for (a, b) in intervals)

is_valid(x, ranges) = any(item_in_intervals(x, intervals) for intervals in values(ranges))

function collect_suitable_labels(ranges, column)
    suitable_labels = []
    for (label, intervals) in pairs(ranges)
        if all(item_in_intervals(x, intervals) for x in column)
            push!(suitable_labels, label)
        end
    end
    suitable_labels
end

function find_unique_assignments(labels)
    result = Array{String}(undef, length(labels))
    ix = findfirst(lst -> length(lst) == 1, labels)
    while ix !== nothing
        label = only(labels[ix])
        result[ix] = label
        labels = [filter(x -> x != label, lst) for lst in labels]
        ix = findfirst(lst -> length(lst) == 1, labels)
    end
    result
end

function departure_product(own_ticket, tickets, ranges)
    is_ticket_valid(ticket) = all(is_valid(x, ranges) for x in ticket)

    valid_tickets = permutedims(hcat(filter(is_ticket_valid, tickets)...))
    label_assignments = Vector{String}[]
    for column in eachcol(valid_tickets)
        labels = collect_suitable_labels(ranges, column)
        push!(label_assignments, labels)
    end

    unique_assignments = find_unique_assignments(label_assignments)
    prod(number for (label, number) in zip(unique_assignments, own_ticket) if startswith(label, "departure"))
end

function main()
    lines = readlines("../resources/input_16.txt")
    ranges, own_ticket, nearby_tickets = read_data(lines)
    result = departure_product(own_ticket, nearby_tickets, ranges)
    println(result)
end

main()


module TestModularityUtilities
using Test
using EcologicalNetworks

# Perfectly modular bipartite network
A = [
  true true true false false false; true true true false false false;
  false false false true true true; false false false true true true
  ]
B = BipartiteNetwork(A)
U = convert(UnipartiteNetwork, B)

LB = Dict(zip(species(B), [1,1,2,2,1,1,1,2,2,2]))
LU = Dict(zip(species(U), [1,1,2,2,1,1,1,2,2,2]))

@test Q(B, LB) == 0.5
@test Q(U, LU) == 0.5
@test Qr(B, LB) == 1.0
@test Qr(U, LU) == 1.0

end


module DiffOperatorsTests

using Test
using Gridap.Arrays
using Gridap.TensorValues
using Gridap.Fields
using Gridap.Fields: MockField
using LinearAlgebra
using FillArrays

np = 4
p = Point(1,2)
x = fill(p,np)

v = 3.0
f = MockField(v)

@test ∇(f) == gradient(f)

@test divergence(f) == tr(gradient(f))

@test curl(f) == grad2curl(gradient(f))

@test ∇⋅f == divergence(f)

@test cross(∇,f) == curl(f)

@test ∇×f == curl(f)

@test outer(∇,f) == ∇(f)

@test ∇⊗f == ∇(f)

@test outer(f,∇) == transpose(∇(f))

@test f⊗∇ == transpose(∇(f))

@test ε(f) == symmetric_part(gradient(f))

@test Δ(f) == ∇⋅∇(f)

l = 10
f = Fill(f,l)

@test Broadcasting(divergence)(f) == Broadcasting(Operation(tr))(Broadcasting(∇)(f))
@test Broadcasting(curl)(f) == Broadcasting(Operation(grad2curl))(Broadcasting(∇)(f))
@test Broadcasting(ε)(f) == Broadcasting(Operation(symmetric_part))(Broadcasting(∇)(f))

# Test automatic differentiation

u_scal(x) = x[1]^2 + x[2]
∇u_scal(x) = VectorValue( 2*x[1], one(x[2]) )
Δu_scal(x) = 2

u_vec(x) = VectorValue( x[1]^2 + x[2], 4*x[1] - x[2]^2 )
∇u_vec(x) = TensorValue( 2*x[1], one(x[2]), 4*one(x[1]), - 2*x[2] )
Δu_vec(x) = VectorValue( 2, -2 )
εu_vec(x) = SymTensorValue( 2*x[1], 0.5*(one(x[2])+4*one(x[1])), - 2*x[2] )

xs = [ Point(1.,1.), Point(2.,0.), Point(0.,3.), Point(-1.,3.)]
for x in xs
  @test ∇(u_scal)(x) == ∇u_scal(x)
  @test Δ(u_scal)(x) == Δu_scal(x)
  @test ∇(u_vec)(x) == ∇u_vec(x)
  @test Δ(u_vec)(x) == Δu_vec(x)
  @test ε(u_vec)(x) == εu_vec(x)
end

u(x) = VectorValue( x[1]^2 + 2*x[2]^2, -x[1]^2 )
∇u(x) = TensorValue( 2*x[1], 4*x[2], -2*x[1], zero(x[1]) )
Δu(x) = VectorValue( 6, -2 )
εu(x) = SymTensorValue( 2*x[1], 2*x[2]-x[1], zero(x[1]) )

for x in xs
  @test (∇⋅u)(x) == tr(∇u(x))
  @test (∇×u)(x) == grad2curl(∇u(x))
  @test Δ(u)(x) == Δu(x)
  @test ε(u)(x) == εu(x)
end

u(x) = VectorValue( x[1]^2 + 2*x[2]^2, 0 )
∇u(x) = TensorValue( 2*x[1], 4*x[2], 0, 0 )
Δu(x) = VectorValue( 6, 0 )
εu(x) = SymTensorValue( 2*x[1], 2*x[2], 0 )

for x in xs
  @test (∇⋅u)(x) == tr(∇u(x))
  @test (∇×u)(x) == grad2curl(∇u(x))
  @test Δ(u)(x) == Δu(x)
  @test ε(u)(x) == εu(x)
end

end # module


# SPDX-License-Identifier: MIT

"""
    AbstractGamma{D} <: Distribution

Abstract type for Gamma distribution implementations.
"""
abstract type AbstractGamma{D} <: Distribution end

function DefaultGammaParameter(α, β)
    DefaultParameter(vcat(-β, α))
end

"""
    mutable struct Gamma{D} <: AbstractGamma
        param::P where P <: AbstractParameter
    end

Set of `D` independent Gamma distributions.

# Constructors

    Gamma(α, β)

where `α` and `β` are the shape and rate parameters of the
distribution.

# Examples
```jldoctest
julia> Gamma([1.0, 1.0], [2.0, 2.0])
Gamma{2}:
  α = [1.0, 1.0]
  β = [2.0, 2.0]
```
"""
mutable struct Gamma{D} <: AbstractGamma{D}
    param::P where P <: AbstractParameter
end

function Gamma(α::AbstractVector{T}, β::AbstractVector{T}) where T
    D = length(α)
    param = DefaultGammaParameter(α, β)
    Gamma{D}(param)
end

#######################################################################
# Distribution interface

basemeasure(::AbstractGamma, x) = -sum(log, x)

function lognorm(g::AbstractGamma{D}, η = naturalform(g.param)) where D
    η₁, η₂ = η[1:D], η[D+1:end]
    sum(loggamma_dot(η₂) - η₂ .* log.(-η₁))
end

function gradlognorm(g::AbstractGamma)
    α, β = stdparam(g, naturalform(g.param))
    vcat(α ./ β, digamma.(α) .- log.(β))
end

function sample(g::AbstractGamma{D}, size) where D
    retval = similar(g.α, D, size)
    α, β = Array(g.α), Array(g.β)
    for i in 1:length(α)
        g_ = Dists.Gamma(α[i], 1/β[i])
        retval[i, :] = Dists.rand(g_, size)
    end
    retval
end

splitgrad(g::AbstractGamma{D}, μ) where D = μ[1:D], μ[D+1:end]

stats(::Gamma, x) = vcat(x, log.(x))

function stdparam(g::AbstractGamma{D},
                  η::AbstractVector{T} = naturalform(g.param)) where {T,D}
    α = η[D+1:end]
    β = -η[1:D]
    (α = α, β = β)
end


@testset "KSP{$ST}" begin
  A = PETSc.Mat(ST, 3,3)
  A_julia = zeros(ST,3,3)
  for i=1:3
    for j=1:3
      pos = (i-1)*3 + j
      val = RC(Complex(pos, pos))
      A[i, j] = val
      A_julia[i,j] = val
     end
  end

  A[3,2] = RC(Complex(6,6))  # make A non singular
  A_julia[3,2] = RC(Complex(6,6))

  b = PETSc.Vec(ST, 3, vtype=PETSc.C.VECMPI)
  b_julia = zeros(ST, 3)
  b[3] = RC(Complex(1,1))
  b_julia[3] = RC(Complex(1,1))

  @testset "ksp GMRES solves" begin
      kspg = PETSc.KSP(A)
      x = kspg\b
      x_julia = A_julia\b_julia
      @test x ≈ x_julia

      x = A\b
      @test x ≈ x_julia

      x = KSPSolveTranspose(kspg, b)
      x_julia = A_julia.'\b_julia
      @test x ≈ x_julia

      x = KSPSolveTranspose(A, b)
      @test x ≈ x_julia

      pc   = PETSc.PC(ST,comm=comm(kspg),pc_type="jacobi")
      PETSc.chk(PETSc.C.PCSetOperators(pc.p,A.p,A.p))
      kspg = PETSc.KSP(pc)
      x = kspg\b
      x_julia = A_julia\b_julia

      @test x ≈ x_julia
      # fixme: test petscview without side effects, e.g.
      # via PetscViewerStringOpen
  end

  @testset "ksp BCGS solves" begin
      kspb = PETSc.KSP(A, ksp_type="bcgs")
      x = kspb\b
      x_julia = A_julia\b_julia
      @test x ≈ x_julia
  end
end


using KitBase, FluxReconstruction, OrdinaryDiffEq, CUDA, LinearAlgebra, Plots

begin
    x0 = -1.0f0
    x1 = 1.0f0
    ncell = 100
    dx = (x1 - x0) / ncell
    deg = 2
    nsp = deg + 1
    cfl = 0.1
    dt = cfl * dx
    t = 0.0f0
    a = 1.0f0
    tspan = (0.0f0, 1.0f0)
    nt = tspan[2] / dt |> floor |> Int
    bc = :period
end

ps = FRPSpace1D(x0, x1, ncell, deg)

u = zeros(Float32, ncell, nsp)
for i = 1:ncell, ppp1 = 1:deg+1
    u[i, ppp1] = exp(-20.0 * ps.xpg[i, ppp1]^2)
end
u = u |> CuArray

prob = FR.FRAdvectionProblem(u, tspan, ps, a, bc)
itg = init(prob, Midpoint(), saveat = tspan[2], adaptive = false, dt = dt)

for iter = 1:nt
    step!(itg)
end

ut = CUDA.@allowscalar itg.u[:, 2] |> Array
plot(ps.xpg[:, 2], ut, label = "t=2")
plot!(ps.xpg[:, 2], exp.(-20 .* ps.xpg[:, 2] .^ 2), label = "t=0", line = :dash)


# corFun.jl

export CorParams, MaternParams,
    CorHypers, MaternHyper_ScInvChiSq,
    corr, corrMat, covMat, PDMat_adj;

abstract type CorParams end

mutable struct MaternParams <: CorParams
    ν::Float64
    lenscale::Float64
    MaternParams(ν, lenscale) = ν > 0.0 && lenscale > 0.0 ? new(ν, lenscale) : error("Both parameters must be positive.")
end
function MaternParams()
    MaternParams(2.5, 1.0)
end

abstract type CorHypers end

mutable struct MaternHyper_ScInvChiSq <: CorHypers
    lenscale_ν::Float64
    lenscale0::Float64
end
function MaternHyper_ScInvChiSq()
    MaternHyper_ScInvChiSq(5.0, 0.6)
end

function corr(d::T, params::MaternParams; logout::Bool=false) where T <: Real
    ## Matern

    ν = params.ν
    lenscale = params.lenscale

    d >= 0.0 || throw(error("Distance d must be non-negative."))
    lenscale > 0.0 || throw(error("Lengthscale parameter lenscale must be positive."))

    if ν == 0.5

        lcor = -d / lenscale

    elseif ν == 1.5

        s3ddl = sqrt(3.0) * d / lenscale
        lcor = log( s3ddl  + 1.0 ) - s3ddl

    elseif ν == 2.5

        s5ddl = sqrt(5.0) * d / lenscale
        lcor = log( s5ddl^2 / 3.0 + s5ddl + 1.0 ) - s5ddl

    elseif ν == Inf  # squared exponential correlation function

        lcor = -0.5 * ( d / lenscale )^2

    else
        throw(error("Unsupported value of ν."))
    end

    logout ? out = lcor : out = exp(lcor)
    return out
end

function corrMat(D::Matrix{T}, params::MaternParams; logout::Bool=false) where T <: Real
    if issymmetric(D)

        n = size(D,1)
        C = Matrix{T}(undef, n, n)

        for i = 1:n
            C[i,i] = corr(D[i,i], params, logout=logout)
            for j = (i+1):n
                C[i,j] = corr(D[i,j], params, logout=logout)
                C[j,i] = C[i,j]
            end
        end

    else

        n1, n2 = size(D)
        C = Matrix{T}(undef, n1, n2)

        for i = 1:n1
            for j = 1:n2
                C[i,j] = corr(D[i,j], params, logout=logout)
            end
        end

    end

    return Symmetric(C)
end

function covMat(D::Matrix{T}, variance::T, params::MaternParams; tol::Float64=1.0e-6) where T <: Real
    variance > 0.0 || throw("Variance must be positive. Its value is $(variance).")
    lv = log(variance)

    if issymmetric(D)

        n = size(D,1)
        C = Matrix{T}(undef, n, n)

        for i = 1:n
            C[i,i] = exp(lv + corr(D[i,i], params, logout=true))
            for j = (i+1):n
                C[i,j] =  exp(lv + corr(D[i,j], params, logout=true))
                C[j,i] = C[i,j]
            end
        end

        # C = PDMat_adj(C, tol) # do this manually where you need it.

    else

        n1, n2 = size(D)
        C = Matrix{T}(undef, n1, n2)

        for i = 1:n1
            for j = 1:n2
                C[i,j] =  exp(lv + corr(D[i,j], params, logout=true))
            end
        end

    end

    return C
end

function PDMat_adj(A::Union{Matrix{Float64}, Symmetric{Float64, Matrix{Float64}}, PDMat},
    maxadd::Float64=1.0e-6,
    epsfact::Float64=100.0, cumadd::Float64=0.0)

    try PDMat(A)
    catch excep
        if isa(excep, PosDefException) && cumadd <= maxadd
            a = epsfact * eps(Float64)
            A += a * I
            cumadd += a
            epsfactnext = 10.0 * epsfact
            PDMat_adj(A, maxadd, epsfactnext, cumadd)
        else
            PDMat(A) # just trigger original error
        end
    end
end


function polygon(t, n, len)
    angle = 360 / n
    polyline(t, n, len, angle)
end

function polyline(t, n, len, angle)
    for i in 1:n
        forward(t, len)
        turn(t, -angle)
    end
end

function arc(t, r, angle)
    arc_len = 2π * r * abs(angle) / 360
    n = trunc(arc_len / 4) + 3
    step_len = arc_len / n
    step_angle = angle / n
    turn(t, -step_angle / 2)
    polyline(t, n, step_len, step_angle)
    turn(t, step_angle / 2)
end

function petal(t, r, angle)
    for i in 1:2
        arc(t, r, angle)
        turn(t, angle - 180)
    end
end

function flower(t, n, r, angle)
    for i in 1:n
        petal(t, r, angle)
        turn(t, 360 / n)
    end
end

function drawpie(t, n, r)
    polypie(t, n, r)
    penup(t)
    forward(t, r * 2 + 10)
    pendown(t)
end

function polypie(t, n, r)
    angle = 360 / n
    for i in 1:n
        isosceles(t, r, angle / 2)
        turn(t, -angle)
    end
end

function isosceles(t, r, angle)
    y = r * sin(angle * pi / 180)
    turn(t, angle)
    forward(t, r)
    turn(t, -90 - angle)
    forward(t, 2 * y)
    turn(t, -90 - angle)
    forward(t, r)
    turn(t, -180 + angle)
end

function spiral(t, n, len, a, b)
    theta = 0.0
    for i in 1:n
        forward(t, len)
        dtheta = 1 / (a + b * theta)
        turn(t, -dtheta)
        theta += dtheta
    end
end


@defcomp radiativeforcing begin
	FORCOTH	  = Variable(index=[time])	 #Exogenous forcing for other greenhouse gases
    FORC      = Variable(index=[time])   #Increase in radiative forcing (watts per m2 from 1900)

    MAT       = Parameter(index=[time])  #Carbon concentration increase in atmosphere (GtC from 1750)
    eqmat     = Parameter()              #Equilibrium concentration of CO2 in atmosphere (GTC)
    fco22x    = Parameter()              #Forcings of equilibrium CO2 doubling (Wm-2)
	fex0	  = Parameter()				 #2015 forcings of non-CO2 GHG (Wm-2)
	fex1	  = Parameter()				 #2100 forcings of non-CO2 GHG (Wm-2)

    function run_timestep(p, v, d, t)
		#Define function for FORCOTH
		for tm = 1:100
			if tm < 19
				v.FORCOTH[tm] = p.fex0 + (1/17) * (p.fex1 - p.fex0) * (tm - 1)
			else
				v.FORCOTH[tm] = p.fex0 + (p.fex1 - p.fex0)
			end
		end

        #Define function for FORC
        v.FORC[t] = p.fco22x * (log((p.MAT[t] / p.eqmat)) / log(2)) + v.FORCOTH[t]
    end
end


function interact(M, N)
    d = 1.0 + (M * N)^0.75 * 2.01E-5 + (M * N)^1.52 * M * 5.31E-15
    return 0.47 * log(d)
end

@defcomp climateforcing begin
    # Atmospheric CO2 concentration
    acco2 = Parameter(index=[time])

    # Pre-industrial atmospheric CO2 concentration
    co2pre = Parameter(default = 275.0)

    # Atmospheric CH4 concentration
    acch4 = Parameter(index=[time])

    # Pre-industrial atmospheric CH4 concentration
    ch4pre = Parameter(default = 790.0)

    # Indirect radiative forcing increase for CH4
    ch4ind = Parameter(default = 0.4)

    # Atmospheric N2O concentration
    acn2o = Parameter(index=[time])

    # Pre-industrial atmospheric N2O concentration
    n2opre = Parameter(default = 285.0)

    # Atmospheric SF6 concentrations
    acsf6 = Parameter(index=[time])

    # Pre-industrial atmospheric SF6 concentration
    sf6pre = Parameter(default = 0.04)

    # Radiative forcing from CO2
    rfco2 = Variable(index=[time])

    # Radiative forcing from CH4
    rfch4 = Variable(index=[time])

    # Radiative forcing from N2O
    rfn2o = Variable(index=[time])

    # Radiative forcing from N2O
    rfsf6 = Variable(index=[time])

    # Radiative forcing from SO2
    rfso2 = Parameter(index=[time])

    # Radiative forcing
    radforc = Variable(index=[time])

    # EMF22 radiative forcing
    rfemf22 = Variable(index=[time])

    function run_timestep(p, v, d, t)

        if !is_first(t)
            ch4n2o = interact(p.ch4pre, p.n2opre)

            v.rfco2[t] = 5.35 * log(p.acco2[t] / p.co2pre)

            v.rfch4[t] = 0.036 * (1.0 + p.ch4ind) * (sqrt(p.acch4[t]) - sqrt(p.ch4pre)) - interact(p.acch4[t], p.n2opre) + ch4n2o

            v.rfn2o[t] = 0.12 * (sqrt(p.acn2o[t]) - sqrt(p.n2opre)) - interact(p.ch4pre, p.acn2o[t]) + ch4n2o

            v.rfsf6[t] = 0.00052 * (p.acsf6[t] - p.sf6pre)

            v.radforc[t] = v.rfco2[t] + v.rfch4[t] + v.rfn2o[t] + v.rfsf6[t] + p.rfso2[t]

            v.rfemf22[t] = v.rfco2[t] + v.rfch4[t] + v.rfn2o[t]
        end
    end
end


using BSplineKit
using Test

# TODO
# - test recombined basis (test function should satisfy BCs...)

# We make sure that the function is specialised on `f`, to fully avoid
# allocations.
# See https://docs.julialang.org/en/v1/manual/performance-tips/#Be-aware-of-when-Julia-avoids-specializing
function check_approximate_in_place!(f::F, fapprox) where {F}
    S = copy(spline(fapprox))  # assumed to already approximate `f`
    @test 0 == @allocated approximate!(f, fapprox)  # no allocations!
    @test spline(fapprox) == S  # check that nothing changed
    nothing
end

function test_approximation(B)
    k = order(B)

    ftest = sin

    f_fast = @inferred approximate(ftest, B, VariationDiminishing())
    @test startswith(repr(f_fast), "SplineApproximation containing")
    check_approximate_in_place!(ftest, f_fast)

    # With these two methods, the approximation should exactly match a
    # polynomial of degree k - 1.
    f_interp = @inferred approximate(ftest, B, ApproxByInterpolation(B))
    @test startswith(repr(f_interp), "SplineApproximation containing")
    check_approximate_in_place!(ftest, f_interp)

    f_opt = @inferred approximate(ftest, B, MinimiseL2Error())
    @test startswith(repr(f_opt), "SplineApproximation containing")
    check_approximate_in_place!(ftest, f_opt)

    # Polynomial of degree k - 1.
    # The interpolation and minimisation methods should exactly approximate such
    # a polynomial.
    @testset "Polynomial" begin
        poly_coefs = ntuple(d -> (-d)^d, Val(k))
        fpoly(x) = evalpoly(x, poly_coefs)

        xfine = range(boundaries(B)...; length = 3 * length(B))

        approximate!(fpoly, f_interp)
        @test fpoly.(xfine) ≈ f_interp.(xfine)

        approximate!(fpoly, f_opt)
        @test fpoly.(xfine) ≈ f_opt.(xfine)
    end

    nothing
end

@testset "Approximation" begin
    N = 16
    breaks = [-cos(π * n / N) for n = 0:N]
    @testset "Order $k" for k = 4:5
        B = BSplineBasis(BSplineOrder(k), copy(breaks))
        # R = RecombinedBSplineBasis(B, Derivative(1))  # Neumann BCs
        test_approximation(B)
    end
end


using WiltonInts84

using Base.Test
using FixedSizeArrays

include("num_quad.jl")

if !isdefined(:record)
    record = false
end

N = 2
T = Float64

J = Vector{Vec{N+3,T}}()
I = similar(J)
L = Vector{Vec{N+3,Vec{3,T}}}()
K = similar(L)

v1 = Vec(1.0, 0.0, 0.0)
v2 = Vec(0.0, 1.0, 0.0)
v3 = Vec(0.0, 0.0, 0.0)
n = normalize(cross(v1-v3,v2-v3))

X = [
    (v1 + v2 + v3)/3               + 20n, # h > 0, inside
    (1-1.5)v1 + 1.5v3              + 20n, # h > 0, on extension [v1,v3]
    0.5v1 + 0.5v3                  + 20n, # h > 0, on the interior of [v1,v3]
    -0.5v1 + 0.5v2 + (1-0.5-0.5)v3 + 20n, # h > 0, outside
    v2                             + 20n, # h > 0, on top of v2

    (v1 + v2 + v3)/3               - 20n, # h < 0, inside
    (1-1.5)v1 + 1.5v3              - 20n, # h < 0, on extension [v1,v3]
    0.5v1 + 0.5v3                  - 20n, # h < 0, on the interior of [v1,v3]
    -0.5v1 + 0.5v2 + (1-0.5-0.5)v3 - 20n, # h < 0, outside
    v2                             - 20n, # h < 0, on top of v2

    (1-1.5)v1 + 1.5v3              - 0n,  # h = 0, on extension [v1,v3]
    -0.5v1 + 0.5v2 + (1-0.5-0.5)v3 - 0n,  # h = 0, outside

    (v1 + v2 + v3)/3               - 0n,  # h = 0, inside
]

for (i,x) in enumerate(X)
    A, B = wiltonints(v1,v2,v3,x,Val{N})
    push!(I,A); push!(K,B);
    if record
        P, Q = dblquadints1(v1,v2,v3,x,Val{N})
        push!(J,P); push!(L,Q);
    end
end

using JLD
fn = joinpath(dirname(@__FILE__),"triangleints.jld")

# convert data to arrays to avoid JLD bug
I = T[I[m][n] for m in eachindex(I), n in 1:length(eltype(I))]
K = T[K[m][n][p] for m in eachindex(K), n in 1:length(eltype(K)), p in 1:3]

if record == true
    J = T[J[m][n] for m in eachindex(J), n in 1:length(eltype(J))]
    L = T[L[m][n][p] for m in eachindex(L), n in 1:length(eltype(L)), p in 1:3]
    jldopen(fn,"w") do file
        write(file, "J", J)
        write(file, "L", L)
    end
else
    J,L = jldopen(fn,"r") do file
        read(file, "J"), read(file, "L")
    end
end

# The actual tests
ϵ = 1.0e-4
for i = 1 : size(I,1)-1
    for j = 1 : size(I,2)
        @test !isnan(I[i,j])
        @test !isnan(K[i,j,1])
        @test !isnan(K[i,j,2])
        @test !isnan(K[i,j,3])
        @test !isinf(I[i,j])
        @test !isinf(K[i,j,1])
        @test !isinf(K[i,j,2])
        @test !isinf(K[i,j,3])
        @test nearlyequal(I[i,j], J[i,j], ϵ)
        @test nearlyequal(vec(K[i,j,:]), vec(L[i,j,:]), ϵ)
    end
end

ϵ = 2.0e-4
for i = size(I,1)
    for j = 1 : size(I,2)
        @test !isnan(I[i,j])
        @test !isnan(K[i,j,1])
        @test !isnan(K[i,j,2])
        @test !isnan(K[i,j,3])
        @test !isinf(I[i,j])
        @test !isinf(K[i,j,1])
        @test !isinf(K[i,j,2])
        @test !isinf(K[i,j,3])
        @test nearlyequal(I[i,j], J[i,j], ϵ)
        j == 1 && continue # dblquad cannot approx Cauchy principal values...
        @test nearlyequal(vec(K[i,j,:]), vec(L[i,j,:]), ϵ)
    end
end


@testset "7.1 Inverse hyperbolic sine" begin
    include("7.1.2 (d x)^m (a+b arcsinh(c x))^n.jl")
    include("7.1.4 (f x)^m (d+e x^2)^p (a+b arcsinh(c x))^n.jl")
    include("7.1.5 Inverse hyperbolic sine functions.jl")
end

test/7 Inverse hyperbolic functions/7.1 Inverse hyperbolic sine/.jl