# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
# ==============================================================================
#
#   Functions to compute the nutation according to IAU-76/FK5.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# References
# ==============================================================================
#
#   [1] Vallado, D. A (2013). Fundamentals of Astrodynamics and Applications.
#       Microcosm Press, Hawthorn, CA, USA.
#
#   [2] Vallado, D. A (06-Feb-2018). Consolidated Errata of Fundamentals of
#       Astrodynamics and Applications 4th Ed.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

export nutation_fk5

################################################################################
#                                  Constants
################################################################################

################################################################################
#
# 1980 IAU Theory of Nutation Coefficients
# ========================================
#
# Those coefficients can be found on:
#
#   Seidelmann, P. K. 1980 IAU theory of nutation - The final report of the IAU
#   Working Group on Nutation. Celestial Mechanics, vol. 27, p. 79-106.
#
# However, the .dat file that was used to create this matrix was obtained from
# this website:
#
#   http://hpiers.obspm.fr/eop-pc/models/nutations/nut.html
#
# Notice that the order of them is not equal to that presented in the original
# paper, but this have no impact when computing the nutation. Moreover, this is
# the same order that is presented in [1](p. 1043).
#
################################################################################

const nut_coefs_1980 = [
  # Notation used in [1, p. 226].
  #
  # an1  an2  an3  an4  an5        Ai            Bi         Ci            Di
  #                  Units:  [0.0001"]  [0.0001"/JC]  [0.0001"]  [0.0001"/JC]
      0    0    0    0    1  -171996.0       -174.2    92025.0           8.9;
      0    0    2   -2    2   -13187.0         -1.6     5736.0          -3.1;
      0    0    2    0    2    -2274.0         -0.2      977.0          -0.5;
      0    0    0    0    2     2062.0          0.2     -895.0           0.5;
      0   -1    0    0    0    -1426.0          3.4       54.0          -0.1;
      1    0    0    0    0      712.0          0.1       -7.0           0.0;
      0    1    2   -2    2     -517.0          1.2      224.0          -0.6;
      0    0    2    0    1     -386.0         -0.4      200.0           0.0;
      1    0    2    0    2     -301.0          0.0      129.0          -0.1;
      0   -1    2   -2    2      217.0         -0.5      -95.0           0.3;
     -1    0    0    2    0      158.0          0.0       -1.0           0.0;
      0    0    2   -2    1      129.0          0.1      -70.0           0.0;
     -1    0    2    0    2      123.0          0.0      -53.0           0.0;
      1    0    0    0    1       63.0          0.1      -33.0           0.0;
      0    0    0    2    0       63.0          0.0       -2.0           0.0;
     -1    0    2    2    2      -59.0          0.0       26.0           0.0;
     -1    0    0    0    1      -58.0         -0.1       32.0           0.0;
      1    0    2    0    1      -51.0          0.0       27.0           0.0;
     -2    0    0    2    0      -48.0          0.0        1.0           0.0;
     -2    0    2    0    1       46.0          0.0      -24.0           0.0;
      0    0    2    2    2      -38.0          0.0       16.0           0.0;
      2    0    2    0    2      -31.0          0.0       13.0           0.0;
      2    0    0    0    0       29.0          0.0       -1.0           0.0;
      1    0    2   -2    2       29.0          0.0      -12.0           0.0;
      0    0    2    0    0       26.0          0.0       -1.0           0.0;
      0    0    2   -2    0      -22.0          0.0        0.0           0.0;
     -1    0    2    0    1       21.0          0.0      -10.0           0.0;
      0    2    0    0    0       17.0         -0.1        0.0           0.0;
      0    2    2   -2    2      -16.0          0.1        7.0           0.0;
     -1    0    0    2    1       16.0          0.0       -8.0           0.0;
      0    1    0    0    1      -15.0          0.0        9.0           0.0;
      1    0    0   -2    1      -13.0          0.0        7.0           0.0;
      0   -1    0    0    1      -12.0          0.0        6.0           0.0;
      2    0   -2    0    0       11.0          0.0        0.0           0.0;
     -1    0    2    2    1      -10.0          0.0        5.0           0.0;
      1    0    2    2    2       -8.0          0.0        3.0           0.0;
      0   -1    2    0    2       -7.0          0.0        3.0           0.0;
      0    0    2    2    1       -7.0          0.0        3.0           0.0;
      1    1    0   -2    0       -7.0          0.0        0.0           0.0;
      0    1    2    0    2        7.0          0.0       -3.0           0.0;
     -2    0    0    2    1       -6.0          0.0        3.0           0.0;
      0    0    0    2    1       -6.0          0.0        3.0           0.0;
      2    0    2   -2    2        6.0          0.0       -3.0           0.0;
      1    0    0    2    0        6.0          0.0        0.0           0.0;
      1    0    2   -2    1        6.0          0.0       -3.0           0.0;
      0    0    0   -2    1       -5.0          0.0        3.0           0.0;
      0   -1    2   -2    1       -5.0          0.0        3.0           0.0;
      2    0    2    0    1       -5.0          0.0        3.0           0.0;
      1   -1    0    0    0        5.0          0.0        0.0           0.0;
      1    0    0   -1    0       -4.0          0.0        0.0           0.0;
      0    0    0    1    0       -4.0          0.0        0.0           0.0;
      0    1    0   -2    0       -4.0          0.0        0.0           0.0;
      1    0   -2    0    0        4.0          0.0        0.0           0.0;
      2    0    0   -2    1        4.0          0.0       -2.0           0.0;
      0    1    2   -2    1        4.0          0.0       -2.0           0.0;
      1    1    0    0    0       -3.0          0.0        0.0           0.0;
      1   -1    0   -1    0       -3.0          0.0        0.0           0.0;
     -1   -1    2    2    2       -3.0          0.0        1.0           0.0;
      0   -1    2    2    2       -3.0          0.0        1.0           0.0;
      1   -1    2    0    2       -3.0          0.0        1.0           0.0;
      3    0    2    0    2       -3.0          0.0        1.0           0.0;
     -2    0    2    0    2       -3.0          0.0        1.0           0.0;
      1    0    2    0    0        3.0          0.0        0.0           0.0;
     -1    0    2    4    2       -2.0          0.0        1.0           0.0;
      1    0    0    0    2       -2.0          0.0        1.0           0.0;
     -1    0    2   -2    1       -2.0          0.0        1.0           0.0;
      0   -2    2   -2    1       -2.0          0.0        1.0           0.0;
     -2    0    0    0    1       -2.0          0.0        1.0           0.0;
      2    0    0    0    1        2.0          0.0       -1.0           0.0;
      3    0    0    0    0        2.0          0.0        0.0           0.0;
      1    1    2    0    2        2.0          0.0       -1.0           0.0;
      0    0    2    1    2        2.0          0.0       -1.0           0.0;
      1    0    0    2    1       -1.0          0.0        0.0           0.0;
      1    0    2    2    1       -1.0          0.0        1.0           0.0;
      1    1    0   -2    1       -1.0          0.0        0.0           0.0;
      0    1    0    2    0       -1.0          0.0        0.0           0.0;
      0    1    2   -2    0       -1.0          0.0        0.0           0.0;
      0    1   -2    2    0       -1.0          0.0        0.0           0.0;
      1    0   -2    2    0       -1.0          0.0        0.0           0.0;
      1    0   -2   -2    0       -1.0          0.0        0.0           0.0;
      1    0    2   -2    0       -1.0          0.0        0.0           0.0;
      1    0    0   -4    0       -1.0          0.0        0.0           0.0;
      2    0    0   -4    0       -1.0          0.0        0.0           0.0;
      0    0    2    4    2       -1.0          0.0        0.0           0.0;
      0    0    2   -1    2       -1.0          0.0        0.0           0.0;
     -2    0    2    4    2       -1.0          0.0        1.0           0.0;
      2    0    2    2    2       -1.0          0.0        0.0           0.0;
      0   -1    2    0    1       -1.0          0.0        0.0           0.0;
      0    0   -2    0    1       -1.0          0.0        0.0           0.0;
      0    0    4   -2    2        1.0          0.0        0.0           0.0;
      0    1    0    0    2        1.0          0.0        0.0           0.0;
      1    1    2   -2    2        1.0          0.0       -1.0           0.0;
      3    0    2   -2    2        1.0          0.0        0.0           0.0;
     -2    0    2    2    2        1.0          0.0       -1.0           0.0;
     -1    0    0    0    2        1.0          0.0       -1.0           0.0;
      0    0   -2    2    1        1.0          0.0        0.0           0.0;
      0    1    2    0    1        1.0          0.0        0.0           0.0;
     -1    0    4    0    2        1.0          0.0        0.0           0.0;
      2    1    0   -2    0        1.0          0.0        0.0           0.0;
      2    0    0    2    0        1.0          0.0        0.0           0.0;
      2    0    2   -2    1        1.0          0.0       -1.0           0.0;
      2    0   -2    0    1        1.0          0.0        0.0           0.0;
      1   -1    0   -2    0        1.0          0.0        0.0           0.0;
     -1    0    0    1    1        1.0          0.0        0.0           0.0;
     -1   -1    0    2    1        1.0          0.0        0.0           0.0;
      0    1    0    1    0        1.0          0.0        0.0           0.0;]

################################################################################
#                                  Functions
################################################################################

"""
    nutation_fk5(JD_TT::Number, n_max::Number = 106, nut_coefs_1980::Matrix = nut_coefs_1980)

Compute the nutation parameters at the Julian Day `JD_TT` [Terrestrial Time]
using the 1980 IAU Theory of Nutation. The coefficients are `nut_coefs_1980`
that must be a matrix in which each line has the following syntax **[1]**(p.
1043):

    an1  an2  an3  an4  an5  Ai  Bi  Ci  Di

where the units of `Ai` and `Ci` are [0.0001"] and the units of `Bi` and `Di`
are [0.0001"/JC]. The user can also specify the number of coefficients `n_max`
that will be used when computing the nutation. If `n_max` is omitted, the it
defaults to 106.

# Returns

* The mean obliquity of the ecliptic [rad].
* The nutation in obliquity of the ecliptic [rad].
* The nutation in longitude [rad].

# References

- **[1]**: Vallado, D. A (2013). Fundamentals of Astrodynamics and Applications.
    Microcosm Press, Hawthorn, CA, USA.
"""
function nutation_fk5(JD_TT::Number, n_max::Number = 106, nut_coefs_1980::Matrix = nut_coefs_1980)
    # Check inputs.
    if n_max > 106
        @warn("The maximum number of coefficients to compute nutation using IAU-76/FK5 theory is 106.")
        n_max = 106
    elseif n_max <= 0
        @warn("n_max must greater than 0. The default value will be used (106).")
        n_max = 106
    end

    # Compute the Julian Centuries from `JD_TT`.
    T_TT = (JD_TT - JD_J2000) / 36525

    # Auxiliary variables
    # ===================

    d2r = π / 180

    # Mean obliquity of the ecliptic
    # ==============================

    # Compute the mean obliquity of the ecliptic [°].
    mϵ_1980 = @evalpoly(T_TT, 23.439291, -0.0130042, -1.64e-7, +5.04e-7)

    # Reduce to the interval [0, 2π]°.
    mϵ_1980 = mod(mϵ_1980, 360) * d2r

    # Delaunay parameters of the Sun and Moon
    # =======================================

    # Evaluate the Delaunay parameters associated with the Moon and the Sun
    # in the interval [0,2π]°.
    #
    # The parameters here were updated as stated in the errata [2].
    r = 360

    M_m = @evalpoly(
        T_TT,
        +134.96298139,
        +(1325r + 198.8673981),
        +0.0086972,
        +1.78e-5
    )
    M_m = mod(M_m, 360) * d2r

    M_s = @evalpoly(
        T_TT,
        +357.52772333,
        +(99r + 359.0503400),
        -0.0001603,
        -3.3e-6
    )
    M_s = mod(M_s, 360) * d2r

    u_Mm = @evalpoly(
        T_TT,
        +93.27191028,
        +(1342r + 82.0175381),
        -0.0036825,
        +3.1e-6
    )
    u_Mm = mod(u_Mm, 360) * d2r

    D_s = @evalpoly(
        T_TT,
        +297.85036306,
        +(1236r + 307.1114800),
        -0.0019142,
        +5.3e-6
    )
    D_s = mod(D_s, 360) * d2r

    Ω_m = @evalpoly(
        T_TT,
        +125.04452222,
        -(5r + 134.1362608),
        +0.0020708,
        +2.2e-6
    )
    Ω_m = mod(Ω_m, 360) * d2r

    # Nutation in longitude and obliquity
    # ===================================

    # Compute the nutation in the longitude and in obliquity.
    ΔΨ_1980 = 0.0
    Δϵ_1980 = 0.0

    @inbounds for i = 1:n_max
        # Unpack values.
        an1 = nut_coefs_1980[i,1]
        an2 = nut_coefs_1980[i,2]
        an3 = nut_coefs_1980[i,3]
        an4 = nut_coefs_1980[i,4]
        an5 = nut_coefs_1980[i,5]
        Ai  = nut_coefs_1980[i,6]
        Bi  = nut_coefs_1980[i,7]
        Ci  = nut_coefs_1980[i,8]
        Di  = nut_coefs_1980[i,9]

        a_pi = an1 * M_m + an2 * M_s + an3 * u_Mm + an4 * D_s + an5 * Ω_m

        sin_a_pi, cos_a_pi = sincos(a_pi)

        ΔΨ_1980 += (Ai + Bi * T_TT) * sin_a_pi
        Δϵ_1980 += (Ci + Di * T_TT) * cos_a_pi
    end

    # The nutation coefficients in `nut_coefs_1980` lead to angles with unit
    # 0.0001". Hence, we must convert to [rad].
    ΔΨ_1980 *= 0.0001 / 3600 * d2r
    Δϵ_1980 *= 0.0001 / 3600 * d2r

    # Return the values.
    return mϵ_1980, Δϵ_1980, ΔΨ_1980
