using SymPy

function binary_code( f::Symbol )
    x = Sym( "x" )
    y = Sym( "y" )
    ux = Sym( "u(x)" )
    uy = Sym( "u(y)" )
    dx = Sym( "d(x)" )
    dy = Sym( "d(y)" )
    ex = eval( quote( $f( x, y ) end ))
    code1 = string( f, "{Ux,Dx,Uy,Dy}( x::Dual{Ux,Dx}, y::Dual{Uy,Dy} ) = Dual",
                   (replace( replace( ex, x, ux ), y, uy ),
                    replace( replace( dx*diff( ex, x )+dy*diff( ex, y ), x, ux), y, uy)))
    code2 = string( f, "{Ux,Dx,Y}( x::Dual{Ux,Dx}, y::Y ) = Dual",
                   (replace( replace( ex, x, ux ), y, uy ),
                    replace( replace( dx*diff( ex, x )+dy*diff( ex, y ), x, ux), y, uy)))
    code3 = string( f, "{X,Uy,Dy}( x::X, y::Dual{Uy,Dy} ) = Dual",
                   (replace( ex, y, uy ),
                    replace( replace( dx*diff( ex, x )+dy*diff( ex, y ), x, ux), y, uy)))

end

function binary_code( f::Symbol )
    x = Sym( "x" )
    y = Sym( "y" )
    ux = Sym( "u(x)" )
    dx = Sym( "d(x)" )
    ex = eval( quote( $f( x, y ) end ))
    string( f, "{Ux,Dx,Y}( x::Dual{Ux,Dx}, y::Y ) = Dual",
           (replace( replace( ex, x, ux ), y, uy ),
            replace( replace( dx*diff( ex, x )+dy*diff( ex, y ), x, ux), y, uy)))
end

for op in (:+, :-, :*, :/)
    binary_code( op )
end

function unary_code( f::Symbol )
    x = Sym( "x" )
    u = Sym( "u(x)" )
    d = Sym( "d(x)" )
    ex = eval( quote $f( x ) end )
    string( f, "{U,D}(x::Dual{U,D}) = Dual",

end

for specfun in (:sin, :cos, :tan, :cot, :sec, :csc)
    @eval unary_code( specfun )
end

for specfun in (:asin :acos, :atan, :acot, :asec, :acsc)
    @eval unary_code( specfun )
end


function jac2cheb(c_jac::AbstractVector{T},α::T,β::T,plan::ChebyshevJacobiPlan{FORWARD,T}) where T<:AbstractFloat
    M,N,nM₀,αN,K = getconstants(plan)
    i₁,i₂,j₁,j₂ = getindices(plan)
    p₁,p₂,rp,c₁,c₂,um,vm,cfs,θ,tempcos,tempsin,tempcosβsinα,tempmindices,cnαβ,cnmαβ = getplan(plan)

    v_cheb = zero(c_jac)

    k=0
    for j = 1:K
        k = j
        copyto!(cnmαβ,cnαβ)

        j₁[k] < nM₀ && (k-=1; break)

        @inbounds for i=i₁[k]:i₁[k+1]-1 v_cheb[i] += backward_recurrence(c_jac,j₂[k],θ[i],tempcos[i],tempsin[i],rp) end
        @inbounds for i=i₂[k+1]+1:i₂[k] v_cheb[i] += backward_recurrence(c_jac,j₂[k],θ[i],tempcos[i],tempsin[i],rp) end

        for m=0:M-1
            # Initialize diagonal N-scaling multiplied by local coefficients and zero out excess
            init_c₁c₂!(c₁,c₂.parent,cnmαβ,c_jac,j₁[k],j₂[k])

            # Apply planned DCT-I and DST-I in-place
            applyTN!(c₁,p₁);applyUN!(c₂,p₂)

            # Compute u_m(θ) and v_m(θ)
            compute_umvm!(um,vm,cfs,α,β,tempcos,tempsin,tempcosβsinα,m,θ,i₁[k+1]:i₂[k+1])

            # Multiply point-wise by u_m(θ) and v_m(θ) for valid indices
            @inbounds for i=i₁[k+1]:i₂[k+1] v_cheb[i] += um[i]*c₁[i] + vm[i]*c₂.parent[i] end

            # Update C_{n,m}^{α,β} by recurrence in m
            @inbounds for i=1:N+1 cnmαβ[i] /= 2(2i+α+β+m) end
        end
    end

    # Finish off recurrence
    @inbounds for i=i₁[k+1]:i₂[k+1] v_cheb[i] += backward_recurrence(c_jac,j₂[k+1],θ[i],tempcos[i],tempsin[i],rp) end

    # perform IDCT-I
    applyTNinv!(v_cheb,p₁)
end


"""
Is there zero intersection between two simplices?
"""
function zerointersection(S1::AbstractArray{Float64, 2}, S2::AbstractArray{Float64, 2})
    if toofaraparattointersect(S1, S2) == true
      return(true)
    end

    # If some simplex is singular, they cannot intersect
    if somesimplexissingular(S1, S2) == true;
      return(true)
    end
end

"""
Checks whether two simplices are too far apart to intersect.
"""
function toofaraparattointersect(S1::AbstractArray{Float64, 2}, S2::AbstractArray{Float64, 2})

      r1 = Geometry.squaredradius(S1)
      r2 = Geometry.squaredradius(S2)
      c1 = Geometry.centroid(S1)
      c2 = Geometry.centroid(S2)

      dist = norm(c1 - c2) - (sqrt(r1) + sqrt(r2))
      if dist >= 0
        return(true)
      else
        return(false)
      end
end

"""
Checks whether at least one of the two simplices defined by the vertices
in  'X' and 'Y' are singular. X and Y are matrices of dimension nx(n+1).
"""
function somesimplexissingular(X::Array{Float64, 2}, Y::Array{Float64, 2})
    n = size(X)[1]
    if (vcat(ones(1, n+1), X) == vcat(ones(1, n + 1), Y))
      print("At least one of the simplices is singular!")
      return true
    else
      return false
    end
end

"""
Make sure simplices are of the same dimension
"""
function validatesimplices(S1::Array{Float64, 2}, S2::Array{Float64, 2})
    assert(size(S1)[1] == size(S1)[1])
end


#
# Manopt.jl – Proximal maps
#
# This file provides several proximal maps on manifolds or on small
# product manifolds, like M^2
#
# ---
# Manopt.jl - R. Bergmann – 2017-07-06

export proxDistance, proxTV, proxParallelTV, proxTV2, proxCollaborativeTV

@doc doc"""
    y = proxDistance(M,λ,f,x [,p=2])

compute the proximal map $\operatorname{prox}_{\lambda\varphi}$ with
parameter λ of $\varphi(x) = \frac{1}{p}d_{\mathcal M}^p(f,x)$.

# Input
* `M` – a [`Manifold`](@ref) $\mathcal M$
* `λ` – the prox parameter
* `f` – an [`MPoint`](@ref) $f\in\mathcal M$ (the data)
* `x` – the argument of the proximal map

# Optional argument
* `p` – (`2`) exponent of the distance.

# Ouput
* `y` – the result of the proximal map of $\varphi$
"""
function proxDistance(M::mT,λ::Number,f::T,x::T,p::Int=2) where {mT <: Manifold, T <: MPoint}
  d = distance(M,f,x)
  if p==2
    t =  λ/(1+λ);
  elseif p==1
    if λ < d
      t = λ/d;
    else
      t = 1.;
    end
  else
      throw(ErrorException(
        "Proximal Map of distance(M,f,x) not implemented for p=$(p) (requires p=1 or 2)"
      ))
  end
  return exp(M,x,log(M,x,f),t);
end
@doc doc"""
    (y1,y2) = proxTV(M,λ,(x1,x2) [,p=1])

Compute the proximal map $\operatorname{prox}_{\lambda\varphi}$ of
$\varphi(x,y) = d_{\mathcal M}^p(x,y)$ with
parameter `λ`.

# Input
* `M` – a [`Manifold`](@ref)
* `λ` – a real value, parameter of the proximal map
* `(x1,x2)` – a tuple of two [`MPoint`](@ref)s,

# Optional
(default is given in brackets)
* `p` – (1) exponent of the distance of the TV term

# Ouput
* `(y1,y2)` – resulting tuple of [`MPoint`](@ref) of the
  $\operatorname{prox}_{\lambda\varphi}($ `(x1,x2)` $)$
"""
function proxTV(M::mT,λ::Number, pointTuple::Tuple{P,P},p::Int=1)::Tuple{P,P} where {mT <: Manifold, P <: MPoint}
  x1 = pointTuple[1];
  x2 = pointTuple[2];
  d = distance(M,x1,x2);
  if p==1
    t = min(0.5, λ/d);
  elseif p==2
    t = λ/(1+2*λ);
  else
    throw(ErrorException(
      "Proximal Map of TV(M,x1,x2,p) not implemented for p=$(p) (requires p=1 or 2)"
    ))
  end
  return (  exp(M, x1, log(M, x1, x2), t), exp(M, x2, log(M, x2, x1), t)  );
end
@doc doc"""
    ξ = proxTV(M,λ,x [,p=1])

compute the proximal maps $\operatorname{prox}_{\lambda\varphi}$ of
all forward differences orrucirng in the power manifold array, i.e.
$\varphi(xi,xj) = d_{\mathcal M}^p(xi,xj)$ with `xi` and `xj` are array
elemets of `x` and `j = i+e_k`, where `e_k` is the $k$th unitvector.
The parameter `λ` is the prox parameter.

# Input
* `M` – a [`Manifold`](@ref)
* `λ` – a real value, parameter of the proximal map
* `x` – a [`PowPoint`](@ref).

# Optional
(default is given in brackets)
* `p` – (1) exponent of the distance of the TV term

# Ouput
* `y` – resulting of [`PowPoint`](@ref) with all mentioned proximal
  points evaluated (in a cylic order).
"""
function proxTV(M::Power, λ::Number, x::PowPoint,p::Int=1)::PowPoint
  R = CartesianIndices(M.powerSize)
  d = length(M.powerSize)
  maxInd = Tuple(last(R))
  y = copy(x)
  for k in 1:d # for all directions
    ek = CartesianIndex(ntuple(i  ->  (i==k) ? 1 : 0, d) ) #k th unit vector
    for l in 0:1
      for i in R # iterate over all pixel
        if (i[k] % 2) == l
          I = [i.I...] # array of index
          J = I .+ 1 .* (1:d .== k) #i + e_k is j
          if all( J .<= maxInd ) # is this neighbor in range?
            j = CartesianIndex(J...) # neigbbor index as Cartesian Index
            (y[i],y[j]) = proxTV( M.manifold,λ,(y[i],y[j]),p) # Compute TV on these
          end
        end
      end # i in R
    end # even odd
  end # directions
  return y
end
@doc doc"""
    ξ = proxParallelTV(M,λ,x [,p=1])

compute the proximal maps $\operatorname{prox}_{\lambda\varphi}$ of
all forward differences orrucirng in the power manifold array, i.e.
$\varphi(xi,xj) = d_{\mathcal M}^p(xi,xj)$ with `xi` and `xj` are array
elemets of `x` and `j = i+e_k`, where `e_k` is the $k$th unitvector.
The parameter `λ` is the prox parameter.

# Input
* `M`     – a [`Power`](@ref) manifold
* `λ`     – a real value, parameter of the proximal map
* `x`     – a [`PowPoint`](@ref).

# Optional
(default is given in brackets)
* `p` – (`1`) exponent of the distance of the TV term

# Ouput
* `y`  – resulting of Array [`PowPoint`](@ref)s with all mentioned proximal
  points evaluated (in a parallel within the arrays elements).

*See also* [`proxTV`](@ref)
"""
function proxParallelTV(M::Power, λ::Number, x::Array{PowPoint{P,N},1}, p::Int=1)::Array{PowPoint{P,N},1} where {P <: MPoint, N}
  R = CartesianIndices(getValue(x[1]))
  d = ndims(getValue(x[1]))
  if length(x) != 2*d
    throw(ErrorException("The number of inputs from the array ($(length(x))) has to be twice the data dimensions ($(d))."))
  end
  maxInd = Tuple(last(R))
  # create an array for even/odd splitted proxes along every dimension
  y = reshape(deepcopy(x),d,2)
  x = reshape(x,d,2)
  for k in 1:d # for all directions
    ek = CartesianIndex(ntuple(i  ->  (i==k) ? 1 : 0, d) ) #k th unit vector
    for l in 0:1 # even odd
      for i in R # iterate over all pixel
        if (i[k] % 2) == l
          I = [i.I...] # array of index
          J = I .+ 1 .* (1:d .== k) #i + e_k is j
          if all( J .<= maxInd ) # is this neighbor in range?
            j = CartesianIndex(J...) # neigbbor index as Cartesian Index
            # parallel means we apply each (direction even/odd) to a seperate copy of the data.
            (y[k,l+1][i],y[k,l+1][j]) = proxTV( M.manifold,λ,(x[k,l+1][i],x[k,l+1][j]),p) # Compute TV on these
          end
        end
      end # i in R
    end # even odd
  end # directions
  return y[:] # return as onedimensional array
end
@doc doc"""
    (y1,y2,y3) = proxTV2(M,λ,(x1,x2,x3),[p=1], kwargs...)

Compute the proximal map $\operatorname{prox}_{\lambda\varphi}$ of
$\varphi(x_1,x_2,x_3) = d_{\mathcal M}^p(c(x_1,x_3),x_2)$ with
parameter `λ`>0, where $c(x,z)$ denotes the mid point of a shortest
geodesic from `x1` to `x3` that is closest to `x2`.

# Input

* `M`          – a manifold
* `λ`          – a real value, parameter of the proximal map
* `(x1,x2,x3)` – a tuple of three [`MPoint`](@ref)s

* `p` – (`1`) exponent of the distance of the TV term

# Optional
`kwargs...` – parameters for the internal [`subGradientMethod`](@ref)
    (if `M` is neither `Euclidean` nor `Circle`, since for these a closed form
    is given)

# Output
* `(y1,y2,y3)` – resulting tuple of [`MPoint`](@ref)s of the proximal map
"""
function proxTV2(M::mT,λ,pointTuple::Tuple{P,P,P},p::Int=1;
  stoppingCriterion::StoppingCriterion = stopAfterIteration(5),
  kwargs...)::Tuple{P,P,P} where {mT <: Manifold, P <: MPoint}
  if p != 1
    throw(ErrorException(
      "Proximal Map of TV2(M,λ,pT,p) not implemented for p=$(p) (requires p=1) on general manifolds."
    ))
  end
  PowX = PowPoint([pointTuple...])
  PowM = Power(M,(3,))
  xInit = PowX
  F(x) = 1/2*distance(PowM,PowX,x)^2 + λ*costTV2(PowM,x)
  ∂F(x) = log(PowM,x,PowX) + λ*gradTV2(PowM,x)
  xR = subGradientMethod(PowM,F,∂F,xInit;stoppingCriterion=stoppingCriterion, kwargs...)
  return (getValue(xR)...,)
end
function proxTV2(M::Circle,λ,pointTuple::Tuple{S1Point,S1Point,S1Point},p::Int=1)::Tuple{S1Point,S1Point,S1Point}
  w = [1., -2. ,1. ]
  x = [getValue.(pointTuple)...]
  if p==1 # Theorem 3.5 in Bergmann, Laus, Steidl, Weinmann, 2014.
    m = min(   λ, abs(  symRem( sum( x .* w  ) )  )/(dot(w,w))   )
    s = sign( symRem(sum(x .* w)) )
    return Tuple( S1Point.( symRem.( x  .-  m .* s .* w ) ) )
  elseif p==2 # Theorem 3.6 ibd.
    t = λ * symRem( sum( x .* w ) ) / (1 + λ*dot(w,w) )
    return Tuple(  S1Point.( symRem.( x - t.*w ) )  )
  else
    throw(ErrorException(
      "Proximal Map of TV2(Circle,λ,pT,p) not implemented for p=$(p) (requires p=1 or 2)"
    ))
  end
end
function proxTV2(M::Euclidean,λ,pointTuple::Tuple{RnPoint,RnPoint,RnPoint},p::Int=1)::Tuple{RnPoint,RnPoint,RnPoint}
  w = [1., -2. ,1. ]
  x = [getValue.(pointTuple)...]
  if p==1 # Example 3.2 in Bergmann, Laus, Steidl, Weinmann, 2014.
    m = min.(Ref(λ),  abs.( x .* w  ) / (dot(w,w))   )
    s = sign.( sum(x .* w) )
    return Tuple( RnPoint.( x  .-  m .* s .* w ) )
  elseif p==2 # Theorem 3.6 ibd.
    t = λ * sum( x .* w ) / (1 + λ*dot(w,w) )
    return Tuple(  RnPoint.( x - t.*w ) )
  else
    throw(ErrorException(
      "Proximal Map of TV2(Euclidean,λ,pT,p) not implemented for p=$(p) (requires p=1 or 2)"
    ))
  end
end
@doc doc"""
    ξ = proxTV2(M,λ,x,[p])

compute the proximal maps $\operatorname{prox}_{\lambda\varphi}$ of
all centered second order differences orrucirng in the power manifold array, i.e.
$\varphi(x_k,x_i,x_j) = d_2(x_k,x_i.x_j)$, where $k,j$ are backward and forward
neighbors (along any dimension in the array of `x`).
The parameter `λ` is the prox parameter.

# Input
* `M` – a [`Manifold`](@ref)
* `λ` – a real value, parameter of the proximal map
* `x` – a [`PowPoint`](@ref).

# Optional
(default is given in brackets)
* `p` – (`1`) exponent of the distance of the TV term

# Ouput
* `y` – resulting of [`PowPoint`](@ref) with all mentioned proximal points
  evaluated (in a cylic order).
"""
function proxTV2(M::Power, λ::Number, x::PowPoint,p::Int=1)::PowPoint
  R = CartesianIndices(M.powerSize)
  d = length(size(x))
  minInd = [first(R).I...]
  maxInd = [last(R).I...]
  y = copy(x)
  for k in 1:d # for all directions
    for l in 0:1
      for i in R # iterate over all pixel
        if (i[k] % 3) == l
          I = [i.I...] # array of index
          JForward = I .+ 1 .* (1:d .== k) #i + e_k
          JBackward = I .+ 1 .* (1:d .== k) # i - e_k
          if all( JForward .<= maxInd ) && all( JBackward .>= minInd)
            jForward = CartesianIndex{d}(JForward...) # neigbbor index as Cartesian Index
            jBackward = CartesianIndex{d}(JForward...) # neigbbor index as Cartesian Index
            (y[jBackward], y[i], y[jForward]) =
              proxTV2( M.manifold, λ, (y[jBackward], y[i], y[jForward]),p) # Compute TV on these
          end
        end # if mod 3
      end # i in R
    end # for mod 3
  end # directions
  return y
end
@doc doc"""
    proxCollaborativeTV(M,λ,x [,p=2,q=1])

compute the prox of the collaborative TV prox for x on the [`Power`](@ref)
manifold, i.e. of the function

```math
F^q(x) = \sum_{i\in\mathcal G}
  \Bigl( \sum_{j\in\mathcal I_i}
    \sum_{k=1^d} \lVert X_{i,j}\rVert_x^p\Bigr)^\frac{q/p},
```

where $\mathcal G$ is the set of indices for $x\in\mathcal M$ and $\mathcal I_i$
is the set of its forward neighbors.
This is adopted from the paper by Duran, Möller, Sbert, Cremers:
_Collaborative Total Variation: A General Framework for Vectorial TV Models_
(arxiv: [1508.01308](https://arxiv.org/abs/1508.01308)), where the most inner
norm is not on a manifold but on a vector space, see their Example 3 for
details.
"""
function proxCollaborativeTV(N::Power,λ::Float64,x::PowPoint,Ξ::PowTVector,p::Float64=2.,q::Float64=1.)
  # Ξ = forwardLogs(M,x)
  if length(size(x)) == 1
    d = 1
    s = 1
    iRep = 1
  else
    d = size(x)[end]
    s = length(size(x))-1
    if s != d
      throw( ErrorException( "the last dimension ($(d)) has to be equal to the number of the previous ones ($(s)) but its not." ))
    end
    iRep = [Integer.(ones(d))...,d]
  end
  if q==1 # Example 3 case 2
    if p==1
      normΞ = norm.(Ref(N.manifold), getValue(x), getValue(Ξ) )
      return PowTVector( max.(normΞ .- λ, 0.) ./ ( (normΞ .== 0) .+ normΞ )  .*  getValue(Ξ) )
    elseif p==2 # Example 3 case 3
      norms = sqrt.( sum( norm.(Ref(N.manifold),getValue(x),getValue(Ξ)).^2, dims=d+1) )
      normΞ = repeat(norms,inner=iRep)
      # if the norm is zero add 1 to avoid division by zero, also then the
      # nominator is already (max(-λ,0) = 0) so it stays zero then
      return PowTVector( max.(normΞ .- λ, 0.) ./ ( (normΞ .== 0) .+ normΞ )  .*  getValue(Ξ) )
    else
      throw( ErrorException("The case p=$p, q=$q is not yet implemented"))
    end
  elseif q==Inf
    if p==2
      norms = sqrt.( sum( norm.(Ref(N.manifold),getValue(x),getValue(Ξ)).^2, dims=d+1) )
      normΞ = repeat(norms,inner=iRep)
    elseif p==1
      norms = sum( norm.(Ref(N.manifold),getValue(x),getValue(Ξ)), dims=d+1)
      normΞ = repeat(norms,inner=iRep)
    elseif p==Inf
      normΞ = norm.(Ref(N.manifold),getValue(x),getValue(Ξ))
    else
      throw( ErrorException("The case p=$p, q=$q is not yet implemented"))
    end
    return PowTVector(
      λ .* getValue(Ξ) ./ max.(Ref(λ), normΞ)
    )
  end # end q
  throw( ErrorException("The case p=$p, q=$q is not yet implemented"))
end
proxCollaborativeTV(N::Power,λ::Float64,x::PowPoint,Ξ::PowTVector,p::Int,q::Float64=1.) = proxCollaborativeTV(N,λ,x,Ξ,Float64(p),q)
proxCollaborativeTV(N::Power,λ::Float64,x::PowPoint,Ξ::PowTVector,p::Float64,q::Int) = proxCollaborativeTV(N,λ,x,Ξ,p,Float64(q))
proxCollaborativeTV(N::Power,λ::Float64,x::PowPoint,Ξ::PowTVector,p::Int,q::Int) = proxCollaborativeTV(N,λ,x,Ξ,Float64(p),Float64(q))


using DiffEqBase, DiffEqBase.InternalEuler

# Try it
ff = ODEFunction(
    (u, p, t) -> u,
    jac=(u, p, t) -> 1.,
    analytic=(u0, p, t) -> u0*exp(t)
)

dt = 0.01
prob = ODEProblem(ff, 1.0, (0.0,1.0) )
sol = solve(prob, InternalEuler.FwdEulerAlg(), tstops=0:dt:1)
sol2 = solve(prob, InternalEuler.BwdEulerAlg(), tstops=0:dt:1)

#using Plots
#plot(sol)
#plot!(sol2)
#plot!(sol2, plot_analytic=true)

#using DiffEqDevTools
#dts = 1./2.^(8:-1:4)
#sim = test_convergence(dts,p2,BwdEulerAlg())
#@show sim.𝒪est[:final]
#plot(sim)


module Day14

using Advent: save_input

function solve_a(filename)
    memory = Dict{Int,UInt}()
    mask0 = nothing
    mask1 = nothing
    for line in eachline(filename)
        left, right = split(line, r" = ")
        if left == "mask"
            mask0 = reduce((x, y) -> (x << 1) | (y != '0'), right, init = zero(UInt64))
            mask1 = reduce((x, y) -> (x << 1) | (y == '1'), right, init = zero(UInt64))
        else
            address = parse(Int, left[5:end - 1])
            value = parse(UInt, right)
            memory[address] = value & mask0 | mask1
        end
    end
    memory |> values |> sum |> Int
end

function solve_b(filename)
    memory = Dict{Int,UInt}()
    mask1 = nothing
    maskf = nothing
    for line in eachline(filename)
        left, right = split(line, r" = ")
        if left == "mask"
            maskf = reduce((x, y) -> (x << 1) | (y == 'X'), right, init = zero(UInt64))
            mask1 = reduce((x, y) -> (x << 1) | (y == '1'), right, init = zero(UInt64))
        else
            address = parse(Int, left[5:end - 1]) & ~maskf | mask1
            value = parse(UInt, right)
            st = maskf
            while st != 0
                # cycle through bits in the mask
                memory[address | st] = value
                st = (st - 1) & maskf
            end
        end
    end
    memory |> values |> sum
end

solve(filename = save_input(2020, 14)) = solve_a(filename), solve_b(filename)
solve_big() = solve("day14_big.in")

end


"""
    Exo3Real{T <: Real} = Parabolic{Exo2Real{T}}

A 3-exo-real number is a parabolic Cayley-Dickson construct with bi-exoplex numbers.
"""
const Exo3Real{T <: Real} = Parabolic{Exo2Real{T}}

function Exo3Real(z::Exo2Real{T}) where T <: Real
    Exo3Real{T}(z, zero(Exo2Real{T}))
end

function Exo3Real(z::Exo1Real{T}) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(z, zero(Exo1Real{T})),
        zero(Exo2Real{T})
    )
