module DirectDiffTest

using Test, Random, LinearAlgebra, SparseArrays
using Distributions
import CovSel, ProximalBase, CoordinateDescent

Random.seed!(123)

function genData(p)
  Sigmax = Matrix(1.0I, p, p)
  Sigmay = zeros(p,p)
  rho = 0.7
  for i=1:p
    for j=1:p
      Sigmay[i,j]=rho^abs(i-j)
    end
  end
  sqmy = sqrt(Sigmay)
  n = 1000
  X = randn(n,p)
  Y = randn(n,p) * sqmy;
  hSx = cov(X)
  hSy = cov(Y)
  hSx, hSy
end

@testset "direct_difference_estimation" begin

  @testset "small" begin
    # simple model that differes in only one edge
    p = 10
    hSx, hSy = genData(p)

    tmp = ones(p,p)
    for i=1:5

      # λ = 0.2
      λ = rand(Uniform(0.05, 0.3))
      g = ProximalBase.ProxL1(λ)

      solShoot = CovSel.Alt.differencePrecisionNaive(hSx, hSy, λ, tmp)
      solShoot1 = CovSel.differencePrecisionActiveShooting(Symmetric(hSx), Symmetric(hSy), g)

      @test convert(Matrix, solShoot1) ≈ convert(Matrix, solShoot) atol=1e-5
    end
  end

  @testset "loss" begin
    for rep=1:5
      p = 10
      hSx, hSy = genData(p)

      A = sprand(p, p, 0.15)
      A = (A + A') / 2.
      As = ProximalBase.SymmetricSparseIterate(A)

      @test CovSel.diffLoss(Symmetric(hSx), As, Symmetric(hSy), 2)  ≈ norm(hSx*A*hSy, 2) atol=1e-12
      @test CovSel.diffLoss(Symmetric(hSx), As, Symmetric(hSy), Inf) ≈ norm(hSx*A*hSy, Inf) atol=1e-12
      @test_throws ArgumentError CovSel.diffLoss(Symmetric(hSx), As, Symmetric(hSy), 3)
    end
  end

  @testset "refit" begin
      for rep=1:50
        p = 3
        hSx, hSy = genData(p)

        S = sprand(p, p, 0.2)
        S = S + S' / 2
        indS = findall(x->x!=0, S)
        ilin = LinearIndices(S)[indS]

        opt = CoordinateDescent.CDOptions(;maxIter=5000, optTol=1e-12, randomize=true)
        x = CovSel.differencePrecisionRefit(Symmetric(hSx), Symmetric(hSy), indS)

        x1 = spzeros(p, p)
        x1[ilin] = (0.5 * (kron(hSy, hSx)[ilin, ilin] + kron(hSx, hSy)[ilin, ilin])) \ (hSy - hSx)[ilin]
        @test convert(Matrix, x1) ≈ convert(Matrix, x) atol = 1e-4
      end
  end

end

end


# make QuasiNewton{SR1, approx} callable and then pass that as the
# "update" parameter
# the minimize can take a single SR1 input as -> QuasiNewton{SR1, Approx}
struct SR1{T1} <: QuasiNewton{T1}
   approx::T1
end
function update(H, s, y, scheme::SR1{<:InverseApprox})
   sHy = s - H*y
   d_sHy_y = inv(dot(sHy, y))
   if d_sHy_y > 1e7
      H = H + d_sHy_y*sHy*sHy'
   end
   H
end
function update(B, s, y, scheme::SR1{<:DirectApprox})
   yBs = y - B*s
   d_yBs_s = inv(dot(yBs, s))
   if d_yBs_s > 1e7
      B = B + d_yBs_s*yBs*yBs'
   end
   B
end
function update!(H, s, y, scheme::SR1{<:InverseApprox})
   sHy = s - H*y
   d_sHy_y = inv(dot(sHy, y))
   if d_sHy_y > 1e7
      H .= H .+ d_sHy_y*sHy*sHy'
   end
   H
end
function update!(B, s, y, scheme::SR1{<:DirectApprox})
   yBs = y - B*s
   d_yBs_s = inv(dot(yBs, s))
   if d_yBs_s > 1e7
      B .= B .+ d_yBs_s*yBs*yBs'
   end
   B
end
function update!(A::UniformScaling, s, y, scheme::SR1{<:InverseApprox})
   update(A, s, y, scheme)
end
function update!(A::UniformScaling, s, y, scheme::SR1{<:DirectApprox})
   update(A, s, y, scheme)
end

function find_direction(A, scheme::SR1, ::DirectApprox, ∇f)
   -(A\∇f)
end
function find_direction!(d, B, scheme::SR1, ::DirectApprox, ∇f)
   d .= -(B\∇f)
   d
end


using OrdinaryDiffEq, RecursiveArrayTools, LinearAlgebra, Test

# Define the constants for the PDE
const α₂ = 1.0
const α₃ = 1.0
const β₁ = 1.0
const β₂ = 1.0
const β₃ = 1.0
const r₁ = 1.0
const r₂ = 1.0
const D = 100.0
const γ₁ = 0.1
const γ₂ = 0.1
const γ₃ = 0.1
const N = 256
const X = reshape([i for i in 1:N for j in 1:N],N,N)
const Y = reshape([j for i in 1:N for j in 1:N],N,N)
const α₁ = 1.0.*(X.>=4*N/5)

const Mx = Tridiagonal([1.0 for i in 1:N-1],[-2.0 for i in 1:N],[1.0 for i in 1:N-1])
const My = copy(Mx)
Mx[2,1] = 2.0
Mx[end-1,end] = 2.0
My[1,2] = 2.0
My[end,end-1] = 2.0

# Define the initial condition as normal arrays
u0 = zeros(N,N,3)

const MyA = zeros(N,N);
const AMx = zeros(N,N);
const DA = zeros(N,N);
# Define the discretized PDE as an ODE function
function f(du,u,p,t)
   A = @view  u[:,:,1]
   B = @view  u[:,:,2]
   C = @view  u[:,:,3]
  dA = @view du[:,:,1]
  dB = @view du[:,:,2]
  dC = @view du[:,:,3]
  mul!(MyA,My,A)
  mul!(AMx,A,Mx)
  @. DA = D*(MyA + AMx)
  @. dA = DA + α₁ - β₁*A - r₁*A*B + r₂*C
  @. dB = α₂ - β₂*B - r₁*A*B + r₂*C
  @. dC = α₃ - β₃*C + r₁*A*B - r₂*C
end

# Solve the ODE
prob = ODEProblem(f,u0,(0.0,100.0))
sol = solve(prob,BS3(),progress=true,save_everystep=false,save_start=false)
sol = solve(prob,ROCK2(),progress=true,save_everystep=false,save_start=false)

println("CPU Times")
println("BS3")
@time sol = solve(prob,BS3(),progress=true,save_everystep=false,save_start=false)
println("ROCK2")
@time sol = solve(prob,ROCK2(),progress=true,save_everystep=false,save_start=false)

using CuArrays
gu0 = CuArray(Float32.(u0))
const gMx = CuArray(Float32.(Mx))
const gMy = CuArray(Float32.(My))
const gα₁ = CuArray(Float32.(α₁))

const gMyA = CuArray(zeros(Float32,N,N))
const gAMx = CuArray(zeros(Float32,N,N))
const gDA = CuArray(zeros(Float32,N,N))

function gf(du,u,p,t)
   A = @view  u[:,:,1]
   B = @view  u[:,:,2]
   C = @view  u[:,:,3]
  dA = @view du[:,:,1]
  dB = @view du[:,:,2]
  dC = @view du[:,:,3]
  mul!(gMyA,gMy,A)
  mul!(gAMx,A,gMx)
  @. gDA = D*(gMyA + gAMx)
  @. dA = gDA + gα₁ - β₁*A - r₁*A*B + r₂*C
  @. dB = α₂ - β₂*B - r₁*A*B + r₂*C
  @. dC = α₃ - β₃*C + r₁*A*B - r₂*C
end

prob2 = ODEProblem(gf,gu0,(0.0,100.0))
CuArrays.allowscalar(false)
sol = solve(prob2,BS3(),save_everystep=false,save_start=false)
sol = solve(prob2,ROCK2(),save_everystep=false,save_start=false)
@test sol.t[end] == 100.0

println("GPU Times")
println("BS3")
@time sol = solve(prob2,BS3(),progress=true,save_everystep=false,save_start=false)
println("ROCK2")
@time sol = solve(prob2,ROCK2(),progress=true,save_everystep=false,save_start=false)


using Statistics:quantile

"""
    classify_quantile(threshold)

Create a percentile-based classifiction function that converts `scores_train::Scores` and `scores_test::Scores` to an
array of classes with `"normal"` indicating normal data and `"outlier"` indicating outliers. The conversion is based on
percentiles of the training data, i.e. all datapoints above the `threshold` percentile are considered outliers.

Parameters
----------
    threshold::Real
The score threshold (number between 0 and 1) used to classify the samples into inliers and outliers.

    scores::Tuple{Scores, Scores}
A tuple consisting of two vectors representing training and test scores.

Returns
----------
    classes::AbstractVector{String}
The vector of classes consisting of `"outlier"` and `"normal"` elements.

Examples
----------
    classify = classify_quantile(0.9)
    scores_train, scores_test = ([1, 2, 3], [4, 3, 2])
    classify(scores_train, scores_train) # ["inlier", "inlier", "outlier"]
    classify(scores_train, scores_test) # ["outlier", "outlier", "inlier"]
"""
function classify_quantile(threshold::Real)
    function percentile(scores::Tuple{Scores, Scores})::Tuple{Labels, Labels}
        scores_train, scores_test = scores
        @assert 0 < threshold < 1
        t = quantile(scores_train, threshold)
        f = scores -> ifelse.(scores .> t, CLASS_OUTLIER, CLASS_NORMAL)
        f(scores_train), f(scores_test)
    end
    return percentile
end


##################################
# Operador de cruce  BLX-alpha
##############################
using Random

"""
    BLX(C1, C2, 𝛂)
Operador de cruce BLX, dados dos cromosomas
devuele dos descencientes.
Si C1 == C2 entoneces el cruce es la identidad
"""
function BLX(C1, C2, alpha=0.3)
    l = length(C1)
    H = Array{Float64, 2}(undef, 2, l)
    for j in 1:l
        c_max = maximum([C1[j], C2[j]])
        c_min = minimum([C1[j], C2[j]])
        I = c_max - c_min
        # auxiliales
        longitud_intervalo = I + 2*I* alpha
        for i in 1:2
            H[i,j] = min(1.0,max(0,rand()*longitud_intervalo + (c_min-I*alpha)))
        end
    end
    return [H[1,:], H[2,:]]
end


@testset "planck_spectra" begin

    ν_grid = collect((1e7/778):0.015:(1e7/755))
    T = 3777

    println("Verifying planck function correctness...")

    Ts = [290, 1000, 3777]
    peak_ν = [568.693, 1961.01, 7406.74]
    peak_λ = [9.99225, 2.89775, 0.76721]
    peak_L_ν = [138.636, 5684.38, 306284]
    peak_L_λ = [8.40098, 4095.81, 3.14829e6]

    for i in 1:length(Ts)

        T_curr = Ts[i]

        peak_ν_true = peak_ν[i]
        peak_L_true = peak_L_ν[i]

        peak_calc = findmax(planck_spectrum_wn(T_curr)[:,2])

        @test isapprox(peak_calc[1], peak_L_true, rtol=0.0001, atol=1)
        @test isapprox(peak_calc[2], peak_ν_true, rtol=0.0001, atol=1)
        @test isapprox(peak_L_λ[i], planck_spectrum_wl(T_curr, [peak_λ[i]])[1], rtol=0.0001, atol=1)

    end

end


path = "docs/src/assets/"

using ISA, Plots
fs = 16_000
t = 0.0:1/fs:3.0
𝐶₀ = AMFMtriplet(t->0.2+0.8cos(11t), t->200.0, 0.0)
𝐶₁ = AMFMtriplet(t->exp(-abs(t/3)), t->100t, 0.1)
𝐶₂ = AMFMtriplet(t->𝒩ᵤ(t; μ=1.5, σ=1.0), t->150 + 125sin(5t), π)
𝐶₃ = AMFMtriplet(t->u(t-1.725)-u(t-2.475), t->50, 0.0)
𝑆 = compSet([𝐶₀,𝐶₁,𝐶₂,𝐶₃])
z = AMFMmodel(𝑆)

plot(𝐶₀,timeaxis=t)
plot!(𝐶₁,timeaxis=t)
plot!(𝐶₂,timeaxis=t)
plot!(𝐶₃,linealpha = sign.(abs.(𝐶₃.a.(t))),timeaxis = t)
png(path*"paperFigureIS.png")

plot(z,timeaxis=t)
png(path*"paperFigureArgand.png")


#=
Author: Daniel Carrera (dcarrera@gmail.com)

Compute the habitable zone limits from Kopparapu et al. (2013).
=#

"""
Computes all the limits for the habitable zone from Kopparapu et al. (2013).
This function uses the updated coefficients from their website.

Input:

    Teff       Star's effective temperature (K)
	L_star     Star's luminosity (L_sun)

Example:

	Teff   = 3700  # Temperature of a 0.5 M_sun star.
	L_star = 0.04  # Luminosity of a 0.5 M_sun star.

	limits = habitable_zone(Teff, L_star)

	@info("Recent Venus                  = \$(limits[1]) AU")
	@info("Runaway Greenhouse            = \$(limits[2]) AU")
	@info("Maximum Greenhouse            = \$(limits[3]) AU")
	@info("Early Mars                    = \$(limits[4]) AU")
	@info("Runaway Greenhouse for 5.0 ME = \$(limits[5]) AU")
	@info("Runaway Greenhouse for 0.1 ME = \$(limits[6]) AU")

Citations:

	Kopparapu et al. (2013)
	http://adsabs.harvard.edu/abs/2013ApJ...765..131K

	Erratum
	http://adsabs.harvard.edu/abs/2013ApJ...770...82K

	Updated values
	https://depts.washington.edu/naivpl/sites/default/files/hz.shtml
"""
function habitable_zone(Teff::Number, L_star::Number)
	@assert(Teff >= 2600)
	@assert(Teff <= 7200)

	Ts = Teff - 5780

	# Table of coefficients from:
	# https://depts.washington.edu/naivpl/sites/default/files/hz.shtml
	#
	# S0 == S_eff_sun
	#
	# i = 1 --> Recent Venus
	# i = 2 --> Runaway Greenhouse
	# i = 3 --> Maximum Greenhouse
	# i = 4 --> Early Mars
	# i = 5 --> Runaway Greenhouse for 5 ME
	# i = 6 --> Runaway Greenhouse for 0.1 ME
	S0 = [ 1.77600e+00   1.10700e+00   3.56000e-01   3.20000e-01   1.18800e+00   9.90000e-01]
	a  = [ 2.13600e-04   1.33200e-04   6.17100e-05   5.54700e-05   1.43300e-04   1.20900e-04]
	b  = [ 2.53300e-08   1.58000e-08   1.69800e-09   1.52600e-09   1.70700e-08   1.40400e-08]
	c  = [-1.33200e-11  -8.30800e-12  -3.19800e-12  -2.87400e-12  -8.96800e-12  -7.41800e-12]
	d  = [-3.09700e-15  -1.93100e-15  -5.57500e-16  -5.01100e-16  -2.08400e-15  -1.71300e-15]

	limits = zeros(6)
	for j in 1:6
		Seff = S0[j] + a[j]*Ts + b[j]*Ts^2 + c[j]*Ts^3 + d[j]*Ts^4

		limits[j] = sqrt(L_star / Seff) # AU
	end

	return limits
end


using Pkg
Pkg.activate(".")
using Printf
push!(LOAD_PATH,"../src/")
using QuantumComputer

# configurable runner is below this function
function grover(qubit_count, target_input, sample_size)
    @printf("searching %d inputs for target of %d using Grover's algorithm...\n", 2^qubit_count, target_input)

    println("building circuit...")
    classical_register = QuantumComputer.ClassicalRegister(qubit_count)
    register = QuantumComputer.Register(qubit_count + 1, 1)
    superposition = QuantumComputer.Superposition(register.qubits)

    circuit_oracle = QuantumComputer.Circuits.identification_oracle(qubit_count + 1, target_input)
    gate_oracle = QuantumComputer.circuit_convert_to_gate(circuit_oracle)
    circuit_grover = QuantumComputer.Circuits.grover(gate_oracle, false)
    measurement = QuantumComputer.Measurement(Array(1:qubit_count), Array(1:qubit_count), sample_size)

    circuit = QuantumComputer.Circuit()
    QuantumComputer.add_subcircuit_to_circuit!(circuit, circuit_grover)
    QuantumComputer.add_measurement_to_circuit!(circuit, measurement)

    @printf("sampling (%d times)...\n", sample_size)
    QuantumComputer.apply_circuit_to_superposition!(superposition, circuit, classical_register)

    @printf("classical_register holds value %d\n", classical_register.value)
    classical_register.value == target_input || throw(ErrorException("Grover has failed!"))
    println("success!")
end

# this one is probably fine.
sample_size = 128

qubit_count = length(ARGS) > 0 ? parse(Int64, ARGS[1]) : 10
if length(ARGS) > 1
    QuantumComputer.gate_set_cache_base(ARGS[2])
end
target_input = length(ARGS) > 2 ? parse(Int64, ARGS[3]) : rand(0:(2^qubit_count-1))

grover(qubit_count, target_input, sample_size)


module RulesAdditionMarginalsTest

using Test
using ReactiveMP
using Random
import ReactiveMP: @test_marginalrules

@testset "marginalrules:typeof(+)" begin

    @testset ":in1_in2 (m_out::UnivariateNormalDistributionsFamily, m_in1::UnivariateNormalDistributionsFamily, m_in2::PointMass)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = NormalMeanVariance(3.0, 4.0), m_in1 = NormalMeanVariance(2.0, 2.0), m_in2 = PointMass(2.0)),
                output = (in1 = NormalWeightedMeanPrecision(5/4, 3/4), in2 = PointMass(2.0))
            ),
            (
                input = (m_out = NormalMeanPrecision(3, 4), m_in1 = NormalMeanPrecision(1, 4), m_in2 = PointMass(1.0)),
                output = (in1 = NormalWeightedMeanPrecision(12.0, 8.0), in2 = PointMass(1.0))
            ),
            (
                input = (m_out = NormalWeightedMeanPrecision(3.0, 4.0), m_in1 = NormalWeightedMeanPrecision(1.0, 4.0), m_in2 = PointMass(1.0)),
                output = (in1 = NormalWeightedMeanPrecision(0.0, 8.0), in2 = PointMass(1.0))
            ),
            (
                input = (m_out = NormalMeanPrecision(2.0, 4.0), m_in1 = NormalMeanVariance(1.0, 2.0), m_in2 = PointMass(1.0)),
                output = (in1 = NormalWeightedMeanPrecision(9/2, 9/2), in2 = PointMass(1.0))
            ),
            (
                input = (m_out = NormalMeanVariance(2.0, 4.0), m_in1 = NormalMeanPrecision(1.0, 2.0), m_in2 = PointMass(1.0)),
                output = (in1 = NormalWeightedMeanPrecision(9/4, 9/4), in2 = PointMass(1.0))
            ),
            (
                input = (m_out = NormalMeanPrecision(3.0, 4.0), m_in1 = NormalWeightedMeanPrecision(1.0, 2.0), m_in2 = PointMass(2.0)),
                output = (in1 = NormalWeightedMeanPrecision(5.0, 6.0), in2 = PointMass(2.0))
            ),
            (
                input = (m_out = NormalWeightedMeanPrecision(2.0, 2.0), m_in1 = NormalMeanPrecision(1.0, 2.0), m_in2 = PointMass(-1.0)),
                output = (in1 = NormalWeightedMeanPrecision(6.0, 4.0), in2 = PointMass(-1.0))
            ),
            (
                input = (m_out = NormalMeanVariance(3.0, 3.0), m_in1 = NormalWeightedMeanPrecision(2.0, 1.0), m_in2 = PointMass(2.0)),
                output = (in1 = NormalWeightedMeanPrecision(7/3, 4/3), in2 = PointMass(2.0))
            ),
            (
                input = (m_out = NormalWeightedMeanPrecision(2.0, 4.0), m_in1 = NormalMeanVariance(2.0, 2.0), m_in2 = PointMass(1.0)),
                output = (in1 = NormalWeightedMeanPrecision(-1.0, 9/2), in2 = PointMass(1.0))
            ),

        ]

    end

    @testset ":in1_in2 (m_out::UnivariateNormalDistributionsFamily, m_in1::PointMass, m_in2::UnivariateNormalDistributionsFamily)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = NormalMeanVariance(3.0, 4.0), m_in1 = PointMass(2.0), m_in2 = NormalMeanVariance(2.0, 2.0)),
                output = (in1 = PointMass(2.0), in2 = NormalWeightedMeanPrecision(5/4, 3/4))
            ),
            (
                input = (m_out = NormalMeanPrecision(3, 4), m_in1 = PointMass(1.0), m_in2 = NormalMeanPrecision(1, 4)),
                output = (in1 = PointMass(1.0), in2 = NormalWeightedMeanPrecision(12.0, 8.0))
            ),
            (
                input = (m_out = NormalWeightedMeanPrecision(3.0, 4.0), m_in1 = PointMass(1.0), m_in2 = NormalWeightedMeanPrecision(1.0, 4.0)),
                output = (in1 = PointMass(1.0), in2 = NormalWeightedMeanPrecision(0.0, 8.0))
            ),
            (
                input = (m_out = NormalMeanPrecision(2.0, 4.0), m_in1 = PointMass(1.0), m_in2 = NormalMeanVariance(1.0, 2.0)),
                output = (in1 = PointMass(1.0), in2 = NormalWeightedMeanPrecision(9/2, 9/2))
            ),
            (
                input = (m_out = NormalMeanVariance(2.0, 4.0), m_in1 = PointMass(1.0), m_in2 = NormalMeanPrecision(1.0, 2.0)),
                output = (in1 = PointMass(1.0), in2 = NormalWeightedMeanPrecision(9/4, 9/4))
            ),
            (
                input = (m_out = NormalMeanPrecision(3.0, 4.0), m_in1 = PointMass(2.0), m_in2 = NormalWeightedMeanPrecision(1.0, 2.0)),
                output = (in1 = PointMass(2.0), in2 = NormalWeightedMeanPrecision(5.0, 6.0))
            ),
            (
                input = (m_out = NormalWeightedMeanPrecision(2.0, 2.0), m_in1 = PointMass(-1.0), m_in2 = NormalMeanPrecision(1.0, 2.0)),
                output = (in1 = PointMass(-1.0), in2 = NormalWeightedMeanPrecision(6.0, 4.0))
            ),
            (
                input = (m_out = NormalMeanVariance(3.0, 3.0), m_in1 = PointMass(2.0), m_in2 = NormalWeightedMeanPrecision(2.0, 1.0)),
                output = (in1 = PointMass(2.0), in2 = NormalWeightedMeanPrecision(7/3, 4/3))
            ),
            (
                input = (m_out = NormalWeightedMeanPrecision(2.0, 4.0), m_in1 = PointMass(1.0), m_in2 = NormalMeanVariance(2.0, 2.0)),
                output = (in1 = PointMass(1.0), in2 = NormalWeightedMeanPrecision(-1.0, 9/2))
            ),

        ]

    end

    @testset ":in1_in2 (m_out::MvNormalMeanCovariance, m_in1::MultivariateNormalDistributionsFamily, m_in2::PointMass)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = MvNormalMeanCovariance([2.0, 2.0], [2.0 0.0; 0.0 2.0]), m_in1 = MvNormalMeanCovariance([1.0, 3.0], [3.0 2.0; 2.0 4.0]), m_in2 = PointMass([1.0, 1.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([1/4, 11/8], [1.0 -1/4; -1/4 7/8]), in2 = PointMass([1.0, 1.0]))
            ),
            (
                input = (m_out = MvNormalMeanCovariance([3.0, 2.0], [2.0 0.0; 0.0 2.0]), m_in1 = MvNormalMeanPrecision([1.0, 1.0], [2.0 1.0; 1.0 2.0]), m_in2 = PointMass([1.0, 1.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([4.0, 7/2], [5/2 1.0; 1.0 5/2]), in2 = PointMass([1.0, 1.0]))
            ),
            (
                input = (m_out = MvNormalMeanCovariance([3.0, 2.0], [4.0 2.0; 2.0 4.0]), m_in1 = MvNormalWeightedMeanPrecision([1.0, 2.0], [2.0 0.0; 0.0 2.0]), m_in2 = PointMass([2.0, 1.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([7/6, 13/6], [7/3 -1/6; -1/6 7/3]), in2 = PointMass([2.0, 1.0]))
            ),

        ]

    end

    @testset ":in1_in2 (m_out::MvNormalMeanPrecision, m_in1::MultivariateNormalDistributionsFamily, m_in2::PointMass)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = MvNormalMeanPrecision([2.0, 2.0], [1.0 0.0; 0.0 1.0]), m_in1 = MvNormalMeanCovariance([2.0, 3.0], [3.0 2.0; 2.0 4.0]), m_in2 = PointMass([1.0, -1.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([5/4, 29/8], [3/2 -1/4; -1/4 11/8]), in2 = PointMass([1.0, -1.0]))
            ),
            (
                input = (m_out = MvNormalMeanPrecision([1.0, 1.0], [3.0 1.0; 1.0 3.0]), m_in1 = MvNormalMeanPrecision([1.0, 1.0], [2.0 1.0; 1.0 2.0]), m_in2 = PointMass([2.0, 2.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([-1.0, -1.0], [5.0 2.0; 2.0 5.0]), in2 = PointMass([2.0, 2.0]))
            ),
            (
                input = (m_out = MvNormalMeanPrecision([3.0, 2.0], [4.0 2.0; 2.0 4.0]), m_in1 = MvNormalWeightedMeanPrecision([1.0, 2.0], [3.0 1.0; 1.0 1.0]), m_in2 = PointMass([-2.0, 1.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([23.0, 16.0], [7.0  3.0; 3.0 5.0]), in2 = PointMass([-2.0, 1.0]))
            ),

        ]

    end

    @testset ":in1_in2 (m_out::MvNormalWeightedMeanPrecision, m_in1::MultivariateNormalDistributionsFamily, m_in2::PointMass)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = MvNormalWeightedMeanPrecision([2.0, 2.0], [1.0 0.0; 0.0 1.0]), m_in1 = MvNormalMeanCovariance([2.0, 3.0], [3.0 2.0; 2.0 4.0]), m_in2 = PointMass([1.0, -1.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([5/4, 29/8], [3/2 -1/4; -1/4 11/8]), in2 = PointMass([1.0, -1.0]))
            ),
            (
                input = (m_out = MvNormalWeightedMeanPrecision([1.0, 1.0], [3.0 1.0; 1.0 2.0]), m_in1 = MvNormalMeanPrecision([1.0, 1.0], [2.0 1.0; 1.0 2.0]), m_in2 = PointMass([2.0, 2.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([-4.0, -2.0], [5.0 2.0; 2.0 4.0]), in2 = PointMass([2.0, 2.0]))
            ),
            (
                input = (m_out = MvNormalWeightedMeanPrecision([1.0, 2.0], [4.0 1.0; 1.0 4.0]), m_in1 = MvNormalWeightedMeanPrecision([1.0, 1.0], [3.0 0.0; 0.0 2.0]), m_in2 = PointMass([-1.0, 1.0])),
                output = (in1 = MvNormalWeightedMeanPrecision([5.0, 0.0], [7.0  1.0; 1.0 6.0]), in2 = PointMass([-1.0, 1.0]))
            ),

        ]

    end

    @testset ":in1_in2 (m_out::MvNormalMeanCovariance, m_in1::PointMass, m_in2::PointMassMultivariateNormalDistributionsFamily)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = MvNormalMeanCovariance([2.0, 2.0], [2.0 0.0; 0.0 2.0]), m_in1 = PointMass([1.0, 1.0]), m_in2 = MvNormalMeanCovariance([1.0, 3.0], [3.0 2.0; 2.0 4.0])),
                output = (in1 = PointMass([1.0, 1.0]), in2 = MvNormalWeightedMeanPrecision([1/4, 11/8], [1.0 -1/4; -1/4 7/8]))
            ),
            (
                input = (m_out = MvNormalMeanCovariance([3.0, 2.0], [2.0 0.0; 0.0 2.0]), m_in1 = PointMass([1.0, 1.0]), m_in2 = MvNormalMeanPrecision([1.0, 1.0], [2.0 1.0; 1.0 2.0])),
                output = (in1 = PointMass([1.0, 1.0]), in2 = MvNormalWeightedMeanPrecision([4.0, 7/2], [5/2 1.0; 1.0 5/2]))
            ),
            (
                input = (m_out = MvNormalMeanCovariance([3.0, 2.0], [4.0 2.0; 2.0 4.0]), m_in1 = PointMass([2.0, 1.0]), m_in2 = MvNormalWeightedMeanPrecision([1.0, 2.0], [2.0 0.0; 0.0 2.0])),
                output = (in1 = PointMass([2.0, 1.0]), in2 = MvNormalWeightedMeanPrecision([7/6, 13/6], [7/3 -1/6; -1/6 7/3]))
            ),

        ]

    end

    @testset ":in1_in2 (m_out::MvNormalMeanPrecision, m_in1::, m_in2::MultivariateNormalDistributionsFamilyPointMass)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = MvNormalMeanPrecision([2.0, 2.0], [1.0 0.0; 0.0 1.0]), m_in1 = PointMass([1.0, -1.0]), m_in2 = MvNormalMeanCovariance([2.0, 3.0], [3.0 2.0; 2.0 4.0])),
                output = (in1 = PointMass([1.0, -1.0]), in2 = MvNormalWeightedMeanPrecision([5/4, 29/8], [3/2 -1/4; -1/4 11/8]))
            ),
            (
                input = (m_out = MvNormalMeanPrecision([1.0, 1.0], [3.0 1.0; 1.0 3.0]), m_in1 = PointMass([2.0, 2.0]), m_in2 = MvNormalMeanPrecision([1.0, 1.0], [2.0 1.0; 1.0 2.0])),
                output = (in1 = PointMass([2.0, 2.0]), in2 = MvNormalWeightedMeanPrecision([-1.0, -1.0], [5.0 2.0; 2.0 5.0]))
            ),
            (
                input = (m_out = MvNormalMeanPrecision([3.0, 2.0], [4.0 2.0; 2.0 4.0]), m_in1 = PointMass([-2.0, 1.0]), m_in2 = MvNormalWeightedMeanPrecision([1.0, 2.0], [3.0 1.0; 1.0 1.0])),
                output = (in1 = PointMass([-2.0, 1.0]), in2 = MvNormalWeightedMeanPrecision([23.0, 16.0], [7.0  3.0; 3.0 5.0]))
            ),

        ]

    end

    @testset ":in1_in2 (m_out::MvNormalWeightedMeanPrecision, m_in1::PointMass, m_in2::MultivariateNormalDistributionsFamily)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = MvNormalWeightedMeanPrecision([2.0, 2.0], [1.0 0.0; 0.0 1.0]), m_in1 = PointMass([1.0, -1.0]), m_in2 = MvNormalMeanCovariance([2.0, 3.0], [3.0 2.0; 2.0 4.0])),
                output = (in1 = PointMass([1.0, -1.0]), in2 = MvNormalWeightedMeanPrecision([5/4, 29/8], [3/2 -1/4; -1/4 11/8]))
            ),
            (
                input = (m_out = MvNormalWeightedMeanPrecision([1.0, 1.0], [3.0 1.0; 1.0 2.0]), m_in1 = PointMass([2.0, 2.0]), m_in2 = MvNormalMeanPrecision([1.0, 1.0], [2.0 1.0; 1.0 2.0])),
                output = (in1 = PointMass([2.0, 2.0]), in2 = MvNormalWeightedMeanPrecision([-4.0, -2.0], [5.0 2.0; 2.0 4.0]))
            ),
            (
                input = (m_out = MvNormalWeightedMeanPrecision([1.0, 2.0], [4.0 1.0; 1.0 4.0]), m_in1 = PointMass([-1.0, 1.0]), m_in2 = MvNormalWeightedMeanPrecision([1.0, 1.0], [3.0 0.0; 0.0 2.0])),
                output = (in1 = PointMass([-1.0, 1.0]), in2 = MvNormalWeightedMeanPrecision([5.0, 0.0], [7.0  1.0; 1.0 6.0]))
            ),

        ]

    end

    @testset ":in1_in2 (m_out::NormalDistributionsFamily, m_in1::NormalDistributionsFamily, m_in2::NormalDistributionsFamily)" begin

        @test_marginalrules [ with_float_conversions = true ] typeof(+)(:in1_in2) [
            (
                input = (m_out = NormalMeanVariance(1.0, 2.0), m_in1 = NormalMeanVariance(3.0, 4.0), m_in2 = NormalMeanVariance(5.0, 6.0)),
                output = (MvNormalWeightedMeanPrecision([5/4, 4/3], [3/4 1/2; 1/2 2/3]))
            ),
            (
                input = (m_out = NormalMeanPrecision(1.0, 2.0), m_in1 = NormalMeanPrecision(3.0, 4.0), m_in2 = NormalMeanPrecision(5.0, 6.0)),
                output = (MvNormalWeightedMeanPrecision([14.0, 32.0], [6.0 2.0; 2.0 8.0]))
            ),
            (
                input = (m_out = NormalWeightedMeanPrecision(1.0, 2.0), m_in1 = NormalWeightedMeanPrecision(3.0, 4.0), m_in2 = NormalWeightedMeanPrecision(5.0, 6.0)),
                output = (MvNormalWeightedMeanPrecision([4.0, 6.0], [6.0 2.0; 2.0 8.0]))
            ),
            (
                input = (m_out = NormalMeanVariance(1.0, 2.0), m_in1 = NormalMeanPrecision(3.0, 4.0), m_in2 = NormalWeightedMeanPrecision(5.0, 6.0)),
                output = (MvNormalWeightedMeanPrecision([25/2, 11/2], [9/2 1/2; 1/2 13/2]))
            ),
            (
                input = (m_out = MvNormalMeanCovariance([1.0, 2.0], [3.0 1.0; 1.0 2.0]), m_in1 = MvNormalMeanCovariance([2.0, 3.0], [3.0 1.0; 1.0 2.0]), m_in2 = MvNormalMeanCovariance([1.0, 2.0], [3.0 1.0; 1.0 2.0])),
                output = (MvNormalWeightedMeanPrecision([1/5, 12/5, 0.0, 2.0], [[0.8 -0.4 0.4 -0.2; -0.4 1.2 -0.2 0.6]; [0.4 -0.2 0.8 -0.4; -0.2 0.6 -0.4 1.2]]))
            ),
            (
                input = (m_out = MvNormalMeanPrecision([1.0, 2.0], [3.0 1.0; 1.0 2.0]), m_in1 = MvNormalMeanPrecision([1.0, 2.0], [3.0 1.0; 1.0 2.0]), m_in2 = MvNormalMeanPrecision([1.0, 2.0], [3.0 1.0; 1.0 2.0])),
                output = (MvNormalWeightedMeanPrecision([10.0, 10.0, 10.0, 10.0], [6.0 2.0 3.0 1.0; 2.0 4.0 1.0 2.0; 3.0 1.0 6.0 2.0; 1.0 2.0 2.0 4.0]))
            ),
            (
                input = (m_out = MvNormalWeightedMeanPrecision([1.0, 2.0], [3.0 1.0; 1.0 1.0]), m_in1 = MvNormalWeightedMeanPrecision([1.0, 2.0], [3.0 1.0; 1.0 1.0]), m_in2 = MvNormalWeightedMeanPrecision([1.0, 2.0], [3.0 1.0; 1.0 1.0])),
                output = (MvNormalWeightedMeanPrecision([2.0, 4.0, 2.0, 4.0], [6.0 2.0 3.0 1.0; 2.0 2.0 1.0 1.0; 3.0 1.0 6.0 2.0; 1.0 1.0 2.0 2.0]))
            ),
            (
                input = (m_out = MvNormalMeanCovariance([1.0, 1.0], [3.0 1.0; 1.0 2.0]), m_in1 = MvNormalMeanPrecision([1.0, 1.0], [3.0 1.0; 1.0 2.0]), m_in2 = MvNormalWeightedMeanPrecision([1.0, 1.0], [3.0 1.0; 1.0 2.0])),
                output = (MvNormalWeightedMeanPrecision([21/5, 17/5, 6/5, 7/5], [17/5 4/5 2/5 -1/5; 4/5 13/5 -1/5 3/5; 2/5 -1/5 17/5 4/5; -1/5 3/5 4/5 13/5]))
            ),

        ]

    end

