### A Pluto.jl notebook ###
# v0.14.8

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
        el
    end
end

# ╔═╡ 34ff0254-da58-11eb-3cd1-9de133b02ae1
begin
	import Pkg
	Pkg.add(["Images","TestImages",
			"TiledIteration",
			"PlutoUI", "Plots", "Combinatorics",
			"MosaicViews", "PerceptualColourMaps", "VideoIO"])
	using Images,TestImages,LinearAlgebra,TiledIteration,PlutoUI,Plots
	using MosaicViews, PerceptualColourMaps, FFTW, VideoIO, Combinatorics
end

# ╔═╡ d238625c-7fcd-4d34-8285-6f889240248d
begin
	vid = openvideo(VideoIO.testvideo("ladybird")) do f
			collect(f)
		end
end

# ╔═╡ 340f339b-9f00-4228-bd65-0a27f58fc996
size(vid)

# ╔═╡ 55b4bd75-e696-4a3c-997a-22ad4e59aa92
begin
	function crop_to_multiple(im, factor; center::Bool = true)
		newsize = div.(size(im), factor).*factor
		offsets = div.(mod.(size(im), factor), center ? 2 : 1)
		return im[range.(offsets.+1, newsize.+offsets, step=1)...]
	end
	vidarr = crop_to_multiple(cat(vid...,dims=3),40) # video as array
end

# ╔═╡ 63c1dc77-4c8f-4afb-a220-bc06238e1d92
begin
	function array2frames(Z::Array{T,3}) where T
		collect(eachslice(Z;dims=3))
	end
	array2frames(vidarr)
end

# ╔═╡ efd07cac-cddf-474f-9cd2-3589dc62b190
begin
	tile_sizes = [5,10,20,40]
	frame_sizes = [5,10,20,40]
end

# ╔═╡ d9a53542-9a5b-4735-bc78-08b86dd35707
@bind tilesz_str Select(string.(tile_sizes) .=> string.(tile_sizes), default="10")

# ╔═╡ 1c843884-070a-41ed-aaac-c46c15bff20d
tilesz = parse(Int, tilesz_str)

# ╔═╡ 5d6be6c2-f011-49cb-9366-64aa48b72a7a
@bind framesz_str Select(string.(frame_sizes) .=> string.(frame_sizes), default="10")

# ╔═╡ 9ee7409a-9bda-4827-9630-56c0d6be3f18
framesz = parse(Int, framesz_str)

# ╔═╡ 22987ff8-c802-41d3-9a70-135fd3daf629
begin
	function tile_reshape(im,tilesz,framesz)
	  hcat(vec(map(tile -> vec(im[tile...]),
			TileIterator(axes((im)), (tilesz,tilesz,framesz))))...)
	end
	X = tile_reshape(vidarr, tilesz, framesz)
end

# ╔═╡ 3674247e-224b-485d-b981-90c0161139be
#tU,tS,tV = svd(Float64.(Gray.(X))); # this is too slow, ugh...

# ╔═╡ 0d8c3e6f-05c8-4ce2-8289-aef937afd8aa
size(X)

