#get dictionary for storing log information
function getLog(m0,nc,maxit)
	 data=Dict{Symbol, Any}()
	 data[:residuals]=zeros(Float64,maxit,m0)
	 data[:feastResidual]=zeros(Float64,maxit)
	 data[:eigenvalues]=zeros(ComplexF64,maxit,m0)
	 data[:ninside]=zeros(Int64,maxit)
	 data[:iterations]=0
	 data[:insideIndices]=Dict{Int64, Any}()
	 data[:shiftIndex]=Dict{ComplexF64,Int64}()

	 data[:linResiduals]=zeros(Float64,m0,nc,maxit)
	 data[:linIts]=zeros(Int64,m0,nc,maxit)

	 data[:hlinResiduals]=zeros(Float64,m0,nc,maxit)
	 data[:hlinIts]=zeros(Int64,m0,nc,maxit)

	 return data
end

########################
# CORE FEAST FUNCTIONS #
########################

#These functions are one- and two-sided implementations of the FEAST algorithm. They are essentially outlines that encompass all possible variations of the FEAST algorithm.

#They are used by passing functions to them to perform the essential operations of the FEAST algorithm, such as evaluating the eigenvector residual and calculating the contour integrand.

#core one-sided FEAST algorithm; this function is used for all variations of FEAST
function feast_core(Tf,integrand,rrsolve,x0,nc,emid,ra,rb,eps::Float64,maxit; log=false, insideEps=1e-2, verbose=1, set_nin=false)
	 #Tf: eigenvector residual function. E.g. for linear problems Tf(l,x) = B*x*diagm(l)-A*x
	 #integrand: function that returns integral integrand evaluated at point z in complex plane, applied to a vector x. E.g. for linear FEAST integrand(z,x,l)=\(z*B-A,x)
	 #rrsolve: function for returning eigenvalue and eigenvector approximations from rayleigh ritz. E.g. for linear FEAST rrsolve(Q)=eig(Q'*A*Q,Q'*B*Q)
	 #x0: initial guess for eigenvectors
	 #nc: number of quadrature points for numerical integration
	 #emid: center of integration ellipse
	 # ra,rb: radii of integration ellipse
	 # eps: convergence tolerance
	 # maxit: maximum number of FEAST iterations
	 # insideEps: residual threshold for determining whether or not an eigenvalue inside the contour is spurious
	 # log: if true, returns dictionary object with logged convergence information
	 # verbose(0-1): how much information to print out
	 # set_nin: if equal to an integer, sets the number of eigenvalues inside the contour to 'set_nin'; used for diagnostic purposes

	 #get shapes of arrays:
	(n,m0)=size(x0)

	 #storing convergence info:
	 data=getLog(m0,nc,maxit)

	 #data[:linsysresidual]=zeros(Float64,1)
	 #data[:linsysits]=zeros(Int64,1)

	 #start with initial guess
	 x=copy(x0)

	 #initialize eigenvalue estimates:
	lest=zeros(ComplexF64,m0,1)

	#initialize iteration number:
	it=0

	 #get quadrature points from trapezoidal rule:
	 (gk,wk)=trapezoidal(nc)
	 offset=pi/nc #offset angle of first quadrature point; makes sure it isn't on real axis for hermitian problems

	 #save indices for contour points
	 for k in 1:nc
		  #curve parametrization angle:
		  theta=gk[k]*pi+pi+offset

		  #quadrature point:
		  z=emid+((ra+rb)/2)*exp(im*theta)+((ra-rb)/2)*exp(-1.0*im*theta)
		  data[:shiftIndex][z]=k
	 end

	 #Initialize FEAST subspace:
	 Q=copy(x)

	 res=1.0 #initial residual
	while res>eps && it<maxit
		it=it+1 #update number of iterations

		  data[:iterations]=it

		  #rayleigh ritz
		  (lest,x)=rrsolve(Q)

		  #sort everything by real part of eigenvalue
		  p=sortperm(real(lest))
		  lest=lest[p]
		  x=x[:,p]

		  #store convergence data
		  #indicate which eigenvalues are inside the contour
		  data[:insideIndices][it]=getInsideIndex(lest,emid,ra,rb)
		  #store all the eigenvalues
		  data[:eigenvalues][it,:]=lest

		  #calculate eigenvector residuals
		  resvecs=Tf(lest,x)
		  for i in 1:m0
				data[:residuals][it,i]=norm(resvecs[:,i])/norm(x[:,i])
		  end

		  #find the largest residual inside the contour
		  res,ninside=getMaxResInside(lest,x,resvecs,emid,ra,rb;inEps=insideEps, set_nin=set_nin)

		  data[:feastResidual][it]=res
		  data[:ninside][it]=ninside

		  verbose>0 && println("  $it  res=$res  minres=$(minimum(data[:residuals][it,:]))  nin=$ninside")

		  if(res<eps)
				if(log)
					return (lest,x,data)
			  else
					return (lest,x)
			  end
		  end

		  #apply contour integral to get FEAST subspace:
		 Q=zero(x)
		 for k in 1:nc
		 #Q=@parallel (+) for k in 1:nc
			  #integration curve is an ellipse centered at emid, with radii ra and rb

			  #curve parametrization angle:
			  theta=gk[k]*pi+pi+offset

			  #quadrature point:
			  z=emid+((ra+rb)/2)*exp(im*theta)+((ra-rb)/2)*exp(-1.0*im*theta)

				#integrand evaluated at quadrature point z

				#println("     linear system $k")

				Qk=integrand(z,x,lest,data,resvecs)

				#add integrand contribution to quadrature:
			  Q=Q+wk[k]*(((ra+rb)/2)*exp(im*theta)-((ra-rb)/2)*exp(-1.0*im*theta))*Qk
			  #    wk[k]*(((ra+rb)/2)*exp(im*theta)-((ra-rb)/2)*exp(-1.0*im*theta))*Qk
		  end

		  #Orthonormalize FEAST subspace to avoid spurious eigenpairs:
		  #0.63
		  #(Qq,Rq)=qr(Q)
		  #Q[:]=Qq

		  #0.7
		  Q=Array(qr(Q).Q)
	end

	 if(log)
		  return (lest,x,data)
	 else
		  return (lest,x)
	 end

end

#core two-sided FEAST algorithm
#Difference from symmetric FEAST: have to solve for left and right eigenvectors simultaneously and biorthogonalize them
function feastNS_core(Tf,hTf,integrand,hintegrand,factorizer,hfactorizer,FactoType,rrsolvens,biortho,x0,y0,nc,emid,ra,rb,eps,maxit; insideEps=1e-2, log=false)
	 #Tf: eigenvector residual function. E.g. for linear problems Tf(l,x) = B*x*diagm(l)-A*x
	 #hTf: hermitian conjugate transpose of residual function
	 #integrand: function that returns integral integrand evaluated at point z in complex plane, applied to a vector x. E.g. for linear FEAST integrand(z,x,l)=\(z*B-A,x)
	 #rrsolvens: function for returning eigenvalue and eigenvector approximations from rayleigh ritz. E.g. for linear FEAST rrsolve(Q)=eig(Q'*A*Q,Q'*B*Q)
	 #x0: initial guess for right eigenvectors
	 #y0: initial guess for left eigenvectors
	 #nc: number of quadrature points for numerical integration
	 #emid: center of integration ellipse
	 # ra,rb: radii of integration ellipse
	 # eps: convergence tolerance
	 # maxit: maximum number of FEAST iterations
	 # insideEps: residual threshold for determining whether or not an eigenvalue inside the contour is spurious
	 # log: if true, returns dictionary object with logged convergence information

	 #get shapes of arrays:
	(n,m0)=size(x0)

	#storing convergence info:
	 data=getLog(m0,nc,maxit)

	 #start with initial guess
	 x=copy(x0)
	 y=copy(y0)

	 #initialize eigenvalue estimates:
	lest=zeros(ComplexF64,m0,1)

	#initialize iteration number:
	it=0

	 #get quadrature points from trapezoidal rule:
	 (gk,wk)=trapezoidal(nc)
	 offset=pi/nc #offset angle of first quadrature point; makes sure it isn't on real axis for hermitian problems

	 #save indices for contour points
	 data[:storedFacto] = Vector{FactoType}(undef, nc)
	 data[:storedFactoH] = Vector{FactoType}(undef, nc)
	 print("Factorizing: ")
	 for k in 1:nc
		  #curve parametrization angle:
		  theta=gk[k]*pi+pi+offset

		  #quadrature point:
		  z=emid+((ra+rb)/2)*exp(im*theta)+((ra-rb)/2)*exp(-1.0*im*theta)
		  data[:shiftIndex][z] = k
		  data[:storedFacto][k] = factorizer(z)
		  data[:storedFactoH][k] = hfactorizer(z)
		  print(k, " ")
	 end
	 println()

	 #Initialize FEAST subspaces:
	 Q=copy(x)
	 R=copy(y)

	 res=1.0 #initial residual
	while res>eps && it<maxit
		it=it+1 #update number of iterations
		data[:iterations]=it

		  #Biorthogonalize Q and R subspaces
		  println("Biorthogonalization")
		  (Q,R)=biortho(Q,R)

		  #rayleigh ritz
		  println("Rayleigh-Ritz")
		  (y,lest,x)=rrsolvens(Q,R)

		  #calculate eigenvector residuals
		  println("Left & Right Residuals")
		  resvecs=Tf(lest,x)
		  hresvecs=hTf(lest,y)

		  println("Convergence data")
		  #store convergence data
		  #indicate which eigenvalues are inside the contour
		  data[:insideIndices][it]=getInsideIndex(lest,emid,ra,rb)
		  #store all the eigenvalues
		  data[:eigenvalues][it,:]=lest

		  println("Right Residuals (duplicate)")
		  #calculate eigenvector residuals
		  resvecs=Tf(lest,x)
		  for i in 1:m0
				data[:residuals][it,i]=norm(resvecs[:,i])/norm(x[:,i])
		  end

		  println("Inside/outside")
		  #find the largest residual inside the contour
		  res,ninside=getMaxResInside(lest,x,resvecs,emid,ra,rb; inEps=insideEps)
		  data[:feastResidual][it]=res
		  data[:ninside][it]=ninside

		  println("  $it  res=$res  minres=$(minimum(data[:residuals][it,:]))  nin=$ninside")
		  if(res<eps)
				if(log)
						  return (y,lest,x,data)
					 else
						  return (y,lest,x)
				end
		  end

		  print("Contour integral: ")
		  #apply contour integral to get FEAST subspace:
		 Q=zero(x)
		 R=zero(y)
		 for k in 1:nc
			  #integration curve is an ellipse centered at emid, with radii ra and rb
			  print(k," ")
			  #curve parametrization angle:
			  theta=gk[k]*pi+pi+offset

			  #quadrature point:
			  z=emid+((ra+rb)/2)*exp(im*theta)+((ra-rb)/2)*exp(-1.0*im*theta)

				#integrand evaluated at quadrature point z
				Qk=integrand(data[:storedFacto][k],x,lest,data,resvecs)
				Rk=hintegrand(data[:storedFactoH][k],y,lest,data,hresvecs)

				#add integrand contribution to quadrature:
			  Q=Q+wk[k]*(((ra+rb)/2)*exp(im*theta)-((ra-rb)/2)*exp(-1.0*im*theta))*Qk
			  R=R+(wk[k]*(((ra+rb)/2)*exp(im*theta)-((ra-rb)/2)*exp(-1.0*im*theta)))'*Rk
		  end
		  println()
	end

		  if(log)
				return (y,lest,x,data)
		  else
				return (y,lest,x)
		  end