end


################################################################################
#
#  Morphism types
#
################################################################################

morphism_type(::Type{T}, ::Type{S}) where {R, T <: AbsAlgAss{R}, S <: AbsAlgAss{R}} = AbsAlgAssMor{T, S, Generic.Mat{R}}

morphism_type(::Type{T}, ::Type{S}) where {R, T <: AbsAlgAss{fmpq}, S <: AbsAlgAss{fmpq}} = AbsAlgAssMor{T, S, fmpq_mat}

morphism_type(::Type{T}, ::Type{S}) where {R, T <: AbsAlgAss{fq}, S <: AbsAlgAss{fq}} = AbsAlgAssMor{T, S, fq_mat}

morphism_type(::Type{T}, ::Type{S}) where {R, T <: AbsAlgAss{fq_nmod}, S <: AbsAlgAss{fq_nmod}} = AbsAlgAssMor{T, S, fq_nmod_mat}

morphism_type(::Type{T}, ::Type{S}) where {R, T <: AbsAlgAss{nmod}, S <: AbsAlgAss{nmod}} = AbsAlgAssMor{T, S, nmod_mat}

morphism_type(::Type{T}, ::Type{S}) where {R, T <: AbsAlgAss{gfp_elem}, S <: AbsAlgAss{gfp_elem}} = AbsAlgAssMor{T, S, gfp_mat}

morphism_type(A::Type{T}) where {T <: AbsAlgAss} = morphism_type(A, A)

################################################################################
#
#  Basis
#
################################################################################

function basis(A::AbsAlgAss)
  if isdefined(A, :basis)
    return A.basis::Vector{elem_type(A)}
  end
  B = Vector{elem_type(A)}(undef, dim(A))
  for i in 1:dim(A)
    z = Vector{elem_type(base_ring(A))}(undef, dim(A))
    for j in 1:dim(A)
      z[j] = zero(base_ring(A))
    end
    z[i] = one(base_ring(A))
    B[i] = A(z)
  end
  A.basis = B
  return B
end

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

issimple_known(A::AbsAlgAss) = A.issimple != 0

################################################################################
#
#  Associativity, Distributivity test
#
################################################################################

function check_associativity(A::AbsAlgAss)
  for i = 1:dim(A)
    for j = 1:dim(A)
      el = A[i] * A[j]
      for k = 1:dim(A)
        if el * A[k] != A[i] * (A[j] * A[k])
          return false
        end
      end
    end
  end
  return true
end

function check_distributivity(A::AbsAlgAss)
  for i = 1:dim(A)
    for j = 1:dim(A)
      el = A[i]*A[j]
      for k = 1:dim(A)
        if A[i] * (A[j] + A[k]) != el + A[i] * A[k]
          return false
        end
      end
    end
  end
  return true
end

################################################################################
#
#  Dimension of center
#
################################################################################

function dimension_of_center(A::AbsAlgAss)
  C, _ = center(A)
  return dim(C)
end

################################################################################
#
#  Subalgebras
#
################################################################################

# Constructs the algebra e*A
# This is the generic fallback which constructs an associative algebra
function subalgebra(A::AbsAlgAss{T}, e::AbsAlgAssElem{T}, idempotent::Bool = false) where {T}
  @assert parent(e) == A
  B, mB = AlgAss(A)
  C, mC = subalgebra(B, mB\e, idempotent)
  mD = compose_and_squash(mB, mC)
  @assert domain(mD) == C
  return C, mD
end

function subalgebra(A::AbsAlgAss{T}, basis::Array{S}) where {T, S}
  B, mB = AlgAss(A)
  basis_pre = elem_type(B)[mB\(basis[i]) for i in 1:length(basis)]
  C, mC = subalgebra(B, basis_pre)
  mD = compose_and_squash(mB, mC)
  @assert domain(mD) == C
  return C, mD
end

################################################################################
#
#  Decomposition
#
################################################################################

# Assume that A is a commutative algebra over a finite field of cardinality q.
# This functions computes a basis for ker(x -> x^q).
function kernel_of_frobenius(A::AbsAlgAss)
  F = base_ring(A)
  q = order(F)

  b = A()
  B = zero_matrix(F, dim(A), dim(A))
  for i = 1:dim(A)
    b.coeffs[i] = one(F)
    if i > 1
      b.coeffs[i - 1] = zero(F)
    end
    c = b^q - b
    for j = 1:dim(A)
      B[j, i] = c.coeffs[j]
    end
  end

  V = right_kernel_basis(B)
  return [ A(v) for v in V ]
end

@doc Markdown.doc"""
    decompose(A::AbsAlgAss{T}) -> AlgAss{T}

Given a semisimple algebra over a field, this function
returns a decomposition of A as a direct sum of simple algebras.
"""
function decompose(A::AbsAlgAss{T}) where {T}
  if isdefined(A, :decomposition)
    return A.decomposition::Vector{Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}}
  end

  if issimple_known(A) && A.issimple == 1
    B, mB = AlgAss(A)
    res = Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}[(B, mB)]
  end

  if A isa AlgAss
    D = _decompose(A)
    return D
  end

  if A isa AlgGrp || A isa AlgMat
    B, mB = AlgAss(A)
    D = _decompose(B)
    res = Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}[]
    for (S, mS) in D
      mD = compose_and_squash(mB, mS)
      push!(res, (S, mD))
    end
    A.decomposition = res
    return res
  end
end

function _decompose(A::AbsAlgAss{T}) where {T}
  if iscommutative(A)
    res = _dec_com(A)
  else
    res = _dec_via_center(A)
  end

  A.decomposition = res
  return res
end

function _dec_via_center(A::S) where {T, S <: AbsAlgAss{T}}
  ZA, mZA = center(A)
  Algs = _dec_com(ZA)
  ZA.decomposition = Algs
  res = Tuple{AlgAss{T}, morphism_type(AlgAss{T}, S)}[ subalgebra(A, mZA(BtoZA(one(B))), true) for (B, BtoZA) in Algs]
  for i in 1:length(res)
    res[i][1].issimple = 1
    B, BtoZA = Algs[i] # B is the centre of res[i][1]
    # Build a map from B to res[i][1] via B -> ZA -> A -> res[i][1]
    M = zero_matrix(base_ring(A), dim(B), dim(res[i][1]))
    for j = 1:dim(B)
      t = mZA(BtoZA(B[j]))
      s = res[i][2]\t
      elem_to_mat_row!(M, j, s)
    end
    if dim(res[i][1]) != dim(B)
      res[i][1].center = (B, hom(B, res[i][1], M))
    else
      # res[i][1] is commutative, so we do not cache the centre
      iM = inv(M)
      BtoA = hom(B, A, M*res[i][2].mat, res[i][2].imat*iM)
      res[i] = (B, BtoA)
    end
  end
  A.decomposition = res
  return res
end

function _dec_com(A::AbsAlgAss)
  if characteristic(base_ring(A)) > 0
    return _dec_com_finite(A)
  else
    return _dec_com_gen(A)
  end
end

function _dec_com_gen(A::AbsAlgAss{T}) where {T <: FieldElem}
  if dim(A) == 1
    A.issimple = 1
    B, mB = AlgAss(A)
    return Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}[(B, mB)]
  end

  F = base_ring(A)

  k = dim(A)

  V = elem_type(A)[A[i] for i in 1:k]

  while true
    c = elem_type(F)[ rand(F, -10:10) for i = 1:k ]
    a = dot(c, V)
    f = minpoly(a)

    if degree(f) < 2
      continue
    end
    if isirreducible(f)
      if degree(f) == dim(A)
        A.issimple = 1
        B, mB = AlgAss(A)
        return Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}[(B, mB)]
      end
      continue
    end

    @assert issquarefree(f)

    fac = factor(f)
    R = parent(f)
    factors = Vector{elem_type(R)}()
    for ff in keys(fac.fac)
      push!(factors, ff)
    end
    sols = Vector{elem_type(R)}()
    right_side = elem_type(R)[ R() for i = 1:length(factors) ]
    max_deg = 0
    for i = 1:length(factors)
      right_side[i] = R(1)
      if i != 1
        right_side[i - 1] = R(0)
      end
      s = crt(right_side, factors)
      push!(sols, s)
      max_deg = max(max_deg, degree(s))
    end
    x = one(A)
    powers = Vector{elem_type(A)}()
    for i = 1:max_deg + 1
      push!(powers, x)
      x *= a
    end
    idems = Vector{elem_type(A)}()
    for s in sols
      idem = A()
      for i = 0:degree(s)
        idem += coeff(s, i)*powers[i + 1]
      end
      push!(idems, idem)
    end

    A.issimple = 2

    res = Vector{Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}}()
    for idem in idems
      S, StoA = subalgebra(A, idem, true)
      decS = _dec_com_gen(S)
      for (B, BtoS) in decS
        BtoA = compose_and_squash(StoA, BtoS)
        push!(res, (B, BtoA))
      end
    end
    return res
  end
end

function _dec_com_finite(A::AbsAlgAss{T}) where T
  if dim(A) == 1
    A.issimple = 1
    B, mB = AlgAss(A)
    return Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}[(B, mB)]
  end

  F = base_ring(A)
  @assert !iszero(characteristic(F))
  V = kernel_of_frobenius(A)
  k = length(V)

  if k == 1
    A.issimple = 1
    B, mB = AlgAss(A)
    return Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}[(B, mB)]
  end

  A.issimple = 2
  c = elem_type(F)[ rand(F) for i = 1:k ]
  M = zero_matrix(F, dim(A), dim(A))
  a = dot(c, V)
  representation_matrix!(a, M)
  f = minpoly(M)
  while degree(f) < 2
    for i = 1:length(c)
      c[i] = rand(F)
    end
    a = dot(c, V)
    zero!(M)
    representation_matrix!(a, M)
    f = minpoly(M)
  end

  #@assert issquarefree(f)
  fac = factor(f)
  R = parent(f)
  factorss = collect(keys(fac.fac))
  sols = Vector{typeof(f)}(undef, length(factorss))
  right_side = typeof(f)[ zero(R) for i = 1:length(factorss) ]
  max_deg = 0
  for i = 1:length(factorss)
    right_side[i] = one(R)
    if 1 != i
      right_side[i - 1] = zero(R)
    end
    sols[i] = crt(right_side, factorss)
    max_deg = max(max_deg, degree(sols[i]))
  end
  powers = Vector{elem_type(A)}(undef, max_deg+1)
  powers[1] = one(A)
  powers[2] = a
  x = a
  for i = 3:max_deg + 1
    x *= a
    powers[i] = x
  end

  idems = Vector{elem_type(A)}()
  for s in sols
    idem = A()
    for i = 0:degree(s)
      idem += coeff(s, i)*powers[i + 1]
    end
    push!(idems, idem)
  end

  res = Vector{Tuple{AlgAss{T}, morphism_type(AlgAss{T}, typeof(A))}}()
  for idem in idems
    S, StoA = subalgebra(A, idem, true)
    decS = _dec_com_finite(S)
    for (B, BtoS) in decS
      BtoA = compose_and_squash(StoA, BtoS)
      push!(res, (B, BtoA))
    end
  end
  return res

end

################################################################################
#
#  Decomposition as number fields
#
################################################################################

@doc Markdown.doc"""
    as_number_fields(A::AbsAlgAss{fmpq})

Given a commutative algebra over QQ, this function returns a decomposition
of A as direct sum of number fields.
"""
function as_number_fields(A::AbsAlgAss{fmpq})
  if isdefined(A, :maps_to_numberfields)
    return A.maps_to_numberfields::Vector{Tuple{AnticNumberField, AbsAlgAssToNfAbsMor{typeof(A), elem_type(A)}}}
  end

  d = dim(A)

  Adec = decompose(A)

  fields_not_cached = false
  for i = 1:length(Adec)
    if !isdefined(Adec[i][1], :maps_to_numberfields)
      fields_not_cached = true
    end
  end

  if fields_not_cached
    # Compute a LLL reduced basis of the maximal order of A to find "small"
    # polynomials for the number fields.
    OA = maximal_order(A)
    L = lll(basis_mat(OA, copy = false).num)
    n = basis_mat(OA, copy = false).den
    basis_lll = [ elem_from_mat_row(A, L, i, n) for i = 1:d ]
  end

  M = zero_matrix(FlintQQ, 0, d)
  matrices = Vector{fmpq_mat}()
  fields = Vector{AnticNumberField}()
  for i = 1:length(Adec)
    # For each small algebra construct a number field and the isomorphism
    B, BtoA = Adec[i]
    dB = dim(B)
    if !isdefined(B, :maps_to_numberfields)
      local K, BtoK
      found_field = false # Only for debugging
      for j = 1:d
        t = BtoA\basis_lll[j]
        mint = minpoly(t)
        if degree(mint) == dB
          found_field = true
          K, BtoK = _as_field_with_isomorphism(B, t, mint)
          B.maps_to_numberfields = Tuple{AnticNumberField, AbsAlgAssToNfAbsMor{typeof(B), elem_type(B)}}[(K, BtoK)]
          push!(fields, K)
          break
        end
      end
      @assert found_field "This should not happen..."
    else
      K, BtoK = B.maps_to_numberfields[1]
      push!(fields, K)
    end

    if length(Adec) == 1
      A.maps_to_numberfields = Tuple{AnticNumberField, AbsAlgAssToNfAbsMor{typeof(A), elem_type(A)}}[(K, BtoK)]
      return A.maps_to_numberfields
    end

    # Construct the map from K to A
    N = zero_matrix(FlintQQ, degree(K), d)
    for j = 1:degree(K)
      t = BtoA(BtoK\basis(K)[j])
      elem_to_mat_row!(N, j, t)
    end
    push!(matrices, N)
    M = vcat(M, N)
  end
  @assert nrows(M) == d

  invM = inv(M)
  matrices2 = Vector{fmpq_mat}(undef, length(matrices))
  offset = 1
  for i = 1:length(matrices)
    r = nrows(matrices[i])
    N = sub(invM, 1:d, offset:(offset + r - 1))
    matrices2[i] = N
    offset += r
  end

  result = Vector{Tuple{AnticNumberField, AbsAlgAssToNfAbsMor{typeof(A), elem_type(A)}}}()
  for i = 1:length(fields)
    push!(result, (fields[i], AbsAlgAssToNfAbsMor(A, fields[i], matrices2[i], matrices[i])))
  end
  A.maps_to_numberfields = result
  return result