end

function Exo3Real(a::T, b::T, c::T, d::T, f::T, g::T, h::T, j::T) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(
            Exo1Real{T}(a, b),
            Exo1Real{T}(c, d)
        ),
        Exo2Real{T}(
            Exo1Real{T}(f, g),
            Exo1Real{T}(h, j)
        )
    )
end

function Exo3Real(a::T, b::T, c::T, d::T, f::T, g::T, h::T) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(
            Exo1Real{T}(a, b),
            Exo1Real{T}(c, d)
        ),
        Exo2Real{T}(
            Exo1Real{T}(f, g),
            Exo1Real{T}(h, zero(T))
        )
    )
end

function Exo3Real(a::T, b::T, c::T, d::T, f::T, g::T) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(
            Exo1Real{T}(a, b),
            Exo1Real{T}(c, d)
        ),
        Exo2Real{T}(
            Exo1Real{T}(f, g),
            zero(Exo1Real{T})
        )
    )
end

function Exo3Real(a::T, b::T, c::T, d::T, f::T) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(
            Exo1Real{T}(a, b),
            Exo1Real{T}(c, d)
        ),
        Exo2Real{T}(
            Exo1Real{T}(f, zero(T)),
            zero(Exo1Real{T})
        )
    )
end

function Exo3Real(a::T, b::T, c::T, d::T) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(
            Exo1Real{T}(a, b),
            Exo1Real{T}(c, d)
        ),
        zero(Exo2Real{T})
    )
end

function Exo3Real(a::T, b::T, c::T) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(
            Exo1Real{T}(a, b),
            Exo1Real{T}(c, zero(T))
        ),
        zero(Exo2Real{T})
    )
end

function Exo3Real(a::T, b::T) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(
            Exo1Real{T}(a, b),
            zero(Exo1Real{T})
        ),
        zero(Exo2Real{T})
    )
end

function Exo3Real(a::T) where T <: Real
    Exo3Real{T}(
        Exo2Real{T}(
            Exo1Real{T}(a, zero(T)),
            zero(Exo1Real{T})
        ),
        zero(Exo2Real{T})
    )
end

function Exo3Real(a::Real, b::Real)
    Exo3Real(promote(a, b)...)
end

function Exo3Real(a::Real, b::Real, c::Real)
    Exo3Real(promote(a, b, c)...)
end

function Exo3Real(a::Real, b::Real, c::Real, d::Real)
    Exo3Real(promote(a, b, c, d)...)
end

function Exo3Real(a::Real, b::Real, c::Real, d::Real, f::Real)
    Exo3Real(promote(a, b, c, d, f)...)
end

function Exo3Real(a::Real, b::Real, c::Real, d::Real, f::Real, g::Real)
    Exo3Real(promote(a, b, c, d, f, g)...)
