# BBM-BBM system

```julia
include("setup.jl")
```

# Periodic boundary conditions

```julia
function relaxation_functional(γ, ηunew, ηuold, param)
    @unpack D, tmp1 = param
    ηnew = ηunew.x[1]
    unew = ηunew.x[2]
    ηold = ηuold.x[1]
    uold = ηuold.x[2]

    @. tmp1 = ((1-γ)*ηold + γ*ηnew)^2 + ((1-γ)*ηold + γ*ηnew + 1) * ((1-γ)*uold + γ*unew)^2
    energy = integrate(tmp1, D)
end
function relaxation_functional(ηu, param)
    @unpack D, tmp1 = param
    η = ηu.x[1]
    u = ηu.x[2]

    @. tmp1 = η^2 + (η + 1) * u^2
    integrate(tmp1, D)
end

function relaxation!(integrator)
    told = integrator.tprev
    ηuold = integrator.uprev
    tnew = integrator.t
    ηunew = integrator.u

    γ = one(tnew)
    terminate_integration = false
    γlo = one(γ)/2
    γhi = 3*one(γ)/2
    energy_old = relaxation_functional(ηuold, integrator.p)
    if (relaxation_functional(γlo, ηunew, ηuold, integrator.p)-energy_old) * (relaxation_functional(γhi, ηunew, ηuold, integrator.p)-energy_old) > 0
        terminate_integration = true
    else
        γ = find_zero(g -> relaxation_functional(g, ηunew, ηuold, integrator.p)-energy_old, (γlo, γhi), Roots.AlefeldPotraShi())
    end
    if γ < eps(typeof(γ))
        terminate_integration = true
    end

#     println(γ)
    @. ηunew = ηuold + γ * (ηunew - ηuold)
    DiffEqBase.set_u!(integrator, ηunew)
    if !(tnew ≈ top(integrator.opts.tstops))
        tγ = told + γ * (tnew - told)
        DiffEqBase.set_t!(integrator, tγ)
    end

    if terminate_integration
        terminate!(integrator)
    end

    nothing
end

function save_func_bbm_bbm_periodic(ηu, t, integrator)
    @unpack D, tmp1 = integrator.p
    η = ηu.x[1]
    u = ηu.x[2]

    mass_η = integrate(η, D)
    mass_u = integrate(u, D)
    @. tmp1 = η^2 + (1 + η) * u^2
    energy = integrate(tmp1, D)

    SVector(mass_η, mass_u, energy)
end

function bbm_bbm_periodic!(dηu, ηu, param, t)
    @unpack D, invImD2, D_ImD2, tmp1, tmp2 = param
    η = ηu.x[1]
    u = ηu.x[2]
    dη = dηu.x[1]
    du = dηu.x[2]

    # energy and mass conservative semidiscretization
    @. tmp1 = -(u + η * u)
    if D_ImD2 === nothing
        ldiv!(tmp2, invImD2, tmp1)
        mul!(dη, D, tmp2)
    else
        mul!(dη, D_ImD2, tmp1)
    end

    @. tmp1 = -(η + 0.5 * u^2)
    if D_ImD2 === nothing
        ldiv!(tmp2, invImD2, tmp1)
        mul!(du, D, tmp2)
    else
        mul!(du, D_ImD2, tmp1)
    end

    nothing
end

function solve_ode_bbm_bbm_periodic(D, D2, tspan, alg, tol, dt, adaptive)
    invImD2 = isa(D2, AbstractMatrix) ? factorize(I - D2) : I - D2
    D_ImD2 = if isa(D2, AbstractMatrix)
        nothing
    else
        D / (I - D2)
    end

    x = grid(D)
    η0 = ηsol.(tspan[1], x, x[1], -x[1])
    u0 = usol.(tspan[1], x, x[1], -x[1])
    ηu0 = ArrayPartition(η0, u0)
    tmp1 = similar(u0); tmp2 = similar(tmp1)
    param = (D=D, invImD2=invImD2, D_ImD2=D_ImD2, tmp1=tmp1, tmp2=tmp2)

    ode = ODEProblem(bbm_bbm_periodic!, ηu0, tspan, param)

    saveat = range(tspan..., length=100)
    saved_values_baseline = SavedValues(eltype(D), SVector{3,eltype(D)})
    saving_baseline = SavingCallback(save_func_bbm_bbm_periodic, saved_values_baseline, saveat=saveat)
    saved_values_relaxation = SavedValues(eltype(D), SVector{3,eltype(D)})
    saving_relaxation = SavingCallback(save_func_bbm_bbm_periodic, saved_values_relaxation, saveat=saveat)
    relaxation = DiscreteCallback((u,t,integrator) -> true, relaxation!, save_positions=(false,true))
    cb_baseline = CallbackSet(saving_baseline)
    cb_relaxation = CallbackSet(relaxation, saving_relaxation)

    sol_relaxation = solve(ode, alg, abstol=tol, reltol=tol, dt=dt, adaptive=adaptive, save_everystep=false,
        callback=cb_relaxation, tstops=saveat)
    sol_baseline = solve(ode, alg, abstol=tol, reltol=tol, dt=dt, adaptive=adaptive, save_everystep=false,
        callback=cb_baseline, tstops=saveat)

    ηnum_baseline   = sol_baseline[end].x[1]
    unum_baseline   = sol_baseline[end].x[2]
    ηnum_relaxation = sol_relaxation[end].x[1]
    unum_relaxation = sol_relaxation[end].x[2]
    ηana = ηsol.(tspan[end], x, x[1], -x[1])
    uana = usol.(tspan[end], x, x[1], -x[1])
    @printf("Error in η (baseline):   %.3e\n", integrate(u->u^2, ηnum_baseline - ηana, D) |> sqrt)
    @printf("Error in η (relaxation): %.3e\n", integrate(u->u^2, ηnum_relaxation - ηana, D) |> sqrt)
    @printf("Error in u (baseline):   %.3e\n", integrate(u->u^2, unum_baseline - uana, D) |> sqrt)
    @printf("Error in u (relaxation): %.3e\n", integrate(u->u^2, unum_relaxation - uana, D) |> sqrt)
    @printf("Difference of baseline and relaxation in η: %.3e\n",
        integrate(u->u^2, ηnum_baseline - ηnum_relaxation, D) |> sqrt)
    @printf("Difference of baseline and relaxation in u: %.3e\n",
        integrate(u->u^2, unum_baseline - unum_relaxation, D) |> sqrt)

    sleep(0.1)
    fig_η, ax = plt.subplots(1, 1)
    plt.plot(x, η0, label=L"\eta^0")
    plt.plot(x, ηana, label=L"$\eta^\mathrm{ana}$")
    plt.plot(x, ηnum_baseline, label=L"$\eta^\mathrm{num}$ (baseline)")
    plt.plot(x, ηnum_relaxation, label=L"$\eta^\mathrm{num}$ (relaxation)")
    plt.xlabel(L"x"); plt.ylabel(L"\eta")
    plt.legend(loc="center left", bbox_to_anchor=(1.0, 0.5));

    fig_u, ax = plt.subplots(1, 1)
    plt.plot(x, u0, label=L"u^0")
    plt.plot(x, uana, label=L"$u^\mathrm{ana}$")
    plt.plot(x, unum_baseline, label=L"$u^\mathrm{num}$ (baseline)")
    plt.plot(x, unum_relaxation, label=L"$u^\mathrm{num}$ (relaxation)")
    plt.xlabel(L"x"); plt.ylabel(L"u")
    plt.legend(loc="center left", bbox_to_anchor=(1.0, 0.5));

    t_baseline = saved_values_baseline.t
    t_relaxation = saved_values_relaxation.t
    mass_η_baseline   = map(x->x[1], saved_values_baseline.saveval)
    mass_η_relaxation = map(x->x[1], saved_values_relaxation.saveval)
    mass_u_baseline   = map(x->x[2], saved_values_baseline.saveval)
    mass_u_relaxation = map(x->x[2], saved_values_relaxation.saveval)
    energy_baseline   = map(x->x[3], saved_values_baseline.saveval)
    energy_relaxation = map(x->x[3], saved_values_relaxation.saveval)

    fig_invariants, ax = plt.subplots(1, 1)
    ax.set_yscale("symlog", linthreshy=1.0e-14)
    plt.plot(t_baseline,   mass_η_baseline   .- mass_η_baseline[1], label=L"$\int \eta$ (baseline)",
                color="#E69F00", linestyle="-")
    plt.plot(t_relaxation, mass_η_relaxation .- mass_η_relaxation[1], label=L"$\int \eta$ (relaxation)",
                color="#56B4E9", linestyle="-")
    plt.plot(t_baseline,   mass_u_baseline   .- mass_u_baseline[1], label=L"$\int u$ (baseline)",
                color="#E69F00", linestyle="--")
    plt.plot(t_relaxation, mass_u_relaxation .- mass_u_relaxation[1], label=L"$\int u$ (relaxation)",
                color="#56B4E9", linestyle="--")
    plt.plot(t_baseline,   energy_baseline   .- energy_baseline[1], label=L"$\int (\eta^2 + (1 + \eta) u^2)$ (baseline)",
                color="#E69F00", linestyle=":")
    plt.plot(t_relaxation, energy_relaxation .- energy_relaxation[1], label=L"$\int (\eta^2 + (1 + \eta) u^2)$ (relaxation)",
                color="#56B4E9", linestyle=":")
    plt.xlabel(L"t"); plt.ylabel("Change of Invariants")
    plt.legend(loc="center left", bbox_to_anchor=(1.0, 0.5))

    fig_η, fig_u, fig_invariants
end

```