end

################################################################################
#
#  Random elements
#
################################################################################

function rand(A::AbsAlgAss{T}) where T
  c = T[rand(base_ring(A)) for i = 1:dim(A)]
  return A(c)
end

function rand(A::AbsAlgAss{T}, rng::UnitRange{Int}) where T
  c = T[rand(base_ring(A), rng) for i = 1:dim(A)]
  return A(c)
end

function rand(A::AlgAss{fmpq}, rng::UnitRange{Int} = -20:20)
  c = [fmpq(rand(FlintZZ, rng)) for i = 1:dim(A)]
  return A(c)
end

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

function gens(A::AbsAlgAss, return_full_basis::Type{Val{T}} = Val{false}) where T
  K = base_ring(A)
  d = dim(A)

  rfb = return_full_basis == Val{true}

  # Sort the basis by the degree of the minpolys (hopefully those with higher
  # degree generate a "bigger" subalgebra)
  minpoly_degrees = [ (i, degree(minpoly(A[i]))) for i = 1:d ]
  sort!(minpoly_degrees, by = x -> x[2], rev = true)

  generators = Vector{elem_type(A)}()
  full_basis = elem_type(A)[ one(A) ] # Contains products of generators which form a full basis
  rfb ? full_basis_indices = Vector{Tuple{Int, Int}}[ Tuple{Int, Int}[] ] : nothing
  B = zero_matrix(K, d, d)

  cur_dim = 0
  for i = 1:d
    if cur_dim == d
      break
    end

    n = length(full_basis)
    b = A[minpoly_degrees[i][1]]
    power = 1
    while cur_dim < d
      for k = 1:d
        B[d, k] = coeffs(b, copy = false)[k]
      end
      new_dim = rref!(B)
      if cur_dim == new_dim
        break
      end
      if power == 1
        push!(generators, b)
      end
      push!(full_basis, b)
      if rfb
        ind = Tuple{Int, Int}[ (length(generators), power) ]
        push!(full_basis_indices, ind)
      end
      cur_dim = new_dim
      cur_dim == d ? break : nothing

      for r = 1:n
        bb = b*full_basis[r]
        for l = 1:n
          t = full_basis[l]*bb
          for k = 1:d
            B[d, k] = coeffs(t, copy = false)[k]
          end
          new_dim = rref!(B)
          if cur_dim == new_dim
            continue
          end
          push!(full_basis, t)
          cur_dim = new_dim
          if rfb
            ind2 = deepcopy(ind)
            prepend!(ind2, full_basis_indices[l])
            append!(ind2, full_basis_indices[r])
            push!(full_basis_indices, ind2)
          end
          cur_dim == d ? break : nothing
        end
        cur_dim == d ? break : nothing
      end
      b *= A[minpoly_degrees[i][1]]
      power += 1
    end
  end

  # Remove the one
  popfirst!(full_basis)
  rfb ? popfirst!(full_basis_indices) : nothing

  if rfb
    return generators, full_basis, full_basis_indices
  else
    return generators
  end
end

################################################################################
#
#  Primitive elements
#
################################################################################

function primitive_element(A::AbsAlgAss)
  a, _ = _primitive_element(A)
  return a
end

function _primitive_element(A::AbsAlgAss)
  error("Not implemented yet")
  return nothing
end

function _primitive_element(A::AbsAlgAss{T}) where T <: Union{nmod, fq, fq_nmod, Generic.Res{fmpz}, fmpq, Generic.ResF{fmpz}, gfp_elem}
  d = dim(A)
  a = rand(A)
  f = minpoly(a)
  while degree(f) < d
    a = rand(A)
    f = minpoly(a)
  end
  return a, f
end

function _as_field(A::AbsAlgAss{T}) where T
  d = dim(A)
  a, mina = _primitive_element(A)
  b = one(A)
  M = zero_matrix(base_ring(A), d, d)
  elem_to_mat_row!(M, 1, b)
  for i in 1:(d - 1)
    b = mul!(b, b, a)
    elem_to_mat_row!(M, i + 1, b)
  end
  B = inv(M)
  N = zero_matrix(base_ring(A), 1, d)
  local f
  let N = N, B = B
    f = function(x)
      for i in 1:d
        N[1, i] = x.coeffs[i]
      end
      return N * B
    end
  end
  return a, mina, f
end

function _as_field_with_isomorphism(A::AbsAlgAss{S}) where { S <: Union{fmpq, gfp_elem, Generic.ResF{fmpz}, fq_nmod, fq} }
  return _as_field_with_isomorphism(A, _primitive_element(A)...)
end

# Assuming a is a primitive element of A and mina its minimal polynomial, this
# functions constructs the field base_ring(A)/mina and the isomorphism between
# A and this field.
function _as_field_with_isomorphism(A::AbsAlgAss{S}, a::AbsAlgAssElem{S}, mina::T) where { S <: Union{fmpq, gfp_elem, Generic.ResF{fmpz}, fq_nmod, fq}, T <: Union{fmpq_poly, gfp_poly, gfp_fmpz_poly, fq_nmod_poly, fq_poly} }
  s = one(A)
  M = zero_matrix(base_ring(A), dim(A), dim(A))
  elem_to_mat_row!(M, 1, s)
  for i = 2:dim(A)
    s = mul!(s, s, a)
    elem_to_mat_row!(M, i, s)
  end

  if base_ring(A) == FlintQQ
    K = number_field(mina, cached = false)[1]
    return K, AbsAlgAssToNfAbsMor(A, K, inv(M), M)
  elseif base_ring(A) isa GaloisField
    Fq = FqNmodFiniteField(mina, Symbol("a"), false)
    return Fq, AbsAlgAssToFqMor(A, Fq, inv(M), M, parent(mina))
  elseif base_ring(A) isa Generic.ResField{fmpz}
    Fq = FqFiniteField(mina, Symbol("a"), false)
    return Fq, AbsAlgAssToFqMor(A, Fq, inv(M), M, parent(mina))
  elseif base_ring(A) isa FqNmodFiniteField || base_ring(A) isa FqFiniteField
    Fr, RtoFr = field_extension(mina)
    return Fr, AbsAlgAssToFqMor(A, Fr, inv(M), M, parent(mina), RtoFr)
  else
    error("Not implemented")
  end
end

################################################################################
#
#  Regular matrix algebra
#
################################################################################

@doc Markdown.doc"""
    regular_matrix_algebra(A::Union{ AlgAss, AlgGrp })

Returns the matrix algebra B generated by the right representation matrices of
the basis elements of A and a map from B to A.
"""
function regular_matrix_algebra(A::Union{ AlgAss, AlgGrp })
  K = base_ring(A)
  B = AlgMat(K, [ representation_matrix(A[i], :right) for i = 1:dim(A) ], isbasis = true, check = false)
  return B, hom(B, A, identity_matrix(K, dim(A)), identity_matrix(K, dim(A)))
end

###############################################################################
#
#  Construction of a crossed product algebra
#
###############################################################################

function find_elem(G::Array{T,1}, el::T) where T
  i=1
  while true
    if el.prim_img==G[i].prim_img
      return i
    end
    i+=1
  end
end

#K/Q is a Galois extension.
function CrossedProductAlgebra(K::AnticNumberField, G::Array{T,1}, cocval::Array{nf_elem, 2}) where T

  n=degree(K)
  m=length(G)
  #=
  Multiplication table
  I order the basis in this way:
  First, I put the basis of the Galois Group, then the product of them with the first
  element of basis of the order and so on...
  =#

  M=Array{fmpq,3}(undef, n*m, n*m, n*m)
  for i=1:n*m
    for j=1:n*m
      for s=1:n*m
        M[i,j,s]=fmpq(0)
      end
    end
  end
  B=basis(K)
  for i=1:n
    for j=1:m
      #I have the element B[i]*G[j]
      for k=1:n
        for h=1:m
          # I take the element B[k]*G[h]
          # and I take the product
          # B[i]*G[j]* B[k]*G[h]=B[i]*G[j](B[k])*c[j,h]*(G[j]*G[h])
          ind=find_elem(G,G[h] * G[j])
          x=B[i]*G[j](B[k])*cocval[j,h]
          #@show i, j, k,h,  ind,B[i],G[j](B[k]),cocval[j,h],  x
          for s=0:n-1
            M[j+(i-1)*n, h+(k-1)*n, ind+s*n]=coeff(x,s)
          end
          #@show M
        end
      end
    end
  end
  return AlgAss(FlintQQ, M)

end

function CrossedProductAlgebra(O::NfOrd, G::Array{T,1}, cocval::Array{nf_elem, 2}) where T

  n=degree(O)
  m=length(G)
  K=nf(O)
  #=
  Multiplication table
  I order the basis in this way:
  First, I put the basis of the Galois Group, then the product of them with the first
  element of basis of the order and so on...
  =#

  M=Array{fmpq,3}(undef, n*m, n*m, n*m)
  for i=1:n*m
    for j=1:n*m
      for s=1:n*m
        M[i,j,s]=fmpq(0)
      end
    end
  end
  B = basis(O, copy = false)
  el = O(0)
  for j=1:m
    for k=1:n
      l =O(G[j](K(B[k])), false)
      for h=1:m
        ind = find_elem(G, G[h] * G[j])
        t = O(cocval[j,h], false)
        for i=1:n
          #I have the element B[i]*G[j]
          # I take the element B[k]*G[h]
          # and I take the product
          # B[i]*G[j]* B[k]*G[h]=B[i]*G[j](B[k])*c[j,h]*(G[j]*G[h])
          mul!(el, B[i], l)
          mul!(el, el, t)
          y = coordinates(el)
          for s=0:n-1
            M[j+(i-1)*m, h+(k-1)*m, ind+s*m] = y[s+1]
          end
        end
      end
    end
  end
  j1 = find_identity(G, *)
  j = find_elem(G, j1)
  O1 = fmpq[0 for i=1:n*m]
  O1[j] = fmpq(1)
  A = AlgAss(FlintQQ, M, O1)
  A.issimple = 1
  return A

end

################################################################################
#
#  Quaternion algebras
#
################################################################################

function quaternion_algebra(a::Int, b::Int)

  M = Array{fmpq,3}(undef, 4,4,4)
  for i = 1:4
    for j = 1:4
      for k = 1:4
        M[i,j,k] = 0
      end
    end
  end
  M[1,1,1] = 1 # 1*1=1
  M[1,2,2] = 1 # 1*i=i
  M[1,3,3] = 1 # 1*j=j
  M[1,4,4] = 1 # 1*ij=1

  M[2,1,2] = 1
  M[2,2,1] = a
  M[2,3,4] = 1
  M[2,4,3] = a

  M[3,1,3] = 1
  M[3,2,4] = -1
  M[3,3,1] = b
  M[3,4,2] = -b

  M[4,1,4] = 1
  M[4,2,3] = -a
  M[4,3,2] = b
  M[4,4,1] = -a*b
  O = fmpq[1, 0, 0, 0]
  return AlgAss(FlintQQ, M, O)

end


module MIPforQP

import JuMP, MathOptInterface
import Mosek, MosekTools
const MOI = MathOptInterface
const MOIU = MOI.Utilities
const VI = MOI.VariableIndex
const SV = MOI.SingleVariable

import LinearAlgebra, SparseArrays
import Gurobi

abstract type AbstractRelaxation end

struct HongboRelaxation <: AbstractRelaxation
    num_layers::Int
end
struct NeuralNetRelaxation <: AbstractRelaxation
    num_layers::Int
end
struct Reformulation{T <: AbstractRelaxation}
    relaxation::T
    soc_lower_bound::Bool
end
Reformulation(relaxation::AbstractRelaxation) = Reformulation(relaxation, true)

