using DifferentialEquations
using PyCall
using PyPlot

function aginglaw(v, theta, dc)
    return 1 - theta * v / dc
end

function sliplaw(v, theta, dc)
    return -v * theta / dc * log(v * theta / dc)
end

function calcdvthetaclassic!(du, u, p, t)
    dc, eta, sigman, a, b, mu, Vp, L, rho, statelaw = p
    theta = u[1]
    v = u[2]
    dtau1_dt = mu * (Vp - v) / L
    du[1] = statelaw(v, theta, dc)
    du[2] = 1 / (eta / sigman + a / v) * (dtau1_dt / sigman - b * du[1] / theta)
    return nothing
end

function calcdv2theta!(du, u, p, t)
    dc, eta, sigman, a, b1, b2, mu, Vp, L, rho, statelaw = p
    theta1 = u[1]
    theta2 = u[2]
    v = u[3]
    dtau1_dt = mu * (Vp - v) / L
    du[1] = statelaw(v, theta1, dc)
    du[2] = statelaw(v, theta2, dc)
    du[3] = 1 / (eta / sigman + a / v) * (dtau1_dt / sigman - b1 * du[1] / theta1 - b2 * du[2] / theta2)
    return nothing
end

function calcdvthetatwo!(du, u, p, t)
    dc, eta, sigman, a, b, mu, Vp, L, rho, statelaw = p

    theta1 = u[1]
    v1 = u[2]
    theta2 = u[3]
    v2 = u[4]
    theta3 = u[5]
    v3 = u[6]

    dtau1_dt = mu * (Vp - (v1 + 0.01*v2 + 0.04*v3)) / L
    dtau2_dt = mu * (Vp - (v2 + 0.05*v1 + 0.95*v3)) / L
    dtau3_dt = mu * (Vp - (v3 + 0.03*v1 - 0.95*v2)) / L

    du[1] = statelaw(v1, theta1, dc)
    du[2] = 1 / (eta / sigman + a / v1) * (dtau1_dt / sigman - b * du[1] / theta1)
    du[3] = statelaw(v2, theta2, dc)
    du[4] = 1 / (eta / sigman + a / v2) * (dtau2_dt / sigman - b * du[3] / theta2)
    du[5] = statelaw(v3, theta3, dc)
    du[6] = 1 / (eta / sigman + a / v3) * (dtau3_dt / sigman - b * du[5] / theta3)

    return nothing
end

function rs_two()
    close("all")

    # Model parameters
    siay = 365.25 * 24 * 60 * 60
    tspan = (0.0, siay * 20000.0) # This will run
    mu = 3e10
    nu = 0.25
    rho = 2700.0
    eta = mu / (2.0 * sqrt(mu / rho))
    L = 60 * 1e3
    a = 0.015
    b = 0.02
    vp = 1e-9
    sigman = 30e6
    dc = 0.1
    abstol = 1e-8
    reltol = 1e-8

    # Time integrate - classic
    icsclassic = [1e8; vp / 1000]
    pclassic = (dc, eta, sigman, a, b, mu, vp, L, rho, sliplaw)
    probclassic = ODEProblem(calcdvthetaclassic!, icsclassic, tspan, pclassic)
    solclassic = solve(probclassic, RK4(), abstol = abstol, reltol = reltol)

    # Plot velocity time series
    t1 = [x / siay for x in solclassic.t]
    theta1 = [x[1] for x in solclassic.u]
    v1 = [x[2] for x in solclassic.u]

    figure()
    plot(t1, v1, "-b", linewidth = 0.5)
    yscale("log")
    xlabel("time (years)")
    ylabel("v (m/s)")

    # Time integrate - two state parameters
    icsclassic = [1e8; 2e8; vp / 1000]
    pclassic = (dc, eta, sigman, a, b, 0.99*b, mu, vp, L, rho, sliplaw)
    probclassic = ODEProblem(calcdv2theta!, icsclassic, tspan, pclassic)
    solclassic = solve(probclassic, RK4(), abstol = abstol, reltol = reltol)

    # Plot velocity time series
    t1 = [x / siay for x in solclassic.t]
    theta1 = [x[1] for x in solclassic.u]
    v1 = [x[3] for x in solclassic.u]

    figure()
    plot(t1, v1, "-b", linewidth = 0.5)
    yscale("log")
    xlabel("time (years)")
    ylabel("v (m/s)")
    return

    # Time integrate - multi
    icstwo = [1e8; vp / 1000; 1e8; vp / 1000; 1e8 ; vp / 1000]

    ptwo = (dc, eta, sigman, a, b, mu, vp, L, rho, sliplaw)
    probtwo = ODEProblem(calcdvthetatwo!, icstwo, tspan, ptwo)
    soltwo = solve(probtwo, RK4(), abstol = abstol, reltol = reltol)

    # Plot velocity time series
    t1 = [x / siay for x in soltwo.t]
    theta1 = [x[1] for x in soltwo.u]
    v1 = [x[2] for x in soltwo.u]
    theta2 = [x[3] for x in soltwo.u]
    v2 = [x[4] for x in soltwo.u]
    v3 = [x[6] for x in soltwo.u]

    figure()
    plot(t1, v1, "-b", linewidth = 0.5)
    yscale("log")
    xlabel("time (years)")
    ylabel("v (m/s)")

    figure()
    plot(t1, v2, "--r", linewidth = 0.5)
    yscale("log")
    xlabel("time (years)")
    ylabel("v (m/s)")

    figure()
    plot(v3, "-g", linewidth = 0.5)
    yscale("log")
    xlabel("time (years)")
    ylabel("v (m/s)")

    figure()
    subplot(2, 2, 1)
    plot(log10.(v1), log10.(v1), "-k", linewidth = 0.1)
    subplot(2, 2, 2)
    plot(log10.(v1), log10.(v3), "-k", linewidth = 0.1)
    subplot(2, 2, 3)
    plot(log10.(v2), log10.(v3), "-k", linewidth = 0.1)

    d1 = zeros(length(v1))
    d2 = zeros(length(v1))
    d3 = zeros(length(v1))
    for i in 2:length(v1)
        d1[i] = d1[i-1] + (v1[i-1]) / (t1[i] - t1[i-1])
        d2[i] = d2[i-1] + (v2[i-1]) / (t1[i] - t1[i-1])
        d3[i] = d3[i-1] + (v3[i-1]) / (t1[i] - t1[i-1])
    end
    figure()
    plot(t1, d1, "-b", linewidth = 0.5)
    plot(t1, d2, "-r", linewidth = 0.5)
    plot(t1, d3, "-g", linewidth = 0.5)

    xlabel("time (years)")
    ylabel("d (m)")

    return nothing
