text:
import CartesianGrids: curl!, Laplacian
import LinearAlgebra: Diagonal, norm
import Base: show

export VortexModel, streamfunction, streamfunction!, vorticity, vorticity!, vortexvelocities!, _computeregularizationmatrix, setvortexstrengths!, setvortexpositions!, getvortexpositions, setvortices!, pushvortices!, setU∞, impulse, addedmass, solve, solve!

# TODO: mention frame of reference for computeimpulse
# TODO: consider no deepcopy for new vortices in the methods and use deepcopy in the scripts instead
# TODO: redo PotentialFlow.jl solution in example 6 with uniform flow instead of moving plate

# TODO: check computef̃limit for multiple bodies (f₀). Maybe Γ₀ needs to be a vector.
# TODO: check if systems can be reduced to basic one and other one for all the relevant cases.
# TODO: set vortex strengths in script? Then warn in impulse that it does not regularize
# TODO: case where steady regularized bodies are next to unregularized bodies with circulation.
# TODO: change vortexvelocities to accept w and don't set vortices to 0 anymore.
# TODO: add parameter to VortexModel constructor to make model unsteady even if no vortices are provided.
# TODO: remove w from solve! arguments and calculate it in the function body. Then also remove it from solve

"""
$(TYPEDEF)

Defines a grid-based vortex model with `Nb` bodies and `Ne` edges that are regularized. If `isshedding` is `true`, the strengths of the last `Ne` vortices in `vortices` can be computed in order to regularized the `Ne` edges.

# Fields

$(TYPEDFIELDS)

# Examples
(under construction)
"""
mutable struct VortexModel{Nb,Ne,TS<:Union{AbstractPotentialFlowSystem,Laplacian},TU<:Nodes,TE<:Edges,TF<:ScalarData,TX<:VectorData}
    """g: The grid on which the vortex model is defined."""
    g::PhysicalGrid
    """bodies: Bodies in the vortex model."""
    bodies::Vector{PotentialFlowBody}
    """vortices: Point vortices in the vortex model."""
    vortices::StructVector{Vortex}
    """U∞: Uniform flow in the vortex model."""
    U∞::Tuple{Float64,Float64}
    """system: Potential flow system that has to be solved with an `AbstractPotentialFlowRHS` and an `AbstractPotentialFlowSolution` to compute the potential flow that governs the vortex model.
    """
    system::TS

    """Internal fields"""
    _nodedata::TU
    _edgedata::TE
    _bodyvectordata::TX
    _ψ::TU
    _f::TF
    _w::TU
    _ψb::TF
end

"""
$(TYPEDSIGNATURES)

Constructs a vortex model using the given function.
"""
function VortexModel(g::PhysicalGrid, bodies::Vector{PotentialFlowBody}, vortices::StructVector{Vortex}, U∞::Tuple{Float64,Float64})

    vortices = deepcopy(vortices)

    e_idx = getregularizededges(bodies)

    Nb = length(bodies)
    Ne = length(e_idx)

    if isempty(bodies)
        sizef = 0
    else
        sizef = sum(length.(bodies))
    end

    # Initialize data structures for internal use
    _nodedata = Nodes(Dual,size(g))
    _edgedata = Edges(Primal,size(g))
    _bodyvectordata = VectorData(sizef)
    _ψ = Nodes(Dual,size(g))
    _f = ScalarData(sizef)
    _w = Nodes(Dual,size(g))
    _ψb = ScalarData(sizef)

    L = plan_laplacian(size(_nodedata),with_inverse=true)
    if Nb == 0
        system = L
    else
        regop = Regularize(VectorData(collect(bodies)), cellsize(g), I0=origin(g), ddftype = CartesianGrids.Yang3, issymmetric=true)
        Rmat,_ = RegularizationMatrix(regop, _f, _nodedata)
        Emat = InterpolationMatrix(regop, _nodedata, _f)

        one_vec = [ScalarData(sizef) for i in 1:Nb]
        for i in 1:Nb
            one_vec[i][getrange(bodies,i)] .= 1.0
        end

        if Ne == 0 # System without regularized edges. Enforce circulation constraints.
            system = ConstrainedIBPoisson(L, Rmat, Emat, one_vec, one_vec)
        else # System with regularized edges. Enforce edge constraints.
            e_vec = [ScalarData(sizef) for i in 1:Ne]
            k = 0
            for i in 1:Nb
                for id in getregularizededges(bodies,i)
                    k += 1
                    e_vec[k][id] = 1.0
                end
            end
            if isempty(vortices) # With regularized edges, but no vortices. This is a steady case.
                system = SteadyRegularizedIBPoisson(L, Rmat, Emat, one_vec, e_vec)
            else # With regularized edges and vortices. This is an unsteady case with vortex shedding.
                vidx_vec = Vector{Vector{Int64}}()
                vidx = collect(1:Ne)
                for i in 1:Nb
                    vidx_i = [pop!(vidx) for k in 1:length(bodies[i].edges)]
                    push!(vidx_vec, vidx_i)
                end
                system = UnsteadyRegularizedIBPoisson(L, Rmat, Emat, one_vec, e_vec, vidx_vec)
            end
        end
    end

    VortexModel{Nb,Ne,typeof(system),typeof(_ψ),typeof(_edgedata),typeof(_f),typeof(_bodyvectordata)}(g, bodies, vortices, U∞, system, _nodedata, _edgedata, _bodyvectordata, _ψ, _f, _w, _ψb)
end

function VortexModel(g::PhysicalGrid; bodies::Vector{PotentialFlowBody}=Vector{PotentialFlowBody}(), vortices::Vector{Vortex}=Vector{Vortex}(), U∞::Tuple{Float64,Float64}=(0.0,0.0))
    return VortexModel(g, bodies, StructVector(vortices), U∞)
end

"""
$(SIGNATURES)

Replaces the vortices of the vortex model `vm` by a copy of `newvortices`.
"""
function setvortices!(vm::VortexModel{Nb,Ne}, newvortices::Vector{Vortex}) where {Nb,Ne}

    vm.vortices = StructArray(deepcopy(newvortices))
end

function setvortices!(vm::VortexModel{Nb,Ne}, newvortices::StructArray{Vortex}) where {Nb,Ne}

    vm.vortices = newvortices
end

"""
$(SIGNATURES)

Adds `newvortices` to the existing vortices in the vortex model `vm`.
"""
function pushvortices!(vm::VortexModel{Nb,Ne}, newvortices...) where {Nb,Ne}

    push!(vm.vortices, newvortices...)
end

"""
$(SIGNATURES)

Sets the positions of the vortices in the vortex model `vm` to the provided `X`.
"""
function setvortexpositions!(vm::VortexModel{Nb,Ne}, X::VectorData{Nv}) where {Nb,Ne,Nv}

    setpositions!(vm.vortices, X.u, X.v)
end

"""
$(SIGNATURES)

Sets the strenghts of the vortices in the vortex model `vm` to the provided `Γ`. Indices can be provided in `idx` to specify the indices of the vortices whose strengths have to be set, in which case the length of `Γ` must be the same as the number of indices.
"""
function setvortexstrengths!(vm::VortexModel{Nb,Ne}, Γ::TΓ, idx=nothing) where {Nb,Ne,TΓ<:AbstractVector}

    setstrengths!(vm.vortices, Γ, idx)
end

"""
$(SIGNATURES)

Returns the positions of the vortices in the vortex model `vm`.
"""
function getvortexpositions(vm::VortexModel{Nb,Ne}) where {Nb,Ne}

    return getpositions(vm.vortices)
end

"""
$(SIGNATURES)

Sets the uniform flow of the vortex model `vm` to `U∞`.
"""
function setU∞(vm::VortexModel, U∞)

    vm.U∞ = U∞
end

function _updatesystemd_vec!(vm::VortexModel{Nb,Ne,UnsteadyRegularizedIBPoisson{Nb,Ne,TU,TF}}, idx::Vector{Int}) where {Nb,Ne,TU,TF}

    v = view(vm.vortices,idx)

    H = Regularize(getpositions(v), cellsize(vm.g), I0=origin(vm.g), ddftype=CartesianGrids.M4prime, issymmetric=true)

    Γ = deepcopy(getstrengths(v))
    for i in 1:Ne
        Γ .= 0
        Γ[i] = 1.0
        H(vm.system.d_vec[i],Γ)
    end
end

"""
$(SIGNATURES)

Computes and stores in `Ẋ_vortices` the flow velocity, associated with the discrete vector potential field `ψ`, as `VectorData` at the locations of the vortices stored in the vortex model `vm`.
"""
function vortexvelocities!(Ẋ_vortices::VectorData, vm::VortexModel{Nb,Ne,TS,TU}, ψ::TU) where {Nb,Ne,TS,TU}

    H = Regularize(getpositions(vm.vortices), cellsize(vm.g), I0=origin(vm.g), ddftype=CartesianGrids.M4prime, issymmetric=true)

    # Velocity is the curl of the vector potential
    # The discrete curl operator requires dividing by the cellsize to account for the grid spacing
    curl!(vm._edgedata, ψ)
    vm._edgedata ./= cellsize(vm.g)

    # For consistent interpolation, first interpolate the velocity to the nodes and use H to interpolate from the nodes to the vortices
    grid_interpolate!(vm._nodedata, vm._edgedata.u);
    H(Ẋ_vortices.u, vm._nodedata)
    grid_interpolate!(vm._nodedata, vm._edgedata.v);
    H(Ẋ_vortices.v, vm._nodedata)

    return Ẋ_vortices
end

"""
$(SIGNATURES)

Returns the flow velocity as `VectorData` at the locations of the vortices stored in the vortex model `vm`, accounting for bodies in `vm`. If the `vm` has `Ne` regularized edges and vortices, the strengths of the last `Ne` vortices will be computed and set in `vm` and the circulation of the shedded vortices will be subtracted from the bound circulation of each body.
"""
function vortexvelocities!(vm::VortexModel{Nb,Ne}) where {Nb,Ne}

    if Nb == 0
        sol = PoissonSolution(vm._ψ)
        solve!(sol, vm)
    else
        sol = ConstrainedIBPoissonSolution(vm._ψ, vm._f, zeros(Float64,Nb), zeros(Float64,Ne))
        solve!(sol, vm)
    end

    Ẋ_vortices = VectorData(length(vm.vortices))

    for k in 1:Ne
        vm.vortices.Γ[end-Ne+k] = sol.δΓ_vec[k]
    end

    if  Ne > 0
        subtractcirculation!(vm.bodies, sol.δΓ_vec)
    end

    return vortexvelocities!(Ẋ_vortices, vm, sol.ψ)
end

"""
$(SIGNATURES)

Computes the vorticity field `w` associated with the vortices stored in the vortex model `vm` on the physical grid. If the extra boolean keyword `onlybulk` is set to `true`, the function ignores the vorticity from the last `Ne` vortices in `vm`.
"""
function vorticity!(wphysical::TU, vm::VortexModel{Nb,Ne,TS,TU,TE,TF}; onlybulk::Bool=false) where {Nb,Ne,TS,TU,TE,TF}

    if onlybulk
        vortices = view(vm.vortices,1:length(vm.vortices)-Ne)
    else
        vortices = vm.vortices
    end

    H = Regularize(getpositions(vortices), cellsize(vm.g), I0=origin(vm.g), ddftype=CartesianGrids.M4prime, issymmetric=true)

    if isempty(vortices)
        wphysical .= 0.0
        return wphysical
    end

    Γ = getstrengths(vortices)
    H(wphysical,Γ)
    wphysical ./= cellsize(vm.g)^2 # Divide by the Δx² to ensure that ∫wdA = ΣΓ

    return wphysical
end