end
end


module Stat31020
using JuMP
using MathProgBase
"""
...
# Arguments
- `x::Array{Float64,1}`: current iterate
- `n::Int64`: number of derivatives to query
- `kkt::Bool`: T/F to return KKT matrix
- `μ::Float64`: objective function multiplier (default = 1.0)
- `λ::Array{Float64,1}`: constraint multiplier (default = [1.0, ..., 1.0])
# Output
- `f::SparseMatrixCSC`: function evaulation
- `g::SparseMatrixCSC`: gradient evaulation
- `H::SparseMatrixCSC`: sparse, symmetric hessian evaulation
- `J::SparseMatrixCSC`: sparse constraint jacobian evaluation
- `KKT::SparseMatrixCSC`: sparse KKT system evaulation
...
"""
function constrained_wrap(x::Array{Float64,1}, nderivs::Int64;
                          kkt=true, μ=nothing, λ=nothing)
    ## setup
    n = length(x)                          ## problem dimension
    m = 0                                  ## number of constraints (TBD below)
    I = 1:n-1                              ## index set

    ## -------------------------------------------------------------------------
    ## TODO: fill in function evaluation
    ## -------------------------------------------------------------------------
    f = sum(x[i]^4 - 2.0*x[i]^2*x[i+1] + x[i]^2 + x[i+1]^2 - 2*x[i] for i=I)
    ## -------------------------------------------------------------------------

    if nderivs == 0
        return f

    elseif nderivs >= 1
        ## initialize JuMP model and MathProgBase interface
        NLmodel = Model()                   ## JuMP object
        @variable(NLmodel, y[1:n])
        ## ---------------------------------------------------------------------
        ## TODO: fill in objective function and constraints
        ## ---------------------------------------------------------------------
        @NLobjective(NLmodel, Min, sum(y[i]^4 -
                                       2.0*y[i]^2*y[i+1] +
                                       y[i]^2 +
                                       y[i+1]^2 -
                                       2*y[i] for i=I))
        @NLconstraint(NLmodel, -(y[1] + 0.25)^2 + 0.75*y[2] >= 0)
        @NLconstraint(NLmodel, -(y[1] + 0.25) + 0.75*y[2]^2 >= 0)
        ## ---------------------------------------------------------------------
        d = JuMP.NLPEvaluator(NLmodel)      ## MPB object
        m = MathProgBase.numconstr(NLmodel) ## number of contraints
        ## initialize dual variable values (if not specified)
        if μ == nothing
            μ = 1.
        end
        if λ == nothing
            λ = ones(m)
        end

        ## evaluate derivatives
        if nderivs == 1
            MathProgBase.initialize(d, [:Grad])
            ## evaluate gradient
            g = zeros(n)
            MathProgBase.eval_grad_f(d, g, x)
            return (f, g)

        elseif nderivs == 2
            MathProgBase.initialize(d, [:Grad, :Hess, :Jac])
            ## evaluate Hessian
            ij_H = MathProgBase.hesslag_structure(d)
            h = ones(length(ij_H[1]))
            MathProgBase.eval_hesslag(d, h, x, μ, λ)
            H = populate_hess_sparse(ij_H[1], ij_H[2], h, n)
            ## evalute gradient (partially pre-computed from above)
            g = zeros(n)
            MathProgBase.eval_grad_f(d, g, x)
            ## evaluate Jacobian (constraints)
            if kkt == true
                ij_J = MathProgBase.jac_structure(d)
                j = ones(length(ij_J[1]))
                MathProgBase.eval_jac_g(d, j, x)
                J = sparse(ij_J[1], ij_J[2], j)
                KKT = [H J'; J zeros(m, m)]
                println(KKT)
                return (f, g, H, J, KKT)
            else
               return (f, g, H)
            end
        end
    end
end

"""
...
# Arguments
- `i::Array{Int64,1}`: row indices
- `j::Array{Int64,1}`: col indices
- `h::Array{Int64,1}`: values s.t. h[1] = H[i[1], j[1]]
- `n::Int64`: dimension of hessian
# Output
- `H::SparseMatrixCSC`: sparse, symmetric hessian
...
"""
function populate_hess_sparse(
    i::Array{Int64,1},
    j::Array{Int64,1},
    h::Array{Float64,1},
    n::Int64)
    ## build matrix; probably a better way to do this...
    H = sparse([i;j], [j;i], [h;h], n, n, +)  ## NOTE: symmetrizing doubles the diagonal
    H = H - Diagonal(H)/2.                    ## NOTE: required removal of 1/2 digonal
    return H
end
end # module


"""

  `module Arcus`

Module Arcus provides a data type Arc, representing points on the unit circle,
or an agular value in the range `(-π,π]`.
For a point given as `(sin(α), cos(α))`, the absolutely lower value of both is stored.
Quadrant information is stored by multiplying a corresponding power of two.
For the first quadrant with `α` in  `[-π/4,π/4]`, the Float64 value of `sin(α)` is
stored unchanged.

Usage:

  `using Arcus`

  `a = Arc(radians)`

  `b = Arc(s, c)`

Trigonometric functions and arithmetic operations are available.
Multiplication and division only with `Real` values.
"""
module Arcus

export Arc, Arcd, deg, rad, sincos

import Base: convert, show, read, write, zero, bits, isapprox
import Base: sin, csc, cos, sec, tan, cot
import Base: (+), (-), (*), (/)

#-v0.5 bitstype 64 Arc
primitive type Arc 64 end

const T = Float64

const expomax = exponent(realmax(0.0)) ÷ 3 # use all available exponent space
const fmin = 2.0^-expomax
const f0 = 1.0                # quadrant 0 - stored values abs < f0
const f1 = 2.0^expomax        # quadrant 1 - f0 ≤ values < f1
const f2 = f1 * f1            # quadrant 2 - f1 ≤ values < f2
const f3 = f2 * f1            # quadrant 3 - f2 ≤ values < f3
const pi180 = pi / 180
const twopi = 2pi
const pi2 = pi / 2

"""

  `Arc(s::Real, c::Real) -> ::Arc`

Construct Arc from cartesian coordinates. `(s, c)` is forced to unit size.
"""
function Arc(s::Real, c::Real)
  h = hypot(s, c)
  s /= h
  c /= h
  sc_to_arc(s, c)
end

"""

  `Arc(rad::Real) -> ::Arc`

Construct Arc from radian value. If rad ∉ (-2π,2π] mind rounding errors.
"""
function Arc(r::Real)
  r = rem(r, twopi)
  if mod(r, pi2) == 0
    case = mod(Int(fld(r, pi2)), 4)
    if case == 0
      Arc(0.0, 1.0)
    elseif case == 1
      Arc(1.0, 0.0)
    elseif case == 2
      Arc(0.0, -1.0)
    else
      Arc(-1.0, 0.0)
    end
  else
    s, c = sin(r), cos(r)
    sc_to_arc(s, c)
  end
end

"""

  `Arcd(degree::Real) -> ::Arc`

Construct Arc from degree value.
"""
# special exactness if d given as integer
function Arcd(d::Real)
  d = mod(d, 360)
  if mod(d, 90) == 0
    case = Int(fld(d, 90))
    if case == 0
      Arc(0.0, 1.0)
    elseif case == 1
      Arc(1.0, 0.0)
    elseif case == 2
      Arc(0.0, -1.0)
    else
      Arc(-1.0, 0.0)
    end
  else
    Arc(d * pi180)
  end
end

# working function for constructor
function sc_to_arc(s::Real, c::Real)
  iq = quadr(s, c)
  rep =
  if iq == 0
    s
  elseif iq == 1
    ( abs(c) <= fmin ) && (c = fmin)
    c * f1
  elseif iq == 2
    ( abs(s) <= fmin ) && (s = fmin)
    s * f2
  else
    ( abs(c) <= fmin ) && (c = fmin)
    c * f3
  end
  reinterpret(Arc, rep)
end

# Find quadrant number from s and c
function quadr(s::Real, c::Real)
  if abs(s) <= abs(c)
    ifelse( c > 0, 0, 2)
  else
    ifelse( s > 0, 1, 3)
  end
end

@inline sq(x::Real) = sqrt(one(x) - x * x)

"""

  `rad(a::Arc) -> rad::Float64 ∈ (-π, π]`

  Calculate radiant of `a`.
"""
function rad(a::Arc)
  sc = reinterpret(T, a)
  sca = abs(sc)
  if sca < f0
    asin(sc)
  elseif sca < f1
    sc /= f1
    ( abs(sc) <= fmin ) && return pi2
    acos(sc)
  elseif sca < f2
    ( abs(sc) <= fmin ) && return 1π
    sc /= f2
    copysign(1π, sc) - asin(sc)
  else
    ( sca <= fmin ) && return -pi2
    sc /= f3
    -acos(sc)
  end
end

"""

  `sincos(a::Arc) -> (s, c)::Tuple{Float64,Float64}`

Calculate both sine and cosine of `a`.
"""
function sincos(a::Arc)
  sc = reinterpret(T, a)
  sca = abs(sc)
  if sca < f0
    sc, sq(sc)
  elseif sca < f1
    sc /= f1
    ( abs(sc) <= fmin ) && return 1.0, 0.0
    sq(sc), sc
  elseif sca < f2
    ( abs(sc) <= fmin ) && return 0.0, -1.0
    sc /= f2
    sc, -sq(sc)
  else
    ( sca <= fmin ) && return -1.0, 0.0
    sc /= f3
    -sq(sc), sc
  end
end

"""

  `sin(a::Arc) -> s::Float64`

Calculate the sine of `a`.
"""
function sin(a::Arc)
  sc = reinterpret(T, a)
  sca = abs(sc)
  if sca <= f0
    sc
  elseif sca < f1
    sc /= f1
    sq(sc)
  elseif sca < f2
    sc /= f2
    ( abs(sc) <= fmin ) && return 0.0
    sc
  else
    sc /= f3
    -sq(sc)
  end
end

"""

  `cos(a::Arc) -> c::Float64`

Calculate the cosine of `a`.
"""
function cos(a::Arc)
  sc = reinterpret(T, a)
  sca = abs(sc)
  if sca <= f0
    sq(sc)
  elseif sca < f1
    sc /= f1
    ( abs(sc) <= fmin ) && return 0.0
    sc
  elseif sca < f2
    sc /= f2
    -sq(sc)
  else
    sc /= f3
    ( abs(sc) <= fmin ) && return 0.0
    sc
  end
end

"""

  `tan(a::Arc) -> c::Float64`

Calculate the tangent of `a`.
"""
function tan(a::Arc)
  s, c = sincos(a)
  s / c
end

"""

  `sec(a::Arc) -> c::Float64`

Calculate the secant of `a`.
"""
sec(a::Arc) = 1.0 / cos(a)
"""

  `csc(a::Arc) -> c::Float64`

Calculate the cosecant of `a`.
"""
csc(a::Arc) = 1.0 / sin(a)
"""

  `cot(a::Arc) -> c::Float64`

Calculate the cotangent of `a`.
"""
cot(a::Arc) = 1.0 / tan(a)

convert(::Type{T}, a::Arc) = rad(a)

convert(::Type{Arc}, x::Real) = Arc(x)

"""
  `deg(a::Arc) -> r::Float`

Convert `a` to degrees in `(180,180]`.
"""
deg(a::Arc) = rad(a) * 180 / pi

# Singular zero constant
const zero_arc = reinterpret(Arc, 0.0)
zero(::Type{Arc}) = zero_arc
zero(::Arc) = zero_arc

show(io::IO, a::Arc) =  print(io, "$(rad(a))r")

function read(s::IO, ::Type{Arc})
    reinterpret(Arc, read(s, T))
end
function write(s::IO, a::Arc)
  write(s, reinterpret(T, a))
end

bits(a::Arc) = bits(reinterpret(T, a))

# Supported arithmetic functions
# plus
function +(a::Arc, b::Arc)
  # sa, ca = sincos(a)
  # sb, cb = sincos(b)
  # Arc(sa*cb + sb*ca, ca*cb - sa*sb)
  as, bs = rad(a), rad(b)
  Arc(as + bs)
end

# minus
function -(a::Arc, b::Arc)
  # sa, ca = sincos(a)
  # sb, cb = sincos(b)
  # Arc(sa*cb - sb*ca, ca*cb + sa*sb)
  as, bs = rad(a), rad(b)
  Arc(as - bs)
end

# unary -
function -(a::Arc)
  sa, ca = sincos(a)
  Arc(-sa, ca)
end

# times real number
function *(f::Integer, a::Arc)
  if abs(f) > 2
    T(f) * a
  elseif f == 2
    a + a
  elseif f == -2
    -( a + a )
  elseif f == -1
    -a
  elseif f == 1
    a
  elseif f == 0
    zero(Arc)
  end
end

*(f::Real, a::Arc) = Arc(rad(a) * f)
*(a::Arc, f::Real) = f * a

# divided by real number
/(a::Arc, f::Real) = (1.0 / f) * a

# Not required if not Arc <: AbstractFloat
# *(a::Arc, b::Arc) = error("invalid operation * with Arc")

isapprox(a::Arc, b::Arc) = abs(mod(rad(a) - rad(b), 2pi)) <= eps()

end # module Arcus


__precompile__()

module OrbitalInsertion

using DifferentialEquations, ForwardDiff, DiffResults, Printf, LinearAlgebra, NLopt, Roots, Interpolations, Plots

abstract type System end

struct EMsystem <:System end
struct SEsystem <:System end

struct ProblemInfo{T<:Float64}
    mu  ::T
    rp  ::T
    rs  ::T
    r12 ::T
    w   ::T
    mup ::T
    x1  ::T
    x2  ::T
end
struct ProblemParameters{T<:Float64,J<:Int64,K<:Bool}
    tspan   ::T
    hgoal   ::T
    hmmin   ::T
    hmmax   ::T
    sType   ::K
    points  ::J
    stop    ::K
    maxt0   ::T
    rest    ::Tuple{J,J}
    lim     ::T
    cTol    ::T
    tRest   ::K
    odeAlg  ::K
    atol    ::T
    rtol    ::T
    retorno ::K
    detMin  ::T
    x0      ::Array{T,1}
    y       ::Array{T,1}
    optTol  ::T
    neval   ::J
    col     ::K
end
mutable struct SystemInfo{T<:Float64}
    mu  ::T
    Rp  ::T
    Rs  ::T
    r12 ::T
    m1  ::T
    G   ::T
end

export createConstants, createSystem, createPar, search, filterGlobalSol, point, plotSols
export EMsystem, SEsystem
export ProblemInfo, ProblemParameters, SystemInfo, ProblemParameters

include("funcs_insert.jl")
include("systems.jl")
include("restrictions.jl")

end # module


using StringParserPEG

calc2 = Grammar("""
  start => (number & op & number){(r,v,f,l,c) -> c[2](c[1],c[3])}

  op => plus | minus
  number => (-(space) & r([0-9]+)r) {(r,v,f,l,c) -> parse(Int,c[1].value)}
  plus => (-(space) & '+'){(a...) -> +}
  minus => (-(space) & '-'){(a...) -> -}
  space => r([ \\t\\n\\r]*)r
""")

data = "4+5"

(ast, pos, err) = parse(calc2, data)
ast


function dft(x::Array{ArbComplex{P},1}) where {P}
    # to guarantee safe bounds
    length = size(x)[1]

    # we transfer the data into a C acb vector

    #(ALLOC x_c) Allocate the memory in C space
    x_c = ccall(@libarb(_acb_vec_init), Ptr{ArbComplex{P}}, (Int32,), length)
    #

    # copy x_c
    @inbounds for i in 1:length
                unsafe_store!(x_c, x[i], i)
              end
    #

    #(ALLOC transf_C)
    transf_c = ccall(@libarb(_acb_vec_init), Ptr{ArbComplex{P}}, (Int32,), length)
    #

    # we call the acb_dft
    # Signature: void acb_dft(acb_ptr w, acb_srcptr v, slong n, slong prec)
    ccall(@libarb(acb_dft), Cvoid, (Ref{ArbComplex{P}}, Ref{ArbComplex{P}}, Cint, Cint ), transf_c, x_c, length, P)

    #(DEALLOC x_c)
    ccall(@libarb(_acb_vec_clear), Cvoid, (Ref{ArbComplex{P}}, Cint ), x_c, length)
    #

    transf = zeros(ArbComplex{P}, length)
    @inbounds for i in 1:length
                transf[i] = unsafe_load(transf_c, i)
              end

    #(DEALLOC transf_c)
    ccall(@libarb(_acb_vec_clear), Cvoid, (Ref{ArbComplex{P}}, Cint ), transf_c, length)
    #

    return transf
end

dft(x::Array{ArbComplex,1}) = dft(Array{ArbComplex{workingprecision(ArbComplex)}, 1}(x))
dft(x::Array{ArbReal{P},1}) where {P} = dft(Array{ArbComplex{P}, 1}(x))
dft(x::Array{ArbFloat{P},1}) where {P} = dft(Array{ArbComplex{P}, 1}(x))
dft(x::Array{ArbReal,1}) = dft(Array{ArbComplex{workingprecision(ArbReal)}, 1}(x))
dft(x::Array{ArbFloat,1}) = dft(Array{ArbComplex{workingprecision(ArbFloat)}, 1}(x))

function inverse_dft(x::Array{ArbComplex{P},1}) where {P}
    # to guarantee safe bounds
    length = size(x)[1]

    # we transfer the data into a C acb vector

    #(ALLOC x_c) Allocate the memory in C space
    x_c = ccall(@libarb(_acb_vec_init), Ptr{ArbComplex{P}}, (Int32,), length)
    #

    # copy x_c
    @inbounds for i in 1:length
                unsafe_store!(x_c, x[i], i)
              end
    #

    #(ALLOC transf_C)
    transf_c = ccall(@libarb(_acb_vec_init), Ptr{ArbComplex{P}}, (Int32,), length)
    #

    # we call the acb_dft
    # Signature: void acb_dft(acb_ptr w, acb_srcptr v, slong n, slong prec)
    ccall(@libarb(acb_dft_inverse), Cvoid, (Ref{ArbComplex{P}}, Ref{ArbComplex{P}}, Cint, Cint ), transf_c, x_c, length, P)

    #(DEALLOC x_c)
    ccall(@libarb(_acb_vec_clear), Cvoid, (Ref{ArbComplex{P}}, Cint ), x_c, length)
    #

    transf = zeros(ArbComplex{P}, length)
    @inbounds for i in 1:length
                transf[i] = unsafe_load(transf_c, i)
              end

    #(DEALLOC transf_c)
    ccall(@libarb(_acb_vec_clear), Cvoid, (Ref{ArbComplex{P}}, Cint ), transf_c, length)
    #

    return transf
end

inverse_dft(x::Array{ArbComplex,1}) = inverse_dft(Array{ArbComplex{workingprecision(ArbComplex)}, 1}(x))
inverse_dft(x::Array{ArbReal{P},1}) where {P} = dft(Array{ArbComplex{P}, 1}(x))
inverse_dft(x::Array{ArbFloat{P},1}) where {P} = dft(Array{ArbComplex{P}, 1}(x))
inverse_dft(x::Array{ArbReal,1}) = dft(Array{ArbComplex{workingprecision(ArbReal)}, 1}(x))
inverse_dft(x::Array{ArbFloat,1}) = dft(Array{ArbComplex{workingprecision(ArbFloat)}, 1}(x))


using ITensors

function run(; maxdim::Int,
               nsweeps::Int = 5,
               outputlevel::Int = 0,
               conserve_qns::Bool = false,
               use_sparse_mpo::Bool = false,
               N::Int = 100)
  sites = siteinds("S=1", N; conserve_qns = conserve_qns)
  ampo = AutoMPO()
  for j in 1:N-1
    ampo .+= 0.5, "S+", j, "S-", j+1
    ampo .+= 0.5, "S-", j, "S+", j+1
    ampo .+=      "Sz", j, "Sz", j+1
  end
  HD = MPO(ampo,sites)
  if use_sparse_mpo
    println("Splitting blocks of H (= making MPO sparse)")
    H = splitblocks(linkinds, HD)
  else
    println("Using dense MPO")
    H = HD
  end
  psi0 = productMPS(sites, n -> isodd(n) ? "↑" : "↓")
  sweeps = Sweeps(nsweeps)
  maxdims = min.(maxdim, [10, 20, 100, maxdim])
  maxdim!(sweeps, maxdims...)
  cutoff!(sweeps, 0.0)
  t = @elapsed begin
  energy, ψ = dmrg(H, psi0, sweeps;
                   outputlevel = outputlevel)
  end
  println(t)
  return
end


module HomotopyContinuation2

export ModelKit

using DynamicPolynomials: @polyvar
import ElasticArrays: ElasticArray
import LinearAlgebra
import LoopVectorization
import MixedSubdivisions
import MultivariatePolynomials
const MP = MultivariatePolynomials
using Parameters: @unpack
import ProgressMeter
import Random
import Printf
import PrettyTables
using Reexport: @reexport
import StructArrays
import TreeViews

const LA = LinearAlgebra

include("ModelKit.jl")
export @polyvar

include("DoubleDouble.jl")
using .DoubleDouble

using ProjectiveVectors: PVector, dims, dimension_indices

include("utils.jl")
include("norm.jl")
include("linear_algebra.jl")
include("linear.jl")
include("systems.jl")
include("homotopies.jl")
include("predictor.jl")
include("newton_corrector.jl")
include("newton.jl")
include("tracker.jl")
include("valuation.jl")
include("path_result.jl")
include("endgame_tracker.jl")
include("path_info.jl")
include("total_degree.jl")
include("binomial_system.jl")
include("polyhedral.jl")
include("overdetermined.jl")
include("solve.jl")
include("result.jl")

end


"
vrr_ss(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has s-type angular momentum, and the C shell has s-type
angular momentum.
"
function vrr_ss(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 1
    vrrs = zeros(Float64,1,1,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)

    return vrrs[:,:,1]
end
"
vrr_ps(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has p-type angular momentum, and the C shell has s-type
angular momentum.
"
function vrr_ps(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 2
    vrrs = zeros(Float64,4,1,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)
    vrrs[1,1,2] = Kab*Kcd*Fgamma(1,T)/sqrt(ze)

    vrrs[2,1,1] = PA[1]*vrrs[1,1,1] + WP[1]*vrrs[1,1,2]
    vrrs[3,1,1] = PA[2]*vrrs[1,1,1] + WP[2]*vrrs[1,1,2]
    vrrs[4,1,1] = PA[3]*vrrs[1,1,1] + WP[3]*vrrs[1,1,2]

    return vrrs[:,:,1]
end
"
vrr_sp(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has s-type angular momentum, and the C shell has p-type
angular momentum.
"
function vrr_sp(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 2
    vrrs = zeros(Float64,1,4,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)
    vrrs[1,1,2] = Kab*Kcd*Fgamma(1,T)/sqrt(ze)

    vrrs[1,2,1] = QC[1]*vrrs[1,1,1] + WQ[1]*vrrs[1,1,2]
    vrrs[1,3,1] = QC[2]*vrrs[1,1,1] + WQ[2]*vrrs[1,1,2]
    vrrs[1,4,1] = QC[3]*vrrs[1,1,1] + WQ[3]*vrrs[1,1,2]

    return vrrs[:,:,1]
end
"
vrr_pp(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has p-type angular momentum, and the C shell has p-type
angular momentum.
"
function vrr_pp(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 3
    vrrs = zeros(Float64,4,4,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)
    vrrs[1,1,2] = Kab*Kcd*Fgamma(1,T)/sqrt(ze)
    vrrs[1,1,3] = Kab*Kcd*Fgamma(2,T)/sqrt(ze)

    vrrs[2,1,1] = PA[1]*vrrs[1,1,1] + WP[1]*vrrs[1,1,2]
    vrrs[2,1,2] = PA[1]*vrrs[1,1,2] + WP[1]*vrrs[1,1,3]
    vrrs[3,1,1] = PA[2]*vrrs[1,1,1] + WP[2]*vrrs[1,1,2]
    vrrs[3,1,2] = PA[2]*vrrs[1,1,2] + WP[2]*vrrs[1,1,3]
    vrrs[4,1,1] = PA[3]*vrrs[1,1,1] + WP[3]*vrrs[1,1,2]
    vrrs[4,1,2] = PA[3]*vrrs[1,1,2] + WP[3]*vrrs[1,1,3]
    vrrs[1,2,1] = QC[1]*vrrs[1,1,1] + WQ[1]*vrrs[1,1,2]
    vrrs[1,2,2] = QC[1]*vrrs[1,1,2] + WQ[1]*vrrs[1,1,3]
    vrrs[2,2,1] = QC[1]*vrrs[2,1,1] + WQ[1]*vrrs[2,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[3,2,1] = QC[1]*vrrs[3,1,1] + WQ[1]*vrrs[3,1,2]
    vrrs[4,2,1] = QC[1]*vrrs[4,1,1] + WQ[1]*vrrs[4,1,2]
    vrrs[1,3,1] = QC[2]*vrrs[1,1,1] + WQ[2]*vrrs[1,1,2]
    vrrs[1,3,2] = QC[2]*vrrs[1,1,2] + WQ[2]*vrrs[1,1,3]
    vrrs[2,3,1] = QC[2]*vrrs[2,1,1] + WQ[2]*vrrs[2,1,2]
    vrrs[3,3,1] = QC[2]*vrrs[3,1,1] + WQ[2]*vrrs[3,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[4,3,1] = QC[2]*vrrs[4,1,1] + WQ[2]*vrrs[4,1,2]
    vrrs[1,4,1] = QC[3]*vrrs[1,1,1] + WQ[3]*vrrs[1,1,2]
    vrrs[1,4,2] = QC[3]*vrrs[1,1,2] + WQ[3]*vrrs[1,1,3]
    vrrs[2,4,1] = QC[3]*vrrs[2,1,1] + WQ[3]*vrrs[2,1,2]
    vrrs[3,4,1] = QC[3]*vrrs[3,1,1] + WQ[3]*vrrs[3,1,2]
    vrrs[4,4,1] = QC[3]*vrrs[4,1,1] + WQ[3]*vrrs[4,1,2] + 1/2ze*vrrs[1,1,2]

    return vrrs[:,:,1]
end
"
vrr_ds(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has d-type angular momentum, and the C shell has s-type
angular momentum.
"
function vrr_ds(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 3
    vrrs = zeros(Float64,10,1,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)
    vrrs[1,1,2] = Kab*Kcd*Fgamma(1,T)/sqrt(ze)
    vrrs[1,1,3] = Kab*Kcd*Fgamma(2,T)/sqrt(ze)

    vrrs[2,1,1] = PA[1]*vrrs[1,1,1] + WP[1]*vrrs[1,1,2]
    vrrs[2,1,2] = PA[1]*vrrs[1,1,2] + WP[1]*vrrs[1,1,3]
    vrrs[3,1,1] = PA[2]*vrrs[1,1,1] + WP[2]*vrrs[1,1,2]
    vrrs[3,1,2] = PA[2]*vrrs[1,1,2] + WP[2]*vrrs[1,1,3]
    vrrs[4,1,1] = PA[3]*vrrs[1,1,1] + WP[3]*vrrs[1,1,2]
    vrrs[4,1,2] = PA[3]*vrrs[1,1,2] + WP[3]*vrrs[1,1,3]
    vrrs[5,1,1] = PA[1]*vrrs[2,1,1] + WP[1]*vrrs[2,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])
    vrrs[6,1,1] = PA[1]*vrrs[3,1,1] + WP[1]*vrrs[3,1,2]
    vrrs[7,1,1] = PA[1]*vrrs[4,1,1] + WP[1]*vrrs[4,1,2]
    vrrs[8,1,1] = PA[2]*vrrs[3,1,1] + WP[2]*vrrs[3,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])
    vrrs[9,1,1] = PA[2]*vrrs[4,1,1] + WP[2]*vrrs[4,1,2]
    vrrs[10,1,1] = PA[3]*vrrs[4,1,1] + WP[3]*vrrs[4,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])

    return vrrs[:,:,1]
end
"
vrr_sd(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has s-type angular momentum, and the C shell has d-type
angular momentum.
"
function vrr_sd(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 3
    vrrs = zeros(Float64,1,10,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)
    vrrs[1,1,2] = Kab*Kcd*Fgamma(1,T)/sqrt(ze)
    vrrs[1,1,3] = Kab*Kcd*Fgamma(2,T)/sqrt(ze)

    vrrs[1,2,1] = QC[1]*vrrs[1,1,1] + WQ[1]*vrrs[1,1,2]
    vrrs[1,2,2] = QC[1]*vrrs[1,1,2] + WQ[1]*vrrs[1,1,3]
    vrrs[1,3,1] = QC[2]*vrrs[1,1,1] + WQ[2]*vrrs[1,1,2]
    vrrs[1,3,2] = QC[2]*vrrs[1,1,2] + WQ[2]*vrrs[1,1,3]
    vrrs[1,4,1] = QC[3]*vrrs[1,1,1] + WQ[3]*vrrs[1,1,2]
    vrrs[1,4,2] = QC[3]*vrrs[1,1,2] + WQ[3]*vrrs[1,1,3]
    vrrs[1,5,1] = QC[1]*vrrs[1,2,1] + WQ[1]*vrrs[1,2,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])
    vrrs[1,6,1] = QC[1]*vrrs[1,3,1] + WQ[1]*vrrs[1,3,2]
    vrrs[1,7,1] = QC[1]*vrrs[1,4,1] + WQ[1]*vrrs[1,4,2]
    vrrs[1,8,1] = QC[2]*vrrs[1,3,1] + WQ[2]*vrrs[1,3,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])
    vrrs[1,9,1] = QC[2]*vrrs[1,4,1] + WQ[2]*vrrs[1,4,2]
    vrrs[1,10,1] = QC[3]*vrrs[1,4,1] + WQ[3]*vrrs[1,4,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])

    return vrrs[:,:,1]
end
"
vrr_dp(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has d-type angular momentum, and the C shell has p-type
angular momentum.
"
function vrr_dp(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 4
    vrrs = zeros(Float64,10,4,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)
    vrrs[1,1,2] = Kab*Kcd*Fgamma(1,T)/sqrt(ze)
    vrrs[1,1,3] = Kab*Kcd*Fgamma(2,T)/sqrt(ze)
    vrrs[1,1,4] = Kab*Kcd*Fgamma(3,T)/sqrt(ze)

    vrrs[2,1,1] = PA[1]*vrrs[1,1,1] + WP[1]*vrrs[1,1,2]
    vrrs[2,1,2] = PA[1]*vrrs[1,1,2] + WP[1]*vrrs[1,1,3]
    vrrs[2,1,3] = PA[1]*vrrs[1,1,3] + WP[1]*vrrs[1,1,4]
    vrrs[3,1,1] = PA[2]*vrrs[1,1,1] + WP[2]*vrrs[1,1,2]
    vrrs[3,1,2] = PA[2]*vrrs[1,1,2] + WP[2]*vrrs[1,1,3]
    vrrs[3,1,3] = PA[2]*vrrs[1,1,3] + WP[2]*vrrs[1,1,4]
    vrrs[4,1,1] = PA[3]*vrrs[1,1,1] + WP[3]*vrrs[1,1,2]
    vrrs[4,1,2] = PA[3]*vrrs[1,1,2] + WP[3]*vrrs[1,1,3]
    vrrs[4,1,3] = PA[3]*vrrs[1,1,3] + WP[3]*vrrs[1,1,4]
    vrrs[5,1,1] = PA[1]*vrrs[2,1,1] + WP[1]*vrrs[2,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])
    vrrs[5,1,2] = PA[1]*vrrs[2,1,2] + WP[1]*vrrs[2,1,3] + 1/2zeta*(vrrs[1,1,2]-eta/ze*vrrs[1,1,3])
    vrrs[6,1,1] = PA[1]*vrrs[3,1,1] + WP[1]*vrrs[3,1,2]
    vrrs[6,1,2] = PA[1]*vrrs[3,1,2] + WP[1]*vrrs[3,1,3]
    vrrs[7,1,1] = PA[1]*vrrs[4,1,1] + WP[1]*vrrs[4,1,2]
    vrrs[7,1,2] = PA[1]*vrrs[4,1,2] + WP[1]*vrrs[4,1,3]
    vrrs[8,1,1] = PA[2]*vrrs[3,1,1] + WP[2]*vrrs[3,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])
    vrrs[8,1,2] = PA[2]*vrrs[3,1,2] + WP[2]*vrrs[3,1,3] + 1/2zeta*(vrrs[1,1,2]-eta/ze*vrrs[1,1,3])
    vrrs[9,1,1] = PA[2]*vrrs[4,1,1] + WP[2]*vrrs[4,1,2]
    vrrs[9,1,2] = PA[2]*vrrs[4,1,2] + WP[2]*vrrs[4,1,3]
    vrrs[10,1,1] = PA[3]*vrrs[4,1,1] + WP[3]*vrrs[4,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])
    vrrs[10,1,2] = PA[3]*vrrs[4,1,2] + WP[3]*vrrs[4,1,3] + 1/2zeta*(vrrs[1,1,2]-eta/ze*vrrs[1,1,3])
    vrrs[1,2,1] = QC[1]*vrrs[1,1,1] + WQ[1]*vrrs[1,1,2]
    vrrs[1,2,2] = QC[1]*vrrs[1,1,2] + WQ[1]*vrrs[1,1,3]
    vrrs[1,2,3] = QC[1]*vrrs[1,1,3] + WQ[1]*vrrs[1,1,4]
    vrrs[2,2,1] = QC[1]*vrrs[2,1,1] + WQ[1]*vrrs[2,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[2,2,2] = QC[1]*vrrs[2,1,2] + WQ[1]*vrrs[2,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[3,2,1] = QC[1]*vrrs[3,1,1] + WQ[1]*vrrs[3,1,2]
    vrrs[3,2,2] = QC[1]*vrrs[3,1,2] + WQ[1]*vrrs[3,1,3]
    vrrs[4,2,1] = QC[1]*vrrs[4,1,1] + WQ[1]*vrrs[4,1,2]
    vrrs[4,2,2] = QC[1]*vrrs[4,1,2] + WQ[1]*vrrs[4,1,3]
    vrrs[5,2,1] = QC[1]*vrrs[5,1,1] + WQ[1]*vrrs[5,1,2] + 2/2ze*vrrs[2,1,2]
    vrrs[6,2,1] = QC[1]*vrrs[6,1,1] + WQ[1]*vrrs[6,1,2] + 1/2ze*vrrs[3,1,2]
    vrrs[7,2,1] = QC[1]*vrrs[7,1,1] + WQ[1]*vrrs[7,1,2] + 1/2ze*vrrs[4,1,2]
    vrrs[8,2,1] = QC[1]*vrrs[8,1,1] + WQ[1]*vrrs[8,1,2]
    vrrs[9,2,1] = QC[1]*vrrs[9,1,1] + WQ[1]*vrrs[9,1,2]
    vrrs[10,2,1] = QC[1]*vrrs[10,1,1] + WQ[1]*vrrs[10,1,2]
    vrrs[1,3,1] = QC[2]*vrrs[1,1,1] + WQ[2]*vrrs[1,1,2]
    vrrs[1,3,2] = QC[2]*vrrs[1,1,2] + WQ[2]*vrrs[1,1,3]
    vrrs[1,3,3] = QC[2]*vrrs[1,1,3] + WQ[2]*vrrs[1,1,4]
    vrrs[2,3,1] = QC[2]*vrrs[2,1,1] + WQ[2]*vrrs[2,1,2]
    vrrs[2,3,2] = QC[2]*vrrs[2,1,2] + WQ[2]*vrrs[2,1,3]
    vrrs[3,3,1] = QC[2]*vrrs[3,1,1] + WQ[2]*vrrs[3,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[3,3,2] = QC[2]*vrrs[3,1,2] + WQ[2]*vrrs[3,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[4,3,1] = QC[2]*vrrs[4,1,1] + WQ[2]*vrrs[4,1,2]
    vrrs[4,3,2] = QC[2]*vrrs[4,1,2] + WQ[2]*vrrs[4,1,3]
    vrrs[5,3,1] = QC[2]*vrrs[5,1,1] + WQ[2]*vrrs[5,1,2]
    vrrs[6,3,1] = QC[2]*vrrs[6,1,1] + WQ[2]*vrrs[6,1,2] + 1/2ze*vrrs[2,1,2]
    vrrs[7,3,1] = QC[2]*vrrs[7,1,1] + WQ[2]*vrrs[7,1,2]
    vrrs[8,3,1] = QC[2]*vrrs[8,1,1] + WQ[2]*vrrs[8,1,2] + 2/2ze*vrrs[3,1,2]
    vrrs[9,3,1] = QC[2]*vrrs[9,1,1] + WQ[2]*vrrs[9,1,2] + 1/2ze*vrrs[4,1,2]
    vrrs[10,3,1] = QC[2]*vrrs[10,1,1] + WQ[2]*vrrs[10,1,2]
    vrrs[1,4,1] = QC[3]*vrrs[1,1,1] + WQ[3]*vrrs[1,1,2]
    vrrs[1,4,2] = QC[3]*vrrs[1,1,2] + WQ[3]*vrrs[1,1,3]
    vrrs[1,4,3] = QC[3]*vrrs[1,1,3] + WQ[3]*vrrs[1,1,4]
    vrrs[2,4,1] = QC[3]*vrrs[2,1,1] + WQ[3]*vrrs[2,1,2]
    vrrs[2,4,2] = QC[3]*vrrs[2,1,2] + WQ[3]*vrrs[2,1,3]
    vrrs[3,4,1] = QC[3]*vrrs[3,1,1] + WQ[3]*vrrs[3,1,2]
    vrrs[3,4,2] = QC[3]*vrrs[3,1,2] + WQ[3]*vrrs[3,1,3]
    vrrs[4,4,1] = QC[3]*vrrs[4,1,1] + WQ[3]*vrrs[4,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[4,4,2] = QC[3]*vrrs[4,1,2] + WQ[3]*vrrs[4,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[5,4,1] = QC[3]*vrrs[5,1,1] + WQ[3]*vrrs[5,1,2]
    vrrs[6,4,1] = QC[3]*vrrs[6,1,1] + WQ[3]*vrrs[6,1,2]
    vrrs[7,4,1] = QC[3]*vrrs[7,1,1] + WQ[3]*vrrs[7,1,2] + 1/2ze*vrrs[2,1,2]
    vrrs[8,4,1] = QC[3]*vrrs[8,1,1] + WQ[3]*vrrs[8,1,2]
    vrrs[9,4,1] = QC[3]*vrrs[9,1,1] + WQ[3]*vrrs[9,1,2] + 1/2ze*vrrs[3,1,2]
    vrrs[10,4,1] = QC[3]*vrrs[10,1,1] + WQ[3]*vrrs[10,1,2] + 2/2ze*vrrs[4,1,2]

    return vrrs[:,:,1]
end
"
vrr_pd(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has p-type angular momentum, and the C shell has d-type
angular momentum.
"
function vrr_pd(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 4
    vrrs = zeros(Float64,4,10,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)
    vrrs[1,1,2] = Kab*Kcd*Fgamma(1,T)/sqrt(ze)
    vrrs[1,1,3] = Kab*Kcd*Fgamma(2,T)/sqrt(ze)
    vrrs[1,1,4] = Kab*Kcd*Fgamma(3,T)/sqrt(ze)

    vrrs[2,1,1] = PA[1]*vrrs[1,1,1] + WP[1]*vrrs[1,1,2]
    vrrs[2,1,2] = PA[1]*vrrs[1,1,2] + WP[1]*vrrs[1,1,3]
    vrrs[2,1,3] = PA[1]*vrrs[1,1,3] + WP[1]*vrrs[1,1,4]
    vrrs[3,1,1] = PA[2]*vrrs[1,1,1] + WP[2]*vrrs[1,1,2]
    vrrs[3,1,2] = PA[2]*vrrs[1,1,2] + WP[2]*vrrs[1,1,3]
    vrrs[3,1,3] = PA[2]*vrrs[1,1,3] + WP[2]*vrrs[1,1,4]
    vrrs[4,1,1] = PA[3]*vrrs[1,1,1] + WP[3]*vrrs[1,1,2]
    vrrs[4,1,2] = PA[3]*vrrs[1,1,2] + WP[3]*vrrs[1,1,3]
    vrrs[4,1,3] = PA[3]*vrrs[1,1,3] + WP[3]*vrrs[1,1,4]
    vrrs[1,2,1] = QC[1]*vrrs[1,1,1] + WQ[1]*vrrs[1,1,2]
    vrrs[1,2,2] = QC[1]*vrrs[1,1,2] + WQ[1]*vrrs[1,1,3]
    vrrs[1,2,3] = QC[1]*vrrs[1,1,3] + WQ[1]*vrrs[1,1,4]
    vrrs[2,2,1] = QC[1]*vrrs[2,1,1] + WQ[1]*vrrs[2,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[2,2,2] = QC[1]*vrrs[2,1,2] + WQ[1]*vrrs[2,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[3,2,1] = QC[1]*vrrs[3,1,1] + WQ[1]*vrrs[3,1,2]
    vrrs[3,2,2] = QC[1]*vrrs[3,1,2] + WQ[1]*vrrs[3,1,3]
    vrrs[4,2,1] = QC[1]*vrrs[4,1,1] + WQ[1]*vrrs[4,1,2]
    vrrs[4,2,2] = QC[1]*vrrs[4,1,2] + WQ[1]*vrrs[4,1,3]
    vrrs[1,3,1] = QC[2]*vrrs[1,1,1] + WQ[2]*vrrs[1,1,2]
    vrrs[1,3,2] = QC[2]*vrrs[1,1,2] + WQ[2]*vrrs[1,1,3]
    vrrs[1,3,3] = QC[2]*vrrs[1,1,3] + WQ[2]*vrrs[1,1,4]
    vrrs[2,3,1] = QC[2]*vrrs[2,1,1] + WQ[2]*vrrs[2,1,2]
    vrrs[2,3,2] = QC[2]*vrrs[2,1,2] + WQ[2]*vrrs[2,1,3]
    vrrs[3,3,1] = QC[2]*vrrs[3,1,1] + WQ[2]*vrrs[3,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[3,3,2] = QC[2]*vrrs[3,1,2] + WQ[2]*vrrs[3,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[4,3,1] = QC[2]*vrrs[4,1,1] + WQ[2]*vrrs[4,1,2]
    vrrs[4,3,2] = QC[2]*vrrs[4,1,2] + WQ[2]*vrrs[4,1,3]
    vrrs[1,4,1] = QC[3]*vrrs[1,1,1] + WQ[3]*vrrs[1,1,2]
    vrrs[1,4,2] = QC[3]*vrrs[1,1,2] + WQ[3]*vrrs[1,1,3]
    vrrs[1,4,3] = QC[3]*vrrs[1,1,3] + WQ[3]*vrrs[1,1,4]
    vrrs[2,4,1] = QC[3]*vrrs[2,1,1] + WQ[3]*vrrs[2,1,2]
    vrrs[2,4,2] = QC[3]*vrrs[2,1,2] + WQ[3]*vrrs[2,1,3]
    vrrs[3,4,1] = QC[3]*vrrs[3,1,1] + WQ[3]*vrrs[3,1,2]
    vrrs[3,4,2] = QC[3]*vrrs[3,1,2] + WQ[3]*vrrs[3,1,3]
    vrrs[4,4,1] = QC[3]*vrrs[4,1,1] + WQ[3]*vrrs[4,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[4,4,2] = QC[3]*vrrs[4,1,2] + WQ[3]*vrrs[4,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[1,5,1] = QC[1]*vrrs[1,2,1] + WQ[1]*vrrs[1,2,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])
    vrrs[1,5,2] = QC[1]*vrrs[1,2,2] + WQ[1]*vrrs[1,2,3] + 1/2eta*(vrrs[1,1,2]-zeta/ze*vrrs[1,1,3])
    vrrs[2,5,1] = QC[1]*vrrs[2,2,1] + WQ[1]*vrrs[2,2,2] + 1/2eta*(vrrs[2,1,1]-zeta/ze*vrrs[2,1,2]) + 1/2ze*vrrs[1,2,2]
    vrrs[3,5,1] = QC[1]*vrrs[3,2,1] + WQ[1]*vrrs[3,2,2] + 1/2eta*(vrrs[3,1,1]-zeta/ze*vrrs[3,1,2])
    vrrs[4,5,1] = QC[1]*vrrs[4,2,1] + WQ[1]*vrrs[4,2,2] + 1/2eta*(vrrs[4,1,1]-zeta/ze*vrrs[4,1,2])
    vrrs[1,6,1] = QC[1]*vrrs[1,3,1] + WQ[1]*vrrs[1,3,2]
    vrrs[1,6,2] = QC[1]*vrrs[1,3,2] + WQ[1]*vrrs[1,3,3]
    vrrs[2,6,1] = QC[1]*vrrs[2,3,1] + WQ[1]*vrrs[2,3,2] + 1/2ze*vrrs[1,3,2]
    vrrs[3,6,1] = QC[1]*vrrs[3,3,1] + WQ[1]*vrrs[3,3,2]
    vrrs[4,6,1] = QC[1]*vrrs[4,3,1] + WQ[1]*vrrs[4,3,2]
    vrrs[1,7,1] = QC[1]*vrrs[1,4,1] + WQ[1]*vrrs[1,4,2]
    vrrs[1,7,2] = QC[1]*vrrs[1,4,2] + WQ[1]*vrrs[1,4,3]
    vrrs[2,7,1] = QC[1]*vrrs[2,4,1] + WQ[1]*vrrs[2,4,2] + 1/2ze*vrrs[1,4,2]
    vrrs[3,7,1] = QC[1]*vrrs[3,4,1] + WQ[1]*vrrs[3,4,2]
    vrrs[4,7,1] = QC[1]*vrrs[4,4,1] + WQ[1]*vrrs[4,4,2]
    vrrs[1,8,1] = QC[2]*vrrs[1,3,1] + WQ[2]*vrrs[1,3,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])
    vrrs[1,8,2] = QC[2]*vrrs[1,3,2] + WQ[2]*vrrs[1,3,3] + 1/2eta*(vrrs[1,1,2]-zeta/ze*vrrs[1,1,3])
    vrrs[2,8,1] = QC[2]*vrrs[2,3,1] + WQ[2]*vrrs[2,3,2] + 1/2eta*(vrrs[2,1,1]-zeta/ze*vrrs[2,1,2])
    vrrs[3,8,1] = QC[2]*vrrs[3,3,1] + WQ[2]*vrrs[3,3,2] + 1/2eta*(vrrs[3,1,1]-zeta/ze*vrrs[3,1,2]) + 1/2ze*vrrs[1,3,2]
    vrrs[4,8,1] = QC[2]*vrrs[4,3,1] + WQ[2]*vrrs[4,3,2] + 1/2eta*(vrrs[4,1,1]-zeta/ze*vrrs[4,1,2])
    vrrs[1,9,1] = QC[2]*vrrs[1,4,1] + WQ[2]*vrrs[1,4,2]
    vrrs[1,9,2] = QC[2]*vrrs[1,4,2] + WQ[2]*vrrs[1,4,3]
    vrrs[2,9,1] = QC[2]*vrrs[2,4,1] + WQ[2]*vrrs[2,4,2]
    vrrs[3,9,1] = QC[2]*vrrs[3,4,1] + WQ[2]*vrrs[3,4,2] + 1/2ze*vrrs[1,4,2]
    vrrs[4,9,1] = QC[2]*vrrs[4,4,1] + WQ[2]*vrrs[4,4,2]
    vrrs[1,10,1] = QC[3]*vrrs[1,4,1] + WQ[3]*vrrs[1,4,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])
    vrrs[1,10,2] = QC[3]*vrrs[1,4,2] + WQ[3]*vrrs[1,4,3] + 1/2eta*(vrrs[1,1,2]-zeta/ze*vrrs[1,1,3])
    vrrs[2,10,1] = QC[3]*vrrs[2,4,1] + WQ[3]*vrrs[2,4,2] + 1/2eta*(vrrs[2,1,1]-zeta/ze*vrrs[2,1,2])
    vrrs[3,10,1] = QC[3]*vrrs[3,4,1] + WQ[3]*vrrs[3,4,2] + 1/2eta*(vrrs[3,1,1]-zeta/ze*vrrs[3,1,2])
    vrrs[4,10,1] = QC[3]*vrrs[4,4,1] + WQ[3]*vrrs[4,4,2] + 1/2eta*(vrrs[4,1,1]-zeta/ze*vrrs[4,1,2]) + 1/2ze*vrrs[1,4,2]

    return vrrs[:,:,1]
end
"
vrr_dd(aexpn,bexpn,cexpn,dexpn, A,B,C,D)

Use Head-Gordon/Pople's vertical recurrence relations to compute
an array of two-electron integrals.

`A`, `B`, `C`, `D` are the centers of four Gaussian functions.
`aexpn`, `bexpn`, `cexpn`, `dexpn` are their exponents.

This is an auto generated routine specific to when the A shell
has d-type angular momentum, and the C shell has d-type
angular momentum.
"
function vrr_dd(aexpn,bexpn,cexpn,dexpn, A,B,C,D)
    mmax = 5
    vrrs = zeros(Float64,10,10,mmax)

    P = gaussian_product_center(aexpn,A,bexpn,B)
    Q = gaussian_product_center(cexpn,C,dexpn,D)
    zeta,eta = aexpn+bexpn,cexpn+dexpn
    ze = zeta+eta
    W = gaussian_product_center(zeta,P,eta,Q)
    rab2 = dist2(A-B)
    rcd2 = dist2(C-D)
    rpq2 = dist2(P-Q)
    T = zeta*eta*rpq2/ze
    Kab = sqrt(2)pi^1.25/zeta*exp(-aexpn*bexpn*rab2/zeta)
    Kcd = sqrt(2)pi^1.25/eta*exp(-cexpn*dexpn*rcd2/eta)
    PA = P-A
    WP = W-P
    QC = Q-C
    WQ = W-Q

    vrrs[1,1,1] = Kab*Kcd*Fgamma(0,T)/sqrt(ze)
    vrrs[1,1,2] = Kab*Kcd*Fgamma(1,T)/sqrt(ze)
    vrrs[1,1,3] = Kab*Kcd*Fgamma(2,T)/sqrt(ze)
    vrrs[1,1,4] = Kab*Kcd*Fgamma(3,T)/sqrt(ze)
    vrrs[1,1,5] = Kab*Kcd*Fgamma(4,T)/sqrt(ze)

    vrrs[2,1,1] = PA[1]*vrrs[1,1,1] + WP[1]*vrrs[1,1,2]
    vrrs[2,1,2] = PA[1]*vrrs[1,1,2] + WP[1]*vrrs[1,1,3]
    vrrs[2,1,3] = PA[1]*vrrs[1,1,3] + WP[1]*vrrs[1,1,4]
    vrrs[2,1,4] = PA[1]*vrrs[1,1,4] + WP[1]*vrrs[1,1,5]
    vrrs[3,1,1] = PA[2]*vrrs[1,1,1] + WP[2]*vrrs[1,1,2]
    vrrs[3,1,2] = PA[2]*vrrs[1,1,2] + WP[2]*vrrs[1,1,3]
    vrrs[3,1,3] = PA[2]*vrrs[1,1,3] + WP[2]*vrrs[1,1,4]
    vrrs[3,1,4] = PA[2]*vrrs[1,1,4] + WP[2]*vrrs[1,1,5]
    vrrs[4,1,1] = PA[3]*vrrs[1,1,1] + WP[3]*vrrs[1,1,2]
    vrrs[4,1,2] = PA[3]*vrrs[1,1,2] + WP[3]*vrrs[1,1,3]
    vrrs[4,1,3] = PA[3]*vrrs[1,1,3] + WP[3]*vrrs[1,1,4]
    vrrs[4,1,4] = PA[3]*vrrs[1,1,4] + WP[3]*vrrs[1,1,5]
    vrrs[5,1,1] = PA[1]*vrrs[2,1,1] + WP[1]*vrrs[2,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])
    vrrs[5,1,2] = PA[1]*vrrs[2,1,2] + WP[1]*vrrs[2,1,3] + 1/2zeta*(vrrs[1,1,2]-eta/ze*vrrs[1,1,3])
    vrrs[5,1,3] = PA[1]*vrrs[2,1,3] + WP[1]*vrrs[2,1,4] + 1/2zeta*(vrrs[1,1,3]-eta/ze*vrrs[1,1,4])
    vrrs[6,1,1] = PA[1]*vrrs[3,1,1] + WP[1]*vrrs[3,1,2]
    vrrs[6,1,2] = PA[1]*vrrs[3,1,2] + WP[1]*vrrs[3,1,3]
    vrrs[6,1,3] = PA[1]*vrrs[3,1,3] + WP[1]*vrrs[3,1,4]
    vrrs[7,1,1] = PA[1]*vrrs[4,1,1] + WP[1]*vrrs[4,1,2]
    vrrs[7,1,2] = PA[1]*vrrs[4,1,2] + WP[1]*vrrs[4,1,3]
    vrrs[7,1,3] = PA[1]*vrrs[4,1,3] + WP[1]*vrrs[4,1,4]
    vrrs[8,1,1] = PA[2]*vrrs[3,1,1] + WP[2]*vrrs[3,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])
    vrrs[8,1,2] = PA[2]*vrrs[3,1,2] + WP[2]*vrrs[3,1,3] + 1/2zeta*(vrrs[1,1,2]-eta/ze*vrrs[1,1,3])
    vrrs[8,1,3] = PA[2]*vrrs[3,1,3] + WP[2]*vrrs[3,1,4] + 1/2zeta*(vrrs[1,1,3]-eta/ze*vrrs[1,1,4])
    vrrs[9,1,1] = PA[2]*vrrs[4,1,1] + WP[2]*vrrs[4,1,2]
    vrrs[9,1,2] = PA[2]*vrrs[4,1,2] + WP[2]*vrrs[4,1,3]
    vrrs[9,1,3] = PA[2]*vrrs[4,1,3] + WP[2]*vrrs[4,1,4]
    vrrs[10,1,1] = PA[3]*vrrs[4,1,1] + WP[3]*vrrs[4,1,2] + 1/2zeta*(vrrs[1,1,1]-eta/ze*vrrs[1,1,2])
    vrrs[10,1,2] = PA[3]*vrrs[4,1,2] + WP[3]*vrrs[4,1,3] + 1/2zeta*(vrrs[1,1,2]-eta/ze*vrrs[1,1,3])
    vrrs[10,1,3] = PA[3]*vrrs[4,1,3] + WP[3]*vrrs[4,1,4] + 1/2zeta*(vrrs[1,1,3]-eta/ze*vrrs[1,1,4])
    vrrs[1,2,1] = QC[1]*vrrs[1,1,1] + WQ[1]*vrrs[1,1,2]
    vrrs[1,2,2] = QC[1]*vrrs[1,1,2] + WQ[1]*vrrs[1,1,3]
    vrrs[1,2,3] = QC[1]*vrrs[1,1,3] + WQ[1]*vrrs[1,1,4]
    vrrs[1,2,4] = QC[1]*vrrs[1,1,4] + WQ[1]*vrrs[1,1,5]
    vrrs[2,2,1] = QC[1]*vrrs[2,1,1] + WQ[1]*vrrs[2,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[2,2,2] = QC[1]*vrrs[2,1,2] + WQ[1]*vrrs[2,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[2,2,3] = QC[1]*vrrs[2,1,3] + WQ[1]*vrrs[2,1,4] + 1/2ze*vrrs[1,1,4]
    vrrs[3,2,1] = QC[1]*vrrs[3,1,1] + WQ[1]*vrrs[3,1,2]
    vrrs[3,2,2] = QC[1]*vrrs[3,1,2] + WQ[1]*vrrs[3,1,3]
    vrrs[3,2,3] = QC[1]*vrrs[3,1,3] + WQ[1]*vrrs[3,1,4]
    vrrs[4,2,1] = QC[1]*vrrs[4,1,1] + WQ[1]*vrrs[4,1,2]
    vrrs[4,2,2] = QC[1]*vrrs[4,1,2] + WQ[1]*vrrs[4,1,3]
    vrrs[4,2,3] = QC[1]*vrrs[4,1,3] + WQ[1]*vrrs[4,1,4]
    vrrs[5,2,1] = QC[1]*vrrs[5,1,1] + WQ[1]*vrrs[5,1,2] + 2/2ze*vrrs[2,1,2]
    vrrs[5,2,2] = QC[1]*vrrs[5,1,2] + WQ[1]*vrrs[5,1,3] + 2/2ze*vrrs[2,1,3]
    vrrs[6,2,1] = QC[1]*vrrs[6,1,1] + WQ[1]*vrrs[6,1,2] + 1/2ze*vrrs[3,1,2]
    vrrs[6,2,2] = QC[1]*vrrs[6,1,2] + WQ[1]*vrrs[6,1,3] + 1/2ze*vrrs[3,1,3]
    vrrs[7,2,1] = QC[1]*vrrs[7,1,1] + WQ[1]*vrrs[7,1,2] + 1/2ze*vrrs[4,1,2]
    vrrs[7,2,2] = QC[1]*vrrs[7,1,2] + WQ[1]*vrrs[7,1,3] + 1/2ze*vrrs[4,1,3]
    vrrs[8,2,1] = QC[1]*vrrs[8,1,1] + WQ[1]*vrrs[8,1,2]
    vrrs[8,2,2] = QC[1]*vrrs[8,1,2] + WQ[1]*vrrs[8,1,3]
    vrrs[9,2,1] = QC[1]*vrrs[9,1,1] + WQ[1]*vrrs[9,1,2]
    vrrs[9,2,2] = QC[1]*vrrs[9,1,2] + WQ[1]*vrrs[9,1,3]
    vrrs[10,2,1] = QC[1]*vrrs[10,1,1] + WQ[1]*vrrs[10,1,2]
    vrrs[10,2,2] = QC[1]*vrrs[10,1,2] + WQ[1]*vrrs[10,1,3]
    vrrs[1,3,1] = QC[2]*vrrs[1,1,1] + WQ[2]*vrrs[1,1,2]
    vrrs[1,3,2] = QC[2]*vrrs[1,1,2] + WQ[2]*vrrs[1,1,3]
    vrrs[1,3,3] = QC[2]*vrrs[1,1,3] + WQ[2]*vrrs[1,1,4]
    vrrs[1,3,4] = QC[2]*vrrs[1,1,4] + WQ[2]*vrrs[1,1,5]
    vrrs[2,3,1] = QC[2]*vrrs[2,1,1] + WQ[2]*vrrs[2,1,2]
    vrrs[2,3,2] = QC[2]*vrrs[2,1,2] + WQ[2]*vrrs[2,1,3]
    vrrs[2,3,3] = QC[2]*vrrs[2,1,3] + WQ[2]*vrrs[2,1,4]
    vrrs[3,3,1] = QC[2]*vrrs[3,1,1] + WQ[2]*vrrs[3,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[3,3,2] = QC[2]*vrrs[3,1,2] + WQ[2]*vrrs[3,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[3,3,3] = QC[2]*vrrs[3,1,3] + WQ[2]*vrrs[3,1,4] + 1/2ze*vrrs[1,1,4]
    vrrs[4,3,1] = QC[2]*vrrs[4,1,1] + WQ[2]*vrrs[4,1,2]
    vrrs[4,3,2] = QC[2]*vrrs[4,1,2] + WQ[2]*vrrs[4,1,3]
    vrrs[4,3,3] = QC[2]*vrrs[4,1,3] + WQ[2]*vrrs[4,1,4]
    vrrs[5,3,1] = QC[2]*vrrs[5,1,1] + WQ[2]*vrrs[5,1,2]
    vrrs[5,3,2] = QC[2]*vrrs[5,1,2] + WQ[2]*vrrs[5,1,3]
    vrrs[6,3,1] = QC[2]*vrrs[6,1,1] + WQ[2]*vrrs[6,1,2] + 1/2ze*vrrs[2,1,2]
    vrrs[6,3,2] = QC[2]*vrrs[6,1,2] + WQ[2]*vrrs[6,1,3] + 1/2ze*vrrs[2,1,3]
    vrrs[7,3,1] = QC[2]*vrrs[7,1,1] + WQ[2]*vrrs[7,1,2]
    vrrs[7,3,2] = QC[2]*vrrs[7,1,2] + WQ[2]*vrrs[7,1,3]
    vrrs[8,3,1] = QC[2]*vrrs[8,1,1] + WQ[2]*vrrs[8,1,2] + 2/2ze*vrrs[3,1,2]
    vrrs[8,3,2] = QC[2]*vrrs[8,1,2] + WQ[2]*vrrs[8,1,3] + 2/2ze*vrrs[3,1,3]
    vrrs[9,3,1] = QC[2]*vrrs[9,1,1] + WQ[2]*vrrs[9,1,2] + 1/2ze*vrrs[4,1,2]
    vrrs[9,3,2] = QC[2]*vrrs[9,1,2] + WQ[2]*vrrs[9,1,3] + 1/2ze*vrrs[4,1,3]
    vrrs[10,3,1] = QC[2]*vrrs[10,1,1] + WQ[2]*vrrs[10,1,2]
    vrrs[10,3,2] = QC[2]*vrrs[10,1,2] + WQ[2]*vrrs[10,1,3]
    vrrs[1,4,1] = QC[3]*vrrs[1,1,1] + WQ[3]*vrrs[1,1,2]
    vrrs[1,4,2] = QC[3]*vrrs[1,1,2] + WQ[3]*vrrs[1,1,3]
    vrrs[1,4,3] = QC[3]*vrrs[1,1,3] + WQ[3]*vrrs[1,1,4]
    vrrs[1,4,4] = QC[3]*vrrs[1,1,4] + WQ[3]*vrrs[1,1,5]
    vrrs[2,4,1] = QC[3]*vrrs[2,1,1] + WQ[3]*vrrs[2,1,2]
    vrrs[2,4,2] = QC[3]*vrrs[2,1,2] + WQ[3]*vrrs[2,1,3]
    vrrs[2,4,3] = QC[3]*vrrs[2,1,3] + WQ[3]*vrrs[2,1,4]
    vrrs[3,4,1] = QC[3]*vrrs[3,1,1] + WQ[3]*vrrs[3,1,2]
    vrrs[3,4,2] = QC[3]*vrrs[3,1,2] + WQ[3]*vrrs[3,1,3]
    vrrs[3,4,3] = QC[3]*vrrs[3,1,3] + WQ[3]*vrrs[3,1,4]
    vrrs[4,4,1] = QC[3]*vrrs[4,1,1] + WQ[3]*vrrs[4,1,2] + 1/2ze*vrrs[1,1,2]
    vrrs[4,4,2] = QC[3]*vrrs[4,1,2] + WQ[3]*vrrs[4,1,3] + 1/2ze*vrrs[1,1,3]
    vrrs[4,4,3] = QC[3]*vrrs[4,1,3] + WQ[3]*vrrs[4,1,4] + 1/2ze*vrrs[1,1,4]
    vrrs[5,4,1] = QC[3]*vrrs[5,1,1] + WQ[3]*vrrs[5,1,2]
    vrrs[5,4,2] = QC[3]*vrrs[5,1,2] + WQ[3]*vrrs[5,1,3]
    vrrs[6,4,1] = QC[3]*vrrs[6,1,1] + WQ[3]*vrrs[6,1,2]
    vrrs[6,4,2] = QC[3]*vrrs[6,1,2] + WQ[3]*vrrs[6,1,3]
    vrrs[7,4,1] = QC[3]*vrrs[7,1,1] + WQ[3]*vrrs[7,1,2] + 1/2ze*vrrs[2,1,2]
    vrrs[7,4,2] = QC[3]*vrrs[7,1,2] + WQ[3]*vrrs[7,1,3] + 1/2ze*vrrs[2,1,3]
    vrrs[8,4,1] = QC[3]*vrrs[8,1,1] + WQ[3]*vrrs[8,1,2]
    vrrs[8,4,2] = QC[3]*vrrs[8,1,2] + WQ[3]*vrrs[8,1,3]
    vrrs[9,4,1] = QC[3]*vrrs[9,1,1] + WQ[3]*vrrs[9,1,2] + 1/2ze*vrrs[3,1,2]
    vrrs[9,4,2] = QC[3]*vrrs[9,1,2] + WQ[3]*vrrs[9,1,3] + 1/2ze*vrrs[3,1,3]
    vrrs[10,4,1] = QC[3]*vrrs[10,1,1] + WQ[3]*vrrs[10,1,2] + 2/2ze*vrrs[4,1,2]
    vrrs[10,4,2] = QC[3]*vrrs[10,1,2] + WQ[3]*vrrs[10,1,3] + 2/2ze*vrrs[4,1,3]
    vrrs[1,5,1] = QC[1]*vrrs[1,2,1] + WQ[1]*vrrs[1,2,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])
    vrrs[1,5,2] = QC[1]*vrrs[1,2,2] + WQ[1]*vrrs[1,2,3] + 1/2eta*(vrrs[1,1,2]-zeta/ze*vrrs[1,1,3])
    vrrs[1,5,3] = QC[1]*vrrs[1,2,3] + WQ[1]*vrrs[1,2,4] + 1/2eta*(vrrs[1,1,3]-zeta/ze*vrrs[1,1,4])
    vrrs[2,5,1] = QC[1]*vrrs[2,2,1] + WQ[1]*vrrs[2,2,2] + 1/2eta*(vrrs[2,1,1]-zeta/ze*vrrs[2,1,2]) + 1/2ze*vrrs[1,2,2]
    vrrs[2,5,2] = QC[1]*vrrs[2,2,2] + WQ[1]*vrrs[2,2,3] + 1/2eta*(vrrs[2,1,2]-zeta/ze*vrrs[2,1,3]) + 1/2ze*vrrs[1,2,3]
    vrrs[3,5,1] = QC[1]*vrrs[3,2,1] + WQ[1]*vrrs[3,2,2] + 1/2eta*(vrrs[3,1,1]-zeta/ze*vrrs[3,1,2])
    vrrs[3,5,2] = QC[1]*vrrs[3,2,2] + WQ[1]*vrrs[3,2,3] + 1/2eta*(vrrs[3,1,2]-zeta/ze*vrrs[3,1,3])
    vrrs[4,5,1] = QC[1]*vrrs[4,2,1] + WQ[1]*vrrs[4,2,2] + 1/2eta*(vrrs[4,1,1]-zeta/ze*vrrs[4,1,2])
    vrrs[4,5,2] = QC[1]*vrrs[4,2,2] + WQ[1]*vrrs[4,2,3] + 1/2eta*(vrrs[4,1,2]-zeta/ze*vrrs[4,1,3])
    vrrs[5,5,1] = QC[1]*vrrs[5,2,1] + WQ[1]*vrrs[5,2,2] + 1/2eta*(vrrs[5,1,1]-zeta/ze*vrrs[5,1,2]) + 2/2ze*vrrs[2,2,2]
    vrrs[6,5,1] = QC[1]*vrrs[6,2,1] + WQ[1]*vrrs[6,2,2] + 1/2eta*(vrrs[6,1,1]-zeta/ze*vrrs[6,1,2]) + 1/2ze*vrrs[3,2,2]
    vrrs[7,5,1] = QC[1]*vrrs[7,2,1] + WQ[1]*vrrs[7,2,2] + 1/2eta*(vrrs[7,1,1]-zeta/ze*vrrs[7,1,2]) + 1/2ze*vrrs[4,2,2]
    vrrs[8,5,1] = QC[1]*vrrs[8,2,1] + WQ[1]*vrrs[8,2,2] + 1/2eta*(vrrs[8,1,1]-zeta/ze*vrrs[8,1,2])
    vrrs[9,5,1] = QC[1]*vrrs[9,2,1] + WQ[1]*vrrs[9,2,2] + 1/2eta*(vrrs[9,1,1]-zeta/ze*vrrs[9,1,2])
    vrrs[10,5,1] = QC[1]*vrrs[10,2,1] + WQ[1]*vrrs[10,2,2] + 1/2eta*(vrrs[10,1,1]-zeta/ze*vrrs[10,1,2])
    vrrs[1,6,1] = QC[1]*vrrs[1,3,1] + WQ[1]*vrrs[1,3,2]
    vrrs[1,6,2] = QC[1]*vrrs[1,3,2] + WQ[1]*vrrs[1,3,3]
    vrrs[1,6,3] = QC[1]*vrrs[1,3,3] + WQ[1]*vrrs[1,3,4]
    vrrs[2,6,1] = QC[1]*vrrs[2,3,1] + WQ[1]*vrrs[2,3,2] + 1/2ze*vrrs[1,3,2]
    vrrs[2,6,2] = QC[1]*vrrs[2,3,2] + WQ[1]*vrrs[2,3,3] + 1/2ze*vrrs[1,3,3]
    vrrs[3,6,1] = QC[1]*vrrs[3,3,1] + WQ[1]*vrrs[3,3,2]
    vrrs[3,6,2] = QC[1]*vrrs[3,3,2] + WQ[1]*vrrs[3,3,3]
    vrrs[4,6,1] = QC[1]*vrrs[4,3,1] + WQ[1]*vrrs[4,3,2]
    vrrs[4,6,2] = QC[1]*vrrs[4,3,2] + WQ[1]*vrrs[4,3,3]
    vrrs[5,6,1] = QC[1]*vrrs[5,3,1] + WQ[1]*vrrs[5,3,2] + 2/2ze*vrrs[2,3,2]
    vrrs[6,6,1] = QC[1]*vrrs[6,3,1] + WQ[1]*vrrs[6,3,2] + 1/2ze*vrrs[3,3,2]
    vrrs[7,6,1] = QC[1]*vrrs[7,3,1] + WQ[1]*vrrs[7,3,2] + 1/2ze*vrrs[4,3,2]
    vrrs[8,6,1] = QC[1]*vrrs[8,3,1] + WQ[1]*vrrs[8,3,2]
    vrrs[9,6,1] = QC[1]*vrrs[9,3,1] + WQ[1]*vrrs[9,3,2]
    vrrs[10,6,1] = QC[1]*vrrs[10,3,1] + WQ[1]*vrrs[10,3,2]
    vrrs[1,7,1] = QC[1]*vrrs[1,4,1] + WQ[1]*vrrs[1,4,2]
    vrrs[1,7,2] = QC[1]*vrrs[1,4,2] + WQ[1]*vrrs[1,4,3]
    vrrs[1,7,3] = QC[1]*vrrs[1,4,3] + WQ[1]*vrrs[1,4,4]
    vrrs[2,7,1] = QC[1]*vrrs[2,4,1] + WQ[1]*vrrs[2,4,2] + 1/2ze*vrrs[1,4,2]
    vrrs[2,7,2] = QC[1]*vrrs[2,4,2] + WQ[1]*vrrs[2,4,3] + 1/2ze*vrrs[1,4,3]
    vrrs[3,7,1] = QC[1]*vrrs[3,4,1] + WQ[1]*vrrs[3,4,2]
    vrrs[3,7,2] = QC[1]*vrrs[3,4,2] + WQ[1]*vrrs[3,4,3]
    vrrs[4,7,1] = QC[1]*vrrs[4,4,1] + WQ[1]*vrrs[4,4,2]
    vrrs[4,7,2] = QC[1]*vrrs[4,4,2] + WQ[1]*vrrs[4,4,3]
    vrrs[5,7,1] = QC[1]*vrrs[5,4,1] + WQ[1]*vrrs[5,4,2] + 2/2ze*vrrs[2,4,2]
    vrrs[6,7,1] = QC[1]*vrrs[6,4,1] + WQ[1]*vrrs[6,4,2] + 1/2ze*vrrs[3,4,2]
    vrrs[7,7,1] = QC[1]*vrrs[7,4,1] + WQ[1]*vrrs[7,4,2] + 1/2ze*vrrs[4,4,2]
    vrrs[8,7,1] = QC[1]*vrrs[8,4,1] + WQ[1]*vrrs[8,4,2]
    vrrs[9,7,1] = QC[1]*vrrs[9,4,1] + WQ[1]*vrrs[9,4,2]
    vrrs[10,7,1] = QC[1]*vrrs[10,4,1] + WQ[1]*vrrs[10,4,2]
    vrrs[1,8,1] = QC[2]*vrrs[1,3,1] + WQ[2]*vrrs[1,3,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])
    vrrs[1,8,2] = QC[2]*vrrs[1,3,2] + WQ[2]*vrrs[1,3,3] + 1/2eta*(vrrs[1,1,2]-zeta/ze*vrrs[1,1,3])
    vrrs[1,8,3] = QC[2]*vrrs[1,3,3] + WQ[2]*vrrs[1,3,4] + 1/2eta*(vrrs[1,1,3]-zeta/ze*vrrs[1,1,4])
    vrrs[2,8,1] = QC[2]*vrrs[2,3,1] + WQ[2]*vrrs[2,3,2] + 1/2eta*(vrrs[2,1,1]-zeta/ze*vrrs[2,1,2])
    vrrs[2,8,2] = QC[2]*vrrs[2,3,2] + WQ[2]*vrrs[2,3,3] + 1/2eta*(vrrs[2,1,2]-zeta/ze*vrrs[2,1,3])
    vrrs[3,8,1] = QC[2]*vrrs[3,3,1] + WQ[2]*vrrs[3,3,2] + 1/2eta*(vrrs[3,1,1]-zeta/ze*vrrs[3,1,2]) + 1/2ze*vrrs[1,3,2]
    vrrs[3,8,2] = QC[2]*vrrs[3,3,2] + WQ[2]*vrrs[3,3,3] + 1/2eta*(vrrs[3,1,2]-zeta/ze*vrrs[3,1,3]) + 1/2ze*vrrs[1,3,3]
    vrrs[4,8,1] = QC[2]*vrrs[4,3,1] + WQ[2]*vrrs[4,3,2] + 1/2eta*(vrrs[4,1,1]-zeta/ze*vrrs[4,1,2])
    vrrs[4,8,2] = QC[2]*vrrs[4,3,2] + WQ[2]*vrrs[4,3,3] + 1/2eta*(vrrs[4,1,2]-zeta/ze*vrrs[4,1,3])
    vrrs[5,8,1] = QC[2]*vrrs[5,3,1] + WQ[2]*vrrs[5,3,2] + 1/2eta*(vrrs[5,1,1]-zeta/ze*vrrs[5,1,2])
    vrrs[6,8,1] = QC[2]*vrrs[6,3,1] + WQ[2]*vrrs[6,3,2] + 1/2eta*(vrrs[6,1,1]-zeta/ze*vrrs[6,1,2]) + 1/2ze*vrrs[2,3,2]
    vrrs[7,8,1] = QC[2]*vrrs[7,3,1] + WQ[2]*vrrs[7,3,2] + 1/2eta*(vrrs[7,1,1]-zeta/ze*vrrs[7,1,2])
    vrrs[8,8,1] = QC[2]*vrrs[8,3,1] + WQ[2]*vrrs[8,3,2] + 1/2eta*(vrrs[8,1,1]-zeta/ze*vrrs[8,1,2]) + 2/2ze*vrrs[3,3,2]
    vrrs[9,8,1] = QC[2]*vrrs[9,3,1] + WQ[2]*vrrs[9,3,2] + 1/2eta*(vrrs[9,1,1]-zeta/ze*vrrs[9,1,2]) + 1/2ze*vrrs[4,3,2]
    vrrs[10,8,1] = QC[2]*vrrs[10,3,1] + WQ[2]*vrrs[10,3,2] + 1/2eta*(vrrs[10,1,1]-zeta/ze*vrrs[10,1,2])
    vrrs[1,9,1] = QC[2]*vrrs[1,4,1] + WQ[2]*vrrs[1,4,2]
    vrrs[1,9,2] = QC[2]*vrrs[1,4,2] + WQ[2]*vrrs[1,4,3]
    vrrs[1,9,3] = QC[2]*vrrs[1,4,3] + WQ[2]*vrrs[1,4,4]
    vrrs[2,9,1] = QC[2]*vrrs[2,4,1] + WQ[2]*vrrs[2,4,2]
    vrrs[2,9,2] = QC[2]*vrrs[2,4,2] + WQ[2]*vrrs[2,4,3]
    vrrs[3,9,1] = QC[2]*vrrs[3,4,1] + WQ[2]*vrrs[3,4,2] + 1/2ze*vrrs[1,4,2]
    vrrs[3,9,2] = QC[2]*vrrs[3,4,2] + WQ[2]*vrrs[3,4,3] + 1/2ze*vrrs[1,4,3]
    vrrs[4,9,1] = QC[2]*vrrs[4,4,1] + WQ[2]*vrrs[4,4,2]
    vrrs[4,9,2] = QC[2]*vrrs[4,4,2] + WQ[2]*vrrs[4,4,3]
    vrrs[5,9,1] = QC[2]*vrrs[5,4,1] + WQ[2]*vrrs[5,4,2]
    vrrs[6,9,1] = QC[2]*vrrs[6,4,1] + WQ[2]*vrrs[6,4,2] + 1/2ze*vrrs[2,4,2]
    vrrs[7,9,1] = QC[2]*vrrs[7,4,1] + WQ[2]*vrrs[7,4,2]
    vrrs[8,9,1] = QC[2]*vrrs[8,4,1] + WQ[2]*vrrs[8,4,2] + 2/2ze*vrrs[3,4,2]
    vrrs[9,9,1] = QC[2]*vrrs[9,4,1] + WQ[2]*vrrs[9,4,2] + 1/2ze*vrrs[4,4,2]
    vrrs[10,9,1] = QC[2]*vrrs[10,4,1] + WQ[2]*vrrs[10,4,2]
    vrrs[1,10,1] = QC[3]*vrrs[1,4,1] + WQ[3]*vrrs[1,4,2] + 1/2eta*(vrrs[1,1,1]-zeta/ze*vrrs[1,1,2])
    vrrs[1,10,2] = QC[3]*vrrs[1,4,2] + WQ[3]*vrrs[1,4,3] + 1/2eta*(vrrs[1,1,2]-zeta/ze*vrrs[1,1,3])
    vrrs[1,10,3] = QC[3]*vrrs[1,4,3] + WQ[3]*vrrs[1,4,4] + 1/2eta*(vrrs[1,1,3]-zeta/ze*vrrs[1,1,4])
    vrrs[2,10,1] = QC[3]*vrrs[2,4,1] + WQ[3]*vrrs[2,4,2] + 1/2eta*(vrrs[2,1,1]-zeta/ze*vrrs[2,1,2])
    vrrs[2,10,2] = QC[3]*vrrs[2,4,2] + WQ[3]*vrrs[2,4,3] + 1/2eta*(vrrs[2,1,2]-zeta/ze*vrrs[2,1,3])
    vrrs[3,10,1] = QC[3]*vrrs[3,4,1] + WQ[3]*vrrs[3,4,2] + 1/2eta*(vrrs[3,1,1]-zeta/ze*vrrs[3,1,2])
    vrrs[3,10,2] = QC[3]*vrrs[3,4,2] + WQ[3]*vrrs[3,4,3] + 1/2eta*(vrrs[3,1,2]-zeta/ze*vrrs[3,1,3])
    vrrs[4,10,1] = QC[3]*vrrs[4,4,1] + WQ[3]*vrrs[4,4,2] + 1/2eta*(vrrs[4,1,1]-zeta/ze*vrrs[4,1,2]) + 1/2ze*vrrs[1,4,2]
    vrrs[4,10,2] = QC[3]*vrrs[4,4,2] + WQ[3]*vrrs[4,4,3] + 1/2eta*(vrrs[4,1,2]-zeta/ze*vrrs[4,1,3]) + 1/2ze*vrrs[1,4,3]
    vrrs[5,10,1] = QC[3]*vrrs[5,4,1] + WQ[3]*vrrs[5,4,2] + 1/2eta*(vrrs[5,1,1]-zeta/ze*vrrs[5,1,2])
    vrrs[6,10,1] = QC[3]*vrrs[6,4,1] + WQ[3]*vrrs[6,4,2] + 1/2eta*(vrrs[6,1,1]-zeta/ze*vrrs[6,1,2])
    vrrs[7,10,1] = QC[3]*vrrs[7,4,1] + WQ[3]*vrrs[7,4,2] + 1/2eta*(vrrs[7,1,1]-zeta/ze*vrrs[7,1,2]) + 1/2ze*vrrs[2,4,2]
    vrrs[8,10,1] = QC[3]*vrrs[8,4,1] + WQ[3]*vrrs[8,4,2] + 1/2eta*(vrrs[8,1,1]-zeta/ze*vrrs[8,1,2])
    vrrs[9,10,1] = QC[3]*vrrs[9,4,1] + WQ[3]*vrrs[9,4,2] + 1/2eta*(vrrs[9,1,1]-zeta/ze*vrrs[9,1,2]) + 1/2ze*vrrs[3,4,2]
    vrrs[10,10,1] = QC[3]*vrrs[10,4,1] + WQ[3]*vrrs[10,4,2] + 1/2eta*(vrrs[10,1,1]-zeta/ze*vrrs[10,1,2]) + 2/2ze*vrrs[4,4,2]

    return vrrs[:,:,1]
end


# useful guide here http://math.mit.edu/~stevenj/18.303/fall13/lecture-10.html

diff1(n, d=1.) = inv(d) .* [ [1.0 zeros(1,n-1)]; diagm(1=>ones(n-1)) - diagm(0=>ones(n)) ]
project_on_diff1(n) = [[1 zeros(1,n-1)]; diagm(1=>0.5.*ones(n-1)) + diagm(0=>vcat(0.5.*ones(n-1), [1.0]))]
project_on_Dx(nz,nx) = kron(project_on_diff1(nx), diagm(0=>ones(nz)))
project_on_Dz(nz,nx) = kron(diagm(0=>ones(nx)), project_on_diff1(nz))
diff1x(nz,nx,dz=1.,dx=1.) = kron(diff1(nx,dx), diagm(0=>ones(nz)))
diff1z(nz,nx,dz=1.,dx=1.) = kron(diagm(0=>ones(nx)), diff1(nz,dz))

#=
x=repeat([1,2,3], 1,4)
xx=project_on_Dz(3,4)*vec(x)

function Laplacian(Nx, Ny)

	dy=inv(Ny-1);
	dx=inv(Nx-1);
   Dx = diff1(Nx) / dx
   Dy = diff1(Ny) / dy
   Ax = Dx' * Dx
   Ay = Dy' * Dy
   return kron(diagm(0=>ones(Ny)), Ax) + kron(Ay, diagm(0=>ones(Nx)))
end

n=5
A=Array(Param(n,n,ones(n*n)).A)[1:n*n,1:n*n]
B=Laplacian(n,n)

x=zeros(n,n)
x[div(n,2)+1,div(n,2)+1]=1.0
x=vec(x)

xx1=reshape(A*x,n,n)
xx2=reshape(B*x,n,n)
=#

"""
* `mpars` : medium parameters for forward or inverse mode
"""
function updateA!(pa::Param, mpars; A=pa.A)
	println("ffffwewe")
	nz=pa.nz; nx=pa.nx; dz=pa.dz; dx=pa.dx
	nznx=nz*nx
	dx=pa.dx
	dz=pa.dz

	Ax=diff1x(nz,nx,dz,dx)' * diagm(0=>project_on_Dx(nz,nx)*vec(mpars))  * diff1x(nz,nx,dz,dx)
	Az=diff1z(nz,nx,dz,dx)' * diagm(0=>project_on_Dz(nz,nx)*vec(mpars))  * diff1z(nz,nx,dz,dx)

	copyto!(A,Ax.+Az)

	# update At
	for i in 1:nznx+1
		for j in 1:nznx
			pa.At[j,i]=A[i,j]
		end
	end
	return pa
end

"""
transpose of dAdx
"""
function dAdmpars(dx,dz,nx,nz,T)

	ddz = inv(dz*dz); # for simplicity below
	ddx = inv(dx*dx);
	X=[spzeros(T,(nz*nx)+1,nz*nx) for i in 1:nz*nx]  # note the size, it has appended zeros

	m1=randn(nz*nx)
	m2=zeros(nz*nx)

	for i in 1:nz*nx
		copyto!(m2,m1)
		m2[i]+=0.1
		Ax1=diff1x(nz,nx,dz,dx)' * diagm(0=>project_on_Dx(nz,nx)*m1)  * diff1x(nz,nx,dz,dx)
		Az1=diff1z(nz,nx,dz,dx)' * diagm(0=>project_on_Dz(nz,nx)*m1)  * diff1z(nz,nx,dz,dx)

		Ax2=diff1x(nz,nx,dz,dx)' * diagm(0=>project_on_Dx(nz,nx)*m2)  * diff1x(nz,nx,dz,dx)
		Az2=diff1z(nz,nx,dz,dx)' * diagm(0=>project_on_Dz(nz,nx)*m2)  * diff1z(nz,nx,dz,dx)
		copyto!(X[i],Ax1+Az1-Ax2-Az2)
		rmul!(X[i],10)
	end

	 return X
end


using LinearAlgebra

export fitEllipse

function fitEllipse(x::Array{Float64, 1}, y::Array{Float64, 1})
    # outputs [A; B; C; D; E; F], a Vector containing the parameters of the equation
    # Ax² + Bxy + Cy² + Dx + Ey + F = 0
    #
    # Based on Direct Least Squares Fitting of Ellipses
    # by A. W. Fitzgibbon, M. Pilu, R. B. Fisher
    # http://cseweb.ucsd.edu/~mdailey/Face-Coord/ellipse-specific-fitting.pdf

    # Build design matrix
    D = [x.*x x.*y y.*y x y ones(Float64, size(x))]

    # Build scatter matrix
    S = transpose(D) * D

    # Build 6x6 constraint matrix
    C = zeros(Float64, (6, 6))
    C[1, 3] = C[3, 1] = -2
    C[2, 2] = 1

    # Solve generalised eigensystem
    geval, gevec = eigen(S, C)
    # geval - eigenvalues
    # gevec - eigenvectors

    # Find the only negative eigenvalue index
    negindex = findall(x->x<0, geval)

    # Get fitted parameters
    return a = gevec[:, negindex]

end


# Some of the common imports that all demonstrations are nice to have.

using LinearAlgebra, SparseArrays
using Plots
using CSV, ProgressMeter


# # Chapter 7

;nvidia-smi

using BenchmarkTools
using Pkg
Pkg.add("CUDAnative");
Pkg.add("CuArrays");
Pkg.add("CUDAdrv");

# ## CUDAnative

using CUDAnative
function cudaprint(n)
    @cuprintf("Thread %ld prints: %ld\n",
        threadIdx().x, n)
     return
 end

 @cuda threads=4 cudaprint(10)

 @device_code_ptx @cuda cudaprint(10)

 @device_code_sass @cuda cudaprint(10)

 @device_code_lowered @cuda cudaprint(10)

 # # CuArrays

 using CuArrays

 a=CuArray([1f0, 2f0, 3f0])

 b = a.^2 .- a.*2 .+ sqrt.(a)

# ## Monte Carlo simulations on the GPU

using CuArrays.CURAND

function pi_gpu(n)
   4 * sum(curand(Float64, n).^2 .+ curand(Float64, n).^2 .<= 1) / n
 end

 function pi_serial(n)
   inside = 0
   for i in 1:n
       x, y = rand(), rand()
       inside += (x^2 + y^2 <= 1)
   end
   return 4 * inside / n
end

@btime pi_serial(10_000_000)

@btime pi_gpu(10_000_000)

function pi_gpu32(n)
   4 * sum(curand(Float32, n).^2 .+ curand(Float32, n).^2 .<= 1) / n
end

@btime pi_gpu32(10_000_000)

# ## Writing your own kernels

function add_gpu!(y, x)
   index = threadIdx().x
   stride = blockDim().x
      for i = index:stride:length(y)
        @inbounds y[i] += x[i]
      end
    return nothing
end

N=1000_000
a = cufill(1.0f0, N)
b = cufill(2.0f0, N)
@cuda threads=256 add_gpu!(b, a)

# ## Measuring GPU Performance

a = CuArray{Float32}(undef, 1024);

@btime identity.($a);

@btime CuArrays.@sync(identity.($a));

CuArrays.@time pi_gpu(10_000_000)

# ## Performance Tips

# ### Scalar Iteration

function addcol_scalar(a, b)
   s = size(a)
   for j = 1:s[2]
      for i = 1:s[1]
         @inbounds a[i,j] = b[i,j+1] + b[i,j]
      end
   end
 end

 function addcol_fast(a, b)
   s = size(a)
   for j = 1:s[2]
      @inbounds a[:,j] .= b[:,j+1] + b[:,j]
   end
end

a = ones(Float32, 10000, 99);
b = ones(Float32, 10000, 100);
@btime addcol_scalar($a, $b)

@btime addcol_scalar($(CuArray(a)), $(CuArray(b))) samples=1;

@btime addcol_fast($(CuArray(a)), $(CuArray(b)));

CuArrays.allowscalar(false)

addcol_scalar(CuArray(a), CuArray(b))  # Will Error!

# ### Combine kernels

function addcol_faster(a, b)
   a .= @views b[:, 2:end] .+ b[:, 1:end-1]
end

@btime addcol_faster($a, $b);

@btime addcol_faster($(CuArray(a)), $(CuArray(b))) ;

# ### Process more data

a = ones(Float32,1000_000, 99);

b = ones(Float32, 1000_000, 100);

@btime addcol_faster($a, $b);

@btime addcol_faster($(CuArray(a)), $(CuArray(b))) ;

# ## Deep learning on the GPU

using Pkg
Pkg.add("Flux")
CuArrays.allowscalar(true)

using Flux, Flux.Data.MNIST, Statistics
using Flux: onehotbatch, onecold, crossentropy, throttle
using Base.Iterators: repeated, partition

imgs = MNIST.images()

labels = onehotbatch(MNIST.labels(), 0:9)

## Partition into batches of size 32
train = [(cat(float.(imgs[i])..., dims = 4), labels[:,i])
 for i in partition(1:60_000, 32)]
## Prepare test set (first 1,000 images)
tX = cat(float.(MNIST.images(:test)[1:1000])..., dims = 4)
tY = onehotbatch(MNIST.labels(:test)[1:1000], 0:9)

m = Chain(
 Conv((3, 3), 1=>32, relu),
 Conv((3, 3), 32=>32, relu),
 x -> maxpool(x, (2,2)),
 Conv((3, 3), 32=>16, relu),
 x -> maxpool(x, (2,2)),
 Conv((3, 3), 16=>10, relu),
 x -> reshape(x, :, size(x, 4)),
 Dense(90, 10), softmax)

 loss(x, y) = crossentropy(m(x), y)
accuracy(x, y) = mean(onecold(m(x)) .== onecold(y))
opt = ADAM(Flux.params(m), )
Flux.train!(loss, train[1:1], opt)
@show(accuracy(tX, tY))

gputrain = gpu.(train[1:10])
gpum = gpu(m)
gputX = gpu(tX)
gputY = gpu(tY)
gpuloss(x, y) = crossentropy(gpum(x), y)
gpuaccuracy(x, y) = mean(onecold(gpum(x)) .== onecold(y))
gpuopt = ADAM(Flux.params(gpum), )
Flux.train!(gpuloss, gpu.(train[1:1]), gpuopt, cb = () -> @show(gpuaccuracy(gputX, gputY)))
@time Flux.train!(gpuloss, gputrain, gpuopt, cb = () -> @show(gpuaccuracy(gputX, gputY)))

# ## ArrayFire

Pkg.add("ArrayFire")
using ArrayFire

a=rand(1000,1000);

b=AFArray(a);

c=b*b;

d=Array(c);

@btime $b*$b;

@btime $a*$a;

@btime begin; sin($b); sync($b); end;

@btime sin.($a);


using Graphs
using GraphViz

g = simple_graph(3)  #Directed Graph (3 vertices, 0 edges)
add_edge!(g, 1, 2)   #edge [1]: 1 -- 2
add_edge!(g, 3, 2)   #edge [2]: 3 -- 2
add_edge!(g, 3, 1)   #edge [3]: 3 -- 1

GraphViz.Graph(Graphs.to_dot(g))


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

# Basic functions
function func(x, y)
    x + y
end;

println(func(2+3im, 3)) # 5+3im

# Compact "assignment form"
f(x) = 5x + 3

println("$(f(0))") # 3

# Copying functions

function func2(x)
    x*(1+1im)
end

g = f # g(x) = 5x + 3

println("g(0) = $(g(0)), f(0) = $(f(0)), func2(0) = $(func2(0))")

f(x) = func2(x) # g = f = func2 = x*(1+1im)

println("g(0) = $(g(0)), f(0) = $(f(0)), func2(0) = $(func2(0))")

# Argument-type declarations
function exp(x::Integer, n::Integer) # exp(x, n) = x^n
    if n==1
        return x
    end

    temp = exp(x, div(n, 2))

    if n%2==1
        return temp*temp*x
    else
        return temp*temp
    end
end

println(exp(2, 5))

# Anonymous Functions
anonym = x -> (x-5)*x
println(anonym(10))

# Do-block syntax
# do keyword create a function that is passed as the first argument
function need_cb(callback::Function, x, y)
    callback(x^2 + 2*x*y + y^2)
end

need_cb(3, 5) do result
                println(result)
            end

# Function composition and piping
# data |> f1 |> f2 |> ...

function arr_to_string(arr)
    join(arr, " ")
end

function print_arr(arr)
    arr |> arr_to_string |> println
end

print_arr(1:10)

# combination of the chaining/piping and dot vectorization syntax
vec = [1, -4, 9, 16, -25]
vec .|> abs .|> sqrt .|> x->print(x," ")


using FdeSolver
using Test
using SpecialFunctions

@testset "FdeSolver.jl" begin

    tSpan = [0, 5]
    y0 = [1, 0.5, 0.3]
    β = [0.5, 0.2, 0.6]

    function F(t, y)

        F1 = 1 / sqrt(pi) * (((y[ 2] - 0.5) * (y[3] - 0.3))^(1 / 6) + t^(1 / 2))
        F2 = gamma(2.2) * (y[1] - 1)
        F3 = gamma(2.8) / gamma(2.2) * (y[2] - 0.5)

        return [F1, F2, F3]

    end

    t, Yapp = FDEsolver(F, tSpan, y0, β)

    @test @isdefined(t)
    @test @isdefined(Yapp)

end


# generate an intensity trace from a blinking fluorophore

using Revise
using SMLMSim
using Plots

## create a blinking fluorophore
γ=10000.0
q=[0 10
   1e-1 0]
f=SMLMSim.GenericFluor(γ,q)

## Simulate intensity trace

# setup sim
nframes = 1000
framerate = 10.0

## generate CTMC
starttime=0.0
endtime = (nframes) / framerate
state1=1
ctmc=SMLMSim.CTMC(q,endtime,state1)
Plots.plot(ctmc.transitiontimes,ctmc.states)

##  generate integrated photons
photons=SMLMSim.intensitytrace(f,nframes,framerate)
Plots.plot((1:nframes),photons)


export PoissonTTSolver,solve!
using LinearAlgebra

struct PoissonTTSolver
    Mx::Array{Float64,2}
    My::Array{Float64,2}
    Mxm1::Array{Float64,2}
    Mym1::Array{Float64,2}
    Dx::Array{Float64,1}
    Dy::Array{Float64,1}
    t1::Array{Float64,2}
    t2::Array{Float64,2}

    function PoissonTTSolver(lp2D)
        # Lx=l1dpαI_dirichlet(nx,dx,0.0)
        Lx=SymTridiagonal(lp2D.lpx)
        nx=size(Lx,1)
        Ly=SymTridiagonal(lp2D.lpy)
        ny=size(Ly,1)

        # Ly=l1dpαI_dirichlet(ny,dy,0.0)

        Ex=eigen(Lx)
        Mx=Ex.vectors
        Dx=Ex.values
        Mxm1=inv(Mx)

        Ey=eigen(Ly)
        My=Ey.vectors
        Dy=Ey.values
        Mym1=inv(My)
        t1=zeros(nx,ny)
        t2=zeros(nx,ny)
        new(Mx,My,Mxm1,Mym1,Dx,Dy,t1,t2)
    end
end

function solve!(Xxy,Bxy,ps::PoissonTTSolver)
    (Mx,My,Mxm1,Mym1,Dx,Dy,t1,t2)=(ps.Mx,ps.My,ps.Mxm1,ps.Mym1,ps.Dx,ps.Dy,ps.t1,ps.t2)
    (nx,ny)=size(Xxy)
    @assert size(Xxy)==size(Bxy)==(size(Mx,1),size(My,2))

    LinearAlgebra.mul!(t1,Mym1,Bxy)
    LinearAlgebra.mul!(t2,Mxm1,transpose(t1))

    for j=1:ny
        @simd for i=1:nx
            @inbounds t2[i,j]/=(Dx[i]+Dy[j])
        end
    end

    LinearAlgebra.mul!(t1,My,t2)
    LinearAlgebra.mul!(Xxy,Mx,transpose(t1))
    nothing
    # Xxy
end


module lorenz
using ReachabilityAnalysis
@taylorize function lorenz!(dx, x, params, t)
    σ, β, ρ = 10.0, 8.0/3.0, 28.0
    dx[1] = σ * (x[2] - x[1])
    dx[2] = x[1] * (ρ - x[3]) - x[2]
    dx[3] = x[1] * x[2] - β * x[3]
    return dx
end

function model(X0)
    S = @system(x' = lorenz!(x), dim:3)
    return IVP(S, X0)
end
end # module


struct BumpOnTail

    α  :: Float64
    kx :: Float64

end

function distribution( mesh1 :: UniformMesh,
                       mesh2 :: UniformMesh,
		       bot   :: BumpOnTail)

    x = mesh1.points
    v = mesh2.points
    α, kx = bot.α, bot.kx
    @. (1.0+α*cos(kx*x)) / (10*sqrt(2π)) *
        transpose(9*exp(-0.5*v^2)+2*exp(-2*(v-4.5)^2))

end


# Fractional Laplacians - fourier
# Author: Ziyi Yin, ziyi.yin@gatech.edu
# Date: July 2020

export laplacian_mask, apply_mask_fourier, laplacian_operator

###############################################################################
# Fractional laplacian implemented by 2D Discrete Fourier Transform

function laplacian_mask(n::Tuple, d::Tuple, order::Number)

	dk = 1/d[1]/n[1]
	mask = ones(Float32,n)
	center_x = (n[1]+1)/2
	center_z = (n[2]+1)/2

	x = reshape(collect(1:n[1]), :, 1)
	z = reshape(collect(1:n[2]), 1, :)

	f = dk*sqrt.((x.-center_x).^2f0.+(z.-center_z).^2f0)

	if order >= 0	# differentiation
		mask = (2*pi*f).^order
	else # integration - warning: null space of low frequency
		#delta = dk/10f0
		#mask = (2*pi*(f.+delta)).^order	# damped by delta
		center_x = Int(round(center_x))
		center_z = Int(round(center_z))
		mask[[1:center_x-1;center_x+1:end],:] = (2*pi*f[[1:center_x-1;center_x+1:end],:]).^order
		mask[center_x,[1:center_z-1;center_z+1:end]] = (2*pi*f[center_x,[1:center_z-1;center_z+1:end]]).^order
		mask[center_x,center_z] = 0.25*(mask[center_x+1,center_z]+mask[center_x-1,center_z]+mask[center_x,center_z+1]+mask[center_x,center_z-1])
	end

	return convert(Array{Float32,2},mask)
end

function apply_mask_fourier(q::Array,mask::Array,nsrc::Int)
	n = size(mask)
	q_t = reshape(q,n[1],n[2],nsrc)
	q_ans = deepcopy(q_t)
	for i = 1:nsrc
		q_f = fftshift(fft(q_t[:,:,i]))
		q_new = mask.*q_f
		q_ans[:,:,i] = real.(ifft(ifftshift(q_new)))
	end
	return reshape(q_ans,size(q))
end

function apply_mask_fourier(q::PhysicalParameter,mask::Array,nsrc::Int)
	return apply_mask_fourier(vec(q.data),mask,nsrc)
end

function apply_mask_fourier(q::judiWeights,mask::Array,nsrc::Int)
	n = size(mask)
	q_ans = deepcopy(q)
	for i = 1:nsrc
		q_t = q.weights[i]
		q_f = fftshift(fft(q_t))
		q_new = mask.*q_f
		q_ans.weights[i] = real.(ifft(ifftshift(q_new)))
	end
	return q_ans
end

function laplacian_operator(model::Model,order::Number,info::Info)
	n = model.n
	#if order >= 0
	if true
		P = joLinearFunctionFwd_T(prod(n), prod(n),
	                             v -> apply_mask_fourier(v,laplacian_mask(model,order),model,info),
	                             w -> apply_mask_fourier(w,laplacian_mask(model,order),model,info),
								 Float32,Float32,name="Fractional laplacian operator")
	else
		P1 = joLinearFunctionFwd_T(prod(n), prod(n),
								 v -> apply_mask_fourier(v,laplacian_mask(model,-order),model,info),
								 w -> apply_mask_fourier(w,laplacian_mask(model,-order),model,info),
								 Float32,Float32,name="Fractional laplacian operator")

		P = joLinearFunctionFwd_T(prod(n), prod(n),
								 v -> lsqr(P1,v,damp=1,verbose=true),
								 w -> lsqr(P1,w,damp=1,verbose=true),
								 Float32,Float32,name="Fractional laplacian operator")
	end

	return P
end


# This file uses constants defined in constant.jl
#
# Follow Bryan and Cox (1972) : An Approximate Equation of State for Numerical Models of Ocean Circulation
# Here we use Table 3, and arbitrary pick Z=0m as the formula.
#

const T_ref =   13.5
const S_ref =   32.6
const ρ_ref = 1024.458

const _ρ1 = -.20134    / ρ_ref
const _ρ2 =  .77096    / ρ_ref
const _ρ3 = -.49261e-2 / ρ_ref
const _ρ4 =  .46092e-3 / ρ_ref
const _ρ5 = -.20105e-2 / ρ_ref
const _ρ6 =  .36597e-4 / ρ_ref
const _ρ7 =  .47371e-5 / ρ_ref
const _ρ8 =  .37735e-4 / ρ_ref
const _ρ9 =  .65493e-5 / ρ_ref

@inline function TS2b(T::Float64, S::Float64)
    ΔT = T - T_ref
    ΔS = S - S_ref
    return - g * ( _ρ1*ΔT + _ρ2*ΔS +  _ρ3*(ΔT^2) + _ρ4*(ΔS^2) + _ρ5*ΔT*ΔS + _ρ6*(ΔT^3) + _ρ7*(ΔS^2)*ΔT + _ρ8*(ΔT^2)*ΔS + _ρ9*(ΔS^3) )
end

@inline function TS2α(T, S)
    ΔT = T - T_ref
    ΔS = S - S_ref
    return  - ( _ρ1 + 2*_ρ3*ΔT + _ρ5*ΔS + 3*_ρ6*(ΔT^2) + _ρ7*(ΔS^2) + 2*_ρ8*ΔS*ΔT )
end

@inline function TS2β(T, S)
    ΔT = T - T_ref
    ΔS = S - S_ref
    return  _ρ2 + 2*_ρ4*ΔS + _ρ5*ΔT + 2*_ρ7*ΔS*ΔT + _ρ8*(ΔT^2) + 3*_ρ9*(ΔS^2)
end