end
rs_two()


# # Immersed layer caches

#md # ```@meta
#md # CurrentModule = ImmersedLayers
#md # ```

#=
This package uses precomputed caches to efficiently implement the immersed layer
operators. The starting point for a cache is the specification of the
(discretized) body shape and the grid. Let's use an example.
=#

# ## Setting up a cache

using ImmersedLayers
using Plots
using LinearAlgebra

#=
### Set up a grid
First, we will set up a grid for performing the operations. We use the `PhysicalGrid`
constructor of the `CartesianGrids.jl` package to create this.
=#
Δx = 0.01
Lx = 4.0
xlim = (-Lx/2,Lx/2)
ylim = (-Lx/2,Lx/2)
g = PhysicalGrid(xlim,ylim,Δx)

#=
### Set the shape
Now let's set a shape to immerse into the grid. We will use a circle, but
there are a variety of other shapes available. Many of these are in the
`RigidBodyTools.jl` package. Note that we set the spacing between the points
on this shape equal to 1.4 times the grid spacing. This is not critical, but it
is generally best to set it to a value between 1 and 2.
=#
RadC = 1.0
Δs = 1.4*cellsize(g)
body = Circle(RadC,Δs)

#=
### Create the cache
After setting the grid and the surface to immerse, the next step for using the
immersed layer tools is to set up a *surface cache*. This allocates a set of
data structures, as well as the critical *regularization* and *interpolation*
operators that will get used.

There are a few choices to make when setting this up
* **What kind of data (scalar or vector) are we dealing with?**

We will demonstrate with scalar data, which means we use the [`SurfaceScalarCache`](@ref)
function. For vector data, use [`SurfaceVectorCache`](@ref).

* **What type of scaling (grid or index) do we wish to apply to the operators?**

Grid scaling, set with `scaling = GridScaling`, means that the various operators are scaled with the physical grid spacing
and/or surface point spacing so that they approximate the continuous operators. This
means that regularization and interpolation are transposes with respect to
inner products that incorporate these physical spacings, rather than the usual
linear algebra inner products. Also, differential operations on the grid are true
approximations of their continuous counterparts. This choice of scaling is usually the best, and
the [`dot`](@ref) operator is extended in this package to implement the physically-
scaled inner products.

On the other hand, `scaling = IndexScaling` does not scale these, but rather, uses
pure differencing for the grid differential operators, and regularization is
the simple matrix transpose of interpolation.

* **What discrete Dirac delta function (DDF) do we wish to use?**

This is specified with the `ddftype` keyword argument. The default is
`ddftype=CartesianGrids.Yang3` [^1]. However, there are
other choices, such as `CartesianGrids.Roma`, `CartesianGrids.Goza`, `CartesianGrids.Witchhat`,
`CartesianGrids.M3`, `CartesianGrids.M4prime`.

* **Are there other data or functions you need to pass along?**

In more complicated cases, you might want to pass along some physical
parameters, in which case you can supply these with the `phys_params`
keyword. Boundary conditions can be provided with the `bc` keyword,
and forcing functions with the `f_funcs` keyword. These are all optional,
of course. We will see examples later in [Problems and the system](@ref)
=#

cache = SurfaceScalarCache(body,g,scaling=GridScaling)

#=
## Plotting the immersed points
We can plot the immersed points with the `plot` function of the `Plots.jl`
package, using a special plot recipe. This accepts all of the
usual attribute keywords of the basic plot function.
=#
plot(cache,xlims=(-2,2),ylims=(-2,2))

#=
In this plotting demonstration, we have used the `points` function
to obtain the coordinates of the immersed points. Other useful
utilities are `normals` and `areas`, e.g.
=#
normals(cache)

#=
## Some basic utilities
We will see deeper uses of this cache in [Surface-grid operations](@ref).
However, for now we can learn how to get basic copies of the grid
and surface data. For example, a copy of the grid data, all initialized to zero:
=#
zeros_grid(cache)

#=
or similarly on the surface
=#
zeros_surface(cache)

#=
We also might need to initialize data on the grid to accept the curl of
a vector field. This is used in conjunction with [`surface_curl!`](@ref),
for example.
=#
zeros_gridcurl(cache)

#=
If we want them initialized to unity, then use
=#
ones_grid(cache)

# and
ones_surface(cache)

#=
To evaluate functions on the grid, it is useful to be able to
fill grid data with the x and y coordinates. For this, we use
=#
x_grid(cache)
#-
y_grid(cache)

#=
## Using norms and inner products
It is useful to compute norms and inner products on grid and surface data.
These tools are easily accessible, e.g., `dot(u,v,cache)` and `norm(u,cache)`,
and they respect the scaling associated with the cache. For example,
the following gives an approximation of the circumference of the circle:
=#
os = ones_surface(cache)
dot(os,os,cache)

#md # ## Cache types and constructors

#md # ```@docs
#md # BasicILMCache
#md # SurfaceScalarCache
#md # SurfaceVectorCache
#md # AbstractExtraILMCache
#md # ```

#md # ## Utilities for creating instances of data

#md # ```@docs
#md # similar_grid
#md # similar_gridgrad
#md # similar_gridcurl
#md # similar_surface
#md # zeros_grid
#md # zeros_gridgrad
#md # zeros_gridcurl
#md # zeros_surface
#md # ones_grid
#md # ones_gridgrad
#md # ones_gridcurl
#md # ones_surface
#md # x_grid
#md # y_grid
#md # x_gridcurl
#md # y_gridcurl
#md # ```

#md # ## Utilities for accessing surface information

#md # ```@docs
#md # areas(::BasicILMCache)
#md # normals(::BasicILMCache)
#md # points(::BasicILMCache)
#md # ```

#md # ## Inner products, norms, and integrals

#md # ```@docs
#md # dot(::GridData,::GridData,::BasicILMCache)
#md # dot(::PointData,::PointData,::BasicILMCache)
#md # norm(::GridData,::BasicILMCache)
#md # norm(::PointData,::BasicILMCache)
#md # integrate(::ScalarData{N},::BasicILMCache{N}) where {N}
#md # dot(::PointData,::PointData,::BasicILMCache,::Int)
#md # norm(::PointData,::BasicILMCache,::Int)
#md # integrate(::ScalarData{N},::BasicILMCache{N},::Int) where {N}
#md # ```

