export latex

using REPL
using Printf

const _SUPERSCRIPTNUMS = collect("⁰¹²³⁴⁵⁶⁷⁸⁹")
subscript(i::Integer) = (i<0 ? "₋" : "") * join('₀'+d for d in reverse(digits(abs(i))))
superscript(i::Integer) = (i<0 ? "⁻" : "") * join(_SUPERSCRIPTNUMS[d+1] for d in reverse(digits(abs(i))))

function printspaced(io::IO,v,trailing_space=false)
    isempty(v) && return
    print(io,v[1])
    iprinted = 1
    exponent = 1
    for i = 2:length(v)
        if v[i]==v[iprinted]
            exponent += 1
            # if the last element is repeated, print the exponent
            i == length(v) && print(io,superscript(exponent))
        else
            # print exponent for previous object
            exponent>1 && print(io,superscript(exponent))
            exponent = 1
            print(io," ")
            print(io,v[i])
            iprinted = i
        end
    end
    trailing_space && print(io," ")
end

Base.print(io::IO,A::BaseOpProduct) = printspaced(io,A.v)
function Base.print(io::IO,ii::QuIndex)
    if isnoindex(ii)
        return
    elseif isintindex(ii)
        print(io,ii.num)
    else
        print(io,ii.sym)
        ii.num != typemin(ii.num) && print(io,subscript(ii.num))
    end
end

_print(io::IO,A::BaseOperator) = print(io,A.name,BaseOpType_sym[Int(A.t)],"(",A.inds...,")")
Base.print(io::IO,A::BaseOperator) = _print(io, unalias(A))
Base.print(io::IO,A::δ) = print(io,"δ(",A.iA,A.iB,")")
Base.print(io::IO,A::Param) = print(io, A.name, A.state=='c' ? "*" : "", length(A.inds)==0 ? "" : "($(A.inds...))")
Base.print(io::IO,A::ExpVal) = print(io,"⟨", A.ops, "⟩")
Base.print(io::IO,A::Corr) = print(io,"⟨", A.ops, "⟩c")

function Base.print(io::IO,A::QuTerm)
    if A.nsuminds > 0
        print(io,"∑")
        for ii=1:A.nsuminds
            print(io,subscript(ii))
        end
        print(io," ")
    end
    isempty(A.δs) || printspaced(io,A.δs,true)
    isempty(A.params) || printspaced(io,A.params,true)
    isempty(A.expvals) || printspaced(io,A.expvals,true)
    isempty(A.corrs) || printspaced(io,A.corrs,true)
    isempty(A.bares) || print(io,A.bares)
end

function print_as_connector(io,str::String,print_connector::Bool)
    if print_connector
        if !isempty(str) && str[1] == '-'
            print(io," - ",str[2:end])
        else
            print(io," + ",str)
        end
    else
        print(io,str)
    end
end

function print_term_scalar(io::IO,t::QuTerm,s::Number, print_connector::Bool)
    if isempty(t)
        print_as_connector(io,string(s),print_connector)
        return
    elseif s == -one(s)
        str = "-"
    elseif s == 1
        str = ""
    else
        str = numstring(s)*" "
    end
    print_as_connector(io,str,print_connector)
    print(io,t)
end

numstring(x::Number) = @sprintf "%g" x
numstring(x::Rational) = denominator(x)==1 ? string(numerator(x)) : string(x)
numstring(x::Complex) = ((r,i) = reim(x); iszero(i) ? numstring(r) : (iszero(r) ? numstring(i)*"i" : "($(numstring(r))$(i<0 ? '-' : '+')$(numstring(abs(i)))i)"))

function Base.print(io::IO,A::QuExpr)
    if isempty(A.terms)
        print(io,0)
    else
        for (ii,(t,s)) in enumerate(sort!(Tuple.(collect(A.terms))))
            print_connector = ii != 1
            print_term_scalar(io,t,s, print_connector)
        end
    end
end

Base.show(io::IO, A::QuantumObject) = print(io,A)
Base.show(io::IO, ::MIME"text/latex", A::QuantumObject) = (print(io,"\$"); printlatex(io,A); print(io,"\$"))

function latex(A::Union{QuantumObject,Number})
    io = IOBuffer()
    printlatex(io, A)
    String(take!(io))
end

function printlatex(io::IO,ii::QuIndex)
    if isnoindex(ii)
        return
    elseif isintindex(ii)
        print(io,ii.num)
    elseif issumindex(ii)
        print(io,"\\#_{",ii.num,"}")
    else
        print(io,ii.sym)
        ii.num != typemin(ii.num) && print(io,"_{",ii.num,"}")
    end
end

function printlatex(io::IO,v::Vector)
    isempty(v) && return
    prevA = v[1]
    exponent = 1
    for i = 2:length(v)
        if v[i]==prevA
            exponent += 1
        else
            # print previous object
            exponent > 1 && print(io,"{")
            printlatex(io,prevA)
            exponent > 1 && print(io,"}^{",exponent,"}")
            exponent = 1
            prevA = v[i]
            print(io," ")
        end
    end
    # print last object
    exponent > 1 && print(io,"{")
    printlatex(io,prevA)
    exponent > 1 && print(io,"}^{",exponent,"}")
end

latexjoin(args...) = join(latex.(args...))
latexindstr(inds::QuIndex...) = latexindstr(inds)
latexindstr(inds) = isempty(inds) ? "" : "_{$(latexjoin(inds))}"

numlatex(x::Number) = @sprintf "%g" x
numlatex(x::Rational) = denominator(x)==1 ? "$(numerator(x))" : "\\frac{$(numerator(x))}{$(denominator(x))}"
numlatex(x::Complex) = @sprintf "(%g%+gi)" real(x) imag(x)
numlatex(x::Complex{Rational{T}}) where T = @sprintf "\\left(%s%s%si\\right)" numlatex(real(x)) (imag(x)>=0 ? "+" : "-") numlatex(abs(imag(x)))

const _unicode_to_latex = Dict(v[1]=>"{$k}" for (k,v) in REPL.REPLCompletions.latex_symbols if length(v)==1)
unicode_to_latex(s::AbstractString) = join(map(c -> get(_unicode_to_latex,c,string(c)), collect(s)))
unicode_to_latex(s::QuOpName) = unicode_to_latex(string(s))