# ╔═╡ be92a63e-b329-46d7-a66c-07e7981e683b
Q,R = qr(Float64.(Gray.(X))');

# ╔═╡ ae872b1c-75ea-4c39-a59b-fbf66fc717ce
tVQ, tS, tU = svd(R);

# ╔═╡ 9f140f91-be19-4f03-96fc-4c82cdb14ab2
tV = Q*tVQ;

# ╔═╡ 898ca787-59ba-4775-a57d-7d7c00b3faaf
begin
	# Need to be careful here as we need to normalize things across all images!
	# Here's the code for apply diverging colour map...
#=
	    minv = minimum(img)
    maxv = maximum(img)

    if refval < minv || refval > maxv
        @warn("Reference value is outside the range of image values")
    end

    dr = maximum([maxv - refval, refval - minv])
    rnge = [-dr dr] .+ refval

    return applycolourmap(img, cmap, rnge)
	=#
	function array2frames_cmap(Z::Array{T,3}) where T
		refval = 0
		minv,maxv = extrema(Z)
		dr = maximum([maxv - refval, refval - minv])
	    rnge = [-dr dr] .+ refval

		frames = array2frames(Z)

		map(A -> colorview(RGB,
			permutedims(applycolourmap(
                        copy(A), cmap("D2"), rnge),
					   (3,1,2))), frames)
	end
	array2frames_cmap(reshape(tU[:,1], (tilesz,tilesz,framesz)))
end

# ╔═╡ 77e39090-d27f-4f3b-a2f1-fb8b8aad21ed
begin
	map(i->
		(array2frames_cmap(reshape(tU[:,i], (tilesz,tilesz,framesz)))),
		1:10)
end

# ╔═╡ 9f631d3e-d0ae-4b6e-81e8-814f604f17e9
begin

	# way more complicated than strictly necessary,
	# but this is a general solution to find the ith index
	# in a multinomial expansion

	# https://math.stackexchange.com/questions/2576122/can-stars-and-bars-method-be-used-with-constraints-on-the-number-of-stars
function stars_and_bars_upper_constraints(n::Integer,upper::NTuple{N,Int}) where N
  k = length(upper)
  #@show k
  nall = binomial(n-1,k-1)
  #@show nall
  for v in upper
    if n-1-v <= 0
      continue
    end
    nall -= binomial(n-1-v,k-1)
  end
  return nall
end

""" Find the ith tuple with sum equal to s and no element larger than the upperbounds. """
function tuple_sum_unrank(s::Integer, upper::NTuple{N,Int}, i::Integer) where N
	# TODO, if sum of upper >= s, then there is a trick...
	# https://math.stackexchange.com/questions/1388764/integer-solutions-using-pie/1388847#1388847
	# see drhab answer...
	for ta = multiexponents(N, s-N) # sum to s-N (because )
		t = NTuple{N,Int}(ta)
		t  = t.+1 # adjust to sum to s so we add one to each index
		if all(t .<= upper) # check for upper-boudns
			i -=1
			if i==0
				return t
			end
		end
	end
	return i
end

##

function idct_id(offset,sz,fsz)
  A = zeros(sz,sz,fsz)
  # find the triangular index
	for s=3:sz+sz+fsz # these are the diagonals...
		# size of the current diagonal
		ndiag = stars_and_bars_upper_constraints(s, (sz,sz,fsz))
		if offset > ndiag
			offset -= ndiag
		else
			# ideally we'd like to unrank this... but ugh...
			# instead, we are just going to enumerate and throw out
			# invalid stuff...
			#@show ndiag, offset
			t = tuple_sum_unrank(s, (sz, sz, fsz), offset)
			#@show t, typeof(t)
			@assert typeof(t) <: Tuple
			A[t...] = 1
			break
		end
	end
	return A
end

	map(i->
		array2frames_cmap(idct(idct_id(i,tilesz,framesz))),
		1:10)
end

# ╔═╡ 3182eb0a-b5c8-4292-ab9f-201473769637
map(i->array2frames_cmap(dct(reshape(tU[:,i], (tilesz,tilesz,framesz)))), 1:10)

# ╔═╡ 40cd74a9-791b-44ab-81ee-940d24fe2e4b
begin
	# Ahh, I stole this from a github issue/pull request for ...
	# https://github.com/JuliaStats/MultivariateStats.jl/pull/130/files
	## Comparison to other implementations
	# The implementation of varimax in R row-normlises the input matrix before
	# application of the algorithm and rescales the rows afterwards.
	## Reference
	# Mikkel B. Stegmann, Karl Sjöstrand, Rasmus Larsen, "Sparse modeling of
	# landmark and texture variability using the orthomax criterion,"
	# Proc. SPIE 6144, Medical Imaging 2006: Image Processing, 61441G
	# (10 March 2006); doi: 10.1117/12.651293
	function orthomax(F::AbstractMatrix, γ, miniter, maxiter, ϵ)
		n, p = size(F)
		if n < 2
			return (F, Matrix{eltype(F)}(I, p, p))
		end

		# Warm up step
		# Do one step. If that first step did not lead away from the identity
		# matrix enough use a random orthogonal matrix as a starting point.
		M = svd(F' * (F .^ 3 - γ / n * F * Diagonal(vec(sum(F .^ 2, dims=1)))))
		R = M.U * M.Vt
		if norm(R - Matrix{eltype(R)}(I, p, p)) < ϵ
			R = qr(randn(p, p)).Q
		end

		# Main iterations
		d = 0
		lastchange = NaN
		converged = false
		for i in 1:maxiter
			dold  = d
			B = F * R
			M = svd(F' * (B .^ 3 - γ / n * B * Diagonal(vec(sum(B .^ 2, dims=1)))))
			R = M.U * M.Vt
			d = sum(M.S)
			lastchange = abs(d - dold) / d
			if lastchange < ϵ && i >= miniter
				converged = true
				break
			end
		end

		converged || throw(ConvergenceException(maxiter, lastchange, ϵ))

		(F * R, R)
	 end
	tU2 = orthomax(tU[:,1:32], 1.0, 20, 1000, 1e-12)[1]
end

# ╔═╡ f3fbac2b-3582-4404-9391-a2099c97e124
begin
	map(i->
		(array2frames_cmap(reshape(tU2[:,i], (tilesz,tilesz,framesz)))),
		1:size(tU2,2))
end

# ╔═╡ 7d617284-7779-470a-821a-296572f00578
begin
	rtile = 3
	function tiled_svd_approx(r::Int, tilesz::Int, framesz::Int; tU, tS, tV)
		Y = tU[:,1:r]*Diagonal(tS[1:r])*tV[:,1:r]'
		B = similar(vidarr, Float64)
		for (i,tile) in enumerate(TileIterator(axes(vidarr), (tilesz,tilesz,framesz)))
		  B[tile...] .= reshape(Y[:,i], (tilesz,tilesz,framesz))
		end
		return B, length(tU[:,1:r])+length(tV[:,1:r])
	end
	tile_svd_approx_gray, nparam_tile_gray = tiled_svd_approx(rtile,tilesz,framesz; tU, tS, tV)
	# save a video
	frames = map(M -> Gray{N0f8}.(clamp01nan.(M)), array2frames((tile_svd_approx_gray)))
end

# ╔═╡ 6d40196d-9f4a-4baa-b0e8-d8f43244ef7d
VideoIO.save("ladybird-grey-tile-$(tilesz)-frame-$(framesz)-rank-$rtile.mp4", frames)

# ╔═╡ 77cfd1a7-2326-47da-b710-9aa443aa62cc
frames

# ╔═╡ Cell order:
# ╠═34ff0254-da58-11eb-3cd1-9de133b02ae1
# ╠═d238625c-7fcd-4d34-8285-6f889240248d
# ╠═340f339b-9f00-4228-bd65-0a27f58fc996
# ╠═55b4bd75-e696-4a3c-997a-22ad4e59aa92
# ╠═63c1dc77-4c8f-4afb-a220-bc06238e1d92
# ╠═efd07cac-cddf-474f-9cd2-3589dc62b190
# ╠═d9a53542-9a5b-4735-bc78-08b86dd35707
# ╠═1c843884-070a-41ed-aaac-c46c15bff20d
# ╠═5d6be6c2-f011-49cb-9366-64aa48b72a7a
# ╠═9ee7409a-9bda-4827-9630-56c0d6be3f18
# ╠═22987ff8-c802-41d3-9a70-135fd3daf629
# ╠═3674247e-224b-485d-b981-90c0161139be
# ╠═0d8c3e6f-05c8-4ce2-8289-aef937afd8aa
# ╠═be92a63e-b329-46d7-a66c-07e7981e683b
# ╠═ae872b1c-75ea-4c39-a59b-fbf66fc717ce
# ╠═9f140f91-be19-4f03-96fc-4c82cdb14ab2
# ╠═898ca787-59ba-4775-a57d-7d7c00b3faaf
# ╠═77e39090-d27f-4f3b-a2f1-fb8b8aad21ed
# ╠═9f631d3e-d0ae-4b6e-81e8-814f604f17e9
# ╠═3182eb0a-b5c8-4292-ab9f-201473769637
# ╠═40cd74a9-791b-44ab-81ee-940d24fe2e4b
# ╠═f3fbac2b-3582-4404-9391-a2099c97e124
# ╠═7d617284-7779-470a-821a-296572f00578
# ╠═6d40196d-9f4a-4baa-b0e8-d8f43244ef7d
# ╠═77cfd1a7-2326-47da-b710-9aa443aa62cc


using AbstractDomains

a = HyperBox([0.0 0.0 0.0
              1.0 1.0 1.0])
b = HyperBox([0.0 0.0 0.0 0.0
              1.0 1.0 1.0 1.0])
@test mid(a) == [0.5,0.5,0.5]
@test ndims(a) == 3
@test a != b
@test isrelational(HyperBox) == false

# Mid
c = HyperBox([0.0 0.0
              1.0 1.0])
prev = prevfloat(0.5)
next = nextfloat(0.5)
c1 = HyperBox([0.0 0.0
              0.5 0.5])
c2 = HyperBox([next 0.0
              1.0 0.5])
c3 = HyperBox([0.0 next
               0.5 1.0])
c4 = HyperBox([next next
               1.0  1.0])
q = mid_split(c)

# Partial Split
d = HyperBox([0.0 0.0 0.0
              10.0 20.0 1.0])
split_points = Dict(1 => 5.0, 3 => 0.5)
@test length(partial_split_box(d,split_points)) == 4
@test length(mid_partial_split(d,[1,2,3])) == 8
split_points2 = Dict(1 => 5.0, 2=> 0.1, 3 => 0.5)
@test length(partial_split_box(d,split_points2)) == 8

@test isequal(q[1],c1)
@test isequal(q[2],c2)
@test isequal(q[3],c3)
@test isequal(q[4],c4)

@test all(Bool[0 <= rb <= 1 for rb in rand(b)])


#nb # %% A slide [markdown] {"slideshow": {"slide_type": "Slide"}}
# ## Meanderline/Strip-Based CPSS
# This example comes from the same authors as the previous example.  The paper is
# A. Ericsson and D. Sjöberg, "Design and Analysis of a Multilayer Meander Line
# Circular Polarization Selective Structure", IEEE Trans. Antennas Propagat.,
# Vol. 65, No. 8, Aug 2017, pp. 4089-4101.
# The design is similar to that of the previous example except that here, the two ``\pm 45^\circ``
# rotated meanderlines are replaced with rectangular strips.
# This allows us to employ the `diagstrip` element and the `orient` keyword for the
# `meander` elements to maintain the same, square unit cell for all sheets. By doing this
# we allow PSSFSS to rigorously account for the inter-sheet coupling using multiple
# high-order modes in the generalized scattering matrix (GSM) formulation.
#
#-
# We begin by computing the skin depth and sheet resistance for the
# copper traces.  The conductivity and thickness are as stated in the paper:

## Compute skin depth and sheet resistance:
using PSSFSS.Constants: μ₀ # free-space permeability [H/m]
f = (10:0.1:20) * 1e9 # frequencies in Hz
σ = 58e6 # conductivity of metalization [S/m]
t = 18e-6 # metalization thickness [m]
Δ = sqrt.(2 ./ (2π*f*σ*μ₀)) # skin depth [m]
@show extrema(t./Δ)
#-
Rs = 1 ./ (σ * Δ)
@show extrema(Rs)

# We see that the metal is many skin depths thick (effectively infinitely thick) so that we can use
# the thick metal surface sheet resistance formula.  Since the latter varies with frequency, we approximate
# it over the band 10-20 GHz by a value near its mean: 0.032 Ω/□.

# Here is the script that analyzes the design from the referenced paper:

using PSSFSS
P = 5.2 # side length of unit square
d1 = 2.61 # Inner layer thickness
d2 = 3.81 # Outer layer thickness
h0 = 2.44 # Inner meanderline dimension (using paper's definition of h)
h2 = 2.83 # Outer meanderline dimension (using paper's definition of h)
w0x = 0.46 # Inner meanderline line thickness of traces running along x
w0y = 0.58 # Inner meanderline line thickness of traces running along y
w1 = 0.21 # Rectangular strips width
w2x = 0.25   # Outer meanderline line thickness of traces running along x
w2y = 0.17 # Outer meanderline line thickness of traces running along y

outer(orient) = meander(a=P, b=P, w1=w2y, w2=w2x, h=h2+w2x, units=mm, ntri=600,
                        Rsheet=0.032, orient=orient)
inner = meander(a=P, b=P, w1=w0y, w2=w0x, h=h0+w0x, units=mm, ntri=600, Rsheet=0.032)
strip(orient) = diagstrip(P=P, w=w1, units=mm, Nl=60, Nw=4, orient=orient, Rsheet=0.032)

substrate = Layer(width=0.127mm, epsr=2.17, tandel=0.0009)
foam(w) = Layer(width=w, epsr=1.043, tandel=0.0017)
sheets = [outer(-90), strip(-45), inner, strip(45), outer(90)]
strata = [
    Layer()
    substrate
    sheets[1]
    foam(d2 * 1mm)
    substrate
    sheets[2]
    foam(d1 * 1mm)
    sheets[3]
    substrate
    foam(d1 * 1mm)
    substrate
    sheets[4]
    foam(d2 * 1mm)
    sheets[5]
    substrate
    Layer() ]
steering = (θ=0, ϕ=0)
flist = 10:0.1:20

results = analyze(strata, flist, steering, logfile=devnull,
                  resultfile=devnull, showprogress=false)
#md nothing # hide

# The PSSFSS run took about 85 seconds on my machine.  Here are plots of the five sheets:

using Plots
default()
ps = []
for k in 1:5
    push!(ps, plot(sheets[k], unitcell=true, title="Sheet $k", linecolor=:red))
end
plot(ps..., layout=5)
#md savefig("cpssb1.png"); nothing  # hide
#-
#md # ![](cpssb1.png)
#-
# Notice that for all 5 sheets, the unit cell is a square of constant side length and is unrotated.
# We can see from the log file (of a previous run where it was not suppressed) that this allows
# PSSFSS to use additional modes in the GSM cascading procedure:
#-
# ```
# Starting PSSFSS analysis on 2021-05-26 at 09:54:02.902
#
#
# Dielectric layer information...
#
#  Layer  Width  units  epsr   tandel   mur  mtandel modes  beta1x  beta1y  beta2x  beta2y
#  ----- ------------- ------- ------ ------- ------ ----- ------- ------- ------- -------
#      1    0.0000  mm    1.00 0.0000    1.00 0.0000     2  1208.3     0.0     0.0  1208.3
#      2    0.1270  mm    2.17 0.0009    1.00 0.0000     0     0.0     0.0     0.0     0.0
#  ==================  Sheet   1  ========================  1208.3     0.0     0.0  1208.3
#      3    3.8100  mm    1.04 0.0017    1.00 0.0000    10  1208.3     0.0     0.0  1208.3
#      4    0.1270  mm    2.17 0.0009    1.00 0.0000     0     0.0     0.0     0.0     0.0
#  ==================  Sheet   2  ========================  1208.3     0.0     0.0  1208.3
#      5    2.6100  mm    1.04 0.0017    1.00 0.0000    18  1208.3     0.0     0.0  1208.3
#  ==================  Sheet   3  ========================  1208.3     0.0     0.0  1208.3
#      6    0.1270  mm    2.17 0.0009    1.00 0.0000     0     0.0     0.0     0.0     0.0
#      7    2.6100  mm    1.04 0.0017    1.00 0.0000    18  1208.3     0.0     0.0  1208.3
#      8    0.1270  mm    2.17 0.0009    1.00 0.0000     0     0.0     0.0     0.0     0.0
#  ==================  Sheet   4  ========================  1208.3     0.0     0.0  1208.3
#      9    3.8100  mm    1.04 0.0017    1.00 0.0000    10  1208.3     0.0     0.0  1208.3
#  ==================  Sheet   5  ========================  1208.3     0.0     0.0  1208.3
#     10    0.1270  mm    2.17 0.0009    1.00 0.0000     0     0.0     0.0     0.0     0.0
#     11    0.0000  mm    1.00 0.0000    1.00 0.0000     2  1208.3     0.0     0.0  1208.3
# ...
# ```
#-
# Layers 3 and 9 were assigned 10 modes each.  Layers 5 and 7, being thinner were assigned
# 18 modes each. The numbers of modes are determined automatically by PSSFSS to ensure
# accurate cascading.
#-
# Here are comparison plots of PSSFSS versus highly converged CST predictions digitized from
# plots presented in the paper:

using Plots, DelimitedFiles
RLl = -extract_result(results, @outputs s11db(l,l))
AR11l = extract_result(results, @outputs ar11db(l))
IL21r = -extract_result(results, @outputs s21db(r,r))
AR21r = extract_result(results, @outputs ar21db(r))

default(lw=2, xlabel="Frequency (GHz)", xlim=(10,20), xtick=10:2:20,
        framestyle=:box, gridalpha=0.3)

plot(flist,RLl,title="LHCP → LHCP Return Loss", label="PSSFSS",
         ylabel="Return Loss (dB)", ylim=(0,3), ytick=0:0.5:3)
cst = readdlm("../src/assets/ericsson_cpss_digitized_rllhcp.csv", ',')
plot!(cst[:,1], cst[:,2], label="CST")
#md savefig("cpssb2.png"); nothing  # hide
#-
#md # ![](cpssb2.png)
#-
plot(flist,AR11l,title="LHCP → LHCP Reflected Axial Ratio", label="PSSFSS",
         ylabel="Axial Ratio (dB)", ylim=(0,3), ytick=0:0.5:3)
cst = readdlm("../src/assets/ericsson_cpss_digitized_arlhcp.csv", ',')
plot!(cst[:,1], cst[:,2], label="CST")
#md savefig("cpssb3.png"); nothing  # hide
#-
#md # ![](cpssb3.png)
#-
plot(flist,AR21r,title="RHCP → RHCP Transmitted Axial Ratio", label="PSSFSS",
     ylabel="Axial Ratio (dB)", ylim=(0,3), ytick=0:0.5:3)
cst = readdlm("../src/assets/ericsson_cpss_digitized_arrhcp.csv", ',')
plot!(cst[:,1], cst[:,2], label="CST")
#md savefig("cpssb4.png"); nothing  # hide
#-
#md # ![](cpssb4.png)
#-
plot(flist,IL21r,title="RHCP → RHCP Insertion Loss", label="PSSFSS",
         ylabel="Insertion Loss (dB)", ylim=(0,3), ytick=0:0.5:3)
cst = readdlm("../src/assets/ericsson_cpss_digitized_ilrhcp.csv", ',')
plot!(cst[:,1], cst[:,2], label="CST")
#md savefig("cpssb5.png"); nothing  # hide
#-
#md # ![](cpssb5.png)
#-

# Differences between the PSSFSS and CST predictions are attributed to the fact that the
# metalization thickness of 18 μm was included in the CST model but cannot be accommodated by PSSFSS.


function f(x::Int64)
  x ^ 2
end

n = 10
println(typeof(n)) # -> Int64
println(f(n))

f2(n::Integer) = n ^ 3

# println(f2(10.1)) # Error
println(f2(10)) # Error


function MrsFunc(X)
Y=X/2
return(Y)
end


# ---
# title: 1018. Binary Prefix Divisible By 5
# id: problem1018
# author: Indigo
# date: 2022-03-19
# difficulty: Easy
# categories: Array
# link: <https://leetcode.com/problems/binary-prefix-divisible-by-5/description/>
# hidden: true
# ---
#
# Given an array `A` of `0`s and `1`s, consider `N_i`: the i-th subarray from
# `A[0]` to `A[i]` interpreted as a binary number (from most-significant-bit to
# least-significant-bit.)
#
# Return a list of booleans `answer`, where `answer[i]` is `true` if and only if
# `N_i` is divisible by 5.
#
# **Example 1:**
#
#
#
#     Input: [0,1,1]
#     Output: [true,false,false]
#     Explanation:
#     The input numbers in binary are 0, 01, 011; which are 0, 1, and 3 in base-10.  Only the first number is divisible by 5, so answer[0] is true.
#
#
# **Example 2:**
#
#
#
#     Input: [1,1,1]
#     Output: [false,false,false]
#
#
# **Example 3:**
#
#
#
#     Input: [0,1,1,1,1,1]
#     Output: [true,false,false,false,true,false]
#
#
# **Example 4:**
#
#
#
#     Input: [1,1,1,0,1]
#     Output: [false,false,false,false,false]
#
#
#
#
# **Note:**
#
#   1. `1 <= A.length <= 30000`
#   2. `A[i]` is `0` or `1`
#
#
## @lc code=start
using LeetCode

function prefixes_div_by5(nums::Vector{Int})
    len = length(nums)
    res = fill(false, len)
    cur = 0
    for i in 1:len
        cur = cur * 2 + nums[i]
        cur % 5 == 0 && (res[i] = true)
    end
    return res
end
## @lc code=end


#  Contains code that is based in part on Chebfun v5's chebfun/standardChop.m,
# which is distributed with the following license:

# Copyright (c) 2017, The Chancellor, Masters and Scholars of the University
# of Oxford, and the Chebfun Developers. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the University of Oxford nor the names of its
#       contributors may be used to endorse or promote products derived from
#       this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Jared Aurentz and Nick Trefethen, July 2015.
#
# Copyright 2017 by The University of Oxford and The Chebfun Developers.
# See http://www.chebfun.org/ for Chebfun information.

function standardchoplength(coeffs, tol)
    # Check magnitude of TOL:
    if tol ≥ 1
        throw(ArgumentError("tolerance must be less than 1"))
    end

    # Make sure COEFFS has length at least 17:
    n = length(coeffs)

    if  n < 17
        # resort to naive chop
        mx = maximum(abs,coeffs)
        if mx == 0
            return 0
        end
        for k=n:-1:1
            if abs(coeffs[k]) > tol*mx
                return k
            end
        end
        return 0
    end

    # Step 1: Convert COEFFS to a new monotonically nonincreasing
    #         vector ENVELOPE normalized to begin with the value 1.

    b = abs.(coeffs)
    m = b[end]*fill(1.0,n)
    for j = n-1:-1:1
        m[j] = max(b[j], m[j+1]);
    end
    if  m[1] == 0
        return 1
    end
    envelope = m/m[1];

    plateauPoint = 0

    for j = 2:n
        j2 = round(Int,1.25*j + 5);
        if  j2 > n
            # there is no plateau: exit
            return n
        end
        e1 = envelope[j]
        e2 = envelope[j2]
        r = 3*(1 - log(e1)/log(tol))
        plateau = (e1 == 0) || (e2/e1 > r)
        if plateau
            # a plateau has been found: go to Step 3
            plateauPoint = j - 1
            break
        end
    end

    # Step 3: fix CUTOFF at a point where ENVELOPE, plus a linear function
    # included to bias the result towards the left end, is minimal.
    #

    if  plateauPoint ≠ 0 && envelope[plateauPoint] == 0
        return plateauPoint
    end

    j3 = sum(envelope .≥ tol^(7/6))
    if j3 < j2
        j2 = j3 + 1
        envelope[j2] = tol^(7/6)
    end
    cc = log10.(envelope[1:j2])
    cc .+= range(0, stop=(-1/3)*log10(tol), length=j2)
    d = argmin(cc)
    return max(d - 1, 1)
end


#
# Plotting routines.
#

"""
$(TYPEDSIGNATURES)

Heuristic check if Plotter is PyPlot
"""
ispyplot(Plotter)=typeof(Plotter)==Module&& isdefined(Plotter,:Gcf)

"""
$(TYPEDSIGNATURES)

Heuristic check if Plotter is Plots
"""
isplots(Plotter)=typeof(Plotter)==Module&& isdefined(Plotter,:gr)

# Plot color scale for grid colors.
function frgb(Plotter,i,max;pastel=false)
    x=Float64(i-1)/Float64(max)
    if (x<0.5)
        r=1.0-2.0*x
        g=2.0*x
        b=0.0
    else
        r=0.0
        g=2.0-2.0*x
        b=2.0*x-1.0
    end
    if pastel
        r=0.5+0.5*r
        g=0.5+0.5*g
        b=0.5+0.5*b
    end
    if ispyplot(Plotter)
        return (r,g,b)
    end
    if isplots(Plotter)
        return Plotter.RGB(r,g,b)
    end
end

"""
$(SIGNATURES)

Find the circumcenter of a triangle.

Created from C source of Jonathan R Shewchuk <jrs@cs.cmu.edu>

Modified to return absolute coordinates.
"""
function tricircumcenter!(circumcenter,a,b,c)

    # Use coordinates relative to point `a' of the triangle.
    xba = b[1] - a[1]
    yba = b[2] - a[2]
    xca = c[1] - a[1]
    yca = c[2] - a[2]

    # Squares of lengths of the edges incident to `a'.
    balength = xba * xba + yba * yba
    calength = xca * xca + yca * yca

    # Calculate the denominator of the formulae.
    # if EXACT
    #    Use orient2d() from http://www.cs.cmu.edu/~quake/robust.html
    #    to ensure a correctly signed (and reasonably accurate) result,
    #    avoiding any possibility of division by zero.
    #  denominator = 0.5 / orient2d((double*) b, (double*) c, (double*) a)

    # Take your chances with floating-point roundoff
    denominator = 0.5 / (xba * yca - yba * xca)

    # Calculate offset (from `a') of circumcenter.
    xcirca = (yca * balength - yba * calength) * denominator
    ycirca = (xba * calength - xca * balength) * denominator

# The result is returned both in terms of x-y coordinates and xi-eta
# coordinates, relative to the triangle's point `a' (that is, `a' is
# the origin of both coordinate systems).	 Hence, the x-y coordinates
# returned are NOT absolute; one must add the coordinates of `a' to
# find the absolute coordinates of the circumcircle.  However, this means
# that the result is frequently more accurate than would be possible if
# absolute coordinates were returned, due to limited floating-point
# precision.  In general, the circumradius can be computed much more
# accurately.

    circumcenter[1] = xcirca+a[1]
    circumcenter[2] = ycirca+a[2]

    return circumcenter
end

"""
$(TYPEDSIGNATURES)

The plot function is not exported, but kept for backward compatibility.
"""
plot(Plotter, tio; kwargs...)=plot_triangulateio(Plotter, tio; kwargs...)

"""
$(TYPEDSIGNATURES)

Plot contents of triangulateio structure.
The plot module (currently only PyPlot is possible, GLMakie may follow)
is passed as parameter.
This allows to keep the package free of heavy
plot package dependencies.
"""
function plot_triangulateio(Plotter,
                            tio::TriangulateIO;
                            voronoi=nothing,
                            aspect=1,
                            circumcircles=false
                            )

    if ispyplot(Plotter)
        PyPlot=Plotter
        ax = PyPlot.matplotlib.pyplot.gca()
        ax.set_aspect(aspect)
        if numberofpoints(tio)==0
            return
        end
        x=tio.pointlist[1,:]
        y=tio.pointlist[2,:]
        xminmax=extrema(x)
        yminmax=extrema(y)

        dx=xminmax[2]-xminmax[1]
        ax.set_xlim(xminmax[1]-dx/10,xminmax[2]+dx/10)
        dy=yminmax[2]-yminmax[1]
        ax.set_ylim(yminmax[1]-dy/10,yminmax[2]+dy/10)

        PyPlot.scatter(x,y, s=10,color="b")
        t=transpose(tio.trianglelist.-1)
        if size(tio.triangleattributelist,2)>0
            PyPlot.tripcolor(x,y,t,tio.triangleattributelist[1,:],cmap="Pastel2")
        end
        if numberoftriangles(tio)>0
            PyPlot.triplot(x,y,t,color="k")
            if circumcircles
                t=0:0.025*π:2π
                circle(x,y,r)=PyPlot.plot(x.+r.*cos.(t),y.+r.*sin.(t),color=(0.4,0.05,0.4), linewidth=0.3)
                cc=zeros(2)
                for itri=1:numberoftriangles(tio)

                    trinodes=tio.trianglelist[:,itri]
                    a=tio.pointlist[:,trinodes[1]]
                    b=tio.pointlist[:,trinodes[2]]
                    c=tio.pointlist[:,trinodes[3]]
                    tricircumcenter!(cc,a,b,c)
                    r=sqrt((cc[1]-a[1])^2+(cc[2]-a[2])^2)
                    circle(cc[1],cc[2],r)
                    PyPlot.scatter([cc[1]],[cc[2]], s=20,color=(0.4,0.05,0.4))
                end
            end
        end
        if numberofsegments(tio)>0
            lines=Any[]
            rgb=Any[]
            markermax=maximum(tio.segmentmarkerlist)
            # see https://gist.github.com/gizmaa/7214002
            for i=1:numberofsegments(tio)
                x1=tio.pointlist[1,tio.segmentlist[1,i]]
                y1=tio.pointlist[2,tio.segmentlist[1,i]]
                x2=tio.pointlist[1,tio.segmentlist[2,i]]
                y2=tio.pointlist[2,tio.segmentlist[2,i]]
                push!(lines,collect(zip([x1,x2],[y1,y2])))
                push!(rgb,frgb(PyPlot,tio.segmentmarkerlist[i],markermax))
            end
            ax.add_collection(PyPlot.matplotlib.collections.LineCollection(lines,colors=rgb,linewidth=3))
        end

        if numberoftriangles(tio)==0 && numberofregions(tio)>0
            x=tio.regionlist[1,:]
            y=tio.regionlist[2,:]
            r=tio.regionlist[3,:]
            PyPlot.scatter(x,y, s=20,c=r,cmap="Dark2")
        end

        if numberoftriangles(tio)==0 && numberofholes(tio)>0
            x=tio.holelist[1,:]
            y=tio.holelist[2,:]
            PyPlot.scatter(x,y, s=20,color="k")
        end

        if voronoi!=nothing && numberofedges(voronoi)>0
            bcx=sum(x)/length(x)
            bcy=sum(y)/length(y)
            wx=maximum(abs.(x.-bcx))
            wy=maximum(abs.(y.-bcy))
            ww=max(wx,wy)

            x=voronoi.pointlist[1,:]
            y=voronoi.pointlist[2,:]
            PyPlot.scatter(x,y, s=10,color="g")
            for i=1:numberofedges(voronoi)
                i1=voronoi.edgelist[1,i]
                i2=voronoi.edgelist[2,i]
                if i1>0 && i2>0
                    PyPlot.plot([voronoi.pointlist[1,i1],voronoi.pointlist[1,i2]],
                                 [voronoi.pointlist[2,i1],voronoi.pointlist[2,i2]],
                                 color="g")
                else
                    x0=voronoi.pointlist[1,i1]
                    y0=voronoi.pointlist[2,i1]
                    xn=voronoi.normlist[1,i]
                    yn=voronoi.normlist[2,i]
                    normscale=1.0e10
                    if x0+normscale*xn>bcx+ww
                        normscale=abs((bcx+ww-x0)/xn)
                    end
                    if x0+normscale*xn<bcx-ww
                        normscale=abs((bcx-ww-x0)/xn)
                    end
                    if y0+normscale*yn>bcy+ww
                        normscale=abs((bcy+ww-y0)/yn)
                    end
                    if y0+normscale*yn<bcy-ww
                        normscale=abs((bcy-ww-y0)/yn)
                    end
                    PyPlot.plot([x0, x0+normscale*xn], [y0,y0+normscale*yn],color="g")
                end

            end
        end
    end

    ### Currently not working
    if isplots(Plotter)
        #=
        if p==nothing
            p=Plotter.plot()
        end
        for icell=1:num_cells(grid)
            rgb=frgb(Plotter,grid.cellregions[icell],num_cellregions(grid),pastel=true)
            coord1=nodecoord(grid,cellnode(grid,1,icell))
            coord2=nodecoord(grid,cellnode(grid,2,icell))
            coord3=nodecoord(grid,cellnode(grid,3,icell))
            # https://github.com/JuliaPlotter/Plotter.jl/issues/605
            tri=Plotter.Shape([coord1[1],coord2[1], coord3[1]],[coord1[2],coord2[2],coord3[2]])
            Plotter.plot!(p,tri,color=rgb,label="")
        end
        for icell=1:num_cells(grid)
                coord1=nodecoord(grid,cellnode(grid,1,icell))
            coord2=nodecoord(grid,cellnode(grid,2,icell))
            coord3=nodecoord(grid,cellnode(grid,3,icell))
            Plotter.plot!(p, [coord1[1],coord2[1]],[coord1[2],coord2[2]]  ,linewidth=0.5,color=:black,label="")
            Plotter.plot!(p, [coord1[1],coord3[1]],[coord1[2],coord3[2]]  ,linewidth=0.5,color=:black,label="")
            Plotter.plot!(p, [coord2[1],coord3[1]],[coord2[2],coord3[2]]  ,linewidth=0.5,color=:black,label="")
        end
        for ibface=1:num_bfaces(grid)
            rgb=frgb(Plotter,grid.bfaceregions[ibface],num_bfaceregions(grid))
            coord1=nodecoord(grid,bfacenode(grid,1,ibface))
            coord2=nodecoord(grid,bfacenode(grid,2,ibface))
            Plotter.plot!(p,[coord1[1],coord2[1]],[coord1[2],coord2[2]]  ,linewidth=5,color=rgb,label="")
        end
        if show
            Plotter.gui(p)
        end
        return p
        =#
    end
end

"""
$(TYPEDSIGNATURES)

Plot  pair of triangulateio structures arranged
in two subplots. This is intendd for visualizing both input
and output data.
"""
function plot_in_out(Plotter, triin, triout;voronoi=nothing, circumcircles=false,title="")
    if ispyplot(Plotter)
        PyPlot=Plotter
        PyPlot.clf()
        PyPlot.subplots(nrows=1, ncols=2)
        PyPlot.suptitle(title)
        PyPlot.subplot(121)
        PyPlot.title("In")
        plot_triangulateio(PyPlot,triin)
        PyPlot.subplot(122)
        PyPlot.title("Out")
        plot_triangulateio(PyPlot,triout,voronoi=voronoi,circumcircles=circumcircles)
        PyPlot.tight_layout()
    end
end


module Pyramids

using Images, Interpolations, Colors

export ImagePyramid, PyramidType, ComplexSteerablePyramid, LaplacianPyramid, GaussianPyramid
export subband, toimage, update_subband, update_subband!, test

"Abstract supertype for the variety of pyramids that can be constructed."
abstract PyramidType
abstract SimplePyramid <: PyramidType

"""Type that indicates a complex steerable pyramid. [1]

[1] http://www.cns.nyu.edu/~eero/steerpyr/"""
type ComplexSteerablePyramid <: PyramidType
end

"""Type that indicates a Laplacian pyramid. [1]

[1] persci.mit.edu/pub_pdfs/pyramid83.pdf"""
type LaplacianPyramid <: SimplePyramid
end

"""Type that indicates a Gaussian pyramid. [1]

[1] http://persci.mit.edu/pub_pdfs/RCA84.pdf"""
type GaussianPyramid <: SimplePyramid
end

"""Type that represents a concrete pyramidal representation of a given input image. Each type of pyramid has its own parameters. The basic construction method is, for example

```pyramid = ImagePyramid(im, ComplexSteerablePyramid(), scale=0.5^0.25)```

See the code for more information on optional arguments."""
type ImagePyramid
    scale::Real
    num_levels::Int
    num_orientations::Int
    t::PyramidType
    pyramid_bands::Dict

    function ImagePyramid(pyr::ImagePyramid)
        return deepcopy(pyr)
    end

    function ImagePyramid(im::Array, t::ComplexSteerablePyramid; scale=0.5, min_size=15, num_orientations=8, max_levels=23, twidth=1)
        this = new()

        this.t = ComplexSteerablePyramid()
        this.scale = scale
        this.num_orientations = num_orientations

        im_dims = size(im)
        h = im_dims[1]
        w = im_dims[2]

        this.num_levels = min(ceil(log2(minimum([h w]))/log2(1/scale) - (log2(min_size)/log2(1/scale))),max_levels);

        pyramid_bands, mtx, harmonics = build_complex_steerable_pyramid(im, this.num_levels, this.num_levels, order=num_orientations-1, twidth=twidth, scale=scale)

        this.pyramid_bands = pyramid_bands

        return this
    end

    function ImagePyramid(im::Image, t::ComplexSteerablePyramid; scale=0.5, min_size=15, num_orientations=8, max_levels=23, twidth=1)
        im_arr = image_to_array(im)

        return ImagePyramid(im_arr, t, scale=scale, min_size=min_size, num_orientations=num_orientations, max_levels=23, twidth=1)
    end

    function ImagePyramid(im::Array, t::GaussianPyramid; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
        this = new()

        pyramid_bands, num_levels = generate_gaussian_pyramid(im, min_size=min_size, max_levels=max_levels, filter=filter)

        this.scale = 0.5
        this.pyramid_bands = pyramid_bands
        this.num_orientations = 1
        this.num_levels = num_levels
        this.t = t

        return this
    end

    function ImagePyramid(im::Array, t::LaplacianPyramid; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
        this = new()

        pyramid_bands, num_levels = generate_laplacian_pyramid(im, min_size=min_size, max_levels=max_levels, filter=filter)

        this.scale = 0.5
        this.pyramid_bands = pyramid_bands
        this.num_orientations = 1
        this.num_levels = num_levels
        this.t = t

        return this
    end

    function ImagePyramid(im::Image, t::SimplePyramid; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
        im_arr = image_to_array(im)

        return ImagePyramid(im_arr, t, min_size=min_size, max_levels=max_levels, filter=filter)
    end

    function ImagePyramid(pyramid_bands::Dict, scale, t, num_levels, num_orientations)
        this = new()

        this.scale = scale
        this.pyramid_bands = pyramid_bands
        this.num_orientations = num_orientations
        this.num_levels = num_levels
        this.t = t

        return this
    end

end

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

"""
    subband(pyramid::ImagePyramid, level; orientation=())

Returns the sub-band of the `pyramid` at `level` and at `orientation.` If orientation is not provided, `subband` assumes that the pyramid is not oriented.

Level 0 is always the high frequency residual."""
function subband(pyramid::ImagePyramid, level; orientation = ())
    if isempty(orientation)
        return pyramid.pyramid_bands[level]
    else
        return pyramid.pyramid_bands[level][orientation]
    end
end

function update_subband!(pyramid::ImagePyramid, level, new_subband; orientation = ())
    if isempty(orientation)
        pyramid.pyramid_bands[level] = copy(new_subband)
    else
        pyramid.pyramid_bands[level][orientation] = copy(new_subband)
    end

    return pyramid
end

function update_subband(pyramid::ImagePyramid, level, new_subband; orientation = ())
    newpyramid = ImagePyramid(pyramid)
    return update_subband!(newpyramid, level, new_subband, orientation=orientation)
end

"Converts a pyramid to a 2-D array (not of type `Image`!)"
function toimage(pyramid::ImagePyramid)
    if typeof(pyramid.t) <: ComplexSteerablePyramid
        im = reconstruct_complex_steerable_pyramid(pyramid, scale=pyramid.scale)
    elseif typeof(pyramid.t) <: LaplacianPyramid
        im = reconstruct_laplacian_pyramid(pyramid)
    elseif typeof(pyramid.t) <: GaussianPyramid
        im = subband(pyramid, 0)
    else
        error("Unsupported pyramid type $(typeof(pyramid.T))")
    end

    return im
end

##############################
# Private helper functions.

function image_to_array(im::Image)
    # Please someone tell me a better way of checking for image type.
    if typeof(im.data[1]) <: Gray
        im_arr = separate(convert(Array{Float64,2}, im))
    else
        warn("ImagePyramid only supports 2D images currently. Returning luminance channel.")
        im_lab = convert(Image{Lab}, im)
        im_arr = convert(Array{Float64,3}, separate(im))[:,:,1]
    end

    return im_arr
end

##############################
# Private functions for building Gaussian and Laplacian pyramids.

function convolve_reflect(im, filter)
    filter_len = length(filter)
    filter_offset::Int = (filter_len-1)/2

    padded_im = zeros((size(im,1) + filter_len*2), (size(im,1) + filter_len*2))

    padded_im[(filter_len+1):(end-filter_len), (filter_len+1):(end-filter_len)] = im

    padded_im[(filter_len+1):(end-filter_len), 1:filter_len] = flipdim(padded_im[(filter_len+1):(end-filter_len), (filter_len+2):(2*filter_len+1)], 2)
    padded_im[(filter_len+1):(end-filter_len), (end-filter_len+1):end] = flipdim(padded_im[(filter_len+1):(end-filter_len), (end-2*filter_len):(end-filter_len-1)], 2)
    padded_im[1:filter_len, (filter_len+1):(end-filter_len),] = flipdim(padded_im[(filter_len+2):(1+2*filter_len), (filter_len+1):(end-filter_len)], 1)
    padded_im[(end-filter_len+1):end, (filter_len+1):(end-filter_len)] = flipdim(padded_im[(end-2*filter_len):(end-filter_len-1), (filter_len+1):(end-filter_len)], 1)

    new_im = conv2(filter, filter, padded_im)

    new_im = new_im[(1+filter_len+filter_offset):(end-filter_len-filter_offset), (1+filter_len+filter_offset):(end-filter_len-filter_offset)]
    return new_im
end

function reduce(im; filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
    new_im = convolve_reflect(im, filter)
    return new_im[1:2:end, 1:2:end]
end

function expand(im; filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
    new_im = zeros((size(im, 1)*2, size(im, 2)*2))
    new_im[1:2:end, 1:2:end] = im

    new_im = convolve_reflect(new_im, filter)
    return new_im
end

function generate_gaussian_pyramid(im; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
    im = convert(Array{Float64}, copy(im))

    pyramid_bands = Dict{Integer, Array}()

    im_dims = collect(size(im))
    num_levels = min(max_levels, ceil(Int, log2(minimum(im_dims)) - log2(min_size)))

    for i = 1:num_levels
        pyramid_bands[i-1] = im
        im = reduce(im, filter=filter)
    end

    return (pyramid_bands, num_levels)
end

function generate_laplacian_pyramid(im; min_size=15, max_levels=23, filter=[0.0625; 0.25; 0.375; 0.25; 0.0625])
    im = convert(Array{Float64}, copy(im))

    pyramid_bands = Dict{Integer, Array}()

    im_dims = collect(size(im))

    num_levels = min(max_levels, ceil(Int, log2(minimum(im_dims)) - log2(min_size)))

    filter_offset::Int = (length(filter)-1)/2

    for i = 1:num_levels
        reduced_im = reduce(im, filter=filter)
        next_im = expand(reduced_im)
        diff_im = im - next_im

        pyramid_bands[i-1] = diff_im

        im = reduced_im
    end

    pyramid_bands[num_levels] = im

    return (pyramid_bands, num_levels)
end

function reconstruct_laplacian_pyramid(pyramid::ImagePyramid)
    output_im::Array{Float64} = subband(pyramid, pyramid.num_levels)

    for i = (pyramid.num_levels-1):-1:0
        output_im = expand(output_im)
        output_im += subband(pyramid, i)
    end

    return output_im
end

##############################
# Private functions for building complex steerable pyramids.

function construct_steering_matrix(harmonics, angles; even = true)
    numh = 2*length(harmonics) - any(harmonics .== 0)

    imtx = zeros(length(angles), numh)
    col = 1
    for h in harmonics
        args = h * angles

        if h == 0
            imtx[:,col] = ones(angles)
            col += 1
        elseif !even
            imtx[:,col] = sin(args)
            imtx[:,col+1] = -cos(args)
            col += 2
        else
            imtx[:,col] = cos(args)
            imtx[:,col+1] = sin(args)
            col += 2
        end
    end

    r = rank(imtx)

    if (r != numh) && (r != length(angles))
        warning("Matrix is not full rank")
    end

    return pinv(imtx)
end

function raisedcosine(width=1, position=0, values=[0,1])
    sz = 256 # arbitrary
    X = pi * (-sz-1:1) / (2 * sz)
    Y = values[1] + (values[2]-values[1]) * cos(X).^2

    Y[1] = Y[2]
    Y[sz+3] = Y[sz+2]
    X = position + (2*width/pi) * (X + pi/4)

    return (X, Y)
end

function build_complex_steerable_pyramid(im, height, nScales; order=3, twidth=1, scale=0.5)
    pyramid_bands = Dict{Integer, Union{Array, Dict{Integer, Array}}}()

    num_orientations = order + 1

    # generate steering matrix and harmonics info
    if mod(num_orientations, 2) == 0
        harmonics = ((0:((num_orientations/2)-1))*2 + 1)'
    else
        harmonics = ((0:((num_orientations-1)/2))*2)'
    end

    steeringmatrix = construct_steering_matrix(harmonics, pi*(0:(num_orientations-1))/num_orientations, even=true)

    imdft = fftshift(fft(im))
    im_dims = collect(size(im))
    ctr = ceil(Int, (im_dims+0.5)/2)

    angle = broadcast(atan2, (((1:im_dims[1]) - ctr[1]) ./ (im_dims[1]/2)), (((1:im_dims[2]) - ctr[2]) ./ (im_dims[2]/2))') #SLOOW
    log_rad = broadcast((x,y) -> log2(sqrt(x.^2 + y.^2)), (((1:im_dims[1]) - ctr[1]) ./ (im_dims[1]/2)), (((1:im_dims[2]) - ctr[2]) ./ (im_dims[2]/2))') #SLOOW
    log_rad[ctr[1], ctr[2]] = log_rad[ctr[1], ctr[2]-1]

    Xrcos, Yrcos = raisedcosine(twidth, (-twidth/2), [0 1])
    Yrcos = sqrt(Yrcos)
    YIrcos = sqrt(1 - Yrcos.^2)

    # generate high frequency residual
    Yrcosinterpolant = interpolate((Xrcos,), Yrcos, Gridded(Linear()))
    hi0mask = reshape(Yrcosinterpolant[log_rad], size(log_rad))
    hi0dft =  imdft .* hi0mask;

    pyramid_bands[0] = ifft(ifftshift(hi0dft));

    YIrcosinterpolant = interpolate((Xrcos,), YIrcos, Gridded(Linear()))
    lo0mask = reshape(YIrcosinterpolant[log_rad], size(log_rad))

    lo0dft = imdft .* lo0mask

    for ht = height:-1:1
        Xrcos = Xrcos - log2(1/scale)
        order = num_orientations - 1

        cnst = (2^(2*order))*(factorial(order)^2)/(num_orientations*factorial(2*order))

        Yrcosinterpolant = interpolate((Xrcos,), Yrcos, Gridded(Linear()))
        himask = reshape(Yrcosinterpolant[log_rad], size(log_rad))

        # loop through each orientation band
        pyramid_level = Dict{Integer, Array}()

        for b in 1:num_orientations
            banddft = zeros(lo0dft)

            for iter in eachindex(lo0dft)
                ang = angle[iter]-pi*(b-1)/num_orientations
                a = (abs(mod(pi+ang, 2*pi) - pi) .< pi/2) .* (2*sqrt(cnst) * (cos(ang).^order))
                banddft[iter] = ((complex(0,-1)).^(num_orientations-1)) * lo0dft[iter] * himask[iter] * a
            end

            pyramid_level[b] = ifft(ifftshift(banddft))
        end

        pyramid_bands[height-ht+1] = pyramid_level

        dims = collect(size(lo0dft))
        ctr = ceil(Int, (dims+0.5)/2)

        lodims = round(Int, im_dims[1:2]*scale^(nScales-ht+1))

        loctr = ceil(Int, (lodims+0.5)/2)
        lostart = ctr - loctr+1
        loend = lostart + lodims - 1

        log_rad = log_rad[lostart[1]:loend[1], lostart[2]:loend[2]]
        angle = angle[lostart[1]:loend[1], lostart[2]:loend[2]]
        lodft = lo0dft[lostart[1]:loend[1], lostart[2]:loend[2]]
        YIrcos = abs(sqrt(1 - Yrcos.^2))

        YIrcosinterpolant = interpolate((Xrcos,), YIrcos, Gridded(Linear()))
        lomask = reshape(YIrcosinterpolant[log_rad], size(log_rad))

        lo0dft = lomask .* lodft
    end

    pyramid_bands[height+1] = real(ifft(ifftshift(lo0dft)))

    return (pyramid_bands, steeringmatrix, harmonics)
end

function make_angle_grid(sz, phase=0, origin=-1)
    if length(sz) == 1
        sz = [sz, sz]
    end

    if origin == -1
        origin = (sz + 1)/2
    end

    xramp = ones(round(Int, sz[1]), 1) * collect((1:sz[2]) - origin[2])'
    yramp = collect((1:sz[1]) - origin[1]) * ones(1, round(Int, sz[2]))

    res = atan2(yramp, xramp)

    res = mod(res+(pi-phase), 2*pi) - pi

    return res
end

function reconstruct_steerable_pyramid(pyr::ImagePyramid; levs="all", bands="all", twidth=1, scale=0.5)
    dims = collect(size(subband(pyr, 0)))
    im_dft = zeros(Complex{Float64}, size(subband(pyr, 0)))

    ctr = ceil(Int, (dims + 0.5)/2)

    angle = broadcast(atan2, (((1:dims[1]) - ctr[1]) ./ (dims[1]/2)), (((1:dims[2]) - ctr[2]) ./ (dims[2]/2))') #SLOOW
    log_rad = broadcast((x,y) -> log2(sqrt(x.^2 + y.^2)), (((1:dims[1]) - ctr[1]) ./ (dims[1]/2)), (((1:dims[2]) - ctr[2]) ./ (dims[2]/2))') #SLOOW
    log_rad[ctr[1], ctr[2]] = log_rad[ctr[1], ctr[2]-1]
    log_rad0 = log_rad
    angle0 = angle

    Xrcos, Yrcos = raisedcosine(twidth, (-twidth/2), [0 1])
    Yrcos = sqrt(Yrcos)
    YIrcos = sqrt(1 - Yrcos.^2)

    # Start with low frequency residual
    low_dft = fftshift(fft(subband(pyr, pyr.num_levels+1)))

    lodims = collect(size(low_dft))
    loctr = ceil(Int, (lodims+0.5)/2)
    lostart = ctr - loctr+1
    loend = lostart + lodims - 1

    log_rad = log_rad0[lostart[1]:loend[1], lostart[2]:loend[2]]
    angle = angle0[lostart[1]:loend[1], lostart[2]:loend[2]]

    Xrcos = Xrcos - log2(1/scale)*pyr.num_levels
    YIrcosinterpolant = interpolate((Xrcos,), YIrcos, Gridded(Linear()))
    lomask = reshape(YIrcosinterpolant[log_rad], size(log_rad))

    im_dft[lostart[1]:loend[1], lostart[2]:loend[2]] += low_dft .* lomask

    # Accumulate mid-bamds
    for level = pyr.num_levels:-1:1
        lodims = collect(size(subband(pyr, level, orientation=1)))
        loctr = ceil(Int, (lodims+0.5)/2)
        lostart = ctr - loctr+1
        loend = lostart + lodims - 1

        log_rad = copy(log_rad0[lostart[1]:loend[1], lostart[2]:loend[2]])
        angle = copy(angle0[lostart[1]:loend[1], lostart[2]:loend[2]])

        Yrcosinterpolant = interpolate((Xrcos,), Yrcos, Gridded(Linear()))
        himask = reshape(Yrcosinterpolant[log_rad], size(log_rad))

        Xrcos = Xrcos + log2(1/scale)
        order = pyr.num_orientations - 1

        cnst = ((complex(0,1)).^(pyr.num_orientations-1)) * sqrt((2^(2*order))*(factorial(order)^2)/(pyr.num_orientations*factorial(2*order)))

        for orientation = 1:pyr.num_orientations
            band_dft = fftshift(fft(subband(pyr, level, orientation=orientation)))
            ang = angle-pi*(orientation-1)/pyr.num_orientations
            angle_mask = 2 *(abs(mod(pi+ang, 2*pi) - pi) .< pi/2) .* (cnst * (cos(ang).^order))

            im_dft[lostart[1]:loend[1], lostart[2]:loend[2]] += band_dft .* himask .* angle_mask
        end

        YIrcosinterpolant = interpolate((Xrcos,), YIrcos, Gridded(Linear()))
        lomask = reshape(YIrcosinterpolant[log_rad], size(log_rad))

        # Everything must be scaled by the low-frequency mask
        im_dft[lostart[1]:loend[1], lostart[2]:loend[2]] .*= lomask
    end

    # Add high frequency residual
    Yrcosinterpolant = interpolate((Xrcos,), Yrcos, Gridded(Linear()))
    hi0mask = reshape(Yrcosinterpolant[log_rad0], size(log_rad))
    im_dft += fftshift(fft(subband(pyr, 0))) .* hi0mask;

    return real(ifft(ifftshift(im_dft)))
end

# make the complex steerable pyramid real
function convert_complex_steerable_pyramid_to_real(pyramid::ImagePyramid; levs="all", bands="all", twidth=1, scale=0.5)
    pyramid = ImagePyramid(pyramid)
    num_levels = pyramid.num_levels
    num_orientations = pyramid.num_orientations

    for nsc in 1:num_levels
        dims = collect(size(subband(pyramid, nsc, orientation=1)))
        ctr = ceil(Int, (dims+0.5)/2)
        ang = make_angle_grid(dims, 0, ctr)
        ang[ctr[1], ctr[2]] = -pi/2

        for nor = 1:num_orientations
            ch = subband(pyramid, nsc, orientation=nor)

            ang0 = pi*(nor-1)/num_orientations
            xang = mod(ang-ang0+pi, 2*pi) - pi

            # this creates an angular mask
            amask = 2*(abs(xang) .< pi/2) + (abs(xang) .== pi/2)
            amask[ctr[1], ctr[2]] = 1
            amask[:,1] = 1
            amask[1,:] = 1

            # and masks the fft by it
            amask = fftshift(amask)
            ch = ifft(amask.*fft(ch))
            ch = 0.5 * real(ch)

            # then creates a new pyramid
            update_subband!(pyramid, nsc, ch, orientation=nor)
        end
    end

    # and returns it as a real
    return pyramid
end

function reconstruct_complex_steerable_pyramid(pyramid::ImagePyramid; levs="all", bands="all", twidth=1, scale=0.5)
    real_pyramid = convert_complex_steerable_pyramid_to_real(pyramid, levs=levs, bands=bands, twidth=twidth, scale=scale)
    return reconstruct_steerable_pyramid(real_pyramid, levs=levs, bands=bands, twidth=twidth, scale=scale)
end

end


# ---
# title: 240. Search a 2D Matrix II
# id: problem240
# author: woclass
# date: 2020-10-29
# difficulty: Medium
# categories: Binary Search, Divide and Conquer
# link: <https://leetcode.com/problems/search-a-2d-matrix-ii/description/>
# hidden: true
# ---
#
# Write an efficient algorithm that searches for a `target` value in an `m x n`
# integer `matrix`. The `matrix` has the following properties:
#
#   * Integers in each row are sorted in ascending from left to right.
#   * Integers in each column are sorted in ascending from top to bottom.
#
#
#
# **Example 1:**
#
# ![](https://assets.leetcode.com/uploads/2020/11/24/searchgrid2.jpg)
#
#
#
#     Input: matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
#     Output: true
#
#
# **Example 2:**
#
# ![](https://assets.leetcode.com/uploads/2020/11/24/searchgrid.jpg)
#
#
#
#     Input: matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
#     Output: false
#
#
#
#
# **Constraints:**
#
#   * `m == matrix.length`
#   * `n == matrix[i].length`
#   * `1 <= n, m <= 300`
#   * `-109 <= matix[i][j] <= 109`
#   * All the integers in each row are **sorted** in ascending order.
#   * All the integers in each column are **sorted** in ascending order.
#   * `-109 <= target <= 109`
#
#
## @lc code=start
using LeetCode

function search_matrix(matrix::Matrix{Int}, target::Int)::Bool
    row, col = size(matrix)
    i, j = row, 1

    while (i > 0 && j <= col)
        last = matrix[i, j]

        if (last == target)
            return true
        elseif last > target
            i -= 1
        else
            j += 1
        end
    end

    return false
end
## @lc code=end


function compute_effective_coefficents!(ν_eff, T, mol::MolecularConstants{FT}) where {FT}#molecules::Array{MolecularConstants{FT}}) where {FT}
    #@unpack Y = mol
    @unpack α̅,  γ̅, α_prime, γ_prime, ϵ, ϵ_prime = mol.effCoeff
    @unpack α̅₀₀, γ̅₀₀, ω₀, α_b, α_c, α₀₀_prime, γ₀₀_prime = mol.PolTensor
    #Computing α̅
    α̅ = α̅₀₀*(1 + α_b*T + α_c*T^2)/(1-(2π*c*ν_eff/ω₀)^2)
    γ̅ = γ̅₀₀
    ϵ = α̅/γ̅
    α_prime = α₀₀_prime * sqrt(mol.Y[1,2]/mol.Y[2,1]) # see Eqs (36a-39b) of Buldakov et al. 1996 (Spectrochimica Acta Part A)
    γ_prime = γ₀₀_prime * sqrt(mol.Y[1,2]/mol.Y[2,1]) # see Eqs (36a-39b) of Buldakov et al. 1996 (Spectrochimica Acta Part A)
    γ_C_Rayl = 3/(45(ϵ)^2+4)
    γ_C_RotRaman = 3/4
    ϵ_prime = α_prime/γ_prime
    γ_C_VibRaman =  3/(45(ϵ_prime)^2+4)
    γ_C_RoVibRaman = 3/4
    rho_depol_Rayl = 2γ_C_Rayl/(1 + γ_C_Rayl) # depolarization factor, as defined in Table 1 (Spurr 2006)
    rho_depol_RotRaman = 2γ_C_RotRaman/(1 + γ_C_RotRaman) # depolarization factor, as defined in Table 1 (Spurr 2006)
    rho_depol_VibRaman = 2γ_C_VibRaman/(1 + γ_C_VibRaman) # depolarization factor, as defined in Table 1 (Spurr 2006)
    rho_depol_RoVibRaman = 2γ_C_RoVibRaman/(1 + γ_C_RoVibRaman) # depolarization factor, as defined in Table 1 (Spurr 2006)

    @pack! mol.effCoeff = rho_depol_Rayl, rho_depol_RotRaman, rho_depol_VibRaman, rho_depol_RoVibRaman,
        α̅, γ̅, α_prime, γ_prime, ϵ, ϵ_prime, γ_C_Rayl, γ_C_RotRaman, γ_C_VibRaman, γ_C_RoVibRaman
end

#Compute elastic scattering cross-section (Cabannes line)
function compute_σ_Rayl_coeff!(mol::MolecularConstants{FT}) where {FT}#ν, molecules::Array{MolecularConstants{FT}}) where {FT}
    @unpack α̅, γ_C_Rayl, σ_Rayl_coeff = mol.effCoeff
    σ_Rayl_coeff = 128π^5 * α̅^2 * (1+2*γ_C_Rayl)/(3-4*γ_C_Rayl)# * ν^4
    @show σ_Rayl_coeff,  α̅^2
    @pack! mol.effCoeff = σ_Rayl_coeff,α̅,γ_C_Rayl
end

#Compute elastic scattering cross-section (Cabannes line)
function compute_σ_Rayl_VibRaman_coeff_hires!(T, mol::MolecularConstants{FT}; Jmax=30) where {FT}#ν, molecules::Array{MolecularConstants{FT}}) where {FT}
    @unpack α̅, γ̅, α_prime, γ_prime, E_vJ, σ_Rayl_coeff_hires, σ_VibRaman_coeff_0to1_hires, σ_VibRaman_coeff_1to0_hires, Δν̃_Rayl_coeff_hires, Δν̃_VibRaman_coeff_0to1_hires, Δν̃_VibRaman_coeff_1to0_hires = mol.effCoeff
    @unpack gₛ = mol
    σ_Rayl_coeff_hires = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    Δν̃_Rayl_coeff_hires = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    σ_VibRaman_coeff_0to1_hires = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    σ_VibRaman_coeff_1to0_hires = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    Δν̃_VibRaman_coeff_0to1_hires = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    Δν̃_VibRaman_coeff_1to0_hires = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);

    kᵥ = 128*π^5
    #σ_Rayl_coeff_hires = 128π^5 * α̅^2 * (1+2*γ_C_Rayl)/(3-4*γ_C_Rayl)# * ν^4
    Z_pf = 0
    for Ji in 0:Jmax
        b_JJ   = Ji*(Ji+1)/((2Ji-1)*(2Ji+3))
        g_N = gₛ[2-Ji%2]

        # Rayleigh
        vi = 0
        vf = 0
        Ni_by_N = exp(-h*c*E_vJ[vi,Ji]/(k_B*T))
        γ_C = 3/(4+45(α̅/(b_JJ*γ̅))^2)
        Z_pf += g_N * (2Ji+1) * (exp(-h*c*E_vJ[0,Ji]/(k_B*T)) + exp(-h*c*E_vJ[1,Ji]/(k_B*T))) #accounting for both the ground- and the first excited vibrational state - depending on the ambient temperature (e.g. on hot exoplanets), more states may have to be accounted for
        # Molecular energy level
        Δν =  E_vJ[vi, Ji]
        # Wavenumber change of scattered light
        Δνₛ = Δν

        σ_Rayl_coeff_hires[Ji] = kᵥ * g_N * (2Ji+1) * Ni_by_N * α̅^2 * (1+2*γ_C)/(3-4*γ_C)# * νₛ[vi, Ji, vf, Jf]^4
        Δν̃_Rayl_coeff_hires[Ji] = Δνₛ

        # Vib. Raman 0->1
        vi = 0
        vf = 1
        Ni_by_N = exp(-h*c*E_vJ[vi,Ji]/(k_B*T))
        γ_C = 3/(4+45(α_prime/(b_JJ*γ_prime))^2)
        # Molecular energy level
        Δν =  E_vJ[vf, Ji] - E_vJ[vi, Ji]
        # Wavenumber change of scattered light
        Δνₛ = -Δν

        σ_VibRaman_coeff_0to1_hires[Ji] = kᵥ * g_N * (2Ji+1) * Ni_by_N * α_prime^2 * (1+2*γ_C)/(3-4*γ_C)# * νₛ[vi, Ji, vf, Jf]^4
        Δν̃_VibRaman_coeff_0to1_hires[Ji] = Δνₛ
        @show Ji, α_prime^2, (1+2*γ_C)/(3-4*γ_C), α_prime^2 * (1+2*γ_C)/(3-4*γ_C)
        # Vib. Raman 1->0
        vi = 1
        vf = 0
        Ni_by_N = exp(-h*c*E_vJ[vi,Ji]/(k_B*T))

        # Molecular energy level
        Δν =  E_vJ[vf, Ji] - E_vJ[vi, Ji]
        # Wavenumber change of scattered light
        Δνₛ = -Δν

        σ_VibRaman_coeff_1to0_hires[Ji] = kᵥ * g_N * (2Ji+1) * Ni_by_N * α_prime^2 * (1+2*γ_C)/(3-4*γ_C)# * νₛ[vi, Ji, vf, Jf]^4
        Δν̃_VibRaman_coeff_1to0_hires[Ji] = Δνₛ
    end
    σ_Rayl_coeff_hires /= Z_pf
    σ_VibRaman_coeff_0to1_hires /= Z_pf
    σ_VibRaman_coeff_1to0_hires /= Z_pf
    #@show Z_pf
    @pack! mol.effCoeff = σ_Rayl_coeff_hires, σ_VibRaman_coeff_0to1_hires, σ_VibRaman_coeff_1to0_hires, Δν̃_Rayl_coeff_hires, Δν̃_VibRaman_coeff_0to1_hires, Δν̃_VibRaman_coeff_1to0_hires
end

#Compute energy levels [in wavenumbers [cm^{-1}]] corresponding to v={0, 1, 2} and J={0, 1, 2,..., 10}
function compute_energy_levels!(mol::MolecularConstants{FT}; vmax=2, Jmax=30) where {FT}#molecules::Array{MolecularConstants{FT}}) where {FT}
    #for mol in molecules
        @unpack E_vJ = mol.effCoeff
        @unpack Y = mol
        E_vJ = OffsetArray(zeros(FT, vmax+1,Jmax+1), 0:vmax, 0:Jmax);
        for v in 0:vmax, J in 0:Jmax
            E₁ = J*(J+1)
            for k in 1:5
                E₂ = (v+0.5)^(k-1)
                for l in 1:5
                    E₃ = E₁^(l-1) * E₂ * Y[k,l]
                    E_vJ[v,J] += E₃
                end
            end
        end
        @pack! mol.effCoeff = E_vJ
    #end
end

#Compute vibrational Raman scattering coefficient (for Δν=±1)
function compute_σ_VibRaman_coeff!(T, mol::MolecularConstants{FT}; vmax=2, Jmax=30) where {FT}#ν, molecules::Array{MolecularConstants{FT}}) where {FT}
    @unpack α_prime, γ_C_VibRaman, E_vJ,σ_VibRaman_coeff_0to1,σ_VibRaman_coeff_1to0, Δν̃_VibRaman_coeff_0to1, Δν̃_VibRaman_coeff_1to0 = mol.effCoeff

    #σ_Rayl_coeff = (128/3)π^5 * α̅^2 * F_King# * ν^4
    #Stokes
    vi = 0
    Δν̃ = E_vJ[1,0]-E_vJ[0,0]
    Nvib = (1 - exp(-h*c*Δν̃/(k_B*T)))^(-1)
    #sq_bₖ = h/(8π²c*E_νJ[vi,0])#bₖ = √(h/8π²cνₖ)
    σ_VibRaman_coeff_0to1 = 128π^5 * α_prime^2 * Nvib * (1+2*γ_C_VibRaman)/(3-4*γ_C_VibRaman)
    Δν̃_VibRaman_coeff_0to1 = -Δν̃
    #Anti-Stokes
    vi = 1

    Nvib = (exp(h*c*Δν̃/(k_B*T))-1)^(-1)
    #sq_bₖ = h/(8π²c*E_νJ[vi,0])#bₖ = √(h/8π²cνₖ)
    σ_VibRaman_coeff_1to0 = 128π^5 * α_prime^2 * Nvib * (1+2*γ_C_VibRaman)/(3-4*γ_C_VibRaman)
    Δν̃_VibRaman_coeff_1to0 = Δν̃

    @show σ_VibRaman_coeff_0to1,σ_VibRaman_coeff_1to0
    @pack! mol.effCoeff = σ_VibRaman_coeff_0to1, σ_VibRaman_coeff_1to0, Δν̃_VibRaman_coeff_0to1, Δν̃_VibRaman_coeff_1to0
end

function compute_σ_RoVibRaman_coeff!(T, mol::MolecularConstants{FT}; vmax=2, Jmax=30) where {FT}#molecules::Array{MolecularConstants{FT}}) where {FT}
    kᵥ = (256/27)*π^5
    #for mol in molecules
    @unpack γ̅, γ_prime,
        E_vJ, σ_RoRaman_coeff_JtoJm2,σ_RoRaman_coeff_JtoJp2,
        σ_RoVibRaman_coeff_0to1_JtoJm2,σ_RoVibRaman_coeff_0to1_JtoJp2,
        σ_RoVibRaman_coeff_1to0_JtoJm2,σ_RoVibRaman_coeff_1to0_JtoJp2,
        Δν̃_RoRaman_coeff_JtoJm2, Δν̃_RoRaman_coeff_JtoJp2,
        Δν̃_RoVibRaman_coeff_0to1_JtoJm2, Δν̃_RoVibRaman_coeff_0to1_JtoJp2,
        Δν̃_RoVibRaman_coeff_1to0_JtoJm2, Δν̃_RoVibRaman_coeff_1to0_JtoJp2 = mol.effCoeff
    @unpack gₛ = mol

    #σ_VibRaman_coeff = OffsetArray(zeros(FT, vmax+1,vmax+1), 0:vmax, 0:vmax);
    σ_RoVibRaman_coeff_0to1_JtoJm2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax); #zeros(3, 11, 3, 11) #(vi, ji, vf, jf)
    σ_RoVibRaman_coeff_0to1_JtoJp2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    σ_RoVibRaman_coeff_1to0_JtoJm2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax); #zeros(3, 11, 3, 11) #(vi, ji, vf, jf)
    σ_RoVibRaman_coeff_1to0_JtoJp2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    σ_RoRaman_coeff_JtoJm2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    σ_RoRaman_coeff_JtoJp2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);

    Δν̃_RoVibRaman_coeff_0to1_JtoJm2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax); #zeros(3, 11, 3, 11) #(vi, ji, vf, jf)
    Δν̃_RoVibRaman_coeff_0to1_JtoJp2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    Δν̃_RoVibRaman_coeff_1to0_JtoJm2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax); #zeros(3, 11, 3, 11) #(vi, ji, vf, jf)
    Δν̃_RoVibRaman_coeff_1to0_JtoJp2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    Δν̃_RoRaman_coeff_JtoJm2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    Δν̃_RoRaman_coeff_JtoJp2 = OffsetArray(zeros(FT, Jmax+1), 0:Jmax);
    #Δνₛ = OffsetArray(zeros(FT, vmax+1,Jmax+1,vmax+1,Jmax+1), 0:vmax, 0:Jmax, 0:vmax, 0:Jmax); #zeros(3, 11, 3, 11)

    Z_pf = 0
    for Ji in 0:Jmax
        b_JJm2 = 3Ji*(Ji-1)/(2*(2Ji+1)*(2Ji-1))
        #b_JJ   = Ji*(Ji+1)/((2Ji-1)*(2Ji+3))
        b_JJp2 = 3*(Ji+1)*(Ji+2)/(2*(2Ji+1)*(2Ji+3))
        g_N = gₛ[2-Ji%2]

        #Rotational Raman scattering
        vi = 0
        vf = 0
        Ni_by_N = exp(-h*c*E_vJ[vi,Ji]/(k_B*T))
        @show vi, Ji, Ni_by_N
        Z_pf += g_N * (2Ji+1) * (exp(-h*c*E_vJ[0,Ji]/(k_B*T)) + exp(-h*c*E_vJ[1,Ji]/(k_B*T))) #accounting for both the ground- and the first excited vibrational state - depending on the ambient temperature (e.g. on hot exoplanets), more states may have to be accounted for

        Jf=Ji-2
        if (Ji-2)>=0 #anti-Stokes
            #Change in molecular energy level
            Δν = E_vJ[vf, Jf] - E_vJ[vi, Ji]
            #Wavenumber change of scattered light
            Δνₛ = - Δν

            σ_RoRaman_coeff_JtoJm2[Ji] = kᵥ * g_N * (2Ji+1) * b_JJm2 * Ni_by_N * γ̅^2# * νₛ[vi, Ji, vf, Jf]^4
            Δν̃_RoRaman_coeff_JtoJm2[Ji] = Δνₛ
        else
            σ_RoRaman_coeff_JtoJm2[Ji] = 0
            Δν̃_RoRaman_coeff_JtoJm2[Ji] = 0
        end
        Jf=Ji+2
        if (Ji+2)<=Jmax #Stokes
            #Change in molecular energy level
            Δν = E_vJ[vf, Jf] - E_vJ[vi, Ji]
            #Wavenumber change of scattered light
            Δνₛ = - Δν

            σ_RoRaman_coeff_JtoJp2[Ji] = kᵥ * g_N * (2Ji+1) * b_JJp2 * Ni_by_N * γ̅^2# * νₛ[vi, Ji, vf, Jf]^4
            Δν̃_RoRaman_coeff_JtoJp2[Ji] = Δνₛ
        else
            σ_RoRaman_coeff_JtoJp2[Ji] = 0
            Δν̃_RoRaman_coeff_JtoJp2[Ji] = 0
        end

        #Rovibrational Raman scattering: Stokes
        vi = 0
        vf = 1

        Ni_by_N = exp(-h*c*E_vJ[vi,Ji]/(k_B*T))

        Jf=Ji-2
        #b_v = (1/2π)*(h/(2c*E_vJ[vf,Jf]))^0.5
        if (Ji-2)>=0 #anti-Stokes
            #Change in molecular energy level
            Δν = E_vJ[vf, Jf] - E_vJ[vi, Ji]
            #Wavenumber change of scattered light
            Δνₛ = - Δν

            σ_RoVibRaman_coeff_0to1_JtoJm2[Ji] = kᵥ * g_N * (2Ji+1) * b_JJm2 * Ni_by_N * (γ_prime)^2# * νₛ[vi, Ji, vf, Jf]^4
            Δν̃_RoVibRaman_coeff_0to1_JtoJm2[Ji] = Δνₛ
        else
            σ_RoVibRaman_coeff_0to1_JtoJm2[Ji] = 0
            Δν̃_RoVibRaman_coeff_0to1_JtoJm2[Ji] = 0
        end
        Jf=Ji+2
        #b_v = (1/2π)*(h/(2c*E_vJ[vf,Jf]))^0.5
        if (Ji+2)<=Jmax #Stokes
            #Change in molecular energy level
            Δν = E_vJ[vf, Jf] - E_vJ[vi, Ji]
            #Wavenumber change of scattered light
            Δνₛ = - Δν

            σ_RoVibRaman_coeff_0to1_JtoJp2[Ji] = kᵥ * g_N * (2Ji+1) * b_JJp2 * Ni_by_N * (γ_prime)^2# * νₛ[vi, Ji, vf, Jf]^4
            Δν̃_RoVibRaman_coeff_0to1_JtoJp2[Ji] = Δνₛ
        else
            σ_RoVibRaman_coeff_0to1_JtoJp2[Ji] = 0
            Δν̃_RoVibRaman_coeff_0to1_JtoJp2[Ji] = 0
        end

        #Rovibrational Raman scattering: anti-Stokes
        vi = 1
        vf = 0

        Ni_by_N = exp(-h*c*E_vJ[vi,Ji]/(k_B*T))

        Jf=Ji-2
        if (Ji-2)>=0 #anti-Stokes
            #Change in molecular energy level
            Δν = E_vJ[vf, Jf] - E_vJ[vi, Ji]
            #Wavenumber change of scattered light
            Δνₛ = - Δν

            σ_RoVibRaman_coeff_1to0_JtoJm2[Ji] = kᵥ * g_N * (2Ji+1) * b_JJm2 * Ni_by_N * (γ_prime)^2# * νₛ[vi, Ji, vf, Jf]^4
            Δν̃_RoVibRaman_coeff_1to0_JtoJm2[Ji] = Δνₛ
        else
            σ_RoVibRaman_coeff_1to0_JtoJm2[Ji] = 0
            Δν̃_RoVibRaman_coeff_1to0_JtoJm2[Ji] = 0
        end
        Jf=Ji+2
        if (Ji+2)<=Jmax #Stokes
            #Change in molecular energy level
            Δν = E_vJ[vf, Jf] - E_vJ[vi, Ji]
            #Wavenumber change of scattered light
            Δνₛ = - Δν

            σ_RoVibRaman_coeff_1to0_JtoJp2[Ji] = kᵥ * g_N * (2Ji+1) * b_JJp2 * Ni_by_N * (γ_prime)^2# * νₛ[vi, Ji, vf, Jf]^4
            Δν̃_RoVibRaman_coeff_1to0_JtoJp2[Ji] = Δνₛ
        else
            σ_RoVibRaman_coeff_1to0_JtoJp2[Ji] = 0
            Δν̃_RoVibRaman_coeff_1to0_JtoJp2[Ji] = 0
        end
    end
    σ_RoRaman_coeff_JtoJm2 /= Z_pf
    σ_RoRaman_coeff_JtoJp2 /= Z_pf
    σ_RoVibRaman_coeff_0to1_JtoJm2 /= Z_pf
    σ_RoVibRaman_coeff_0to1_JtoJp2 /= Z_pf
    σ_RoVibRaman_coeff_1to0_JtoJm2 /= Z_pf
    σ_RoVibRaman_coeff_1to0_JtoJp2 /= Z_pf

    @show Z_pf
    @pack! mol.effCoeff = σ_RoRaman_coeff_JtoJm2, σ_RoRaman_coeff_JtoJp2,
        σ_RoVibRaman_coeff_0to1_JtoJm2, σ_RoVibRaman_coeff_0to1_JtoJp2,
        σ_RoVibRaman_coeff_1to0_JtoJm2, σ_RoVibRaman_coeff_1to0_JtoJp2,
        Δν̃_RoRaman_coeff_JtoJm2, Δν̃_RoRaman_coeff_JtoJp2,
        Δν̃_RoVibRaman_coeff_0to1_JtoJm2, Δν̃_RoVibRaman_coeff_0to1_JtoJp2,
        Δν̃_RoVibRaman_coeff_1to0_JtoJm2, Δν̃_RoVibRaman_coeff_1to0_JtoJp2
end


using JuMP, EAGO

                     m = Model()

                     EAGO.register_eago_operators!(m)

                     @variable(m, -1 <= x[i=1:2] <= 1)

                     @variable(m, -6.355742389270071 <= q <= 5.291892702058364)

                     add_NL_constraint(m, :(swish(-0.046987465146302476 + 0.9722813338228051*$(x[1]) + -0.7814511744189074*$(x[2])) + swish(-0.9625414337890552 + 0.5872522747234861*$(x[1]) + 0.38591887418496684*$(x[2])) + swish(0.22754410958881088 + -0.15392955055702906*$(x[1]) + -0.6389708691593214*$(x[2])) + swish(-0.10351168983583303 + 0.8921688569394703*$(x[1]) + -0.5909582391668393*$(x[2])) + swish(0.35357163557652704 + -0.6460660745152582*$(x[1]) + 0.17482029817613265*$(x[2])) - $q <= 0.0))

                     @objective(m, Min, q)

                     return m


module Lookup
using Combinatorics
using ..PokerCard

export get_lexographically_next_bit_sequence, flush_lookup, unsuited_lookup

# ref lookup.py in deuces

"""
Number of Distinct Hand Values:
Straight Flush   10
Four of a Kind   156      [(13 choose 2) * (2 choose 1)]
Full Houses      156      [(13 choose 2) * (2 choose 1)]
Flush            1277     [(13 choose 5) - 10 straight flushes]
Straight         10
Three of a Kind  858      [(13 choose 3) * (3 choose 1)]
Two Pair         858      [(13 choose 3) * (3 choose 2)]
One Pair         2860     [(13 choose 4) * (4 choose 1)]
High Card      + 1277     [(13 choose 5) - 10 straights]
-------------------------
TOTAL            7462
Here we create a lookup table which maps:
    5 card hand's unique prime product => rank in range [1, 7462]
Examples:
* Royal flush (best hand possible)          => 1
* 7-5-4-3-2 unsuited (worst hand possible)  => 7462
"""

###############################
# SOME CONSTANTS
###############################
MAX_STRAIGHT_FLUSH  = 10
MAX_FOUR_OF_A_KIND  = 166
MAX_FULL_HOUSE      = 322
MAX_FLUSH           = 1599
MAX_STRAIGHT        = 1609
MAX_THREE_OF_A_KIND = 2467
MAX_TWO_PAIR        = 3325
MAX_PAIR            = 6185
MAX_HIGH_CARD       = 7462
MAX_TO_RANK_CLASS = Dict(
    MAX_STRAIGHT_FLUSH => 1,
    MAX_FOUR_OF_A_KIND => 2,
    MAX_FULL_HOUSE => 3,
    MAX_FLUSH => 4,
    MAX_STRAIGHT => 5,
    MAX_THREE_OF_A_KIND => 6,
    MAX_TWO_PAIR => 7,
    MAX_PAIR => 8,
    MAX_HIGH_CARD => 9
)
RANK_CLASS_TO_STRING = Dict(
    1 => "Straight Flush",
    2 => "Four of a Kind",
    3 => "Full House",
    4 => "Flush",
    5 => "Straight",
    6 => "Three of a Kind",
    7 => "Two Pair",
    8 => "Pair",
    9 => "High Card"
)
flush_lookup = Dict()
unsuited_lookup = Dict()
###############################
# BITHACK FUNCTION
###############################
function get_lexographically_next_bit_sequence(bits)
    """
    Bit hack from here:
    http://www-graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation
    Generator even does this in poker order rank
    so no need to sort when done! Perfect.
    """
    bits = bits%UInt
    t = (bits | (bits -1)) + 1
    a = (t & -t)
    b = UInt(a) / UInt((bits & -bits))
    c = (UInt(b) >> UInt(1)) - 1
    next = UInt(t) | UInt(c)
    return (next)
end
###############################
# STRAIGHTS AND HIGHCARDS
###############################
function straight_and_highcards(straights, highcards)
    """
    Unique five card sets. Straights and highcards.
    Reuses bit sequences from flush calculations.
    """
    ### STRAIGHTS ###
    # since straights are weaker than flushes, we start iterating from there
    rank = MAX_FLUSH + 1
    for s in straights
        # iterate through the straights
        prime_product = PokerCard.prime_product_from_rankbits(s)
        unsuited_lookup[prime_product] = rank
        rank += 1
    end

    ### HIGH CARDS ###
    # high cards are weaker than pairs, we'll iterate after the weakest pair
    rank = MAX_PAIR
    for h in highcards
        prime_product = PokerCard.prime_product_from_rankbits(h)
        unsuited_lookup[prime_product] = rank
        rank += 1
    end
end # straight_and_highcards
###############################
# FLUSHES
###############################
function flushes()
    """
    Straight flushes and flushes.
    Lookup is done on 13 bit integer (2^13 > 7462):
    xxxbbbbb bbbbbbbb => integer hand index
    """
    flushes = []
    global straight_flushes = [
        7936, # int('0b1111100000000', 2), # royal flush
        3968, # int('0b111110000000', 2),
        1984, # int('0b11111000000', 2),
        992, # int('0b1111100000', 2),
        496, # int('0b111110000', 2),
        248, # int('0b11111000', 2),
        124, # int('0b1111100', 2),
        62, # int('0b111110', 2),
        31, # int('0b11111', 2),
        4111 # int('0b1000000001111', 2) # 5 high flush
    ]
    # start the current bit sequence

    # 1277 = number of high cards (ie no pairs = all unique ranks)
    # 1277 + len(str_flushes) is number of hands with all cards unique rank

    for i in 1:(1277 + length(straight_flushes) -1 )
        if i==1
            v = parse(UInt, "11111"; base=2)
            global current_bit_sequence = get_lexographically_next_bit_sequence(v)
        else
            global current_bit_sequence = get_lexographically_next_bit_sequence(current_bit_sequence)
        end

        notSF = true
        for sf in straight_flushes
            # if current_bit_sequence XOR sf == 0
            # then we have a sf and should not add
            if current_bit_sequence ⊻ sf == false
                notSF = false
            end
        end
        if notSF
            push!(flushes, current_bit_sequence)
        end
    end
    # we started from the lowest straight pattern, now we want to start ranking from
    # the most powerful hands, so we reverse
    reverse!(flushes)
    # now add to the lookup map:
    # start with straight flushes and the rank of 1
    # since theyit is the best hand in poker
    # rank 1 = Royal Flush!
    rank  = 1
    for sf in straight_flushes
        prime_product = PokerCard.prime_product_from_rankbits(sf)
        flush_lookup[prime_product] = rank
        rank += 1
    end
    # we start the counting for flushes starting from  max full house, which
    # is the worst rank that a full house can have (2,2,2,3,3)
    rank = MAX_FULL_HOUSE + 1
    for f in flushes
        prime_product = PokerCard.prime_product_from_rankbits(f)
        flush_lookup[prime_product] = rank
        rank += 1
    end
    @assert length(flush_lookup) == 1287 # 1277 flushes + 10 straight FLUSHES

    # make sure we account for the overlaps
    straight_and_highcards(straight_flushes, flushes)
end # flushes
###############################
# MULTIPLES
###############################
function multiples()
    """
    Pair, Two Pair, Three of a Kind, Full House, and 4 of a Kind.
    """
    backwards_ranks = reverse!(collect(1:13))

    ### FOUR OF A KIND
    rank = MAX_STRAIGHT_FLUSH + 1
    for i in backwards_ranks
        kickers = copy(backwards_ranks)
        filter!(e -> e ≠ i, kickers)
        for k in kickers
            product = (PokerCard.PRIMES[i] ^ 4) * PokerCard.PRIMES[k]
            unsuited_lookup[product] = rank
            rank +=1
        end
    end

    ### FULL HOUSE
    rank = MAX_FOUR_OF_A_KIND + 1
    for i in backwards_ranks
        pairranks = copy(backwards_ranks)
        filter!(e -> e ≠ i, pairranks)
        for pr in pairranks
            product = (PokerCard.PRIMES[i] ^ 3) * PokerCard.PRIMES[pr] ^ 2
            unsuited_lookup[product] = rank
            rank += 1
        end
    end

    ### THREE OF A KIND
    rank = MAX_STRAIGHT + 1
    for r in backwards_ranks
        kickers = copy(backwards_ranks)
        filter!(e -> e ≠ r, kickers)
        kickers_combos = collect(combinations(kickers, 2))
        for kickers in kickers_combos
            c1, c2 = kickers
            product = PokerCard.PRIMES[r]^3 * PokerCard.PRIMES[c1] * PokerCard.PRIMES[c2]
            unsuited_lookup[product] = rank
            rank += 1
        end

    end
    ### TWO PAIR
    rank = MAX_THREE_OF_A_KIND
    tpgen = collect(combinations(backwards_ranks, 2))
    for tp in tpgen
        pair1, pair2 = tp
        kickers = copy(backwards_ranks)
        filter!(e -> e ∉ tp, kickers)
        for kicker in kickers
            product = (PokerCard.PRIMES[pair1]^2) * (PokerCard.PRIMES[pair2])^2 * (PokerCard.PRIMES[kicker])
            unsuited_lookup[product] = rank
            rank += 1
        end
    end
    ### PAIRS
    rank = MAX_TWO_PAIR + 1
    for pairrank in backwards_ranks
        kickers = copy(backwards_ranks)
        filter!(e -> e ≠ pairrank, kickers)
        kgen = collect(combinations(kickers, 3))
        for kickers in kgen
            k1, k2, k3 = kickers
            product = PokerCard.PRIMES[pairrank]^2 * PokerCard.PRIMES[k1] * PokerCard.PRIMES[k2] * PokerCard.PRIMES[k3]
            unsuited_lookup[product] = rank
            rank += 1
        end
    end
end # multiples

function write_table_to_disk(table, filepath)
    open(filepath, "w") do io
        for val in table
            prime_prod = val[1]
            rank = val[2]
            write(io, "$prime_prod, $rank \n")
        end
    end
end

flushes()
multiples()

# write the table to disk
# write_table_to_disk(flush_lookup, "Data/LOOKUP_TABLE_FLUSH.csv")
# write_table_to_disk(unsuited_lookup, "Data/LOOKUP_TABLE_UNSUITED.csv")

end # Lookup module


type ReLU{A<:AbstractArray} <: Pointwise{A}
    output :: A
    gradInput :: A
end

ReLU(T::Type, s...) = ReLU(Array(T, s...), Array(T, s...))

function updateOutput!{A}(self :: ReLU{A}, input :: A)
    self.output = input .* (input .>= zero(eltype(input)))
    return self.output
end

function updateGradInput!{A}(self :: ReLU{A}, input :: A, gradOutput :: A)
    self.gradInput = gradOutput .* (input .>= zero(eltype(input)))
    return self.gradInput
end

export ReLU


export plotGrid

"""
function plotGrid

	plots nodal and cell-centered grids

	Requirement: PyPlot must be installed.

	Usage:

	plotGrid(M::AbstractTensorMesh) - plots nodal grid M
	plotGrid(x,M)                   - plots grid defined by x (nodal or cc)

	plotGrid is based on PyPlot.plot and PyPlot.plot3D. All keywords from those functions
	are available and forwarded to those methods.

"""
function plotGrid(y,M;spacing=[1,1,1],color="blue",kwargs...)

    if length(y)==M.dim*prod(M.n)
        nn = M.n
    elseif length(y)==M.dim*prod(M.n .+ 1)
        nn = M.n .+ 1
    else
        error("plotGrid - unknown grid type, length(y)=$(length(y)), n=$(M.n), dim=$(M.dim)")
    end
    Y = reshape(y,(prod(nn),M.dim))
    yi(d) = reshape(Y[:,d],tuple(nn...))
    if M.dim==2
        J1 = 1:spacing[1]:nn[1]; y1 = reshape(yi(1),tuple(nn...));
        J2 = 1:spacing[2]:nn[2]; y2 = reshape(yi(2),tuple(nn...));
        p1 = plot!(y1[:,J2],y2[:,J2]; color=color,legend=nothing,  kwargs...);
        p2 = plot!(y1[J1,:]',y2[J1,:]';color=color,legend=nothing,  kwargs...);
    elseif M.dim==3
        pt1 = p3!(yi(1),yi(2),yi(3),nn,[1 2 3],spacing; color=color,legend=nothing, kwargs...)
		pt2 = p3!(yi(1),yi(2),yi(3),nn,[2,1,3],spacing; color=color,legend=nothing, kwargs...);
        pt3 = p3!(yi(1),yi(2),yi(3),nn,[3,1,2],spacing; color=color,legend=nothing, kwargs...);
	else
		error("plotGrid - cannot handle $(M.dim)-dimensional grids")
    end
end

plotGrid(M::AbstractMesh;kwargs...) = plotGrid(getNodalGrid(M),M;kwargs...)

"""
function p3

	helper function for plotGrid

"""
function p3!(y1,y2,y3,m,order,s;kwargs...)
    m  = m[order];
    s  = s[order];
    y(x)  =  reshape(permutedims(x,order),(m[1],m[2]*m[3]));
    y1    = y(y1); y2 = y(y2); y3 = y(y3);
    dx = Int.(round.(m./s))
    J2 = 1:s[2]:m[2];
    J3 = 1:s[3]:m[3];
    K2 = collect(J2)*ones(Int64,1,length(J3));
    K3 = ones(Int64,length(J2),1)*(m[2]*(collect(J3).-1))'
    K  = vec(K2)+vec(K3);
    for k=1:length(K)
        plot3D!(y1[:,K[k]],y2[:,K[k]], y3[:,K[k]];kwargs...)
    end
end


using Test, HoeffdingD
ab =[
    -1.0	2.03
    -1.0	2.11
    -1.0	2.03
    -0.8	1.58
    -0.8	1.79
    -0.8	1.58
    -0.6	1.35
    -0.6	1.46
    -0.6	1.37
    -0.4	1.28
    -0.4	1.15
    -0.4	1.19
    -0.2	0.93
    -0.2	0.99
    -0.2	0.99
    0.0 1.03
    0.0 0.98
    0.0 1.02
    0.2 1.00
    0.2 1.06
    0.2 1.00
    0.4 1.31
    0.4 1.33
    0.4 0.99
    0.6 1.28
    0.6 1.42
    0.6 1.50
    0.8 1.68
    0.8 1.64
    0.8 1.68
    1.0 2.07
    1.0 1.94
    1.0 2.14 ]

a = ab[:, 1]
b = ab[:, 2]

@testset "HoeffdingD" begin
    #results values are from SAS PROC Corr
    @test isapprox(0.1108736664, HoeffdingD.hoeffdingd(a, b))
    @test isapprox(0.1108736664, HoeffdingD.hoeffdingd(b, a))
    @test isapprox(0.9665923259, HoeffdingD.hoeffdingd(b, b))
    @test isapprox(0.8450535317, HoeffdingD.hoeffdingd(a, a))

    @test isapprox(0.6875, HoeffdingD.hoeffdingd(
            [9, 8,  7,  missing,    missing,    2,          5, 10, 2],
            [6, 5,  4,  missing,    3,          missing,    -5, 1000, 4]))

    @test isapprox([0.845053531701891 0.110873666447568; 0.110873666447568 0.9665923258586983], HoeffdingD.hoeffdingd(ab))

    linear_f(x) = @. 3x + 2
    quad_f(x) = @. x^2 + 1
    x_test = -1:0.05:1
    y_linear = linear_f.(x_test)
    y_quad = quad_f.(x_test)
    xyy = hcat(x_test, y_linear, y_quad)
    @test isapprox( [1.0 1.0 0.2183712793468891; 1.0 1.0 0.2183712793468891; 0.2183712793468891 0.2183712793468891 0.9369001852153328] , HoeffdingD.hoeffdingd(xyy))

end

@testset "HoeffdingD α" begin
    @test_throws DomainError HoeffdingD.hoeffdingd(ab, -1)
    @test [true false; false true] == last.(HoeffdingD.hoeffdingd(ab, .001))
    @test [true true; true true] == last.(HoeffdingD.hoeffdingd(ab, .05))
end

using Distributions

@testset "HoeffdingD independence" begin
    x = collect(1:200)
    y_uniform = rand(Distributions.Uniform(2,5), length(x))
    @test isapprox(0, last(HoeffdingD.hoeffdingd(x, y_uniform, 0.95)))
    @test isapprox(0, last(HoeffdingD.hoeffdingd(x, y_uniform, 0.5)))
    @test isapprox(0, last(HoeffdingD.hoeffdingd(x, y_uniform, 0.1)))
    @test isapprox(0, last(HoeffdingD.hoeffdingd(x, y_uniform, 0.05)))
    @test isapprox(0, last(HoeffdingD.hoeffdingd(x, y_uniform, 0.005)))
    @test isapprox(0, last(HoeffdingD.hoeffdingd(x, y_uniform, 0.001)))
end

# using BenchmarkTools

# @btime HoeffdingD.hoeffdingd(a, b)
# @btime HoeffdingD.hoeffdingd(b, a)
# @btime HoeffdingD.hoeffdingd(ab)

# @btime HoeffdingD.hoeffdingd(a, b, 0.05)
# @btime HoeffdingD.hoeffdingd(b, a, 0.05)
# @btime HoeffdingD.hoeffdingd(ab, 0.05)

# @btime HoeffdingD.hoeffdingd( [9, 8,  7.,  missing,    missing,    2,          5, 10, 2],
#             [6, 5,  4.,  missing,    3,          missing,    -5, 1000, 4])


function expandkwargs(kwargs::Vector)
  m = length(kwargs)
  keys = Vector{Symbol}(m)
  vals = Vector{Any}(m)
  for i in 1:m
    keys[i] = kwargs[i][1]
    vals[i] = kwargs[i][2]
  end
  return keys, vals
end

macro attach(obj, fields...)
  block = Expr(:block)
  for f in fields
    e = :($f = $obj.$f)
    push!(block.args, e)
  end
  return esc(:($block))
end

const greekdict = Dict(:σ => :sigma, :ρ => :rho, :η => :eta)

function greekin(g::Symbol, c::AbstractArray)
  if g in c
    return true
  else
    s = get(greekdict, g, :notfound)
    if s == :notfound
      error("an unknown greek letter is given")
    else
      return s in c
    end
  end
end

function greekfind(c::AbstractArray, g::Symbol)
  if g in c
    return findfirst(c, g)
  else
    s = get(greekdict, g, :notfound)
    if s == :notfound
      error("an unknown greek letter is given")
    else
      return findfirst(c, s)
    end
  end
end

function ∫(f::Function, a::Number, b::Number; e::Float64=1e-6)
  val, err = quadgk(f, a, b, reltol=e)
  return val
end

# Ref: http://www.eweb.unex.es/eweb/fisteor/santos/files/rdf.nb
function inverselaplace(F::Function, t::Float64, nterm::Int, meuler::Int)
  a::Float64 = 30.0
  h::Float64 = π/t
  u::Float64 = exp(a/2) / t
  x::Float64 = a / 2t

  c(m::Int)::Int = binomial(meuler, m-1)
  fs(s::Complex{Float64})::Float64 = real(F(s))

  suma::Float64 = fs(x + 0im)/2
  @simd for n in 1:nterm
    @inbounds suma += (-1)^n * fs(x + im*n*h)
  end

  su = Vector{Float64}(meuler+2)
  su[1] = suma
  for k in 1:meuler+1
    n = nterm + k
    @inbounds su[k+1] = su[k] + (-1)^n * fs(x + im*n*h)
  end

  # argsu::Float64 = 0
  argsu1::Float64 = 0
  @simd for j in 1:meuler+1
    # @inbounds argsu = argsu + c(j)*su[j]
    @inbounds argsu1 += c(j)*su[j+1]
  end

  # fun::Float64 = u * argsu / 2^meuler
  fun1::Float64 = u * argsu1 / 2^meuler

  return fun1
end

function spline(f::Function, a::Float64, b::Float64, N::Int; bc="error")::Function
  spl = spline_spl(f, a, b, N, bc)

  fspl(x::Real)::Float64 = evaluate(spl, x)
end

function spline_derivative(f::Function, a::Float64, b::Float64, N::Int;
                           bc="error")::Function
  spl = spline_spl(f, a, b, N, bc)

  f′spl(x::Real)::Float64 = derivative(spl, x)
end

function spline_integral(f::Function, a::Float64, b::Float64, N::Int)::Float64
  spl = spline_spl(f, a, b, N, "zero")
  integrate(spl, a, b)
end

function spline_spl(f::Function, a::Float64, b::Float64, N::Int,
                    bc::AbstractString)::Spline1D
  @assert a < b "invalid arguments for spline"

  Δx::Float64 = (b - a) / N
  𝐱::Vector{Float64} = collect(a : Δx : b)
  𝐲::Vector{Float64} = [f(x) for x in 𝐱]

  Spline1D(𝐱, 𝐲, k=3, bc=bc)
end

function cardano(a₃::Real, a₂::Real, a₁::Real, a₀::Real)
  A₂ = a₂ / a₃
  A₁ = a₁ / a₃
  A₀ = a₀ / a₃

  p = (A₁ - 1/3*A₂^2) / 3
  q = (A₀ - 1/3*A₁*A₂ + 2/27*A₂^3) / 2

  D = - (q^2 + p^3)

  if D ≈ 0 && q ≈ 0
    y = [0]
  elseif D ≈ 0
    y = [-2*∛q, ∛q]
  elseif D > 0
    Θ = atan2(√D, -q)
    R = (q^2 + D)^(1/6) * cos(Θ/3)
    Q = (q^2 + D)^(1/6) * sin(Θ/3)
    y = [2R, -R - √3*Q, -R + √3*Q]
  elseif D < 0
    S = ∛(-q + √(-D))
    T = ∛(-q - √(-D))
    y = [S+T, -1/2*(S+T) + √3/2*im*(S-T), -1/2*(S+T) - √3/2*im*(S-T)]
  end

  x = y - A₂/3
end


module TestARMA

using QuantEcon
using Base.Test
using FactCheck

# set up
phi = [.95, -.4, -.4]
theta = zeros(3)
sigma = .15
lp = ARMA(phi, theta, sigma)

facts("Testing arma.jl") do
    # test simulate
    sim = simulation(lp, ts_length=250)
    @fact length(sim) --> 250

    # test impulse response
    imp_resp = impulse_response(lp, impulse_length=75)
    @fact length(imp_resp) --> 75

    context("test constructors") do
        phi = 0.5
        theta = 0.4
        sigma = 0.15

        a1 = ARMA(phi, theta, sigma)
        a2 = ARMA([phi;], theta, sigma)
        a3 = ARMA(phi, [theta;], sigma)

        for nm in fieldnames(a1)
            @fact getfield(a1, nm) --> getfield(a2, nm)
            @fact getfield(a1, nm) --> getfield(a3, nm)
        end
    end

    context("test autocovariance") do
        θ = 0.5
        σ = 0.15
        ma1 = ARMA(Float64[], [θ], σ)
        ac = autocovariance(ma1; num_autocov=5)

        # first is the variance. equal to (1 + θ^2) sigma^2
        @fact ac[1] --> roughly((1+θ^2)*σ^2; atol=1e-3)

        # second should be θ σ^2
        @fact ac[2] --> roughly(θ*σ^2; atol=1e-3)

        # all others should be 0
        @fact ac[3:end] --> roughly(zeros(ac[3:end]); atol=1e-3)
    end

end  # facts
end  # module


import Pkg
Pkg.instantiate() # downloads all dependencies for the current project

Pkg.add("Plots")

using Plots, DataFrames, CSV

df = CSV.read("../externals/Core.Math.Data/data/Pejcic_318.csv", copycols = true)

a = df.ATT # use something to plot;
b = df.ATV
c = df.AOP
d = df.ANN

# display(plot(histogram(a)));

p1 = (histogram(a))
p2 = (histogram(b))
p3 = (histogram(c))
p4 = (histogram(d))

plot(p1, p2, p3, p4, layout = (2, 2), legend = false)


using Plots

"""
`plot_phimatrix(outputfile::String, burnin::Int64, thin::Int64)`

Plots the mean phi values resulting from pmdi output
## Input
- `outputFile::String` a string referring to the location on disk of pmdi output
- `burnin::Int64` an integer of the number of initial iterations to discard as burn-in
- `thin::Int64` an integer for the rate at which to thin output, `thin = 2` discards
every second iteration

## Output
Outputs a heatmap of pairwise dataset Φ values
"""
function get_phi(outputFile::String, burnin::Int64 = 0, thin::Int64 = 0)
    outputNames = split(readline(outputFile), ',')
    phiColumns = map(outputNames) do str
        occursin(r"phi_", str)
    end
    output = readdlm(outputFile, ',', header = false, skipstart = burnin + 1)

    phiValues = Matrix{Float64}(output[1:thin:end, phiColumns])
    return phiValues
end

function plot_phi_matrix(outputFile::String, burnin::Int64 = 0, thin::Int64 = 1)
    phi_values = get_phi(outputFile, burnin, thin)
    K = Int64(0.5 + sqrt(8 * size(phi_values, 2) + 1) * 0.5)
    @assert K > 1 "Φ not inferred for no. of datasets = 1"
    phi_matrix = Matrix{Float64}(undef, K, K)
    phi_matrix .= NaN

    i = 1
    for k1 = 1:(K - 1)
        for k2 = (k1 + 1):K
            phi_matrix[k1, k2] = phi_matrix[k2, k1] = Statistics.mean(phi_values[:, i])
            i += 1
        end
    end

    Plots.heatmap(["phi (., $i)" for i = 1:K],
                  ["phi ($i, .)" for i = 1:K],
                  phi_matrix,
                  yflip = true,
                  widen = false,
                  left_margin = 20px,
                  c = :viridis,
                  clim = (0, maximum(phi_matrix)),
                  aspect_ratio = 1)
end

"""
`plot_phichain(outputfile::String, burnin::Int64, thin::Int64)`

Plots the phi values at each iteration resulting from pmdi output
## Input
- `outputFile::String` a string referring to the location on disk of pmdi output
- `burnin::Int64` an integer of the number of initial iterations to discard as burn-in
- `thin::Int64` an integer for the rate at which to thin output, `thin = 2` discards
every second iteration

## Output
Outputs a line plot of phi values resulting from pmdi output
"""
function plot_phi_chain(outputFile::String, burnin::Int64 = 0, thin::Int64 = 1)
    phi_values = get_phi(outputFile, burnin, thin)
    n_phis = size(phi_values, 2)
    K = Int64(0.5 + sqrt(8 * size(phi_values, 2) + 1) * 0.5)
    @assert K > 1 "Φ not inferred for no. of datasets = 1"
    phi_names = ["phi ($i, $j)" for i = 1:(K - 1) for j = (i + 1):K]
    Plots.plot(phi_values,
               layout = n_phis,
               legend = false,
               palette = :viridis,
               ylims = [0, maximum(phi_values)],
               title = [phi_names[i] for j = 1:1, i = 1:n_phis],
               xticks = false)
end


module SkeletonTriangulationsTests

using Test
using Gridap.TensorValues
using Gridap.Arrays
using Gridap.Fields
using Gridap.ReferenceFEs
using Gridap.Geometry
using Gridap.Geometry: IN, OUT
using Gridap.Geometry: DiscreteModelMock

model = DiscreteModelMock()

strian = SkeletonTriangulation(model)

trian = get_volume_triangulation(strian)

fun(x) = sin(pi*x[1])*sin(pi*x[2])

q2x = get_cell_map(trian)

q2fun = compose(fun,q2x)

s2fun = restrict(q2fun,strian)

s = CompressedArray([Point{1,Float64}[(0.25,),(0.75,)]],get_cell_type(strian))

fs = evaluate(s2fun.left-s2fun.right,s)

r = fill(zeros(2),length(fs))

test_array(fs,r)

#function polar(q)
#  r, t, z = q
#  x = r*cos(t)
#  y = r*sin(t)
#  Point(x,y,z)
#end
#domain = (1,2,0,pi,0,0.5)
#partition = (10,30,4)
#model = CartesianDiscreteModel(domain,partition,polar)

domain = (0,1,0,1,0,1)
partition = (3,3,3)
model = CartesianDiscreteModel(domain,partition)

strian = SkeletonTriangulation(model)
test_triangulation(strian)

trian = get_volume_triangulation(strian)

s = CompressedArray([Point{2,Float64}[(0.25,0.25),(0.75,0.75)]],get_cell_type(strian))

s2x = get_cell_map(strian)
x = evaluate(s2x,s)

nvec = get_normal_vector(strian)
nx = evaluate(nvec,s)
collect(nx)

fun(x) = sin(pi*x[1])*cos(pi*x[2])

q2x = get_cell_map(trian)

funq = compose(fun,q2x)

fun_gamma = restrict(funq,strian)

@test isa(fun_gamma, SkeletonPair)

cellids = collect(1:num_cells(trian))

cellids_gamma = reindex(cellids,strian)
@test isa(fun_gamma, SkeletonPair)
@test cellids_gamma.left == get_face_to_cell(strian.left)
@test cellids_gamma.right == get_face_to_cell(strian.right)

ids = get_cell_id(strian)
@test isa(ids,SkeletonPair)

#using Gridap.Visualization
#
#writevtk(trian,"trian")
#writevtk(strian,"strian")
#writevtk(x,"x",nodaldata=["nvec" => nx])

model = DiscreteModelMock()

trian = Triangulation(model)

cell_to_is_left = [true,true,false,true,false]

itrian = InterfaceTriangulation(model,cell_to_is_left)

ltrian = get_left_boundary(itrian)
rtrian = get_right_boundary(itrian)

ni = get_normal_vector(itrian)
nl = get_normal_vector(ltrian)
nr = get_normal_vector(rtrian)

#using Gridap.Visualization
#
#writevtk(trian,"trian")
#writevtk(itrian,"itrian",nsubcells=10,cellfields=["ni"=>ni,"nl"=>nl,"nr"=>nr])

domain = (0,1,0,1)
partition = (10,10)
model = CartesianDiscreteModel(domain,partition)

cell_to_inout = zeros(Int8,num_cells(model))
cell_to_inout[1:13] .= OUT
cell_to_inout[14:34] .= IN

itrian = InterfaceTriangulation(model,cell_to_inout)
@test num_cells(itrian) == 11

itrian = InterfaceTriangulation(model,1:13,14:34)
@test num_cells(itrian) == 11

#ltrian = get_left_boundary(itrian)
#rtrian = get_right_boundary(itrian)
#ni = get_normal_vector(itrian)
#nl = get_normal_vector(ltrian)
#nr = get_normal_vector(rtrian)
#trian = Triangulation(model)
#
#using Gridap.Visualization
#writevtk(trian,"trian",celldata=["inout"=>cell_to_inout])
#writevtk(itrian,"itrian",nsubcells=10,cellfields=["ni"=>ni,"nl"=>nl,"nr"=>nr])

reffe = LagrangianRefFE(Float64,QUAD,(2,2))
conf = CDConformity((CONT,DISC))
face_own_dofs = get_face_own_dofs(reffe,conf)
strian = SkeletonTriangulation(model,reffe,face_own_dofs)
ns = get_normal_vector(strian)

#using Gridap.Visualization
#writevtk(strian,"strian",cellfields=["normal"=>ns])

end # module


module TestDecisionTree

using Test
import CategoricalArrays
import CategoricalArrays.categorical
using MLJBase
using Random
Random.seed!(1234)

# load code to be tested:
import MLJModels
import DecisionTree
using MLJModels.DecisionTree_

# get some test data:
X, y = @load_iris

baretree = DecisionTreeClassifier()

baretree.max_depth = 1
fitresult, cache, report = MLJBase.fit(baretree, 2, X, y);
baretree.max_depth = -1 # no max depth
fitresult, cache, report =
    MLJBase.update(baretree, 1, fitresult, cache, X, y);

# in this case decision tree is a perfect predictor:
yhat = MLJBase.predict_mode(baretree, fitresult, X);
@test yhat == y

# but pruning upsets this:
baretree.post_prune = true
baretree.merge_purity_threshold=0.1
fitresult, cache, report =
    MLJBase.update(baretree, 2, fitresult, cache, X, y)
yhat = MLJBase.predict_mode(baretree, fitresult, X);
@test yhat != y
yhat = MLJBase.predict(baretree, fitresult, X);

# check preservation of levels:
yyhat = predict_mode(baretree, fitresult, MLJBase.selectrows(X, 1:3))
@test MLJBase.classes(yyhat[1]) == MLJBase.classes(y[1])

info_dict(baretree)

# # testing machine interface:
# tree = machine(baretree, X, y)
# fit!(tree)
# yyhat = predict_mode(tree, MLJBase.selectrows(X, 1:3))
using Random: seed!
seed!(0)

n,m = 10^3, 5;
raw_features = rand(n,m);
weights = rand(-1:1,m);
labels = raw_features * weights;
features = MLJBase.table(raw_features);

R1Tree = DecisionTreeRegressor(min_samples_leaf=5, merge_purity_threshold=0.1)
R2Tree = DecisionTreeRegressor(min_samples_split=5)
model1, = MLJBase.fit(R1Tree,1, features, labels)

vals1 = MLJBase.predict(R1Tree,model1,features)
R1Tree.post_prune = true
model1_prune, = MLJBase.fit(R1Tree,1, features, labels)
vals1_prune = MLJBase.predict(R1Tree,model1_prune,features)
@test vals1 !=vals1_prune

@test DecisionTree.R2(labels, vals1) > 0.8

model2, = MLJBase.fit(R2Tree, 1, features, labels)
vals2 = MLJBase.predict(R2Tree, model2, features)
@test DecisionTree.R2(labels, vals2) > 0.8

## TEST ON ORDINAL FEATURES OTHER THAN CONTINUOUS

N = 20
X = (x1=rand(N), x2=categorical(rand("abc", N), ordered=true), x3=collect(1:N))
yfinite = X.x2
ycont = float.(X.x3)

rgs = DecisionTreeRegressor()
fitresult, _, _ = MLJBase.fit(rgs, 1, X, ycont)
@test rms(predict(rgs, fitresult, X), ycont) < 1.5

clf = DecisionTreeClassifier(pdf_smoothing=0)
fitresult, _, _ = MLJBase.fit(clf, 1, X, yfinite)
@test sum(predict(clf, fitresult, X) .== yfinite) == 0 # perfect prediction

info_dict(R1Tree)

# --  Ensemble

rfc = RandomForestClassifier()
abs = AdaBoostStumpClassifier()

X, y = MLJBase.make_blobs(100, 3; rng=555)

m = machine(rfc, X, y)
fit!(m)
@test accuracy(predict_mode(m, X), y) > 0.95

m = machine(abs, X, y)
fit!(m)
@test accuracy(predict_mode(m, X), y) > 0.95

X, y = MLJBase.make_regression(rng=5124)
rfr = RandomForestRegressor()
m = machine(rfr, X, y)
fit!(m)
@test rms(predict(m, X), y) < 0.3

end
true


using Genie, Genie.Renderer.Html, Stipple, StipplePlotly

Genie.config.log_requests = false

xx = -π:(2π/250):π

xxs = -3.0:0.2:3.0

# Data:

pl1 = PlotData(
    x = xx, y = sin.(xx), plot = StipplePlotly.Charts.PLOT_TYPE_SCATTER,
    name = "sine", mode = "lines", xaxis = "x", yaxis = "y", line = PlotlyLine(color = "rgb(0,0,192)", dash="solid")
)

pl2 = PlotData(
    x = xx, y = sinh.(xx), plot = StipplePlotly.Charts.PLOT_TYPE_SCATTER,
    name = "sinh", mode = "lines", xaxis = "x2", yaxis = "y2", line = PlotlyLine(color = "rgb(0,192,0)", dash="dot")
)

pl3 = PlotData(
    x = xx, y = cos.(xx), plot = StipplePlotly.Charts.PLOT_TYPE_SCATTER,
    name = "cosine", mode = "lines", xaxis = "x3", yaxis = "y3", line = PlotlyLine(color = "rgb(192,0,0)", dash="dash")
)

pl4 = PlotData(
    x = xx, y = cosh.(xx), plot = StipplePlotly.Charts.PLOT_TYPE_SCATTER,
    name = "cosh", mode = "lines", xaxis = "x4", yaxis = "y4", line = PlotlyLine(color = "rgb(192,0,192)", dash="dashdot")
)

ps1 = PlotData(
    x = xxs, y = sin.(xxs) .+ rand(Float64, size(xxs)) .- 0.5, plot = StipplePlotly.Charts.PLOT_TYPE_SCATTER,
    name = "sine", mode = "markers", xaxis = "x", yaxis = "y", marker = PlotDataMarker(color="rgb(0,0,192)", symbol="circle", size=10, opacity=0.5)
)

ps2 = PlotData(
    x = xxs, y = sinh.(xxs) .+ 3.0 .* rand(Float64, size(xxs)) .- 1.5, plot = StipplePlotly.Charts.PLOT_TYPE_SCATTER,
    name = "sinh", mode = "markers", xaxis = "x2", yaxis = "y2", marker = PlotDataMarker(color = "rgb(0,192,0)", symbol="circle-open", size=14)
)

ps3 = PlotData(
    x = xxs, y = cos.(xxs) .+ rand(Float64, size(xxs)) .- 0.5, plot = StipplePlotly.Charts.PLOT_TYPE_SCATTER,
    name = "cosine", mode = "markers", xaxis = "x3", yaxis = "y3", marker = PlotDataMarker(color = "rgb(192,0,0)", symbol="diamond", size=10, opacity=0.5)
)

ps4 = PlotData(
    x = xxs, y = cosh.(xxs) .+ 3.0 .* rand(Float64, size(xxs)) .- 1.5, plot = StipplePlotly.Charts.PLOT_TYPE_SCATTER,
    name = "cosh", mode = "markers", xaxis = "x4", yaxis = "y4", marker = PlotDataMarker(color = "rgb(192,0,192)", symbol="diamond-open", size=3)
)

plotdata = [ps1, ps2, ps3, ps4, pl1, pl2, pl3, pl4];

# Layout

layout = PlotLayout(
    title = PlotLayoutTitle(text="Multiple Mixed Subplots", font=Font(24)),
    showlegend = false,
    grid = PlotLayoutGrid(rows = 2, columns = 2, pattern = "independent"),
    xaxis = [
        PlotLayoutAxis(xy = "x", index = 1, ticks = "outside", showline = true, zeroline = false),
        PlotLayoutAxis(xy = "x", index = 2, ticks = "outside", showline = true, zeroline = false),
        PlotLayoutAxis(xy = "x", index = 3, ticks = "outside", showline = true, zeroline = false, title="range (arb. units)"),
        PlotLayoutAxis(xy = "x", index = 4, ticks = "outside", showline = true, zeroline = false, title="range (arb. units)")
    ],
    yaxis = [
        PlotLayoutAxis(xy = "y", index = 1, ticks = "outside", showline = true, zeroline = false, title="response A"),
        PlotLayoutAxis(xy = "y", index = 2, ticks = "outside", showline = true, zeroline = false, title="response B"),
        PlotLayoutAxis(xy = "y", index = 3, ticks = "outside", showline = true, zeroline = false, title="response C"),
        PlotLayoutAxis(xy = "y", index = 4, ticks = "outside", showline = true, zeroline = false, title="response D")
    ],
)

Base.@kwdef mutable struct Model <: ReactiveModel
  data::R{Vector{PlotData}} = plotdata, READONLY
  layout::R{PlotLayout} = layout, READONLY
  config::R{PlotConfig} = PlotConfig(), READONLY
end

model = Stipple.init(Model())

function ui()
  page(
    vm(model), class="container", [
        plot(:data, layout = :layout, config = :config)
    ]
  ) |> html
end

route("/", ui)

up()