```julia
# # (unphysical) traveling wave solution of Chen
# xmin = -35.
# xmax = -xmin
# get_c() = 5/2
# function ηsol(t, x, xmin, xmax)
#     α = sqrt(1/6)
#     c = get_c()
#     ρ = 18/5
#     x_t = mod(x - c*t - xmin, xmax - xmin) + xmin

#     -1 + c^2*ρ^2/81 + 5*c^2*ρ^2/108 * (2 / cosh(sqrt(ρ)/2*α*x_t)^2 - 3 / cosh(sqrt(ρ)/2*α*x_t)^4)
# end
# function usol(t, x, xmin, xmax)
#     α = sqrt(1/6)
#     c = get_c()
#     ρ = 18/5
#     x_t = mod(x - c*t - xmin, xmax - xmin) + xmin

#     c * (1 - 5*ρ/18) + 5*c*ρ/6 / cosh(sqrt(ρ)/2*α*x_t)^2
# end

# traveling wave solution with η>-1 obtained numerically
c, data = open("bbm_bbm_traveling_wave_init_c12_l90_N65536.txt", "r") do io
    line = readline(io)
    line = readline(io)
    line = readline(io)
    c = parse(Float64, line[7:end])
    data = readdlm(io, comments=true)
    c, data
end
x = range(data[1,1], data[end,1], length=size(data,1))
xmin = x[1]; xmax = x[end] + (x[2] - x[1])
η0 = data[:, 2]
u0 = data[:, 3]
η0itp = CubicSplineInterpolation((x,), η0, extrapolation_bc=Periodic())
u0itp = CubicSplineInterpolation((x,), u0, extrapolation_bc=Periodic())
get_c() = c
function ηsol(t, x, xmin, xmax)
    c = get_c()
    x_t = mod(x - c*t - xmin, xmax - xmin) + xmin
    η0itp(x_t)
end
function usol(t, x, xmin, xmax)
    c = get_c()
    x_t = mod(x - c*t - xmin, xmax - xmin) + xmin
    u0itp(x_t)
end

println("c = ", get_c())
println("xmin = ", xmin)
println("xmax = ", xmax)
@show ηsol(0., xmax, xmin, xmax)
@show usol(0., xmax, xmin, xmax)
@show N = 2^9
@show dt = 0.5 * (xmax - xmin) / (N * get_c())

# @show tspan = (0., (xmax-xmin)/(3*get_c()))
# @show tspan = (0.0, (xmax-xmin)/(3*get_c()) + 1*(xmax-xmin)/get_c())
# @show tspan = (0.0, (xmax-xmin)/(3*get_c()) + 10*(xmax-xmin)/get_c())
@show tspan = (0.0, (xmax-xmin)/(3*get_c()) + 50*(xmax-xmin)/get_c())
# @show tspan = (0.0, (xmax-xmin)/(3*get_c()) + 100*(xmax-xmin)/get_c())
# @show tspan = (0.0, (xmax-xmin)/(3*get_c()) + 500*(xmax-xmin)/get_c())
flush(stdout)

tol = 1.0e-7
adaptive = false
# D = fourier_derivative_operator(xmin, xmax, N)
# D = periodic_derivative_operator(1, 4, xmin, xmax, N+1)
# D = periodic_derivative_operator(Holoborodko2008(), 1, 4, xmin, xmax, N+1)
# D2 = D^2
# D2 = periodic_derivative_operator(2, 4, xmin, xmax, N+1)
# D2 = periodic_derivative_operator(Holoborodko2008(), 1, 4, xmin, xmax, N+1)^2
p = 4; mesh = UniformPeriodicMesh1D(xmin, xmax, N÷p)
D = couple_continuosly(legendre_derivative_operator(-1., 1., p+1), mesh)
D2 = sparse(D)^2
# D2 = sparse(couple_continuosly(legendre_second_derivative_operator(-1., 1., p+1), mesh))
p = 3; Dop = legendre_derivative_operator(-1., 1., p+1); mesh = UniformPeriodicMesh1D(xmin, xmax, N÷(p+1))
D = couple_discontinuosly(Dop, mesh); D₊ = couple_discontinuosly(Dop, mesh, Val(:plus)); D₋ = couple_discontinuosly(Dop, mesh, Val(:minus))
# D2 = sparse(D)^2
D2 = sparse(D₊) * sparse(D₋)
# D2 = sparse(D₋) * sparse(D₊)

fig_η, fig_u, fig_invariants = solve_ode_bbm_bbm_periodic(D, D2, tspan, RK4(), tol, dt, adaptive)
```

# Convergence study with manufactured solutions

```julia
import SymPy; sp = SymPy

function math_replacements(s)
    s = replace(s, "cos(pi*" => "cospi(")
    s = replace(s, "sin(pi*" => "sinpi(")
end

function ηsol(t, x)
    exp(t) * cospi(2*(x-2t))
end
function usol(t, x)
    exp(t/2) * sinpi(2*(x-t/2))
end

let (t, x) = sp.symbols("t, x", real=true)
    η = ηsol(t, x)
    u = usol(t, x)

    println("η:")
    sp.diff(η, t) - sp.diff(η, x, 2, t, 1) + sp.diff(u, x) + sp.diff(η*u, x) |>
        sp.simplify |> sp.string |> math_replacements |> println

    println("u:")
    sp.diff(u, t) - sp.diff(u, x, 2, t, 1) + sp.diff(η, x) + sp.diff(u^2/2, x) |>
        sp.simplify |> sp.string |> math_replacements |> println
end
```

```julia
function save_func_bbm_bbm_manufactured(ηu, t, integrator)
    @unpack D, tmp1, x = integrator.p
    η = ηu.x[1]
    u = ηu.x[2]

    tmp1 .= ( ηsol.(t, x) .- η ).^2
    error_η = integrate(tmp1, D) |> sqrt
    tmp1 .= ( usol.(t, x) .- u ).^2
    error_u = integrate(tmp1, D) |> sqrt

    SVector(error_η, error_u)
end

function bbm_bbm_periodic_manufactured!(dηu, ηu, param, t)
    @unpack D, invImD2, tmp1, tmp2, x = param
    η = ηu.x[1]
    u = ηu.x[2]
    dη = dηu.x[1]
    du = dηu.x[2]

    # energy and mass conservative semidiscretization
    @. tmp2 = -(u + η * u)
    mul!(tmp1, D, tmp2)
    @. tmp1 += 4*pi^2*(-4*pi*sinpi((4*t - 2*x)) + cospi((4*t - 2*x)))*exp(t) - 2*pi*exp(3*t/2)*sinpi((t - 2*x))*sinpi((4*t - 2*x)) + 2*pi*exp(3*t/2)*cospi((t - 2*x))*cospi((4*t - 2*x)) + 2*pi*exp(t/2)*cospi((t - 2*x)) - 4*pi*exp(t)*sinpi((4*t - 2*x)) + exp(t)*cospi((4*t - 2*x))
    ldiv!(dη, invImD2, tmp1)

    @. tmp2 = -(η + 0.5 * u^2)
    mul!(tmp1, D, tmp2)
    @. tmp1 += -2*pi^2*(sinpi((t - 2*x)) + 2*pi*cospi((t - 2*x)))*exp(t/2) - exp(t/2)*sinpi((t - 2*x))/2 - pi*exp(t/2)*cospi((t - 2*x)) - pi*exp(t)*sinpi((2*t - 4*x)) + 2*pi*exp(t)*sinpi((4*t - 2*x))
    ldiv!(du, invImD2, tmp1)

    nothing
end

function errors_bbm_bbm_periodic_manufactured(ηsol, usol, D, D2, tspan, alg, tol, dt, adaptive)
    invImD2 = isa(D2, AbstractMatrix) ? factorize(I - D2) : I - D2

    x = grid(D)
    η0 = ηsol.(tspan[1], x)
    u0 = usol.(tspan[1], x)
    ηu0 = ArrayPartition(η0, u0)
    tmp1 = similar(u0); tmp2 = similar(tmp1)
    param = (D=D, invImD2=invImD2, tmp1=tmp1, tmp2=tmp2, x=x)

    ode = ODEProblem(bbm_bbm_periodic_manufactured!, ηu0, tspan, param)

    saveat = range(tspan..., length=2)
    saved_values_baseline = SavedValues(eltype(D), SVector{2,eltype(D)})
    saving_baseline = SavingCallback(save_func_bbm_bbm_manufactured, saved_values_baseline, saveat=saveat)
    cb_baseline = CallbackSet(saving_baseline)

    sol_baseline = solve(ode, alg, abstol=tol, reltol=tol, dt=dt, adaptive=adaptive, save_everystep=false,
        callback=cb_baseline, tstops=saveat)

    error_η_baseline = map(x->x[end-1], saved_values_baseline.saveval)
    error_u_baseline = map(x->x[end  ], saved_values_baseline.saveval)

    error_η = error_η_baseline[end]
    error_u = error_u_baseline[end]

    error_η, error_u
end

xmin = 0.0
xmax = 1.0
tspan = (0.0, 1.0)

tol = 1.0e-12

# val_N = round.(Int, 2 .^ range(4, 5, length=2))
# val_N = round.(Int, 2 .^ range(4, 8, length=7)) |> evenodd_values
# val_N = round.(Int, 2 .^ range(3, 4.75, length=7)) |> evenodd_values
# val_N = 2 .^ (2:7) .+ 1
val_N = [128, 256]
val_error_η = Float64[]
val_error_u = Float64[]
for N in val_N
#     D = fourier_derivative_operator(xmin, xmax, N)
#     p = 6; D = periodic_derivative_operator(1, p, xmin, xmax, N+1)
#     p = 4; D = periodic_derivative_operator(Holoborodko2008(), 1, p, xmin, xmax, N+1)
#     D2 = D^2
#     D2 = periodic_derivative_operator(2, p, xmin, xmax, N+1)
#     D2 = periodic_derivative_operator(Holoborodko2008(), 1, p, xmin, xmax, N+1)^2
    p = 3; mesh = UniformPeriodicMesh1D(xmin, xmax, N)
    D = couple_continuosly(legendre_derivative_operator(-1., 1., p+1), mesh)
#     D2 = sparse(D)^2
    D2 = sparse(couple_continuosly(legendre_second_derivative_operator(-1., 1., p+1), mesh))
    p = 6; Dop = legendre_derivative_operator(-1., 1., p+1); mesh = UniformPeriodicMesh1D(xmin, xmax, N)
    D = couple_discontinuosly(Dop, mesh); D₊ = couple_discontinuosly(Dop, mesh, Val(:plus)); D₋ = couple_discontinuosly(Dop, mesh, Val(:minus))
    D2 = sparse(D)^2
#     D2 = sparse(D₊) * sparse(D₋)
#     D2 = sparse(D₋) * sparse(D₊)

    error_η, error_u = errors_bbm_bbm_periodic_manufactured(ηsol, usol, D, D2, tspan, Tsit5(), tol, 1/N, true)
    push!(val_error_η, error_η)
    push!(val_error_u, error_u)
end

fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)
plt.plot(val_N, val_error_η, label=L"$\| \eta - \eta_{\mathrm{ana}} \|$")
c0, c1 = linear_regression(log.(val_N), log.(val_error_η))
plt.plot(val_N, exp(c0) .* val_N.^c1, marker="", linestyle=":", color="gray", label=@sprintf("Order %.2f", -c1))
plt.plot(val_N, val_error_u, label=L"$\| u - u_{\mathrm{ana}} \|$")
c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
plt.plot(val_N, exp(c0) .* val_N.^c1, marker="", linestyle=":", color="gray", label=@sprintf("Order %.2f", -c1))
plt.xscale("log", basex=2)
plt.yscale("symlog", linthreshy=1.0e-12)
plt.legend(loc="center left", bbox_to_anchor=(1.0, 0.5))
# ax.locator_params(axis="y", numticks=9)
```