printlatex(io::IO,x::Number) = print(io, imag(x)==0 ? numlatex(real(x)) : (real(x)==0 ? numlatex(imag(x))*"i" : numlatex(x)))
_printlatex(io::IO,A::BaseOperator) = print(io,"{", unicode_to_latex(A.name), "}", latexindstr(A.inds), BaseOpType_latex[Int(A.t)])
printlatex(io::IO,A::BaseOperator) = _printlatex(io, unalias(A))
printlatex(io::IO,A::δ) = print(io, "\\delta", latexindstr(A.iA,A.iB))
printlatex(io::IO,A::Param) = print(io, unicode_to_latex(A.name), latexindstr(A.inds), A.state=='c' ? "^{*}" : "")
printlatex(io::IO,A::BaseOpProduct) = printlatex(io,A.v)
printlatex(io::IO,A::ExpVal) = (print(io, "\\langle "); printlatex(io, A.ops); print(io," \\rangle"))
printlatex(io::IO,A::Corr)   = (print(io, "\\langle "); printlatex(io, A.ops); print(io," \\rangle_{c}"))

function printlatex(io::IO,A::QuTerm)
    if A.nsuminds > 0
        print(io,"\\sum",latexindstr(sumindex.(1:A.nsuminds)))
    end
    printlatex(io,A.δs)
    printlatex(io,A.params)
    printlatex(io,A.expvals)
    printlatex(io,A.corrs)
    printlatex(io,A.bares)
end

function printlatex(io::IO,A::QuExpr)
    if isempty(A)
        print(io,0)
    else
        # convert to tuples so sorting ignores the numbers if terms are different (which they are)
        for (ii,(t,s)) in enumerate(sort!(Tuple.(collect(A.terms))))
            if isempty(t)
                printlatex(io,s)
            else
                ls = s==one(s) ? "" : (s==-one(s) ? "-" : latex(s))
                if ii > 1 && (ls=="" || (ls[1] ∉ ('-','+')))
                    print(io," + ")
                end
                print(io,ls)
                printlatex(io,t)
            end
        end
    end
end