"""
$(SIGNATURES)

Computes the vorticity field associated with the vortices stored in the vortex model `vm` on the physical grid.
"""
function vorticity(vm::VortexModel{Nb,Ne,TS,TU,TE,TF}) where {Nb,Ne,TS,TU,TE,TF}

    w = TU()
    vorticity!(w,vm)

    return w
end

"""
$(SIGNATURES)

Solves the saddle point system associated with the vortex model `vm` and stores the solution in `sol`. If the vortex model contains bodies with regularized edges and a number of vortices which is greater than or equal to the number of regularized edges, the returned solution contains the computed strengths of the last N vortices in `vm`, with N the number of regularized edges.
"""
function solve!(sol::PoissonSolution, vm::VortexModel{0,0,TS,TU}) where {Nb,TU,TS<:Laplacian}

    vorticity!(vm._w, vm)
    rhs = PoissonRHS(vm._w)
    _scaletoindexspace!(rhs, cellsize(vm.g))
    vm._nodedata .= .-rhs.w
    ldiv!(sol.ψ, vm.system, vm._nodedata)
    _scaletophysicalspace!(sol, cellsize(vm.g))

    _addψ∞!(sol.ψ, vm) # Add the uniform flow to the approximation to the continuous stream function field

    return sol
end

function solve!(sol::ConstrainedIBPoissonSolution, vm::VortexModel{Nb,0,ConstrainedIBPoisson{Nb,TU,TF}}) where {Nb,TU,TF}

    vorticity!(vm._w, vm)
    _streamfunctionbcs!(vm._ψb, vm)
    Γb = deepcopy(getΓ.(vm.bodies)) # deepcopy because _scaletoindexspace mutates

    rhs = ConstrainedIBPoissonRHS(vm._w, vm._ψb, Γb)
    _scaletoindexspace!(rhs, cellsize(vm.g))
    ldiv!(sol, vm.system, rhs)
    _scaletophysicalspace!(sol, cellsize(vm.g))

    _addψ∞!(sol.ψ, vm) # Add the uniform flow to the approximation to the continuous stream function field

    return sol
end

function solve!(sol::ConstrainedIBPoissonSolution, vm::VortexModel{Nb,Ne,SteadyRegularizedIBPoisson{Nb,Ne,TU,TF}}) where {Nb,Ne,TU,TF}

    vorticity!(vm._w, vm)
    _streamfunctionbcs!(vm._ψb, vm)

    f̃lim_vec = Vector{f̃Limit}()
    for i in 1:Nb
        append!(f̃lim_vec, _computef̃limit.(vm.bodies[i].σ, Ref(vm.bodies[i].points), sum(vm.system.f₀_vec[i])))
    end

    rhs = ConstrainedIBPoissonRHS(vm._w, vm._ψb, f̃lim_vec)
    _scaletoindexspace!(rhs, cellsize(vm.g))
    ldiv!(sol, vm.system, rhs)
    _scaletophysicalspace!(sol, cellsize(vm.g))

    _addψ∞!(sol.ψ, vm) # Add the uniform flow to the approximation to the continuous stream function field

    return sol
end

function solve!(sol::ConstrainedIBPoissonSolution, vm::VortexModel{Nb,Ne,UnsteadyRegularizedIBPoisson{Nb,Ne,TU,TF}}) where {Nb,Ne,TU,TF}

    vorticity!(vm._w, vm, onlybulk=true)
    _streamfunctionbcs!(vm._ψb, vm)
    _updatesystemd_vec!(vm,collect(length(vm.vortices)-(Ne-1):length(vm.vortices))) # Update the system.d_vec to agree with the latest vortex positions

    f̃lim_vec = Vector{f̃Limits}()
    for i in 1:Nb
        append!(f̃lim_vec, _computef̃range.(vm.bodies[i].σ, Ref(vm.bodies[i].points), sum(vm.system.f₀_vec[i])))
    end
    Γw = -deepcopy(getΓ.(vm.bodies)) # deepcopy because _scaletoindexspace mutates

    rhs = UnsteadyRegularizedIBPoissonRHS(vm._w, vm._ψb, f̃lim_vec, Γw)
    _scaletoindexspace!(rhs, cellsize(vm.g))
    ldiv!(sol, vm.system, rhs)
    _scaletophysicalspace!(sol, cellsize(vm.g))

    _addψ∞!(sol.ψ, vm) # Add the uniform flow to the approximation to the continuous stream function field

    return sol
end

"""
$(SIGNATURES)

Solves the saddle point system associated with the vortex model `vm` and returns the solution. If the vortex model contains bodies with regularized edges and a number of vortices which is greater than or equal to the number of regularized edges, the returned solution contains the computed strengths of the last N vortices in `vm`, with N the number of regularized edges.
"""
function solve(vm::VortexModel{0,0,TS,TU}) where {Nb,Ne,TS<:Laplacian,TU}

    sol = PoissonSolution(TU())
    solve!(sol, vm)

    return sol
end

function solve(vm::VortexModel{Nb,Ne,TS,TU,TE,TF}) where {Nb,Ne,TS<:AbstractPotentialFlowSystem,TU,TE,TF}

    sol = ConstrainedIBPoissonSolution(TU(), TF(), zeros(Float64,Nb), zeros(Float64,Ne))
    solve!(sol, vm)

    return sol
end

"""
$(SIGNATURES)

Computes the stream function field `ψ` on the physical grid for the potential flow associated with the current state of the vortex model `vm`.
"""
function streamfunction!(ψ::TU, vm::VortexModel{0,0,TS,TU}) where {Nb,Ne,TS<:Laplacian,TU}

    sol = PoissonSolution(ψ)
    solve!(sol, vm)

    return sol.ψ
end

function streamfunction!(ψ::TU, vm::VortexModel{Nb,Ne,TS,TU,TE,TF}) where {Nb,Ne,TS<:AbstractPotentialFlowSystem,TU,TE,TF}

    sol = ConstrainedIBPoissonSolution(ψ, vm._f, zeros(Float64,Nb), zeros(Float64,Ne))
    solve!(sol, vm)

    return sol.ψ
end

"""
$(SIGNATURES)

Computes and returns the stream function field on the physical grid for the potential flow associated with the current state of the vortex model `vm`.
"""
function streamfunction(vm::VortexModel{Nb,Ne,TS,TU,TE,TF}) where {Nb,Ne,TS,TU,TE,TF}

    ψ = TU()
    streamfunction!(ψ, vm)

    return ψ
end