function formulate_quadratics!(model::JuMP.Model, reformulation::Reformulation)
    vq_ci, vq_f, vq_s = vectorize_quadratics!(model)
    # The following is a hacky way to determine if the objective is linear or quadratic.
    obj = try
        MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}())
        nothing
    catch InexactError
        MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
    end
    if obj === nothing
        MOI.delete(JuMP.backend(model), vq_ci)
        bridge = _relax_quadratic!(JuMP.backend(model), reformulation, vq_f, vq_s)
    else
        bridge = _relax_quadratic!(JuMP.backend(model), reformulation, vq_f, vq_s, obj)
    end
    MOIU.attach_optimizer(model)
    return bridge
end

function vectorize_quadratics!(model::JuMP.Model)
    moi_backend = JuMP.backend(model)
    @assert typeof(moi_backend) <: MOIU.CachingOptimizer
    @assert moi_backend.state in (MOIU.NO_OPTIMIZER, MOIU.EMPTY_OPTIMIZER)
    return vectorize_quadratics!(moi_backend.model_cache.model)
end

function vectorize_quadratics!(cached_model::MOIU.Model{T}) where {T}
    scalar_quad_cons = cached_model.moi_scalarquadraticfunction
    @assert isempty(scalar_quad_cons.moi_equalto)
    @assert isempty(scalar_quad_cons.moi_interval)
    @assert isempty(scalar_quad_cons.moi_semicontinuous)
    @assert isempty(scalar_quad_cons.moi_semiinteger)
    @assert isempty(scalar_quad_cons.moi_zeroone)

    num_gt = length(scalar_quad_cons.moi_greaterthan)
    num_lt = length(scalar_quad_cons.moi_lessthan)

    constants = Array{T}(undef, 0)
    affine_terms = Array{MOI.VectorAffineTerm{T}}(undef, 0)
    quad_terms = Array{MOI.VectorQuadraticTerm{T}}(undef, 0)

    func_count = 0
    for (q_ci, q_f, q_s) in scalar_quad_cons.moi_greaterthan
        func_count += 1
        push!(constants, -(q_f.constant - q_s.lower))
        for aff in q_f.affine_terms
            push!(affine_terms, MOI.VectorAffineTerm{T}(func_count, MOI.ScalarAffineTerm{T}(-aff.coefficient, aff.variable_index)))
        end
        for quad in q_f.quadratic_terms
            push!(quad_terms, MOI.VectorQuadraticTerm{T}(func_count, MOI.ScalarQuadraticTerm{T}(-quad.coefficient, quad.variable_index_1, quad.variable_index_2)))
        end
    end

    @assert func_count == num_gt

    for (q_ci, q_f, q_s) in scalar_quad_cons.moi_lessthan
        func_count += 1
        push!(constants, q_f.constant - q_s.upper)
        for aff in q_f.affine_terms
            push!(affine_terms, MOI.VectorAffineTerm{T}(func_count, MOI.ScalarAffineTerm{T}(aff.coefficient, aff.variable_index)))
        end
        for quad in q_f.quadratic_terms
            push!(quad_terms, MOI.VectorQuadraticTerm{T}(func_count, MOI.ScalarQuadraticTerm{T}(quad.coefficient, quad.variable_index_1, quad.variable_index_2)))
        end
    end

    @assert func_count == num_gt + num_lt

    vector_quad_cons = cached_model.moi_vectorquadraticfunction.moi_nonpositives
    @assert isempty(vector_quad_cons)
    constraint_index = MOI.ConstraintIndex{MOI.VectorQuadraticFunction{T},MOI.Nonpositives}(1)
    vector_func = MOI.VectorQuadraticFunction{T}(affine_terms, quad_terms, constants)
    vector_set = MOI.Nonpositives(num_gt + num_lt)
    retval = (constraint_index, vector_func, vector_set)
    push!(vector_quad_cons, retval)
    empty!(scalar_quad_cons.moi_greaterthan)
    empty!(scalar_quad_cons.moi_lessthan)
    return retval
end

struct MIPforQPBridge
    reformulation::Reformulation
    squared_vars::Dict{VI,VI}
    binaries::Dict{VI,Union{Nothing,Vector{VI}}}
end
MIPforQPBridge(reformulation::Reformulation) = MIPforQPBridge(reformulation, Dict{VI,VI}(), Dict{VI,Vector{VI}}())

function _indices_and_coefficients(
    I::AbstractVector{Int}, J::AbstractVector{Int}, V::AbstractVector{Float64},
    indices::AbstractVector{Int}, coefficients::AbstractVector{Float64},
    f::MOI.ScalarQuadraticFunction, canonical_index::Dict{VI,Int}
)
    variables_seen = 0
    for (i, term) in enumerate(f.quadratic_terms)
        vi_1 = term.variable_index_1
        vi_2 = term.variable_index_2
        if !haskey(canonical_index, vi_1)
            variables_seen += 1
            canonical_index[vi_1] = variables_seen
        end
        if !haskey(canonical_index, vi_2)
            variables_seen += 1
            canonical_index[vi_2] = variables_seen
        end

        I[i] = canonical_index[term.variable_index_1]
        J[i] = canonical_index[term.variable_index_2]
        V[i] =  term.coefficient
        # Gurobi returns a list of terms. MOI requires 0.5 x' Q x. So, to get
        # from
        #   Gurobi -> MOI => multiply diagonals by 2.0
        #   MOI -> Gurobi => multiply diagonals by 0.5
        # Example: 2x^2 + x*y + y^2
        #   |x y| * |a b| * |x| = |ax+by bx+cy| * |x| = 0.5ax^2 + bxy + 0.5cy^2
        #           |b c|   |y|                   |y|
        #   Gurobi needs: (I, J, V) = ([0, 0, 1], [0, 1, 1], [2, 1, 1])
        #   MOI needs:
        #     [SQT(4.0, x, x), SQT(1.0, x, y), SQT(2.0, y, y)]
        if I[i] == J[i]
            V[i] *= 0.5
        end
    end
    for (i, term) in enumerate(f.affine_terms)
        indices[i] = term.variable_index.value
        coefficients[i] = term.coefficient
    end
    return
end

function _indices_and_coefficients(f::MOI.ScalarQuadraticFunction, canonical_index::Dict{VI,Int})
    f_canon = MOI.Utilities.canonical(f)
    nnz_quadratic = length(f_canon.quadratic_terms)
    nnz_affine = length(f_canon.affine_terms)
    I = Vector{Int}(undef, nnz_quadratic)
    J = Vector{Int}(undef, nnz_quadratic)
    V = Vector{Float64}(undef, nnz_quadratic)
    indices = Vector{Int}(undef, nnz_affine)
    coefficients = Vector{Float64}(undef, nnz_affine)
    _indices_and_coefficients(I, J, V, indices, coefficients, f_canon, canonical_index)
    return indices, coefficients, I, J, V
end

function _get_bounds(model, x::VI)
    c_lt = MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}(x.value)
    c_gt = MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}(x.value)
    c_int = MOI.ConstraintIndex{MOI.SingleVariable,MOI.Interval{Float64}}(x.value)
    if MOI.is_valid(model, c_int)
        @assert !MOI.is_valid(c_lt) && !MOI.is_valid(c_gt)
        int::MOI.Interval{Float64} = MOI.get(model, MOI.ConstraintSet(), c_int)
        return int.lower, int.upper
    elseif MOI.is_valid(model, c_lt)
        lt::MOI.LessThan{Float64} = MOI.get(model, MOI.ConstraintSet(), c_lt)
        if MOI.is_valid(model, c_gt)
            gt_1::MOI.GreaterThan{Float64} = MOI.get(model, MOI.ConstraintSet(), c_gt)
            return gt_1.lower, lt.upper
        else
            return -Inf, lt.upper
        end
    elseif MOI.is_valid(model, c_gt)
        gt_2::MOI.GreaterThan{Float64} = MOI.get(model, MOI.ConstraintSet(), c_gt)
        return gt_2.lower, Inf
    else
        return -Inf, Inf
    end
end

function _map_to_interval_if_needed(model, x::VI, y::VI, l::Float64, u::Float64)
    if l == 0 && u == 1
        return x, y
    else
        Δ = u - l
        @assert Δ > 0
        x̃_v, x̃_c = MOI.add_constrained_variable(model, MOI.Interval(0.0, 1.0))
        ỹ_v, ỹ_c = MOI.add_constrained_variable(model, MOI.Interval(0.0, 1.0))
        MOIU.normalize_and_add_constraint(model, SV(x) - (l + Δ * SV(x̃_v)), MOI.EqualTo(0.0))
        MOIU.normalize_and_add_constraint(model, SV(y) - (l^2 + 2l * Δ * SV(x̃_v) + Δ^2 * SV(ỹ_v)), MOI.EqualTo(0.0))
        return x̃_v, ỹ_v
    end
end

function reformulate_unary_quadratic_term!(model::MOI.ModelLike, reformulation::Reformulation{HongboRelaxation}, _x::VI, _y::VI, l::Float64, u::Float64)
    num_layers = reformulation.relaxation.num_layers
    ξ_v = MOI.add_variables(model, num_layers)
    η_v = MOI.add_variables(model, num_layers)
    λ_1_v, λ_1_c = MOI.add_constrained_variables(model, [MOI.GreaterThan(0.0) for i in 1:num_layers])
    λ_2_v, λ_2_c = MOI.add_constrained_variables(model, [MOI.GreaterThan(0.0) for i in 1:num_layers])
    z_v, z_c = MOI.add_constrained_variables(model, [MOI.ZeroOne() for i in 1:num_layers])
    x = SV(_x)
    y = SV(_y)
    ξ = [SV(ξ_v[i]) for i in 1:num_layers]
    η = [SV(η_v[i]) for i in 1:num_layers]
    λ_1 = [SV(λ_1_v[i]) for i in 1:num_layers]
    λ_2 = [SV(λ_2_v[i]) for i in 1:num_layers]
    z = [SV(z_v[i]) for i in 1:num_layers]

    θ_min = (if l > 0
        atan(l^2 - 1) / (2l)
    elseif l == 0
        -π / 2
    else
        atan(l^2 - 1) / (2l) - π
    end)
    θ_max = (if u > 0
        atan(u^2 - 1) / (2u)
    elseif u == 0
        -π / 2
    else
        atan(u^2 - 1) / (2u) - π
    end)
    @assert -3π / 2 < θ_min < π / 2
    @assert -3π / 2 < θ_max < π / 2
    θ_d = θ_max - θ_min
    θ_mid = (θ_max + θ_min) / 2

    ν = num_layers
    # RLP inequality (1)
    MOIU.normalize_and_add_constraint(model, y - (l + u) * x + l * u, MOI.LessThan(0.0))
    # Equation (11)
    MOIU.normalize_and_add_constraint(model, ξ[ν] * cos(θ_d / 2^(ν + 1)) + η[ν] * sin(θ_d / 2^(ν + 1)) - (y + 1.0) / 2.0 * cos(θ_d / 2^(ν + 1)), MOI.GreaterThan(0.0))
    if !reformulation.soc_lower_bound
        # Equation 12
        MOIU.normalize_and_add_constraint(model, ξ[ν] * cos(θ_d / 2^ν) + η[ν] * sin(θ_d / 2^ν) - (y + 1.0) / 2.0, MOI.LessThan(0.0))
        # Equation 13
        MOIU.normalize_and_add_constraint(model, ξ[ν] - (y + 1.0) / 2.0, MOI.LessThan(0.0))
    end
    # Equation block (20)
    MOIU.normalize_and_add_constraint(model, ξ[1] - x * cos(θ_mid) - (y / 2.0 - 0.5) * sin(θ_mid), MOI.EqualTo(0.0))
    C = max(l^2, u^2) / 2 + 0.5
    MOIU.normalize_and_add_constraint(model, (1.0λ_2[1] - 1.0λ_1[1]) * C + x * sin(θ_mid) - (y / 2.0 - 0.5) * cos(θ_mid), MOI.EqualTo(0.0))
    MOI.add_constraint(model, η[1] - (1.0λ_1[1] + 1.0λ_2[1]) * C, MOI.EqualTo(0.0))
    MOIU.normalize_and_add_constraint(model, 1.0λ_1[1] - (1.0 - z[1]), MOI.LessThan(0.0))
    MOI.add_constraint(model, 1.0λ_2[1] - 1.0z[1], MOI.LessThan(0.0))
    for j in 1:(ν - 1)
        # Equation block 21
        MOI.add_constraint(model, ξ[j + 1] - ξ[j] * cos(θ_d / 2^(j + 1)) - η[j] * sin(θ_d / 2^(j + 1)), MOI.EqualTo(0.0))
        C_j = C * sin(θ_d / 2^(j + 1))
        MOI.add_constraint(model, (1.0λ_2[j + 1] - 1.0λ_1[j + 1]) * C_j + ξ[j] * sin(θ_d / 2^(j + 1)) - η[j] * cos(θ_d / 2^(j + 1)), MOI.EqualTo(0.0))
        MOI.add_constraint(model, η[j + 1] - (1.0λ_1[j + 1] + 1.0λ_2[j + 1]) * C_j, MOI.EqualTo(0.0))
        MOIU.normalize_and_add_constraint(model, 1.0λ_1[j] - (1.0 - z[j + 1]), MOI.LessThan(0.0))
        MOI.add_constraint(model, 1.0λ_2[j] - 1.0z[j + 1], MOI.LessThan(0.0))
    end
end

function reformulate_unary_quadratic_term!(model::MOI.ModelLike, reformulation::Reformulation{NeuralNetRelaxation}, _x::VI, _y::VI, l::Float64, u::Float64)
    x, y = _map_to_interval_if_needed(model, _x, _y, l, u)
    MOI.add_constraint(model, 1.0SV(x) - SV(y), MOI.GreaterThan(0.0))
    return _formulate_zero_one_quadratic!(model, x, y, reformulation.relaxation.num_layers, !reformulation.soc_lower_bound)