#md # ## Other cache utilities
#md #
#md # ```@docs
#md # view(::PointData,::BasicILMCache,::Int)
#md # RegularizationMatrix(::BasicILMCache,::PointData,::GridData)
#md # InterpolationMatrix(::BasicILMCache,::GridData,::PointData)
#md # ```

#md # [^1]: Yang, X., et al., (2009) "A smoothing technique for discrete delta functions with application to immersed boundary method in moving boundary simulations," J. Comput. Phys., 228, 7821--7836.


@testset "trunc" begin

@test trunc(Int64, 3.14) == 3
@test trunc(Int64, 3.41) == 3
@test trunc(Int64, 3.51) == 3
@test trunc(Int64, 3.91) == 3

end

@testset "round" begin
@test round(1.4) == 1
@test round(1.5) == 2
end

@testset "parse" begin
@test parse(Int64, "13") == 13
# Why would this work???
@test parse(Float64, "13.1415926") == 13.1415926
end


include("_common.jl")

using CategoricalArrays, StatsPlots

climate_fig_path = joinpath(FIG_PATH, "climate") * "/"
mkpath(climate_fig_path)

# Change plot theme for publication
theme(:default, grid=false)

dataset = copy(FULL_DATASET)
data_years = Dates.year.(dataset.Date)
start_year = minimum(data_years)
end_year = maximum(data_years)
half_decades = cut(data_years, start_year:5:end_year, extend=true)
annual = cut(data_years, start_year:1:end_year, extend=true)

dataset.half_decade_set = half_decades
dataset.annual = annual

half_decade_dataset = groupby(dataset, :half_decade_set)
annual_dataset = groupby(dataset, :annual)

# Plot historic rainfall
thresholds = quantile(FULL_DATASET[:, "410730_P"], [0.6, 0.95])
bar(FULL_DATASET.Date, FULL_DATASET[:, "410730_P"],
	title="Observed Rainfall",
	label="",
	xlabel="Year",
	ylabel="Rainfall [mm]")
hline!(thresholds, label="Bin thresholds")
savefig("$(climate_fig_path)rainfall_record.png")

rain_sym = Symbol("410730_P")
temp_sym = Symbol("410730_max_T")
flow_sym = Symbol("410730_Q")

climate_annual_stats = combine(annual_dataset,
						rain_sym => mean,
						rain_sym => std,
						rain_sym => maximum,
						rain_sym => sum,
						temp_sym => mean,
						temp_sym => std,
						temp_sym => maximum,
						temp_sym => sum)

# Plot annual rain
bar(start_year:end_year, climate_annual_stats[:, "410730_P_sum"],
	title="Observed Annual Rainfall",
	label="",
	xlabel="Year",
	ylabel="Rainfall [mm]")
savefig("$(climate_fig_path)rainfall.png")

climate_half_decade_stats = combine(half_decade_dataset,
							rain_sym => mean,
							rain_sym => std,
							rain_sym => maximum,
							rain_sym => sum)

flow_annual_stats = combine(annual_dataset,
						flow_sym => mean,
						flow_sym => std,
						flow_sym => maximum,
						flow_sym => sum)

flow_half_decade_stats = combine(half_decade_dataset,
							flow_sym => mean,
							flow_sym => std,
							flow_sym => maximum,
							flow_sym => sum)

# flow_half_decade_stats

bar(start_year+1:end_year, climate_annual_stats[:, "410730_P_sum"], label="",
	title="Total Annual Rainfall",
	xlabel="Year",
	ylabel="Rainfall [mm]")
savefig("$(climate_fig_path)rainfall_sum.png")

p_total = climate_annual_stats[:, "410730_P_sum"]
violin(["Calibration"], p_total[1:30], label="", title="Annual Rainfall", ylabel="Total [mm]")
violin!(["Validation"], p_total[31:end], label="")
dotplot!(["Calibration"], p_total[1:30],
		 marker=(:black, stroke(0)),
		 label="")
dotplot!(["Validation"], p_total[31:end],
		 marker=(:black, stroke(0)),
		 label="")
savefig("$(climate_fig_path)rainfall_comparison.png")

# Average temperatures
plot(start_year+1:end_year, climate_annual_stats[:, "410730_max_T_mean"], color="red",
	 label="",
	 title="Mean Annual Temperature",
	 xlabel="Year",
	 ylabel="Temperature [°C]")
savefig("$(climate_fig_path)average_max_T.png")

T_total = climate_annual_stats[:, "410730_max_T_mean"]
violin(["Calibration"], T_total[1:30], label="", title="Mean Max Temperature", ylabel="Degrees [°C]")
violin!(["Validation"], T_total[31:end], label="")
dotplot!(["Calibration"], T_total[1:30],
		 marker=(:black, stroke(0)),
		 label="")
dotplot!(["Validation"], T_total[31:end],
		 marker=(:black, stroke(0)),
		 label="")
savefig("$(climate_fig_path)temperature_comparison.png")

# Coefficient of variation
climate_annual_stats.rainfall_variability = climate_annual_stats[:, "410730_P_std"] ./ climate_annual_stats[:, "410730_P_mean"]

p_vari = climate_annual_stats.rainfall_variability
violin(["Calibration"], p_vari[1:30], label="", title="Rainfall Variability", ylabel="Variability [CV]")
violin!(["Validation"], p_vari[31:end], label="")
dotplot!(["Calibration"], p_vari[1:30],
		 marker=(:black, stroke(0)),
		 label="")
dotplot!(["Validation"], p_vari[31:end],
		 marker=(:black, stroke(0)),
		 label="")
savefig("$(climate_fig_path)rainfall_variability_comparison.png")

bar(start_year:end_year, flow_annual_stats[:, "410730_Q_sum"] ./ 1000,
		xlabel="Year", ylabel="Annual Flow [GL]", label="",
		title="Annual Flow [GL]"
)

savefig("$(climate_fig_path)annual_flow.png")

flow_annual_stats.flow_variability = (flow_annual_stats[:, "410730_Q_std"] ./ flow_annual_stats[:, "410730_Q_mean"])

bar(start_year:end_year, flow_annual_stats.flow_variability,
	xlabel="Year", ylabel="Flow variability [CV]", label="",
	title="Variability of Annual Streamflow"
)