# Constructs a state-space representation of an ARIMA model given
# vectors of AR and MA coefficients and a number of differences.
# This implementation is based on Section 8.3 in Brockwell and Davis 2002,
# "Introduction to Time Series and Forecasting," 2nd ed.
function arima_statespace{T, I<:Integer}(ar::Vector{T}, d::I, ma::Vector{T},
		sigma::T)
	p = length(ar)
	q = length(ma)
	r = max(p, q + 1)

	F = [zeros(r - 1) diagm(ones(r - 1)); [zeros(max(0, r - p)), reverse(ar)]']
	V = diagm([zeros(r - 1), sigma])
	G = [zeros(max(0, r - q - 1)), reverse(ma), 1]'
	W = zeros(1, 1)
	# differencing
	if d > 0
		A = [zeros(d - 1), 1]
		B = [zeros(d - 1) diagm(ones(d - 1));
			 T[(-1.0)^(d + 1 - i) * binomial(d, d-i) for i in 0:(d - 1)]']
		# redefine matrices to incorporate differencing into state
		F = [F zeros(r, d); A*G B]
		V = [V zeros(r, d); zeros(d, r + d)]
		G = [G B[end, :]]
		x0 = zeros(r + d)
		P0 = diagm(ones(r + d) * 1e7)
	else
		x0 = zeros()
		P0 = diagm(ones(r) * 1e7)
	end
	StateSpaceModel(F, V, G, W, x0, P0)
end

function arima{T, I<:Integer}(y::Vector{T}, p::I, d::I, q::I)
	build(par::Vector{T}) = arima_statespace(par[1:p], par[p+1], par[p+2:end])
	par0 = zeros(p + d + q)
	fit(y, build, par0)
end


"""
    ARTSCENE.jl

Description:
    All of the visual filter functions for the ARTSCENE algorithm.
"""

using Distributed
using SharedArrays

"""
    color_to_gray(image::RealArray)

ARTSCENE Stage 1: Color-to-gray image transformation.
"""
function color_to_gray(image::RealArray)
    # Treat the image as a column-major array, cast to grayscale
    dim, n_row, n_column = size(image)
    return [sum(image[:,i,j])/3 for i=1:n_row, j=1:n_column]
end # color_to_gray(image::RealArray)

"""
    surround_kernel(i::Integer, j::Integer, p::Integer, q::Integer, scale::Integer)

Surround kernel S function for ARTSCENE Stage 2
"""
function surround_kernel(i::Integer, j::Integer, p::Integer, q::Integer, scale::Integer)
    return 1/(2*pi*scale^2)*MathConstants.e^(-((i-p)^2 + (j-q)^2)/(2*scale^2))
end # surround_kernel(i::Integer, j::Integer, p::Integer, q::Integer, scale::Integer)

"""
    ddt_x(x::RealArray, image::RealArray, sigma_s::RealArray, distributed::Bool)

Time rate of change of LGN network (ARTSCENE Stage 2).
"""
function ddt_x(x::RealArray, image::RealArray, sigma_s::RealArray, distributed::Bool)
    n_row, n_column = size(x)
    n_g = length(sigma_s)
    kernel_r = 5

    # dx = zeros(n_row, n_column, 4)
    # for g = 1:n_g
    if distributed
        dx = SharedArray{Float64, 3}((n_row, n_column, n_g))
        @sync @distributed for g = 1:n_g
        for i = 1:n_row
        for j = 1:n_column
            # Compute the surround kernel
            kernel_h = max(1, i-kernel_r):min(n_row, i + kernel_r)
            kernel_w = max(1, j-kernel_r):min(n_row, j + kernel_r)
            S_ijg_I = sum([surround_kernel(i, j, p, q, sigma_s[g])*image[p, q]
                        for p in kernel_h, q in kernel_w])
            # Compute the enhanced contrast
            dx[i,j,g] = - x[i,j,g] + (1 - x[i,j,g])*image[i,j] - (1 + x[i,j,g])*S_ijg_I
        end
        end
        end
    end
    return dx
end # ddt_x(x::RealArray, image::RealArray, sigma_s::RealArray, distributed::Bool)

"""
    contrast_normalization(image::RealArray ; distributed::Bool=true)

ARTSCENE Stage 2: Constrast normalization.
"""
function contrast_normalization(image::RealArray ; distributed::Bool=true)
    # All scale parameters
    sigma_s = [1, 4, 8, 12]
    n_g = length(sigma_s)

    # Number if iterations to settle on contrast result
    n_iterations = 4
    # Get the shape of the image
    n_row, n_column = size(image)
    x = zeros(n_row, n_column, n_g)

    for g = 1:n_g
        x[:,:, g] = deepcopy(image)
    end

    for i = 1:n_iterations
        x += ddt_x(x, image, sigma_s, distributed)
    end

    return x
end # contrast_normalization(image::RealArray ; distributed::Bool=true)

"""
    oriented_kernel(i::Integer, j::Integer, p::Integer, q::Integer, k::Integer, sigma_h::Real, sigma_v::Real ; sign::String="plus")

Oriented, elongated, spatially offset kernel G for ARTSCENE Stage 3.
"""
function oriented_kernel(i::Integer, j::Integer, p::Integer, q::Integer, k::Integer, sigma_h::Real, sigma_v::Real ; sign::String="plus")
    m = sin(pi*k/4)
    n = cos(pi*k/4)

    if sign == "plus"
        G = (1/(2*pi*sigma_h*sigma_v)*
            MathConstants.e^(-0.5*((((p-i+m)*n-(q-j+n)*m)/sigma_h)^2
                                  +(((p-i+m)*m+(q-j+n)*n)/sigma_v)^2)))
    elseif sign == "minus"
        G = (1/(2*pi*sigma_h*sigma_v)*
            MathConstants.e^(-0.5*((((p-i-m)*n-(q-j-n)*m)/sigma_h)^2
                                  +(((p-i-m)*m+(q-j-n)*n)/sigma_v)^2)))
    else
        throw("Incorrect sign option for oriented kernel function")
    end

    return G
end # oriented_kernel(i::Integer, j::Integer, p::Integer, q::Integer, k::Integer, sigma_h::Real, sigma_v::Real ; sign::String="plus")

"""
    ddt_y(y::RealArray, X_plus::RealArray, X_minus::RealArray, alpha::Real, distributed::Bool)

Shunting equation for ARTSCENE Stage 3.
"""
function ddt_y(y::RealArray, X_plus::RealArray, X_minus::RealArray, alpha::Real, distributed::Bool)
    # n_row, n_column = size(x) # TODO: SOURCE OF WRONGNESS
    n_row, n_column = size(y)
    n_k = 4
    sigma_v = [0.25, 1, 2, 3]
    sigma_h = [0.75, 3, 6, 9]
    n_g = length(sigma_v)
    kernel_r = 5

    # dy = zeros(n_row, n_column, n_k, n_g)
    # for k = 1:n_k
    if distributed
        dy = SharedArray{Float64, 4}((n_row, n_column, n_k, n_g))
        @sync @distributed for k = 1:n_k
        for g = 1:n_g
        for i = 1:n_row
        for j = 1:n_column
            # Compute the surround kernel
            kernel_h = max(1, i-kernel_r):min(n_row, i + kernel_r)
            kernel_w = max(1, j-kernel_r):min(n_row, j + kernel_r)
            Gp = [oriented_kernel(i, j, p, q, k-1, sigma_h[g], sigma_v[g], sign="plus")
                for p in kernel_h, q in kernel_w]
            Gm = [oriented_kernel(i, j, p, q, k-1, sigma_h[g], sigma_v[g], sign="minus")
                for p in kernel_h, q in kernel_w]
            dy[i,j,g,k] = (-alpha*y[i,j,g,k]
                        + (1-y[i,j,g,k])*sum(X_plus[kernel_h, kernel_w, g].*Gp
                                            + X_minus[kernel_h, kernel_w, g].*Gm)
                        - (1+y[i,j,g,k])*sum(X_plus[kernel_h, kernel_w, g].*Gm
                                            + X_minus[kernel_h, kernel_w, g].*Gp))
        end
        end
        end
        end
    end
    return dy
end # ddt_y(y::RealArray, X_plus::RealArray, X_minus::RealArray, alpha::Real, distributed::Bool)

"""
    contrast_sensitive_oriented_filtering(image::RealArray, x::RealArray ; distributed::Bool=true)

ARTSCENE Stage 3: Contrast-sensitive oriented filtering.
"""
function contrast_sensitive_oriented_filtering(image::RealArray, x::RealArray ; distributed::Bool=true)
    # Get the size of the field
    n_row, n_column = size(x)

    # Parameters
    n_g = 4             # Number of scales
    n_k = 4             # Number of orientations
    alpha = 1           # Passive decay rate
    n_iterations = 4    # Number if iterations to settle on contrast result

    # Compute the LGN ON-cell and OFF-cell output signals
    X_plus = [max(0, x[i,j,g]) for i=1:n_row, j=1:n_column, g=1:n_g]
    X_minus = [max(0, -x[i,j,g]) for i=1:n_row, j=1:n_column, g=1:n_g]

    # Get the shape of the image
    n_row, n_column = size(x)
    y = zeros(n_row, n_column, n_g, n_k)
    for k = 1:n_k
        y[:,:,:,k] = deepcopy(x)
    end

    for _ = 1:n_iterations
        y += ddt_y(y, X_plus, X_minus, alpha, distributed)
    end

    return y
end # contrast_sensitive_oriented_filtering(image::RealArray, x::RealArray ; distributed::Bool=true)

"""
    contrast_insensitive_oriented_filtering(y::RealArray)

ARTSCENE Stage 4: Contrast-insensitive oriented filtering.
"""
function contrast_insensitive_oriented_filtering(y::RealArray)
    n_row, n_column, n_g, n_k = size(y)

    # Compute the LGN ON-cell and OFF-cell output signals
    Y_plus = [max(0, y[i,j,g,k]) for i=1:n_row, j=1:n_column, g=1:n_g, k=1:n_k]
    Y_minus = [max(0, -y[i,j,g,k]) for i=1:n_row, j=1:n_column, g=1:n_g, k=1:n_k]

    return Y_plus + Y_minus
end # contrast_insensitive_oriented_filtering(y::RealArray)

"""
    competition_kernel(l::Integer, k::Integer ; sign::String="plus")

Competition kernel for ARTSCENE: Stage 5.
"""
function competition_kernel(l::Integer, k::Integer ; sign::String="plus")

    if sign == "plus"
        g = ( 1/(0.5*sqrt(2*pi))*MathConstants.e^(-0.5*((l-k)/0.5)^2) )
    elseif sign == "minus"
        g = ( 1/(sqrt(2*pi))*MathConstants.e^(-0.5*(l-k)^2) )
    else
        throw("Incorrect sign option for oriented kernel function")
    end

    return g
end # competition_kernel(l::Integer, k::Integer ; sign::String="plus")

"""
    ddt_z(z::RealArray ; distributed=true)

Time rate of change for ARTSCENE: Stage 5.
"""
function ddt_z(z::RealArray ; distributed::Bool=true)
    n_row, n_column, n_g, n_k = size(z)
    kernel_r = 5

    # dz = zeros(n_row, n_column, n_k, n_g)
    # for k = 1:n_k
    if distributed
        dz = SharedArray{Float64, 4}((n_row, n_column, n_k, n_g))
        @sync @distributed for k = 1:n_k
        for g = 1:n_g
        for i = 1:n_row
        for j = 1:n_column
            zgp = sum([z[i,j,g,l]*competition_kernel(l,k,sign="plus") for l = 1:n_g])
            zgm = sum([z[i,j,g,l]*competition_kernel(l,k,sign="minus") for l = 1:n_g])
            dz[i,j,g,k] = (- z[i,j,g,k]
                        + (1 - z[i,j,g,k]*zgp)
                        - (1 + z[i,j,g,k]*zgm))
        end
        end
        end
        end
    end

    return dz
end # ddt_z(z::RealArray ; distributed=true)

"""
    orientation_competition(z::RealArray)

ARTSCENE Stage 5: Orientation competition at the same position.
"""
function orientation_competition(z::RealArray)

    # Parameters
    n_iterations = 4    # Number if iterations to settle on contrast result

    # Get the shape of the image
    # n_row, n_column, n_g, n_k = size(z)
    # Z = zeros(n_row, n_column, n_g, n_k)
    # for k = 1:n_k
    #     Z[:,:,:,k] = deepcopy(z)
    # end

    for _ = 1:n_iterations
        z += ddt_z(z)
    end

    return z
end

"""
    patch_orientation_color(z::RealArray, image::RealArray)

ARTSCENE Stage 6: Create patch feature vectors.
"""
function patch_orientation_color(z::RealArray, image::RealArray)
    n_i, n_j, n_g, n_k = size(z)
    patch_i = 4
    patch_j = 4
    n_colors = 3
    n_patches = patch_i * patch_j
    size_i = n_i / patch_i
    size_j = n_j / patch_j
    size_patch = size_i * size_j
    O = zeros(patch_i, patch_j, n_g, n_k)
    C = zeros(patch_i, patch_j, n_colors)
    for p_i = 1:patch_i
        for p_j = 1:patch_j
            # Get the correct range objects for the grid
            i_range = Integer(floor(size_i*(p_i-1)+1)):Integer(floor(size_i*p_i))
            j_range = Integer(floor(size_j*(p_j-1)+1)):Integer(floor(size_j*p_j))
            # Compute the color averages
            for c = 1:n_colors
                C[p_i,p_j,c] = 1/size_patch*sum(image[c, i_range, j_range])
            end
            # Compute the orientation averages
            for k = 1:4
                for g = 1:4
                    O[p_i,p_j,k,g] = 1/size_patch * sum(z[i_range, j_range, k, g])
                end
            end
        end
    end
    return O, C
end # patch_orientation_color(z::RealArray, image::RealArray)

"""
    artscene_filter(raw_image::Array{T, 3} ;  distributed::Bool=true) where {T<:Real}

Process the full artscene filter toolchain on an image.
"""
function artscene_filter(raw_image::Array{T, 3} ;  distributed::Bool=true) where {T<:Real}

    # Get the number of workers
    n_processes = nprocs()
    n_workers = nworkers()
    @debug "Processes: $n_processes, Workers: $n_workers"

    # Random image
    image_size = size(raw_image)
    image_type =  typeof(raw_image)
    @debug "Original: Size = $image_size, Type = $image_type"

    # Stage 1: Grayscale
    image = color_to_gray(raw_image)
    image_size = size(image)
    image_type = typeof(image)
    @debug "Stage 1 Complete: Grayscale: Size = $image_size, Type = $image_type"

    # Stage 2: Contrast normalization
    x = contrast_normalization(image, distributed=true)
    image_size = size(x)
    image_type = typeof(x)
    @debug "Stage 2 Complete: Contrast: Size = $image_size, Type = $image_type"

    # Stage 3: Contrast-sensitive oriented filtering
    y = contrast_sensitive_oriented_filtering(image, x)
    image_size = size(y)
    image_type = typeof(y)
    @debug "Stage 3 Complete: Sensitive Oriented: Size = $image_size, Type = $image_type"

    # Stage 4: Contrast-insensitive oriented filtering
    z = contrast_insensitive_oriented_filtering(y)
    image_size = size(z)
    image_type = typeof(z)
    @debug "Stage 4 Complete: Insensitive Oriented: Size = $image_size, Type = $image_type"

    # Stage 5: Orientation competition
    z = orientation_competition(z)
    image_size = size(z)
    image_type = typeof(z)
    @debug "Stage 5 Complete: Orientation Competition: Size = $image_size, Type = $image_type"

    # *Stage 6*: Compute patch vectors (orientation and color)
    O, C = patch_orientation_color(z, raw_image)
    @debug "Stage 6 Complete"

    return O, C
end # artscene_filter(raw_image::Array{T, 3} ;  distributed::Bool=true) where {T<:Real}


### A Pluto.jl notebook ###
# v0.12.18

using Markdown
using InteractiveUtils

# ╔═╡ be6b935c-0918-11eb-227a-f7ffbb70c325
using IterTools, PGField, Primes, SparseArrays

# ╔═╡ 2f42420e-7bf8-11eb-2529-e3a58c1ee4b4
md"""
# HYPER
Functions to generate design.
"""

# ╔═╡ 9268d880-09a9-11eb-2d79-af4d3eaa85f8
function gen2factors(m)
	(m % 2 == 0) || throw("m must be a multiple of 2, got m=$m.")

	r = m-1
	starter = vcat(
		[[GFElem{r}(0),PGInf{r}()]],
		[[GFElem{r}(i),GFElem{r}(-i)] for i in 1:(r-1)÷2]
	)

	return ([Ai.+GFElem{r}(g) for Ai in starter] for g in 0:r-1)
end

# ╔═╡ c2fde6ba-0993-11eb-066b-6b54080e7bc1
function gen3factors(m)
	(m % 6 == 0) || throw("m must be a multiple of 6, got m=$m.")
	isprime(m-1) || throw("m-1 must be prime, got m=$m.")

	r = m-1
	F = [GFElem{r}(i) for i in 0:r-1]
	PG = [F; PGInf{r}()]

	O = (Set∘map)(PG) do i
		orbit = iterated(x -> -(inv(x)*(one(x)+x)), i)
		Set(collect(Iterators.take(orbit,r+1)))
	end
	ω = F[findfirst(i -> order(i) == r-1, F)]

	return ([(λ.*Ai).+g for Ai in O] for g in F, λ in ω.^(1:(r-1)÷2))
end

# ╔═╡ 770a9a5c-09a0-11eb-1043-852c0087f749
function graphdesign(n,m,q; reorder=true)
	# Generate factors
	if q == 1
		r = m-1
		factors = vcat([[GFElem{r}(i)] for i in 0:r-1], [[PGInf{r}()]])
	elseif q == 2
		factors = Iterators.flatten(gen2factors(m))
	elseif q == 3
		factors = Iterators.flatten(gen3factors(m))
	else
		throw("q must be 1, 2 or 3, got q=$q.")
	end

	# Form pools and (optionally) reorder to group individuals in identical pools together
	pools = collect(Iterators.take(Iterators.cycle(factors),n))
	if reorder
		nfactors = binomial(m,q)
		pools = collect(Iterators.flatten(pools[i:nfactors:end] for i in 1:nfactors))
	end

	# Convert to pool indices and create sparse array
	poolidx = map(pool -> [num isa GFElem ? num.value+1 : m for num in pool], pools)
	return sparse(collect(Iterators.flatten(poolidx)),repeat(1:n,inner=q),1,m,n) |> collect
end

# ╔═╡ Cell order:
# ╟─2f42420e-7bf8-11eb-2529-e3a58c1ee4b4
# ╠═be6b935c-0918-11eb-227a-f7ffbb70c325
# ╠═9268d880-09a9-11eb-2d79-af4d3eaa85f8
# ╠═c2fde6ba-0993-11eb-066b-6b54080e7bc1
# ╠═770a9a5c-09a0-11eb-1043-852c0087f749


function backprop(net::Network,input::Vector{Float64},label::Vector{Float64})::Vector{Vector{Float64}}
    feedforward!(net,input)

    # δ^L = ∇C_a ⊙ σ'(z^L)
    output_error = net.∇C_a(net.neurons[end],label).*net.act_p(net.z[end])
    errors = [output_error]

    for i=1:(length(net.layers)-1)
        # δ^l=((w^l+1)T δ^l+1) ⊙ σ′(z^l)
        next_error = (net.weights[length(net.layers)-i+1]*errors[1]).*net.act_p(net.z[length(net.layers)-i])
        prepend!(errors,next_error)
    end

    return errors, net.neurons
end


"""
    function exponentiate(A, t::Number, x; kwargs...)
    function exponentiate(A, t::Number, x, algorithm)

Compute ``y = exp(t*A) x``, where `A` is a general linear map, i.e. a `AbstractMatrix` or
just a general function or callable object and `x` is of any Julia type with vector like
behavior.

### Arguments:

The linear map `A` can be an `AbstractMatrix` (dense or sparse) or a general function or
callable object that implements the action of the linear map on a vector. If `A` is an
`AbstractMatrix`, `x` is expected to be an `AbstractVector`, otherwise `x` can be of any
type that behaves as a vector and supports the required methods (see KrylovKit docs).

The time parameter `t` can be real or complex, and it is better to choose `t` e.g. imaginary
and `A` hermitian, then to absorb the imaginary unit in an antihermitian `A`. For the
former, the Lanczos scheme is used to built a Krylov subspace, in which an approximation to
the exponential action of the linear map is obtained. The argument `x` can be of any type
and should be in the domain of `A`.

### Return values:

The return value is always of the form `y, info = exponentiate(...)` with

  - `y`: the result of the computation, i.e. `y = exp(t*A)*x`

  - `info`: an object of type [`ConvergenceInfo`], which has the following fields

      + `info.converged::Int`: 0 or 1 if the solution `y` was approximated up to the
        requested tolerance `tol`.
      + `info.residual::Nothing`: value `nothing`, there is no concept of a residual in
        this case
      + `info.normres::Real`: a (rough) estimate of the error between the approximate and
        exact solution
      + `info.numops::Int`: number of times the linear map was applied, i.e. number of times
        `f` was called, or a vector was multiplied with `A`
      + `info.numiter::Int`: number of times the Krylov subspace was restarted (see below)

!!! warning "Check for convergence"

    By default (i.e. if `verbosity = 0`, see below), no warning is printed if the solution
    was not found with the requested precision, so be sure to check `info.converged == 1`.

### Keyword arguments:

Keyword arguments and their default values are given by:

  - `verbosity::Int = 0`: verbosity level, i.e. 0 (no messages), 1 (single message
    at the end), 2 (information after every iteration), 3 (information per Krylov step)
  - `krylovdim = 30`: the maximum dimension of the Krylov subspace that will be constructed.
    Note that the dimension of the vector space is not known or checked, e.g. `x₀` should
    not necessarily support the `Base.length` function. If you know the actual problem
    dimension is smaller than the default value, it is useful to reduce the value of
    `krylovdim`, though in principle this should be detected.
  - `tol = 1e-12`: the requested accuracy per unit time, i.e. if you want a certain
    precision `ϵ` on the final result, set `tol = ϵ/abs(t)`. If you work in e.g. single
    precision (`Float32`), you should definitely change the default value.
  - `maxiter::Int = 100`: the number of times the Krylov subspace can be rebuilt; see below
    for further details on the algorithms.
  - `issymmetric`: if the linear map is symmetric, only meaningful if `T<:Real`
  - `ishermitian`: if the linear map is hermitian
    The default value for the last two depends on the method. If an `AbstractMatrix` is
    used, `issymmetric` and `ishermitian` are checked for that matrix, otherwise the default
    values are `issymmetric = false` and `ishermitian = T <: Real && issymmetric`.
  - `eager::Bool = false`: if true, eagerly try to compute the result after every expansion
    of the Krylov subspace to test for convergence, otherwise wait until the Krylov subspace
    as dimension `krylovdim`. This can result in a faster return, for example if the total
    time for the evolution is quite small, but also has some overhead, as more computations
    are performed after every expansion step.

### Algorithm

This is actually a simple wrapper over more general method [`expintegrator`](@ref) for
for integrating a linear non-homogeneous ODE.
"""
function exponentiate end

exponentiate(A, t::Number, v; kwargs...) = expintegrator(A, t, v; kwargs...)
exponentiate(A, t::Number, v, alg::Union{Lanczos,Arnoldi}) = expintegrator(A, t, (v,), alg)


function glaubersweeps(nsweeps::Int64,model::IsingModel,cbevery::Int64=1,cb::Function=defaultcb)
  ndata::Int64 = length(model.data)
  for i in 1:nsweeps # loop over sweeps
    if i%cbevery == 0 && !(cb == nothing)
      cb(model)
    end
    randinds = rand(model.rng,1:ndata,ndata)
    for ind in randinds # loop entries in the data
      ediff::Float64 = localediff(ind,model)
      if rand(model.rng) < 1/(1+exp(ediff/model.T))
        flipbit!(ind,model)
      end
    end
  end
end


using FrankWolfe
using LinearAlgebra
using Test
using SparseArrays

@testset "Testing Blended Pairwise Conditional Gradients" begin
    f(x) = norm(x)^2
    function grad!(storage, x)
        @. storage = 2x
        return nothing
    end
    lmo_prob = FrankWolfe.ProbabilitySimplexOracle(4)
    x0 = FrankWolfe.compute_extreme_point(lmo_prob, zeros(10))
    res_bpcg = FrankWolfe.blended_pairwise_conditional_gradient(
        f,
        grad!,
        lmo_prob,
        x0,
        max_iteration=6000,
        line_search=FrankWolfe.Adaptive(),
        verbose=false,
    )
    res_afw = FrankWolfe.away_frank_wolfe(
        f,
        grad!,
        lmo_prob,
        x0,
        max_iteration=6000,
        line_search=FrankWolfe.Adaptive(),
        print_iter=100,
        verbose=false,
    )
    @test res_afw[3] ≈ res_bpcg[3]
    @test norm(res_afw[1] - res_bpcg[1]) ≈ 0 atol=1e-6
    res_bpcg2 = FrankWolfe.blended_pairwise_conditional_gradient(
        f,
        grad!,
        lmo_prob,
        x0,
        max_iteration=6000,
        line_search=FrankWolfe.Adaptive(),
        verbose=false,
        lazy=true
    )
    @test res_bpcg2[3] ≈ res_bpcg[3] atol=1e-5
end


module Linearisers

using ForwardDiff

export get_jacobian

"""
    get_jacobian(func, args...)

Compute the jacobian of given function `func`.
`args...` should be arguments of `func`.

# Equations
jacobian = [[∂f1/∂x1 ... ∂f1/∂xn];
                ⋮           ⋮
            [∂fm/∂x1 ... ∂fm/∂xn]]

# Examples
```julia
using SuccessiveConvexProgrammings
using LinearAlgebra

function my_func_array(x, u, t, k)
    A = [1 2;
         3 4;
         5 6]
    B = Matrix(I, 3, 3)
    return A*x + B*u
end

x = [1, 2]
u = [3, 4, 5]
t = [6, 7]
k = [8, 9]
jacob = get_jacobian(my_func_array, x, u, t, k)
# result
3×9 Array{Float64,2}:
 1.0  2.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0
 3.0  4.0  0.0  1.0  0.0  0.0  0.0  0.0  0.0
 5.0  6.0  0.0  0.0  1.0  0.0  0.0  0.0  0.0
```
"""
function get_jacobian(func, args...)
    jacob = zeros(length(func(args...)), 0)  # dummy calculation
    args_dim = 0
    for (i, arg) in enumerate(args)
        args_tmp = [args...]
        args_tmp[1], args_tmp[i] = args_tmp[i], args_tmp[1]
        altered_func = function(args2...)
            args_tmp2 = [args2...]
            args_tmp2[1], args_tmp2[i] = args_tmp2[i], args_tmp2[1]
            return func(args_tmp2...)
        end
        new_func = function(arg)
            return altered_func(arg, args_tmp[2:end]...)
        end
        jacob = cat(jacob, ForwardDiff.jacobian(new_func, arg), dims=2)
    end
    return jacob
end

end


##Specific problems from Kloden&Platen 1997
abstract type Problem end

struct Problem31 <: Problem
    avec::Vector{Float64}
end

function mgen(prob::Problem31, x0::AbstractVector)
    return length(x0)
end

function a!(prob::Problem31)
    coeff = -prob.avec.^2
    return function (t, x, f)
        f .= coeff.*(x.*(1-x.^2))
    end
end

function b!(prob::Problem31)
    return function (t, x, g)
        g .= diagm(prob.avec.*(1-x.^2))
    end
end

function bbp!(prob::Problem31)
    coeff = -2*prob.avec.^2
    return function (t, x, bbp)
        bbp .= coeff.*(x-x.^3)
    end
end

#Diffeq has a different convention from v4.0
function diffeq_a!(prob::Problem31)
    coeff = -prob.avec.^2
    return function (t, x, p, f)
        f .= coeff.*(x.*(1-x.^2))
    end
end

function diffeq_b!(prob::Problem31)
    return function (t, x, p, g)
        g .= diagm(prob.avec.*(1-x.^2))
    end
end

function solution(prob::Problem31, x0, t, Wvec)
    avec = prob.avec
    return [tanh.(avec.*W+atanh.(x0)) for W in Wvec]
end


#=

Code to simulate the model in Globalization and Synchronization of Innovation
Cycles by Kiminori Matsuyama, Laura Gardini and Iryna Sushko

See <http://www.centreformacroeconomics.ac.uk/Discussion-Papers/2015/CFMDP2015-27-Paper.pdf>

=#

using PyPlot

"""
If we expand the implicit function for h_j(n_k) then we find that
it is a quadratic. We know that h_j(n_k) > 0 so we can get its
value by using the quadratic form
"""

function h_j(j::Int64, nk, s1::Float64, s2::Float64, theta::Float64, delta::Float64, rho::Float64)
    # Find out who's h we are evaluating
    if j == 1
        sj = s1
        sk = s2
    else
        sj = s2
        sk = s1
    end

    # Coefficients on the quadratic a x^2 + b x + c = 0
    a = 1.0
    b = ((rho + 1/rho)*nk - sj - sk)
    c = (nk*nk - (sj*nk)/rho - sk*rho*nk)

    # Positive solution of quadratic form
    root = (-b + sqrt(b*b - 4*a*c))/(2*a)

    return root
end

"Determine whether (n1, n2) is in the set DLL"
function DLL(n1::Float64, n2::Float64, s1_rho::Float64, s2_rho::Float64,
        s1::Float64, s2::Float64, theta::Float64, delta::Float64, rho::Float64)
    return (n1 <= s1_rho) && (n2 <= s2_rho)
end

"Determine whether (n1, n2) is in the set DHH"
function DHH(n1::Float64, n2::Float64, s1_rho::Float64, s2_rho::Float64,
        s1::Float64, s2::Float64, theta::Float64, delta::Float64, rho::Float64)
    return (n1 >= h_j(1, n2, s1, s2, theta, delta, rho)) && (n2 >= h_j(2, n1, s1, s2, theta, delta, rho))
end

"Determine whether (n1, n2) is in the set DHL"
function DHL(n1::Float64, n2::Float64, s1_rho::Float64, s2_rho::Float64,
        s1::Float64, s2::Float64, theta::Float64, delta::Float64, rho::Float64)
    return (n1 >= s1_rho) && (n2 <= h_j(2, n1, s1, s2, theta, delta, rho))
end

"Determine whether (n1, n2) is in the set DLH"
function DLH(n1::Float64, n2::Float64, s1_rho::Float64, s2_rho::Float64,
        s1::Float64, s2::Float64, theta::Float64, delta::Float64, rho::Float64)
    return (n1 <= h_j(1, n2, s1, s2, theta, delta, rho)) && (n2 >= s2_rho)
end

"""
Takes a current value for (n_{1, t}, n_{2, t}) and returns the
values (n_{1, t+1}, n_{2, t+1}) according to the law of motion.
"""
function one_step(n1::Float64, n2::Float64, s1_rho::Float64, s2_rho::Float64,
        s1::Float64, s2::Float64, theta::Float64, delta::Float64, rho::Float64)
    # Depending on where we are, evaluate the right branch
    if DLL(n1, n2, s1_rho, s2_rho, s1, s2, theta, delta, rho)
        n1_tp1 = delta*(theta*s1_rho + (1-theta)*n1)
        n2_tp1 = delta*(theta*s2_rho + (1-theta)*n2)
    elseif DHH(n1, n2, s1_rho, s2_rho, s1, s2, theta, delta, rho)
        n1_tp1 = delta*n1
        n2_tp1 = delta*n2
    elseif DHL(n1, n2, s1_rho, s2_rho, s1, s2, theta, delta, rho)
        n1_tp1 = delta*n1
        n2_tp1 = delta*(theta*h_j(2, n1, s1, s2, theta, delta, rho) + (1-theta)*n2)
    elseif DLH(n1, n2, s1_rho, s2_rho, s1, s2, theta, delta, rho)
        n1_tp1 = delta*(theta*h_j(1, n2, s1, s2, theta, delta, rho) + (1-theta)*n1)
        n2_tp1 = delta*n2
    end

    return n1_tp1, n2_tp1
end

"""
Given an initial condition, continues to yield new values of
n1 and n2
"""
function new_n1n2(n1_0::Float64, n2_0::Float64, s1_rho::Float64, s2_rho::Float64,
        s1::Float64, s2::Float64, theta::Float64, delta::Float64, rho::Float64)
    n1_tp1, n2_tp1 = one_step(n1_0, n2_0, s1_rho, s2_rho, s1, s2, theta, delta, rho)
    return n1_tp1, n2_tp1
end

"""
Takes initial values and iterates forward to see whether
the histories eventually end up in sync.

If countries are symmetric then as soon as the two countries have the
same measure of firms then they will by synchronized -- However, if
they are not symmetric then it is possible they have the same measure
of firms but are not yet synchronized. To address this, we check whether
firms stay synchronized for `npers` periods with Euclidean norm

Parameters
----------
n1_0 : scalar(Float)
    Initial normalized measure of firms in country one
n2_0 : scalar(Float)
    Initial normalized measure of firms in country two
maxiter : scalar(Int)
    Maximum number of periods to simulate
npers : scalar(Int)
    Number of periods we would like the countries to have the
    same measure for

Returns
-------
synchronized : scalar(Bool)
    Did they two economies end up synchronized
pers_2_sync : scalar(Int)
    The number of periods required until they synchronized
"""
function pers_till_sync(n1_0::Float64, n2_0::Float64, s1_rho::Float64, s2_rho::Float64,
        s1::Float64, s2::Float64, theta::Float64, delta::Float64, rho::Float64,maxiter::Int64, npers)

    # Initialize the status of synchronization
    synchronized = false
    pers_2_sync = maxiter
    iters = 0

    nsync = 0

    while (~synchronized) && (iters < maxiter)
        # Increment the number of iterations and get next values
        iters += 1

        n1_t, n2_t = new_n1n2(n1_0, n2_0, s1_rho, s2_rho, s1, s2, theta, delta, rho)

        # Check whether same in this period
        if abs(n1_t - n2_t) < 1e-8
            nsync += 1
        # If not, then reset the nsync counter
        else
            nsync = 0
        end

        # If we have been in sync for npers then stop and countries
        # became synchronized nsync periods ago
        if nsync > npers
            synchronized = true
            pers_2_sync = iters - nsync
        end
        n1_0, n2_0 = n1_t, n2_t
    end
    return synchronized, pers_2_sync
end

function create_attraction_basis(s1_rho, s2_rho, s1, s2, theta, delta, rho,
        maxiter, npers, npts)
    # Create unit range with npts
    synchronized, pers_2_sync = false, 0
    unit_range = linspace(0.0, 1.0, npts)

    # Allocate space to store time to sync
    time_2_sync = Array{Float64}(npts, npts)
    # Iterate over initial conditions
    for (i, n1_0) in enumerate(unit_range)
        for (j, n2_0) in enumerate(unit_range)
            #println((i,j))
            synchronized, pers_2_sync = pers_till_sync(n1_0, n2_0, s1_rho, s2_rho,
                                                        s1, s2, theta, delta, rho,
                                                        maxiter, npers)
            time_2_sync[i, j] = pers_2_sync
        end
    end

    return time_2_sync
end

# == Now we define a class for the model == #

"""
The paper "Globalization and Synchronization of Innovation Cycles" presents
a two country model with endogenous innovation cycles. Combines elements
from Deneckere Judd (1985) and Helpman Krugman (1985) to allow for a
model with trade that has firms who can introduce new varieties into
the economy.

We focus on being able to determine whether two countries eventually
synchronize their innovation cycles. To do this, we only need a few
of the many parameters. In particular, we need the parameters listed
below

Parameters
----------
s1 : scalar(Float)
    Amount of total labor in country 1 relative to total worldwide labor
theta : scalar(Float)
    A measure of how mcuh more of the competitive variety is used in
    production of final goods
delta : scalar(Float)
    Percentage of firms that are not exogenously destroyed every period
rho : scalar(Float)
    Measure of how expensive it is to trade between countries
"""
type MSGSync
    s1::Float64
    s2::Float64
    s1_rho::Float64
    s2_rho::Float64
    theta::Float64
    delta::Float64
    rho::Float64
end

function MSGSync(s1::Float64=0.5, theta::Float64=2.5,
                 delta::Float64=0.7, rho::Float64=0.2)
    # Store other cutoffs and parameters we use
    s2 = 1 - s1
    s1_rho = min((s1 - rho*s2) / (1 - rho),1)
    s2_rho = 1 - s1_rho

    model=MSGSync(s1,s2,s1_rho,s2_rho,theta,delta,rho)

    return model
end

function unpack_params(model::MSGSync)
    return model.s1, model.s2, model.theta, model.delta, model.rho, model.s1_rho, model.s2_rho
end

"""
Simulates the values of (n1, n2) for T periods

Parameters
----------
n1_0 : scalar(Float)
Initial normalized measure of firms in country one
n2_0 : scalar(Float)
Initial normalized measure of firms in country two
T : scalar(Int)Number of periods to simulate

Returns
-------
n1 : Array{Float64}(ndim=1)
A history of normalized measures of firms in country one
n2 : Array{Float64}(ndim=1)
A history of normalized measures of firms in country two
"""
function simulate_n(model::MSGSync, n1_0::Float64, n2_0::Float64, T::Int64)
    # Unpack parameters
    s1, s2, theta, delta, rho, s1_rho, s2_rho = unpack_params(model)

    # Allocate space
    n1 = Array{Float64}(T)
    n2 = Array{Float64}(T)

    # Simulate for T periods
    for t in 1:T
        # Get next values
        n1[t], n2[t] = n1_0, n2_0
        n1_0, n2_0 = new_n1n2(n1_0, n2_0, s1_rho, s2_rho, s1, s2, theta, delta, rho)
    end

    return n1, n2
end

"""
Takes initial values and iterates forward to see whether
the histories eventually end up in sync.

If countries are symmetric then as soon as the two countries have the
same measure of firms then they will by synchronized -- However, if
they are not symmetric then it is possible they have the same measure
of firms but are not yet synchronized. To address this, we check whether
firms stay synchronized for `npers` periods with Euclidean norm

Parameters
----------
n1_0 : scalar(Float)
    Initial normalized measure of firms in country one
n2_0 : scalar(Float)
    Initial normalized measure of firms in country two
maxiter : scalar(Int)
    Maximum number of periods to simulate
npers : scalar(Int)
    Number of periods we would like the countries to have the
    same measure for

Returns
-------
synchronized : scalar(Bool)
    Did they two economies end up synchronized
pers_2_sync : scalar(Int)
    The number of periods required until they synchronized
"""
function pers_till_sync(model::MSGSync, n1_0::Float64, n2_0::Float64,
                        maxiter=500, npers=3)
    # Unpack parameters
    s1, s2, theta, delta, rho, s1_rho, s2_rho = unpack_params(model)

    return pers_till_sync(n1_0, n2_0, s1_rho, s2_rho, s1, s2, theta, delta, rho, maxiter, npers)
end

"""
Creates an attraction basis for values of n on [0, 1] X [0, 1] with npts in each dimension
"""
function create_attraction_basis(model::MSGSync; maxiter=250, npers=3, npts=50)
    # Unpack parameters
    s1, s2, theta, delta, rho, s1_rho, s2_rho = unpack_params(model)

    ab = create_attraction_basis(s1_rho, s2_rho, s1, s2, theta, delta,
                                  rho, maxiter, npers, npts)
    return ab
end


"""
A MappedGrid consists of a grid and a map. Each grid point of the mapped grid
is the map of the corresponding point of the underlying grid.
"""
struct MappedGrid{G,M,T,N} <: AbstractGrid{T,N}
	supergrid	::	G
	map			::	M

	MappedGrid{G,M,T,N}(supergrid::AbstractGrid{T,N}, map) where {G,M,T,N} = new(supergrid, map)
end

const MappedGrid1d{G,M,T<:Number,N} = MappedGrid{G,M,T,N}

MappedGrid(grid::AbstractGrid{T,N}, map::AbstractMap) where {T,N} =
	MappedGrid{typeof(grid),typeof(map),T,N}(grid, map)

name(grid::MappedGrid) = "Mapped grid"

supergrid(g::MappedGrid) = g.supergrid

mapping(g::MappedGrid) = g.map

mapped_grid(grid::AbstractGrid, map::AbstractMap) = MappedGrid(grid, map)

# avoid multiple mappings
mapped_grid(g::MappedGrid, map::AbstractMap) = MappedGrid(supergrid(g), map∘mapping(g))

# Convenience function, similar to apply_map for Dictionary's
apply_map(grid::AbstractGrid, map::AbstractMap) = mapped_grid(grid, map)

for op in (:length, :size, :eachindex, :indextype, :isperiodic)
	@eval $op(g::MappedGrid) = $op(supergrid(g))
end

for op in (:minimum, :maximum, :support)
	@eval $op(g::MappedGrid1d) = mapping(g)*$op(supergrid(g))
end

resize(g::MappedGrid, n::Int) = apply_map(resize(supergrid(g), n), mapping(g))

unsafe_getindex(g::MappedGrid, idx) = applymap(g.map, g.supergrid[idx])

function rescale(g::AbstractGrid1d, a, b)
	m = interval_map(endpoints(support(g))..., a, b)
	mapped_grid(g, m)
end

# Preserve tensor product structure
function rescale(g::ProductGrid, a::SVector{N}, b::SVector{N}) where {N}
	scaled_grids = [ rescale(element(g, i), a[i], b[i]) for i in 1:N]
	ProductGrid(scaled_grids...)
end