end

function _formulate_zero_one_quadratic!(model::MOI.ModelLike, x::VI, y::VI, num_layers::Int, impose_lower_bound::Bool)
    z_v, z_c = MOI.add_constrained_variables(model, [MOI.ZeroOne() for i in 1:num_layers])
    g_v, g_c = MOI.add_constrained_variables(model, [MOI.Interval(0.0, 1.0) for i in 1:num_layers])

    gv = Dict(i => SV(g_v[i]) for i in 1:num_layers)
    gv[0] = SV(x)

    agg_aff = 1.0 * SV(y) - SV(x) + sum(gv[s] / 2.0^(2s) for s in 1:num_layers)

    if impose_lower_bound
        agg_c = MOI.add_constraint(model, agg_aff, MOI.EqualTo(0.0))
    else
        agg_c = MOI.add_constraint(model, agg_aff, MOI.LessThan(0.0))
    end

    for i in 1:num_layers
        α_i = SV(z_v[i])
        MOI.add_constraint(model, gv[i] - 2.0gv[i - 1], MOI.LessThan(0.0))
        MOIU.normalize_and_add_constraint(model, gv[i] + 2.0gv[i - 1] - 2.0, MOI.LessThan(0.0))
        MOI.add_constraint(model, gv[i] + 2.0gv[i - 1] - 2.0 * α_i, MOI.GreaterThan(0.0))
        MOIU.normalize_and_add_constraint(model, gv[i] - 2.0gv[i - 1] + 2.0 * α_i, MOI.GreaterThan(0.0))
    end
    return z_v
end

function _get_Q_matrix(q_func::MOI.ScalarQuadraticFunction, canonical_index::Dict{VI,Int}, n::Int)
    indices, coefficients, I, J, V = _indices_and_coefficients(q_func, canonical_index)
    Q = Matrix{Float64}(SparseArrays.sparse(I, J, V, n, n))
    Q = 1 / 2 * (Q + Q')

    @assert n == size(Q, 1) == size(Q, 2)
    @assert LinearAlgebra.issymmetric(Q)
    return Q
end

function _compute_diagonal_shift(Q::Matrix{Float64})
    n = size(Q, 1)
    @assert n == size(Q, 2)
    sdp_model = JuMP.Model(JuMP.with_optimizer(Mosek.Optimizer))
    δ = JuMP.@variable(sdp_model, [i = 1:n])
    JuMP.@objective(sdp_model, Min, sum(δ))
    JuMP.@SDconstraint(sdp_model, Q + LinearAlgebra.diagm(δ .- 1e-4) >= 0)
    JuMP.optimize!(sdp_model)
    @assert JuMP.primal_status(sdp_model) == MOI.FEASIBLE_POINT
    return JuMP.value.(δ)
end

# Assumption: Constraints we wish to reformulate have been bridged to the form
# `VectorQuadraticFunction`-in-`Nonpositives`.
function _relax_quadratic!(
    model,
    reformulation::Reformulation,
    f::MOI.VectorQuadraticFunction{Float64},
    s::MOI.Nonpositives,
    obj::Union{Nothing,MOI.ScalarQuadraticFunction{Float64}}=nothing
)
    squared_vars = Dict{VI,VI}()
    binaries = Dict{VI,Union{Nothing,Vector{VI}}}()
    # Walk all constraints in vector and populate squared_vars dict
    sqt_terms = vcat([t -> t.scalar_term for t in f.quadratic_terms], obj.quadratic_terms)
    for st in sqt_terms
        for x in (st.variable_index_1, st.variable_index_2)
            if !haskey(squared_vars, x)
                l, u = _get_bounds(model, x)
                y_lb = l < 0 < u ? 0.0 : min(l^2, u^2)
                y_ub = max(l^2, u^2)
                y_v, y_c = MOI.add_constrained_variable(model, MOI.Interval{Float64}(y_lb, y_ub))
                squared_vars[x] = y_v
                if reformulation.soc_lower_bound
                    # Alternatively, could pass SOC constraint with
                    # MOI.add_constraint(model, MOI.VectorOfVariables([y_v, x]), MOI.SecondOrderCone(2))
                    MOI.add_constraint(model, 1.0SV(y_v) - 1.0SV(x) * SV(x), MOI.GreaterThan(0.0))
                end
                z_v = reformulate_unary_quadratic_term!(model, reformulation, x, y_v, l, u)
                binaries[x] = z_v
            end
        end
    end

    n = length(keys(squared_vars))
    q_funcs = MOIU.scalarize(f)
    canonical_index = Dict{VI,Int}()
    for q_func in q_funcs
        Q = _get_Q_matrix(q_func, canonical_index, n)
        δ_value = _compute_diagonal_shift(Q)
        quad_shift = MOI.ScalarQuadraticFunction(MOI.ScalarAffineTerm{Float64}[], [MOI.ScalarQuadraticTerm(2δ_value[canonical_index[xi]], xi, xi) for xi in keys(canonical_index)], 0.0)
        aff_shift = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-δ_value[canonical_index[xi]], squared_vars[xi]) for xi in keys(canonical_index)], 0.0)
        shifted_q_func = MOIU.canonical(q_func + quad_shift + aff_shift)
        MOIU.normalize_and_add_constraint(model, shifted_q_func, MOI.LessThan(0.0))
    end
    # Now handle objective
    if obj !== nothing
        Q = _get_Q_matrix(obj, canonical_index, n)
        δ_value = _compute_diagonal_shift(Q)
        quad_shift = MOI.ScalarQuadraticFunction(MOI.ScalarAffineTerm{Float64}[], [MOI.ScalarQuadraticTerm(2δ_value[canonical_index[xi]], xi, xi) for xi in keys(canonical_index)], 0.0)
        aff_shift = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-δ_value[canonical_index[xi]], squared_vars[xi]) for xi in keys(canonical_index)], 0.0)
        shifted_q_func = MOIU.canonical(obj + quad_shift + aff_shift)
        MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), shifted_q_func)
    end

    return MIPforQPBridge(reformulation, squared_vars, binaries)
end

include("box_qp_util.jl")

end # module


import NNlib: conv_direct, conv_im2col

@testset "Conv Inference" begin
    x = rand(10, 10, 3, 2)
    w = rand(3, 3, 3, 1)

    impl = [conv, conv_direct, conv_im2col]
    NNlib.is_nnpack_available() && push!(impl, NNlib.conv_nnpack)

    for T in impl
        @test T(x, w, DenseConvDims(x, w)) isa AbstractArray{K,4} where K
    end
end


using CUDA
const pretrained_folder = "./pretrained_discriminators"

# Dictionary for pretrained discriminator paths
pretrained = Dict(
            "clickbait" => "clickbait_classifier_head_1024.bson",
            "toxicity"  => "toxicity_classifier_head_768.bson",
            "sentiment" => "sentiment_classifier_head_1024.bson"
        )

"""
struct ClassifierHead
    linear_layer::Dense
    embed_size::Int
    class_size::Int
end

Struct for ClassifiedHead, defined with a single linear layer and two paramters: embed_size-> Size of Embedding, class_size->Number of classes.

"""
struct ClassifierHead
    linear_layer::Dense
    embed_size::Int
    class_size::Int
end

"""
    ClassifierHead(class_size::Int=1, embed_size::Int=768; load_from_pretrained=false, discrim=nothing, file_name=nothing, path=nothing)

Function to initiate ClassifierHead layer, with defined class_size and embed_size. If `load_from_pretrained` is set to be true, load model from pretrained models (based on `discrim` name) or from specified `path`.
"""
function ClassifierHead(class_size::Int=1, embed_size::Int=768; load_from_pretrained=false, discrim=nothing, file_name=nothing, path=nothing)
    global pretrained_folder
    if load_from_pretrained==true
        if discrim==nothing
            error("Discriminator not provided")
        else
            if discrim in keys(pretrained)
                path_f = joinpath(@__DIR__, pretrained_folder, pretrained[discrim])
            else
                print("Discriminator $discrim not in pretrained discriminators, checking if `path` provided")
                if isnothing(path)
                    error("`path` not provided for custom discriminator bson file. Try `ClassifierHead(...; path=...)` or `getDiscriminator(...; ... , path=... )`")
                else
                    path_f = path
                end
            end
            BSON.@load path_f config_metadata weights
            layer = Dense(weights["weight"], weights["bias"])
            class_size = length(weights["bias"])
        end
    else
        layer = Dense(embed_size, class_size)
        config_metadata=nothing
    end
    ClassifierHead(layer, embed_size, class_size), config_metadata
end

Flux.@functor ClassifierHead (linear_layer,)

"""
    (m::ClassifierHead)(hidden_state)

Method for passing hidden state through Linear layer of ClassifierHead `m`, return the final logits.

"""
function (m::ClassifierHead)(hidden_state)
    lm_logits = m.linear_layer(hidden_state)
    lm_logits
end

"""
struct DiscriminatorV1
    cl_head::ClassifierHead
    model
    embed_size::Int
end

Struct to contain the model and ClassifierHead of PPLM model, to be used in masked training.

"""
struct DiscriminatorV1
    cl_head::ClassifierHead
    model
    embed_size::Int
end

Flux.@functor DiscriminatorV1 (cl_head,)

"""
    avg_representation(m::DiscriminatorV1, input_ids; args=nothing)

Function to create average representation (without mask) of Input tokens `input_ids`, using the DiscriminatorV1 `m`.

"""
function avg_representation(m::DiscriminatorV1, input_ids; args=nothing)
    if args!=nothing && args.cached == true
        return input_ids
    else
        outputs = m.model(input_ids; output_attentions=false,
                    output_hidden_states=true, use_cache=false)
        hidden = output.hidden_states
        hidden = sum(hidden, dims = 2)
        hidden_ = dropdims(hidden, dims=2)
        return hidden_
    end
end

# DiscriminatorV1 -> without mask on hidden_state
function (m::DiscriminatorV1)(x; args=nothing)
    hidden_state = avg_representation(m, x; args)
    logits = m.cl_head(hidden_state)
    logits, dims=1
end

"""
struct DiscriminatorV2
    cl_head::ClassifierHead
    model
    embed_size::Int
end

Struct to contain the model and ClassifierHead of PPLM model, to be used in masked training.

"""
struct DiscriminatorV2
    cl_head::ClassifierHead
    model
    embed_size::Int
end

Flux.@functor DiscriminatorV2 (cl_head,)

"""
    avg_representation(m::DiscriminatorV2, input_ids, mask; args=nothing)

Function to create masked average representation of Input tokens `input_ids`, using the DiscriminatorV2 `m`.

"""
function avg_representation(m::DiscriminatorV2, input_ids, mask; args=nothing)
    if args!=nothing && args.cached == true
        return input_ids
    else
        outputs = m.model(input_ids; output_attentions=false,
                    output_hidden_states=true, use_cache=false)
        hidden = outputs.hidden_states[end]
        hidden.*= mask
        hidden = sum(hidden, dims = 2) ./ sum(mask, dims=2)
        hidden_ = dropdims(hidden, dims=2)
        return hidden_
    end
end

function (m::DiscriminatorV2)(x, mask; args=nothing)
    hidden_state = avg_representation(m, x, mask; args)
    logits = m.cl_head(hidden_state)
    logits
end

"""
    get_discriminator(model; load_from_pretrained=false, discrim=nothing, file_name=nothing, version=2, class_size::Int=1, embed_size::Int=768, path=nothing)

Function to create discriminator based on provided model. Incase, `load_from_pretrained` is set to be true, loads ClassifierHead layer from pretrained models or `path` provided.

"""
function get_discriminator(model; load_from_pretrained=false, discrim=nothing, file_name=nothing, version=2, class_size::Int=1, embed_size::Int=768, path=nothing)
    global device
    model = model |> device
    cl_head, _ = ClassifierHead(class_size, embed_size; load_from_pretrained=load_from_pretrained, discrim=discrim, file_name=file_name, path=path) |> gpu
    if version == 1
        return DiscriminatorV1(cl_head, model, embed_size)
    else
        return DiscriminatorV2(cl_head, model, embed_size)
    end
end

"""
    save_classifier_head(cl_head; file_name=nothing, path=nothing, args=nothing, register_discrim=true, discrim_name="")

Function to save the ClassifiedHead as a BSON once the training is complete, based on the path provided. In case path is set as nothing, it saves the discriminators in `./pretrained_discriminators` folder relative to the package directory.
"""
function save_classifier_head(cl_head; file_name=nothing, path=nothing, args=nothing, register_discrim=true, discrim_name="")
    if path == nothing
        joinpath(@__DIR__,"./pretrained_discriminators")
    end
    if file_name == nothing
        file_name = string("custom_classifier_head_", cl_head.embed_size, "_.jl")
    end

    cl_head = cl_head |> cpu
    save_path = joinpath(path, file_name)
    config_metadata = args
    weights = Dict("weight"=> cl_head.linear_layer.W, "bias"=>cl_head.linear_layer.bias)
    if args==nothing
        @warn "Saving classifier without Hyperparameter information"
        config_metadata = Dict()
        config_metadata["embed_size"]=size(weights["weight"])[2]
        config_metadata["class_size"]=size(weights["weight"])[1]
        config_metadata["default_class"]=1
    else
        config_metadata = Dict()
        config_metadata=args
    end

    BSON.@save save_path config_metadata weights