# Qualitatively determined separation between calib start/end
c_end = start_year + 30
v_start = start_year + 31
vline!([v_start], label="")
annotate!((start_year+0.5, 1.6, text("Calibration", font(11), :left)))
annotate!((v_start+0.5, 1.6, text("Validation", font(11), :left)))
savefig("$(climate_fig_path)flow_variability.png")

flow_col = Symbol("flow_variability")
violin(["Calibration"], flow_annual_stats[1:30, flow_col],
		label="", ylabel="Variability of Flow [CV]",
		title="Annual Flow\nVariability")
violin!(["Validation"], flow_annual_stats[31:end, flow_col],
		label="")
dotplot!(["Calibration"], flow_annual_stats[1:30, flow_col],
		 marker=(:black, stroke(0)),
		 label="")
dotplot!(["Validation"], flow_annual_stats[31:end, flow_col],
		 marker=(:black, stroke(0)),
		 label="")
savefig("$(climate_fig_path)flow_variability_comparison.png")

"""
Köppen aridity index

See:
https://doi.org/10.1016/j.palaeo.2013.05.008

AIₖ : arid: < 40; humid: 40–160; perhumid: > 160
"""
AI_k(P, T) = P ./ (T .+ 33.0)

AP = climate_annual_stats[:, "410730_P_sum"]
AT = climate_annual_stats[:, "410730_max_T_mean"]
aridity = AI_k(AP, AT)

bar(start_year:end_year, aridity,
	label="",
	xlabel="Year",
	ylabel="\$AI_{k}\$",
	title="Köppen Aridity Index"
)

vline!([v_start], label="", linewidth=2.0)
annotate!((start_year, 34.0, text("Calibration", font(11), :left)))
annotate!((v_start+0.5, 34.0, text("Validation", font(11), :left)))
savefig("$(climate_fig_path)aridity.png")

violin(["Calibration"], aridity[1:30], label="",
		ylabel="\$AI_{k}\$",
		title="Aridity Characteristics")
violin!(["Validation"], aridity[31:end], label="")
dotplot!(["Calibration"], aridity[1:30],
		 marker=(:black, stroke(0)),
		 label="")
dotplot!(["Validation"], aridity[31:end],
		 marker=(:black, stroke(0)),
		 label="")
savefig("$(climate_fig_path)aridity_comparison.png")

@info "Timeframe:" start_year, end_year
@info minimum(dataset.Date), maximum(dataset.Date)

flow_col = Symbol("410730_Q_sum")
violin(["Calibration"], flow_annual_stats[1:30, flow_col] ./ 1000,
		label="", ylabel="Annual Flow [GL]",
		title="Annual Flow\nCharacteristics")
violin!(["Validation"], flow_annual_stats[31:end, flow_col] ./ 1000,
		label="")
dotplot!(["Calibration"], flow_annual_stats[1:30, flow_col] ./1000,
		 marker=(:black, stroke(0)),
		 label="")
dotplot!(["Validation"], flow_annual_stats[31:end, flow_col] ./1000,
		 marker=(:black, stroke(0)),
		 label="")
savefig("$(climate_fig_path)flow_comparison.png")

describe(flow_annual_stats[1:30, Symbol("410730_Q_sum")] ./ 1000)
# Summary Stats:
# Length:         30
# Missing Count:  0
# Mean:           48.433578
# Minimum:        6.393224
# 1st Quartile:   31.157348
# Median:         46.762521
# 3rd Quartile:   62.423117
# Maximum:        110.813315
# Type:           Float64

describe(flow_annual_stats[31:end, Symbol("410730_Q_sum")] ./ 1000)
# Summary Stats:
# Length:         21
# Missing Count:  0
# Mean:           35.346256
# Minimum:        8.673252
# 1st Quartile:   21.041727
# Median:         35.154885
# 3rd Quartile:   51.299297
# Maximum:        67.310798
# Type:           Float64

# Plot streamflow
plot(CALIB_DATES, FULL_DATASET[1:CALIB_LN, "410730_Q"], label="Calibration", color="darkblue", title="Historic Streamflow")
plot!(VALID_DATES, FULL_DATASET[CALIB_LN+1:end, "410730_Q"], label="Validation", color="lightblue", ylabel="Streamflow [ML]")
savefig("$(climate_fig_path)historic_flow.png")


# rule_generation.jl

struct Rule
    p::Array{Int32,1}
    q::Int32
    supp::Float64
    conf::Float64
    lift::Float64
end

function update_support_cnt!(supp_dict::Dict, nd::Node)
    supp_dict[nd.item_ids] = nd.supp
end

function grow_support_dict!(supp_cnt::Dict{Array{Int32,1}, Int}, node::Node)
    if has_children(node)
        for nd in node.children
            update_support_cnt!(supp_cnt, nd)
            grow_support_dict!(supp_cnt, nd)
        end
    end
end

# This function generates a dictionary whose keys are the frequent
# itemsets (their integer represenations, actually), and whose values
# are the support count for the given itemset. This function is used
# for computing support, confidence, and lift of association rules.
function gen_support_dict(root::Node, num_transacts)
    supp_cnt = Dict{Array{Int32, 1}, Int}()
    supp_cnt[Int32[]] = num_transacts
    grow_support_dict!(supp_cnt, root)
    return supp_cnt
end

# Given a single node in a frequent item tree, this function generates all the
# rules for that node. This does not include rules for the node's children.
function gen_node_rules(node::Node, supp_dict::Dict{Array{Int32,1}, Int}, k, num_transacts, minconf)
    lhs_keep = trues(k)
    rules = Array{Rule, 1}(undef, 0)
    for i = 1:k
        lhs_keep[i] = false
        if i > 1
            lhs_keep[i-1] = true
        end
        p = node.item_ids[lhs_keep]
        supp = node.supp/num_transacts
        # for debugging
        if supp == 0.0
            warn("zero supp for: ", node.item_ids)
            return rules            # NOTE: returning early if zero support
        else
            conf = supp/((supp_dict[node.item_ids[lhs_keep]])/num_transacts)
            if conf ≥ minconf
                rhs_keep = .!lhs_keep
                q = first(node.item_ids[rhs_keep])
                lift = conf/((supp_dict[node.item_ids[rhs_keep]])/num_transacts)

                rule = Rule(p, q, supp, conf, lift)
                push!(rules, rule)
            end
        end
    end
    rules