## Convergence study: Plots FD

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        D = periodic_derivative_operator(1, p, xmin, xmax, N+1)
#         D2 = D^2
        D2 = periodic_derivative_operator(2, p, xmin, xmax, N+1)
        error_η, error_u = errors_bbm_bbm_periodic_manufactured(ηsol, usol, D, D2, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_periodic_manufactured_convergence_FD_narrow"
even_odd_values = evenodd_values
res = Dict{String,Any}()

p = 2
val_N = round.(Int, 2 .^ range(4, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(4, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(4, 8.25, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 8
val_N = round.(Int, 2 .^ range(4, 6.75, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_periodic_manufactured_convergence_FD_narrow"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[4], 2.0*val_error_u[4]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[6], 2.0*val_error_u[6]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[12], 2.0*val_error_u[12]), color="gray")
end

let p = 8
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (0.15*val_N[end], val_error_u[end]), color="gray")
end

ax.set_ylim(1.0e-13, 1.5e0)
ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-13)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        D = periodic_derivative_operator(1, p, xmin, xmax, N+1)
        D2 = D^2
#         D2 = periodic_derivative_operator(2, p, xmin, xmax, N+1)
        error_η, error_u = errors_bbm_bbm_periodic_manufactured(ηsol, usol, D, D2, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_periodic_manufactured_convergence_FD_wide"
even_odd_values = evenodd_values
res = Dict{String,Any}()

p = 2
val_N = round.(Int, 2 .^ range(4, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(4, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(4, 8.25, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 8
val_N = round.(Int, 2 .^ range(4, 6.75, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_periodic_manufactured_convergence_FD_wide"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[4], 2.0*val_error_u[4]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[6], 2.0*val_error_u[6]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[12], 2.0*val_error_u[12]), color="gray")
end

let p = 8
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (0.15*val_N[end], val_error_u[end]), color="gray")
end

ax.set_ylim(1.0e-13, 1.5e0)
ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-13)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

## Convergence study: Plots CG

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        mesh = UniformPeriodicMesh1D(xmin, xmax, N)
        D = couple_continuosly(legendre_derivative_operator(-1., 1., p+1), mesh)
        D2 = sparse(D)^2
#         D2 = sparse(couple_continuosly(legendre_second_derivative_operator(-1., 1., p+1), mesh))
        error_η, error_u = errors_bbm_bbm_periodic_manufactured(ηsol, usol, D, D2, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_periodic_manufactured_convergence_CG_D1c2"
even_odd_values = evenodd_values
res = Dict{String,Any}()

p = 1
val_N = round.(Int, 2 .^ range(6, 12, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 2
val_N = round.(Int, 2 .^ range(5, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 3
val_N = round.(Int, 2 .^ range(4, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(4, 7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 5
val_N = round.(Int, 2 .^ range(2.5, 5.5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(2, 4.75, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_periodic_manufactured_convergence_CG_D1c2"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 1
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[2], 2.0*val_error_u[2]), color="gray")
end

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 0.1*val_error_u[end]), color="gray")
end

let p = 3
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 1.5*val_error_u[end]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 0.1*val_error_u[end]), color="gray")
end

let p = 5
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.2*val_error_u[end]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.2*val_N[end], 0.1*val_error_u[end]), color="gray")
end

ax.set_xlim(2^2-0.5, 2^13-1)
ax.set_ylim(1.0e-13, 1.5e0)
ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-13)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        mesh = UniformPeriodicMesh1D(xmin, xmax, N)
        D = couple_continuosly(legendre_derivative_operator(-1., 1., p+1), mesh)
#         D2 = sparse(D)^2
        D2 = sparse(couple_continuosly(legendre_second_derivative_operator(-1., 1., p+1), mesh))
        error_η, error_u = errors_bbm_bbm_periodic_manufactured(ηsol, usol, D, D2, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_periodic_manufactured_convergence_CG_narrow"
even_odd_values = evenodd_values
res = Dict{String,Any}()

p = 1
val_N = round.(Int, 2 .^ range(6, 12, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 2
val_N = round.(Int, 2 .^ range(5, 8.75, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 3
val_N = round.(Int, 2 .^ range(4, 7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(3, 5.5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 5
val_N = round.(Int, 2 .^ range(2.5, 4.7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(2, 4.25, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_periodic_manufactured_convergence_CG_narrow"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 1
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[2], 2.0*val_error_u[2]), color="gray")
end

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 1.5*val_error_u[end]), color="gray")
end

let p = 3
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.5*val_error_u[end]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 0.15*val_error_u[end]), color="gray")
end

let p = 5
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 0.1*val_error_u[end]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.2*val_N[end], 0.1*val_error_u[end]), color="gray")
end

ax.set_xlim(2^2-0.5, 2^13-1)
ax.set_ylim(1.0e-13, 1.5e0)
ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-13)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

## Convergence study: Plots DG

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        Dop = legendre_derivative_operator(-1., 1., p+1); mesh = UniformPeriodicMesh1D(xmin, xmax, N)
        D = couple_discontinuosly(Dop, mesh); D₊ = couple_discontinuosly(Dop, mesh, Val(:plus)); D₋ = couple_discontinuosly(Dop, mesh, Val(:minus))
        D2 = sparse(D)^2
#         D2 = sparse(D₊) * sparse(D₋)
#         D2 = sparse(D₋) * sparse(D₊)
        error_η, error_u = errors_bbm_bbm_periodic_manufactured(ηsol, usol, D, D2, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_periodic_manufactured_convergence_DG_D1c2"
even_odd_values = evenodd_values
res = Dict{String,Any}()

p = 1
val_N = round.(Int, 2 .^ range(6, 12, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 2
val_N = round.(Int, 2 .^ range(5, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 3
val_N = round.(Int, 2 .^ range(4, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(4, 7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 5
val_N = round.(Int, 2 .^ range(2.5, 5.5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(2.5, 4.75, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_periodic_manufactured_convergence_DG_D1c2"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 1
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[2], 2.0*val_error_u[2]), color="gray")
end

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 0.1*val_error_u[end]), color="gray")
end

let p = 3
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.5*val_N[end], 0.1*val_error_u[end]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 0.1*val_error_u[end]), color="gray")
end

let p = 5
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[1], val_error_u[1]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.2*val_N[end], 0.1*val_error_u[end]), color="gray")
end

ax.set_xlim(2^2-0.5, 2^13-1)
ax.set_ylim(1.0e-13, 1.5e0)
ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-13)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        Dop = legendre_derivative_operator(-1., 1., p+1); mesh = UniformPeriodicMesh1D(xmin, xmax, N)
        D = couple_discontinuosly(Dop, mesh); D₊ = couple_discontinuosly(Dop, mesh, Val(:plus)); D₋ = couple_discontinuosly(Dop, mesh, Val(:minus))
#         D2 = sparse(D)^2
        D2 = sparse(D₊) * sparse(D₋)
#         D2 = sparse(D₋) * sparse(D₊)
        error_η, error_u = errors_bbm_bbm_periodic_manufactured(ηsol, usol, D, D2, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_periodic_manufactured_convergence_DG_D1pD1m"
even_odd_values = evenodd_values
res = Dict{String,Any}()

p = 1
val_N = round.(Int, 2 .^ range(6, 12, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 2
val_N = round.(Int, 2 .^ range(5, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 3
val_N = round.(Int, 2 .^ range(4, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(4, 7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 5
val_N = round.(Int, 2 .^ range(2.5, 5.5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(2.5, 4.75, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_periodic_manufactured_convergence_DG_D1pD1m"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 1
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[2], 2.0*val_error_u[2]), color="gray")
end

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 0.1*val_error_u[end]), color="gray")
end

let p = 3
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.2*val_error_u[end]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.8*val_error_u[end]), color="gray")
end

let p = 5
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.8*val_N[end], 0.1*val_error_u[end]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.2*val_N[end], 0.1*val_error_u[end]), color="gray")
end

ax.set_xlim(2^2-0.5, 2^13-1)
ax.set_ylim(1.0e-13, 1.5e0)
ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-13)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

```julia

```

# Reflecting boundary conditions

```julia
function relaxation_functional(γ, ηunew, ηuold, param)
    @unpack D, tmp1 = param
    ηnew = ηunew.x[1]
    unew = ηunew.x[2]
    ηold = ηuold.x[1]
    uold = ηuold.x[2]

    @. tmp1 = ((1-γ)*ηold + γ*ηnew)^2 + ((1-γ)*ηold + γ*ηnew + 1) * ((1-γ)*uold + γ*unew)^2
    energy = integrate(tmp1, D)
end
function relaxation_functional(ηu, param)
    @unpack D, tmp1 = param
    η = ηu.x[1]
    u = ηu.x[2]

    @. tmp1 = η^2 + (η + 1) * u^2
    integrate(tmp1, D)
end

function relaxation!(integrator)
    told = integrator.tprev
    ηuold = integrator.uprev
    tnew = integrator.t
    ηunew = integrator.u

    γ = one(tnew)
    terminate_integration = false
    γlo = one(γ)/2
    γhi = 3*one(γ)/2
    energy_old = relaxation_functional(ηuold, integrator.p)
    if (relaxation_functional(γlo, ηunew, ηuold, integrator.p)-energy_old) * (relaxation_functional(γhi, ηunew, ηuold, integrator.p)-energy_old) > 0
        terminate_integration = true
    else
        γ = find_zero(g -> relaxation_functional(g, ηunew, ηuold, integrator.p)-energy_old, (γlo, γhi), Roots.AlefeldPotraShi())
    end
    if γ < eps(typeof(γ))
        terminate_integration = true
    end

#     println(γ)
    @. ηunew = ηuold + γ * (ηunew - ηuold)
    DiffEqBase.set_u!(integrator, ηunew)
    if !(tnew ≈ top(integrator.opts.tstops))
        tγ = told + γ * (tnew - told)
        DiffEqBase.set_t!(integrator, tγ)
    end

    if terminate_integration
        terminate!(integrator)
    end

    nothing
end

function save_func_bbm_bbm_reflecting(ηu, t, integrator)
    @unpack D, tmp1 = integrator.p
    η = ηu.x[1]
    u = ηu.x[2]

    mass_η = integrate(η, D)
    mass_u = integrate(u, D)
    @. tmp1 = η^2 + (1 + η) * u^2
    energy = integrate(tmp1, D)

    SVector(mass_η, mass_u, energy)
end

function bbm_bbm_reflecting!(dηu, ηu, param, t)
    @unpack D, invImD2d, invImD2n, tmp1, tmp2 = param
    η = ηu.x[1]
    u = ηu.x[2]
    dη = dηu.x[1]
    du = dηu.x[2]

    # energy and mass(η) conservative semidiscretization
    @. tmp2 = -(u + η * u)
    mul!(tmp1, D, tmp2)
    ldiv!(dη, invImD2n, tmp1)

    @. tmp2 = -(η + 0.5 * u^2)
    mul!(tmp1, D, tmp2)
    ldiv!((@view du[2:end-1]), invImD2d, (@view tmp1[2:end-1]))
    du[1] = du[end] = zero(eltype(du))

    nothing
end

function solve_ode_bbm_bbm_reflecting(D, tspan, alg, tol, dt, adaptive)
    D1 = BandedMatrix(D)
    M = mass_matrix(D)
    D2 = D1^2
    Pd = BandedMatrix((-1=>fill(one(eltype(D1)), size(D1,1)-2),), (size(D1,1), size(D1,1)-2))
    D2d = (D2 * Pd)[2:end-1, :]
    invImD2d = factorize(I - D2d)
    m = diag(M); m[1] = 0; m[end] = 0
    PdM = Diagonal(m)
    invImD2n = factorize(I + inv(M) * D1' * PdM * D1)

    x = grid(D)
    η0 = ηsol.(tspan[1], x, x[1], -x[1])
    u0 = usol.(tspan[1], x, x[1], -x[1])
    u0[1] = u0[end] = 0
    ηu0 = ArrayPartition(η0, u0)
    tmp1 = similar(u0); tmp2 = similar(tmp1)
    param = (D=D, invImD2d=invImD2d, invImD2n=invImD2n, tmp1=tmp1, tmp2=tmp2)

    ode = ODEProblem(bbm_bbm_reflecting!, ηu0, tspan, param)

    saveat = range(tspan..., length=100)
    saved_values_baseline = SavedValues(eltype(D), SVector{3,eltype(D)})
    saving_baseline = SavingCallback(save_func_bbm_bbm_reflecting, saved_values_baseline, saveat=saveat)
    saved_values_relaxation = SavedValues(eltype(D), SVector{3,eltype(D)})
    saving_relaxation = SavingCallback(save_func_bbm_bbm_reflecting, saved_values_relaxation, saveat=saveat)
    relaxation = DiscreteCallback((u,t,integrator) -> true, relaxation!, save_positions=(false,true))
    cb_baseline = CallbackSet(saving_baseline)
    cb_relaxation = CallbackSet(relaxation, saving_relaxation)

    sol_relaxation = solve(ode, alg, abstol=tol, reltol=tol, dt=dt, adaptive=adaptive, save_everystep=false,
        callback=cb_relaxation, tstops=saveat)
    sol_baseline = solve(ode, alg, abstol=tol, reltol=tol, dt=dt, adaptive=adaptive, save_everystep=false,
        callback=cb_baseline, tstops=saveat)

    ηnum_baseline   = sol_baseline[end].x[1]
    unum_baseline   = sol_baseline[end].x[2]
    ηnum_relaxation = sol_relaxation[end].x[1]
    unum_relaxation = sol_relaxation[end].x[2]
    @printf("Difference of baseline and relaxation in η: %.3e\n",
        integrate(u->u^2, ηnum_baseline - ηnum_relaxation, D) |> sqrt)
    @printf("Difference of baseline and relaxation in u: %.3e\n",
        integrate(u->u^2, unum_baseline - unum_relaxation, D) |> sqrt)

    sleep(0.1)
    fig_η, ax = plt.subplots(1, 1)
    ax.plot(x, η0, label=L"\eta^0")
    ax.plot(x, ηnum_baseline, label=L"$\eta$ (baseline)")
    ax.plot(x, ηnum_relaxation, label=L"$\eta$ (relaxation)")
    ax.set_xlabel(L"x"); ax.set_ylabel(L"\eta")
    ax.autoscale(enable=true, axis="x", tight=true)
    ax.legend(loc="center left", bbox_to_anchor=(1.0, 0.5));

    fig_u, ax = plt.subplots(1, 1)
    ax.plot(x, u0, label=L"u^0")
    ax.plot(x, unum_baseline, label=L"$u$ (baseline)")
    ax.plot(x, unum_relaxation, label=L"$u$ (relaxation)")
    ax.set_xlabel(L"x"); ax.set_ylabel(L"u")
    ax.autoscale(enable=true, axis="x", tight=true)
    ax.legend(loc="center left", bbox_to_anchor=(1.0, 0.5));

    t_baseline = saved_values_baseline.t
    t_relaxation = saved_values_relaxation.t
    mass_η_baseline   = map(x->x[1], saved_values_baseline.saveval)
    mass_η_relaxation = map(x->x[1], saved_values_relaxation.saveval)
    mass_u_baseline   = map(x->x[2], saved_values_baseline.saveval)
    mass_u_relaxation = map(x->x[2], saved_values_relaxation.saveval)
    energy_baseline   = map(x->x[3], saved_values_baseline.saveval)
    energy_relaxation = map(x->x[3], saved_values_relaxation.saveval)

    fig_invariants, ax = plt.subplots(1, 1)
    ax.set_yscale("symlog", linthreshy=1.0e-14)
    plt.plot(t_baseline, mass_η_baseline .- mass_η_baseline[1], label=L"$\int \eta$ (baseline)",
                color="#E69F00", linestyle="-")
    plt.plot(t_relaxation, mass_η_relaxation .- mass_η_relaxation[1], label=L"$\int \eta$ (relaxation)",
                color="#56B4E9", linestyle="-")
#     plt.plot(t_baseline, mass_u_baseline .- mass_u_baseline[1], label=L"$\int u$ (baseline)",
#                 color="#E69F00", linestyle="--")
#     plt.plot(t_relaxation, mass_u_relaxation .- mass_u_relaxation[1], label=L"$\int u$ (relaxation)",
#                 color="#56B4E9", linestyle="--")
    plt.plot(t_baseline, energy_baseline .- energy_baseline[1], label=L"$\int (\eta^2 + (1 + \eta) u^2)$ (baseline)",
                color="#E69F00", linestyle=":")
    plt.plot(t_relaxation, energy_relaxation .- energy_relaxation[1], label=L"$\int (\eta^2 + (1 + \eta) u^2)$ (relaxation)",
                color="#56B4E9", linestyle=":")
    plt.xlabel(L"t"); plt.ylabel("Change of Invariants")
    plt.legend(loc="center left", bbox_to_anchor=(1.0, 0.5))

    fig_η, fig_u, fig_invariants
end

```

```julia
# traveling wave solution with η>-1 obtained numerically
# c, data = open("bbm_bbm_traveling_wave_init_c12_l60_N8192.txt", "r") do io
c, data = open("bbm_bbm_traveling_wave_init_c12_l90_N65536.txt", "r") do io
    line = readline(io)
    line = readline(io)
    line = readline(io)
    c = parse(Float64, line[7:end])
    data = readdlm(io, comments=true)
    c, data
end
x = range(data[1,1], data[end,1], length=size(data,1))
xmin = x[1]; xmax = x[end] + (x[2] - x[1])
η0 = data[:, 2]
u0 = data[:, 3]
η0itp = CubicSplineInterpolation((x,), η0, extrapolation_bc=Periodic())
u0itp = CubicSplineInterpolation((x,), u0, extrapolation_bc=Periodic())
get_c() = c
function ηsol(t, x, xmin, xmax)
    c = get_c()
    x_t = mod(x - c*t - xmin, xmax - xmin) + xmin
    η0itp(x_t)
end
function usol(t, x, xmin, xmax)
    c = get_c()
    x_t = mod(x - c*t - xmin, xmax - xmin) + xmin
    u0itp(x_t)
end
println("c = ", c)
println("xmin = ", xmin)
println("xmax = ", xmax)
@show η0itp(xmax)
@show u0itp(xmax)
flush(stdout)

# tspan = (0., (xmax-xmin)/(3*get_c()))
@show tspan = (0.0, (xmax-xmin)/(3*get_c()) + 20*(xmax-xmin)/get_c())
# tspan = (0.0, (xmax-xmin)/(3*get_c()) + 100*(xmax-xmin)/get_c())

tol = 1.0e-7
dt = 0.1
adaptive = false
N = 2^8
D = derivative_operator(MattssonNordström2004(), 1, 6, xmin, xmax, N)
# D = derivative_operator(MattssonAlmquistCarpenter2014Extended(), 1, 6, xmin, xmax, N)
# D = derivative_operator(MattssonAlmquistCarpenter2014Optimal(), 1, 6, xmin, xmax, N)
# D = derivative_operator(MattssonAlmquistVanDerWeide2018Minimal(), 1, 6, xmin, xmax, N)
# D = derivative_operator(MattssonAlmquistVanDerWeide2018Accurate(), 1, 6, xmin, xmax, N)
# p=5; D = couple_continuosly(legendre_derivative_operator(-1., 1., p+1), UniformMesh1D(xmin, xmax, N÷p))
# p=5; D = couple_discontinuosly(legendre_derivative_operator(-1., 1., p+1), UniformMesh1D(xmin, xmax, N÷(p+1)))

fig_η, fig_u, fig_invariants = solve_ode_bbm_bbm_reflecting(D, tspan, RK4(), tol, dt, adaptive)
```

## Plots for the manuscript

```julia
# traveling wave solution with η>-1 obtained numerically
# c, data = open("bbm_bbm_traveling_wave_init_c12_l60_N8192.txt", "r") do io
c, data = open("bbm_bbm_traveling_wave_init_c12_l90_N65536.txt", "r") do io
    line = readline(io)
    line = readline(io)
    line = readline(io)
    c = parse(Float64, line[7:end])
    data = readdlm(io, comments=true)
    c, data
end
x = range(data[1,1], data[end,1], length=size(data,1))
xmin = x[1]; xmax = x[end] + (x[2] - x[1])
η0 = data[:, 2]
u0 = data[:, 3]
η0itp = CubicSplineInterpolation((x,), η0, extrapolation_bc=Periodic())
u0itp = CubicSplineInterpolation((x,), u0, extrapolation_bc=Periodic())
get_c() = c
function ηsol(t, x, xmin, xmax)
    c = get_c()
    x_t = mod(x - c*t - xmin, xmax - xmin) + xmin
    η0itp(x_t)
end
function usol(t, x, xmin, xmax)
    c = get_c()
    x_t = mod(x - c*t - xmin, xmax - xmin) + xmin
    u0itp(x_t)
end
println("c = ", c)
println("xmin = ", xmin)
println("xmax = ", xmax)
@show η0itp(xmax)
@show u0itp(xmax)
flush(stdout)

# tspan = (0., (xmax-xmin)/(3*get_c()))
tspan = (0.0, (xmax-xmin)/(3*get_c()) + 20*(xmax-xmin)/get_c())
# tspan = (0.0, (xmax-xmin)/(3*get_c()) + 100*(xmax-xmin)/get_c())

tol = 1.0e-7
dt = 0.1
adaptive = false
N = 2^8

D = derivative_operator(MattssonNordström2004(), 1, 6, xmin, xmax, N+1)
fig_η, fig_u, fig_invariants = solve_ode_bbm_bbm_reflecting(D, tspan, RK4(), tol, dt, adaptive)
fig_η.axes[1].get_legend() !== nothing && fig_η.axes[1].get_legend().remove()
ax1 = fig_η.axes[1]
x = ax1.lines[1].get_xdata()
η0 = ax1.lines[1].get_ydata()
η_baseline   = ax1.lines[2].get_ydata()
η_relaxation = ax1.lines[3].get_ydata()
ax2 = inset_locator.inset_axes(ax1, width="40%", height="25%", loc=2)
ax2.tick_params(labelleft=false, labelbottom=false)
ax2.plot(x, η0)
ax2.plot(x, η_baseline)
ax2.plot(x, η_relaxation)
ax2.set_ylim(-0.0075, 0.0075)
ax2.set_xlim(-60, -30)
inset_locator.mark_inset(ax1, ax2, loc1=2, loc2=1, fc="none", ec="0.5")
fig_η.savefig("../figures/bbm_bbm_reflecting_eta_classical.pdf", bbox_inches="tight")

D = derivative_operator(MattssonAlmquistVanDerWeide2018Accurate(), 1, 6, xmin, xmax, N+1)
fig_η, fig_u, fig_invariants = solve_ode_bbm_bbm_reflecting(D, tspan, RK4(), tol, dt, adaptive)
plt.figure()
handles, labels = fig_η.axes[1].get_legend_handles_labels()
plt.figlegend(handles, labels, loc="center", ncol=3)
plt.savefig("../figures/bbm_bbm_reflecting_eta_legend.pdf", bbox_inches="tight")
fig_η.axes[1].get_legend() !== nothing && fig_η.axes[1].get_legend().remove()
ax1 = fig_η.axes[1]
x = ax1.lines[1].get_xdata()
η0 = ax1.lines[1].get_ydata()
η_baseline   = ax1.lines[2].get_ydata()
η_relaxation = ax1.lines[3].get_ydata()
ax2 = inset_locator.inset_axes(ax1, width="40%", height="25%", loc=2)
ax2.tick_params(labelleft=false, labelbottom=false)
ax2.plot(x, η0)
ax2.plot(x, η_baseline)
ax2.plot(x, η_relaxation)
ax2.set_ylim(-0.0075, 0.0075)
ax2.set_xlim(-60, -30)
inset_locator.mark_inset(ax1, ax2, loc1=2, loc2=1, fc="none", ec="0.5")
fig_η.savefig("../figures/bbm_bbm_reflecting_eta_accurate.pdf", bbox_inches="tight")
fig_invariants.axes[1].locator_params(axis="y", numticks=6)
fig_invariants.axes[1].ticklabel_format(axis="x", style="scientific", scilimits=(-1,1))
fig_invariants.savefig("../figures/bbm_bbm_reflecting_invariants_accurate.pdf", bbox_inches="tight")

D = derivative_operator(MattssonAlmquistVanDerWeide2018Accurate(), 1, 6, xmin, xmax, N+1)
fig_η, fig_u, fig_invariants = solve_ode_bbm_bbm_reflecting(D, tspan, RK4(), tol, 10*dt, adaptive)
fig_η.axes[1].get_legend() !== nothing && fig_η.axes[1].get_legend().remove()
ax1 = fig_η.axes[1]
x = ax1.lines[1].get_xdata()
η0 = ax1.lines[1].get_ydata()
η_baseline   = ax1.lines[2].get_ydata()
η_relaxation = ax1.lines[3].get_ydata()
ax2 = inset_locator.inset_axes(ax1, width="40%", height="25%", loc=2)
ax2.tick_params(labelleft=false, labelbottom=false)
ax2.plot(x, η0)
ax2.plot(x, η_baseline)
ax2.plot(x, η_relaxation)
ax2.set_ylim(-0.0075, 0.0075)
ax2.set_xlim(-60, -30)
inset_locator.mark_inset(ax1, ax2, loc1=2, loc2=1, fc="none", ec="0.5")
fig_η.savefig("../figures/bbm_bbm_reflecting_eta_accurate_large_dt.pdf", bbox_inches="tight")
```

```julia

```

# Convergence study with manufactured solutions

```julia
import SymPy; sp = SymPy

function math_replacements(s)
    s = replace(s, "cos(pi*" => "cospi(")
    s = replace(s, "sin(pi*" => "sinpi(")
end

function ηsol(t, x)
    exp(2t) * cospi(x)
end
function usol(t, x)
    exp(t) * x * sinpi(x)
end

let (t, x) = sp.symbols("t, x", real=true)
    η = ηsol(t, x)
    u = usol(t, x)

    println("η:")
    sp.diff(η, t) - sp.diff(η, x, 2, t, 1) + sp.diff(u, x) + sp.diff(η*u, x) |>
        sp.simplify |> sp.string |> math_replacements |> println

    println("u:")
    sp.diff(u, t) - sp.diff(u, x, 2, t, 1) + sp.diff(η, x) + sp.diff(u^2/2, x) |>
        sp.simplify |> sp.string |> math_replacements |> println
end
```

```julia
function save_func_bbm_bbm_manufactured(ηu, t, integrator)
    @unpack D, tmp1, x = integrator.p
    η = ηu.x[1]
    u = ηu.x[2]

    tmp1 .= ( ηsol.(t, x) .- η ).^2
    error_η = integrate(tmp1, D) |> sqrt
    tmp1 .= ( usol.(t, x) .- u ).^2
    error_u = integrate(tmp1, D) |> sqrt

    SVector(error_η, error_u)
end

function bbm_bbm_reflecting_manufactured!(dηu, ηu, param, t)
    @unpack D, invImD2d, invImD2n, tmp1, tmp2, x = param
    η = ηu.x[1]
    u = ηu.x[2]
    dη = dηu.x[1]
    du = dηu.x[2]

    # energy and mass(η) conservative semidiscretization
    @. tmp2 = -(u + η * u)
    mul!(tmp1, D, tmp2)
    @. tmp1 += (pi*x*exp(2*t)*cos(2*pi*x) + pi*x*cospi(x) + exp(2*t)*sin(2*pi*x)/2 + 2*exp(t)*cospi(x) + 2*pi^2*exp(t)*cospi(x) + sinpi(x))*exp(t)
    ldiv!(dη, invImD2n, tmp1)

    @. tmp2 = -(η + 0.5 * u^2)
    mul!(tmp1, D, tmp2)
    @. tmp1 += (pi*x^2*exp(t)*sin(2*pi*x)/2 + x*exp(t)*sinpi(x)^2 + x*sinpi(x) + pi*(pi*x*sinpi(x) - 2*cospi(x)) - pi*exp(t)*sinpi(x))*exp(t)
    ldiv!((@view du[2:end-1]), invImD2d, (@view tmp1[2:end-1]))
    du[1] = du[end] = zero(eltype(du))

    nothing
end

function errors_bbm_bbm_reflecting(ηsol, usol, D, tspan, alg, tol, dt, adaptive)
    D1 = BandedMatrix(D)
    M = mass_matrix(D)
    D2 = D1^2
    Pd = BandedMatrix((-1=>fill(one(eltype(D1)), size(D1,1)-2),), (size(D1,1), size(D1,1)-2))
    D2d = (D2 * Pd)[2:end-1, :]
    invImD2d = factorize(I - D2d)
    m = diag(M); m[1] = 0; m[end] = 0
    PdM = Diagonal(m)
    invImD2n = factorize(I + inv(M) * D1' * PdM * D1)

    x = grid(D)
    η0 = ηsol.(tspan[1], x)
    u0 = usol.(tspan[1], x)
    u0[1] = u0[end] = 0
    ηu0 = ArrayPartition(η0, u0)
    tmp1 = similar(u0); tmp2 = similar(tmp1)
    param = (D=D, invImD2d=invImD2d, invImD2n=invImD2n, tmp1=tmp1, tmp2=tmp2, x=x)

    ode = ODEProblem(bbm_bbm_reflecting_manufactured!, ηu0, tspan, param)

    saveat = range(tspan..., length=2)
    saved_values_baseline = SavedValues(eltype(D), SVector{2,eltype(D)})
    saving_baseline = SavingCallback(save_func_bbm_bbm_manufactured, saved_values_baseline, saveat=saveat)
    cb_baseline = CallbackSet(saving_baseline)

    sol_baseline = solve(ode, alg, abstol=tol, reltol=tol, dt=dt, adaptive=adaptive, save_everystep=false,
        callback=cb_baseline, tstops=saveat)

    error_η_baseline = map(x->x[end-1], saved_values_baseline.saveval)
    error_u_baseline = map(x->x[end  ], saved_values_baseline.saveval)

    error_η = error_η_baseline[end]
    error_u = error_u_baseline[end]

    error_η, error_u
end

xmin = 0.0
xmax = 1.0
tspan = (0.0, 1.0)

tol = 1.0e-12

val_N = round.(Int, 2 .^ range(4, 5, length=2))
# val_N = round.(Int, 2 .^ range(4, 9, length=7))
# val_N = round.(Int, 2 .^ range(4, 9, length=7)) |> evenodd_values
# powers = 5:9; val_N = sort(vcat(2 .^ powers, 2 .^ powers .+ 1))
# powers = 2:7; val_N = sort(vcat(2 .^ powers, 2 .^ powers .+ 1))
# val_N = 2 .^ (2:7) .+ 1
val_error_η = Float64[]
val_error_u = Float64[]
for N in val_N
#     D = derivative_operator(MattssonNordström2004(), 1, 6, xmin, xmax, N)
#     D = derivative_operator(MattssonAlmquistCarpenter2014Extended(), 1, 6, xmin, xmax, N)
#     D = derivative_operator(MattssonAlmquistCarpenter2014Optimal(), 1, 6, xmin, xmax, N)
#     D = derivative_operator(MattssonAlmquistVanDerWeide2018Minimal(), 1, 6, xmin, xmax, N)
#     D = derivative_operator(MattssonAlmquistVanDerWeide2018Accurate(), 1, 6, xmin, xmax, N)
#     p=3; D = couple_continuosly(legendre_derivative_operator(-1., 1., p+1), UniformMesh1D(xmin, xmax, N))
    p=2; D = couple_discontinuosly(legendre_derivative_operator(-1., 1., p+1), UniformMesh1D(xmin, xmax, N))

    error_η, error_u = errors_bbm_bbm_reflecting(ηsol, usol, D, tspan, Tsit5(), tol, 1/N, true)
    push!(val_error_η, error_η)
    push!(val_error_u, error_u)
end

fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)
plt.plot(val_N, val_error_η, label=L"$\| \eta - \eta_{\mathrm{ana}} \|$")
c0, c1 = linear_regression(log.(val_N), log.(val_error_η))
plt.plot(val_N, exp(c0) .* val_N.^c1, marker="", linestyle=":", color="gray", label=@sprintf("Order %.2f", -c1))
plt.plot(val_N, val_error_u, label=L"$\| u - u_{\mathrm{ana}} \|$")
c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
plt.plot(val_N, exp(c0) .* val_N.^c1, marker="", linestyle=":", color="gray", label=@sprintf("Order %.2f", -c1))
plt.xscale("log", basex=2)
plt.yscale("symlog", linthreshy=1.0e-12)
plt.legend(loc="center left", bbox_to_anchor=(1.0, 0.5))
# ax.locator_params(axis="y", numticks=9)

val_error_η
```

## Convergence study: Plots FD

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        D = derivative_operator(MattssonAlmquistVanDerWeide2018Accurate(), 1, p, xmin, xmax, N)
        error_η, error_u = errors_bbm_bbm_reflecting(ηsol, usol, D, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_reflecting_convergence_FD_even"
even_odd_values = even_values
res = Dict{String,Any}()

p = 4
val_N = round.(Int, 2 .^ range(4, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(4.1, 9, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 8
val_N = round.(Int, 2 .^ range(4.5, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end

filename = "bbm_bbm_reflecting_convergence_FD_odd"
even_odd_values = odd_values
res = Dict{String,Any}()

p = 4
val_N = round.(Int, 2 .^ range(4, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(4.1, 9, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 8
val_N = round.(Int, 2 .^ range(4.5, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_reflecting_convergence_FD_even"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[4], 2.0*val_error_u[4]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (0.5*val_N[end], 0.2*val_error_u[end]), color="gray")
end

let p = 8
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[end], 0.2*val_error_u[end]), color="gray")
end

ax.set_ylim(5.0e-13, 1.5e-2)
ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-12)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
ax.locator_params(axis="y", numticks=6)
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_reflecting_convergence_FD_odd"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[4], 2.0*val_error_u[4]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (0.5*val_N[end], 0.2*val_error_u[end]), color="gray")
end

let p = 8
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("\$ p = %d \$, EOC %.2f", p, -c1), (val_N[end], 0.2*val_error_u[end]), color="gray")
end

ax.set_ylim(5.0e-13, 1.5e-2)
ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-12)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
ax.locator_params(axis="y", numticks=6)
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

## Convergence study: Plots CG

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        D = couple_continuosly(legendre_derivative_operator(-1., 1., p+1), UniformMesh1D(xmin, xmax, N))
        error_η, error_u = errors_bbm_bbm_reflecting(ηsol, usol, D, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_reflecting_convergence_CG_even"
even_odd_values = even_values
res = Dict{String,Any}()

p = 1
val_N = round.(Int, 2 .^ range(6, 12, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 2
val_N = round.(Int, 2 .^ range(5, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 3
val_N = round.(Int, 2 .^ range(4, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(4, 7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 5
val_N = round.(Int, 2 .^ range(2, 5.5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(1, 4.75, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_reflecting_convergence_CG_even"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 1
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[2], 2.0*val_error_u[2]), color="gray")
end

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.15*val_N[1], val_error_u[1]), color="gray")
end

let p = 3
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.5*val_error_u[end]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.3*val_N[end], 0.15*val_error_u[end]), color="gray")
end

let p = 5
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.5*val_error_u[end]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.2*val_N[end], 0.1*val_error_u[end]), color="gray")
end

ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-12)
ax.set_ylim(5.0e-13, 1.5e-2)
# @show ax.get_xlim()
@show ax.set_xlim(1, 6000)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
ax.locator_params(axis="y", numticks=6)
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")

plt.figure()
handles, labels = ax.get_legend_handles_labels()
plt.figlegend(handles, labels, loc="center", ncol=8)
plt.savefig("../figures/bbm_bbm_reflecting_convergence_Galerkin_legend.pdf", bbox_inches="tight")
```

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        D = couple_continuosly(legendre_derivative_operator(-1., 1., p+1), UniformMesh1D(xmin, xmax, N))
        error_η, error_u = errors_bbm_bbm_reflecting(ηsol, usol, D, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_reflecting_convergence_CG_odd"
even_odd_values = odd_values
res = Dict{String,Any}()

p = 1
val_N = round.(Int, 2 .^ range(6, 12, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 2
val_N = round.(Int, 2 .^ range(5, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 3
val_N = round.(Int, 2 .^ range(4, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(4, 7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 5
val_N = round.(Int, 2 .^ range(2.5, 6, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(1.5, 4.75, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_reflecting_convergence_CG_odd"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 1
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[2], 2.0*val_error_u[2]), color="gray")
end

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.15*val_N[1], val_error_u[1]), color="gray")
end

let p = 3
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.5*val_error_u[end]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.5*val_error_u[end]), color="gray")
end

let p = 5
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (1.2*val_N[end], 0.2*val_error_u[end]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.2*val_N[end], 0.1*val_error_u[end]), color="gray")
end

ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-12)
ax.set_ylim(5.0e-13, 1.5e-2)
# @show ax.get_xlim()
@show ax.set_xlim(1, 6000)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
ax.locator_params(axis="y", numticks=6)
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

## Convergence study: Plots DG

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        D = couple_discontinuosly(legendre_derivative_operator(-1., 1., p+1), UniformMesh1D(xmin, xmax, N))
        error_η, error_u = errors_bbm_bbm_reflecting(ηsol, usol, D, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_reflecting_convergence_DG_even"
even_odd_values = even_values
res = Dict{String,Any}()

p = 1
val_N = round.(Int, 2 .^ range(6, 12, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 2
val_N = round.(Int, 2 .^ range(5, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 3
val_N = round.(Int, 2 .^ range(4, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(3, 7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 5
val_N = round.(Int, 2 .^ range(2, 5.5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(1, 5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_reflecting_convergence_DG_even"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 1
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[2], 0.03*val_error_u[2]), color="gray")
end

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 2.0*val_error_u[end]), color="gray")
end

let p = 3
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[1], 2.0*val_error_u[1]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[6], 2.0*val_error_u[6]), color="gray")
end

let p = 5
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.6*val_N[1], 2.0*val_error_u[1]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.2*val_N[end], 0.1*val_error_u[end]), color="gray")
end

ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-12)
ax.set_ylim(5.0e-13, 1.5e0)
# @show ax.get_xlim()
@show ax.set_xlim(1, 6000)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
ax.locator_params(axis="y", numticks=5)
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

```julia
function do_stuff(p, val_N)
    val_error = Float64[]
    for N in val_N
        D = couple_discontinuosly(legendre_derivative_operator(-1., 1., p+1), UniformMesh1D(xmin, xmax, N))
        error_η, error_u = errors_bbm_bbm_reflecting(ηsol, usol, D, tspan, Tsit5(), tol, 1/N, true)
        push!(val_error, error_η + error_u)
    end
    Dict("val_p$(p)_N" => val_N, "val_p$(p)_error" => val_error)
end

filename = "bbm_bbm_reflecting_convergence_DG_odd"
even_odd_values = odd_values
res = Dict{String,Any}()

p = 1
val_N = round.(Int, 2 .^ range(6, 12, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 2
val_N = round.(Int, 2 .^ range(5, 10, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 3
val_N = round.(Int, 2 .^ range(4, 8, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 4
val_N = round.(Int, 2 .^ range(3, 7, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 5
val_N = round.(Int, 2 .^ range(2, 5.5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

p = 6
val_N = round.(Int, 2 .^ range(1, 5, length=7)) |> even_odd_values
res = merge(res, do_stuff(p, val_N))

open("../data/" * filename * ".json", "w") do io
    JSON.print(io, res, 2)
end
```

```julia
fig, ax = plt.subplots(1, 1)
ax.set_prop_cycle(marker_cycler)

filename = "bbm_bbm_reflecting_convergence_DG_odd"
res = JSON.parsefile("../data/" * filename * ".json")

let p = 1
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[2], 0.03*val_error_u[2]), color="gray")
end

let p = 2
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[end], 2.0*val_error_u[end]), color="gray")
end

let p = 3
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[1], 2.0*val_error_u[1]), color="gray")
end

let p = 4
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (val_N[6], 2.0*val_error_u[6]), color="gray")
end

let p = 5
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.6*val_N[1], 2.0*val_error_u[1]), color="gray")
end

let p = 6
    val_N, val_error_u = res["val_p$(p)_N"], res["val_p$(p)_error"]
    ax.plot(val_N, val_error_u, label="\$ p = $p \$")
    c0, c1 = linear_regression(log.(val_N), log.(val_error_u))
    plt.plot(val_N, exp(c0).*val_N.^c1, marker="", linestyle=":", color="gray")
    plt.annotate(@sprintf("EOC %.2f", -c1), (0.2*val_N[end], 0.1*val_error_u[end]), color="gray")
end

ax.set_xscale("log", basex=2)
ax.set_yscale("symlog", linthreshy=1.0e-12)
ax.set_ylim(5.0e-13, 1.5e0)
# @show ax.get_xlim()
@show ax.set_xlim(1, 6000)
ax.set_xlabel(L"N")
ax.set_ylabel(L"\| \eta - \eta_{\mathrm{ana}} \| + \| u - u_{\mathrm{ana}} \|")
ax.locator_params(axis="y", numticks=5)
fig.savefig("../figures/" * filename * ".pdf", bbox_inches="tight")
```

```julia

```


# Writing Low-Level TensorFlow Code

**Learning Objectives**

 1. Practice defining and performing basic operations on constant Tensors
 2. Use Tensorflow's automatic differentiation capability
 3. Learn how to train a linear regression from scratch with TensorFLow

## Introduction

In this notebook, we will start by reviewing the main operations on Tensors in TensorFlow and understand how to manipulate TensorFlow Variables. We explain how these are compatible with python built-in list and numpy arrays.

Then we will jump to the problem of training a linear regression from scratch with gradient descent. The first order of business will be to understand how to compute the gradients of a function (the loss here) with respect to some of its arguments (the model weights here). The TensorFlow construct allowing us to do that is `tf.GradientTape`, which we will describe.

At last we will create a simple training loop to learn the weights of a 1-dim linear regression using synthetic data generated from a linear model.

As a bonus exercise, we will do the same for data generated from a non linear model, forcing us to manual engineer non-linear features to improve our linear model performance.

Each learning objective will correspond to a __#TODO__  in this student lab notebook -- try to complete this notebook first and then review the [solution notebook](https://github.com/GoogleCloudPlatform/training-data-analyst/blob/master/courses/machine_learning/deepdive2/introduction_to_tensorflow/solutions/write_low_level_code.ipynb)

```python
import numpy as np
from matplotlib import pyplot as plt
import tensorflow as tf
```

```python
print(tf.__version__)
```

    2.3.0

## Operations on Tensors

### Variables and Constants

Tensors in TensorFlow are either contant (`tf.constant`) or variables (`tf.Variable`).
Constant values can not be changed, while variables values can be.

The main difference is that instances of `tf.Variable` have methods allowing us to change
their values while tensors constructed with `tf.constant` don't have these methods, and
therefore their values can not be changed. When you want to change the value of a `tf.Variable`
`x` use one of the following method:

* `x.assign(new_value)`
* `x.assign_add(value_to_be_added)`
* `x.assign_sub(value_to_be_subtracted`

```python
x = tf.constant([2, 3, 4])
x
```

    <tf.Tensor: shape=(3,), dtype=int32, numpy=array([2, 3, 4], dtype=int32)>

```python
x = tf.Variable(2.0, dtype=tf.float32, name='my_variable')
```

```python
x.assign(45.8)
x
```

    <tf.Variable 'my_variable:0' shape=() dtype=float32, numpy=45.8>

```python
x.assign_add(4)
x
```

    <tf.Variable 'my_variable:0' shape=() dtype=float32, numpy=49.8>

```python
x.assign_sub(3)
x
```

    <tf.Variable 'my_variable:0' shape=() dtype=float32, numpy=46.8>

### Point-wise operations

Tensorflow offers similar point-wise tensor operations as numpy does:

* `tf.add` allows to add the components of a tensor
* `tf.multiply` allows us to multiply the components of a tensor
* `tf.subtract` allow us to substract the components of a tensor
* `tf.math.*` contains the usual math operations to be applied on the components of a tensor
* and many more...

Most of the standard arithmetic operations (`tf.add`, `tf.substrac`, etc.) are overloaded by the usual corresponding arithmetic symbols (`+`, `-`, etc.)

**Lab Task #1:** Performing basic operations on Tensors
1. In the first cell, define two constants `a` and `b` and compute their sum in c and d respectively, below using `tf.add` and `+` and verify both operations produce the same values.
2. In the second cell, compute the product of the constants `a` and `b` below using `tf.multiply` and `*` and verify both operations produce the same values.
3. In the third cell, compute the exponential of the constant `a` using `tf.math.exp`. Note, you'll need to specify the type for this operation.

```python
# TODO 1a
a = # TODO -- Your code here.
b = # TODO -- Your code here.
c = # TODO -- Your code here.
d = # TODO -- Your code here.

print("c:", c)
print("d:", d)
```

    c: tf.Tensor([ 8  2 10], shape=(3,), dtype=int32)
    d: tf.Tensor([ 8  2 10], shape=(3,), dtype=int32)

```python
# TODO 1b
a = # TODO -- Your code here.
b = # TODO -- Your code here.
c = # TODO -- Your code here.
d = # TODO -- Your code here.

print("c:", c)
print("d:", d)
```

    c: tf.Tensor([15 -3 16], shape=(3,), dtype=int32)
    d: tf.Tensor([15 -3 16], shape=(3,), dtype=int32)

```python
# TODO 1c
# tf.math.exp expects floats so we need to explicitly give the type
a = # TODO -- Your code here.
b = # TODO -- Your code here.

print("b:", b)
```

    b: tf.Tensor([ 148.41316    20.085537 2980.958   ], shape=(3,), dtype=float32)

### NumPy Interoperability

In addition to native TF tensors, tensorflow operations can take native python types and NumPy arrays as operands.

```python
# native python list
a_py = [1, 2]
b_py = [3, 4]
```

```python
tf.add(a_py, b_py)
```

    <tf.Tensor: shape=(2,), dtype=int32, numpy=array([4, 6], dtype=int32)>

```python
# numpy arrays
a_np = np.array([1, 2])
b_np = np.array([3, 4])
```

```python
tf.add(a_np, b_np)
```

    <tf.Tensor: shape=(2,), dtype=int64, numpy=array([4, 6])>

```python
# native TF tensor
a_tf = tf.constant([1, 2])
b_tf = tf.constant([3, 4])
```

```python
tf.add(a_tf, b_tf)
```

    <tf.Tensor: shape=(2,), dtype=int32, numpy=array([4, 6], dtype=int32)>

You can convert a native TF tensor to a NumPy array using .numpy()

```python
a_tf.numpy()
```

    array([1, 2], dtype=int32)

## Linear Regression

Now let's use low level tensorflow operations to implement linear regression.

Later in the course you'll see abstracted ways to do this using high level TensorFlow.

### Toy Dataset

We'll model the following function:

\begin{equation}
y= 2x + 10
\end{equation}

```python
X = tf.constant(range(10), dtype=tf.float32)
Y = 2 * X + 10

print("X:{}".format(X))
print("Y:{}".format(Y))
```

    X:[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
    Y:[10. 12. 14. 16. 18. 20. 22. 24. 26. 28.]

Let's also create a test dataset to evaluate our models:

```python
X_test = tf.constant(range(10, 20), dtype=tf.float32)
Y_test = 2 * X_test + 10

print("X_test:{}".format(X_test))
print("Y_test:{}".format(Y_test))
```

    X_test:[10. 11. 12. 13. 14. 15. 16. 17. 18. 19.]
    Y_test:[30. 32. 34. 36. 38. 40. 42. 44. 46. 48.]

#### Loss Function

The simplest model we can build is a model that for each value of x returns the sample mean of the training set:

```python
y_mean = Y.numpy().mean()

def predict_mean(X):
    y_hat = [y_mean] * len(X)
    return y_hat

Y_hat = predict_mean(X_test)
```

Using mean squared error, our loss is:
\begin{equation}
MSE = \frac{1}{m}\sum_{i=1}^{m}(\hat{Y}_i-Y_i)^2
\end{equation}

For this simple model the loss is then:

```python
errors = (Y_hat - Y)**2
loss = tf.reduce_mean(errors)
loss.numpy()
```

    33.0

This values for the MSE loss above will give us a baseline to compare how a more complex model is doing.

Now, if $\hat{Y}$ represents the vector containing our model's predictions when we use a linear regression model
\begin{equation}
\hat{Y} = w_0X + w_1
\end{equation}

we can write a loss function taking as arguments the coefficients of the model:

```python
def loss_mse(X, Y, w0, w1):
    Y_hat = w0 * X + w1
    errors = (Y_hat - Y)**2
    return tf.reduce_mean(errors)
```

### Gradient Function

To use gradient descent we need to take the partial derivatives of the loss function with respect to each of the weights. We could manually compute the derivatives, but with Tensorflow's automatic differentiation capabilities we don't have to!

During gradient descent we think of the loss as a function of the parameters $w_0$ and $w_1$. Thus, we want to compute the partial derivative with respect to these variables.

For that we need to wrap our loss computation within the context of `tf.GradientTape` instance which will record gradient information:

```python
with tf.GradientTape() as tape:
    loss = # computation
```

This will allow us to later compute the gradients of any tensor computed within the `tf.GradientTape` context with respect to instances of `tf.Variable`:

```python
gradients = tape.gradient(loss, [w0, w1])
```

We illustrate this procedure by computing the loss gradients with respect to the model weights:

**Lab Task #2:** Complete the function below to compute the loss gradients with respect to the model weights `w0` and `w1`.

```python
# TODO 2
def compute_gradients(X, Y, w0, w1):
    # TODO -- Your code here.
```

```python
w0 = tf.Variable(0.0)
w1 = tf.Variable(0.0)

dw0, dw1 = compute_gradients(X, Y, w0, w1)
```

```python
print("dw0:", dw0.numpy())
```

    dw0: -204.0

```python
print("dw1", dw1.numpy())
```

    dw1 -38.0

### Training Loop

Here we have a very simple training loop that converges. Note we are ignoring best practices like batching, creating a separate test set, and random weight initialization for the sake of simplicity.

**Lab Task #3:** Complete the `for` loop below to train a linear regression.
1. Use `compute_gradients` to compute `dw0` and `dw1`.
2. Then, re-assign the value of `w0` and `w1` using the `.assign_sub(...)` method with the computed gradient values and the `LEARNING_RATE`.
3. Finally, for every 100th step , we'll compute and print the `loss`. Use the `loss_mse` function we created above to compute the `loss`.

```python
# TODO 3
STEPS = 1000
LEARNING_RATE = .02
MSG = "STEP {step} - loss: {loss}, w0: {w0}, w1: {w1}\n"

w0 = tf.Variable(0.0)
w1 = tf.Variable(0.0)

for step in range(0, STEPS + 1):

    dw0, dw1 = # TODO -- Your code here.

    if step % 100 == 0:
        loss = # TODO -- Your code here.
        print(MSG.format(step=step, loss=loss, w0=w0.numpy(), w1=w1.numpy()))

```

    STEP 0 - loss: 35.70719528198242, w0: 4.079999923706055, w1: 0.7599999904632568
    STEP 100 - loss: 2.6017532348632812, w0: 2.4780430793762207, w1: 7.002389907836914
    STEP 200 - loss: 0.26831889152526855, w0: 2.153517961502075, w1: 9.037351608276367
    STEP 300 - loss: 0.027671903371810913, w0: 2.0493006706237793, w1: 9.690855979919434
    STEP 400 - loss: 0.0028539239428937435, w0: 2.0158326625823975, w1: 9.90071964263916
    STEP 500 - loss: 0.0002943490108009428, w0: 2.005084753036499, w1: 9.96811580657959
    STEP 600 - loss: 3.0356444767676294e-05, w0: 2.0016329288482666, w1: 9.989760398864746
    STEP 700 - loss: 3.1322738323069643e-06, w0: 2.0005245208740234, w1: 9.996710777282715
    STEP 800 - loss: 3.2238213520940917e-07, w0: 2.0001683235168457, w1: 9.998944282531738
    STEP 900 - loss: 3.369950718479231e-08, w0: 2.000054359436035, w1: 9.999658584594727
    STEP 1000 - loss: 3.6101481803996194e-09, w0: 2.0000178813934326, w1: 9.99988842010498

Now let's compare the test loss for this linear regression to the test loss from the baseline model that outputs always the mean of the training set:

```python
loss = loss_mse(X_test, Y_test, w0, w1)
loss.numpy()
```

    2.4563633e-08

This is indeed much better!

## Bonus

Try modeling a non-linear function such as: $y=xe^{-x^2}$

```python
X = tf.constant(np.linspace(0, 2, 1000), dtype=tf.float32)
Y = X * tf.exp(-X**2)
```

```python
%matplotlib inline

plt.plot(X, Y)
```

```python
def make_features(X):
    f1 = tf.ones_like(X)  # Bias.
    f2 = X
    f3 = tf.square(X)
    f4 = tf.sqrt(X)
    f5 = tf.exp(X)
    return tf.stack([f1, f2, f3, f4, f5], axis=1)
```

```python
def predict(X, W):
    return tf.squeeze(X @ W, -1)
```

```python
def loss_mse(X, Y, W):
    Y_hat = predict(X, W)
    errors = (Y_hat - Y)**2
    return tf.reduce_mean(errors)
```

```python
def compute_gradients(X, Y, W):
    with tf.GradientTape() as tape:
        loss = loss_mse(Xf, Y, W)
    return tape.gradient(loss, W)
```

```python
STEPS = 2000
LEARNING_RATE = .02

Xf = make_features(X)
n_weights = Xf.shape[1]

W = tf.Variable(np.zeros((n_weights, 1)), dtype=tf.float32)

# For plotting
steps, losses = [], []
plt.figure()

for step in range(1, STEPS + 1):

    dW = compute_gradients(X, Y, W)
    W.assign_sub(dW * LEARNING_RATE)

    if step % 100 == 0:
        loss = loss_mse(Xf, Y, W)
        steps.append(step)
        losses.append(loss)
        plt.clf()
        plt.plot(steps, losses)

print("STEP: {} MSE: {}".format(STEPS, loss_mse(Xf, Y, W)))

```

```python
# The .figure() method will create a new figure, or activate an existing figure.
plt.figure()
# The .plot() is a versatile function, and will take an arbitrary number of arguments. For example, to plot x versus y.
plt.plot(X, Y, label='actual')
plt.plot(X, predict(Xf, W), label='predicted')
# The .legend() method will place a legend on the axes.
plt.legend()
```

Copyright 2021 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License