end

"""
    save_discriminator(discrim, discrim_name="Custom"; file_name=nothing, path=nothing, args=nothing)

Function to save ClassifiedHead part of discriminator (by calling `save_classifier_head` function), which is the only trainable part of discriminator
"""
function save_discriminator(discrim, discrim_name="Custom"; file_name=nothing, path=nothing, args=nothing)
    if path == nothing
        path = joinpath(@__DIR__, "./pretrained_discriminators")
    end
    if file_name == nothing
        file_name = string("custom_classifier_head_", discrim.embed_size, "_.jl")
    end
    save_path = joinpath(path, file_name)
    println("Saving classifier head weights for the discriminator to $save_path")
    save_classifier_head(discrim.cl_head; file_name=file_name, path=path, args=args, discrim_name=discrim_name)
    print("Discriminator saved successfully")
end


#module UltrasoundDesignGallery

using Statistics
using Distributions
using LinearAlgebra
using Random
using StatsFuns

import Base.Threads
import CMAEvolutionStrategy
import CUDA
import Colors
import DSP
import FileIO
import Gtk
import Gtk.GConstants
import Gtk.ShortNames
import GtkReactive
import ImageCore
import ImageFiltering
import ImageTransformations
import ImageView
import Images
import JLD
import KernelFunctions
import LineSearches
import NLopt
import OffsetArrays
import OnlineStats
import Optim
import PDMats
import ProgressMeter
import REPL.TerminalMenus
import Reactive

include("latentgp/likelihood.jl")
include("latentgp/laplace.jl")
include("latentgp/map_laplace.jl")
include("latentgp/mcmc.jl")
include("latentgp/prediction.jl")
include("latentgp/utils.jl")
include("bayesopt/bayesian_optimization.jl")
include("bayesopt/optimization.jl")
include("bayesopt/acquisition.jl")
include("imageproc/diffusion.jl")
include("imageproc/filter.jl")
include("imageproc/laplacian_pyramid.jl")
include("ui/pairwise.jl")
include("ui/linesearch.jl")
include("ui/menu.jl")
include("ui/ui.jl")
include("ui/window.jl")
#include("ui/common.jl")
include("simulation/ultrasound.jl")

#end


struct Numeric end

@pure Numeric +, -, *, /, &, |, ^, !, >, >=, <, <=, ==, !=, sin, cos, tan, sqrt, float,
  trailing_zeros, >>, <<, abs, unsigned, rem

@abstract Numeric rand() = Float64
@abstract Numeric randn() = Float64
@abstract Numeric rand(::Type{Bool}) where T = Bool

@abstract Numeric sum(xs::Array{T,N}; dims = :) where {T,N} =
  dims == Const(:) ? T : Array{T,N}


#####
##### Types for describing an Instrument Operations (convolution, resampling)
#####

"""
    type AbstractInstrumentOperator
Abstract AbstractInstrumentOperator type
"""
abstract type AbstractInstrumentOperator end

"""
    type AbstractInstrument
Abstract Instrument type
"""
abstract type AbstractInstrument end

"""
    struct FixedKernelInstrument{FT}

A struct which provides all parameters for the convolution with a kernel, which is identical across the instrument spectral axis

# Fields
$(DocStringExtensions.FIELDS)
"""
struct FixedKernelInstrument{FT} <: AbstractInstrumentOperator
    "convolution Kernel"
    kernel::OffsetArray{FT,1}
    "Output spectral grid"
    ν_out::Array{FT,1}
end;

"""
    struct VariableInstrument{FT}

        A struct which provides all parameters for the convolution with a kernel, which varies across the instrument spectral axis

# Fields
$(DocStringExtensions.FIELDS)
"""
struct VariableKernelInstrument{FT,AA} <: AbstractInstrumentOperator
    "convolution Kernel"
    kernel::OffsetArray{FT,2,AA}
    "Output spectral grid"
    ν_out::Array{FT,1}
    "Output index grid"
    ind_out::Array{Int,1}
end;

struct FTSInstrument{FT} <: AbstractInstrument
    "Maximum Optical Path Difference (cm)"
    MOPD::FT
    "Field of View (rad)"
    FOV::FT
    "Assymmetry parameter"
    β::FT
end

"""
    type AbstractNoiseModel
Abstract AbstractNoiseModel type
"""
abstract type AbstractNoiseModel end

"""
    struct GratingNoiseModel{FT}

A struct which stores important variable to compute the noise of a grating spectrometer

# Fields
$(DocStringExtensions.FIELDS)
"""
Base.@kwdef  struct GratingNoiseModel <: AbstractNoiseModel
    "Integration time `[s]`"
    t_int::Unitful.Time
    "Detector pixel size (assuming quadratic) `[μm]`"
    detector_size::Unitful.Length
    "Detector quantum efficiency"
    Qₑ
    "Efficiency of the optical bench (Telescope, grating and other optical elements)"
    η
    "F-number"
    Fnumber
    "Spectral Sampling Interval SSI or Δλ `[μm]`"
    Δλ::Unitful.Length
    "Readout noise `[e⁻]`"
    σ_read
    "Dark current `[e⁻/s]`"
    dark_current::PerTime
    # "Slit width `[μm]`"
    # slit_width::Unitful.Length
end;

function createGratingNoiseModel(ET, DS, FPA_qe, effTransmission, fnumber, SSI, RN, DC)
    # @info "Creating instrument model"
    GratingNoiseModel(
       uconvert(u"s",ET),
       uconvert(u"μm",DS),
       FPA_qe, effTransmission,
       fnumber,
       uconvert(u"μm",SSI),
       RN,
       uconvert(u"s^-1",DC))
end

function Base.show(io::IO, m::GratingNoiseModel)
    compact = get(io, :compact, false)

    if !compact
        println("Instance of GratingNoiseModel:")
        println("Integration time         = ", m.t_int)
        println("Detector size            = ", m.detector_size)
        println("FPA Quantum efficiency   = ", m.Qₑ)
        println("Optical bench efficiency = ", m.η)
        println("F-number                 = ", m.Fnumber)
        println("Spectral Sampling        = ", m.Δλ)
        println("Readout noise            = ", m.σ_read)
        println("Dark_current             = ", m.dark_current)
    else
        show(io, "Instance of GratingNoiseModel")
    end
end

"""
    type AbstractL1File
Abstract AbstractL1File type
"""
abstract type AbstractL1File end
struct L1_OCO <: AbstractL1File
    geometry::Dict{String, Any}
    ils::Dict{String, Any}
    meteo::Dict{String, Any}
    measurement::Dict{String, Any}
end;

abstract type AbstractMeasurement end

struct MeasurementOCO{FT} <: AbstractMeasurement
    "Wavelength axis"
    SpectralGrid::Array{FT,1}
    "Band ID axis"
    BandID::Tuple
    "Spectral radiances (concatenated)"
    SpectralMeasurement::Array{FT,1}
    "Radiance Unit"
    MeasurementUnit::String
    "Wavelength unit"
    GridUnit::String
    "Latitude [°]"
    latitude::FT
    "Longitude [°]"
    longitude::FT
    "Viewing Zenith Angle [°]"
    vza::FT
    "Solar Zenith Angle [°]"
    sza::FT
    "Relative Azimuth Angle [°]"
    raa::FT
    "Gravity at surface"
    g₀::FT
    "Polarization Angle [°]"
    ϕ::FT
    "Mueller Coefficients"
    mueller::Array{FT,1}
    "Pressure profile half levels"
    p_half::Array{FT,1}
    "Pressure profile half levels"
    p_full::Array{FT,1}
    "Temperature profile full level"
    T::Array{FT,1}
    "Specific humidity full level"
    q::Array{FT,1}
    "Instrument Line Shape"
    ils::Tuple
end;


# The [Neherlab COVID-19](https://neherlab.org/covid19/) forecast model
include("COVID-19-utils.jl")
include("COVID-19-model.jl")
include("COVID-19-run-model.jl")
include("COVID-19-data.jl")

using BlackBoxOptim

# If running for the first time, or no updates for a long time
# updateData()

COUNTRY_LIST = [("France",                      :north),
                ("Germany",                     :north),
                ("Italy",                       :north),
                ("Spain",                       :north),
                ("Switzerland",                 :north),
                ("United Kingdom",              :north),
                ("United States of America",    :north)]

countryData = Dict( c => populateCountryDate(c, h) for (c, h) in COUNTRY_LIST)

#--------------------------------------------------------------------------------------------------
#--
#-- Find optimal parameters for every country
#--
for (c, _) in COUNTRY_LIST
    global country = c

    # Determine optimal parameters - 15 seconds per country
    result = bboptimize(deathsLoss, SearchRange = countryData[country][:range], MaxTime = 15)
    global countryData[country][:params] = best_candidate(result)
end

# Select a country
country = "Spain"

#--------------------------------------------------------------------------------------------------
#--
#-- Default parameters
#--
sol = calculateSolution(country, countryData[country][:params])
calculateTotalDeaths(sol)

#--------------------------------------------------------------------------------------------------
#-- Plot efault parameters
#--
pyplot();
clf();
ioff();

fig, ax = PyPlot.subplots();

ax.plot(countryData[country][:cases].time,
        calculateTotalDeaths(calculateSolution(country, countryData[country][:params])),
        label = "Forecast");

ax.plot(countryData[country][:cases].time, countryData[country][:cases].deaths, "ro", label = "Actual", alpha = 0.3);

ax.legend(loc="lower right");
ax.set_xlabel("time");
ax.set_ylabel("Individuals");
ax.set_yscale("log");

gcf()

#--------------------------------------------------------------------------------------------------
#-- Optimal parameters
#--
result = bboptimize(deathsLoss, SearchRange = countryData[country][:range], MaxTime = 15)
countryData[country][:params] = best_candidate(result)

#-------------------------------------------------------------------------------------------------
#--
#-- Global optimisation across all countries
#--

# One run is optimising the disease, then optimising the countries.
for runs in 1:5

    # First freeze the countries and optimise the epidemiology
    freezeCountryRange()

    # Optimise across all countries
    result = bboptimize(allCountriesLoss, SearchRange = countryData["France"][:range], MaxTime = 30)

    #Update the parameters of each country
    for (c, _) in COUNTRY_LIST
        global countryData[c][:params] = (diseaseMask .* best_candidate(result)) .+ (countryMask .* countryData[c][:params])
    end

    #-- Freeze the parameters related to the disease
    freezeDiseaseRange()

    # Then optimise each country
    for (c, _) in COUNTRY_LIST
        # Make a note of the disease parameters
        p = countryData[c][:params]

        # Determine optimal parameters - 15 seconds per country
        result = bboptimize(deathsLoss, SearchRange = countryData[country][:range], MaxTime = 15)
        global countryData[c][:params] = (diseaseMask .* p) .+ (countryMask .* best_candidate(result))
    end
end

allParams = [countryData[c][:params] for (c, _) in COUNTRY_LIST]

using Printf

# Do what I say, not what I do
Base.show(io::IO, f::Float64) = @printf io "%1.3f" f

@show allParams


export to_long_mi, logsumexp_cuda,
    pop_cuda!, push_cuda!, all_empty, length_cuda,
    generate_all, generate_data_all, kfold

using DataFrames
using CUDA: CUDA

###################
# Misc.
####################

function to_long_mi(m::Matrix{Float64}, min_int, max_int)::Matrix{Int64}
    δmi = maximum(m) - minimum(m)
    δint = max_int - min_int
    return @. round(Int64, m * δint / δmi + min_int)
end

# TODO: get rid of all copies
@inline function logsumexp_cuda(x,y)
    Δ = ifelse(x == y, zero(x), CUDA.abs(x - y))
    max(x, y) + CUDA.log1p(CUDA.exp(-Δ))
end

###################
# Rudimentary CUDA-compatible stack data structure
####################

# sadly making `i` varargs doesn't work; kernel won't compile

function pop_cuda!(stack, i)
    if @inbounds stack[i,1] == zero(eltype(stack))
        return zero(eltype(stack))
    else
        @inbounds stack[i,1] -= one(eltype(stack))
        @inbounds return stack[i,stack[i,1]+2]
    end
end

function pop_cuda!(stack, i, j)
    if @inbounds stack[i,j,1] == zero(eltype(stack))
        return zero(eltype(stack))
    else
        @inbounds stack[i,j,1] -= one(eltype(stack))
        @inbounds return stack[i,j,stack[i,j,1]+2]
    end
end

function push_cuda!(stack, v, i)
    @inbounds stack[i,1] += one(eltype(stack))
    @inbounds CUDA.@cuassert 1+stack[i,1] <= size(stack, ndims(stack)) "CUDA stack overflow"
    @inbounds stack[i,1+stack[i,1]] = v
    return nothing
end

function push_cuda!(stack, v, i, j)
    @inbounds stack[i, j,1] += one(eltype(stack))
    @inbounds CUDA.@cuassert 1+stack[i, j,1] <= size(stack, ndims(stack)) "CUDA stack overflow"
    @inbounds stack[i, j,1+stack[i, j,1]] = v
    return nothing
end

all_empty(stack::AbstractArray{T,2}) where T =
    all(x -> iszero(x), stack[:,1])

all_empty(stack::AbstractArray{T,3}) where T =
    all(x -> iszero(x), stack[:,:,1])

length_cuda(stack, i...) = stack[i...,1]