end

function gen_rules!(rules::Array{Rule, 1}, node::Node, supp_dict::Dict{Array{Int32, 1}, Int}, k, num_transacts, minconf)
    m = length(node.children)

    for child in node.children
        rules_tmp = gen_node_rules(child, supp_dict, k, num_transacts, minconf)
        append!(rules, rules_tmp)
        if !isempty(child.children)
            gen_rules!(rules, child, supp_dict, k+1, num_transacts, minconf)
        end
    end
end

function gen_rules(root::Node, supp_dict::Dict{Array{Int32, 1}, Int}, num_transacts, minconf)
    rules = Array{Rule, 1}(undef, 0)
    n_kids = length(root.children)
    if n_kids > 0
        for i = 1:n_kids
            gen_rules!(rules, root.children[i], supp_dict, 2, num_transacts, minconf)
        end
    end
    rules
end

function rules_to_dataframe(rules::Array{Rule, 1}, item_lkup::Dict{T, S}; join_str = " | ") where {T <: Integer, S}
    n_rules = length(rules)
    dt = DataFrame(lhs = fill("", n_rules),
                   rhs = fill("", n_rules),
                   supp = zeros(n_rules),
                   conf = zeros(n_rules),
                   lift = zeros(n_rules))
    for i = 1:n_rules
        lhs_items = map(x -> string.(item_lkup[x]), rules[i].p)

        lhs_string = "{" * join(lhs_items, join_str) * "}"
        dt[i, :lhs] = lhs_string
        dt[i, :rhs] = string.(item_lkup[rules[i].q])
        dt[i, :supp] = rules[i].supp
        dt[i, :conf] = rules[i].conf
        dt[i, :lift] = rules[i].lift
    end
    dt
end

"""
    apriori(transactions; supp, conf, maxlen)
Given an array of transactions (a vector of string vectors), this function runs the a-priori
algorithm for generating frequent item sets. These frequent items are then used to generate
association rules. The `supp` argument allows us to stipulate the minimum support
required for an itemset to be considered frequent. The `conf` argument allows us to exclude
association rules without at least `conf` level of confidence. The `maxlen` argument stipulates
the maximum length of an association rule (i.e., total items on left- and right-hand sides)
"""
function apriori(transactions::Array{Array{S, 1}, 1}; supp::Float64 = 0.01, conf = 0.8, maxlen::Int = 5) where S
    n = length(transactions)
    uniq_items = unique_items(transactions)
    item_lkup = Dict{Int32, S}()
    for (i, itm) in enumerate(uniq_items)
        item_lkup[i] = itm
    end
    minsupp = floor(Int, supp * n)
    if minsupp == 0
        minsupp += 1
    end
    freq_tree = frequent_item_tree(transactions, uniq_items, minsupp, maxlen)
    supp_lkup = gen_support_dict(freq_tree, n)
    rules = gen_rules(freq_tree, supp_lkup, n, conf)
    rules_dt = rules_to_dataframe(rules, item_lkup)
    return rules_dt
end

"""
apriori(occurrences, item_lkup; supp, conf, maxlen)

Given an boolean occurrence matrix of transactions (rows are transactions, columns are items) and
a lookup dictionary of column-index to items-string, this function runs the a-priori
algorithm for generating frequent item sets. These frequent items are then used to generate
association rules. The `supp` argument allows us to stipulate the minimum support
required for an itemset to be considered frequent. The `conf` argument allows us to exclude
association rules without at least `conf` level of confidence. The `maxlen` argument stipulates
the maximum length of an association rule (i.e., total items on left- and right-hand sides)
"""
function apriori(occurrences::BitArray{2}; supp::Float64 = 0.01, conf = 0.8, maxlen::Int = 5)
    n = size(occurrences, 1)
    minsupp = floor(Int, supp * n)
    if minsupp == 0
        minsupp = 1
    end
    freq_tree = frequent_item_tree(occurrences, minsupp, maxlen)
    supp_lkup = gen_support_dict(freq_tree, n)
    rules = gen_rules(freq_tree, supp_lkup, n, conf)

    return rules
end


# ---
# title: 1463. Cherry Pickup II
# id: problem1463
# author: Tian Jun
# date: 2020-10-31
# difficulty: Hard
# categories: Dynamic Programming
# link: <https://leetcode.com/problems/cherry-pickup-ii/description/>
# hidden: true
# ---
#
# Given a `rows x cols` matrix `grid` representing a field of cherries. Each
# cell in `grid` represents the number of cherries that you can collect.
#
# You have two robots that can collect cherries for you, Robot #1 is located at
# the top-left corner (0,0) , and Robot #2 is located at the top-right corner
# (0, cols-1) of the grid.
#
# Return the maximum number of cherries collection using both robots  by
# following the rules below:
#
#   * From a cell (i,j), robots can move to cell (i+1, j-1) , (i+1, j) or (i+1, j+1).
#   * When any robot is passing through a cell, It picks it up all cherries, and the cell becomes an empty cell (0).
#   * When both robots stay on the same cell, only one of them takes the cherries.
#   * Both robots cannot move outside of the grid at any moment.
#   * Both robots should reach the bottom row in the `grid`.
#
#
#
# **Example 1:**
#
# **![](https://assets.leetcode.com/uploads/2020/04/29/sample_1_1802.png)**
#
#
#
#     Input: grid = [[3,1,1],[2,5,1],[1,5,5],[2,1,1]]
#     Output: 24
#     Explanation:  Path of robot #1 and #2 are described in color green and blue respectively.
#     Cherries taken by Robot #1, (3 + 2 + 5 + 2) = 12.
#     Cherries taken by Robot #2, (1 + 5 + 5 + 1) = 12.
#     Total of cherries: 12 + 12 = 24.
#
#
# **Example 2:**
#
# **![](https://assets.leetcode.com/uploads/2020/04/23/sample_2_1802.png)**
#
#
#
#     Input: grid = [[1,0,0,0,0,0,1],[2,0,0,0,0,3,0],[2,0,9,0,0,0,0],[0,3,0,5,4,0,0],[1,0,2,3,0,0,6]]
#     Output: 28
#     Explanation:  Path of robot #1 and #2 are described in color green and blue respectively.
#     Cherries taken by Robot #1, (1 + 9 + 5 + 2) = 17.
#     Cherries taken by Robot #2, (1 + 3 + 4 + 3) = 11.
#     Total of cherries: 17 + 11 = 28.
#
#
# **Example 3:**
#
#
#
#     Input: grid = [[1,0,0,3],[0,0,0,3],[0,0,3,3],[9,0,3,3]]
#     Output: 22
#
#
# **Example 4:**
#
#
#
#     Input: grid = [[1,1],[1,1]]
#     Output: 4
#
#
#
#
# **Constraints:**
#
#   * `rows == grid.length`
#   * `cols == grid[i].length`
#   * `2 <= rows, cols <= 70`
#   * `0 <= grid[i][j] <= 100 `
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