"""
$(SIGNATURES)

Computes the impulse associated with the vorticity `wphysical`, the bound vortex sheet strength `fphysical`, and the velocities of the discrete points of the bodies in `vm`.
"""
function impulse(vm::VortexModel{Nb,Ne}, wphysical::Nodes{Dual}, fphysical::ScalarData) where {Nb,Ne}

    xg, yg = coordinates(wphysical, vm.g)
    Δx = cellsize(vm.g)
    _bodypointsvelocity!(vm._bodyvectordata, vm.bodies)

    # Formula 61 (see formula 6.16 in book)
    p = [Δx^2*sum(wphysical.*yg'),Δx^2*sum(-wphysical.*xg)]

    for i in 1:Nb
        r = getrange(vm.bodies,i)
        p += _impulsesurfaceintegral(vm.bodies[i].points, fphysical[r], vm._bodyvectordata.u[r], vm._bodyvectordata.v[r])
    end

    return p[1], p[2]
end

"""
$(SIGNATURES)

Computes the impulse associated with the body motions in the vortex model `vm` and the vortex sheet strength in `sol`. Note that newly inserted vortices should have their strengths set prior to calling this function to be included in the impulse calculation.
"""
function impulse(sol::TS, vm::VortexModel) where {TS<:AbstractIBPoissonSolution}
    # Compute vorticity field again, because now it can contain the vorticity of newly shedded vortices
    vorticity!(vm._nodedata, vm)
    impulse(vm, vm._nodedata, sol.f)
end

"""
$(SIGNATURES)

Computes the impulse associated with the current state vortices and bodies in the vortex model `vm`. Note that newly inserted vortices should have their strengths set prior to calling this function to be included in the impulse calculation.
"""
function impulse(vm::VortexModel)
    sol = solve(vm)
    impulse(sol, vm)
end

"""
$(SIGNATURES)

Computes the translational coefficients of the added mass matrix of the bodies in the vortex model `vm`.
"""
function addedmass(vm::VortexModel{Nb,Ne}) where {Nb,Ne}

    # Deepcopy bodies because we will change the velocities one by one
    origbodies = deepcopy(vm.bodies)

    # For now only translational
    M = zeros(Nb*2,Nb*2)

    for movingbodyindex in 1:Nb
        for dir in 1:2
            for i in 1:Nb
                setU(vm.bodies[i], (0.0,0.0))
            end
            if dir == 1
                setU(vm.bodies[movingbodyindex], (1.0,0.0))
            else
                setU(vm.bodies[movingbodyindex], (0.0,1.0))
            end
            _bodypointsvelocity!(vm._bodyvectordata, vm.bodies)
            sol = solve(vm)
            for i in 1:Nb
                r = getrange(vm.bodies,i)
                M[(i-1)*2+1:(i-1)*2+2,(movingbodyindex-1)*2+dir] .= _impulsesurfaceintegral(vm.bodies[i].points, sol.f[r], vm._bodyvectordata.u[r], vm._bodyvectordata.v[r])
            end
        end
    end
    vm.bodies = origbodies
    return M
end

function _impulsesurfaceintegral(body::Body{N,RigidBodyTools.ClosedBody}, f, u, v) where {N}
    nx,ny = normalmid(body)
    Δs = dlengthmid(body)
    return _crossproductsurfaceintegral(body,(f./Δs + nx.*v - ny.*u).*Δs)
end

function _impulsesurfaceintegral(body::Body{N,RigidBodyTools.OpenBody}, f, u, v) where {N}
    return _crossproductsurfaceintegral(body,f)
end

function _crossproductsurfaceintegral(body::Body, z)
    rx,ry = collect(body)
    return [ry'*z, -rx'*z]
end

function _bodypointsvelocity!(v::VectorData, bodies)
    bodypointsvelocity!(v.u, bodies, 1)
    bodypointsvelocity!(v.v, bodies, 2)
    return v
end

function _addψ∞!(ψ, vm::VortexModel)

    xg,yg = coordinates(ψ,vm.g)
    ψ .+= vm.U∞[1].*yg' .- vm.U∞[2].*xg

    return ψ
end

function _streamfunctionbcs!(ψb::ScalarData, vm::VortexModel)

    _bodypointsvelocity!(vm._bodyvectordata, vm.bodies) # Convert U into VectorData corresponding to the body points

    # The discrete streamfunction field is constrained to a prescribed streamfunction on the body that describes the body motion. The body presence in the uniform flow is taken into account by subtracting its value from the body motion (i.e. a body motion in the -U∞ direction) and adding the uniform flow at the end of the solve routine.
    ψb .= -vm.U∞[1]*(collect(vm.bodies)[2]) .+ vm.U∞[2]*(collect(vm.bodies)[1]);
    ψb .+= vm._bodyvectordata.u .* collect(vm.bodies)[2] .- vm._bodyvectordata.v .* collect(vm.bodies)[1]

    return ψb
end

function _streamfunctionbcs!(ψb::ScalarData, vm::VortexModel{0})
    return
end

function show(io::IO, model::VortexModel{Nb,Ne,isshedding}) where {Nb,Ne,isshedding}
    NX = model.g.N[1]
    NY = model.g.N[2]
    N = length(model._f)
    Nv = length(model.vortices)

    println(io, "Vortex model on a grid of size $NX x $NY and $N immersed points with $((Nv == 1) ? "1 vortex" : "$Nv vortices"), $((Nb == 1) ? "1 body" : "$Nb bodies"), and $((Ne == 1) ? "1 regularized edge" : "$Ne regularized edges")")
end

@recipe function f(h::VortexModel)
    xlims := h.g.xlim[1]
    ylims := h.g.xlim[2]
    legend := :false
    grid := :false
    aspect_ratio := 1

    @series begin
        seriestype := :scatter
        markersize := 3
        markerstrokewidth := 0
        marker_z := h.vortices.Γ
        h.vortices.x, h.vortices.y
    end

    for b in h.bodies
        @series begin
            b.points
        end
    end
end


text:
using Distributed
@everywhere using WhiskerTracking, Gtk.ShortNames, Distributed, Knet

println("Whisker Tracking Loaded")

Knet.cuallocator()=false
myhandles=make_gui();

println("GUI Made")

WhiskerTracking.add_callbacks(myhandles.b,myhandles)

if !isinteractive()
  c=Condition()
  signal_connect(myhandles.b["win"],:destroy) do c_widget
    notify(c)
  end
  wait(c)
end

ext: jl

max_stars_repo_name: paulmthompson/WhiskerTracking-docker

max_issues_repo_name: paulmthompson/WhiskerTracking-docker

max_forks_repo_name: paulmthompson/WhiskerTracking-docker


text:
using Mux
using Test
using Lazy
using HTTP

@test Mux.notfound()(d())[:status] == 404

# Test basic server
@app test = (
  Mux.defaults,
  page("/",respond("
Hello World!
")),
  page("/about", respond("
Boo!
")),
  page("/user/:user", req -> "
Hello, $(req[:params][:user])!
"),
  Mux.notfound())
serve(test)
@test String(HTTP.get("http://localhost:8000").body) ==
            "
Hello World!
"
@test String(HTTP.get("http://localhost:8000/about").body) ==
            "
Boo!
"
@test String(HTTP.get("http://localhost:8000/user/julia").body) ==
            "
Hello, julia!
"

# Issue #68
@test Mux.fileheaders("foo.css")["Content-Type"] == "text/css"
@test Mux.fileheaders("foo.html")["Content-Type"] == "text/html"
@test Mux.fileheaders("foo.js")["Content-Type"] == "application/javascript"


text:
module BitCircuits

export Operation, Constant, Variable, evaluate, equal

typealias BitString Uint64
typealias VarIdx Uint64
typealias Context Uint64

abstract Node

# ----------
# Operations
# ----------

immutable Operation <: Node
    sym::String
    left::Node
    right::Node
    cache::BitString
end

function Operation(func::Function, left::Node, right::Node)
    sym = string(func)
    cache = func(left.cache, right.cache)
    return Operation(sym, left, right, cache)
end

# ---------
# Constants
# ---------

immutable Constant <: Node
    val::Bool
    sym::String
    cache::BitString
    function Constant(val::Bool)
        if val
            cache = 2 ^ 64 - 1
            sym = "TRUE"
        else
            cache = 0
            sym = "FALSE"
        end
        return new(val, sym, cache |> uint64)
    end
end

# ---------
# Variables
# ---------

immutable Variable <: Node
    idx::VarIdx
    sym::String
    cache::BitString
    function Variable(idx::Uint64)
        if idx > 5
            error("idx must be  in [0, 5]")
        end
        cache = uint64(0)
        for ctxval = uint64(0):uint64(63)
            varmask = 2 ^ idx |> uint64
            if (ctxval & varmask) > 0
                cache = cache | (uint64(2) ^ ctxval)
            end
        end
        return new(idx, "X$(idx)", cache)
    end
end

Variable(idx::Int64) = Variable(uint64(idx))

# ---------------
# Tree evaluation
# ---------------

function evaluate(root::Node, ctxval::Context)
    mask = 2 ^ ctxval |> uint64
    return root.cache & mask > 0
end

evaluate(root::Node, ctxval::Uint8) = evaluate(root, ctxval |> uint64)

# ---------------
# Tree comparison
# ---------------

function equal(left::Node, right::Node)
    return left.cache == right.cache
end

end


text:
@test LogNum(5.5, 0) == LogNum()
@test LogNum(-Inf, 0) == LogNum()
@test LogNum(-Inf, 43) == LogNum()

@test iszero(LogNum())
@test iszero(LogNum(0))
@test !iszero(LogNum(1))
@test !iszero(LogNum(-1))

@test isfinite(LogNum(0))
@test isfinite(LogNum(1))
@test isfinite(LogNum(-1))
@test !isfinite(LogNum(Inf))
@test !isfinite(LogNum(-Inf))
@test isinf(LogNum(Inf))
@test !isinf(LogNum(0))

@test isnan(LogNum(NaN))
@test isnan(LogNum(NaN, 1))
@test isnan(LogNum(NaN, -1))
@test !isnan(LogNum(1.))
@test isnan(LogNum(NaN) + LogNum(1))
@test isnan(LogNum(NaN) - LogNum(1))
@test isnan(LogNum(NaN) * LogNum(1))
@test isnan(LogNum(NaN) / LogNum(1))
@test isnan(LogNum(NaN) ^ LogNum(1))

@test convert(Float64, LogNum(1.)) == 1.

for x = -10.:10.
    @test LogNum(x) == LogNum(x) == x
    @test LogNum(x) ≥ LogNum(x) ≥ x
    @test LogNum(x) ≤ LogNum(x) ≤ x
    @test LogNum(x) < LogNum(x + 1)
    @test LogNum(x + 1) > LogNum(x)
end

@test sign(LogNum(1)) == 1
@test sign(LogNum(-42.1)) == -1
@test sign(LogNum(0.)) == 0

@test LogNum(2) > LogNum(1)
@test LogNum(-1) < LogNum(1)
@test LogNum(3.4) ≤ LogNum(3.4)

@test LogNum(-10) ≈ LogNum(-20) + LogNum(10)
@test LogNum(-200) ≈ LogNum(20) * LogNum(-10)
@test LogNum(-30) ≈ LogNum(-10) - LogNum(20)
@test LogNum(-2) ≈ LogNum(20) / LogNum(-10)

@test LogNum(5) - 1 ≈ LogNum(4)
@test 1 - LogNum(5) ≈ LogNum(-4)
@test LogNum(5) + 1 ≈ LogNum(6)
@test 1 + LogNum(5) ≈ LogNum(6)
@test 2 * LogNum(5) ≈ LogNum(10)
@test LogNum(5) * 2 ≈ LogNum(10)
@test LogNum(4) / 2 ≈ LogNum(2)
@test 4 / LogNum(2) ≈ LogNum(2)
@test LogNum(2) > 1
@test 2 > LogNum(1)

@test convert(Float64, LogNum(5)) ≈ 5

@test LogNum(3) * LogNum(-5) ≈ -15

@test exp(LogNum(3)) ≈ exp(3)
@test exp(LogNum(-3)) ≈ exp(-3)

for x = 0.:10.
    @test log(LogNum(x)) ≈ log(x)
end

for x = -10.:10.
    @test convert(Float64, LogNum(x)) ≈ x
    @test abs(LogNum(x)) ≈ abs(x)
    @test exp(LogNum(x)) ≈ exp(x)
    @test sign(LogNum(x)) == sign(x)
end

for x = 0.:5., y = -5.:5.
    if x ≠ 0 || y ≠ 0
        @test isapprox(LogNum(x) ^ LogNum(y), x ^ y; atol=1e-12)
        @test isapprox(LogNum(x) ^ y, x ^ y; atol=1e-12)
        @test isapprox(x ^ LogNum(y), x ^ y; atol=1e-12)
    end
end

for x = -10.:10., y = -10.:10.
    @test LogNum(x) + LogNum(y) ≈ x + y
    @test LogNum(x) - LogNum(y) ≈ x - y
    @test LogNum(x) * LogNum(y) ≈ x * y
    if x ≠ 0 || y ≠ 0
        @test LogNum(x) / LogNum(y) ≈ x / y
    end
end

@test LogNum(5.)^LogNum(2.) ≈ 5.^2.
@test LogNum(5.)^LogNum(-2.) ≈ 5.^-2.
@test LogNum(5.)^LogNum(0) ≈ 1
@test LogNum(0)^LogNum(1) ≈ 0
@test LogNum(0) ^ LogNum(0) == 1
@test LogNum(4) ^ 2 ≈ 4^2


text:
export Device, DeviceParams, deviceID, params, dependencies, dependency, hasDependency, init, checkDependencies

abstract type VirtualDevice <: Device end

"""
Abstract type for all device parameters

Every device must implement a parameter struct allowing for
automatic instantiation from the configuration file.
"""
abstract type DeviceParams end

"Retrieve the ID of a device."
deviceID(device::Device) = :deviceID in fieldnames(typeof(device)) ? device.deviceID : error("The device struct for `$(typeof(device))` must have a field `deviceID`.")

"Retrieve the parameters of a device."
params(device::Device) = :params in fieldnames(typeof(device)) ? device.params : error("The device struct for `$(typeof(device))` must have a field `params`.")

"Check whether the device is optional."
isOptional(device::Device) = :optional in fieldnames(typeof(device)) ? device.optional : error("The device struct for `$(typeof(device))` must have a field `optional`.")

"Check whether the device is present."
isPresent(device::Device) = :present in fieldnames(typeof(device)) ? device.present : error("The device struct for `$(typeof(device))` must have a field `present`.")

"Retrieve the dependencies of a device."
dependencies(device::Device) = :dependencies in fieldnames(typeof(device)) ? device.dependencies : error("The device struct for `$(typeof(device))` must have a field `dependencies`.")

"Retrieve all dependencies of a certain type."
dependencies(device::Device, type::DataType) = [dependency for dependency in values(dependencies(device)) if dependency isa type]

"Retrieve a single dependency of a certain type and error if there are more dependencies."
function dependency(device::Device, type::DataType)
  dependencies_ = dependencies(device, type)
  if length(dependencies_) > 1
    throw(ScannerConfigurationError("Retrieving a dependency of type `$type` for the device with the ID `$(deviceID(device))` "*
                                    "returned more than one item and can therefore not be retrieved unambiguously."))
  else
    return dependencies_[1]
  end
end

"Check whether the device has a dependency of the given `type`."
hasDependency(device::Device, type::DataType) = length(dependencies(device, type)) > 0

"Retrieve all expected dependencies of a device."
expectedDependencies(device::Device)::Vector{DataType} = vcat(neededDependencies(device), optionalDependencies(device))

"Check if a device is an expected dependency of another device."
function isExpectedDependency(device::Device, dependency::Device)
  for expectedDependency in expectedDependencies(device)
    if typeof(dependency) <: expectedDependency
      return true
    end
  end

  return false
end

@mustimplement init(device::Device)
@mustimplement neededDependencies(device::Device)::Vector{DataType}
@mustimplement optionalDependencies(device::Device)::Vector{DataType}

function checkDependencies(device::Device)
  # Check if all needed dependencies are assigned
  for neededDependency in neededDependencies(device)
    if !hasDependency(device, neededDependency)
      throw(ScannerConfigurationError("The device with ID `$(deviceID(device))` "*
                                      "needs a dependency of type $(neededDependency) "*
                                      "but it is not assigned."))
      return false
    end
  end

  # Check if superfluous dependencies are assigned
  for (dependencyID, dependency) in dependencies(device)
    if !isExpectedDependency(device, dependency)
      @warn "The device with ID `$(deviceID(device))` has a superfluous dependency "*
            "to a device with ID `$dependencyID`."
    end
  end

  return true
end


text:
# This file is auto-generated by AWSMetadata.jl
using AWS
using AWS.AWSServices: managedblockchain
using AWS.Compat
using AWS.UUIDs

"""
    create_member(client_request_token, invitation_id, member_configuration, network_id)
    create_member(client_request_token, invitation_id, member_configuration, network_id, params::Dict{String,<:Any})

Creates a member within a Managed Blockchain network. Applies only to Hyperledger Fabric.

# Arguments
- `client_request_token`: A unique, case-sensitive identifier that you provide to ensure
  the idempotency of the operation. An idempotent operation completes no more than one time.
  This identifier is required only if you make a service request directly using an HTTP
  client. It is generated automatically if you use an AWS SDK or the AWS CLI.
- `invitation_id`: The unique identifier of the invitation that is sent to the member to
  join the network.
- `member_configuration`: Member configuration parameters.
- `network_id`: The unique identifier of the network in which the member is created.

"""
function create_member(
    ClientRequestToken,
    InvitationId,
    MemberConfiguration,
    networkId;
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks/$(networkId)/members",
        Dict{String,Any}(
            "ClientRequestToken" => ClientRequestToken,
            "InvitationId" => InvitationId,
            "MemberConfiguration" => MemberConfiguration,
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function create_member(
    ClientRequestToken,
    InvitationId,
    MemberConfiguration,
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks/$(networkId)/members",
        Dict{String,Any}(
            mergewith(
                _merge,
                Dict{String,Any}(
                    "ClientRequestToken" => ClientRequestToken,
                    "InvitationId" => InvitationId,
                    "MemberConfiguration" => MemberConfiguration,
                ),
                params,
            ),
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    create_network(client_request_token, framework, framework_version, member_configuration, name, voting_policy)
    create_network(client_request_token, framework, framework_version, member_configuration, name, voting_policy, params::Dict{String,<:Any})

Creates a new blockchain network using Amazon Managed Blockchain. Applies only to
Hyperledger Fabric.

# Arguments
- `client_request_token`: A unique, case-sensitive identifier that you provide to ensure
  the idempotency of the operation. An idempotent operation completes no more than one time.
  This identifier is required only if you make a service request directly using an HTTP
  client. It is generated automatically if you use an AWS SDK or the AWS CLI.
- `framework`: The blockchain framework that the network uses.
- `framework_version`: The version of the blockchain framework that the network uses.
- `member_configuration`: Configuration properties for the first member within the network.
- `name`: The name of the network.
- `voting_policy`:  The voting rules used by the network to determine if a proposal is
  approved.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"Description"`: An optional description for the network.
- `"FrameworkConfiguration"`:  Configuration properties of the blockchain framework
  relevant to the network configuration.
- `"Tags"`: Tags to assign to the network. Each tag consists of a key and optional value.
  When specifying tags during creation, you can specify multiple key-value pairs in a single
  request, with an overall maximum of 50 tags added to each resource. For more information
  about tags, see Tagging Resources in the Amazon Managed Blockchain Ethereum Developer
  Guide, or Tagging Resources in the Amazon Managed Blockchain Hyperledger Fabric Developer
  Guide.
"""
function create_network(
    ClientRequestToken,
    Framework,
    FrameworkVersion,
    MemberConfiguration,
    Name,
    VotingPolicy;
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks",
        Dict{String,Any}(
            "ClientRequestToken" => ClientRequestToken,
            "Framework" => Framework,
            "FrameworkVersion" => FrameworkVersion,
            "MemberConfiguration" => MemberConfiguration,
            "Name" => Name,
            "VotingPolicy" => VotingPolicy,
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function create_network(
    ClientRequestToken,
    Framework,
    FrameworkVersion,
    MemberConfiguration,
    Name,
    VotingPolicy,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks",
        Dict{String,Any}(
            mergewith(
                _merge,
                Dict{String,Any}(
                    "ClientRequestToken" => ClientRequestToken,
                    "Framework" => Framework,
                    "FrameworkVersion" => FrameworkVersion,
                    "MemberConfiguration" => MemberConfiguration,
                    "Name" => Name,
                    "VotingPolicy" => VotingPolicy,
                ),
                params,
            ),
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    create_node(client_request_token, node_configuration, network_id)
    create_node(client_request_token, node_configuration, network_id, params::Dict{String,<:Any})

Creates a node on the specified blockchain network. Applies to Hyperledger Fabric and
Ethereum.

# Arguments
- `client_request_token`: A unique, case-sensitive identifier that you provide to ensure
  the idempotency of the operation. An idempotent operation completes no more than one time.
  This identifier is required only if you make a service request directly using an HTTP
  client. It is generated automatically if you use an AWS SDK or the AWS CLI.
- `node_configuration`: The properties of a node configuration.
- `network_id`: The unique identifier of the network for the node. Ethereum public networks
  have the following NetworkIds:    n-ethereum-mainnet     n-ethereum-rinkeby
  n-ethereum-ropsten

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"MemberId"`: The unique identifier of the member that owns this node. Applies only to
  Hyperledger Fabric.
- `"Tags"`: Tags to assign to the node. Each tag consists of a key and optional value. When
  specifying tags during creation, you can specify multiple key-value pairs in a single
  request, with an overall maximum of 50 tags added to each resource. For more information
  about tags, see Tagging Resources in the Amazon Managed Blockchain Ethereum Developer
  Guide, or Tagging Resources in the Amazon Managed Blockchain Hyperledger Fabric Developer
  Guide.
"""
function create_node(
    ClientRequestToken,
    NodeConfiguration,
    networkId;
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks/$(networkId)/nodes",
        Dict{String,Any}(
            "ClientRequestToken" => ClientRequestToken,
            "NodeConfiguration" => NodeConfiguration,
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function create_node(
    ClientRequestToken,
    NodeConfiguration,
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks/$(networkId)/nodes",
        Dict{String,Any}(
            mergewith(
                _merge,
                Dict{String,Any}(
                    "ClientRequestToken" => ClientRequestToken,
                    "NodeConfiguration" => NodeConfiguration,
                ),
                params,
            ),
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    create_proposal(actions, client_request_token, member_id, network_id)
    create_proposal(actions, client_request_token, member_id, network_id, params::Dict{String,<:Any})

Creates a proposal for a change to the network that other members of the network can vote
on, for example, a proposal to add a new member to the network. Any member can create a
proposal. Applies only to Hyperledger Fabric.

# Arguments
- `actions`: The type of actions proposed, such as inviting a member or removing a member.
  The types of Actions in a proposal are mutually exclusive. For example, a proposal with
  Invitations actions cannot also contain Removals actions.
- `client_request_token`: A unique, case-sensitive identifier that you provide to ensure
  the idempotency of the operation. An idempotent operation completes no more than one time.
  This identifier is required only if you make a service request directly using an HTTP
  client. It is generated automatically if you use an AWS SDK or the AWS CLI.
- `member_id`: The unique identifier of the member that is creating the proposal. This
  identifier is especially useful for identifying the member making the proposal when
  multiple members exist in a single AWS account.
- `network_id`:  The unique identifier of the network for which the proposal is made.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"Description"`: A description for the proposal that is visible to voting members, for
  example, \"Proposal to add Example Corp. as member.\"
- `"Tags"`: Tags to assign to the proposal. Each tag consists of a key and optional value.
  When specifying tags during creation, you can specify multiple key-value pairs in a single
  request, with an overall maximum of 50 tags added to each resource. If the proposal is for
  a network invitation, the invitation inherits the tags added to the proposal. For more
  information about tags, see Tagging Resources in the Amazon Managed Blockchain Ethereum
  Developer Guide, or Tagging Resources in the Amazon Managed Blockchain Hyperledger Fabric
  Developer Guide.
"""
function create_proposal(
    Actions,
    ClientRequestToken,
    MemberId,
    networkId;
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks/$(networkId)/proposals",
        Dict{String,Any}(
            "Actions" => Actions,
            "ClientRequestToken" => ClientRequestToken,
            "MemberId" => MemberId,
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function create_proposal(
    Actions,
    ClientRequestToken,
    MemberId,
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks/$(networkId)/proposals",
        Dict{String,Any}(
            mergewith(
                _merge,
                Dict{String,Any}(
                    "Actions" => Actions,
                    "ClientRequestToken" => ClientRequestToken,
                    "MemberId" => MemberId,
                ),
                params,
            ),
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    delete_member(member_id, network_id)
    delete_member(member_id, network_id, params::Dict{String,<:Any})

Deletes a member. Deleting a member removes the member and all associated resources from
the network. DeleteMember can only be called for a specified MemberId if the principal
performing the action is associated with the AWS account that owns the member. In all other
cases, the DeleteMember action is carried out as the result of an approved proposal to
remove a member. If MemberId is the last member in a network specified by the last AWS
account, the network is deleted also. Applies only to Hyperledger Fabric.

# Arguments
- `member_id`: The unique identifier of the member to remove.
- `network_id`: The unique identifier of the network from which the member is removed.

"""
function delete_member(
    memberId, networkId; aws_config::AbstractAWSConfig=global_aws_config()
)
    return managedblockchain(
        "DELETE",
        "/networks/$(networkId)/members/$(memberId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function delete_member(
    memberId,
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "DELETE",
        "/networks/$(networkId)/members/$(memberId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    delete_node(network_id, node_id)
    delete_node(network_id, node_id, params::Dict{String,<:Any})

Deletes a node that your AWS account owns. All data on the node is lost and cannot be
recovered. Applies to Hyperledger Fabric and Ethereum.

# Arguments
- `network_id`: The unique identifier of the network that the node is on. Ethereum public
  networks have the following NetworkIds:    n-ethereum-mainnet     n-ethereum-rinkeby
  n-ethereum-ropsten
- `node_id`: The unique identifier of the node.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"memberId"`: The unique identifier of the member that owns this node. Applies only to
  Hyperledger Fabric and is required for Hyperledger Fabric.
"""
function delete_node(networkId, nodeId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "DELETE",
        "/networks/$(networkId)/nodes/$(nodeId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function delete_node(
    networkId,
    nodeId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "DELETE",
        "/networks/$(networkId)/nodes/$(nodeId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    get_member(member_id, network_id)
    get_member(member_id, network_id, params::Dict{String,<:Any})

Returns detailed information about a member. Applies only to Hyperledger Fabric.

# Arguments
- `member_id`: The unique identifier of the member.
- `network_id`: The unique identifier of the network to which the member belongs.

"""
function get_member(memberId, networkId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/members/$(memberId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function get_member(
    memberId,
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/members/$(memberId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    get_network(network_id)
    get_network(network_id, params::Dict{String,<:Any})

Returns detailed information about a network. Applies to Hyperledger Fabric and Ethereum.

# Arguments
- `network_id`: The unique identifier of the network to get information about.

"""
function get_network(networkId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "GET",
        "/networks/$(networkId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function get_network(
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    get_node(network_id, node_id)
    get_node(network_id, node_id, params::Dict{String,<:Any})

Returns detailed information about a node. Applies to Hyperledger Fabric and Ethereum.

# Arguments
- `network_id`: The unique identifier of the network that the node is on.
- `node_id`: The unique identifier of the node.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"memberId"`: The unique identifier of the member that owns the node. Applies only to
  Hyperledger Fabric and is required for Hyperledger Fabric.
"""
function get_node(networkId, nodeId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/nodes/$(nodeId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function get_node(
    networkId,
    nodeId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/nodes/$(nodeId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    get_proposal(network_id, proposal_id)
    get_proposal(network_id, proposal_id, params::Dict{String,<:Any})

Returns detailed information about a proposal. Applies only to Hyperledger Fabric.

# Arguments
- `network_id`: The unique identifier of the network for which the proposal is made.
- `proposal_id`: The unique identifier of the proposal.

"""
function get_proposal(
    networkId, proposalId; aws_config::AbstractAWSConfig=global_aws_config()
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/proposals/$(proposalId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function get_proposal(
    networkId,
    proposalId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/proposals/$(proposalId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    list_invitations()
    list_invitations(params::Dict{String,<:Any})

Returns a list of all invitations for the current AWS account. Applies only to Hyperledger
Fabric.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"maxResults"`: The maximum number of invitations to return.
- `"nextToken"`: The pagination token that indicates the next set of results to retrieve.
"""
function list_invitations(; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "GET", "/invitations"; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET
    )
end
function list_invitations(
    params::AbstractDict{String}; aws_config::AbstractAWSConfig=global_aws_config()
)
    return managedblockchain(
        "GET",
        "/invitations",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    list_members(network_id)
    list_members(network_id, params::Dict{String,<:Any})

Returns a list of the members in a network and properties of their configurations. Applies
only to Hyperledger Fabric.

# Arguments
- `network_id`: The unique identifier of the network for which to list members.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"isOwned"`: An optional Boolean value. If provided, the request is limited either to
  members that the current AWS account owns (true) or that other AWS accounts own (false). If
  omitted, all members are listed.
- `"maxResults"`: The maximum number of members to return in the request.
- `"name"`: The optional name of the member to list.
- `"nextToken"`: The pagination token that indicates the next set of results to retrieve.
- `"status"`: An optional status specifier. If provided, only members currently in this
  status are listed.
"""
function list_members(networkId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/members";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function list_members(
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/members",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    list_networks()
    list_networks(params::Dict{String,<:Any})

Returns information about the networks in which the current AWS account participates.
Applies to Hyperledger Fabric and Ethereum.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"framework"`: An optional framework specifier. If provided, only networks of this
  framework type are listed.
- `"maxResults"`: The maximum number of networks to list.
- `"name"`: The name of the network.
- `"nextToken"`: The pagination token that indicates the next set of results to retrieve.
- `"status"`: An optional status specifier. If provided, only networks currently in this
  status are listed. Applies only to Hyperledger Fabric.
"""
function list_networks(; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "GET", "/networks"; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET
    )
end
function list_networks(
    params::AbstractDict{String}; aws_config::AbstractAWSConfig=global_aws_config()
)
    return managedblockchain(
        "GET", "/networks", params; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET
    )
end

"""
    list_nodes(network_id)
    list_nodes(network_id, params::Dict{String,<:Any})

Returns information about the nodes within a network. Applies to Hyperledger Fabric and
Ethereum.

# Arguments
- `network_id`: The unique identifier of the network for which to list nodes.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"maxResults"`: The maximum number of nodes to list.
- `"memberId"`: The unique identifier of the member who owns the nodes to list. Applies
  only to Hyperledger Fabric and is required for Hyperledger Fabric.
- `"nextToken"`: The pagination token that indicates the next set of results to retrieve.
- `"status"`: An optional status specifier. If provided, only nodes currently in this
  status are listed.
"""
function list_nodes(networkId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/nodes";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function list_nodes(
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/nodes",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    list_proposal_votes(network_id, proposal_id)
    list_proposal_votes(network_id, proposal_id, params::Dict{String,<:Any})

Returns the list of votes for a specified proposal, including the value of each vote and
the unique identifier of the member that cast the vote. Applies only to Hyperledger Fabric.

# Arguments
- `network_id`:  The unique identifier of the network.
- `proposal_id`:  The unique identifier of the proposal.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"maxResults"`:  The maximum number of votes to return.
- `"nextToken"`:  The pagination token that indicates the next set of results to retrieve.
"""
function list_proposal_votes(
    networkId, proposalId; aws_config::AbstractAWSConfig=global_aws_config()
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/proposals/$(proposalId)/votes";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function list_proposal_votes(
    networkId,
    proposalId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/proposals/$(proposalId)/votes",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    list_proposals(network_id)
    list_proposals(network_id, params::Dict{String,<:Any})

Returns a list of proposals for the network. Applies only to Hyperledger Fabric.

# Arguments
- `network_id`:  The unique identifier of the network.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"maxResults"`:  The maximum number of proposals to return.
- `"nextToken"`:  The pagination token that indicates the next set of results to retrieve.
"""
function list_proposals(networkId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/proposals";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function list_proposals(
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/networks/$(networkId)/proposals",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    list_tags_for_resource(resource_arn)
    list_tags_for_resource(resource_arn, params::Dict{String,<:Any})

Returns a list of tags for the specified resource. Each tag consists of a key and optional
value. For more information about tags, see Tagging Resources in the Amazon Managed
Blockchain Ethereum Developer Guide, or Tagging Resources in the Amazon Managed Blockchain
Hyperledger Fabric Developer Guide.

# Arguments
- `resource_arn`: The Amazon Resource Name (ARN) of the resource. For more information
  about ARNs and their format, see Amazon Resource Names (ARNs) in the AWS General Reference.

"""
function list_tags_for_resource(
    resourceArn; aws_config::AbstractAWSConfig=global_aws_config()
)
    return managedblockchain(
        "GET",
        "/tags/$(resourceArn)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function list_tags_for_resource(
    resourceArn,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "GET",
        "/tags/$(resourceArn)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    reject_invitation(invitation_id)
    reject_invitation(invitation_id, params::Dict{String,<:Any})

Rejects an invitation to join a network. This action can be called by a principal in an AWS
account that has received an invitation to create a member and join a network. Applies only
to Hyperledger Fabric.

# Arguments
- `invitation_id`: The unique identifier of the invitation to reject.

"""
function reject_invitation(invitationId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "DELETE",
        "/invitations/$(invitationId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function reject_invitation(
    invitationId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "DELETE",
        "/invitations/$(invitationId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    tag_resource(tags, resource_arn)
    tag_resource(tags, resource_arn, params::Dict{String,<:Any})

Adds or overwrites the specified tags for the specified Amazon Managed Blockchain resource.
Each tag consists of a key and optional value. When you specify a tag key that already
exists, the tag value is overwritten with the new value. Use UntagResource to remove tag
keys. A resource can have up to 50 tags. If you try to create more than 50 tags for a
resource, your request fails and returns an error. For more information about tags, see
Tagging Resources in the Amazon Managed Blockchain Ethereum Developer Guide, or Tagging
Resources in the Amazon Managed Blockchain Hyperledger Fabric Developer Guide.

# Arguments
- `tags`: The tags to assign to the specified resource. Tag values can be empty, for
  example, \"MyTagKey\" : \"\". You can specify multiple key-value pairs in a single request,
  with an overall maximum of 50 tags added to each resource.
- `resource_arn`: The Amazon Resource Name (ARN) of the resource. For more information
  about ARNs and their format, see Amazon Resource Names (ARNs) in the AWS General Reference.

"""
function tag_resource(Tags, resourceArn; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "POST",
        "/tags/$(resourceArn)",
        Dict{String,Any}("Tags" => Tags);
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function tag_resource(
    Tags,
    resourceArn,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/tags/$(resourceArn)",
        Dict{String,Any}(mergewith(_merge, Dict{String,Any}("Tags" => Tags), params));
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    untag_resource(resource_arn, tag_keys)
    untag_resource(resource_arn, tag_keys, params::Dict{String,<:Any})

Removes the specified tags from the Amazon Managed Blockchain resource. For more
information about tags, see Tagging Resources in the Amazon Managed Blockchain Ethereum
Developer Guide, or Tagging Resources in the Amazon Managed Blockchain Hyperledger Fabric
Developer Guide.

# Arguments
- `resource_arn`: The Amazon Resource Name (ARN) of the resource. For more information
  about ARNs and their format, see Amazon Resource Names (ARNs) in the AWS General Reference.
- `tag_keys`: The tag keys.

"""
function untag_resource(
    resourceArn, tagKeys; aws_config::AbstractAWSConfig=global_aws_config()
)
    return managedblockchain(
        "DELETE",
        "/tags/$(resourceArn)",
        Dict{String,Any}("tagKeys" => tagKeys);
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function untag_resource(
    resourceArn,
    tagKeys,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "DELETE",
        "/tags/$(resourceArn)",
        Dict{String,Any}(mergewith(_merge, Dict{String,Any}("tagKeys" => tagKeys), params));
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    update_member(member_id, network_id)
    update_member(member_id, network_id, params::Dict{String,<:Any})

Updates a member configuration with new parameters. Applies only to Hyperledger Fabric.

# Arguments
- `member_id`: The unique identifier of the member.
- `network_id`: The unique identifier of the Managed Blockchain network to which the member
  belongs.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"LogPublishingConfiguration"`: Configuration properties for publishing to Amazon
  CloudWatch Logs.
"""
function update_member(
    memberId, networkId; aws_config::AbstractAWSConfig=global_aws_config()
)
    return managedblockchain(
        "PATCH",
        "/networks/$(networkId)/members/$(memberId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function update_member(
    memberId,
    networkId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "PATCH",
        "/networks/$(networkId)/members/$(memberId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    update_node(network_id, node_id)
    update_node(network_id, node_id, params::Dict{String,<:Any})

Updates a node configuration with new parameters. Applies only to Hyperledger Fabric.

# Arguments
- `network_id`: The unique identifier of the network that the node is on.
- `node_id`: The unique identifier of the node.

# Optional Parameters
Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
- `"LogPublishingConfiguration"`: Configuration properties for publishing to Amazon
  CloudWatch Logs.
- `"MemberId"`: The unique identifier of the member that owns the node. Applies only to
  Hyperledger Fabric.
"""
function update_node(networkId, nodeId; aws_config::AbstractAWSConfig=global_aws_config())
    return managedblockchain(
        "PATCH",
        "/networks/$(networkId)/nodes/$(nodeId)";
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function update_node(
    networkId,
    nodeId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "PATCH",
        "/networks/$(networkId)/nodes/$(nodeId)",
        params;
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end

"""
    vote_on_proposal(vote, voter_member_id, network_id, proposal_id)
    vote_on_proposal(vote, voter_member_id, network_id, proposal_id, params::Dict{String,<:Any})

Casts a vote for a specified ProposalId on behalf of a member. The member to vote as,
specified by VoterMemberId, must be in the same AWS account as the principal that calls the
action. Applies only to Hyperledger Fabric.

# Arguments
- `vote`:  The value of the vote.
- `voter_member_id`: The unique identifier of the member casting the vote.
- `network_id`:  The unique identifier of the network.
- `proposal_id`:  The unique identifier of the proposal.

"""
function vote_on_proposal(
    Vote,
    VoterMemberId,
    networkId,
    proposalId;
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks/$(networkId)/proposals/$(proposalId)/votes",
        Dict{String,Any}("Vote" => Vote, "VoterMemberId" => VoterMemberId);
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end
function vote_on_proposal(
    Vote,
    VoterMemberId,
    networkId,
    proposalId,
    params::AbstractDict{String};
    aws_config::AbstractAWSConfig=global_aws_config(),
)
    return managedblockchain(
        "POST",
        "/networks/$(networkId)/proposals/$(proposalId)/votes",
        Dict{String,Any}(
            mergewith(
                _merge,
                Dict{String,Any}("Vote" => Vote, "VoterMemberId" => VoterMemberId),
                params,
            ),
        );
        aws_config=aws_config,
        feature_set=SERVICE_FEATURE_SET,
    )
end


text:
"""
Naive implementation of Dynamic Mode Decomposition with control (DMDc)

Defaults to predicting the second input as a function of the first.
    Can also be passed a 'mode', "discrete" or "continuous" to process the
    data 'X' to produce the variable to be solved for
"""
function dmdc(X, X_grad=nothing, U=nothing; mode="discrete")
    if X_grad == nothing
        if mode=="discrete"
            X_grad = X[:,2:end]
            X = X[:,1:end-1]
        elseif mode=="continuous"
            X_grad = numerical_derivative(X)
        end
    end

    U == nothing ? (Ω = X) : (Ω = vcat(X,U))
    # Solve for dynamics and separate out
    AB = X_grad / Ω
    if U == nothing
        A = AB
        B = nothing
    else
        n, m = size(X)
        A = AB[:, 1:n]
        B = AB[:, n+1:end]
    end

    return (A=A, B=B)
end

export dmdc


text:
using PyPlot
figure(figsize=(8,5))
fill_between(X.tt,  first.(Xmeanm + 1.96*Xstdm), first.(Xmeanm - 1.96*Xstdm), edgecolor=:darkblue, facecolor=:lightblue, hatch="X", label="95% cred. band")

plot(X.tt, first.(X.yy), label=L"X_1", color=:darkviolet)

plot(X.tt, first.(Xmeanm), label=L"x_1", color=:darkgreen)

if partial
    plot(V.tt, V.yy, "*", label=L"V",  color=:darkviolet)
else
    plot(V.tt, first.(V.yy), ",", label=L"V_1",  color=:darkviolet)
end
PyPlot.legend()

figure(figsize=(8,5))

for i in 1:length(Paths)
    plot(X.tt[1:end-1], first.(Paths[i]), color=:lightblue)
end
plot(X.tt, first.(X.yy), label=L"X_1", color=:darkviolet)
plot(X.tt, first.(Xmeanm), label=L"x_1", color=:darkgreen)

if partial
    plot(V.tt, V.yy, "*", label=L"V",  color=:darkviolet)
else
    plot(V.tt, first.(V.yy), ",", label=L"V_1",  color=:darkviolet)
end
PyPlot.legend()

figure(figsize=(5,5))

for i in 1:length(Paths)
    plot(first.(Paths[i]), last.(Paths[i]), color=:lightblue)
end
plot(first.(X.yy), last.(X.yy), label=L"X", color=:darkviolet)

plot(first.(Xmeanm), last.(Xmeanm), label=L"x_1", color=:darkgreen)

PyPlot.legend()

figure(figsize=(8,5))
plot(X.tt, last.(X.yy), label=L"X_2", linewidth=0.2, color=:darkviolet)
plot(X.tt, last.(Xmeanm), ":", label=L"x_2", color=:darkgreen)
fill_between(X.tt,  last.(Xmeanm + 1.96*Xstdm), last.(Xmeanm - 1.96*Xstdm), edgecolor=:darkblue, facecolor=:lightblue, hatch="X", label="95% cred. band")

if !partial
    plot(V.tt, last.(V.yy), "*", label=L"V_2",  color=:darkviolet)
end
PyPlot.legend()


text:
"""
    restrict(img[, region]) -> imgr

Reduce the size of `img` by approximately two-fold along the dimensions listed in
`region`, or all spatial coordinates if `region` is not specified.
The term `restrict` is taken from the coarsening operation of algebraic multigrid
methods; it is the adjoint of "prolongation" (which is essentially interpolation).
`restrict` anti-aliases the image as it goes, so is better than a naive summation
over 2x2 blocks.
The implementation of `restrict` has been tuned for performance, and should be
a fast method for constructing [pyramids](https://en.wikipedia.org/wiki/Pyramid_(image_processing)).

If `l` is the size of `img` along a particular dimension, `restrict` produces an
array of size `(l+1)÷2` for odd `l`,
and `l÷2 + 1` for even `l`. See the example below for an explanation.

See also [`imresize`](@ref).

# Example

```julia
a_course = [0, 1, 0.3]
```
If we were to interpolate this at the halfway points, we'd get
```julia
a_fine = [0, 0.5, 1, 0.65, 0.3]
```
Note that `a_fine` is obtained from `a_course` via the *prolongation* operator `P` as
`P*a_course`, where
```julia
P = [1   0   0;      # this line "copies over" the first point
     0.5 0.5 0;      # this line takes the mean of the first and second point
     0   1   0;      # copy the second point
     0   0.5 0.5;    # take the mean of the second and third
     0   0   1]      # copy the third
```
`restrict` is the adjoint of prolongation. Consequently,
```julia
julia> restrict(a_fine)
3-element Array{Float64,1}:
 0.125
 0.7875
 0.3125

julia> (P'*a_fine)/2
3-element Array{Float64,1}:
 0.125
 0.7875
 0.3125
```
where the division by 2 approximately preserves the mean intensity of the input.

As we see here, for odd-length `a_fine`, restriction is the adjoint of interpolation at half-grid points.
When `length(a_fine)` is even, restriction is the adjoint of interpolation at 1/4 and 3/4-grid points.
This turns out to be the origin of the `l->l÷2 + 1` behavior.

One consequence of this definition is that the edges move towards zero:
```julia
julia> restrict(ones(11))
6-element Array{Float64,1}:
 0.75
 1.0
 1.0
 1.0
 1.0
 0.75
```
In some applications (e.g., image registration), you may find it useful to trim the edges.
"""
restrict(img::AbstractArray, ::Tuple{}) = img

restrict(A::AbstractArray, region::Vector{Int}) = restrict(A, (region...,))
restrict(A::AbstractArray) = restrict(A, coords_spatial(A))
function restrict(A::AbstractArray, region::Dims)
    restrict(restrict(A, region[1]), Base.tail(region))
end

function restrict(A::AbstractArray{T,N}, dim::Integer) where {T,N}
    indsA = axes(A)
    newinds = ntuple(i->i==dim ? restrict_indices(indsA[dim]) : indsA[i], Val(N))
    out = similar(Array{restrict_eltype(first(A)),N}, newinds)
    restrict!(out, A, dim)
    out
end

restrict_eltype_default(x)          = typeof(x/4 + x/2)
restrict_eltype(x)                  = restrict_eltype_default(x)
restrict_eltype(x::AbstractGray)    = restrict_eltype_default(x)
restrict_eltype(x::AbstractRGB)     = restrict_eltype_default(x)
restrict_eltype(x::Color)           = restrict_eltype_default(convert(RGB, x))
restrict_eltype(x::TransparentGray) = restrict_eltype_default(x)
restrict_eltype(x::TransparentRGB)  = restrict_eltype_default(x)
restrict_eltype(x::Colorant)        = restrict_eltype_default(convert(ARGB, x))

function restrict!(out::AbstractArray{T,N}, A::AbstractArray, dim) where {T,N}
    if dim > N
        return copyto!(out, A)
    end
    indsout, indsA = axes(out), axes(A)
    ndims(out) == ndims(A) || throw(DimensionMismatch("input and output must have the same number of dimensions"))
    for d = 1:length(indsA)
        target = d==dim ? restrict_indices(indsA[d]) : indsA[d]
        indsout[d] == target || error("input and output must have corresponding indices; to be consistent with the input indices,\ndimension $d should be $target, got $(indsout[d])")
    end
    indspre, indspost = indsA[1:dim-1], indsA[dim+1:end]
    _restrict!(out, indsout[dim], A, indspre, indsA[dim], indspost)
end

@generated function _restrict!(out, indout, A,
                               indspre::NTuple{Npre,AbstractUnitRange},
                               indA,
                               indspost::NTuple{Npost,AbstractUnitRange}) where {Npre,Npost}
    Ipre = [Symbol(:ipre_, d) for d = 1:Npre]
    Ipost = [Symbol(:ipost_, d) for d = 1:Npost]
    quote
        $(Expr(:meta, :noinline))
        T = eltype(out)
        if isodd(length(indA))
            half = convert(eltype(T), 0.5)
            quarter = convert(eltype(T), 0.25)
            @nloops $Npost ipost d->indspost[d] begin
                iout = first(indout)
                @nloops $Npre ipre d->indspre[d] begin
                    out[$(Ipre...), iout, $(Ipost...)] = zero(T)
                end
                ispeak = true
                for iA in indA
                    if ispeak
                        @inbounds @nloops $Npre ipre d->indspre[d] begin
                            out[$(Ipre...), iout, $(Ipost...)] +=
                                half*convert(T, A[$(Ipre...), iA, $(Ipost...)])
                        end
                    else
                        @inbounds @nloops $Npre ipre d->indspre[d] begin
                            tmp = quarter*convert(T, A[$(Ipre...), iA, $(Ipost...)])
                            out[$(Ipre...), iout, $(Ipost...)]   += tmp
                            out[$(Ipre...), iout+1, $(Ipost...)] = tmp
                        end
                    end
                    ispeak = !ispeak
                    iout += ispeak
                end
            end
        else
            threeeighths = convert(eltype(T), 0.375)
            oneeighth = convert(eltype(T), 0.125)
            z = zero(T)
            fill!(out, zero(T))
            @nloops $Npost ipost d->indspost[d] begin
                peakfirst = true
                iout = first(indout)
                for iA in indA
                    if peakfirst
                        @inbounds @nloops $Npre ipre d->indspre[d] begin
                            tmp = convert(T, A[$(Ipre...), iA, $(Ipost...)])
                            out[$(Ipre...), iout, $(Ipost...)] += threeeighths*tmp
                            out[$(Ipre...), iout+1, $(Ipost...)] += oneeighth*tmp
                        end
                    else
                        @inbounds @nloops $Npre ipre d->indspre[d] begin
                            tmp = convert(T, A[$(Ipre...), iA, $(Ipost...)])
                            out[$(Ipre...), iout, $(Ipost...)]   += oneeighth*tmp
                            out[$(Ipre...), iout+1, $(Ipost...)] += threeeighths*tmp
                        end
                    end
                    peakfirst = !peakfirst
                    iout += peakfirst
                end
            end
        end
        out
    end
end

# If we're restricting along dimension 1, there are some additional efficiencies possible
@generated function _restrict!(out, indout, A, ::NTuple{0,AbstractUnitRange},
                               indA, indspost::NTuple{Npost,AbstractUnitRange}) where Npost
    Ipost = [Symbol(:ipost_, d) for d = 1:Npost]
    quote
        $(Expr(:meta, :noinline))
        T = eltype(out)
        if isodd(length(indA))
            half = convert(eltype(T), 0.5)
            quarter = convert(eltype(T), 0.25)
            @inbounds @nloops $Npost ipost d->indspost[d] begin
                iout, iA = first(indout), first(indA)
                nxt = convert(T, A[iA+1, $(Ipost...)])
                out[iout, $(Ipost...)] = half*convert(T, A[iA, $(Ipost...)]) + quarter*nxt
                for iA in first(indA)+2:2:last(indA)-2
                    prv = nxt
                    nxt = convert(T, A[iA+1, $(Ipost...)])
                    out[iout+=1, $(Ipost...)] = quarter*(prv+nxt) + half*convert(T, A[iA, $(Ipost...)])
                end
                out[iout+1, $(Ipost...)] = quarter*nxt + half*convert(T, A[last(indA), $(Ipost...)])
            end
        else
            threeeighths = convert(eltype(T), 0.375)
            oneeighth = convert(eltype(T), 0.125)
            z = zero(T)
            @inbounds @nloops $Npost ipost d->indspost[d] begin
                c = d = z
                iA = first(indA)
                for iout = first(indout):last(indout)-1
                    a, b = c, d
                    c, d = convert(T, A[iA, $(Ipost...)]), convert(T, A[iA+1, $(Ipost...)])
                    iA += 2
                    out[iout, $(Ipost...)] = oneeighth*(a+d) + threeeighths*(b+c)
                end
                out[last(indout), $(Ipost...)] = oneeighth*c + threeeighths*d
            end
        end
        out
    end
end

restrict_size(len::Integer) = isodd(len) ? (len+1)>>1 : (len>>1)+1
function restrict_indices(r::AbstractUnitRange)
    f, l = first(r), last(r)
    isodd(f) && return (f+1)>>1:restrict_size(l)
    f>>1 : (isodd(l) ? (l+1)>>1 : l>>1)
end
restrict_indices(r::Base.OneTo) = Base.OneTo(restrict_size(length(r)))
function restrict_indices(r::UnitRange)
    f, l = first(r), last(r)
    isodd(f) && return (f+1)>>1:restrict_size(l)
    f>>1 : (isodd(l) ? (l+1)>>1 : l>>1)
end

# imresize
imresize(original::AbstractArray, dim1::T, dimN::T...) where T<:Union{Integer,AbstractUnitRange} = imresize(original, (dim1,dimN...))
function imresize(original::AbstractArray; ratio)
    all(ratio .> 0) || throw(ArgumentError("ratio $ratio should be positive"))
    new_size = ceil.(Int, size(original) .* ratio) # use ceil to avoid 0
    imresize(original, new_size)
end

function imresize(original::AbstractArray, short_size::Union{Indices{M},Dims{M}}) where M
    len_short = length(short_size)
    len_short > ndims(original) && throw(DimensionMismatch("$short_size has too many dimensions for a $(ndims(original))-dimensional array"))
    new_size = ntuple(i -> (i > len_short ? odims(original, i, short_size) : short_size[i]), ndims(original))
    imresize(original, new_size)
end
odims(original, i, short_size::Tuple{Integer,Vararg{Integer}}) = size(original, i)
odims(original, i, short_size::Tuple{}) = axes(original, i)
odims(original, i, short_size) = oftype(first(short_size), axes(original, i))

"""
    imresize(img, sz) -> imgr
    imresize(img, inds) -> imgr
    imresize(img; ratio) -> imgr

Change `img` to be of size `sz` (or to have indices `inds`). If `ratio` is used, then
`sz = ceil(Int, size(img).*ratio)`. This interpolates the values at sub-pixel locations.
If you are shrinking the image, you risk aliasing unless you low-pass filter `img` first.

# Examples
```julia
julia> img = testimage("lena_gray_256") # 256*256
julia> imresize(img, 128, 128) # 128*128
julia> imresize(img, 1:128, 1:128) # 128*128
julia> imresize(img, (128, 128)) # 128*128
julia> imresize(img, (1:128, 1:128)) # 128*128
julia> imresize(img, (1:128, )) # 128*256
julia> imresize(img, 128) # 128*256
julia> imresize(img, ratio = 0.5) #
julia> imresize(img, ratio = (2, 1)) # 256*128

σ = map((o,n)->0.75*o/n, size(img), sz)
kern = KernelFactors.gaussian(σ)   # from ImageFiltering
imgr = imresize(imfilter(img, kern, NA()), sz)
```

See also [`restrict`](@ref).
"""
function imresize(original::AbstractArray{T,0}, new_inds::Tuple{}) where T
    Tnew = imresize_type(first(original))
    copyto!(similar(original, Tnew), original)
end

function imresize(original::AbstractArray{T,N}, new_size::Dims{N}) where {T,N}
    Tnew = imresize_type(first(original))
    inds = axes(original)
    if map(length, inds) == new_size
        dest = similar(original, Tnew, new_size)
        if axes(dest) == inds
            copyto!(dest, original)
        else
            copyto!(dest, CartesianIndices(axes(dest)), original, CartesianIndices(inds))
        end
    else
        imresize!(similar(original, Tnew, new_size), original)
    end
end

function imresize(original::AbstractArray{T,N}, new_inds::Indices{N}) where {T,N}
    Tnew = imresize_type(first(original))
    if axes(original) == new_inds
        copyto!(similar(original, Tnew), original)
    else
        imresize!(similar(original, Tnew, new_inds), original)
    end
end

# To choose the output type, rather than forcing everything to
# Float64 by multiplying by 1.0, we exploit the fact that the scale
# changes correspond to integer ratios.  We mimic ratio arithmetic
# without actually using Rational (which risks promoting to a
# Rational type, too slow for image processing).
imresize_type(c::Colorant) = base_colorant_type(c){eltype(imresize_type(Gray(c)))}
imresize_type(c::Gray) = Gray{imresize_type(gray(c))}
imresize_type(c::FixedPoint) = typeof(c)
imresize_type(c) = typeof((c*1)/1)

function imresize!(resized::AbstractArray{T,N}, original::AbstractArray{S,N}) where {T,S,N}
    # FIXME: avoid allocation for interpolation
    itp = interpolate(original, BSpline(Linear()))
    imresize!(resized, itp)
end

function imresize!(resized::AbstractArray{T,N}, original::AbstractInterpolation{S,N}) where {T,S,N}
    # Define the equivalent of an affine transformation for mapping
    # locations in `resized` to the corresponding position in
    # `original`. We take the viewpoint that a pixel at `i, j` is a
    # sensor that *integrates* the intensity over an area spanning
    # `i±0.5, j±0.5` (this is a good model of how a camera pixel
    # actually works). We then map the *outer corners* of the two
    # images to each other, i.e., in typical cases
    #     (0.5, 0.5) -> (0.5, 0.5)  (outer corner, top left)
    #     size(resized)+0.5 -> size(original)+0.5  (outer corner, lower right)
    # This ensures that both images cover exactly the same area.
    Ro, Rr = CartesianIndices(axes(original)), CartesianIndices(axes(resized))
    sf = map(/, (last(Ro)-first(Ro)).I .+ 1, (last(Rr)-first(Rr)).I .+ 1) # +1 for outer corners
    offset = map((io,ir,s)->io - 0.5 - s*(ir-0.5), first(Ro).I, first(Rr).I, sf)
    if all(x->x >= 1, sf)
        @inbounds for I in Rr
            I_o = map3((i,s,off)->s*i+off, I.I, sf, offset)
            resized[I] = original(I_o...)
        end
    else
        @inbounds for I in Rr
            I_o = clampR(map3((i,s,off)->s*i+off, I.I, sf, offset), Ro)
            resized[I] = original(I_o...)
        end
    end
    resized
end

# map isn't optimized for 3 tuple-arguments, so do it here
@inline map3(f, a, b, c) = (f(a[1], b[1], c[1]), map3(f, tail(a), tail(b), tail(c))...)
@inline map3(f, ::Tuple{}, ::Tuple{}, ::Tuple{}) = ()

function clampR(I::NTuple{N}, R::CartesianIndices{N}) where N
    map3(clamp, I, first(R).I, last(R).I)
end


text:
# This file is based on rowvector.jl in Julia. License is MIT: https://julialang.org/license
# The motivation for this file is to allow RowVector which doesn't transpose the entries

import Base: convert, similar, length, size, axes, IndexStyle,
            IndexLinear, @propagate_inbounds, getindex, setindex!,
            broadcast, hcat, typed_hcat, map, parent

"""
    RowVector(vector)

A lazy-view wrapper of an `AbstractVector`, which turns a length-`n` vector into a `1×n`
shaped row vector and represents the transpose of a vector (the elements are also transposed
recursively).

By convention, a vector can be multiplied by a matrix on its left (`A * v`) whereas a row
vector can be multiplied by a matrix on its right (such that `transpose(v) * A = transpose(transpose(A) * v)`). It
differs from a `1×n`-sized matrix by the facts that its transpose returns a vector and the
inner product `transpose(v1) * v2` returns a scalar, but will otherwise behave similarly.
"""
struct RowVector{T,V<:AbstractVector} <: AbstractMatrix{T}
    vec::V
    function RowVector{T,V}(v::V) where V<:AbstractVector where T
        new(v)
    end
end

# Constructors that take a vector
@inline RowVector(vec::AbstractVector{T}) where {T} = RowVector{T,typeof(vec)}(vec)
@inline RowVector{T}(vec::AbstractVector{T}) where {T} = RowVector{T,typeof(vec)}(vec)

# Constructors that take a size and default to Array
@inline RowVector{T}(n::Int) where {T} = RowVector{T}(Vector{T}(n))
@inline RowVector{T}(n1::Int, n2::Int) where {T} = n1 == 1 ?
    RowVector{T}(Vector{T}(n2)) :
    error("RowVector expects 1×N size, got ($n1,$n2)")
@inline RowVector{T}(n::Tuple{Int}) where {T} = RowVector{T}(Vector{T}(n[1]))
@inline RowVector{T}(n::Tuple{Int,Int}) where {T} = n[1] == 1 ?
    RowVector{T}(Vector{T}(n[2])) :
    error("RowVector expects 1×N size, got $n")

# Conversion of underlying storage
convert(::Type{RowVector{T,V}}, rowvec::RowVector) where {T,V<:AbstractVector} =
    RowVector{T,V}(convert(V,rowvec.vec))

# similar tries to maintain the RowVector wrapper and the parent type
@inline similar(rowvec::RowVector) = RowVector(similar(parent(rowvec)))
@inline similar(rowvec::RowVector, ::Type{T}) where {T} = RowVector(similar(parent(rowvec), transpose_type(T)))

# Resizing similar currently loses its RowVector property.
@inline similar(rowvec::RowVector, ::Type{T}, dims::Dims{N}) where {T,N} = similar(parent(rowvec), T, dims)

parent(rowvec::RowVector) = rowvec.vec

# AbstractArray interface
@inline length(rowvec::RowVector) =  length(rowvec.vec)
@inline size(rowvec::RowVector) = (1, length(rowvec.vec))
@inline size(rowvec::RowVector, d) = ifelse(d==2, length(rowvec.vec), 1)
@inline axes(rowvec::RowVector) = (Base.OneTo(1), axes(rowvec.vec)[1])
@inline axes(rowvec::RowVector, d) = ifelse(d == 2, axes(rowvec.vec)[1], Base.OneTo(1))
IndexStyle(::RowVector) = IndexLinear()
IndexStyle(::Type{<:RowVector}) = IndexLinear()

@propagate_inbounds getindex(rowvec::RowVector, i) = rowvec.vec[i]
@propagate_inbounds setindex!(rowvec::RowVector, v, i) = setindex!(rowvec.vec, v, i)

# Cartesian indexing is distorted by getindex
# Furthermore, Cartesian indexes don't have to match shape, apparently!
@inline function getindex(rowvec::RowVector, i::CartesianIndex)
    @boundscheck if !(i.I[1] == 1 && i.I[2] ∈ axes(rowvec.vec)[1] && check_tail_indices(i.I...))
        throw(BoundsError(rowvec, i.I))
    end
    @inbounds return rowvec.vec[i.I[2]]
end
@inline function setindex!(rowvec::RowVector, v, i::CartesianIndex)
    @boundscheck if !(i.I[1] == 1 && i.I[2] ∈ axes(rowvec.vec)[1] && check_tail_indices(i.I...))
        throw(BoundsError(rowvec, i.I))
    end
    @inbounds rowvec.vec[i.I[2]] = v
end

@propagate_inbounds getindex(rowvec::RowVector, ::CartesianIndex{0}) = getindex(rowvec)
@propagate_inbounds getindex(rowvec::RowVector, i::CartesianIndex{1}) = getindex(rowvec, i.I[1])

@propagate_inbounds setindex!(rowvec::RowVector, v, ::CartesianIndex{0}) = setindex!(rowvec, v)
@propagate_inbounds setindex!(rowvec::RowVector, v, i::CartesianIndex{1}) = setindex!(rowvec, v, i.I[1])

# helper function for below
@inline to_vec(rowvec::RowVector) = rowvec.vec

# map: Preserve the RowVector by un-wrapping and re-wrapping, but note that `f`
# expects to operate within the transposed domain, so to_vec transposes the elements
@inline map(f, rowvecs::RowVector...) = RowVector(map(f, to_vecs(rowvecs...)...))

# broacast (other combinations default to higher-dimensional array)
@inline broadcast(f, rowvecs::Union{Number,RowVector}...) =
    RowVector(broadcast(f, to_vecs(rowvecs...)...))

# Horizontal concatenation #

@inline hcat(X::RowVector...) = RowVector(vcat(X...))
@inline hcat(X::Union{RowVector,Number}...) = RowVector(vcat(X...))

@inline typed_hcat(::Type{T}, X::RowVector...) where {T} =
    RowVector(typed_vcat(T, X...))
@inline typed_hcat(::Type{T}, X::Union{RowVector,Number}...) where {T} =
    RowVector(typed_vcat(T, X...))

# Multiplication #

# inner product -> dot product specializations
@inline *(rowvec::RowVector{T}, vec::AbstractVector{T}) where {T<:Real} = dotu(parent(rowvec), vec)

# Generic behavior
@inline function *(rowvec::RowVector, vec::AbstractVector)
    if length(rowvec) != length(vec)
        throw(DimensionMismatch("A has dimensions $(size(rowvec)) but B has dimensions $(size(vec))"))
    end
    sum(@inbounds(rowvec[i]*vec[i]) for i = 1:length(vec))
end
@inline *(rowvec::RowVector, mat::AbstractMatrix) = RowVector(transpose(transpose()mat) * rowvec.vec)
*(::RowVector, ::RowVector) = throw(DimensionMismatch("Cannot multiply two transposed vectors"))
@inline *(vec::AbstractVector, rowvec::RowVector) = vec .* rowvec

## Removed A_* overrides for now


text:
"""
Supertype for all prior distributions
"""
abstract type Prior end

"""
    logpdf(::Prior, θ)

Evaluate the log-probability density function at `θ` for a given prior.
"""
function Distributions.logpdf(::T, θ) where {T<:Prior}
    error("logpdf not implemented for prior $T.")
end

"""
Flat prior
"""
struct ImproperPrior <: Prior end
Distributions.logpdf(::ImproperPrior, θ) = 0.0

"""
Flat prior for positive coordinates
"""
struct ImproperPosPrior <: Prior end
Distributions.logpdf(::ImproperPosPrior, θ) = -sum(log.(θ))

"""
    struct StandardPrior{T} <: Prior
        dist::T
    end

A standard prior π(θ).
"""
struct StandardPrior{T} <: Prior
    dist::T
    StandardPrior(dist::T) where T = new{T}(dist)
end
Distributions.logpdf(prior::StandardPrior, θ) = logpdf(prior.dist, θ)

@doc raw"""
    struct ProductPrior{T,K} <: Prior
        dists::T
        idx::K
    end

Generic prior distribution over parameter vector θ written in a facorized form.
For instance if the prior may be written as
```math
π(θ) = π_1(θ_{1:k})π_2(θ_{(k+1):n}),
```
then `dists` would correspond to a list containing `π_1` and `π_2` and `idx`
would be a list containing `1:k` and `(k+1):n`.

    ProductPrior(dists, dims)

Base consructor specifying a list of prior dsitributions `dists` and a
corresponding number of dimensions to which each prior distribution refers to.
"""
struct ProductPrior{T,K} <: Prior
    dists::T
    idx::K

    function ProductPrior(dists, dims)
        dims_reformatted = Any[]
        last_idx = 1
        for dim in dims
            if dim == 1
                push!(dims_reformatted, dim)
                last_idx += 1
            else
                push!(dims_reformatted, last_idx:last_idx+dim-1)
                last_idx += dim
            end
        end
        idx = tuple(dims_reformatted...)
        dists = tuple(dists...)
        new{typeof(dists), typeof(idx)}(dists, idx)
    end
end

function Distributions.logpdf(prior::ProductPrior, θ)
    lp = 0.0
    for (dist, idx) in zip(prior.dists, prior.idx)
        lp += logpdf(dist, θ[idx])
    end
    lp
end


text:
# MIT License
#
# Copyright (c) 2018 Martin Biel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

abstract type AbstractProgressiveHedgingExecution end
# Execution API #
# ------------------------------------------------------------
initialize_subproblems!(ph::AbstractProgressiveHedging, scenarioproblems::AbstractScenarioProblems, penaltyterm::AbstractPenaltyTerm) = initialize_subproblems!(ph, ph.execution, scenarioproblems, penaltyterm)
finish_initilization!(ph::AbstractProgressiveHedging, penalty::AbstractFloat) = finish_initilization!(ph.execution, penalty)
restore_subproblems!(ph::AbstractProgressiveHedging) = restore_subproblems!(ph, ph.execution)
iterate!(ph::AbstractProgressiveHedging) = iterate!(ph, ph.execution)
start_workers!(ph::AbstractProgressiveHedging) = start_workers!(ph, ph.execution)
close_workers!(ph::AbstractProgressiveHedging) = close_workers!(ph, ph.execution)
resolve_subproblems!(ph::AbstractProgressiveHedging) = resolve_subproblems!(ph, ph.execution)
update_iterate!(ph::AbstractProgressiveHedging) = update_iterate!(ph, ph.execution)
update_subproblems!(ph::AbstractProgressiveHedging) = update_subproblems!(ph, ph.execution)
update_dual_gap!(ph::AbstractProgressiveHedging) = update_dual_gap!(ph, ph.execution)
calculate_objective_value(ph::AbstractProgressiveHedging) = calculate_objective_value(ph, ph.execution)
scalar_subproblem_reduction(value::Function, ph::AbstractProgressiveHedging) = scalar_subproblem_reduction(value, ph.execution)
vector_subproblem_reduction(value::Function, ph::AbstractProgressiveHedging, n::Integer) = scalar_subproblem_reduction(value, ph.execution, n)
# ------------------------------------------------------------
include("common.jl")
include("serial.jl")
include("channels.jl")
include("distributed.jl")
include("synchronous.jl")
include("asynchronous.jl")


text:
# Before running this, please make sure to activate and instantiate the environment
# corresponding to [this `Project.toml`](https://raw.githubusercontent.com/alan-turing-institute/DataScienceTutorials.jl/master/Project.toml) and [this `Manifest.toml`](https://raw.githubusercontent.com/alan-turing-institute/DataScienceTutorials.jl/master/Manifest.toml)
# so that you get an environment which matches the one used to generate the tutorials:
#
# ```julia
# cd("DataScienceTutorials") # cd to folder with the *.toml
# using Pkg; Pkg.activate("."); Pkg.instantiate()
# ```

# **Main author**: Yaqub Alwan (IQVIA).## ## Getting started
using MLJ
using PrettyPrinting
import DataFrames
import Statistics
using PyPlot
using StableRNGs

@load LGBMRegressor

# Let us try LightGBM out by doing a regression task on the Boston house prices dataset.# This is a commonly used dataset so there is a loader built into MLJ.
# Here, the objective is to show how LightGBM can do better than a Linear Regressor# with minimal effort.## We start out by taking a quick peek at the data itself and its statistical properties.
features, targets = @load_boston
features = DataFrames.DataFrame(features)
@show size(features)
@show targets[1:3]
first(features, 3) |> pretty

# We can also describe the dataframe
DataFrames.describe(features)

# Do the usual train/test partitioning. This is important so we can estimate generalisation.
train, test = partition(eachindex(targets), 0.70, shuffle=true,
                        rng=StableRNG(52))

# Let us investigation some of the commonly tweaked LightGBM parameters. We start with looking at a learning curve for number of boostings.
lgb = LGBMRegressor() #initialised a model with default params
lgbm = machine(lgb, features[train, :], targets[train, 1])
boostrange = range(lgb, :num_iterations, lower=2, upper=500)
curve = learning_curve!(lgbm, resampling=CV(nfolds=5),
                        range=boostrange, resolution=100,
                        measure=rms)

figure(figsize=(8,6))
plot(curve.parameter_values, curve.measurements)
xlabel("Number of rounds", fontsize=14)
ylabel("RMSE", fontsize=14)

# \fig{lgbm_hp1.svg}
# It looks like that we don't need to go much past 100 boosts
# Since LightGBM is a gradient based learning method, we also have a learning rate parameter which controls the size of gradient updates.# Let us look at a learning curve for this parameter too
lgb = LGBMRegressor() #initialised a model with default params
lgbm = machine(lgb, features[train, :], targets[train, 1])
learning_range = range(lgb, :learning_rate, lower=1e-3, upper=1, scale=:log)
curve = learning_curve!(lgbm, resampling=CV(nfolds=5),
                        range=learning_range, resolution=100,
                        measure=rms)

figure(figsize=(8,6))
plot(curve.parameter_values, curve.measurements)
xscale("log")
xlabel("Learning rate (log scale)", fontsize=14)
ylabel("RMSE", fontsize=14)

# \fig{lgbm_hp2.svg}
# It seems like near 0.5 is a reasonable place. Bearing in mind that for lower# values of learning rate we possibly require more boosting in order to converge, so the default# value of 100 might not be sufficient for convergence. We leave this as an exercise to the reader.# We can still try to tune this parameter, however.
# Finally let us check number of datapoints required to produce a leaf in an individual tree. This parameter# controls the complexity of individual learner trees, and too low a value might lead to overfitting.
lgb = LGBMRegressor() #initialised a model with default params
lgbm = machine(lgb, features[train, :], targets[train, 1])

# dataset is small enough and the lower and upper sets the tree to have certain number of leaves
leaf_range = range(lgb, :min_data_in_leaf, lower=1, upper=50)

curve = learning_curve!(lgbm, resampling=CV(nfolds=5),
                        range=leaf_range, resolution=50,
                        measure=rms)

figure(figsize=(8,6))
plot(curve.parameter_values, curve.measurements)
xlabel("Min data in leaf", fontsize=14)
ylabel("RMSE", fontsize=14)

# \fig{lgbm_hp3.svg}
# It does not seem like there is a huge risk for overfitting, and lower is better for this parameter.
# Using the learning curves above we can select some small-ish ranges to jointly search for the best# combinations of these parameters via cross validation.
r1 = range(lgb, :num_iterations, lower=50, upper=100)
r2 = range(lgb, :min_data_in_leaf, lower=2, upper=10)
r3 = range(lgb, :learning_rate, lower=1e-1, upper=1e0)
tm = TunedModel(model=lgb, tuning=Grid(resolution=5),
                resampling=CV(rng=StableRNG(123)), ranges=[r1,r2,r3],
                measure=rms)
mtm = machine(tm, features, targets)
fit!(mtm, rows=train);

# Lets see what the cross validation best model parameters turned out to be?
best_model = fitted_params(mtm).best_model
@show best_model.learning_rate
@show best_model.min_data_in_leaf
@show best_model.num_iterations

# Great, and now let's predict using the held out data.
predictions = predict(mtm, rows=test)
rms_score = round(rms(predictions, targets[test, 1]), sigdigits=4)

@show rms_score

# This file was generated using Literate.jl, https://github.com/fredrikekre/Literate.jl


text:
using Makie
using MakieLayout

begin
    scene = Scene(camera=campixel!)
    display(scene)

    outergrid = GridLayout(scene, alignmode=Outside(30))

    innergrid = outergrid[1, 1] = GridLayout(3, 3)

    las = innergrid[:, 1] = [LAxis(scene) for i in 1:3]

    alignedgrid1 = innergrid[:, 2] = GridLayout(2, 1; rowsizes=Relative(0.33), valign=:center)
    alignedgrid1[1, 1] = LAxis(scene, yaxisposition=:right, yticklabelalign=(:left, :center))
    alignedgrid1[2, 1] = LAxis(scene)

    alignedgrid2 = innergrid[:, 3] = GridLayout(rowsizes=Relative(0.5); valign=:bottom)
    alignedgrid2[1, 1] = LAxis(scene, xaxisposition=:top, xticklabelalign=(:center, :bottom))

    buttonsgl = innergrid[end+1, :] = GridLayout()

    valigns = (:bottom, :top, :center)

    buttons = buttonsgl[1, 1:3] = [LButton(scene; height=40, label="$v") for v in valigns]

    for (button, align) in zip(buttons, valigns)
        on(button.clicks) do c
            alignedgrid1.valign[] = align
            alignedgrid2.valign[] = align
        end
    end
end