###################
# One-Hot Encoding
####################

"""
One-hot encode data (2-D Array) based on categories (1-D Array)
Each row of the return value is a concatenation of one-hot encoding of elements of the same row in data
Assumption: both input arrays have elements of same type
"""
function one_hot_encode(X::Array{T, 2}, categories::Array{T,1}) where {T<:Any}
    X_dash = zeros(Bool, size(X)[1], length(categories)*size(X)[2])
    for i = 1:size(X)[1], j = 1:size(X)[2]
            X_dash[i, (j-1)*length(categories) + findfirst(==(X[i,j]), categories)] = 1
    end
    X_dash
end

###################
# Testing Utils
####################

"""
Given some missing values generates all possible fillings
"""
function generate_all(row::Vector)
    miss_count = count(ismissing, row)
    lits = length(row)
    result = Bool.(zeros(1 << miss_count, lits))

    if miss_count == 0
        result[1, :] = copy(row)
    else
        for mask = 0: (1<<miss_count) - 1
            cur = missings(Bool, lits)
            cur .= row
            cur[ismissing.(row)] = transpose(parse.(Bool, split(bitstring(mask)[end-miss_count+1:end], "")))
            result[mask+1,:] = cur
        end
    end
    DataFrame(result, :auto)
end

"""
Generates all possible binary configurations of size N
"""
function generate_data_all(N::Int)
    data_all = transpose(parse.(Bool, split(bitstring(0)[end-N+1:end], "")));
    for mask = 1: (1<<N) - 1
        data_all = vcat(data_all,
            transpose(parse.(Bool, split(bitstring(mask)[end-N+1:end], "")))
        );
    end
    DataFrame(data_all, :auto)
end

#####################
# K-fold partitioning
#####################

"Returns a(n index) partitioning a la k-fold."
function kfold(n::Int, p::Int)::Vector{Tuple{UnitRange, Vector{Int}}}
    F = Vector{Tuple{UnitRange, Vector{Int}}}(undef, p)
    j = s = 1
    k = n÷p
    for i ∈ 1:n%p
        if s > 1
            I = collect(1:s-1)
            if s+k < n append!(I, s+k+1:n) end
        else I = collect(s+k+1:n) end
        F[j] = (s:s+k, I)
        s += k+1
        j += 1
    end
    k = n÷p-1
    for i ∈ 1:p-n%p
        if s > 1
            I = collect(1:s-1)
            if s+k < n append!(I, s+k+1:n) end
        else I = collect(s+k+1:n) end
        F[j] = (s:s+k, I)
        s += k+1
        j += 1
    end
    return F
end


function _add_system_balance_slacks!(
    psi_container::PSIContainer,
    slack_name::String,
    expression::Symbol,
    single_first_axes::Bool = false,
)
    time_steps = model_time_steps(psi_container)
    expression_array = get_expression(psi_container, expression)
    var_name_up = make_variable_name(slack_name, SLACK_UP)
    var_name_dn = make_variable_name(slack_name, SLACK_DN)
    single_first_axes && (first_index = [axes(expression_array)[1][1]])
    !single_first_axes && (first_index = axes(expression_array)[1])
    variable_up = add_var_container!(psi_container, var_name_up, first_index, time_steps)
    variable_dn = add_var_container!(psi_container, var_name_dn, first_index, time_steps)
    for ix in first_index, jx in time_steps
        variable_up[ix, jx] = JuMP.@variable(
            psi_container.JuMPmodel,
            base_name = "$(var_name_up)_{$(ix), $(jx)}",
            lower_bound = 0.0
        )
        variable_dn[ix, jx] = JuMP.@variable(
            psi_container.JuMPmodel,
            base_name = "$(var_name_dn)_{$(ix), $(jx)}",
            lower_bound = 0.0
        )
        add_to_expression!(expression_array, ix, jx, variable_up[ix, jx], 1.0)
        add_to_expression!(expression_array, ix, jx, variable_dn[ix, jx], -1.0)
        JuMP.add_to_expression!(
            psi_container.cost_function,
            (variable_dn[ix, jx] + variable_up[ix, jx]) * BALANCE_SLACK_COST,
        )
    end
    return
end

function add_slacks!(psi_container::PSIContainer, ::Type{CopperPlatePowerModel})
    _add_system_balance_slacks!(psi_container, ACTIVE_POWER, :nodal_balance_active, true)
    return
end

function add_slacks!(
    psi_container::PSIContainer,
    ::Type{T},
) where {T <: PM.AbstractActivePowerModel}
    _add_system_balance_slacks!(psi_container, ACTIVE_POWER, :nodal_balance_active)
    return
end

function add_slacks!(
    psi_container::PSIContainer,
    ::Type{T},
) where {T <: PM.AbstractPowerModel}
    _add_system_balance_slacks!(psi_container, ACTIVE_POWER, :nodal_balance_active)
    _add_system_balance_slacks!(psi_container, REACTIVE_POWER, :nodal_balance_reactive)
    return
end


module OutputALASmod

using OutputLSmod

type OutputALAS

 solved::Int64 #renvoi 0 si réussi
 solve_message::AbstractString #message de sortie
 xtab::Array #tableau des itérés
 dtab::Array #tableau des directions
 rhotab::Array{Float64,1}
 steptab::Array{Float64,1}

 realisabilite::Array{Float64,1}
 dualrealisabilite::Array{Float64,1}
 obj::Array{Float64,1}

 inner_output_linesearch::Any #en fait une liste de OutputLS

end

#Initialisation
function OutputALAS(x0::Vector,dj::Vector,feas::Float64,dualfeas::Float64,rho::Vector,ht::Float64)
 return OutputALAS(0,"Success",x0,dj,[norm(rho,Inf)],[],[feas],[dualfeas],[ht],[])
end

#Mise à jour
function Update(oa::OutputALAS,xk::Vector,rho::Vector,feas::Float64,dualfeas::Float64,dj::Vector,step::Float64,outputls::OutputLSmod.OutputLS,ht::Float64,n::Int64)

 oa.realisabilite=[oa.realisabilite;feas]
 oa.dualrealisabilite=[oa.dualrealisabilite;dualfeas]
 oa.obj=[oa.obj;ht]
 oa.rhotab=[oa.rhotab;norm(rho,Inf)]
 oa.steptab=[oa.steptab;step]
 oa.xtab=[oa.xtab xk]
 oa.dtab=[oa.dtab dj]
 oa.inner_output_linesearch=[oa.inner_output_linesearch;outputls]

Print(oa,n,2,j=length(oa.steptab))

 return oa
end

#Print
function Print(oa::OutputALAS,n::Int64,verbose::Int64;j::Int64=1)
 if verbose>1
  nb_comp=Int64(0.5*(length(oa.xtab)/length(oa.realisabilite)-n))
  while j <= length(oa.steptab)
   #println("k :",j," xjk=",oa.xtab[1:n,j+1]," sg=",oa.xtab[n+1:n+nb_comp,j+1]," sh=",oa.xtab[n+nb_comp+1:n+2*nb_comp,j+1],"\n\|c(xjk)\|=",oa.realisabilite[j+1]," JL(xjk)=",oa.dualrealisabilite[j+1]," rho= ",oa.rhotab[j+1]," \nalpha=",oa.steptab[j]," d=",oa.dtab[1:n+2*nb_comp,j+1])
   if n<=4
    if nb_comp>0
     #print_with_color(:blue, "	k: $j xjk=$(oa.xtab[1:n,j+1]) sg=$(oa.xtab[n+1:n+nb_comp,j+1]) sh=$(oa.xtab[n+nb_comp+1:n+2*nb_comp,j+1]) \n\|c(xjk)\|=$(oa.realisabilite[j+1]) JL(xjk)=$(oa.dualrealisabilite[j+1]) rho=$(oa.rhotab[j+1]) \nalpha=$(oa.steptab[j]) d=$(oa.dtab[1:n+2*nb_comp,j+1]) fpen=$(oa.obj[j]) \n")
     print_with_color(:blue, "	k: $j \|xjk\|=$(norm(oa.xtab[1:n,j+1],Inf)) \|c(xjk)\|=$(oa.realisabilite[j+1]) L'(xjk)=$(oa.dualrealisabilite[j+1]) rho=$(oa.rhotab[j+1]) \nalpha=$(oa.steptab[j]) \|d\|=$(norm(oa.dtab[1:n+2*nb_comp,j+1],Inf)) fpen=$(oa.obj[j]) \n")
    else
     print_with_color(:blue, "	k: $j xjk=$(oa.xtab[1:n,j+1]) \n\|c(xjk)\|=$(oa.realisabilite[j+1]) JL(xjk)=$(oa.dualrealisabilite[j+1]) rho=$(oa.rhotab[j+1]) \nalpha=$(oa.steptab[j]) d=$(oa.dtab[1:n+2*nb_comp,j+1]) fpen=$(oa.obj[j]) \n")
    end
   else
    if nb_comp>0
     #print_with_color(:blue, "	k: $j xjk=[$(oa.xtab[1,j+1])..$(oa.xtab[n,j+1])] sg=[$(oa.xtab[n+1,j+1])..$(oa.xtab[n+nb_comp,j+1])] sh=[$(oa.xtab[n+nb_comp+1,j+1])..$(oa.xtab[n+2*nb_comp,j+1])] \n\|c(xjk)\|=$(oa.realisabilite[j+1]) JL(xjk)=$(oa.dualrealisabilite[j+1]) rho=$(oa.rhotab[j+1]) \nalpha=$(oa.steptab[j]) d=(oa.dtab[1:n+2*nb_comp,j+1]) fpen=$(oa.obj[j]) \n")
     print_with_color(:blue, "	k: $j \|xjk\|=$(norm(oa.xtab[1:n,j+1],Inf)) \|c(xjk)\|=$(oa.realisabilite[j+1]) L'(xjk)=$(oa.dualrealisabilite[j+1]) rho=$(oa.rhotab[j+1]) \nalpha=$(oa.steptab[j]) \|d\|=$(norm(oa.dtab[1:n+2*nb_comp,j+1],Inf)) fpen=$(oa.obj[j]) \n")
    else
     print_with_color(:blue, "	k: $j xjk=[$(oa.xtab[1,j+1])..$(oa.xtab[n,j+1])] \n\|c(xjk)\|=$(oa.realisabilite[j+1]) JL(xjk)=$(oa.dualrealisabilite[j+1]) rho=$(oa.rhotab[j+1]) \nalpha=$(oa.steptab[j]) d=(oa.dtab[1:n+2*nb_comp,j+1]) fpen=$(oa.obj[j]) \n")
    end
   end
   OutputLSmod.Print(oa.inner_output_linesearch[j],n,verbose)
   j+=1
  end
  println("")
 end

 return true
end

#end of module
end


"""
Julia interface to a local verison of the empca
(https://github.com/sbailey/empca) python package
"""
__precompile__()
module EMPCA
    using PyCall
    const empca = PyNULL()
    function __init__()
        pyimport_conda("scipy", "scipy")
        pushfirst!(PyVector(pyimport("sys")."path"), @__DIR__)
        copy!(empca, pyimport("empca"))
        @assert empca != PyNULL()
    end
    export empca
end


module PolicyResponse

using ClimateMARGO.Models
using ClimateMARGO.Diagnostics

export step_forward!, add_emissions_bump!

include("stepping_forward.jl")

end


module SinkTest

using HPAT

@acc hpat function sink(file3)
    y = randn(10)
    DataSink(y,HDF5,"/y", file3)
end

end

SinkTest.sink("y.hdf5")
using MPI
if MPI.Comm_rank(MPI.COMM_WORLD)==0 rm("y.hdf5") end


mutable struct Config

    # phenotype config
    input_nodes::Int64
    output_nodes::Int64
    initial_hidden_nodes::Int64
    max_weight::Float64
    min_weight::Float64
    nn_activation::Symbol
    #weight_stdev::Float64

    # GA config
    pop_size::Int64
    max_fitness_threshold::Float64
    prob_addnode::Float64
    prob_removenode::Float64
    prob_addlayer::Float64
    prob_mutatebias::Float64
    bias_mutation_power::Float64
    prob_mutate_weight::Float64 # dynamic mutation rate (future release)
    weight_mutation_power::Float64
    prob_structural_mutation::Float64
    prob_enable_mutation::Float64
    elitism::Bool

    # genotype compatibility
    compatibility_threshold::Float64
    compatibility_change::Float64
    excess_coeficient::Float64
    disjoint_coeficient::Float64
    weight_coeficient::Float64

    # species
    species_size::Int64
    survival_threshold::Float64 # only the best 20% for each species is allowed to mate
    old_threshold::Int64
    youth_threshold::Int64
    old_penalty::Float64    # always in (0,1)
    youth_boost::Float64    # always in (1,2)
    max_stagnation::Int64
    best_max_stagnation::Int64
    limit_species::Int64

    # evaluation
    dataset_id::Int64
    fitness_function_id::Int64
    normalise_input::Bool
    balance_dataset::Bool

    # checkpoint
    checkpoint_filename::String

    function Config(params::Dict{String,String})

        new(
            # phenotype
            parse(Int64, params["input_nodes"]),
            parse(Int64, params["output_nodes"]),
            parse(Int64, params["initial_hidden_nodes"]),
            parse(Float64, params["max_weight"]),
            parse(Float64, params["min_weight"]),
            Meta.parse(params["nn_activation"]),
            #float(params["weight_stdev"]),

            # GA
            parse(Int64, params["pop_size"]),
            parse(Float64, params["max_fitness_threshold"]),
            parse(Float64, params["prob_addnode"]),
            parse(Float64, params["prob_removenode"]),
            parse(Float64, params["prob_addlayer"]),
            parse(Float64, params["prob_mutatebias"]),
            parse(Float64, params["bias_mutation_power"]),
            parse(Float64, params["prob_mutate_weight"]),
            parse(Float64, params["weight_mutation_power"]),
            parse(Float64, params["prob_structural_mutation"]),
            parse(Float64, params["prob_enable_mutation"]),
            parse(Bool, params["elitism"]),

            # genotype compatibility
            parse(Float64, params["compatibility_threshold"]),
            parse(Float64, params["compatibility_change"]),
            parse(Float64, params["excess_coeficient"]),
            parse(Float64, params["disjoint_coeficient"]),
            parse(Float64, params["weight_coeficient"]),

            # species
            parse(Int64, params["species_size"]),
            parse(Float64, params["survival_threshold"]),
            parse(Int64, params["old_threshold"]),
            parse(Int64, params["youth_threshold"]),
            parse(Float64, params["old_penalty"]),
            parse(Float64, params["youth_boost"]),
            parse(Int64, params["max_stagnation"]),
            parse(Int64, params["best_max_stagnation"]),
            parse(Int64, params["limit_species"]),

            # evaluation
            parse(Int64, params["dataset_id"]),
            parse(Int64, params["fitness_function_id"]),
            parse(Bool, params["normalise_input"]),
            parse(Bool, params["balance_dataset"]),

            # checkpoint
            params["checkpoint_filename"]
         )
    end