@testset "Linear Algebra" begin
    @testset "lu" begin
        m = AxisArray([1.0 2; 3 4], (2:3, 3:4))
        x = @inferred(lu(m))

        @test m[x.p, :] == parent(m)[parent(x).p,:]
        @test x.factors == lu(parent(m)).factors
    end

    # FIXME inferrence for eigen
    @testset "eigen" begin
        m = AxisArray([1.0 2; 3 4], (2:3, 3:4))
        x = eigen(m)
        @test keys.(axes(x.vectors)) == (2:3, 3:4)
        @test eigvals(m) == eigvals(parent(m))
    end

    @testset "svd" begin
        m = AxisArray([1.0 2; 3 4], (2:3, 3:4))
        x = svd(m)
        u, s, v = x
        @test size(x) == (2, 2)
        @test size(x) == (2, 2)
        @test size(x, 1) == 2
        @test keys.(axes(x.S)) == (1:2,)

        F = svd(AxisArray([1.0 2; 3 4], (Axis(2:3 => Base.OneTo(2)), Axis(3:4 => Base.OneTo(2)))));
        io = IOBuffer()
        show(io, F)
        str = String(take!(io))
        @test str[1:7] == "AxisSVD"
    end

    @testset "LQ" begin
        m = AxisArray([1.0 2; 3 4], (2:3, 3:4))
        x = @inferred(lq(m))
        @test x.factors == lq(parent(m)).factors
    end

    @testset "qr" begin
        for pivot in (true, false)
            for data in ([1.0 2; 3 4], [big"1.0" 2; 3 4])
                m = AxisArray(data, (2:3, 3:4))
                x = @inferred(qr(m, Val(pivot)));

                @test keys.(axes(x.Q)) == (2:3, 1:2)
                @test keys.(axes(x.R)) == (1:2, 3:4)
                @test keys.(axes(x.Q * x.R)) == (2:3, 3:4)
                @test x.factors == qr(parent(m), Val(pivot)).factors

                pivot && @testset "pivoted" begin
                    @test x isa QRPivoted
                    @test x.p == [3, 2]
                    @test keys.(axes(x.P)) == (2:3, 2:3)
                    @test keys.(axes(x.P * m)) == (2:3, 3:4)
                    @test m[x.p,:] == parent(m)[parent(x).p,:]
                end
            end
        end
    end

    @testset "diagonal" begin
        m = AxisArray([1 2 3; 4 5 6; 7 8 9], ["a", "b", "c"], nothing);
        @test @inferred(diag(m)) == diag(parent(m))
        @test @inferred(diag(m, 1)) == diag(parent(m), 1)
        @test @inferred(diag(m, 1; dim=Val(2))) == diag(parent(m), 1)
    end
end

#= TODO figure allocations
@testset "allocations: matmul names" begin
    @test 0 == @allocated (() -> matrix_prod_names((:foo, :bar), (:bar,)))()
    @test 0 == @allocated (() -> symmetric_names((:foo, :bar), 1))()
end
=#

@testset "Mutmul with special types" begin
    a = AxisArray(ones(5,5), (2:6, 3:7))
    @testset "$T" for T in (Diagonal, Symmetric, Tridiagonal, SymTridiagonal, BitArray,)
        x = T(ones(5,5))
        @test keys.(axes(x * a)) == (1:5, 3:7)
        @test keys.(axes(a * x)) == (2:6, 1:5)
    end
end

@testset "inv" begin
    a = AxisArray([1.0 2; 3 4], (2:3, 4:5));
    @test keys.(axes(inv(a))) == (4:5, 2:3)
    @test a * inv(a) ≈ [1.0 0; 0 1]
    @test keys.(axes(a * inv(a))) == (2:3, 2:3)

    @test inv(a) * a ≈ [1.0 0; 0 1]
    @test keys.(axes(inv(a) * a)) == (4:5, 4:5)
end

@testset "cov/cor" begin
    @testset "$f" for f in (cov, cor)
        @testset "matrix input, matrix result" begin
            A = rand(3, 5)
            a = AxisArray(A, (2:4, 2:6))
            @test f(a; dims=1) == f(A, dims=1)
            @test keys.(axes(f(a; dims=1))) == (2:6, 2:6)
            @test keys.(axes(f(a, dims=2))) == (2:4, 2:4)
            @test keys.(axes(f(a, dims=3))) == (2:4, 2:4)
            @test keys.(axes(f(a[:,2:4], dims=3))) == (2:4, 2:4)
        end
        @testset "vector input, scalar result" begin
            v = rand(4)
            av = AxisArray(v, (2:5,))
            @test f(av) isa Number
            @test f(av) == f(v)
        end
    end
    @testset "cov corrected=$bool" for bool in (true, false)
        # test that kwargs get passed on correctly
        A = rand(2, 4)
        a = AxisArray(A)
        @test cov(a; corrected=bool) == cov(A; corrected=bool)
        @test cov(a; corrected=bool, dims=2)  == cov(A; corrected=bool, dims=2)
    end
end


using DoubleDouble
using Base.Test

x = sqrt(2.0)
bx = big(x)
sx = Single(x)
dx = Double(x)

y = 0.1
by = big(y)
sy = Single(y)
dy = Double(y)

@test x == sx == dx
@test y == sy == dy

@test zero(Double{Float64}) == Double(0.0, 0.0)
@test one(Double{Float64}) == Double(1.0, 0.0)
@test zero(Double(0.0, 0.0)) == Double(0.0, 0.0)
@test one(Double(0.0, 0.0)) == Double(1.0, 0.0)

dxy = dx*dy
bxy = bx*by
@test sx*sy == dxy
@test x*y == Float64(dxy)
@test dxy == Double(bxy)

@test x+y == Float64(dx+dy)
@test dx+dy == Double(bx+by)