end

function Exo3Real(a::Real, b::Real, c::Real, d::Real, f::Real, g::Real, h::Real)
    Exo3Real(promote(a, b, c, d, f, g, h)...)
end

function Exo3Real(a::Real, b::Real, c::Real, d::Real, f::Real, g::Real, h::Real, j::Real)
    Exo3Real(promote(a, b, c, d, f, g, h, j)...)
end

function selfstar(z::Exo3Real)
    (z + star(z)) / 2
end

function anti_selfstar(z::Exo3Real)
    (z - star(z)) / 2
end

"""
    unreal(z::Exo3Real)

The unreal part of a 3-exo-real number is a 7-vector.
"""
function unreal(z::Exo3Real)
    vcat(unreal(z.l), array(z.r))
end

function show(io::IO, z::Exo3Real)
    print(io, "[1: ")
    print(io, z.l.l.l)
    print(io, ", a: ")
    print(io, z.l.l.r)
    print(io, ", b: ")
    print(io, z.l.r.l)
    print(io, ", ab: ")
    print(io, z.l.r.r)
    print(io, ", c: ")
    print(io, z.r.l.l)
    print(io, ", ac: ")
    print(io, z.r.l.r)
    print(io, ", bc: ")
    print(io, z.r.r.l)
    print(io, ", (ab)c: ")
    print(io, z.r.r.r)
    print(io, "]")
end

function random(::Type{Exo3Real{T}}) where T <: Real
    Exo3Real{T}(random(Exo2Real{T}), random(Exo2Real{T}))
end


export iauS2pv
"""
Convert position/velocity from spherical to Cartesian coordinates.

This function is part of the International Astronomical Union's
SOFA (Standards Of Fundamental Astronomy) software collection.

Status:  vector/matrix support function.

Given:
   theta    double          longitude angle (radians)
   phi      double          latitude angle (radians)
   r        double          radial distance
   td       double          rate of change of theta
   pd       double          rate of change of phi
   rd       double          rate of change of r

Returned:
   pv       double[2][3]    pv-vector

This revision:  2013 June 18

SOFA release 2018-01-30

Copyright (C) 2018 IAU SOFA Board.  See notes at end.
"""

# void iauS2pv(double theta, double phi, double r,
#              double td, double pd, double rd,
#              double pv[2][3])