end

function loadConfig(file::String)
    str = open(f->read(f, String), file)

    str = replace(str, r"\r(\n)?" => '\n')
    ls = split(str, "\n")

    ls = filter(l->length(l)>0 && l[1] != '#', ls)
    lsMap = map(x->split(x,'='),ls)
    params = Dict{String,String}()
    for i = 1:length(lsMap)
#         println((i,lsMap[i][1],lsMap[i][2]))
        params[rstrip(lsMap[i][1])] = lstrip(lsMap[i][2])
    end

    return params
end


# Thu 08 Nov 2012 05:07:44 PM EST
#
# NeHe Tut 7 - Implement lights and rotate a textured cube
#
# Q - quit
# L - turn lights on/off
# F - change texture filter (linear, nearest, mipmap)
# PageUp/Down - move camera closer/further away from cube
# Left/Right - increase/decrease y-rotation speed
# Up/Down - increase/decrease x-rotation speed

# load necessary GLUT/OpenGL routines

global OpenGLver="1.0"
using OpenGL
using GLUT

### auxiliary functions

function cube(size)  # the cube function now includes surface normal specification for proper lighting
  glBegin(GL_QUADS)
    # Front Face
    glNormal(0.0,0.0,1.0)
    glTexCoord(0.0, 0.0)
    glVertex(-size, -size, size)
    glTexCoord(1.0, 0.0)
    glVertex(size, -size, size)
    glTexCoord(1.0, 1.0)
    glVertex(size, size, size)
    glTexCoord(0.0, 1.0)
    glVertex(-size, size, size)

    # Back Face
    glNormal(0.0,0.0,-1.0)
    glTexCoord(1.0, 0.0)
    glVertex(-size, -size, -size)
    glTexCoord(1.0, 1.0)
    glVertex(-size, size, -size)
    glTexCoord(0.0, 1.0)
    glVertex(size, size, -size)
    glTexCoord(0.0, 0.0)
    glVertex(size, -size, -size)

    # Top Face
    glNormal(0.0,1.0,0.0)
    glTexCoord(0.0, 1.0)
    glVertex(-size, size, -size)
    glTexCoord(0.0, 0.0)
    glVertex(-size, size, size)
    glTexCoord(1.0, 0.0)
    glVertex(size, size, size)
    glTexCoord(1.0, 1.0)
    glVertex(size, size, -size)

    # Bottom Face
    glNormal(0.0,-1.0,0.0)
    glTexCoord(1.0, 1.0)
    glVertex(-size, -size, -size)
    glTexCoord(0.0, 1.0)
    glVertex(size, -size, -size)
    glTexCoord(0.0, 0.0)
    glVertex(size, -size, size)
    glTexCoord(1.0, 0.0)
    glVertex(-size, -size, size)

    # Right Face
    glNormal(1.0,0.0,0.0)
    glTexCoord(1.0, 0.0)
    glVertex(size, -size, -size)
    glTexCoord(1.0, 1.0)
    glVertex(size, size, -size)
    glTexCoord(0.0, 1.0)
    glVertex(size, size, size)
    glTexCoord(0.0, 0.0)
    glVertex(size, -size, size)

    # Left Face
    glNormal(-1.0,0.0,0.0)
    glTexCoord(0.0, 0.0)
    glVertex(-size, -size, -size)
    glTexCoord(1.0, 0.0)
    glVertex(-size, -size, size)
    glTexCoord(1.0, 1.0)
    glVertex(-size, size, size)
    glTexCoord(0.0, 1.0)
    glVertex(-size, size, -size)
  glEnd()
end

### end of auxiliary functions

# initialize variables

global window

global filter        = 3
global light         = true

global xrot          = 0.0
global yrot          = 0.0
global xspeed        = 0.0
global yspeed        = 0.0

global tex           = Array(Uint32,3) # generating 3 textures

global cube_size     = 1.0

global z             = -5.0

width                = 640
height               = 480

global LightAmbient  = [0.5f0, 0.5f0, 0.5f0, 1.0f0]
global LightDiffuse  = [1.0f0, 1.0f0, 1.0f0, 1.0f0]
global LightPosition = [0.0f0, 0.0f0, 2.0f0, 1.0f0]

# load textures from images

function LoadGLTextures()
    global tex

    img, w, h = glimread(expanduser("~/.julia/GLUT/Examples/NeHe/tut7/crate.bmp"))

    glGenTextures(3,tex)
    glBindTexture(GL_TEXTURE_2D,tex[1])
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexImage2D(GL_TEXTURE_2D, 0, 3, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, img)

    glBindTexture(GL_TEXTURE_2D,tex[2])
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexImage2D(GL_TEXTURE_2D, 0, 3, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, img)

    glBindTexture(GL_TEXTURE_2D,tex[3])
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST)
    glTexImage2D(GL_TEXTURE_2D, 0, 3, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, img)

    gluBuild2DMipmaps(GL_TEXTURE_2D, 3, w, h, GL_RGB, GL_UNSIGNED_BYTE, img)
end

# function to init OpenGL context

function initGL(w::Integer,h::Integer)
    global LightAmbient
    global LightDiffuse
    global LightPosition

    glViewport(0,0,w,h)
    LoadGLTextures()

    # enable texture mapping
    glEnable(GL_TEXTURE_2D)

    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClearDepth(1.0)
    glDepthFunc(GL_LESS)
    glEnable(GL_DEPTH_TEST)
    glShadeModel(GL_SMOOTH)

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()

    gluPerspective(45.0,w/h,0.1,100.0)

    glMatrixMode(GL_MODELVIEW)

    # initialize lights
    glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient)
    glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse)
    glLightfv(GL_LIGHT1, GL_POSITION, LightPosition)

    glEnable(GL_LIGHT1)
    glEnable(GL_LIGHTING)
end

# prepare Julia equivalents of C callbacks that are typically used in GLUT code

function ReSizeGLScene(w::Int32,h::Int32)
    if h == 0
        h = 1
    end

    glViewport(0,0,w,h)

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()

    gluPerspective(45.0,w/h,0.1,100.0)

    glMatrixMode(GL_MODELVIEW)

    return nothing
end

_ReSizeGLScene = cfunction(ReSizeGLScene, Void, (Int32, Int32))

function DrawGLScene()
    global xrot
    global yrot
    global tex
    global cube_size
    global xspeed
    global yspeed
    global filter

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glLoadIdentity()

    glTranslate(0.0,0.0,z)

    glRotate(xrot,1.0,0.0,0.0)
    glRotate(yrot,0.0,1.0,0.0)

    glBindTexture(GL_TEXTURE_2D,tex[filter])
    cube(cube_size)

    xrot +=xspeed
    yrot +=yspeed

    glutSwapBuffers()

    return nothing
end

_DrawGLScene = cfunction(DrawGLScene, Void, ())

function keyPressed(the_key::Char,x::Int32,y::Int32)
    global filter
    global light

    if the_key == int('q')
        glutDestroyWindow(window)
    elseif the_key == int('l')
        println("Light was: $light")
        light = (light ? false : true)
        println("Light is now: $light")
        if light
            glEnable(GL_LIGHTING)
        else
            glDisable(GL_LIGHTING)
        end
    elseif the_key == int('f')
        println("Filter was: $filter")
        filter += 1
        if filter > 3
            filter = 1
        end
        println("Filter is now: $filter")
    end

    return nothing # keyPressed returns "void" in C. this is a workaround for Julia's "automatically return the value of the last expression in a function" behavior.
end

_keyPressed = cfunction(keyPressed, Void, (Char, Int32, Int32))

function specialKeyPressed(the_key::Int32,x::Int32,y::Int32)
    global z
    global xspeed
    global yspeed

    if the_key == GLUT_KEY_PAGE_UP
        z -= 0.02
    elseif the_key == GLUT_KEY_PAGE_DOWN
        z += 0.02
    elseif the_key == GLUT_KEY_UP
        xspeed -= 0.01
    elseif the_key == GLUT_KEY_DOWN
        xspeed += 0.01
    elseif the_key == GLUT_KEY_LEFT
        yspeed -= 0.01
    elseif the_key == GLUT_KEY_RIGHT
        yspeed += 0.01
    end

    return nothing # specialKeyPressed returns "void" in C. this is a workaround for Julia's "automatically return the value of the last expression in a function" behavior.
end

_specialKeyPressed = cfunction(specialKeyPressed, Void, (Int32, Int32, Int32))

# run GLUT routines

glutInit()
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH)
glutInitWindowSize(width, height)
glutInitWindowPosition(0, 0)

window = glutCreateWindow("NeHe Tut 7")

glutDisplayFunc(_DrawGLScene)
glutFullScreen()

glutIdleFunc(_DrawGLScene)
glutReshapeFunc(_ReSizeGLScene)
glutKeyboardFunc(_keyPressed)
glutSpecialFunc(_specialKeyPressed)

initGL(width, height)

glutMainLoop()


@testset "Test Parity Buffer Control" begin

    dc = deepcopy(DECODER)
    @suppress begin
        buffer = deepcopy(BUFFER_TEST_TRUE)
        dc = GNSSDecoder.buffer_control(dc, buffer)
    end
    @test dc.data_integrity == true

    dc = deepcopy(DECODER)
    @suppress begin
        dc = deepcopy(DECODER)
        buffer = deepcopy(BUFFER_TEST_TRUE)
        dc.prev_30 = 1
        dc = GNSSDecoder.buffer_control(dc, buffer)
    end
    @test dc.data_integrity == false

    dc = deepcopy(DECODER)
    @suppress begin
        dc = deepcopy(DECODER)
        BUFFER_TEST_FALSE = deepcopy(BUFFER_TEST_TRUE)
        BUFFER_TEST_FALSE[2][3][11] = !BUFFER_TEST_FALSE[2][3][11]
        dc = GNSSDecoder.buffer_control(dc, BUFFER_TEST_FALSE)
    end
    @test dc.data_integrity == false
end


export
    IterativeSampler

"""
    struct IterativeSampler <: IterativeAlgorithm

An iterative algorithm that uses a sampler to accumulate more samples on each refinement.
"""
struct IterativeSampler <: IterativeAlgorithm
    base_algorithm :: InstantAlgorithm
end

function prepare(alg::IterativeSampler, runtime::InstantRuntime,
    evidence::Dict{Symbol, <:Score} = Dict{Symbol, Score}(),
    interventions::Dict{Symbol, <:Dist} = Dict{Symbol, Dist}(),
    placeholder_beliefs = get_placeholder_beliefs(runtime))
    net = get_network(runtime)
    for (n,e) in evidence
        v = get_node(net, n)
        inst = current_instance(runtime, v)
        post_evidence!(runtime, inst, e)
    end
    for (n,i) in interventions
        v = get_node(net, n)
        inst = current_instance(runtime, v)
        post_intervention!(runtime, inst, i)
    end
    for (n,b) in placeholder_beliefs
        p = get_node(net, n)
        inst = current_instance(runtime, p)
        post_belief!(runtime, inst, b)
    end
    set_state!(runtime, :particles, Particles(Sample[], Float64[]))
end

function refine(alg::IterativeSampler, runtime::InstantRuntime)
    current_particles = get_state(runtime, :particles)
    infer(alg.base_algorithm, runtime)
    new_particles = get_state(runtime, :particles)
    all_samples = copy(current_particles.samples)
    append!(all_samples, new_particles.samples)
    all_log_weights = copy(current_particles.log_weights)
    append!(all_log_weights, new_particles.log_weights)
    set_state!(runtime, :particles, Particles(all_samples, all_log_weights))
end

answer(q::Query, alg::IterativeSampler, run::Runtime, inst::VariableInstance) = answer(q, alg.base_algorithm, run, inst)

answer(q::Query, alg::IterativeSampler, run::Runtime, insts::Vector{VariableInstance}) = answer(q, alg.base_algorithm, run, insts)