@test x-y == Float64(dx-dy)
@test dx-dy == Double(bx-by)

@test x/y == Float64(dx/dy)
@test dx/dy == Double(bx/by)

@test sqrt(y) == Float64(sqrt(dy))
@test sqrt(dy) == Double(sqrt(by))

#@test rem(dxy,1.0) == Double(rem(bxy,1.0))

## New
@test Double(pi) == Double{Float64}(3.141592653589793, 1.2246467991473532e-16)
@test Double(3.5) == Double{Float64}(3.5, 0.0)
@test Double(3.5) == Double{Float64}(3.5, 0.0)

a = Double(big"3.1")
@test a == Double(3.1, -8.881784197001253e-17)
@test BigFloat(a) == big"3.099999999999999999999999999999995069619342368676216176696466982586064542459781"

@test Single(3) === Single(3.0)
@test Double(3) === Double(3.0, 0.0)
@test Double(big(3)) === Double(3.0, 0.0)

@test convert(Single{Float64}, 1) === Single(1.0)
@test convert(Single{Float32}, 1) === Single(1.0f0)
@test convert(Double{Float64}, 1) === Double(1.0, 0.0)
@test convert(Double{Float32}, 1) === Double(1.0f0, 0.0f0)

@test Single{Float32}(3) === Single{Float32}(3.0f0)
@test Double{Float32}(3) === Double{Float32}(3.0f0, 0.0f0)
@test Single{Float32}(BigFloat(3)) === Single{Float32}(3.0f0)
@test Double{Float32}(BigFloat(3)) === Double{Float32}(3.0f0, 0.0f0)


# model.jl

"""
    abstract type AbstractModel end

Implements a statistical model. Has so far only SDEModel as subtype.
"""
abstract type AbstractModel end

"""
    struct SDEModel{T<:Union{Float64, <:Function}} <: AbstractModel
        σ::T
        beginvalue::Float64
        endtime::Float64
        Δ::Float64
    end

# Constructors

```julia
    SDEModel(σ::Q, beginvalue::R, endtime::S, Δ::T) where {Q<:Real, R<:Real,
        S<:Real, T<:Real}
    SDEModel(σ::Q, beginvalue::R, endtime::S, Δ::T) where {Q<:Function, R<:Real,
        S<:Real, T<:Real}
```

Implements an SDE model with begin value X_0=beginvalue (at time zero) variance
σ, up to end time endtime. The SDE is discretised with precision Δ.

# Warning
It is assumed that
for every b under consideration, the laws of dX_t=b(X_t)dt+σ(X_t)dW_t are
equivalent.

# Example

```julia
SDEModel(1.0, 0.0, 10.0, 0.01)
# Implements the model dX_t=b(X_t)dt+σ(X_t)dW_t, with X_0=0.0, up to time 10,
# with precision 0.01.
#
SDEModel(identity, 0.0, 100.0, 0.1)
# Implements the model dX_t=b(X_t)+X_tdW_t, with X_0=0.0, up to time 100,
# discretised with precision 0.1.
```
"""
struct SDEModel{T} <: AbstractModel where T<:Union{Float64, <:Function}
    σ::T
    beginvalue::Float64
    endtime::Float64
    Δ::Float64

    function SDEModel(σ::Q, beginvalue::Float64, endtime::Float64, Δ::Float64) where {
            Q<:Union{Float64, <:Function}}
        endtime > 0 || throw(ArgumentError("Endtime should be positive."))
        Δ > 0 || throw(ArgumentError("Δ should be positive."))
        new{typeof(σ)}(σ, beginvalue, endtime, Δ)
    end
end


module MultistartOptimization

export MinimizationProblem, LocationValue, NLoptLocalMethod, local_minimization, TikTak,
    multistart_minimization

using ArgCheck: @argcheck
using Base.Threads: @spawn, fetch
using DocStringExtensions: FIELDS, SIGNATURES, TYPEDEF
using NLopt: NLopt
using Parameters: @unpack
using Sobol: SobolSeq, Sobol

####
#### structures for the problem and results
####

"""
$(TYPEDEF)

Wrapper for a minimization problem.

# Fields

$(FIELDS)
"""
struct MinimizationProblem{F,T}
    "The function to be minimized."
    objective::F
    "Lower bounds (a vector of real numbers)."
    lower_bounds::T
    "Upper bounds (a vector of real numbers)."
    upper_bounds::T
    # FIXME constructor checks
end

"""
$(TYPEDEF)

A location-value pair.

# Fields

$(FIELDS)
"""
struct LocationValue{T <: AbstractVector{<:Real}, S <: Real}
    "Location (a vector of real numbers)."
    location::T
    "The value of the objective at `location`."
    value::S
end

####
#### internal utilities
####

"""
$(SIGNATURES)

Evaluate and return points of an `N`-element Sobol sequence.

An effort is made to parallelize the code using `Threads` when available.
"""
function sobol_starting_points(minimization_problem::MinimizationProblem, N::Integer)
    @unpack objective, lower_bounds, upper_bounds = minimization_problem
    s = SobolSeq(lower_bounds, upper_bounds)
    skip(s, N)                  # better uniformity
    map(fetch, map(x -> @spawn(LocationValue(x, objective(x))), Iterators.take(s, N)))
end

"""
$(SIGNATURES)

Helper function to keep the `N` points with the lowest `value`.
"""
function _keep_lowest(xs, N)
    @argcheck 1 ≤ N ≤ length(xs)
    partialsort(xs, 1:N, by = p -> p.value)
end

####
#### local minimization
####

Base.@kwdef struct NLoptLocalMethod
    algorithm::NLopt.Algorithm
    xtol_abs::Float64 = 1e-8
    xtol_rel::Float64 = 1e-8
    maxeval::Int = 0
    maxtime::Float64 = 0.0
end

"""
Solve `minimization_problem` using `local_method`, starting from `x`. Return a
`LocationValue`.
"""
function local_minimization(opt, x)
    optf, optx, ret = NLopt.optimize(opt, x)
    # FIXME ret is ignored, save it? is it useful?
    LocationValue(optx, optf)
end

####
#### multistart minimization
####

struct TikTak
    quasirandom_N::Int
    initial_N::Int
    θ_min::Float64
    θ_max::Float64
    θ_pow::Float64
end