function iauS2pv(theta::Real, phi::Real, r::Real,
                td::Real, pd::Real, rd::Real)
   pv = zeros(Float64, 3, 2)

   ccall((:iauS2pv, libsofa_c), Cvoid,
       (Cdouble, Cdouble, Cdouble,
       Cdouble, Cdouble, Cdouble,
       Ptr{Cdouble}),
       convert(Float64, theta),
       convert(Float64, phi),
       convert(Float64, r),
       convert(Float64, td),
       convert(Float64, pd),
       convert(Float64, rd),
       pv)

   return SMatrix{2,3}(pv')
end


geps = 1e-5

"""
Function to define inf. velocity of a horseshoe vortex

* `a`: first kin (relative to collocation point: `colpt .- kin`)
* `b`: second kin (relative to collocation point: `colpt .- kin`)
* `x`: wind direction (unit vector for adimensionalized results, equal to vectorial
freestream velocity if dimensional results are desired)

* return: the influence velocity over the collocation point
"""
function hshoe_vinf(a::AbstractVector, b::AbstractVector, x::AbstractVector)
    na=norm(a)
    nb=norm(b)
    ab=dot(a, b)

    d1=(na*nb+ab)
    if d1>-geps && d1<=0.0
        d1=-geps
    elseif d1<geps && d1>0.0
        d1=geps
    end

    d2=(na-dot(a, x))
    if d2>-geps && d2<=0.0
        d2=-geps
    elseif d2<geps && d2>0.0
        d2=geps
    end

    d3=(nb-dot(b, x))
    if d3>-geps && d3<=0.0
        d3=-geps
    elseif d3<geps && d3>0.0
        d3=geps
    end

    vbound = (cross(a, b)/d1)*(1.0/na+1.0/nb)
    vinf = cross(a, x)/(na*d2).-cross(b, x)/(nb*d3)

    vinf./=(4*pi)
    vbound./=(4*pi)

    return vinf, vbound
end

"""
Function to define inf. velocity of a finite vortex line segment

* `a`: first kin (relative to collocation point: `colpt .- kin`)
* `b`: second kin (relative to collocation point: `colpt .- kin`)

* return: the influence velocity over the collocation point
"""
function vline_segment_vinf(a::AbstractVector, b::AbstractVector)
    na=norm(a)
    nb=norm(b)
    ab=dot(a, b)

    d1=(na*nb+ab)
    if d1>-geps && d1<=0.0
        d1=-geps
    elseif d1<geps && d1>0.0
        d1=geps
    end

    vinf=(cross(a, b)/d1)*(1.0/na+1.0/nb)

    vinf/=(4*pi)

    return vinf
end

"""
Build linear aerodynamic problem for an aircraft
"""
function linear_problem(acft, kins, cols, norms, us, xhat, PGβ)

    b = [
        begin
            nu = norm(u)

            (
                - nu * strip.afl.CL0 - u ⋅ n * strip.afl.CLα
            )
        end * PGβ for (i, (u, n, strip)) in enumerate(zip(us, norms, acft.wing_strips))
    ]

    Fg = eltype(b)

    As = [
        Matrix{Fg}(undef, length(b), length(b)) for i = 1:3
    ]

    A = [
        begin
            vinfl, vbound = hshoe_vinf(cols[i] .- kins[j][1], cols[i] .- kins[j][2], xhat)

            for (id, A) in enumerate(As)
                A[i, j] = vinfl[id] + vbound[id]
            end

            C = acft.wing_strips[i].afl.CLα * PGβ * (
                (vinfl .+ vbound) ⋅
                norms[i]
            )

            if i == j
                C -= 2.0 / acft.wing_strips[i].c
            end

            C
        end for i = 1:length(acft.wing_strips), j = 1:length(acft.wing_strips)
    ]

    A, b, As

end

"""
Add downwash to velocities at collocation points
"""
function add_downwash!(us, Γs, As)

    dVs = [
        A * Γs for A in As
    ]

    for (id, dV) in enumerate(dVs)
        for (i, u) in enumerate(us)
            u[id] += dV[i]
        end
    end

end


# Comparison

function (==)(a::MultiValue,b::MultiValue)
  a.array == b.array
end

function (≈)(a::MultiValue,b::MultiValue)
  a.array ≈ b.array
end

function (≈)(
  a::AbstractArray{<:MultiValue}, b::AbstractArray{<:MultiValue})
  if size(a) != size(b); return false; end
  for (ai,bi) in zip(a,b)
    if !(ai≈bi); return false; end
  end
  true
end

# Addition / subtraction

for op in (:+,:-)
  @eval begin

    function ($op)(a::MultiValue{S}) where S
      r = $op(a.array)
      MultiValue(r)
    end

    function ($op)(a::MultiValue{S},b::MultiValue{S}) where S
      r = $op(a.array, b.array)
      MultiValue(r)
    end

  end
end

# Matrix Division

function (\)(a::TensorValue, b::MultiValue)
  r = a.array \ b.array
  MultiValue(r)
end

# Operations with other numbers

for op in (:+,:-,:*)
  @eval begin
    ($op)(a::MultiValue,b::Number) = MultiValue($op(a.array,b))
    ($op)(a::Number,b::MultiValue) = MultiValue($op(a,b.array))
  end
end

(/)(a::MultiValue,b::Number) = MultiValue(a.array/b)

# Dot product (simple contraction)

(*)(a::VectorValue{D}, b::VectorValue{D}) where D = inner(a,b)

function (*)(a::MultiValue,b::MultiValue)
  r = a.array * b.array
  MultiValue(r)
end

@inline dot(u::VectorValue,v::VectorValue) = inner(u,v)

# Inner product (full contraction)

inner(a::Real,b::Real) = a*b

"""
"""
@generated function inner(a::MultiValue{S,T,N,L}, b::MultiValue{S,W,N,L}) where {S,T,N,L,W}
  str = join([" a.array.data[$i]*b.array.data[$i] +" for i in 1:L ])
  Meta.parse(str[1:(end-1)])
end

# Reductions

for op in (:sum,:maximum,:minimum)
  @eval begin
    $op(a::MultiValue) = $op(a.array)
  end
end

# Outer product (aka dyadic product)

"""
"""
outer(a::Real,b::Real) = a*b

outer(a::MultiValue,b::Real) = a*b

outer(a::Real,b::MultiValue) = a*b

@generated function outer(a::VectorValue{D},b::VectorValue{Z}) where {D,Z}
  str = join(["a.array[$i]*b.array[$j], " for j in 1:Z for i in 1:D])
  Meta.parse("MultiValue(SMatrix{$D,$Z}($str))")
end

@generated function outer(a::VectorValue{D},b::MultiValue{Tuple{A,B}}) where {D,A,B}
  str = join(["a.array[$i]*b.array[$j,$k], "  for k in 1:B for j in 1:A for i in 1:D])
  Meta.parse("MultiValue(SArray{Tuple{$D,$A,$B}}($str))")
end

# Linear Algebra

det(a::TensorValue) = det(a.array)

inv(a::TensorValue) = MultiValue(inv(a.array))

# Measure

"""
"""
meas(a::VectorValue) = sqrt(inner(a,a))

meas(a::TensorValue) = abs(det(a))

@inline norm(u::VectorValue) = sqrt(inner(u,u))

@inline norm(u::VectorValue{0,T}) where T = sqrt(zero(T))

# conj

conj(a::MultiValue) = MultiValue(conj(a.array))

# Trace

@generated function tr(v::TensorValue{D}) where D
  str = join([" v.array.data[$i+$((i-1)*D)] +" for i in 1:D ])
  Meta.parse(str[1:(end-1)])
end

@generated function tr(v::MultiValue{Tuple{A,A,B}}) where {A,B}
  str = ""
  for k in 1:B
    for i in 1:A
      if i !=1
        str *= " + "
      end
      str *= " v.array[$i,$i,$k]"
    end
    str *= ", "
  end
  Meta.parse("VectorValue($str)")
end

# Adjoint and transpose

function adjoint(v::TensorValue)
  t = adjoint(v.array)
  TensorValue(t)
end

function transpose(v::TensorValue)
  t = transpose(v.array)
  TensorValue(t)
end

# Symmetric part

"""
"""
@generated function symmetic_part(v::TensorValue{D}) where D
  str = "("
  for j in 1:D
    for i in 1:D
      str *= "0.5*v.array.data[$i+$((j-1)*D)] + 0.5*v.array.data[$j+$((i-1)*D)], "
    end
  end
  str *= ")"
  Meta.parse("TensorValue($str)")
end


using PortHamiltonian
using PyPlot
using LinearAlgebra
using ForwardDiff

include("dataexperiment.jl")

# load structural parameters
pslosh, pbeam, prigid = dataexperiment(0.4);

L = pslosh["a"];
N = 20;
disc = discrete_phs_closed(N, 0, L)

Br = [Matrix(1I, N, N); zeros(N,N);zeros(1,N)];
B = blkdiag(zeros(2*N,1),[1]);
D = blkdiag(disc.D,[0]);
Jrb = [0];
invM = disc.Q[N+1:end,N+1:end]
R = 000000.00*Br*norm(invM)*Br';
J = blkdiag(disc.J,Jrb)-R;

# defining Q for saint-venant equation (no rotation)
rho = pslosh["rho"];
g = pslosh["g"];
b = pslosh["b"];
hbar = pslosh["h"];
xi,w = PortHamiltonian.lgwt(N,0,L);
Q = blkdiag(disc.Q,[0])
pfluid = PortHamiltonian.Phs(J, B, D, Q);
#PortHamiltonian.set_constraint!(pfluid, B[:,1],[0])

function Hamiltonian(X)
	alpha1 = X[1:N]
	alpha2 = X[N+1:2*N]
	p = X[end]
	M = w' * (alpha1 .* alpha2)
	H = (w'*((alpha1 .* (alpha2.^2))/rho .+ rho*g*((alpha1).^2)/b)/2 .+ ((p.-M).^2)/(2*prigid["mass"]))[1]
	return H
end

hess = x->ForwardDiff.hessian(Hamiltonian, x)
xeq = [ones(N,1)*b*hbar;zeros(N,1);0.001*prigid["mass"]*0][:]
Qlin = hess(xeq[:])
pfluid.Q = Qlin
pfluid.Hamiltonian = x-> Hamiltonian(x)
pfluid.GradHam = x-> ForwardDiff.gradient(Hamiltonian, x)
pfluid.hessian = x-> hess(x)

#pfluid.GradHam = x -> Q*x
function dynamics(t, X, Xd)
	Xd[1:end]=  pfluid.J * pfluid.GradHam(X[1:end]) + 0.2*pfluid.B[:,2] * sin(2*pi*0.5*t)
	#r[end:end] = pfluid.G' * pfluid.hessian(X[1:end-1]) * (pfluid.J * pfluid.GradHam(X[1:end-1]) + pfluid.B[:,3] * sin(2*pi*t) *0.5+ pfluid.G * X[end])
end

#
 using Sundials
#
 t = range(0,stop = 8,length = 1000)
 yout = Sundials.cvode(dynamics,xeq, collect(t))
 #yout, ypout = Sundials.idasol(dynamics2, [xeq;l0;0], [xeq;0;0]*0, [t])
 pfluid.GradHam = x-> Qlin*x
 youtlin = Sundials.cvode(dynamics,xeq, collect(t))
 #youtlin, ypoutlin = Sundials.idasol(dynamics2, [xeq;l0; 0], [xeq;0; 0]*0, [t])
 #pn = PortHamiltonian.constraint_elimination(pfluid)
 using PyPlot

 figure(1);
 surf(xi,t,yout[:,N+1:end-1])
 #surf(xi,t,yout[:,1:N])
 #include("anima.jl")


function molar_to_weight(value,x,MW)
    return value/LinearAlgebra.dot(MW,x)*1000.0
end

function weight_to_molar(value,x,MW)
    return (value*LinearAlgebra.dot(MW,x))/1000.0
end

#transformation of arbitrary molar units to the SI units
@inline function _transform_v(v,mw,x)
    return 1.0*v
end

@inline function _transform_v(v::Unitful.Quantity{<:Real, Unitful.𝐋^3*Unitful.𝐍^-1},mw,x)
    return Unitful.ustrip(u"m^3/mol"(v))
end

@inline function _transform_v(v::Unitful.Quantity{<:Real, Unitful.𝐋^-3*Unitful.𝐍^1},mw,x)
    return 1/Unitful.ustrip(u"mol/m^3"(v))
end

@inline function _transform_v(v::Unitful.Quantity{<:Real, Unitful.𝐋^-3*Unitful.𝐌^1},mw,x)
    return weight_to_molar(1/Unitful.ustrip(u"kg/m^3"(v)),mw,x)
end

@inline function _transform_v(v::Unitful.Quantity{<:Real, Unitful.𝐋^3*Unitful.𝐌^-1},mw,x)
    return weight_to_molar(Unitful.ustrip(u"m^3/kg"(v)),mw,x)
end

@inline function _transform_T(T)
    return 1.0*T
end

@inline function _transform_T(T::TT) where TT <: Unitful.Temperature
        return Unitful.ustrip(u"K"(T))
end

function _transformVT(V,T,mw,x)
    return (_transform_v(V,mw,x),_transform_T(T))
end

@inline function _transform_P(P)
    return 1.0*P
end

@inline function _transform_P(P::TT) where TT <: Unitful.Pressure
        return Unitful.ustrip(u"Pa"(P))
end

###
### fraction utilities
###
function normalizefrac!(x)
    summ = sum(x)
    for i = 1:length(x)
        x[i] /=summ
    end
    x
end

function normalizefrac(x)
    summ = sum(x)
    y = copy(x)
    for i = 1:length(x)
        y[i] /=summ
    end
    return y
end
function randfrac(N::Integer)
    x = rand(N)
    return x ./sum(x)
end
function randfrac(x0::Array)
    length(x0)>0 && begin
    x = similar(x0)
    zerox = 0.0
    for i = 1:length(x0)
     x[i]=ifelse(x0[i]==zerox,zerox,rand())
    end
    x /= sum(x)
    return x
end
end

function randpurefrac(x0::Array)
    x = zeros(length(x0))
    i = rand(LinearIndices(findall(x->x!=0.0,x0)))
    x[i] = 1.0
    return x
end

function solve_cubic_eq(coeff1::T,coeff2::T,coeff3::T,coeff4::T) where T<:Real
    # Cubic equation solver for complex polynomial (degree=3)
    # http://en.wikipedia.org/wiki/Cubic_function   Lagrange's method

    a1  =  1 / coeff4
    E1  = -coeff3*a1
    E2  =  coeff2*a1
    E3  = -coeff1*a1
    s0  =  E1
    E12 =  E1*E1
    A   =  2*E1*E12 - 9*E1*E2 + 27*E3 # = s1^3 + s2^3
    B   =  E12 - 3*E2                 # = s1 s2
    # quadratic equation: z^2 - Az + B^3=0  where roots are equal to s1^3 and s2^3
    Δ = sqrt(A*A - 4*B*B*B)
    if real(conj(A)*Δ)>=0 # scalar product to decide the sign yielding bigger magnitude
        s1 = exp(log(0.5 * (A + Δ)) * third)
    else
        s1 = exp(log(0.5 * (A - Δ)) * third)
    end
    if s1 == 0
        s2 = s1
    else
        s2 = B / s1
    end
    zeta1 = complex(-0.5, sqrt(T(3.0))*0.5)
    zeta2 = conj(zeta1)
    return third*(s0 + s1 + s2), third*(s0 + s1*zeta2 + s2*zeta1), third*(s0 + s1*zeta1 + s2*zeta2)
end


"""
# module ShootingMethod

# Examples

```jldoctest
julia>
```
"""
module ShootingMethod

using Roots: find_zero, AbstractUnivariateZeroMethod

using NumericalMethodsInQuantumMechanics.EigenvalueProblems:
    InitialCondition, BoundaryCondition
using NumericalMethodsInQuantumMechanics.EigenvalueProblems.NumerovMethod: integrate

export shoot

# last means ``y(t1)``, `ic` contains a guess, `bc[2]` is ``y1``.
# `last(integrate(ic_guess, r, g, s)) - bc[2]` means ``y(t1; yd0) - y1``.
function shoot(g, s, bc::BoundaryCondition, init, args...)
    f = guess -> last(integrate(g, s, InitialCondition(bc.y0, guess), args...)) - bc.y1
    # `x` of the root finding is `y′0`.
    return find_zero(f, init)
end
shoot(g::AbstractVector, bc::BoundaryCondition, init, args...) =
    shoot(g, zeros(length(g)), bc, init, args...)
shoot(g::Function, bc::BoundaryCondition, init, args...) = shoot(g, zero, bc, init, args...)

end


########## Routines related to setting up interpolators.

##### Routines related to Barycentric interpolation.

# Barycentric Interpolations
function Barycentric𝓵(x::Real, 𝓧)
    return prod( x-𝓧[i] for i = 1:length(𝓧))
end

# This is l' in thesis (see Eq. 3.15).
function Barycentric𝓵prime(k::Int, 𝓧)
    return prod( 𝓧[k]-𝓧[i] for i = 1:length(𝓧) if i != k)
end

function get𝑤(k::Int, 𝓧::Vector{T})::T where T
    return one(T)/Barycentric𝓵prime(k, 𝓧)
end

function get𝑤(𝓧::Vector{T})::Vector{T} where T
    return collect( get𝑤(k,𝓧) for k = 1:length(𝓧) )
end

function get𝑤scaled!(w::Vector{T}, 𝓧::Vector{T})::Nothing where T
    L = length(𝓧) - 2
    factor::T = L*log(one(T)+one(T)) # C is 0.5 for the interval [-1,1]

    resize!(w,length(𝓧))
    for k = 0:length(𝓧)-1

        # convert to Int to speed up the multiplication used in prod().
        numerator = prod( convert(Int,sign(𝓧[k+1]-𝓧[i])) for i = 1:length(𝓧) if i != k+1)

        # log-sum-exp trick for adding ill-conditioned numbers.
        denominator = exp( factor + sum( log(abs(𝓧[k+1]-𝓧[i])) for i = 1:length(𝓧) if i != k+1 ) )

        w[k+1] = numerator/denominator
        if !isfinite(w[k+1])
            println("𝓧 is ", 𝓧)
            println("k is ", k)
            println("𝓧[k] is ", 𝓧[k])
            println("numerator is ", numerator)
            println("denominator is ", denominator)
        end
        @assert isfinite(w[k+1])
    end

    return nothing
end

function get𝑤scaled(𝓧::Vector{T})::Vector{T} where T

    w = Vector{T}(undef,length(𝓧))
    get𝑤scaled!(w,𝓧)

    return w
end

# uses SharedArray{Float64} for internal computation.
function get𝑤scaledfast(𝓧::Vector{T})::Vector{T} where T

    L = length(𝓧) - 2
    factor::T = L*log(one(T)+one(T)) # C is 0.5 for the interval [-1,1]

    w = SharedArray{Float64}(length(𝓧))
    @sync @distributed for k = 0:length(𝓧)-1

        numerator::T = prod( sign(𝓧[k+1]-𝓧[i]) for i = 1:length(𝓧) if i != k+1)
        denominator::T = exp( factor + sum( log(abs(𝓧[k+1]-𝓧[i])) for i = 1:length(𝓧) if i != k+1 ) )

        w[k+1] = convert(Float64,numerator/denominator)
    end

    # sanity check.
    for k = 0:length(𝓧)-1
        if !isfinite(w[k+1])
            println("𝓧 is ", 𝓧)
            println("k is ", k)
            println("𝓧[k] is ", 𝓧[k])
            println("numerator is ", numerator)
            println("denominator is ", denominator)
        end
        @assert isfinite(w[k+1])
    end

    return convert(Vector{T},w)
end

##### Routines related to Chebyshev interpolation at nodes.

function getChebyshevinterpolator(  f::Function,
                                    L::Int,
                                    νf::Vector{T},
                                    ν::Vector{T})::Tuple{Vector{T},Vector{T}} where T
    Lp1 = L + 1

    a = Vector{BigFloat}(undef,Lp1)
    for k = 0:L

        term1 = 1/L * f(νf[1]) * Chebyshev1st(ν[1],k)

        term2 = BigFloat("0")
        for l = 1:L-1
            term2 += f(νf[l+1]) * Chebyshev1st(ν[l+1],k)
        end
        term2 *= 2/L

        term3 = 1/L * f(νf[Lp1]) * Chebyshev1st(ν[Lp1],k)

        a[k+1] = term1 + term2 + term3
    end
    a[1] = a[1]/BigFloat(2)

    return a, ν
end

# efficient version. Less accurate.
function getChebyshevinterpolatorfast(  f::Function,
                                        L::Int,
                                        νf::SharedArray{Float64,1},
                                        ν::SharedArray{Float64,1})::Tuple{SharedArray{Float64,1},SharedArray{Float64,1}}
    Lp1 = L + 1

    ### pre-compute.

    # f(νf), length νf.
    f_νf = SharedArray{Float64}(length(νf))
    @sync @distributed for i = 1:length(f_νf)
        f_νf[i] = convert(Float64, f( convert(BigFloat,νf[i]) ))
    end

    ###

    a = SharedArray{Float64}(Lp1)
    for k = 0:L

        term1 = 1/L * f_νf[1] * Chebyshev1st(ν[1],k)

        term2 = zero(Float64)
        for l = 1:L-1
            term2 += f_νf[l+1] * Chebyshev1st(ν[l+1],k)
        end
        term2 *= 2/L

        term3 = 1/L * f_νf[Lp1] * Chebyshev1st(ν[Lp1],k)

        a[k+1] = term1 + term2 + term3
    end
    a[1] = a[1]/2

    return a, ν
end

# Barycentric interpolation coefficients to Chebyshev interpolation coefficients.
# Used in the minimax solution's conversion to impulse response in packagefiltersolution().
function BarycentrictoChebyshev(𝑋, wfunc, dfunc, type_num)
    𝑤 = get𝑤scaled(𝑋)
    h = geth(𝑤, wfunc, dfunc, 𝑋)
    𝑓 = collect( dfunc(𝑋[i]) - (-1)^(i-1)*h/wfunc(𝑋[i]) for i = 1:length(𝑋))

    f_itp = xx->getp(xx, 𝑓, 𝑤, h, wfunc, dfunc, 𝑋)
    if type_num == 2
        f_itp = xx->getp(xx, 𝑓, 𝑤, h, wfunc, dfunc, 𝑋)/type2weightfunc(xx)

    elseif type_num == 3
        f_itp = xx->getp(xx, 𝑓, 𝑤, h, wfunc, dfunc, 𝑋)/type3weightfunc(xx)

    elseif type_num == 4
        f_itp = xx->getp(xx, 𝑓, 𝑤, h, wfunc, dfunc, 𝑋)/type4weightfunc(xx)

    end

    # get Chebyshev coefficients.
    c, ν = getChebyshevinterpolator(f_itp,length(𝑋)-1)

    # sanity check.
    f_itp_Cheby = xx->evalunivariateChebyshevpolynomial(c,xx)

    xq = convert(Vector{BigFloat}, collect(LinRange(-1,1,200)) )
    yq = f_itp.(xq)
    yq2 = f_itp_Cheby.(xq)
    discrepancy = LinearAlgebra.norm(yq-yq2)
    return c, ν, discrepancy, xq, yq, yq2
end

# does not use BigFloats.
function BarycentrictoChebyshevfast(𝑋, wfunc, dfunc, type_num)

    𝑤 = get𝑤scaled(𝑋)
    h = geth(𝑤, wfunc, dfunc, 𝑋)

    𝑓 = collect( dfunc(𝑋[i]) - (-1)^(i-1)*h/wfunc(𝑋[i]) for i = 1:length(𝑋))

    f_itp = xx->getp(xx, 𝑓, 𝑤, h, wfunc, dfunc, 𝑋)
    if type_num == 2
        f_itp = xx->getp(xx, 𝑓, 𝑤, h, wfunc, dfunc, 𝑋)/type2weightfunc(xx)

    elseif type_num == 3
        f_itp = xx->getp(xx, 𝑓, 𝑤, h, wfunc, dfunc, 𝑋)/type3weightfunc(xx)

    elseif type_num == 4
        f_itp = xx->getp(xx, 𝑓, 𝑤, h, wfunc, dfunc, 𝑋)/type4weightfunc(xx)

    end

    # get Chebyshev coefficients.
    c_SharedArray, ν_SharedArray = getChebyshevinterpolatorfast(f_itp,length(𝑋)-1)

    c = convert(Vector{BigFloat},c_SharedArray)
    ν = convert(Vector{BigFloat},ν_SharedArray)

    # sanity check.
    f_itp_Cheby = xx->clenshaweval(c,xx)

    xq = convert( Vector{BigFloat}, collect(LinRange(-1,1,200)) )

    yq = f_itp.(xq)
    yq2 = f_itp_Cheby.(xq)

    discrepancy = LinearAlgebra.norm(yq-yq2)
    return c, ν, discrepancy, xq, yq, yq2
end


using PQEcolaPoint
using Dates

# what is the contract?
short_put_contract = PutContractModel()
short_put_contract.ticker = "XYZ"
short_put_contract.expiration_date = Date(2022, 04, 14)
short_put_contract.strike_price = 105.0
short_put_contract.premium = 0.0
short_put_contract.number_of_contracts = 1
short_put_contract.direction = 1

# other version -
val_vega = vega(short_put_contract; Sₒ=113.46, number_of_levels=80, σ=0.5212, T=(27.0 / 365), μ=0.0047)


#################################
# uniform mixture from template #
#################################

import Gen: geometric
import LinearAlgebra
import NearestNeighbors
import Memoize: @memoize
import StatsFuns: logsumexp

struct UniformMixtureFromTemplate <: Distribution{Matrix{Float64}} end

"""
    Y = uniform_mixture_from_template(X, p_outlier, radius, bounds)

Distribution over variable-sized point clouds representing i.i.d. samples from
a mixture of uniform distributions over:

* balls centered at each point of the template point cloud `X` (inliers), and
* a big box with bounds `bounds` (outliers).

That is, this model is an infinite mixture (for `n`) of finite mixtures (for
each of the `n` points in the point cloud).

The distribution on `n` is geometric, with expected value proportional to the
number of points in the template point cloud `X`.

The inner mixture component corresponding to the box `bounds` has weight
`p_outlier`, and for each `i`, the inner mixture component corresponding to the
sphere centered at `X[:,i]` with radius `radius` has weight `(1 - p_outlier) /
m`, where `size(X) = (3, m)`.

This distribution is "collapsed," in the sense that the `logpdf` marginalizes
out the inlier/outlierness.

The returned matrix `Y` has dimensions `3 × n`.
"""
const uniform_mixture_from_template = UniformMixtureFromTemplate()

"""
Parameter of the geometric distribution for number of points in the random
point cloud `Y`, given the number of points in the template point cloud `X`
"""
function _UniformMixtureFromTemplate_p_numpoints_Y(numpoints_X::Integer)
    return min(1 - eps(), 3 / numpoints_X)
end

# Reference implementation of the sampler -- to clarify the semantics of the
# distribution, even if it's not used in code.
function Gen.random(::UniformMixtureFromTemplate, X::Matrix{Float64},
                    p_outlier::Float64, radius::Float64,
                    bounds::Tuple)
    @assert size(X, 1) == 3
    m = size(X, 2)
    n = Gen.random(geometric, _UniformMixtureFromTemplate_p_numpoints_Y(m))
    mins = [bounds[1], bounds[3], bounds[5]]
    maxs = [bounds[2], bounds[5], bounds[6]]

    outlier = (rand(n) .< p_outlier)
    return reduce(hcat, (if outlier[i]
                             mins + rand(3) .* (maxs - mins)
                         else
                             v = LinearAlgebra.normalize(randn(3))
                             X[:, rand(1:m)] + radius * rand() * v
                         end for i in 1:n);
                  init=zeros(3, 0))
end

@memoize function get_tree_from_cloud(Y)
    NearestNeighbors.KDTree(Y)
end

function Gen.logpdf(
        ::UniformMixtureFromTemplate, Y::Matrix{Float64}, X::Matrix{Float64},
        p_outlier::Float64, radius::Float64, bounds::Tuple)
    size(X, 1) == 3 || error("X must have size 3 × something, each column represents a point.  Got size(X) = $(size(X))")
    size(Y, 1) == 3 || error("Y must have size 3 × something, each column represents a point.  Got size(Y) = $(size(Y))")
    all((Y .≥ [bounds[1], bounds[3], bounds[5]]) .&
        (Y .≤ [bounds[2], bounds[4], bounds[6]])
       ) || @warn("Some points in Y were out of bounds")

    tree = get_tree_from_cloud(Y)

    m = size(X, 2)
    n = size(Y, 2)
    logp_numpoints = Gen.logpdf(geometric, n,
                                _UniformMixtureFromTemplate_p_numpoints_Y(m))
    r = radius

    # all_idxs is an array of arrays, where the array all_idxs[i] contains all indices j such that
    # the points X[:,i] and Y[:,j] are within a distance of r units of each other.
    all_idxs = NearestNeighbors.inrange(tree, X, r)

    # For each point in Y, count how many points in X it is within r units of.
    num_close_pts = zeros(n)
    for idxs in all_idxs
        num_close_pts[idxs] .= num_close_pts[idxs] .+ 1
    end

    # Compute volume of inlier sphere, outlier bounds box, and the difference.
    Volume_inlier = 4.0 /3 * pi * r^3
    Volume_outlier = (bounds[2] - bounds[1]) * (bounds[4] - bounds[3]) * (bounds[6] - bounds[5])

    # Consider each column `Y[:,j]` to represent a single sample.
    # Then `logp_y_and_inlier` is an array whose `j`th entry is
    #     log( p( the jth sample is an inlier && the jth sample equals Y[:,j] ) )
    logp_y_and_inlier = log.( ((1-p_outlier) / Volume_inlier / m) .* num_close_pts )
    logp_y_and_outlier = log(p_outlier / Volume_outlier)

    # Log probability of each point (i.e., each column of `Y`), with the
    # inlier/outlierness marginalized out.
    @assert ndims(logp_y_and_inlier) == 1
    logp_y = logsumexp.(logp_y_and_inlier,
                        logp_y_and_outlier)

    # Return product of likelihoods across points.
    return logp_numpoints + sum(logp_y)
end

struct UniformMixtureFromTemplateMultiCloud <: Distribution{Matrix{Float64}} end
const uniform_mixture_from_template_multi_cloud = UniformMixtureFromTemplateMultiCloud()

function Gen.logpdf(
        ::UniformMixtureFromTemplateMultiCloud, Y::Matrix{Float64}, X::Array{Matrix{Float64}},
        p_outlier::Float64, radius::Float64, bounds::Tuple)
   log_pdfs = [logpdf(uniform_mixture_from_template, Y, X[i], p_outlier, radius, bounds) for i in 1:length(X)]
   sum(log_pdfs)
end

export uniform_mixture_from_template, uniform_mixture_from_template_multi_cloud


# Parses distribution expression and returns expression for a segment cost function
# Required for PELT and BS macros
function cost_function(data::Any, dist_expr::Expr)
    if !Meta.isexpr(dist_expr,:call)
        error("Syntax error: expected distribution as argument")
    end

    dist_type = dist_expr.args[1]

    if dist_type == :Normal
        if length(dist_expr.args) != 3
            error("Normal distribution has two parameters")
        end
        μ, σ = dist_expr.args[2:3]
        if μ == :(:?) && σ != :(:?)
            println("Changepoint distribution is Normal with changing mean and fixed variance")
            return :(NormalMeanSegment($data , $σ))
        elseif μ != :(:?) && σ == :(:?)
            println("Changepoint distribution is Normal with fixed mean and changing variance")
            #μ = eval(Main, μ)
            return :(NormalVarSegment($data, $μ))
        elseif μ == :(:?) && σ == :(:?)
            println("Changepoint distribution is Normal with changing mean and changing variance")
            return :(NormalMeanVarSegment($data))
        else
            error("Must mark at least one Normal parameter as changing with a :? symbol")
        end

    elseif dist_type == :Exponential
        println("Changepoint distribution is Exponential with changing mean")
        return :(ExponentialSegment($data))

    elseif dist_type == :Poisson
        println("Changepoint distribution is Poisson with changing mean")
        return :(PoissonSegment($data))

    elseif dist_type == :Gamma
        if length(dist_expr.args) != 3
            error("Gamma distribution has two parameters")
        end
        alpha , beta = dist_expr.args[2:3]
        if alpha == :(:?) && beta != :(:?)
            println("Changepoint distribution is Gamma with changing shape and fixed rate")
            return :(GammaShapeSegment($data, $beta))
        elseif alpha != :(:?) && beta == :(:?)
            println("Changepoint distribution is Gamma with fixed shape and changing rate")
            return :(GammaRateSegment($data, $alpha))
        else
            error("Must mark at least one Gamma parameter as changing with a :? symbol")
        end

    elseif dist_type == :Nonparametric
        K = dist_expr.args[2]
        println("Changepoint method is Nonparametric")
        return :(NonparametricSegment($data, $K))

    elseif dist_type == :OLS
        println("Changepoints in peicewise linear regressions")
        return :(OLSSegment($data))

    else
        error("Distribution $(dist_type) has no implemented cost functions")
    end
end

"""
Creates a segment cost function given data and changepoint model expression

# Usage
@segement_cost data changepoint_model

# Cost functions

A changepoint model is an expression which describes what segment cost function
should be constructed for use with PELT. For parametric segment cost functions,
this is represented by the name of a distribution (as in the Distributions package)
with some parameters replaced by '?' to indicate that a parameters is changing.
The full list of available cost functions based on parametric distributions is
as follows:

* `Normal(?, σ): Normal model with changing mean and fixed standard deviation σ (see also NormalMeanSegment)
* `Normal(μ, ?)`: Normal model with fixed mean μ and changing standard deviation (see also NormalVarSegment)
* `Normal(?, ?)`: Normal model with changing mean and standard deviation (see also NormalMeanVarSegment)
* `Exponential(?)`: Exponential model with changing mean (see also ExponentialSegment)
* `Poisson(?)`: Poisson model with changing mean (see also PoissonSegment)
* `Gamma(?, β)`: Gamma model with fixed rate parameter β and changing shape parameter (see also GammaShapeSegment)
* `Gamma(α, ?)`: Gamma model with fixed shape parameter α and changing rate parameter (see also GammaRateSegment)

A nonparametric cost function is also provided and the model is represented with
the following expression:

* `Nonparametric(k)`: Nonparametric cost function with parameter k (see also NonparametricSegment)

# Example
```julia
n = 1000
λ = 100
μ, σ = Normal(0.0, 10.0), 1.0
# Samples changepoints from Normal distribution with changing mean
sample, cps = @changepoint_sampler n λ Normal(μ, σ)
# Create cost function
seg_cost = @segment_cost sample Normal(?, σ)
# Calculate changepoints using PELT and BS
pelt_cps, cost = PELT(seg_cost, n)
bs_cps = BS(seg_cost, n)
```
"""
macro segment_cost(data, dist)
    esc(cost_function(data, dist))
end

"""
Runs the PELT algorithm using a specified cost function and penalty value to find the position and number of changepoints

# Usage

1. `@PELT data changepoint_model`: Run PELT with default penalty value

2. `@PELT data changepoint_model β`: Run PELT at penalty value β

3. `@PELT data changepoint_model β₁ β₂`: Run CROPS algorithm for penalties between β₁ and β₂

# Example
```
n = 1000
λ = 100
μ, σ = Normal(0.0, 10.0), 1.0
# Samples changepoints from Normal distribution with changing mean
sample, cps = @changepoint_sampler n λ Normal(μ, σ)
# Run PELT on sample
pelt_cps, pelt_cost = @PELT sample Normal(?, σ)
```

# See also
PELT, @segment_cost
"""
macro PELT(data, dist, args...)
    cost_func = cost_function(data, dist)
    if length(args) == 0
        return esc(:(PELT($(cost_func), length($data))))
    elseif length(args) == 1
        return esc(:(PELT($(cost_func), length($data), pen=$(args[1]))))
    else
        return esc(:(CROPS($(cost_func), length($data), ($(args[1]), $(args[2])))))
    end
end

"""
# Description
Runs the Binary Segmentation algorithm using a specified cost function for a given penalty

# Usage
1. `@BS data changepoint_model`: Run binary segmentation with default penalty value

2. `@BS data changepoint_model β`: Run binary segmentation with penalty value β

# Example
```
n = 1000   # Length of time series
λ = 100    # Frequency of changepoints
α, β = Uniform(0.0, 10.0), 1.0
# Samples changepoints from Gamma distribution with changing shape
sample, cps = @changepoint_sampler n λ Gamma(α, β)
# Run binary segmentation on sample
bs_cps = @BS sample Gamma(?, β)
```

# See also
BS, @segment_cost
"""
macro BS(data, dist, args...)
    cost_func = cost_function(data, dist)
    if length(args) == 0
        return esc(:(BS($(cost_func), length($data))))
    else
        return esc(:(BS($(cost_func), length($data), pen=$(args[1]))))
    end
end


struct State_fixed{T<:AbstractVector}
    st::T
    ope::Op_fixed
    State_fixed(st,ope) = length(st) != binomial(dim(ope)+nume(ope)-1,nume(ope)) ? error("lenght of vector does not match dimension") : new{typeof(st)}(st/sqrt(st'*st),ope)
end

st(s::State_fixed) = s.st

ope(s::State_fixed) = s.ope

nume(s::State_fixed) = nume(s.ope)

dim(s::State_fixed) = dim(s.ope)

basis(s::State_fixed) = basis(s.ope)

typ(s::State_fixed) = eltype(s.st)

function rhosp(sta::State_fixed, precis=15)
    n = dim(sta)
    num = nume(sta)
    rhospv = spzeros(typ(sta),n,n)
    estate = st(sta)
    o = ope(sta)
    for i in 1:n
        for j in 1:n
            rhospv[i,j] = round(estate'*bdb(o, i, j)*estate, digits = precis)
        end
    end
    return rhospv
end


using ADCME
using AdFem

include("../chip_unstructured_solver.jl")
include("../chip_unstructured_geometry.jl")

k_mold = 0.014531
k_chip_ref = 2.60475
k_air = 0.64357

function k_exact(x, y)
    k_mold + 1000 * k_chip_ref * (x-0.49)^2 / (1 + x^2)
end

function k_nn(x, y, θ)
    out =  fc(x, [20,20,20,1], θ)^2  .+ k_mold
    squeeze(out)
end

xy = mesh.nodes
xy2 = zeros(mesh.nedge, 2)
for i = 1:mesh.nedge
    xy2[i,:] = (mesh.nodes[mesh.edges[i,1], :] + mesh.nodes[mesh.edges[i,2], :])/2
end
xy = [xy;xy2]

x, y = xy[chip_fem_idx, 1], xy[chip_fem_idx, 2]

n = 901
θ = Variable(randn(n))
k_chip = k_nn(x, y, θ)
k_chip_exact = @. k_exact(x, y)

loss =  mean((k_chip .- k_chip_exact)^2)
loss = loss * 1e10

# ---------------------------------------------------
_loss = Float64[]
cb = (vs, iter, loss)->begin
    global _loss
    push!(_loss, loss)
    printstyled("[#iter $iter] loss=$loss\n", color=:green)
end

ADCME.options.training.training = placeholder(true)
l = placeholder(rand(193,))
x = placeholder(rand(5875, 2))

# train the neural network
opt = AdamOptimizer().minimize(loss)
sess = Session(); init(sess)
for i = 1:10
    _, loss_ = run(sess, [opt, loss], feed_dict=Dict(l=>k_chip_exact, x=>xy))
    @info i, loss_
end

# sess = Session(); init(sess)
BFGS!(sess, loss, vars = [k_chip], callback = cb)
matwrite("k_diagnose_theta.mat", Dict("theta"=>run(sess, θ)))
matwrite("k_diagnose_loss.mat", Dict("loss"=>_loss))


using StudentHousing, StatsBase, Distributions, JuMP, Gurobi

srand(32)

# =============================================================================
# Data
# =============================================================================
nbedrooms_range = collect(1:2)
nbedrooms_frequency = Weights([0.5, 0.5])
nbathrooms_range = collect(1:2)
nbathrooms_frequency = Weights([0.5, 0.5])
prices_range_pp = [800.0, 1000.0]
area_ranges = [0.0]
market_data = StudentHousing.MarketData(nbedrooms_range, nbedrooms_frequency,
    nbathrooms_range, nbathrooms_frequency, prices_range_pp, area_ranges)
budget = 1e6
problem_data = StudentHousingData(market_data, nhouses = 2, budget = budget,
                  demand_distribution = Uniform(0.9, 1.1))

d = problem_data
nhouses = length(d.houses)
npatterns = length(d.patterns_allow)

# =============================================================================
# Solve problem
# =============================================================================
m, U_generated, λ_generated, pattern_choice = solve_pattern_generation(d)

# =============================================================================
# Look at solution
# =============================================================================
getobjectivevalue(m)
# Recover the solution
# In case one of our initial columns was in the optimal basis
results = zeros(nhouses, length(d.patterns_allow))
for k = 1:npatterns
    if getvalue(m[:λ][k]) ≈ 1.0
        for i = 1:nhouses
            if house_fits_pattern(i, k, d)
                results[i, k] .= 1.0
                break
            end
        end
    end
end
# Check all the other columns we added
for k = 1:length(U_generated)
    if getvalue(λ_generated[k]) ≈ 1.0
        # Find the assignment that we picked for our house
        houses = find(U_generated[k] .≈ 1.0)
        # Check the pattern index
        p = pattern_choice[k]
        # Mark the assignment
        results[houses, p] .= 1.0
    end
end

println("House 1 assigned to:", find(results[1,:] .≈ 1.0))
# House 1 assigned to:[6, 7]

# sanity check
StudentHousing.house_allowedby(1, d)
# 5-element Array{Int64,1}:
#   6
#   7
#  11
#  12
#  13

println("House 2 assigned to:", find(results[2,:] .≈ 1.0))
# House 2 assigned to:[10]
# sanity check
StudentHousing.house_allowedby(2, d)
# 11-element Array{Int64,1}:
#   3
#   4
#   5
#   6
#   7
#   8
#   9
#  10
#  11
#  12
#  13


function K_DKGQ(elm::Quad)::Matrix{Float64}
    return K_GQ12(elm)+K_DKQ(elm)
end


function GetExprMetric(file)
    df = DataFrame(CSV.File(file,types=Dict(1=>String)))
    exprs = []
    for (d,f) ∈ eachrow(df[df.Coefficient .!= "0",[:Index,:Coefficient]])
        #println((d,f))
        d = d == "-" ? "" : d
        d = replace(d,"th"=>"θ")
        ps = "ψ"*d
        push!(exprs,"($f)*$(ps)(r,x)")
    end
    func_def = "$(join(exprs," + "))"
    println(func_def)
    Meta.parse(func_def)
end

function GetExprDerivative(file)
    df = DataFrame(CSV.File(file,types=Dict(1=>String)))
    exprs = []
    for (d,f) ∈ eachrow(df[df.Coefficient .!= "0",:])
        #println((d,f))
        d = d == "-" ? "" : d
        d = replace(d,"th"=>"θ")
        ps = "ψ"*d
        push!(exprs,"($f)*$(ps)(r,x)")
    end
    func_def = "$(join(exprs," + "))"
    println(func_def)
    Meta.parse(func_def)
end

function MakeδT(file; additional_params = ())
    thisexpr = GetExprMetric(file)
    @eval δT(r,x,a,m,ω,s,ψ11,ψ12,ψ22,ψ1,ψ2,ψ,$(additional_params...); M=1) = $thisexpr
    δT
end

#δT = MakeδT("TuekolskyShifts.csv")

function Make∂ωT(file; additional_params = ())
    thisexpr2 = GetExprDerivative(file)
    @eval ∂ωT(r,x,a,m,ω,s,ψ11,ψ12,ψ22,ψ1,ψ2,ψ,$(additional_params...); M=1) = $thisexpr2
    ∂ωT
end

#∂ωT = Make∂ωT("TuekolskyFrequencyDerivative.csv")


#=
Author : Ng Yeesian
Modified : Guy Farmer
generates the AmbulanceDeployment package
=#
module AmbulanceDeployment

    import JuMP, Gurobi
    import DataStructures: PriorityQueue, enqueue!, dequeue!
    import DataFrames: DataFrame, nrow
    import Distributions
    import DataStructures: PriorityQueue, enqueue!, dequeue!
    import Pkg
    import CSV, Query
    using Gurobi, CSV, Query, JuMP, Dates, JLD, GLPK
    using DataFrames, CSV, Random, JSON, Distributions #Plots

    const PROJECT_ROOT = pkgdir(AmbulanceDeployment)

    include("problem.jl")
    include("model.jl")
    include("dispatch/closestdispatch.jl")
    include("simulate.jl")
    include("evaluate.jl")
    include(PROJECT_ROOT * "/test/generate_simulation.jl")
    include("deployment/robust.jl")
    include("deployment/stochastic.jl")
    include("deployment/stochastic_hyp.jl")
    include("deployment/stochastic_rem.jl")
    include("deployment/malp.jl")
    include("deployment/mexclp.jl")
    include("plot.jl")
    #include("Ambulance_Deployment_experiments.jl")
    include("Single_Robust.jl")

    export
           DeploymentProblem,
           DispatchProblem,
           RobustDeployment,
           StochasticDeployment,
           MALPDeployment,
           MEXCLPDeployment,
           ClosestDispatch,
           AssignmentModel,
           solve,
           evaluate,
           deployment,
           convergence_plot,
           compose_neighborhoods,
           compose_locations,
           compose_chloropleth,
           performance,
           test_performance,
           plot_timings,
           simulate_events!,
           generate_simulation,
           PROJECT_ROOT,
           optimize!,
           generate_deployment,
           DeploymentModel,
           StochasticDeployment_hyp,
           StochasticDeployment_rem
end


using NetworkEpidemics
using LightGraphs
using Statistics
using Plots
using ColorSchemes
using LaTeXStrings
using Random

CorrectedMetapopulation(χ::Real, mp::Metapopulation{SI}) = Metapopulation(mp.h, mp.D, SI(χ*mp.dynamics.β))

##

function rewire_network(g::SimpleGraph, p)
    h = SimpleGraph(g)
    for e in edges(g)
        u = src(e)
        v = dst(e)
        if rand() < p
            if rand() < 0.5
                w = u
            else
                w = v
            end
            x = rand(setdiff(vertices(h), [u,v]))
            # If there's already an edge, we just swap them, so the graph is unchanged
            if !has_edge(h,w,x)
                rem_edge!(h,u,v)
                add_edge!(h,w,x)
            end
        end
    end
    return h
end

##
Random.seed!(2020)

N = 1000
M = 10

h = complete_graph(M)
#g = watts_strogatz(N, 100, 0.0)
g = random_regular_graph(N, 50)

k = degree(g)

χ = mean(k)/N # doesn't change so only compute it once

β = 0.1
D = 0.1

mp = Metapopulation(h, D, SI(β))
mpc = CorrectedMetapopulation(χ, mp)

mpx = Metaplex(g, h, D, SI(β))

x0_i = fill(1, N)
x0_i[1:10] .= 2
x0_μ = rand(1:M, N)
x0_mp = zeros(Int, M, 2)
for i in 1:N
    x0_mp[x0_μ[i], x0_i[i]] += 1
end

tmax = 30.0
nmax = 10000
nsims = 1000
nbins = 200

ts_mp, output_mp = gillespie(mp, x0_mp, tmax=tmax, nmax=nmax)
ts_mpx, output_mpx = gillespie(mpx, [x0_i,x0_μ], tmax=tmax, nmax=nmax)

plot(ts_mp, output_mp[2], label="")
plot(ts_mpx, output_mpx[2], label="")

ts = LinRange(0.0, tmax, nbins)
ps = LinRange(0.0, 1.0, 5)

Random.seed!(2020)

ts_av_mpx, u_av_mpx = average(mpx, [x0_i,x0_μ], tmax=tmax, nbins=nbins,nsims=nsims,nmax=nmax)

ts_mf_mp, u_mf_mp = meanfield(mp, x0_mp, tmax=tmax, saveat=ts)
ts_mf_mpc, u_mf_mpc = meanfield(mpc, x0_mp, tmax=tmax, saveat=ts)

u_mf_mpx = zeros(length(ps), length(ts))
d_stds = Array{Float64,2}(undef, length(ps), 100)
for i in 1:length(ps), j in 1:100
    f = rewire_network(g, ps[i])
    d_stds[i,j] = std(degree(f))
    local mpx = Metaplex(f, h, D, SI(β))
    t, u = meanfield(mpx, [x0_i,x0_μ], tmax=tmax, saveat=ts)
    u_mf_mpx[i,:] += sum(u[2], dims=2)
end
u_mf_mpx /= 100

colors = ColorSchemes.GnBu_9;
orange = ColorSchemes.Oranges_3[3];

plot(ts, sum(u_mf_mpc[2], dims=2)/N,
    #label="Meanfield (No correction)",
    label = "Mean degree correction",
    xlabel="Time",
    ylabel="Fraction of infected individuals",
    legend=:bottomright,
    color = orange
    )
    plot!(ts, u_mf_mpx[1,:]/N,
    label = "p=0.0",
    color = colors[5]
    )
    plot!(ts, u_mf_mpx[2,:]/N,
    label = "p=0.25",
    color = colors[6]
    )
    plot!(ts, u_mf_mpx[3,:]/N,
    label = "p=0.5",
    color = colors[7]
    )
    plot!(ts, u_mf_mpx[4,:]/N,
    label = "p=0.75",
    color = colors[8]
    )
    plot!(ts, u_mf_mpx[5,:]/N,
    label = "p=1.0",
    color = colors[9]
    )
    plot!(ts, sum(u_mf_mpc[2], dims=2)/N,
    label="",
    color = orange
    )

# necessary for consistent LaTeX
pyplot()

plot(ps, mean(d_stds, dims=2).^2,
    label="",
    xlabel="p",
    ylabel=L"\sigma^2")

cols = map(x->[x.r, x.g, x.b], colors.colors)
or = [orange.r, orange.g, orange.b]

##
using MATLAB

mat"
figure
xlabel('Time')
ylabel('Fraction of Infected Individuals')
hold on
plot($ts, $(sum(u_mf_mpc[2], dims=2)/N), 'Color', $(or))
plot($ts, $(u_mf_mpx[1,:]/N), 'Color', $(cols[5]))
plot($ts, $(u_mf_mpx[2,:]/N), 'Color', $(cols[6]))
plot($ts, $(u_mf_mpx[3,:]/N), 'Color', $(cols[7]))
plot($ts, $(u_mf_mpx[4,:]/N), 'Color', $(cols[8]))
plot($ts, $(u_mf_mpx[5,:]/N), 'Color', $(cols[9]))
hold off
legend({'Mean degree correction', 'p=0.0', 'p=0.25', 'p=0.5', 'p=0.75', 'p=1.0'}, 'Location','southeast')
"


export ZeroPad

"""
`ZeroPad([domainType::Type,] dim_in::Tuple, zp::Tuple)`

`ZeroPad(x::AbstractArray, zp::Tuple)`

Create a `LinearOperator` which, when multiplied to an array `x` of size `dim_in`, returns an expanded array `y` of size `dim_in .+ zp` where `y[1:dim_in[1], 1:dim_in[2] ... ] = x` and zero elsewhere.

```julia
julia> Z = ZeroPad((2,2),(0,2))
[I;0]  ℝ^(2, 2) -> ℝ^(2, 4)

julia> Z*ones(2,2)
2×4 Array{Float64,2}:
 1.0  1.0  0.0  0.0
 1.0  1.0  0.0  0.0

```

"""
struct ZeroPad{T,N} <: LinearOperator
	dim_in::NTuple{N,Int}
	zp::NTuple{N,Int}
end

# Constructors
#standard constructor
function ZeroPad(domainType::Type, dim_in::NTuple{N,Int}, zp::NTuple{M,Int}) where {N,M}
	M != N && error("dim_in and zp must have the same length")
	any([zp...].<0) && error("zero padding cannot be negative")
	ZeroPad{domainType,N}(dim_in,zp)
end

ZeroPad(dim_in::Tuple, zp::NTuple{N,Int})                   where {N} = ZeroPad(Float64, dim_in, zp)
ZeroPad(domainType::Type, dim_in::Tuple, zp::Vararg{Int,N}) where {N} = ZeroPad(domainType, dim_in, zp)
ZeroPad(dim_in::Tuple, zp::Vararg{Int,N})                   where {N} = ZeroPad(Float64, dim_in, zp)
ZeroPad(x::AbstractArray, zp::NTuple{N,Int})                where {N} = ZeroPad(eltype(x), size(x), zp)
ZeroPad(x::AbstractArray, zp::Vararg{Int,N})                where {N} = ZeroPad(eltype(x), size(x), zp)

# Mappings
@generated function mul!(y::AbstractArray{T,N},L::ZeroPad{T,N},b::AbstractArray{T,N}) where {T,N}

	# builds
	#for i1 =1:size(y,1), i2 =1:size(y,2)
	#	y[i1,i2] = i1 <= size(b,1) && i2 <= size(b,2)  ?  b[i1,i2] : 0.
	#end

	ex = "for "
	for i = 1:N ex *= "i$i =1:size(y,$i)," end

	ex = ex[1:end-1] #remove ,

	ex *= " y["
	for i = 1:N ex *= "i$i," end

	ex = ex[1:end-1] #remove ,

	ex *= "] = "
	for i = 1:N ex *= " i$i <= size(b,$i) &&" end

	ex = ex[1:end-2] #remove &&

	ex *= " ?  b["
	for i = 1:N ex *= "i$i," end

	ex = ex[1:end-1] #remove ,
	ex *= "] : 0. end"

	ex = Meta.parse(ex)

end

@generated function mul!(y::AbstractArray{T,N},L::AdjointOperator{ZeroPad{T,N}},b::AbstractArray{T,N}) where {T,N}

	#builds
	#for l = 1:size(y,1), m = 1:size(y,2)
	#	y[l,m] = b[l,m]
	#end

	ex = "for "
	for i = 1:N ex *= "i$i =1:size(y,$i)," end

	ex *= " y["
	idx = ""
	for i = 1:N idx *= "i$i," end

	idx = idx[1:end-1] #remove ,

	ex *= idx

	ex *= "] = b["
	ex *= idx
	ex *= "] end"

	ex = Meta.parse(ex)

end

# Properties

  domainType(L::ZeroPad{T}) where {T} = T
codomainType(L::ZeroPad{T}) where {T} = T

size(L::ZeroPad) = L.dim_in .+ L.zp, L.dim_in

fun_name(L::ZeroPad)       = "[I;0]"
is_AcA_diagonal(L::ZeroPad) = true
diag_AcA(L::ZeroPad) = 1

is_full_column_rank(L::ZeroPad) = true


# Basic algebra
#---------------
Base.copy(t::AbstractTensorMap) = Base.copyto!(similar(t), t)

Base.:-(t::AbstractTensorMap) = mul!(similar(t), t, -one(eltype(t)))
function Base.:+(t1::AbstractTensorMap, t2::AbstractTensorMap)
    T = promote_type(eltype(t1), eltype(t2))
    return axpy!(one(T), t2, copyto!(similar(t1, T), t1))
end
function Base.:-(t1::AbstractTensorMap, t2::AbstractTensorMap)
    T = promote_type(eltype(t1), eltype(t2))
    return axpy!(-one(T), t2, copyto!(similar(t1, T), t1))
end

Base.:*(t::AbstractTensorMap, α::Number) =
    mul!(similar(t, promote_type(eltype(t), typeof(α))), t, α)
Base.:*(α::Number, t::AbstractTensorMap) =
    mul!(similar(t, promote_type(eltype(t), typeof(α))), α, t)
Base.:/(t::AbstractTensorMap, α::Number) = *(t, one(α)/α)
Base.:\(α::Number, t::AbstractTensorMap) = *(t, one(α)/α)

LinearAlgebra.normalize!(t::AbstractTensorMap, p::Real = 2) = rmul!(t, inv(norm(t, p)))
LinearAlgebra.normalize(t::AbstractTensorMap, p::Real = 2) =
    mul!(similar(t), t, inv(norm(t, p)))

Base.:*(t1::AbstractTensorMap, t2::AbstractTensorMap) =
    mul!(similar(t1, promote_type(eltype(t1),eltype(t2)), codomain(t1)←domain(t2)), t1, t2)
Base.exp(t::AbstractTensorMap) = exp!(copy(t))
Base.:^(t::AbstractTensorMap, p::Integer) =
    p < 0 ? Base.power_by_squaring(inv(t), -p) : Base.power_by_squaring(t, p)

# Special purpose constructors
#------------------------------
Base.zero(t::AbstractTensorMap) = fill!(similar(t), 0)
function Base.one(t::AbstractTensorMap)
    domain(t) == codomain(t) ||
        throw(SectorMismatch("no identity if domain and codomain are different"))
    return one!(similar(t))
end
function one!(t::AbstractTensorMap)
    domain(t) == codomain(t) ||
        throw(SectorMismatch("no identity if domain and codomain are different"))
    for (c,b) in blocks(t)
        _one!(b)
    end
    return t
end

"""
    id([A::Type{<:DenseMatrix} = Matrix{Float64},] space::VectorSpace) -> TensorMap

Construct the identity endomorphism on space `space`, i.e. return a `t::TensorMap` with `domain(t) == codomain(t) == V`, where `storagetype(t) = A` can be specified.
"""
id(A, V::ElementarySpace) = id(A, ProductSpace(V))
id(V::VectorSpace) = id(Matrix{Float64}, V)
id(::Type{A}, P::ProductSpace) where {A<:DenseMatrix} =
    one!(TensorMap(s->A(undef, s), P, P))

"""
    isomorphism([A::Type{<:DenseMatrix} = Matrix{Float64},]
                    cod::VectorSpace, dom::VectorSpace)
    -> TensorMap

Return a `t::TensorMap` that implements a specific isomorphism between the codomain `cod`
and the domain `dom`, and for which `storagetype(t)` can optionally be chosen to be of type
`A`. If the two spaces do not allow for such an isomorphism, and are thus not isomorphic,
and error will be thrown. When they are isomorphic, there is no canonical choice for a
specific isomorphism, but the current choice is such that
`isomorphism(cod, dom) == inv(isomorphism(dom, cod))`.

See also [`unitary`](@ref) when `spacetype(cod) isa EuclideanSpace`.
"""
isomorphism(cod::TensorSpace, dom::TensorSpace) = isomorphism(Matrix{Float64}, cod, dom)
isomorphism(P::TensorMapSpace) = isomorphism(codomain(P), domain(P))
isomorphism(A::Type{<:DenseMatrix}, P::TensorMapSpace) =
    isomorphism(A, codomain(P), domain(P))
isomorphism(A::Type{<:DenseMatrix}, cod::TensorSpace, dom::TensorSpace) =
    isomorphism(A, convert(ProductSpace, cod), convert(ProductSpace, dom))
function isomorphism(::Type{A}, cod::ProductSpace, dom::ProductSpace) where {A<:DenseMatrix}
    spacetype(cod) == spacetype(dom) ||
        throw(SpaceMismatch("codomain $cod and domain $dom are not isomorphic"))
    for c in union(blocksectors(cod), blocksectors(dom))
        blockdim(cod, c) == blockdim(dom, c) ||
            throw(SpaceMismatch("codomain $cod and domain $dom are not isomorphic"))
    end
    t = TensorMap(s->A(undef, s), cod, dom)
    for (c,b) in blocks(t)
        _one!(b)
    end
    return t
end

const EuclideanTensorSpace = TensorSpace{<:EuclideanSpace}
const EuclideanTensorMapSpace = TensorMapSpace{<:EuclideanSpace}
const AbstractEuclideanTensorMap = AbstractTensorMap{<:EuclideanTensorSpace}
const EuclideanTensorMap = TensorMap{<:EuclideanTensorSpace}

"""
    unitary([A::Type{<:DenseMatrix} = Matrix{Float64},] cod::VectorSpace, dom::VectorSpace)
    -> TensorMap

Return a `t::TensorMap` that implements a specific unitary isomorphism between the codomain
`cod` and the domain `dom`, for which `spacetype(dom)` (`== spacetype(cod)`) must be a
subtype of `EuclideanSpace`. Furthermore, `storagetype(t)` can optionally be chosen to be
of type `A`. If the two spaces do not allow for such an isomorphism, and are thus not
isomorphic, and error will be thrown. When they are isomorphic, there is no canonical choice
for a specific isomorphism, but the current choice is such that
`isomorphism(cod, dom) == inv(isomorphism(dom, cod)) = adjoint(unitary(dom, cod))`.
"""
unitary(cod::EuclideanTensorSpace, dom::EuclideanTensorSpace) = isomorphism(cod, dom)
unitary(P::EuclideanTensorMapSpace) = isomorphism(P)
unitary(A::Type{<:DenseMatrix}, P::EuclideanTensorMapSpace) = isomorphism(A, P)
unitary(A::Type{<:DenseMatrix}, cod::EuclideanTensorSpace, dom::EuclideanTensorSpace) =
    isomorphism(A, cod, dom)

"""
    isometry([A::Type{<:DenseMatrix} = Matrix{Float64},] cod::VectorSpace, dom::VectorSpace)
    -> TensorMap

Return a `t::TensorMap` that implements a specific isometry that embeds the domain `dom`
into the codomain `cod`, and which requires that `spacetype(dom)` (`== spacetype(cod)`) is
a subtype of `EuclideanSpace`. An isometry `t` is such that its adjoint `t'` is the left
inverse of `t`, i.e. `t'*t = id(dom)`, while `t*t'` is some idempotent endomorphism of
`cod`, i.e. it squares to itself. When `dom` and `cod` do not allow for such an isometric
inclusion, an error will be thrown.
"""
isometry(cod::EuclideanTensorSpace, dom::EuclideanTensorSpace) =
    isometry(Matrix{Float64}, cod, dom)
isometry(P::EuclideanTensorMapSpace) = isometry(codomain(P), domain(P))
isometry(A::Type{<:DenseMatrix}, P::EuclideanTensorMapSpace) =
    isometry(A, codomain(P), domain(P))
isometry(A::Type{<:DenseMatrix}, cod::EuclideanTensorSpace, dom::EuclideanTensorSpace) =
    isometry(A, convert(ProductSpace, cod), convert(ProductSpace, dom))
function isometry(::Type{A},
                    cod::ProductSpace{S},
                    dom::ProductSpace{S}) where {A<:DenseMatrix, S<:EuclideanSpace}
    spacetype(cod) == spacetype(dom) || throw(SpaceMismatch())
    for c in union(blocksectors(cod), blocksectors(dom))
        blockdim(cod, c) >= blockdim(dom, c) ||
            throw(SpaceMismatch("codomain $cod and domain $dom do not allow for an isometric mapping"))
    end
    t = TensorMap(s->A(undef, s), cod, dom)
    for (c,b) in blocks(t)
        _one!(b)
    end
    return t
end

# In-place methods
#------------------
# Wrapping the blocks in a StridedView enables multithreading if JULIA_NUM_THREADS > 1
# Copy, adjoint! and fill:
function Base.copyto!(tdest::AbstractTensorMap, tsource::AbstractTensorMap)
    codomain(tdest) == codomain(tsource) && domain(tdest) == domain(tsource) ||
        throw(SpaceMismatch())
    for c in blocksectors(tdest)
        copyto!(StridedView(block(tdest, c)), StridedView(block(tsource, c)))
    end
    return tdest
end
function Base.fill!(t::AbstractTensorMap, value::Number)
    for (c,b) in blocks(t)
        fill!(b, value)
    end
    return t
end
function LinearAlgebra.adjoint!(tdst::AbstractEuclideanTensorMap,
                                tsrc::AbstractEuclideanTensorMap)
    (codomain(tdst) == domain(tsrc) && domain(tdst) == codomain(tsrc)) ||
        throw(SpaceMismatch())
    for c in blocksectors(tdst)
        adjoint!(StridedView(block(tdst, c)), StridedView(block(tsrc, c)))
    end
    return tdst
end

# Basic vector space methods: addition and scalar multiplication
LinearAlgebra.rmul!(t::AbstractTensorMap, α::Number) = mul!(t, t, α)
LinearAlgebra.lmul!(α::Number, t::AbstractTensorMap) = mul!(t, α, t)

function LinearAlgebra.mul!(t1::AbstractTensorMap, t2::AbstractTensorMap, α::Number)
    (codomain(t1)==codomain(t2) && domain(t1) == domain(t2)) || throw(SpaceMismatch())
    for c in blocksectors(t1)
        mul!(StridedView(block(t1, c)), StridedView(block(t2, c)), α)
    end
    return t1
end
function LinearAlgebra.mul!(t1::AbstractTensorMap, α::Number, t2::AbstractTensorMap)
    (codomain(t1)==codomain(t2) && domain(t1) == domain(t2)) || throw(SpaceMismatch())
    for c in blocksectors(t1)
        mul!(StridedView(block(t1, c)), α, StridedView(block(t2, c)))
    end
    return t1
end
function LinearAlgebra.axpy!(α::Number, t1::AbstractTensorMap, t2::AbstractTensorMap)
    (codomain(t1)==codomain(t2) && domain(t1) == domain(t2)) || throw(SpaceMismatch())
    for c in blocksectors(t1)
        axpy!(α, StridedView(block(t1, c)), StridedView(block(t2, c)))
    end
    return t2
end
function LinearAlgebra.axpby!(α::Number, t1::AbstractTensorMap,
                                β::Number, t2::AbstractTensorMap)
    (codomain(t1)==codomain(t2) && domain(t1) == domain(t2)) || throw(SpaceMismatch())
    for c in blocksectors(t1)
        axpby!(α, StridedView(block(t1, c)), β, StridedView(block(t2, c)))
    end
    return t2
end

# inner product and norm only valid for spaces with Euclidean inner product
function LinearAlgebra.dot(t1::AbstractEuclideanTensorMap, t2::AbstractEuclideanTensorMap)
    (codomain(t1) == codomain(t2) && domain(t1) == domain(t2)) || throw(SpaceMismatch())
    iter = blocksectors(t1)
    if isempty(iter)
        return zero(eltype(t1))*zero(eltype(t2))
    else
        return sum(dim(c)*dot(block(t1,c), block(t2,c)) for c in blocksectors(t1))
    end
end

LinearAlgebra.norm(t::AbstractEuclideanTensorMap, p::Real) =
    isempty(blocks(t)) ? zero(real(eltype(t))) : _norm(blocks(t), p)
function _norm(blockiterator, p::Real)
    if p == Inf
        maximum(norm(b, p) for (c,b) in blockiterator)
    elseif p == -Inf
        minimum(norm(b, p) for (c,b) in blockiterator)
    elseif p == 1
        sum(dim(c)*norm(b, p) for (c,b) in blockiterator)
    else
        s = sum(dim(c)*norm(b, p)^p for (c,b) in blockiterator)
        return s^inv(oftype(s, p))
    end
end

# TensorMap trace
function LinearAlgebra.tr(t::AbstractTensorMap)
    domain(t) == codomain(t) ||
        throw(SpaceMismatch("Trace of a tensor only exist when domain == codomain"))
    return sum(dim(c)*tr(b) for (c,b) in blocks(t))
end

# TensorMap multiplication:
function LinearAlgebra.mul!(tC::AbstractTensorMap,
                            tA::AbstractTensorMap,
                            tB::AbstractTensorMap, α = true, β = false)
    if !(codomain(tC) == codomain(tA) && domain(tC) == domain(tB) && domain(tA) == codomain(tB))
        throw(SpaceMismatch())
    end
    for c in blocksectors(tC)
        if hasblock(tA, c) # then also tB should have such a block
            A = block(tA, c)
            B = block(tB, c)
            C = block(tC, c)
            if isbitstype(eltype(A)) && isbitstype(eltype(B)) && isbitstype(eltype(C))
                @unsafe_strided A B C mul!(C, A, B, α, β)
            else
                mul!(StridedView(C), StridedView(A), StridedView(B), α, β)
            end
        elseif β != one(β)
            rmul!(block(tC, c), β)
        end
    end
    return tC
end

# TensorMap inverse
function Base.inv(t::AbstractTensorMap)
    cod = codomain(t)
    dom = domain(t)
    for c in union(blocksectors(cod), blocksectors(dom))
        blockdim(cod, c) == blockdim(dom, c) ||
            throw(SpaceMismatch("codomain $cod and domain $dom are not isomorphic: no inverse"))
    end
    if sectortype(t) === Trivial
        return TensorMap(inv(block(t, Trivial())), domain(t)←codomain(t))
    else
        data = empty(t.data)
        for (c,b) in blocks(t)
            data[c] = inv(b)
        end
        return TensorMap(data, domain(t)←codomain(t))
    end
end
function LinearAlgebra.pinv(t::AbstractTensorMap; kwargs...)
    if sectortype(t) === Trivial
        return TensorMap(pinv(block(t, Trivial()); kwargs...), domain(t)←codomain(t))
    else
        data = empty(t.data)
        for (c,b) in blocks(t)
            data[c] = pinv(b; kwargs...)
        end
        return TensorMap(data, domain(t)←codomain(t))
    end
end
function Base.:(\)(t1::AbstractTensorMap, t2::AbstractTensorMap)
    codomain(t1) == codomain(t2) ||
        throw(SpaceMismatch("non-matching codomains in t1 \\ t2"))
    if sectortype(t1) === Trivial
        data = block(t1, Trivial()) \ block(t2, Trivial())
        return TensorMap(data, domain(t1)←domain(t2))
    else
        cod = codomain(t1)
        data = SectorDict(c=>block(t1,c) \ block(t2,c) for c in blocksectors(codomain(t1)))
        return TensorMap(data, domain(t1)←domain(t2))
    end
end
function Base.:(/)(t1::AbstractTensorMap, t2::AbstractTensorMap)
    domain(t1) == domain(t2) ||
        throw(SpaceMismatch("non-matching domains in t1 / t2"))
    if sectortype(t1) === Trivial
        data = block(t1, Trivial()) / block(t2, Trivial())
        return TensorMap(data, codomain(t1)←codomain(t2))
    else
        data = SectorDict(c=>block(t1,c) / block(t2,c) for c in blocksectors(domain(t1)))
        return TensorMap(data, codomain(t1)←codomain(t2))
    end
end

# TensorMap exponentation:
function exp!(t::TensorMap)
    domain(t) == codomain(t) ||
        error("Exponentional of a tensor only exist when domain == codomain.")
    for (c,b) in blocks(t)
        copyto!(b, LinearAlgebra.exp!(b))
    end
    return t
end

# functions that map ℝ to (a subset of) ℝ
for f in (:cos, :sin, :tan, :cot, :cosh, :sinh, :tanh, :coth, :atan, :acot, :asinh)
    sf = string(f)
    @eval function Base.$f(t::AbstractTensorMap)
        domain(t) == codomain(t) ||
            error("$sf of a tensor only exist when domain == codomain.")
        G = sectortype(t)
        T = similarstoragetype(t, float(eltype(t)))
        if sectortype(t) === Trivial
            local data::T
            if eltype(t) <: Real
                data = real($f(block(t, Trivial())))
            else
                data = $f(block(t, Trivial()))
            end
            return TensorMap(data, codomain(t), domain(t))
        else
            if eltype(t) <: Real
                datadict = SectorDict{G,T}(c=>real($f(b)) for (c,b) in blocks(t))
            else
                datadict = SectorDict{G,T}(c=>$f(b) for (c,b) in blocks(t))
            end
            return TensorMap(datadict, codomain(t), domain(t))
        end
    end
end
# functions that don't map ℝ to (a subset of) ℝ
for f in (:sqrt, :log, :asin, :acos, :acosh, :atanh, :acoth)
    sf = string(f)
    @eval function Base.$f(t::AbstractTensorMap)
        domain(t) == codomain(t) ||
            error("$sf of a tensor only exist when domain == codomain.")
        G = sectortype(t)
        T = similarstoragetype(t, complex(float(eltype(t))))
        if sectortype(t) === Trivial
            data::T = $f(block(t, Trivial()))
            return TensorMap(data, codomain(t), domain(t))
        else
            datadict = SectorDict{G,T}(c=>$f(b) for (c,b) in blocks(t))
            return TensorMap(datadict, codomain(t), domain(t))
        end
    end
end

# # concatenate tensors
# function concatenate(t1::AbstractTensorMap{S}, ts::AbstractTensorMap{S}...; direction::Symbol) where {S}
#     if direction = :domain
#         numin(t1) == 1 || throw(SpaceMismatch("concatenation along domain"))
#         for t2 in ts
#             domain(t1) == domain(t2) || throw(SpaceMismatch())
#         end
#         t = t1
#         for t2 in ts
#             cat(t1)

function catdomain(t1::AbstractTensorMap{S,N₁,1}, t2::AbstractTensorMap{S,N₁,1}) where {S,N₁}
    codomain(t1) == codomain(t2) || throw(SpaceMismatch())

    V1, = domain(t1)
    V2, = domain(t2)
    isdual(V1) == isdual(V2) ||
        throw(SpaceMismatch("cannot hcat tensors whose domain has non-matching duality"))

    V = V1 ⊕ V2
    t = TensorMap(undef, promote_type(eltype(t1), eltype(t2)), codomain(t1), V)
    for c in sectors(V)
        block(t, c)[:,1:dim(V1, c)] .= block(t1, c)
        block(t, c)[:,dim(V1, c) .+ (1:dim(V2, c))] .= block(t2, c)
    end
    return t
end
function catcodomain(t1::AbstractTensorMap{S,1,N₂}, t2::AbstractTensorMap{S,1,N₂}) where {S,N₂}
    domain(t1) == domain(t2) || throw(SpaceMismatch())

    V1, = codomain(t1)
    V2, = codomain(t2)
    isdual(V1) == isdual(V2) ||
        throw(SpaceMismatch("cannot hcat tensors whose domain has non-matching duality"))

    V = V1 ⊕ V2
    t = TensorMap(undef, promote_type(eltype(t1),eltype(t2)), V, domain(t1))
    for c in sectors(V)
        block(t, c)[1:dim(V1, c),:] .= block(t1, c)
        block(t, c)[dim(V1,c) .+ (1:dim(V2, c)),:] .= block(t2, c)
    end
    return t
end

# tensor product of tensors
"""
    ⊗(t1::AbstractTensorMap{S}, t2::AbstractTensorMap{S}, ...) -> TensorMap{S}

Compute the tensor product between two `AbstractTensorMap` instances, which results in a
new `TensorMap` instance whose codomain is `codomain(t1) ⊗ codomain(t2)` and whose domain
is `domain(t1) ⊗ domain(t2)`.
"""
function ⊗(t1::AbstractTensorMap{S}, t2::AbstractTensorMap{S}) where S
    cod1, cod2 = codomain(t1), codomain(t2)
    dom1, dom2 = domain(t1), domain(t2)
    cod = cod1 ⊗ cod2
    dom = dom1 ⊗ dom2
    t = TensorMap(zeros, promote_type(eltype(t1), eltype(t2)), cod, dom)
    if sectortype(S) === Trivial
        d1 = dim(cod1)
        d2 = dim(cod2)
        d3 = dim(dom1)
        d4 = dim(dom2)
        m1 = reshape(t1[], (d1, 1, d3, 1))
        m2 = reshape(t2[], (1, d2, 1, d4))
        m = reshape(t[], (d1, d2, d3, d4))
        m .= m1 .* m2
    else
        for (f1l, f1r) in fusiontrees(t1)
            for (f2l, f2r) in fusiontrees(t2)
                c1 = f1l.coupled # = f1r.coupled
                c2 = f2l.coupled # = f2r.coupled
                for c in c1 ⊗ c2
                    degeneracyiter = FusionStyle(c) isa DegenerateNonAbelian ?
                                        (1:Nsymbol(c1, c2, c)) : (nothing,)
                    for μ in degeneracyiter
                        for (fl, coeff1) in merge(f1l, f2l, c, μ)
                            for (fr, coeff2) in merge(f1r, f2r, c, μ)
                                d1 = dim(cod1, f1l.uncoupled)
                                d2 = dim(cod2, f2l.uncoupled)
                                d3 = dim(dom1, f1r.uncoupled)
                                d4 = dim(dom2, f2r.uncoupled)
                                m1 = reshape(t1[f1l,f1r], (d1, 1, d3, 1))
                                m2 = reshape(t2[f2l,f2r], (1, d2, 1, d4))
                                m = reshape(t[fl, fr], (d1, d2, d3, d4))
                                m .+= coeff1 .* coeff2 .* m1 .* m2
                            end
                        end
                    end
                end
            end
        end
    end
    return t
end


module Bigsimr

using Base.Threads: @threads
using Distributions
using FastGaussQuadrature: gausshermite
using HypergeometricFunctions: _₂F₁
using IntervalArithmetic: interval, mid
using IntervalRootFinding: roots, Krawczyk
using IterTools: subsets
using LinearAlgebra
using LsqFit: curve_fit, coef
using PDMats
using Polynomials: Polynomial, derivative
using QuadGK: quadgk
using SharedArrays
using SpecialFunctions: erfc, erfcinv
using StatsBase: corspearman, corkendall

import Distributions: mean, std, quantile, cdf, pdf, var, params
import LinearAlgebra: diag, inv, logdet
import PDMats: dim, quad, quad!, invquad!, invquad, pdadd, pdadd!,
    X_A_Xt, Xt_A_X, X_invA_Xt, Xt_invA_X,
    whiten!, unwhiten!
import Statistics: cor, clampcor

struct ValidCorrelationError <: Exception end

abstract type Correlation end
"""
    Pearson <: Correlation

Pearson's ``r`` product-moment correlation
"""
struct Pearson <: Correlation end
"""
    Spearman <: Correlation

Spearman's ``ρ`` rank correlation
"""
struct Spearman <: Correlation end
"""
    Kendall <: Correlation

Kendall's ``τ`` rank correlation
"""
struct Kendall <: Correlation end

export rvec, rmvn
export pearson_match, pearson_bounds
export Correlation, Pearson, Spearman, Kendall
export PDCorMat
export cor, cor_fast
export cor_nearPD, cor_fastPD, cor_fastPD!
export cor_randPD, cor_randPSD
export cor_convert, cor_bounds, cor_constrain, cor_constrain!
export cov2cor, cov2cor!, clamp, cor_clamp
export iscorrelation

const UD  = UnivariateDistribution
const CUD = ContinuousUnivariateDistribution
const DUD = DiscreteUnivariateDistribution

const sqrt2 = sqrt(2)
const invsqrt2 = inv(sqrt(2))
const invsqrtpi = inv(sqrt(π))
const invsqrt2π = inv(sqrt(2π))

include("utils.jl")

include("PDCorMat.jl")

include("RandomVector/rvec.jl")
include("RandomVector/rmvn.jl")
include("RandomVector/utils.jl")

include("Correlation/cor_bounds.jl")
include("Correlation/fast_pos_def.jl")
include("Correlation/nearest_pos_def.jl")
include("Correlation/random.jl")
include("Correlation/utils.jl")

include("PearsonMatching/pearson_match.jl")
include("PearsonMatching/pearson_bounds.jl")
include("PearsonMatching/utils.jl")

include("GSDist/GSDist.jl")
include("GSDist/utils.jl")

include("precompile.jl")

end


todisplay = VERSION <= VersionNumber("1.2") ? true : false

using CAGD
using SparseArrays
using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation

if todisplay
	using ViewerGL
	GL = ViewerGL
	include("../examples/3d/views.jl")
end

V1, (VV,EV,FV,CV) = Lar.simplex(3, true)
tetra1 = V1, EV, FV, CV
V2 = [
    0.0 0.5 0.1 0.1
    0.0 0.1 0.5 0.1
    0.0 0.1 0.1 0.5
]
tetra2 = V2, EV, FV, CV
twotetra = Lar.Struct([ tetra1, tetra2 ])
V,EV,FV,CV = Lar.struct2lar(twotetra)
model = CAGD.Model(V);
CAGD.addModelCells!(model, 1, convert(Lar.ChainOp, Lar.coboundary_0(EV::Lar.Cells)))
CAGD.addModelCells!(model, 2, Lar.coboundary_1(V, FV::Lar.Cells, EV::Lar.Cells))

# Spatial Arrangement
split_model = CAGD.facesplitting(model)

if todisplay  displayModel(split_model)  end

congr_model = CAGD.mergeModelVertices(split_model, signed_merge=true)

if todisplay  displayModel(congr_model)  end

gift_model = deepcopy(congr_model)
FCcycles, bicon_comps = CAGD.tgw(congr_model, 3)
CAGD.addModelCells!(gift_model, 3, convert(Lar.ChainOp, FCcycles'))

if todisplay  viewExplode(gift_model)  end

externals = [bc[CAGD.getExternalBoundary(congr_model, 3, FCcycles, bc)] for bc in bicon_comps]
internals = [setdiff(bicon_comps[i], externals[i]) for i = 1 : length(externals)]
assembled_model = deepcopy(congr_model)
## To Fix
FC = FCcycles[:, internals...]
JK = ones(Int8, length(externals))
FC = [FCcycles * SparseArrays.sparse(externals, JK, JK, size(FCcycles, 2), 1) FC]
CAGD.addModelCells!(assembled_model, 3, convert(Lar.ChainOp, FC'))

if todisplay  viewExplode(assembled_model)  end