end


abstract type AbstractGaussNewton{T} <: Direction{T} end

function objective!(GN::AbstractGaussNewton, y::AbstractVector, x::AbstractVector)
	norm(GN.f(y, x))
end
# WARNING overwrites value vector
function objective!(GN::AbstractGaussNewton, x::AbstractVector)
	y = DiffResults.value(GN.r)
    norm(GN.f(y, x))
end
function objective(GN::AbstractGaussNewton, x::AbstractVector)
	y = similar(DiffResults.value(GN.r))
	norm(GN.f(y, x))
end
function get_value_jacobian(GN::AbstractGaussNewton)
    return DiffResults.value(GN.r), DiffResults.jacobian(GN.r)
end

function update_jacobian!(GN::AbstractGaussNewton, x::AbstractVector)
    y, J = get_value_jacobian(GN)
    jacobian!(GN.r, GN.f, y, x, GN.cfg) # constant allocation
    return y, J
end

# update_jacobian! needs to be called prior to update_direction for correctness
function update_direction!(GN::AbstractGaussNewton, λ::Real, min_diagonal::Real = 1e-12)
    !(isinf(λ) || isnan(λ)) || throw(DomainError("$λ: has to be finite"))
    y, J = get_value_jacobian(GN)
    mul!(GN.d, J', y)
    mul!(GN.JJ, J', J)
    lm_scaling!(GN.JJ, λ, min_diagonal)
    C = cholesky!(GN.JJ) # constant allocation
    ldiv!(C, GN.d)
    GN.d .*= -1
    return y, GN.d, J, C
end

function lm_scaling!(JJ::AbstractMatrix, λ::Real, min_diagonal::Real = 1e-12)
    for i in diagind(JJ)
        JJ[i] = max(JJ[i] * (1+λ), min_diagonal)
    end
    JJ
end

################################################################################
# Gauss-Newton algorithm, minimizes sum of squares of vector-valued function f w.r.t. x
struct GaussNewton{T, F, D, R, C, JT} <: AbstractGaussNewton{T}
    f::F
    d::D # pre-allocation for parameter direction vector
    # y::Y # pre-allocation for output / residual vector
    r::R # diff results
    cfg::C # Jacobian config
    JJ::JT # pre-allocation for Hessian approximation
    function GaussNewton(f::F, x::U, y::V) where {T, U<:AbstractVector{T},
                                                V<:AbstractVector, F<:Function}
        d = similar(x)
        r = DiffResults.JacobianResult(y, x)
        cfg = JacobianConfig(f, y, x)
        J = DiffResults.jacobian(r)
        JJ = J'J
        new{T, F, U, typeof(r), typeof(cfg), typeof(JJ)}(f, d, r, cfg, JJ)
    end
end

# using qr factorization instead of cholesky of normal equations
function valdir(GN::GaussNewton, x::AbstractVector, qr::Val{true})
    y, J = update_jacobian!(GN, x)
    ldiv!(GN.d, qr!(J), y)
    GN.d .*= -1
    return norm(y), GN.d
end

function valdir(GN::GaussNewton, x::AbstractVector, min_diagonal::Real = 1e-12)
    update_jacobian!(GN, x)
    y, d, _, _ = update_direction!(GN, 0, min_diagonal)
	return norm(y), GN.d
end

################################################################################
# TODO: geodesic acceleration (see https://en.wikipedia.org/wiki/Levenberg–Marquardt_algorithm)
struct LevenbergMarquart{T, F, D, R, C, JT} <: AbstractGaussNewton{T}
    f::F
    d::D # pre-allocation for parameter direction vector
    r::R # DiffResult
    cfg::C # Jacobian config
    JJ::JT # pre-allocation for Hessian approximation
    function LevenbergMarquart(f::F, x::U, y::V) where {T, U<:AbstractVector{T},
                                                V<:AbstractVector, F<:Function}
		d = similar(x)
        r = DiffResults.JacobianResult(y, x)
        cfg = JacobianConfig(f, y, x)
        J = DiffResults.jacobian(r)
        JJ = J'J
        new{T, F, U, typeof(r), typeof(cfg), typeof(JJ)}(f, d, r, cfg, JJ)
    end
end

function valdir(LM::LevenbergMarquart, x::AbstractVector, λ::Real, min_diagonal::Real = 1e-12)
    update_jacobian!(LM, x)
    update_direction!(LM, λ, min_diagonal)
end

################################################################################
# settings for LM optimization algorithm
struct LevenbergMarquartSettings{T<:Real}
	max_iter::Int # maximum number of iterations
	min_iter::Int # see min_decrease below
	max_backtrack::Int # maximum number of consecutive backtracks (λ increases)

	min_resnorm::T # minimum norm of residual vector for termination
	min_res::T # elementwise minimum of residual vector for termination

	min_decrease::T # minimum decrease in residual norm for termination
	 # ... after at least miniter iterations have been executed

	max_step::T # maximum norm of step proposal
	min_diagonal::T # minimum diagonal entries of Hessian approximation

	min_λ::T
	max_λ::T

	increase_factor::T # factor by which λ is increased
	decrease_factor::T
end

function LevenbergMarquartSettings(;max_iter::Int = 128, max_backtrack::Int = 32,
	min_resnorm::Real = 1e-6, min_res::Real = 1e-6,
	min_decrease::Real = 1e-12, min_iter::Int = 1,
	max_step::Real = Inf, min_diagonal::Real = 1e-12,
	min_lambda::Real = 1e-12, max_lambda::Real = 1e12,
	increase_factor::Real = 10, decrease_factor::Real = 7)

	LevenbergMarquartSettings(max_iter, min_iter, max_backtrack,
								promote(min_resnorm, min_res,
								min_decrease, max_step, min_diagonal,
								min_lambda, max_lambda,
								increase_factor, decrease_factor)...)
end

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

function optimize!(LM::LevenbergMarquart{<:Real},
			x::AbstractVector, y::AbstractVector, geodesic::Val{false},
			stn::LevenbergMarquartSettings = LevenbergMarquartSettings(),
			λ::Real = 1e-6, verbose::Union{Val{true}, Val{false}} = Val(false)
			)
    oldx = copy(x)
	val, newval = objective!(LM, y, x), Inf
    for i in 1:stn.max_iter
		update_jacobian!(LM, oldx)
		newval, λ = lm_backtrack!(LM, x, oldx, y, λ, stn)
		verbose isa Val{false} || lm_verbose(i, newval, val, λ, y)
		if converged(stn, newval, val, y, i)
			return x, i
		end
		val = newval
        copy!(oldx, x)
	end
	return x, stn.max_iter
end

function optimize!(LM::LevenbergMarquart{<:Real}, x::AbstractVector, y::AbstractVector, geodesic::Val{true},
	stn::LevenbergMarquartSettings = LevenbergMarquartSettings(),
	λ::Real = 1e-6, h::Float64=0.01, α::Float64=0.75, verbose::Union{Val{true}, Val{false}} = Val(false))

    oldx = copy(x)
	val, newval = objective!(LM, y, x), Inf
	temp = zero(y)
	temp2 = zero(y)
	r = zero(y)
	dx2 = zero(x)

    for i in 1:stn.max_iter
		update_jacobian!(LM, oldx)
		newval, λ = lm_geo_backtrack!(LM, x, oldx, y, temp, temp2, r, dx2, λ, h, α, stn)
		verbose isa Val{false} || lm_verbose(i, newval, val, λ, y)
		if converged(stn, newval, val, y, i)
			return x, i
		end
		val = newval
        copy!(oldx, x)
	end
	return x, stn.max_iter
end

function lm_backtrack!(LM::LevenbergMarquart,
						x::AbstractVector, oldx::AbstractVector, y::AbstractVector,
						λ::Real, stn::LevenbergMarquartSettings)
	val = 0.
	for j in 1:stn.max_backtrack
		w, dx, _, _ = update_direction!(LM, λ, stn.min_diagonal)
		val = norm(w)
		@. x = oldx + dx
		newval = objective!(LM, y, x) # norm(LM.f(y, x))
		if !(newval ≥ val || isnan(newval) || any(x->abs(x) > stn.max_step, dx))
			λ = max(λ / stn.decrease_factor, stn.min_λ)
			return newval, λ
		end
		λ = min(λ * stn.increase_factor, stn.max_λ)
	end
	copy!(x, oldx) # if backtracking wasn't successful
	return val, λ
end

function lm_geo_backtrack!(LM::LevenbergMarquart,
	x::AbstractVector, oldx::AbstractVector, y::AbstractVector,
	temp::AbstractVector, temp2::AbstractVector, r::AbstractVector, dx2::AbstractVector,
	λ::Real, h::Float64=0.01, α::Float64=0.75, stn::LevenbergMarquartSettings = LevenbergMarquartSettings())

	val = 0.
	for j in 1:stn.max_backtrack
		w, dx, J, C = update_direction!(LM, λ, stn.min_diagonal)
		val = norm(w)
		r .= 2/h * ( (LM.f(temp2, oldx + h.*dx)- w)/h - mul!(temp, J, dx) )
		mul!(dx2, J', r)
		ldiv!(C, dx2)
		if 2(norm(dx2))/norm(dx) < α
			@. x = oldx + dx - 0.5dx2
			newval = objective!(LM, y, x) # norm(LM.f(y, x))
			if !(newval ≥ val || isnan(newval) || any(x->abs(x) > stn.max_step, dx - 0.5dx2))
				λ = max(λ / stn.decrease_factor, stn.min_λ)
				return newval, λ
			end
		end
		λ = min(λ * stn.increase_factor, stn.max_λ)
	end
	copy!(x, oldx) #
end

function converged(stn::LevenbergMarquartSettings, newval::Real, val::Real,
						y::AbstractVector, i::Int)
	newval ≤ stn.min_resnorm && return true
	(abs(newval-val) < stn.min_decrease && i ≥ stn.min_iter) && return true
	all(x -> abs(x) < stn.min_res, y) && return true
	return false
end

function lm_verbose(i::Int, newval::Real, val::Real, λ::Real, y::AbstractVector)
	println("iteration $i:
		residual norm = $(norm(y)),
		maximum residual = $(maximum(y)),
		λ = $λ,
		improvement = $(val-newval)")
end


module ShaleDrillingLikelihood_bvn_test

using ShaleDrillingLikelihood: bvncdf
using StatsFuns, Test

@testset "bvncdf: bivariate normal cdf" begin

    @test bvncdf(0.0, 0.0, 0.0) == 0.25
    @test bvncdf(0.0, Inf, 0.0) == 0.5
    @test bvncdf(Inf, 0.0, 0.0) == 0.5
    @test bvncdf(Inf, Inf, 0.0) == 1.0

    for r in -1.0:0.25:1.0
        for x in -10.0:0.5:10.0
            @test bvncdf(x, Inf, r) == normcdf(x)
            @test bvncdf(Inf, x, r) == normcdf(x)
            @test bvncdf(x, -Inf, r) == 0.0
            @test bvncdf(-Inf, x, r) == 0.0
        end
    end

    @test_throws DomainError bvncdf(0.0, 0.0, -2.0)
    @test_throws DomainError bvncdf(0.0, 0.0, 2.0)

    @test bvncdf(0.0, -100000.0, 0.0) ≈ 0.0
    @test bvncdf(0.0,  100000.0, 0.0) ≈ 0.5
    @test bvncdf(100000.0,  100000.0, 0.0000) ≈ 1.0
    @test bvncdf(-100000.0,  100000.0, 0.0000) ≈ 0.0

    for x in -100.0:100.0, y in -100.0:100.0, r in -1.0:0.1:1.0
        @test !isnan(bvncdf(x,y,r))
    end
end

end # module


#=
Part of this work is derived from astropy/photutils. The relevant derivations
are considered under a BSD 3-clause license. =#

module Aperture

using TypedTables
using Transducers

export photometry,
       Subpixel,
       CircularAperture,
       CircularAnnulus,
       EllipticalAperture,
       EllipticalAnnulus,
       RectangularAperture,
       RectangularAnnulus

"""
    AbstractAperture{T} <: AbstractMatrix{T}

The abstract super-type for Apertures.

Apertures can be thought of as a cutout or stamp of a geometric shape with shading applied. For example, a circular aperture with a diameter of 3 pixels will require a 5x5 pixel grid (when perfectly on-grid) to represent.

```jldoctest ap1
julia> ap = CircularAperture(3, 3, 2.5)
5×5 CircularAperture{Float64} with indices 1:5×1:5:
 0.136857  0.769325  0.983232  0.769325  0.136857
 0.769325  1.0       1.0       1.0       0.769325
 0.983232  1.0       1.0       1.0       0.983232
 0.769325  1.0       1.0       1.0       0.769325
 0.136857  0.769325  0.983232  0.769325  0.136857
```

This is a useful way of thinking about apertures: if we have some data, we can weight the data with the aperture

```jldoctest ap1
julia> data = fill(2, 5, 5);

julia> idxs = map(intersect, axes(ap), axes(data)) |> CartesianIndices;

julia> weighted_cutout = data[idxs] .* ap[idxs]
5×5 Array{Float64,2}:
 0.273713  1.53865  1.96646  1.53865  0.273713
 1.53865   2.0      2.0      2.0      1.53865
 1.96646   2.0      2.0      2.0      1.96646
 1.53865   2.0      2.0      2.0      1.53865
 0.273713  1.53865  1.96646  1.53865  0.273713
```

Performing aperture photometry is merely summing the weighted cutout shown above.

```jldoctest ap1
julia> flux = sum(weighted_cutout)
39.269908169872416

julia> flux ≈ (π * 2.5^2) * 2 # area of circle times intensity of 2
true
```

What's interesting about the implementation of apertures, though, is they are lazy. This means there is no stored matrix of aperture values; rather, they are calculated on the fly as needed.

```jldoctest ap1
julia> axes(ap)
(1:5, 1:5)

julia> ap[-10, -10] # out-of-bounds, but calculated on the fly
0.0

julia> ap .* ones(5, 7) # broadcasts to eachindex(data), regardless of ap bound
5×7 Array{Float64,2}:
 0.136857  0.769325  0.983232  0.769325  0.136857  0.0  0.0
 0.769325  1.0       1.0       1.0       0.769325  0.0  0.0
 0.983232  1.0       1.0       1.0       0.983232  0.0  0.0
 0.769325  1.0       1.0       1.0       0.769325  0.0  0.0
 0.136857  0.769325  0.983232  0.769325  0.136857  0.0  0.0
```

This allows extremely efficient computation of aperture photometry from small to medium sized apertures.

```julia
julia> using BenchmarkTools

julia> @btime sum(idx -> \$ap[idx] * \$data[idx], \$idxs)
  1.097 μs (0 allocations: 0 bytes)
39.26990816987243
```

This is essentially the full implementation of [`photometry`](@ref), save for the packing of additional information into a tabular form.
"""
abstract type AbstractAperture{T}  <: AbstractMatrix{T} end

"""
    bounds(::AbstractAperture)

Return the (`xlow`, `xhigh`, `ylow`, `yhigh`) bounds for a given Aperture.
"""
bounds(::AbstractAperture)

center(ap::AbstractAperture) = ap.y, ap.x # greedy

"""
    size(::AbstractAperture)

Return (`ny`, `nx`) of the aperture.
"""
function Base.size(ap::AbstractAperture)
    xmin, xmax, ymin, ymax = bounds(ap)
    return ymax - ymin + 1, xmax - xmin + 1
end

function Base.axes(ap::AbstractAperture)
    xmin, xmax, ymin, ymax = bounds(ap)
    return ymin:ymax, xmin:xmax
end

"""
    Subpixel(ap, N=1) <: AbstractAperture

Use a subpixel quadrature approximation for pixel shading instead of exact geometric methods.

For any pixel laying on the border of `ap`, this alters the shading algorithm by breaking the border pixel up into `(N, N)` subpixels. The shading value is the fraction of these subpixels within the geometric border of `ap`.

Using a subpixel shading method is sometimes faster than exact methods at the cost of accuracy. For [`CircularAperture`](@ref) the subpixel method is only faster than the exact method for `N` ~ 7. for [`EllipticalAperture`](@ref) the cutoff is `N` ~ 12, and for [`RectangularAperture`](@ref) the cutoff is `N` ~ 20.

# Examples

```jldoctest
julia> ap = CircularAperture(3, 3, 2.5)
5×5 CircularAperture{Float64} with indices 1:5×1:5:
 0.136857  0.769325  0.983232  0.769325  0.136857
 0.769325  1.0       1.0       1.0       0.769325
 0.983232  1.0       1.0       1.0       0.983232
 0.769325  1.0       1.0       1.0       0.769325
 0.136857  0.769325  0.983232  0.769325  0.136857

julia> sub_ap = Subpixel(ap, 5)
5×5 Subpixel{Float64,CircularAperture{Float64}} with indices 1:5×1:5:
 0.12  0.76  1.0  0.76  0.12
 0.76  1.0   1.0  1.0   0.76
 1.0   1.0   1.0  1.0   1.0
 0.76  1.0   1.0  1.0   0.76
 0.12  0.76  1.0  0.76  0.12
```

!!! note
    `photutils` offers a `center` shading method which is equivalent to using the `Subpixel` method with 1 subpixel. To avoid unneccessary namespace cluttering, we simply instruct users to use `Subpixel(ap)` instead.
"""
struct Subpixel{T,AP<:AbstractAperture{T}} <: AbstractAperture{T}
    ap::AP
    N::Int
end

function Base.show(io::IO, c::Subpixel)
    print(io, "Subpixel(")
    print(io, c.ap)
    print(io, ", $(c.N))")
end

center(sp_ap::Subpixel) = center(sp_ap.ap)
bounds(sp_ap::Subpixel) = bounds(sp_ap.ap)
overlap(sp_ap::Subpixel, i, j) = overlap(sp_ap.ap, i, j)

Subpixel(ap::AbstractAperture) = Subpixel(ap, 1)

@enum OverlapFlag Inside Outside Partial

function Base.getindex(ap::AbstractAperture, i::Int, j::Int)
    flag = overlap(ap, i, j)
    flag === Outside && return 0.0
    flag === Inside && return 1.0
    cy, cx = center(ap)

    return partial(ap, j - cx, i - cy)
end

# This bypasses checking aperture axes for broadcasting
Broadcast.combine_axes(ap::AbstractAperture, arrs...) = Broadcast.combine_axes(arrs...)
Broadcast.combine_axes(arr, ap::AbstractAperture) = axes(arr)

###########

"""
    photometry(::AbstractAperture, data::AbstractMatrix, [error])
    photometry(::AbstractVector{<:AbstractAperture}, data::AbstractMatrix, [error])

Perform aperture photometry on `data` given aperture(s). If `error` (the pixel-wise standard deviation) is provided, will calculate sum error. If a list of apertures is provided the output will be a `TypedTables.Table`, otherwise a `NamedTuple`.

!!! tip
    This code is automatically multi-threaded. To take advantage of this please make sure `JULIA_NUM_THREADS` is set before starting your runtime.
"""
function photometry(ap::AbstractAperture, data::AbstractMatrix, error)
    cy, cx = center(ap)
    meta = (xcenter = cx, ycenter = cy)
    idxs = map(intersect, axes(ap), axes(data), axes(error))
    any(isempty, idxs) && return (meta..., aperture_sum = 0.0, aperture_sum_err = NaN)

    aperture_sum = sum(CartesianIndices(idxs) |> Map(idx -> ap[idx] * data[idx]))
    aperture_sum_var = sum(CartesianIndices(idxs) |> Map(idx -> ap[idx] * error[idx]^2))
    aperture_sum_err = sqrt(aperture_sum_var)

    return (meta..., aperture_sum = aperture_sum, aperture_sum_err = aperture_sum_err)
end

function photometry(ap::AbstractAperture, data::AbstractMatrix)
    cy, cx = center(ap)
    meta = (xcenter = cx, ycenter = cy)
    idxs = map(intersect, axes(ap), axes(data))
    any(isempty, idxs) && return (meta..., aperture_sum = 0.0)
    aperture_sum = sum(CartesianIndices(idxs) |> Map(idx -> ap[idx] * data[idx]))
    return (meta..., aperture_sum = aperture_sum)
end

function photometry(aps::AbstractVector{<:AbstractAperture}, data::AbstractMatrix, error)
    rows = tcollect(aps |> Map(ap -> photometry(ap, data, error)))
    return Table(rows)
end

function photometry(aps::AbstractVector{<:AbstractAperture}, data::AbstractMatrix)
    rows = tcollect(aps |> Map(ap -> photometry(ap, data)))
    return Table(rows)
end

include("circular.jl")
include("elliptical.jl")
include("rectangle.jl")
include("overlap.jl")
include("plotting.jl")

end


# Checking gradients and Hessian vector products

function check_single_grad(loss::Function, grad!::Function, datapoint::Vector, theta::Vector, lambda::Float64, eps::Float64)
  p = length(theta)
  delta = randn(p)
  deriv_approx = (loss(datapoint, theta+eps*delta, lambda) - loss(datapoint, theta-eps*delta, lambda))/(2*eps)
  grad = zeros(p)
  grad!(datapoint, theta, lambda, grad)
  deriv_exact = dot(grad, delta)
  return norm(deriv_approx - deriv_exact)
end

function check_grad(loss::Function, grad!::Function, data::Matrix, theta::Vector, lambda::Float64, eps::Float64)
  n = size(data, 2)
  p = length(theta)
  delta = randn(p)
  deriv_approx = (loss(data, theta+eps*delta, lambda) - loss(data, theta-eps*delta, lambda))/(2*eps)
  grad = zeros(p)
  for i = 1:n
    grad!(vec(data[:,i]), theta, lambda, grad)
  end
  deriv_exact = dot(grad/n, delta)
  return norm(deriv_approx - deriv_exact)
end

# Compute the full gradient over the whole data matrix
function full_grad(grad!::Function, data::Matrix, theta::Vector, lambda::Float64)
  n = size(data, 2)
  result = zeros(length(theta))
  for i = 1:n
    grad!(vec(data[:,i]), theta, lambda, result)
  end
  return result
end

function single_grad(grad!::Function, datapoint::Vector, theta::Vector, lambda::Float64)
  result = zeros(length(theta))
  grad!(datapoint, theta, lambda, result)
  return result
end

# plase check with check_gradient if the gradient function is correct before you use this function
function check_hvp(grad!::Function, hvp!::Function, data::Matrix, theta::Vector, lambda::Float64, eps::Float64)
  n = size(data, 2)
  p = length(theta)
  delta = randn(p)
  hvp_exact = zeros(p)
  for i = 1:n
    hvp!(vec(data[:,i]), theta, delta, lambda, hvp_exact)
  end
  hvp_approx = (full_grad(grad!, data, theta+eps*delta, lambda) - full_grad(grad!, data, theta-eps*delta, lambda))/(2*eps)
  return norm(hvp_exact - hvp_approx)
end

function check_single_hvp(grad!::Function, hvp!::Function, datapoint::Vector, theta::Vector, lambda::Float64, eps::Float64)
  p = length(theta)
  delta = randn(p)
  hvp_exact = zeros(p)
  hvp!(datapoint, theta, delta, lambda, hvp_exact)
  hvp_approx = (single_grad(grad!, datapoint, theta+eps*delta, lambda) - single_grad(grad!, datapoint, theta-eps*delta, lambda))/(2*eps)
  return norm(hvp_exact - hvp_approx)
end

function ad_hoc_check(mode::String)
  println("Checking gradients and hessian vector products")
  data = randn(100, 100)
  if mode == "svm"
    data[100,:] = 2.0*randbool(100) - 1
  end
  if mode == "logistic regression"
    data[100,:] = 2.0*randbool(100) - 1
  end
  theta = randn(99)
  gnorm = norm(check_grad(loss, grad!, data, theta, 1e-3, 1e-4))
  hnorm = norm(check_hvp(grad!, hvp!, data, theta, 1e-3, 1e-4))
  println(gnorm)
  @assert gnorm <= 1e-4
  println(hnorm)
  @assert hnorm <= 1e-4
end


page_years = [2020, 2030, 2040, 2050, 2075, 2100, 2150, 2200, 2250, 2300]
page_year_0 = 2015

function getpageindexfromyear(year)
    i = findfirst(isequal(year), page_years)
    if i == 0
        error("Invalid PAGE year: $year.")
    end
    return i
end

function getperiodlength(year)      # same calculations made for yagg_periodspan in the model
    i = getpageindexfromyear(year)

    if year == page_years[1]
        start_year = page_year_0
    else
        start_year = page_years[i - 1]
    end

    if year == page_years[end]
        last_year = page_years[end]
    else
        last_year = page_years[i + 1]
    end

    return (last_year - start_year) / 2
end

"""
Applies undiscounting factor to get the SCC, discounted to the emissions year instead of the base year.
"""
function undiscount_scc(m::Model, year::Int)
    df = m[:EquityWeighting, :df_utilitydiscountfactor]
    consfocus0 = m[:GDP, :cons_percap_consumption_0][1]
    consfocus = m[:GDP, :cons_percap_consumption][:, 1]
    emuc = m[:EquityWeighting, :emuc_utilityconvexity]
    sccii = getpageindexfromyear(year)

    return df[sccii] * ((consfocus[sccii] / consfocus0)^-emuc)
end

@defcomp ExtraEmissions begin
    e_globalCO2emissions = Parameter(index=[time], unit="Mtonne/year")
    pulse_size = Parameter(unit="Mtonne CO2")
    pulse_year = Parameter()
    e_globalCO2emissions_adjusted = Variable(index=[time], unit="Mtonne/year")

    function run_timestep(p, v, d, t)
        if gettime(t) == p.pulse_year
            # pulse is applied to the years around this year, as a triangular distribution
            v.e_globalCO2emissions_adjusted[t] = p.e_globalCO2emissions[t] + p.pulse_size / getperiodlength(p.pulse_year)
        else
            v.e_globalCO2emissions_adjusted[t] = p.e_globalCO2emissions[t]
        end
    end
end

"""
    compute_scc(
        m::Model = get_model();
        year::Union{Int, Nothing} = nothing,
        eta::Union{Float64, Nothing} = nothing,
        prtp::Union{Float64, Nothing} = nothing,
        equity_weighting::Bool = true,
        pulse_size = 100_000.,
        n::Union{Int,Nothing}=nothing,
        trials_output_filename::Union{String, Nothing} = nothing,
        seed::Union{Int, Nothing} = nothing)

Computes the social cost of CO2 for an emissions pulse in `year` for the provided the model `m`.
If no model is provided, the default model from get_model() is used.
Units of the returned value are \$ per metric tonne of CO2.

The discounting scheme can be specified by the `eta` and `prtp` parameters, which will update the values of emuc_utilitiyconvexity
and ptp_timepreference in the model. If no values are provided, the discount factors will be computed using the default
PAGE values of emuc_utilitiyconvexity=1.1666666667 and ptp_timepreference=1.0333333333.

The size of the marginal emission defaults to 100_000 metric megatonnes of CO2 (Mtonne CO2), and this
pulse can be modified with the `pulse_size` keyword argument, in metric megatonnes of CO2 (Mtonne CO2)
(this does not change the units of the returned value, which is always normalized by the
`pulse_size` used). A pulse in `year` is actually a gradual increase throughout the timestep preceeding `year`,
followed by a gradual decrease in emissions in the timestep superseeding `year`. Emissions are linearly interpolated
between the points given by the years.

By default, `n = nothing`, and a single value for the "best guess" social cost of CO2 is returned. If a positive
value for keyword `n` is specified, then a Monte Carlo simulation with sample size `n` will run, sampling from
all of PAGE's random variables, and a vector of `n` social cost values will be returned.
Optionally providing a CSV file path to `trials_output_filename` will save all of the sampled trial data as a CSV file.
Optionally providing a `seed` value will set the random seed before running the simulation, allowing the
results to be replicated.
"""
function compute_scc(
        m::Model=get_model();
        year::Union{Int,Nothing}=nothing,
        eta::Union{Float64,Nothing}=nothing,
        prtp::Union{Float64,Nothing}=nothing,
        equity_weighting::Bool=true,
        pulse_size=75000.,
        n::Union{Int,Nothing}=nothing,
        trials_output_filename::Union{String,Nothing}=nothing,
        seed::Union{Int,Nothing}=nothing
        )

    year === nothing ? error("Must specify an emission year. Try `compute_scc(m, year=2020)`.") : nothing
    !(year in page_years) ? error("Cannot compute the scc for year $year, year must be within the model's time index $page_years.") : nothing

    if eta !== nothing
        try
            set_param!(m, :emuc_utilityconvexity, eta)      # since eta is a default parameter in PAGE, we need to use `set_param!` if it hasn't been set yet
        catch e
            update_param!(m, :emuc_utilityconvexity, eta)   # or update_param! if it has been set
        end
    end

    if prtp !== nothing
        try
            set_param!(m, :ptp_timepreference, prtp * 100)      # since prtp is a default parameter in PAGE, we need to use `set_param!` if it hasn't been set yet
        catch e
            update_param!(m, :ptp_timepreference, prtp * 100)   # or update_param! if it has been set
        end
    end

    if !equity_weighting
        try
            set_param!(m, :equity_proportion, 0)
        catch e
            update_param!(m, :equity_proportion, 0)
        end
    end

    # note here that we use `pulse_size` as the `delta` keyword argument for
    # the marginal model so we can normalize to $ per ton
    mm = get_marginal_model(m, year=year, pulse_size=pulse_size)   # Returns a marginal model that has already been run

    if n === nothing
        # Run the "best guess" social cost calculation
        run(mm)
        scc = mm[:EquityWeighting, :td_totaldiscountedimpacts] / undiscount_scc(mm.base, year)
    elseif n < 1
        error("Invalid `n` value, only values >=1 allowed.")
    else
        # Run a Monte Carlo simulation

        simdef = getsim()   # get the default simulation, need to remove :emuc_utilityconvexity and :ptp_timepreference RVs if user specified values for these
        eta !== nothing ? _remove_RV!(simdef, "EquityWeighting.emuc_utilityconvexity") : nothing
        prtp !== nothing ? _remove_RV!(simdef, "EquityWeighting.ptp_timepreference") : nothing

        seed !== nothing ? Random.seed!(seed) : nothing
        Mimi.set_payload!(simdef, (Vector{Float64}(undef, n), year))  # pass the year and a vector for storing SCC values to the `run` function
        si = run(simdef, mm, n, trials_output_filename=trials_output_filename, post_trial_func=_scc_post_trial_func)
        scc = Mimi.payload(si)[1]   # collect the values computed during the post-trial function
    end

    return scc
end

# Post trial function for calculating the scc in Monte Carlo mode
function _scc_post_trial_func(si::SimulationInstance, trial::Int, ntimesteps::Int, tup::Nothing)
    (scc_array, year) = Mimi.payload(si)
    mm = si.models[1]
    scc_array[trial] = mm[:EquityWeighting, :td_totaldiscountedimpacts] / undiscount_scc(mm.base, year)
end

# Helper function for removing a random variable by its parameter name in the model.
#   Their random variable names have been appended with a unique number by Mimi,
#   so need to look them up this way to find the RV name in the simulation definition
#   in order to use the Mimi.delete_RV! function.
#   If it is an array variable, it will be represented by multiple RVs; this deletes them all.
function _remove_RV!(simdef, _name)
    all_rv_names = collect(keys(simdef.rvdict))
    rv_names = all_rv_names[findall(startswith(String(_name)), map(String, all_rv_names))]
    [Mimi.delete_RV!(simdef, rv_name) for rv_name in rv_names]
end

"""
    compute_scc_mm(m::Model = get_model(); year::Union{Int, Nothing} = nothing, eta::Union{Float64, Nothing} = nothing, prtp::Union{Float64, Nothing} = nothing, pulse_size = 100000.)

Returns a NamedTuple (scc=scc, mm=mm) of the social cost of carbon and the MarginalModel used to compute it.
Computes the social cost of CO2 for an emissions pulse in `year` for the provided Mimi-PAGE model.

This pulse defaults to 100_000 metric megatonnes of CO2 (Mtonne CO2), and is spread over all years within the
period following `year`. A pulse in `year` is actually a gradual increase throughout the timestep preceeding `year`,
followed by a gradual decrease in emissions in the timestep superseeding `year`. Emissions are linearly interpolated
between the points given by the years.
The SCC will be returned in dollars per ton regardless of pulse size, because it is normalized over the pulse size.
If no model is provided, the default model from MimiPAGE2020.get_model() is used. Discounting scheme can be
specified by the `eta` and `prtp` parameters, which will  update the values of emuc_utilitiyconvexity and
ptp_timepreference in the model.  If no values are provided, the discount factors will be computed using the
default PAGE values of emuc_utilitiyconvexity=1.1666666667 and ptp_timepreference=1.0333333333.
"""
function compute_scc_mm(m::Model=get_model(); year::Union{Int,Nothing}=nothing, eta::Union{Float64,Nothing}=nothing, prtp::Union{Float64,Nothing}=nothing, pulse_size=75000.)
    year === nothing ? error("Must specify an emission year. Try `compute_scc(m, year=2020)`.") : nothing
    !(year in page_years) ? error("Cannot compute the scc for year $year, year must be within the model's time index $page_years.") : nothing

    if eta != nothing
        try
            set_param!(m, :emuc_utilityconvexity, eta)      # since eta is a default parameter in PAGE, we need to use `set_param!` if it hasn't been set yet
        catch e
            update_param!(m, :emuc_utilityconvexity, eta)   # or update_param! if it has been set
        end
    end

    if prtp != nothing
        try
            set_param!(m, :ptp_timepreference, prtp * 100)      # since prtp is a default parameter in PAGE, we need to use `set_param!` if it hasn't been set yet
        catch e
            update_param!(m, :ptp_timepreference, prtp * 100)   # or update_param! if it has been set
        end
    end

    # note here that we use `pulse_size` as the `delta` keyword argument for
    # the marginal model so we can normalize to $ per ton
    mm = get_marginal_model(m, year=year, pulse_size=pulse_size)   # Returns a marginal model that has already been run
    scc = mm[:EquityWeighting, :td_totaldiscountedimpacts] / undiscount_scc(mm.base, year)
    scc_disaggregated = mm[:EquityWeighting, :addt_equityweightedimpact_discountedaggregated] / undiscount_scc(mm.base, year)

    return (scc = scc, scc_disaggregated = scc_disaggregated, mm = mm)
end

"""
    get_marginal_model(m::Model = get_model(); year::Union{Int, Nothing} = nothing, pulse_size = 100000.)

Returns a Mimi MarginalModel where the provided m is the base model, and the
marginal model has additional emissions of CO2 in year `year`. If no Model m is
provided, the default model from MimiPAGE2020.get_model() is used as the base model.
Note that the returned MarginalModel has already been run. The `pulse_size` defaults
to 100_000 metric megatonnes of CO2 (Mtonne CO2), and is spread over all years within the
period following `year`.
"""
function get_marginal_model(m::Model=get_model(); year::Union{Int,Nothing}=nothing, pulse_size=75000.)
    year === nothing ? error("Must specify an emission year. Try `get_marginal_model(m, year=2020)`.") : nothing
    !(year in page_years) ? error("Cannot add marginal emissions in $year, year must be within the model's time index $page_years.") : nothing

    # note here that we use `pulse_size` as the `delta` keyword argument for
    # the marginal model so we can normalize to $ per ton
    mm = create_marginal_model(m, pulse_size)

    add_comp!(mm.modified, ExtraEmissions, :extra_emissions; after=:co2emissions)
    connect_param!(mm.modified, :extra_emissions => :e_globalCO2emissions, :co2emissions => :e_globalCO2emissions)
    set_param!(mm.modified, :extra_emissions, :pulse_size, pulse_size)
    set_param!(mm.modified, :extra_emissions, :pulse_year, year)

    connect_param!(mm.modified, :CO2Cycle => :e_globalCO2emissions, :extra_emissions => :e_globalCO2emissions_adjusted)

    run(mm)
    return mm
end

function compute_scc_mcs(m::Model, samplesize::Int; year::Union{Int,Nothing}=nothing, eta::Union{Float64,Nothing}=nothing, prtp::Union{Float64,Nothing}=nothing, pulse_size=100000.)
    # Setup of location of final results
    scc_results = zeros(samplesize)

    function mc_scc_calculation(sim_inst::SimulationInstance, trialnum::Int, ntimesteps::Int, ignore::Nothing)
        marginal = sim_inst.models[1]
        marg_damages = marginal[:EquityWeighting, :td_totaldiscountedimpacts] / undiscount_scc(mm.base, year)
        scc_results[trialnum] = marg_damages
    end

    # get simulation
    mcs = getsim()

    # Setup models
    eta == nothing ? nothing : update_param!(m, :emuc_utilityconvexity, eta)
    prtp == nothing ? nothing : update_param!(m, :ptp_timepreference, prtp * 100.)

    mm = get_marginal_model(m, year=year, pulse_size=pulse_size)   # Returns a marginal model that has already been run

    # Run
    res = run(mcs, mm, samplesize; post_trial_func=mc_scc_calculation)

    scc_results
end


using Random; Random.seed!(1234)
using OrdinaryDiffEq
using Statistics
using ForwardDiff, Calculus
using DiffEqSensitivity
using Test
using Zygote

@testset "LSS" begin
  @testset "Lorentz single parameter" begin
    function lorenz!(du,u,p,t)
      du[1] = 10*(u[2]-u[1])
      du[2] = u[1]*(p[1]-u[3]) - u[2]
      du[3] = u[1]*u[2] - (8//3)*u[3]
    end

    p = [28.0]
    tspan_init = (0.0,30.0)
    tspan_attractor = (30.0,50.0)
    u0 = rand(3)
    prob_init = ODEProblem(lorenz!,u0,tspan_init,p)
    sol_init = solve(prob_init,Tsit5())
    prob_attractor = ODEProblem(lorenz!,sol_init[end],tspan_attractor,p)
    sol_attractor = solve(prob_attractor,Vern9(),abstol=1e-14,reltol=1e-14)

    g(u,p,t) = u[end]
    function dg(out,u,p,t,i)
      fill!(out, zero(eltype(u)))
      out[end] = -one(eltype(u))
    end
    lss_problem1 = ForwardLSSProblem(sol_attractor, ForwardLSS(), g)
    lss_problem1a = ForwardLSSProblem(sol_attractor, ForwardLSS(), nothing, dg)
    lss_problem2 = ForwardLSSProblem(sol_attractor, ForwardLSS(alpha=DiffEqSensitivity.Cos2Windowing()), g)
    lss_problem2a = ForwardLSSProblem(sol_attractor, ForwardLSS(alpha=DiffEqSensitivity.Cos2Windowing()), nothing, dg)
    lss_problem3 = ForwardLSSProblem(sol_attractor, ForwardLSS(alpha=10), g)
    lss_problem3a = ForwardLSSProblem(sol_attractor, ForwardLSS(alpha=10), g, dg) #ForwardLSS with time dilation requires knowledge of g

    adjointlss_problem = AdjointLSSProblem(sol_attractor, AdjointLSS(alpha=10.0), g)
    adjointlss_problem_a = AdjointLSSProblem(sol_attractor, AdjointLSS(alpha=10.0), g, dg)

    res1 = shadow_forward(lss_problem1)
    res1a = shadow_forward(lss_problem1a)
    res2 = shadow_forward(lss_problem2)
    res2a = shadow_forward(lss_problem2a)
    res3 = shadow_forward(lss_problem3)
    res3a = shadow_forward(lss_problem3a)

    res4 = shadow_adjoint(adjointlss_problem)
    res4a = shadow_adjoint(adjointlss_problem_a)

    @test res1[1] ≈ 1 atol=5e-2
    @test res2[1] ≈ 1 atol=5e-2
    @test res3[1] ≈ 1 atol=5e-2

    @test res1 ≈ res1a atol=1e-10
    @test res2 ≈ res2a atol=1e-10
    @test res3 ≈ res3a atol=1e-10
    @test res3 ≈ res4 atol=1e-10
    @test res3 ≈ res4a atol=1e-10

    # fixed saveat to compare with concrete solve
    sol_attractor2 = solve(prob_attractor,Vern9(),abstol=1e-14,reltol=1e-14, saveat=0.01)
    lss_problem1 = ForwardLSSProblem(sol_attractor2, ForwardLSS(), g)
    lss_problem1a = ForwardLSSProblem(sol_attractor2, ForwardLSS(), nothing, dg)
    lss_problem2 = ForwardLSSProblem(sol_attractor2, ForwardLSS(alpha=DiffEqSensitivity.Cos2Windowing()), g)
    lss_problem2a = ForwardLSSProblem(sol_attractor2, ForwardLSS(alpha=DiffEqSensitivity.Cos2Windowing()), nothing, dg)
    lss_problem3 = ForwardLSSProblem(sol_attractor2, ForwardLSS(alpha=10), g)
    lss_problem3a = ForwardLSSProblem(sol_attractor2, ForwardLSS(alpha=10), g, dg) #ForwardLSS with time dilation requires knowledge of g

    adjointlss_problem = AdjointLSSProblem(sol_attractor2, AdjointLSS(alpha=10.0), g)
    adjointlss_problem_a = AdjointLSSProblem(sol_attractor2, AdjointLSS(alpha=10.0), g, dg)

    res1 = shadow_forward(lss_problem1)
    res1a = shadow_forward(lss_problem1a)
    res2 = shadow_forward(lss_problem2)
    res2a = shadow_forward(lss_problem2a)
    res3 = shadow_forward(lss_problem3)
    res3a = shadow_forward(lss_problem3a)

    res4 = shadow_adjoint(adjointlss_problem)
    res4a = shadow_adjoint(adjointlss_problem_a)

    @test res1[1] ≈ 1 atol=5e-2
    @test res2[1] ≈ 1 atol=5e-2
    @test res3[1] ≈ 1 atol=5e-2

    @test res1 ≈ res1a atol=1e-10
    @test res2 ≈ res2a atol=1e-10
    @test res3 ≈ res3a atol=1e-10
    @test res3 ≈ res4 atol=1e-10
    @test res3 ≈ res4a atol=1e-10

    function G(p; sensealg=ForwardLSS(), dt=0.01, g=nothing)
      _prob = remake(prob_attractor,p=p)
      _sol = solve(_prob,Vern9(),abstol=1e-14,reltol=1e-14,saveat=dt,sensealg=sensealg, g=g)
      sum(getindex.(_sol.u,3))
    end

    dp1 = Zygote.gradient((p)->G(p),p)
    @test res1 ≈ dp1[1] atol=1e-10

    dp1 = Zygote.gradient((p)->G(p, sensealg=ForwardLSS(alpha=DiffEqSensitivity.Cos2Windowing())),p)
    @test res2 ≈ dp1[1] atol=1e-10

    dp1 = Zygote.gradient((p)->G(p, sensealg=ForwardLSS(alpha=10), g=g),p)
    @test res3 ≈ dp1[1] atol=1e-10

    dp1 = Zygote.gradient((p)->G(p, sensealg=AdjointLSS(alpha=10.0), g=g),p)
    @test res4 ≈ dp1[1] atol=1e-10

    @show res1[1] res2[1] res3[1]
  end

  @testset "Lorentz" begin
    function lorenz!(du,u,p,t)
      du[1] = p[1]*(u[2]-u[1])
      du[2] = u[1]*(p[2]-u[3]) - u[2]
      du[3] = u[1]*u[2] - p[3]*u[3]
    end

    p = [10.0, 28.0, 8/3]

    tspan_init = (0.0,30.0)
    tspan_attractor = (30.0,50.0)
    u0 = rand(3)
    prob_init = ODEProblem(lorenz!,u0,tspan_init,p)
    sol_init = solve(prob_init,Tsit5())
    prob_attractor = ODEProblem(lorenz!,sol_init[end],tspan_attractor,p)
    sol_attractor = solve(prob_attractor,Vern9(),abstol=1e-14,reltol=1e-14)

    g(u,p,t) = u[end] + sum(p)
    function dgu(out,u,p,t,i)
      fill!(out, zero(eltype(u)))
      out[end] = -one(eltype(u))
    end
    function dgp(out,u,p,t,i)
      fill!(out, -one(eltype(p)))
    end

    lss_problem = ForwardLSSProblem(sol_attractor, ForwardLSS(alpha=10), g)
    lss_problem_a = ForwardLSSProblem(sol_attractor, ForwardLSS(alpha=10), g, (dgu,dgp))
    adjointlss_problem = AdjointLSSProblem(sol_attractor, AdjointLSS(alpha=10.0), g)
    adjointlss_problem_a = AdjointLSSProblem(sol_attractor, AdjointLSS(alpha=10.0), g, (dgu,dgp))

    resfw = shadow_forward(lss_problem)
    resfw_a = shadow_forward(lss_problem_a)
    resadj = shadow_adjoint(adjointlss_problem)
    resadj_a = shadow_adjoint(adjointlss_problem_a)

    @test resfw ≈ resadj rtol=1e-10
    @test resfw ≈ resfw_a rtol=1e-10
    @test resfw ≈ resadj_a rtol=1e-10

    sol_attractor2 = solve(prob_attractor,Vern9(),abstol=1e-14,reltol=1e-14, saveat=0.01)
    lss_problem = ForwardLSSProblem(sol_attractor2, ForwardLSS(alpha=10), g)
    resfw = shadow_forward(lss_problem)

    function G(p; sensealg=ForwardLSS(), dt=0.01, g=nothing)
      _prob = remake(prob_attractor,p=p)
      _sol = solve(_prob,Vern9(),abstol=1e-14,reltol=1e-14,saveat=dt,sensealg=sensealg, g=g)
      sum(getindex.(_sol.u,3)) + sum(p)
    end

    dp1 = Zygote.gradient((p)->G(p, sensealg=ForwardLSS(alpha=10), g=g),p)
    @test resfw ≈ dp1[1] atol=1e-10

    dp1 = Zygote.gradient((p)->G(p, sensealg=AdjointLSS(alpha=10.0), g=g),p)
    @test resfw ≈ dp1[1] atol=1e-10

    @show resfw
  end

  @testset "T0skip and T1skip" begin
    function lorenz!(du,u,p,t)
      du[1] = p[1]*(u[2]-u[1])
      du[2] = u[1]*(p[2]-u[3]) - u[2]
      du[3] = u[1]*u[2] - p[3]*u[3]
    end

    p = [10.0, 28.0, 8/3]

    tspan_init = (0.0,30.0)
    tspan_attractor = (30.0,50.0)
    u0 = rand(3)
    prob_init = ODEProblem(lorenz!,u0,tspan_init,p)
    sol_init = solve(prob_init,Tsit5())
    prob_attractor = ODEProblem(lorenz!,sol_init[end],tspan_attractor,p)
    sol_attractor = solve(prob_attractor,Vern9(),abstol=1e-14,reltol=1e-14, saveat=0.01)

    g(u,p,t) = u[end]^2/2 + sum(p)
    function dgu(out,u,p,t,i)
      fill!(out, zero(eltype(u)))
      out[end] = -u[end]
    end
    function dgp(out,u,p,t,i)
      fill!(out, -one(eltype(p)))
    end

    function G(p; sensealg=ForwardLSS(), dt=0.01, g=nothing, t0skip=0.0, t1skip=0.0)
      _prob = remake(prob_attractor,p=p)
      _sol = solve(_prob,Vern9(),abstol=1e-14,reltol=1e-14,saveat=dt,sensealg=sensealg, g=g, t0skip=t0skip, t1skip=t1skip)
      sum(getindex.(_sol.u,3).^2)/2 + sum(p)
    end

    ## ForwardLSS

    lss_problem = ForwardLSSProblem(sol_attractor, ForwardLSS(alpha=10), g)
    resfw = shadow_forward(lss_problem)

    res = deepcopy(resfw)

    dp1 = Zygote.gradient((p)->G(p, sensealg=ForwardLSS(alpha=10), g=g),p)
    @test res ≈ dp1[1] atol=1e-10

    resfw = shadow_forward(lss_problem; t0skip=10.0, t1skip=5.0)
    resskip = deepcopy(resfw)

    dp1 = Zygote.gradient((p)->G(p, sensealg=ForwardLSS(alpha=10), g=g, t0skip=10.0, t1skip=5.0),p)
    @test resskip ≈ dp1[1] atol=1e-10

    @show res resskip

    ## ForwardLSS with dgdu and dgdp

    lss_problem = ForwardLSSProblem(sol_attractor, ForwardLSS(alpha=10), g, (dgu,dgp))
    res2 = shadow_forward(lss_problem)
    @test res ≈ res2 atol=1e-10
    res2 = shadow_forward(lss_problem; t0skip=10.0, t1skip=5.0)
    @test resskip ≈ res2 atol=1e-10

    ## AdjointLSS

    lss_problem = AdjointLSSProblem(sol_attractor, AdjointLSS(alpha=10.0), g)
    res2 = shadow_adjoint(lss_problem)
    @test res ≈ res2 atol=1e-10
    res2 = shadow_adjoint(lss_problem; t0skip=10.0, t1skip=5.0)
    @test_broken resskip ≈ res2 atol=1e-10

    dp1 = Zygote.gradient((p)->G(p, sensealg=AdjointLSS(alpha=10.0), g=g),p)
    @test res ≈ dp1[1] atol=1e-10

    dp1 = Zygote.gradient((p)->G(p, sensealg=AdjointLSS(alpha=10), g=g, t0skip=10.0, t1skip=5.0),p)
    @test res2 ≈ dp1[1] atol=1e-10

    ## AdjointLSS with dgdu and dgd

    lss_problem = AdjointLSSProblem(sol_attractor, AdjointLSS(alpha=10.0), g, (dgu,dgp))
    res2 = shadow_adjoint(lss_problem)
    @test res ≈ res2 atol=1e-10
    res2 = shadow_adjoint(lss_problem; t0skip=10.0, t1skip=5.0)
    @test_broken resskip ≈ res2 atol=1e-10
  end
end

@testset "NILSS" begin
  @testset "Lorentz single parameter" begin
    function lorenz!(du,u,p,t)
      du[1] = 10*(u[2]-u[1])
      du[2] = u[1]*(p[1]-u[3]) - u[2]
      du[3] = u[1]*u[2] - (8//3)*u[3]
    end

    p = [28.0]
    tspan_init = (0.0,100.0)
    tspan_attractor = (100.0,120.0)
    u0 = rand(3)
    prob_init = ODEProblem(lorenz!,u0,tspan_init,p)
    sol_init = solve(prob_init,Tsit5())
    prob_attractor = ODEProblem(lorenz!,sol_init[end],tspan_attractor,p)

    g(u,p,t) = u[end]
    function dg(out,u,p,t,i)
      fill!(out, zero(eltype(u)))
      out[end] = -one(eltype(u))
    end

    nseg = 50 # number of segments on time interval
    nstep = 2001 # number of steps on each segment

    nilss_prob = NILSSProblem(prob_attractor, NILSS(nseg, nstep), g)
    res1 = DiffEqSensitivity.shadow_forward(nilss_prob,Tsit5())

    @test res1[1] ≈ 1 atol=5e-2
  end
end


# Test of Bellman Ford algorithm for shortest paths

using Graphs
using Base.Test

# g1: the example in CLRS (2nd Ed.)
g1 = simple_inclist(5)

g1_wedges = [
    (1, 2, 10.),
    (1, 3, 5.),
    (2, 3, 2.),
    (3, 2, 3.),
    (2, 4, 1.),
    (3, 5, 2.),
    (4, 5, 4.),
    (5, 4, 6.),
    (5, 1, 7.),
    (3, 4, 9.) ]

ne = length(g1_wedges)
eweights1 = zeros(ne)
for i = 1 : ne
    we = g1_wedges[i]
    add_edge!(g1, we[1], we[2])
    eweights1[i] = we[3]
end

@assert num_vertices(g1) == 5
@assert num_edges(g1) == 10

s1 = bellman_ford_shortest_paths(g1, eweights1, [1])

@test s1.parents == [1, 3, 1, 2, 3]
@test s1.dists == [0., 8., 5., 9., 7.]
@test !has_negative_edge_cycle(g1, eweights1)

immutable MyEdge{V}
    index::Int
    source::V
    target::V
    dist::Float64
end
Graphs.target{V}(e::MyEdge{V}, g::AbstractGraph{V}) = e.target
Graphs.source{V}(e::MyEdge{V}, g::AbstractGraph{V}) = e.source
Graphs.edge_index(e::MyEdge) = e.index
g2 = inclist([i for i=1:10], MyEdge{Int})

for i = 2:10
    for j = 1:(i - 1)
        index = div((i-1)*(i-2), 2) + j
        weight = (i == j + 1) ? 1.0 : (i - j + 1.0)
        edge = MyEdge(index, j, i, weight)
        add_edge!(g2, edge)
    end
end

type MyEdgePropertyInspector{T} <: AbstractEdgePropertyInspector{T} end

Graphs.edge_property{T,V}(inspector::MyEdgePropertyInspector{T}, e::MyEdge, g::AbstractGraph{V}) = e.dist
insp =  MyEdgePropertyInspector{Float64}()
s2 = bellman_ford_shortest_paths(g2, insp, [1])
@test s2.dists == [i * 1.0 for i=0:9]
@test s2.parents == [i ==0 ? 1 : i for i = 0:9]
@test !has_negative_edge_cycle(g2, insp)
add_edge!(g2, MyEdge{Int}(46, 10, 1, -10.0))
@test has_negative_edge_cycle(g2, insp)
@test_throws NegativeCycleError bellman_ford_shortest_paths(g2, insp, [1])

# g1: the example in CLR[~S] (1st Ed. Figure 25.7)
g3 = simple_inclist(5)

g3_wedges = [
    (1, 2,  5.),
    (1, 3, -4.),
    (1, 4,  8.),
    (2, 1, -2.),
    (3, 2,  7.),
    (3, 5,  2.),
    (4, 2, -3.),
    (4, 3,  9.),
    (5, 4,  7.),
    (5, 1,  6.)]

ne3 = length(g3_wedges)
eweights3 = zeros(ne3)
for i = 1 : ne3
    we = g3_wedges[i]
    add_edge!(g3, we[1], we[2])
    eweights3[i] = we[3]
end

@assert num_vertices(g3) == 5
@assert num_edges(g3) == 10

s3 = bellman_ford_shortest_paths(g3, eweights3, [5])

@test s3.parents == [2, 4, 1, 5, 5]
@test s3.dists == [2.0, 4.0, -2.0, 7.0, 0.0]
@test !has_negative_edge_cycle(g3, eweights1)


struct AdaDeltaIterable{T, F, R}
    w0::T
    f::F
    alpha::R
    epsilon::R
end

AdaDeltaIterable(w0, f; alpha=0.99, epsilon=1e-8) = AdaDeltaIterable(w0, f, alpha, epsilon)

Base.IteratorSize(::Type{<:AdaDeltaIterable}) = Base.IsInfinite()

mutable struct AdaDeltaState{T, G}
    w::T
    f_w
    grad_f_w::G
    update::G
    mean_squared_grad::G
    mean_squared_update::G
end

function Base.iterate(iter::AdaDeltaIterable)
    w = copy(iter.w0)
    grad_f_w, f_w = gradient(iter.f, w)
    state = AdaDeltaState(w, f_w, grad_f_w, zero(w), zero(w), zero(w))
    return IterationOutput(state.w, state.f_w, state.grad_f_w), state
end

function Base.iterate(iter::AdaDeltaIterable, state::AdaDeltaState)
    state.mean_squared_grad .= (1 - iter.alpha) .* (state.grad_f_w .^ 2) .+ iter.alpha .* state.mean_squared_grad
    state.update = sqrt.(state.mean_squared_update .+ iter.epsilon) ./ sqrt.(state.mean_squared_grad .+ iter.epsilon) .* state.grad_f_w
    state.mean_squared_update .= (1 - iter.alpha) .* (state.update .^ 2) .+ iter.alpha .* state.mean_squared_update
    state.w .-= state.update
    state.grad_f_w, state.f_w = gradient(iter.f, state.w)
    return IterationOutput(state.w, state.f_w, state.grad_f_w), state
end

struct AdaDelta
    kwargs
    AdaDelta(; kwargs...) = new(kwargs)
end

(alg::AdaDelta)(args...) = AdaDeltaIterable(args...; alg.kwargs...)


@testset "Filter time series" begin
	sig = ones(15);
	imp = ones(5)./5;
	filtsig = mmconv(sig,imp);
	@test sum(filter(!isnan,filtsig)) ≈ 11.
	for i in [1,2,14,15]
		@test isnan(filtsig[i])
	end
	@test length(filtsig) == 15

	invec = collect(1.:1:17.);
	invec[[6,10,11,12,14,17]] .= NaN;
	start1,stop1 = findblocks(invec);
	@test start1 == [1,7,13,15];
	@test stop1 == [5,9,13,16];
	# Add NaN to the first position
	invec[1] = NaN;
	invec = vcat(invec,[18]);
	start2,stop2 = findblocks(invec);
	@test start2 == [2,7,13,15,18];
	@test stop2 == [5,9,13,16,18];
	# Test Vector input without NaNs
	start3,stop3 = findblocks([1.,2.,3.,4.,5.]);
	@test start3 == [1];
	@test stop3 == [5]

	invec = collect(1.:1:17.);
	invec[[6,10,11,12,14,17]] .= NaN;
	start1,stop1 = findnanblocks(invec);
	@test start1 == [6,10,14,17];
	@test stop1 == [6,12,14,17];
	# Add NaN to the first & remove from last position
	invec[1] = NaN;
	invec = vcat(invec,[18]);
	start2,stop2 = findnanblocks(invec);
	@test start2 == [1,6,10,14,17];
	@test stop2 == [1,6,12,14,17];
	# Test Vector input without NaNs
	start3,stop3 = findnanblocks([1.,2.,3.,4.,5.]);
	@test isempty(start3)
	@test isempty(stop3)

	sig = vcat(ones(10),NaN,ones(12) .+ 1);
	out = filtblocks(sig,ones(3)./3);
	for i in [1,10,11,12,length(sig)]
		@test isnan(out[i])
	end
	@test sum(filter(!isnan,out)) ≈ 8+10*2.0
end

@testset "De-mean & -trend" begin
	o = [-1.,NaN,0.,1.];
	s = o .+ 1.234
	sf = demean(s)
	for (i,v) in enumerate(sf)
		if i != 2
			@test v ≈ o[i]
		else
			@test isnan(v)
		end
	end

	x = collect(1.:1:10);
	y = ones(length(x));
	yc0 = detrend(x,y,deg=0);
	@test isapprox(sum(yc0),0.0)

	yc1 = detrend(x,x .* 0.5,deg=1);
	@test isapprox(sum(yc1),0.0,atol=1e-10)

	o = [-1.,NaN,0.,1.];
	sf = defirst(o)
	for (i,v) in enumerate(sf)
		if i != 2
			@test v ≈ o[i]-o[1]
		else
			@test isnan(v)
		end
	end
end


using Revise, WaltonForaging
##
init_forage_time = [1,0.5,0.227]
reward = 10
θ = 5
e1 = Exponential(θ)
mean(e1)
rand(e1)
quantile(e1,0.025)
quantile(e1,0.925)

function update_time(rew_num, initial)
    M = initial*(1.3^(rew_num-1))
    EXP = Exponential(M)
    CIlow = quantile(EXP,0.025)
    CIhigh =  quantile(EXP,0.925)
    return M, CIlow, CIhigh
end

function update_time(initial; n_rewards = 15)
    df = DataFrame(Mean = Float64[],CI_L = Float64[], CI_H = Float64[], Rew = Float64[])
    prev = 0
    for i in 1:n_rewards
        m, cil, cih = update_time(i,initial)
        push!(df,(Mean = m+prev,CI_L = cil+prev, CI_H = cih+prev, Rew = i))
        prev +=m
    end
    return df
end
##
poor = update_time(1)
medium = update_time(0.5)
rich = update_time(0.227)

@df poor plot(:Rew,:Mean, ribbon = (:Mean - :CI_L,:CI_H - :Mean), fillalpha = 0.2, label = "poor")
@df medium plot!(:Rew,:Mean, ribbon = (:Mean - :CI_L,:CI_H - :Mean), fillalpha = 0.2, label = "medium")
@df rich plot!(:Rew,:Mean, ribbon = (:Mean - :CI_L,:CI_H - :Mean), fillalpha = 0.2, label = "high", legend = :topleft)
##
@df poor plot(:Mean, :Rew, ribbon = (:CI_L,:CI_H), fillalpha = 0.2,label = "poor")
@df medium plot!(:Mean, :Rew, ribbon = (:CI_L,:CI_H), fillalpha = 0.2, label = "medium")
@df rich plot!(:Mean, :Rew, ribbon = (:CI_L,:CI_H), fillalpha = 0.2, label = "high",
     legend = :topleft, xlims = (0,15), ylims=(1,18),xticks = 0:1:15,yticks = 0:1:18)
##
xaxis = 0:20
low = [0.075,32.5]
high = [0.075,57.5]
plot(xaxis,basic(xaxis,low),label = "low", color = :grey)
plot!(xaxis,basic(xaxis,high),label = "high", color = :black)
low2 = [0.075*1.1,32.5]
high2 = [0.075*1.1,57.5]
plot!(xaxis,basic(xaxis,low2),label = "low2", color = :grey, linestyle = :dash)
plot!(xaxis,basic(xaxis,high2),label = "high2", color = :black, linestyle = :dash)
##
inverse(20,low2) - inverse(20,low)
inverse(20,high2) - inverse(20,high)
vline!([inverse(20,high)], color = :red)
vline!([inverse(20,high2)], color = :red, linestyle = :dash)
vline!([inverse(20,low)], color = :green)
vline!([inverse(20,low2)], color = :green, linestyle = :dash)
hline!([20], color = :blue)
##
xaxis = 0:20
low = [0.075,32.5]
high = [0.075,57.5]
plot(xaxis,basic(xaxis,low),label = "low", color = :grey, legend = :topleft)
plot!(xaxis,basic(xaxis,high),label = "high", color = :black)
hline!([20], color = :blue, label = "threshold")
scaling_factor = 1.1
low2 = [0.075*scaling_factor,32.5]
high2 = [0.075*scaling_factor,57.5]
plot!(xaxis,basic(xaxis,low2),label = "scaled_low", color = :grey, linestyle = :dash)
plot!(xaxis,basic(xaxis,high2),label = "scaled_high", color = :black, linestyle = :dash)
vline!([inverse(20,high)], color = :red, label = "")
vline!([inverse(20,high2)], color = :red, linestyle = :dash, label = "")
vline!([inverse(20,low)], color = :green, label = "")
vline!([inverse(20,low2)], color = :green, linestyle = :dash, label = "")
##
inverse(20,low2) - inverse(20,low)
inverse(20,high2) - inverse(20,high)
##
xaxis = 0:20
low = [0.075,32.5]
high = [0.075,57.5]
plot(xaxis,basic(xaxis,low),label = "low", color = :grey, legend = :topleft)
plot!(xaxis,basic(xaxis,high),label = "high", color = :black)
hline!([20], color = :blue, label = "threshold")
scaling_factor = 1.1
hline!([20*scaling_factor], color = :blue, linestyle = :dash, label = "scaled_treshold")
vline!([inverse(20,high)], color = :red, label = "")
vline!([inverse(20*scaling_factor,high)], color = :red, linestyle = :dash, label = "")
vline!([inverse(20,low)], color = :green, label = "")
vline!([inverse(20*scaling_factor,low)], color = :green, linestyle = :dash, label = "")
##
inverse(20*scaling_factor,low) - inverse(20,low)
inverse(20*scaling_factor,high) - inverse(20,high)


using ParallelRandomFields
using ParallelRandomFields.grf2D_Threads

using ParallelStencil
using ParallelStencil.FiniteDifferences2D
@init_parallel_stencil(Threads, Float64, 2)

using MAT, Plots

@views function generate_grf2D()
    cov_typ = "expon"
    do_viz  = true
    do_save = false
    # Physics
    lx, ly  = 100.0, 100.0  # domain size
    sf      = 1.0           # standard deviation
    # -- exponential setup
    cl_e    = (10.0, 8.0)   # correlation lengths in [x,y]
    # -- gaussian setup
    cl_g    = 5.0           # correlation length isotropic
    k_m     = 100.0         # maximum value of the wave number
    # Numerics
    nh      = 10000         # inner parameter, number of harmonics
    nx, ny  = 64, 64        # numerical grid resolution
    # Derived numerics
    dx, dy  = lx/nx, ly/ny  # numerical grid step size
    # Array allocation
    Yf      = @zeros(nx, ny)
    # Visu init
    if do_viz
        X, Y = -lx/2:dx:lx/2, -ly/2:dy:ly/2
    end
    if cov_typ=="expon"
        # Generate the 2D exponential covariance function
        grf2D_expon!(Yf, sf, cl_e, nh, nx, ny, dx, dy; do_reset=true)
    elseif cov_typ=="gauss"
        # Generate the 2D Gaussian covariance function
        grf2D_gauss!(Yf, sf, cl_g, nh, k_m, nx, ny, dx, dy; do_reset=true)
    else
        error("trying to run with undefined covariance function")
    end
    # Visualisation
    if do_viz
        display(heatmap(X, Y, Array(Yf)', aspect_ratio=1, xlims=(X[1],X[end]), ylims=(Y[1],Y[end]), c=:hot, title="2D RandomField"))
    end
    if do_save  file = matopen("grf2D_$(cov_typ).mat", "w"); write(file, "grf2D", Array(Yf)); close(file)  end
    return
end

generate_grf2D()