"""
$(SIGNATURES)

The “TikTak” multistart method, as described in *Arnoud, Guvenen, and Kleineberg (2019)*.

This implements the *multistart* part, can be called with arbitrary local methods, see
[`multistart_minimization`](@ref).

# Arguments

- `quasirandom_N`: the number of quasirandom points for the first pass (using a Sobol
  sequence).

# Keyword arguments

- `keep_ratio`: the fraction of best quasirandom points which are kept

- `θ_min` and `θ_max` clamp the weight parameter, `θ_pow` determines the power it is raised
  to.

The defaults are from the paper cited above.
"""
function TikTak(quasirandom_N; keep_ratio = 0.1, θ_min = 0.1, θ_max = 0.995, θ_pow = 0.5)
    @argcheck 0 < keep_ratio ≤ 1
    TikTak(quasirandom_N, ceil(keep_ratio * quasirandom_N), θ_min, θ_max, θ_pow)
end

function _weight_parameter(t::TikTak, i)
    @unpack initial_N, θ_min, θ_max, θ_pow = t
    clamp((i / initial_N)^θ_pow, θ_min, θ_max)
end

"""
$(SIGNATURES)

Solve `minimization_problem` by using `local_method` within `multistart_method`.
"""
function multistart_minimization(multistart_method::TikTak, local_method,
                                 minimization_problem)
    @unpack quasirandom_N, initial_N, θ_min, θ_max, θ_pow = multistart_method
    quasirandom_points = sobol_starting_points(minimization_problem, quasirandom_N)
    initial_points = _keep_lowest(quasirandom_points, initial_N)
    function _step(visited_minimum, (i, initial_point))
        θ = _weight_parameter(multistart_method, i)
        x = @. (1 - θ) * initial_point.location + θ * visited_minimum.location
        local_minimum = local_minimization(local_method, minimization_problem, x)
        local_minimum.value < visited_minimum.value ? local_minimum : visited_minimum
    end
    foldl(_step, enumerate(initial_points); init = first(initial_points))
end

end # module


function palindrome_permutation(str :: ASCIIString)
  """
  Convert a character to a number according
  to the corresponding shift from 'a'
  """
  function char_to_int(ch :: Char)
    a :: Int = int('a')
    ch :: Int = int(ch)
    if a <= ch <= int('z')
      return ch - a
    else
      return -1
    end
  end

  """
  Toggle the bit at a specific index in a bit vector
  """
  function toggle_bit(bit_vector :: Int, idx :: Int)
    if idx <= 0
      return bit_vector
    end
    return bit_vector $= 1 << idx
  end

  """
  Create a bit vecotor for a given string
  """
  function create_bit_vector(str :: ASCIIString)
    bit_vector :: Int = 0
    for ch in str
      bit_vector = toggle_bit(bit_vector, char_to_int(ch))
    end
    return bit_vector
  end

  """
  Check whether a bit vecotr has only one bit
  """
  function has_one_bit(bit_vector :: Int)
    ((bit_vector - 1) & bit_vector) == 0
  end

  bv :: Int  = create_bit_vector(lowercase(str))

  return (bv == 0) || has_one_bit(bv) # satisfies either all even or only one odd
end

# Time  : O(n)
# Space : O(1)

using Base: Test

@test palindrome_permutation("Tact Coa") == true
@test palindrome_permutation("Tact Coad") == false


module PlusMinus

export plusminus, ±, minusplus, ∓

plusminus(x::Number,y::Number) = (x+y,x-y)
±(x,y) = plusminus(x,y)

minusplus(x::Number,y::Number) = (x-y,x+y)
∓(x,y) = minusplus(x,y)

end # module


if haskey(ENV, "CI") && ENV["CI"] == "true"
    ENV["PYTHON"] = ""
    using Pkg
    Pkg.build("PyCall")
end

using PyPlot
using Glob
using Oceananigans

using ConvergenceTests
using ConvergenceTests.ForcedFlowFreeSlip: u

arch = CUDA.has_cuda() ? GPU() : CPU()

defaultcolors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
removespine(side) = gca().spines[side].set_visible(false)
removespines(sides...) = [removespine(side) for side in sides]

xy_filenames = glob("forced_free_slip_xy*.jld2", joinpath(@__DIR__, "data"))
xz_filenames = glob("forced_free_slip_xz*.jld2", joinpath(@__DIR__, "data"))

xy_errors = ConvergenceTests.compute_errors((x, y, z, t) -> u(x, y, t), xy_filenames...)
xz_errors = ConvergenceTests.compute_errors((x, y, z, t) -> u(x, z, t), xz_filenames...)

sizes = ConvergenceTests.extract_sizes(xy_filenames...)

Nx = map(sz -> sz[1], sizes)

names = (L"(x, y)", L"(x, z)")
errors = (xy_errors, xz_errors)

close("all")
fig, ax = subplots()

for i = 1:length(errors)

    error = errors[i]
    name = names[i]

    L₁ = map(err -> err.L₁, error)
    L∞ = map(err -> err.L∞, error)

    loglog(Nx, L₁, color=defaultcolors[i], alpha=0.6, mfc="None",
           linestyle="None", marker="o", label="\$L_1\$-norm, $name")
    loglog(Nx, L∞, color=defaultcolors[i], alpha=0.6, mfc="None",
           linestyle="None", marker="^", label="\$L_\\infty\$-norm, $name")
end

L₁ = map(err -> err.L₁, errors[1])
loglog(Nx, L₁[end] * (Nx[end] ./ Nx).^2, "k-", linewidth=1, alpha=0.6, label=L"\sim N_x^{-2}")

legend()
xscale("log", base=2)
yscale("log", base=10)
xlabel(L"N_x")
ylabel("Norms of the absolute error, \$ | u_{\\mathrm{sim}} - u_{\\mathrm{exact}} | \$")
title("Convergence for forced free slip")
removespines("top", "right")

filename = "forced_free_slip_convergence_$(typeof(arch)).png"
filepath = joinpath(@__DIR__, "figs", filename)
mkpath(dirname(filepath))
savefig(filepath, dpi=480)

p = sortperm(Nx)
for (name, error) in zip(names, errors)
    L₁ = map(e -> e.L₁, error)
    L∞ = map(e -> e.L∞, error)
    name = "Forced flow free slip " * strip(name.s, '$')
    test_rate_of_convergence(L₁[p], Nx[p], expected=-2.0, atol=0.001, name=name * " L₁")
    test_rate_of_convergence(L∞[p], Nx[p], expected=-2.0, atol=0.005, name=name * " L∞")
end