text:
## Transforms take values at Chebyshev points of the first and second kinds and produce Chebyshev coefficients

abstract type ChebyshevPlan{T} <: Plan{T} end

size(P::ChebyshevPlan) = isdefined(P, :plan) ? size(P.plan) : (0,)
length(P::ChebyshevPlan) = isdefined(P, :plan) ? length(P.plan) : 0

const FIRSTKIND = FFTW.REDFT10
const SECONDKIND = FFTW.REDFT00

struct ChebyshevTransformPlan{T,kind,K,inplace,N,R} <: ChebyshevPlan{T}
    plan::FFTW.r2rFFTWPlan{T,K,inplace,N,R}
    ChebyshevTransformPlan{T,kind,K,inplace,N,R}(plan) where {T,kind,K,inplace,N,R} = new{T,kind,K,inplace,N,R}(plan)
    ChebyshevTransformPlan{T,kind,K,inplace,N,R}() where {T,kind,K,inplace,N,R} = new{T,kind,K,inplace,N,R}()
end

ChebyshevTransformPlan{T,kind,K}(plan::FFTW.r2rFFTWPlan{T,K,inplace,N,R}) where {T,kind,K,inplace,N,R} =
    ChebyshevTransformPlan{T,kind,K,inplace,N,R}(plan)

# jump through some hoops to make inferrable
@inline kindtuple(KIND,N) = ntuple(_ -> KIND,N)
@inline kindtuple(KIND,N,::Integer) = (KIND,)
function plan_chebyshevtransform!(x::AbstractArray{T,N}, ::Val{1}, dims...; kws...) where {T<:fftwNumber,N}
    if isempty(x)
        ChebyshevTransformPlan{T,1,kindtuple(FIRSTKIND,N,dims...),true,N,isempty(dims) ? UnitRange{Int} : typeof(dims)}()
    else
        ChebyshevTransformPlan{T,1,kindtuple(FIRSTKIND,N,dims...)}(FFTW.plan_r2r!(x, FIRSTKIND, dims...; kws...))
    end
end
function plan_chebyshevtransform!(x::AbstractArray{T,N}, ::Val{2}, dims...; kws...) where {T<:fftwNumber,N}
    any(≤(1),size(x)) && throw(ArgumentError("Array must contain at least 2 entries"))
    ChebyshevTransformPlan{T,2,kindtuple(SECONDKIND,N,dims...)}(FFTW.plan_r2r!(x, SECONDKIND, dims...; kws...))
end

function plan_chebyshevtransform(x::AbstractArray{T,N}, ::Val{1}, dims...; kws...) where {T<:fftwNumber,N}
    if isempty(x)
        ChebyshevTransformPlan{T,1,kindtuple(FIRSTKIND,N,dims...),false,N,isempty(dims) ? UnitRange{Int} : typeof(dims)}()
    else
        ChebyshevTransformPlan{T,1,kindtuple(FIRSTKIND,N,dims...)}(FFTW.plan_r2r(x, FIRSTKIND, dims...; kws...))
    end
end
function plan_chebyshevtransform(x::AbstractArray{T,N}, ::Val{2}, dims...; kws...) where {T<:fftwNumber,N}
    any(≤(1),size(x)) && throw(ArgumentError("Array must contain at least 2 entries"))
    ChebyshevTransformPlan{T,2,kindtuple(SECONDKIND,N,dims...)}(FFTW.plan_r2r(x, SECONDKIND, dims...; kws...))
end

plan_chebyshevtransform!(x::AbstractArray, dims...; kws...) = plan_chebyshevtransform!(x, Val(1), dims...; kws...)
plan_chebyshevtransform(x::AbstractArray, dims...; kws...) = plan_chebyshevtransform(x, Val(1), dims...; kws...)

# convert x if necessary
@inline _plan_mul!(y::AbstractArray{T}, P::Plan{T}, x::StridedArray{T}) where T = mul!(y, P, x)
@inline _plan_mul!(y::AbstractArray{T}, P::Plan{T}, x::AbstractArray) where T = mul!(y, P, convert(Array{T}, x))

@inline _cheb1_rescale!(_, y::AbstractVector) = (y[1] /= 2; ldiv!(length(y), y))

@inline function _cheb1_rescale!(d::Number, y::AbstractMatrix{T}) where T
    if isone(d)
        ldiv!(2, view(y,1,:))
    else
        ldiv!(2, view(y,:,1))
    end
    ldiv!(size(y,d), y)
end

# TODO: higher dimensional arrays
@inline function _cheb1_rescale!(d::UnitRange, y::AbstractMatrix{T}) where T
    @assert d == 1:2
    ldiv!(2, view(y,1,:))
    ldiv!(2, view(y,:,1))
    ldiv!(prod(size(y)), y)
end

function *(P::ChebyshevTransformPlan{T,1,K,true,N}, x::AbstractArray{T,N}) where {T,K,N}
    n = length(x)
    n == 0 && return x

    y = P.plan*x # will be  === x if in-place
    _cheb1_rescale!(P.plan.region, y)
end

function mul!(y::AbstractArray{T,N}, P::ChebyshevTransformPlan{T,1,K,false,N}, x::AbstractArray{<:Any,N}) where {T,K,N}
    n = length(x)
    length(y) == n || throw(DimensionMismatch("output must match dimension"))
    n == 0 && return y
    _plan_mul!(y, P.plan, x)
    _cheb1_rescale!(P.plan.region, y)
end

_cheb2_rescale!(_, y::AbstractVector) = (y[1] /= 2; y[end] /= 2; ldiv!(length(y)-1, y))

function _cheb2_rescale!(d::Number, y::AbstractMatrix{T}) where T
    if isone(d)
        ldiv!(2, @view(y[1,:]))
        ldiv!(2, @view(y[end,:]))
    else
        ldiv!(2, @view(y[:,1]))
        ldiv!(2, @view(y[:,end]))
    end
    ldiv!(size(y,d)-1, y)
end

# TODO: higher dimensional arrays
function _cheb2_rescale!(d::UnitRange, y::AbstractMatrix{T}) where T
    @assert d == 1:2
    ldiv!(2, @view(y[1,:]))
    ldiv!(2, @view(y[end,:]))
    ldiv!(2, @view(y[:,1]))
    ldiv!(2, @view(y[:,end]))
    ldiv!(prod(size(y) .- 1), y)
end

function *(P::ChebyshevTransformPlan{T,2,K,true,N}, x::AbstractArray{T,N}) where {T,K,N}
    n = length(x)
    y = P.plan*x # will be  === x if in-place
    _cheb2_rescale!(P.plan.region, y)
end

function mul!(y::AbstractArray{T,N}, P::ChebyshevTransformPlan{T,2,K,false,N}, x::AbstractArray{<:Any,N}) where {T,K,N}
    n = length(x)
    length(y) == n || throw(DimensionMismatch("output must match dimension"))
    _plan_mul!(y, P.plan, x)
    _cheb2_rescale!(P.plan.region, y)
end

*(P::ChebyshevTransformPlan{T,kind,K,false,N}, x::AbstractArray{T,N}) where {T,kind,K,N} =
    mul!(similar(x), P, x)

"""
    chebyshevtransform!(x, kind=Val(1))

transforms from values on a Chebyshev grid of the first or second kind to Chebyshev
coefficients, in-place
"""
chebyshevtransform!(x, dims...; kws...) = plan_chebyshevtransform!(x, dims...; kws...)*x

"""
    chebyshevtransform(x, kind=Val(1))

transforms from values on a Chebyshev grid of the first or second kind to Chebyshev
coefficients.
"""
chebyshevtransform(x, dims...; kws...) = plan_chebyshevtransform(x, dims...; kws...) * x

## Inverse transforms take Chebyshev coefficients and produce values at Chebyshev points of the first and second kinds

const IFIRSTKIND = FFTW.REDFT01

struct IChebyshevTransformPlan{T,kind,K,inplace,N,R} <: ChebyshevPlan{T}
    plan::FFTW.r2rFFTWPlan{T,K,inplace,N,R}
    IChebyshevTransformPlan{T,kind,K,inplace,N,R}(plan) where {T,kind,K,inplace,N,R} = new{T,kind,K,inplace,N,R}(plan)
    IChebyshevTransformPlan{T,kind,K,inplace,N,R}() where {T,kind,K,inplace,N,R} = new{T,kind,K,inplace,N,R}()
end

IChebyshevTransformPlan{T,kind,K}(F::FFTW.r2rFFTWPlan{T,K,inplace,N,R}) where {T,kind,K,inplace,N,R} =
    IChebyshevTransformPlan{T,kind,K,inplace,N,R}(F)

# second kind Chebyshev transforms share a plan with their inverse
# so we support this via inv
inv(P::ChebyshevTransformPlan{T,2,K}) where {T,K} = IChebyshevTransformPlan{T,2,K}(P.plan)
inv(P::IChebyshevTransformPlan{T,2,K}) where {T,K} = ChebyshevTransformPlan{T,2,K}(P.plan)

\(P::ChebyshevTransformPlan, x::AbstractArray) = inv(P) * x
\(P::IChebyshevTransformPlan, x::AbstractArray) = inv(P) * x

function plan_ichebyshevtransform!(x::AbstractArray{T,N}, ::Val{1}, dims...; kws...) where {T<:fftwNumber,N}
    if isempty(x)
        IChebyshevTransformPlan{T,1,kindtuple(IFIRSTKIND,N,dims...),true,N,isempty(dims) ? UnitRange{Int} : typeof(dims)}()
    else
        IChebyshevTransformPlan{T,1,kindtuple(IFIRSTKIND,N,dims...)}(FFTW.plan_r2r!(x, IFIRSTKIND, dims...; kws...))
    end
end

function plan_ichebyshevtransform!(x::AbstractArray{T}, ::Val{2}, dims...; kws...) where T<:fftwNumber
    inv(plan_chebyshevtransform!(x, Val(2), dims...; kws...))
end

function plan_ichebyshevtransform(x::AbstractArray{T,N}, ::Val{1}, dims...; kws...) where {T<:fftwNumber,N}
    if isempty(x)
        IChebyshevTransformPlan{T,1,kindtuple(IFIRSTKIND,N,dims...),false,N,isempty(dims) ? UnitRange{Int} : typeof(dims)}()
    else
        IChebyshevTransformPlan{T,1,kindtuple(IFIRSTKIND,N,dims...)}(FFTW.plan_r2r(x, IFIRSTKIND, dims...; kws...))
    end
end

function plan_ichebyshevtransform(x::AbstractArray{T}, ::Val{2}, dims...; kws...) where T<:fftwNumber
    inv(plan_chebyshevtransform(x, Val(2), dims...; kws...))
end

plan_ichebyshevtransform!(x::AbstractArray, dims...; kws...) = plan_ichebyshevtransform!(x, Val(1), dims...; kws...)
plan_ichebyshevtransform(x::AbstractArray, dims...; kws...) = plan_ichebyshevtransform(x, Val(1), dims...; kws...)

@inline _icheb1_prescale!(_, x::AbstractVector) = (x[1] *= 2)
@inline function _icheb1_prescale!(d::Number, x::AbstractMatrix)
    if isone(d)
        lmul!(2, view(x,1,:))
    else
        lmul!(2, view(x,:,1))
    end
    x
end
@inline function _icheb1_prescale!(d::UnitRange, x::AbstractMatrix)
    lmul!(2, view(x,:,1))
    lmul!(2, view(x,1,:))
    x
end
@inline _icheb1_postscale!(_, x::AbstractVector) = (x[1] /= 2)
@inline function _icheb1_postscale!(d::Number, x::AbstractMatrix)
    if isone(d)
        ldiv!(2, view(x,1,:))
    else
        ldiv!(2, view(x,:,1))
    end
    x
end

@inline function _icheb1_postscale!(d::UnitRange, x::AbstractMatrix)
    ldiv!(2, view(x,1,:))
    ldiv!(2, view(x,:,1))
    x
end

function *(P::IChebyshevTransformPlan{T,1,K,true,N}, x::AbstractArray{T,N}) where {T<:fftwNumber,K,N}
    n = length(x)
    n == 0 && return x

    _icheb1_prescale!(P.plan.region, x)
    x = ldiv!(2^length(P.plan.region), P.plan*x)
    x
end

function mul!(y::AbstractArray{T,N}, P::IChebyshevTransformPlan{T,1,K,false,N}, x::AbstractArray{T,N}) where {T<:fftwNumber,K,N}
    size(y) == size(x) || throw(DimensionMismatch("output must match dimension"))
    isempty(x) && return y

    _icheb1_prescale!(P.plan.region, x) # Todo: don't mutate x
    _plan_mul!(y, P.plan, x)
    _icheb1_postscale!(P.plan.region, x)
    ldiv!(2^length(P.plan.region), y)
end

@inline _icheb2_prescale!(_, x::AbstractVector) = (x[1] *= 2; x[end] *= 2)
@inline function _icheb2_prescale!(d::Number, x::AbstractMatrix)
    if isone(d)
        lmul!(2, @view(x[1,:]))
        lmul!(2, @view(x[end,:]))
    else
        lmul!(2, @view(x[:,1]))
        lmul!(2, @view(x[:,end]))
    end
    x
end
@inline function _icheb2_prescale!(d::UnitRange, x::AbstractMatrix)
    lmul!(2, @view(x[1,:]))
    lmul!(2, @view(x[end,:]))
    lmul!(2, @view(x[:,1]))
    lmul!(2, @view(x[:,end]))
    x
end
@inline _icheb2_postrescale!(_, x::AbstractVector) = (x[1] /= 2; x[end] /= 2)
@inline function _icheb2_postrescale!(d::Number, x::AbstractMatrix)
    if isone(d)
        ldiv!(2, @view(x[1,:]))
        ldiv!(2, @view(x[end,:]))
    else
        ldiv!(2, @view(x[:,1]))
        ldiv!(2, @view(x[:,end]))
    end
    x
end
@inline function _icheb2_postrescale!(d::UnitRange, x::AbstractMatrix)
    ldiv!(2, @view(x[1,:]))
    ldiv!(2, @view(x[end,:]))
    ldiv!(2, @view(x[:,1]))
    ldiv!(2, @view(x[:,end]))
    x
end
@inline function _icheb2_rescale!(d::Number, y::AbstractArray{T}) where T
    _icheb2_prescale!(d, y)
    lmul!(convert(T, size(y,d) - 1)/2, y)
    y
end
@inline function _icheb2_rescale!(d::UnitRange, y::AbstractArray{T}) where T
    _icheb2_prescale!(d, y)
    lmul!(prod(convert.(T, size(y) .- 1)./2), y)
    y
end

function *(P::IChebyshevTransformPlan{T,2,K,true,N}, x::AbstractArray{T,N}) where {T<:fftwNumber,K,N}
    n = length(x)

    _icheb2_prescale!(P.plan.region, x)
    x = inv(P)*x
    _icheb2_rescale!(P.plan.region, x)
end

function mul!(y::AbstractArray{T,N}, P::IChebyshevTransformPlan{T,2,K,false,N}, x::AbstractArray{<:Any,N}) where {T<:fftwNumber,K,N}
    n = length(x)
    length(y) == n || throw(DimensionMismatch("output must match dimension"))

    _icheb2_prescale!(P.plan.region, x)
    _plan_mul!(y, inv(P), x)
    _icheb2_postrescale!(P.plan.region, x)
    _icheb2_rescale!(P.plan.region, y)
end

*(P::IChebyshevTransformPlan{T,kind,K,false,N}, x::AbstractArray{T,N}) where {T,kind,K,N} = mul!(similar(x), P, x)
ichebyshevtransform!(x::AbstractArray, dims...; kwds...) = plan_ichebyshevtransform!(x, dims...; kwds...)*x
ichebyshevtransform(x, dims...; kwds...) = plan_ichebyshevtransform(x, dims...; kwds...)*x

## Chebyshev U

const UFIRSTKIND = FFTW.RODFT10
const USECONDKIND = FFTW.RODFT00

struct ChebyshevUTransformPlan{T,kind,K,inplace,N,R} <: ChebyshevPlan{T}
    plan::FFTW.r2rFFTWPlan{T,K,inplace,N,R}
    ChebyshevUTransformPlan{T,kind,K,inplace,N,R}(plan) where {T,kind,K,inplace,N,R} = new{T,kind,K,inplace,N,R}(plan)
    ChebyshevUTransformPlan{T,kind,K,inplace,N,R}() where {T,kind,K,inplace,N,R} = new{T,kind,K,inplace,N,R}()
end

ChebyshevUTransformPlan{T,kind,K}(plan::FFTW.r2rFFTWPlan{T,K,inplace,N,R}) where {T,kind,K,inplace,N,R} =
    ChebyshevUTransformPlan{T,kind,K,inplace,N,R}(plan)

function plan_chebyshevutransform!(x::AbstractArray{T,N}, ::Val{1}, dims...; kws...) where {T<:fftwNumber,N}
    if isempty(x)
        ChebyshevUTransformPlan{T,1,kindtuple(UFIRSTKIND,N,dims...),true,N,isempty(dims) ? UnitRange{Int} : typeof(dims)}()
    else
        ChebyshevUTransformPlan{T,1,kindtuple(UFIRSTKIND,N,dims...)}(FFTW.plan_r2r!(x, UFIRSTKIND, dims...; kws...))
    end
end
function plan_chebyshevutransform!(x::AbstractArray{T,N}, ::Val{2}, dims...; kws...) where {T<:fftwNumber,N}
    any(≤(1),size(x)) && throw(ArgumentError("Array must contain at least 2 entries"))
    ChebyshevUTransformPlan{T,2,kindtuple(USECONDKIND,N,dims...)}(FFTW.plan_r2r!(x, USECONDKIND, dims...; kws...))
end

function plan_chebyshevutransform(x::AbstractArray{T,N}, ::Val{1}, dims...; kws...) where {T<:fftwNumber,N}
    if isempty(x)
        ChebyshevUTransformPlan{T,1,kindtuple(UFIRSTKIND,N,dims...),false,N,isempty(dims) ? UnitRange{Int} : typeof(dims)}()
    else
        ChebyshevUTransformPlan{T,1,kindtuple(UFIRSTKIND,N,dims...)}(FFTW.plan_r2r(x, UFIRSTKIND, dims...; kws...))
    end
end
function plan_chebyshevutransform(x::AbstractArray{T,N}, ::Val{2}, dims...; kws...) where {T<:fftwNumber,N}
    any(≤(1),size(x)) && throw(ArgumentError("Array must contain at least 2 entries"))
    ChebyshevUTransformPlan{T,2,kindtuple(USECONDKIND,N,dims...)}(FFTW.plan_r2r(x, USECONDKIND, dims...; kws...))
end

plan_chebyshevutransform!(x::AbstractArray, dims...; kws...) = plan_chebyshevutransform!(x, Val(1), dims...; kws...)
plan_chebyshevutransform(x::AbstractArray, dims...; kws...) = plan_chebyshevutransform(x, Val(1), dims...; kws...)

@inline function _chebu1_prescale!(_, x::AbstractVector{T}) where T
    n = length(x)
    for k=1:n # sqrt(1-x_j^2) weight
        x[k] *= sinpi(one(T)/(2n) + (k-one(T))/n)/n
    end
    x
end

@inline function _chebu1_postscale!(_, x::AbstractVector{T}) where T
    n = length(x)
    for k=1:n # sqrt(1-x_j^2) weight
        x[k] /= sinpi(one(T)/(2n) + (k-one(T))/n)/n
    end
    x
end

function *(P::ChebyshevUTransformPlan{T,1,K,true}, x::AbstractVector{T}) where {T,K}
    length(x) ≤ 1 && return x
    _chebu1_prescale!(P.plan.region, x)
    P.plan * x
end

function mul!(y::AbstractVector{T}, P::ChebyshevUTransformPlan{T,1,K,false}, x::AbstractVector{T}) where {T,K}
    n = length(x)
    length(x) ≤ 1 && return copyto!(y, x)
    _chebu1_prescale!(P.plan.region, x)
    _plan_mul!(y, P.plan, x)
    _chebu1_postscale!(P.plan.region, x)
    y
end

@inline function _chebu2_prescale!(_, x::AbstractVector{T}) where T
    n = length(x)
    c = one(T)/ (n+1)
    for k=1:n # sqrt(1-x_j^2) weight
        x[k] *= sinpi(k*c)
    end
    x
end

@inline function _chebu2_postscale!(_, x::AbstractVector{T}) where T
    n = length(x)
    c = one(T)/ (n+1)
    @inbounds for k=1:n # sqrt(1-x_j^2) weight
        x[k] /= sinpi(k*c)
    end
    x
end

function *(P::ChebyshevUTransformPlan{T,2,K,true}, x::AbstractVector{T}) where {T,K}
    n = length(x)
    n ≤ 1 && return x
    _chebu2_prescale!(P.plan.region, x)
    lmul!(one(T)/ (n+1), P.plan * x)
end

function mul!(y::AbstractVector{T}, P::ChebyshevUTransformPlan{T,2,K,false}, x::AbstractVector{T}) where {T,K}
    n = length(x)
    n ≤ 1 && return copyto!(y, x)
    _chebu2_prescale!(P.plan.region, x)
    _plan_mul!(y, P.plan, x)
    _chebu2_postscale!(P.plan.region, x)
    lmul!(one(T)/ (n+1), y)
end

*(P::ChebyshevUTransformPlan{T,kind,K,false,N}, x::AbstractArray{T,N}) where {T,kind,K,N} =
    mul!(similar(x), P, x)

chebyshevutransform!(x::AbstractVector{T}, dims...; kws...) where {T<:fftwNumber} =
    plan_chebyshevutransform!(x, dims...; kws...)*x

"""
    chebyshevutransform(x, ::Val{kind}=Val(1))

transforms from values on a Chebyshev grid of the first or second kind to Chebyshev
coefficients of the 2nd kind (Chebyshev U expansion).
"""
chebyshevutransform(x, dims...; kws...) = plan_chebyshevutransform(x, dims...; kws...)*x

## Inverse transforms take ChebyshevU coefficients and produce values at ChebyshevU points of the first and second kinds
const IUFIRSTKIND = FFTW.RODFT01

struct IChebyshevUTransformPlan{T,kind,K,inplace,N,R} <: ChebyshevPlan{T}
    plan::FFTW.r2rFFTWPlan{T,K,inplace,N,R}
    IChebyshevUTransformPlan{T,kind,K,inplace,N,R}(plan) where {T,kind,K,inplace,N,R} = new{T,kind,K,inplace,N,R}(plan)
    IChebyshevUTransformPlan{T,kind,K,inplace,N,R}() where {T,kind,K,inplace,N,R} = new{T,kind,K,inplace,N,R}()
end

IChebyshevUTransformPlan{T,kind,K}(F::FFTW.r2rFFTWPlan{T,K,inplace,N,R}) where {T,kind,K,inplace,N,R} =
    IChebyshevUTransformPlan{T,kind,K,inplace,N,R}(F)

function plan_ichebyshevutransform!(x::AbstractArray{T,N}, ::Val{1}, dims...; kws...) where {T<:fftwNumber,N}
    if isempty(x)
        IChebyshevUTransformPlan{T,1,kindtuple(IUFIRSTKIND,N,dims...),true,N,isempty(dims) ? UnitRange{Int} : typeof(dims)}()
    else
        IChebyshevUTransformPlan{T,1,kindtuple(IUFIRSTKIND,N,dims...)}(FFTW.plan_r2r!(x, IUFIRSTKIND, dims...; kws...))
    end
end
function plan_ichebyshevutransform!(x::AbstractArray{T,N}, ::Val{2}, dims...; kws...) where {T<:fftwNumber,N}
    any(≤(1),size(x)) && throw(ArgumentError("Array must contain at least 2 entries"))
    IChebyshevUTransformPlan{T,2,kindtuple(USECONDKIND,N,dims...)}(FFTW.plan_r2r!(x, USECONDKIND))
end

function plan_ichebyshevutransform(x::AbstractArray{T,N}, ::Val{1}, dims...; kws...) where {T<:fftwNumber,N}
    if isempty(x)
        IChebyshevUTransformPlan{T,1,kindtuple(IUFIRSTKIND,N,dims...),false,N,isempty(dims) ? UnitRange{Int} : typeof(dims)}()
    else
        IChebyshevUTransformPlan{T,1,kindtuple(IUFIRSTKIND,N,dims...)}(FFTW.plan_r2r(x, IUFIRSTKIND, dims...; kws...))
    end
end
function plan_ichebyshevutransform(x::AbstractArray{T,N}, ::Val{2}, dims...; kws...) where {T<:fftwNumber,N}
    any(≤(1),size(x)) && throw(ArgumentError("Array must contain at least 2 entries"))
    IChebyshevUTransformPlan{T,2,kindtuple(USECONDKIND,N,dims...)}(FFTW.plan_r2r(x, USECONDKIND))
end

plan_ichebyshevutransform!(x::AbstractArray, dims...; kws...) = plan_ichebyshevutransform!(x, Val(1), dims...; kws...)
plan_ichebyshevutransform(x::AbstractArray, dims...; kws...) = plan_ichebyshevutransform(x, Val(1), dims...; kws...)

function _ichebyu1_postscale!(_, x::AbstractVector{T}) where T
    n = length(x)
    @inbounds for k=1:n # sqrt(1-x_j^2) weight
        x[k] /= 2sinpi(one(T)/(2n) + (k-one(T))/n)
    end
    x
end
function *(P::IChebyshevUTransformPlan{T,1,K,true}, x::AbstractVector{T}) where {T<:fftwNumber,K}
    n = length(x)
    n ≤ 1 && return x

    x = P.plan * x
    _ichebyu1_postscale!(P.plan.region, x)
end

function mul!(y::AbstractVector{T}, P::IChebyshevUTransformPlan{T,1,K,false}, x::AbstractVector{T}) where {T<:fftwNumber,K}
    n = length(x)
    length(y) == n || throw(DimensionMismatch("output must match dimension"))
    n ≤ 1 && return x

    _plan_mul!(y, P.plan, x)
    _ichebyu1_postscale!(P.plan.region, y)
end

function _ichebu2_rescale!(_, x::AbstractVector{T}) where T
    n = length(x)
    c = one(T)/ (n+1)
    for k=1:n # sqrt(1-x_j^2) weight
        x[k] /= sinpi(k*c)
    end
    ldiv!(2, x)
    x
end

function *(P::IChebyshevUTransformPlan{T,2,K,true}, x::AbstractVector{T}) where {T<:fftwNumber,K}
    n = length(x)
    n ≤ 1 && return x

    x = P.plan * x
    _ichebu2_rescale!(P.plan.region, x)
end

function mul!(y::AbstractVector{T}, P::IChebyshevUTransformPlan{T,2,K,false}, x::AbstractVector{T}) where {T<:fftwNumber,K}
    n = length(x)
    length(y) == n || throw(DimensionMismatch("output must match dimension"))
    n ≤ 1 && return x

    _plan_mul!(y, P.plan, x)
    _ichebu2_rescale!(P.plan.region, y)
end

ichebyshevutransform!(x::AbstractVector{T}, dims...; kwds...) where {T<:fftwNumber} =
    plan_ichebyshevutransform!(x, dims...; kwds...)*x

ichebyshevutransform(x, dims...; kwds...) = plan_ichebyshevutransform(x, dims...; kwds...)*x

*(P::IChebyshevUTransformPlan{T,k,K,false,N}, x::AbstractArray{T,N}) where {T,k,K,N} =
    mul!(similar(x), P, x)

## Code generation for integer inputs

for func in (:chebyshevtransform,:ichebyshevtransform,:chebyshevutransform,:ichebyshevutransform)
    @eval $func(x::AbstractVector{T}, dims...; kwds...) where {T<:Integer} = $func(convert(AbstractVector{Float64},x), dims...; kwds...)
end

## points

struct ChebyshevGrid{kind,T} <: AbstractVector{T}
    n::Int
    function ChebyshevGrid{1,T}(n::Int) where T
        n ≥ 0 || throw(ArgumentError("Number of points must be nonnehative"))
        new{1,T}(n)
    end
    function ChebyshevGrid{2,T}(n::Int) where T
        n ≥ 2 || throw(ArgumentError("Number of points must be greater than 2"))
        new{2,T}(n)
    end
end

ChebyshevGrid{kind}(n::Integer) where kind = ChebyshevGrid{kind,Float64}(n)

size(g::ChebyshevGrid) = (g.n,)
getindex(g::ChebyshevGrid{1,T}, k::Integer) where T =
    sinpi(convert(T,g.n-2k+1)/(2g.n))

getindex(g::ChebyshevGrid{2,T}, k::Integer) where T =
    sinpi(convert(T,g.n-2k+1)/(2g.n-2))

chebyshevpoints(::Type{T}, n::Integer, ::Val{kind}) where {T<:Number,kind} = ChebyshevGrid{kind,T}(n)
chebyshevpoints(::Type{T}, n::Integer) where T = chebyshevpoints(T, n, Val(1))
chebyshevpoints(n::Integer, kind=Val(1)) = chebyshevpoints(Float64, n, kind)

# sin(nθ) coefficients to values at Clenshaw-Curtis nodes except ±1
#
# struct DSTPlan{T,kind,inplace,P} <: Plan{T}
#     plan::P
# end
#
# DSTPlan{k,inp}(plan) where {k,inp} =
#     DSTPlan{eltype(plan),k,inp,typeof(plan)}(plan)
#
#
# plan_DSTI!(x) = length(x) > 0 ? DSTPlan{1,true}(FFTW.FFTW.plan_r2r!(x, FFTW.FFTW.RODFT00)) :
#                                 fill(one(T),1,length(x))
#
# function *(P::DSTPlan{T,1}, x::AbstractArray) where {T}
#     x = P.plan*x
#     rmul!(x,half(T))
# end

###
# BigFloat
# Use `Nothing` and fall back to FFT
###

plan_chebyshevtransform(x::AbstractArray{T,N}, ::Val{kind}, dims...; kws...) where {T,N,kind} =
    ChebyshevTransformPlan{T,kind,Nothing,false,N,UnitRange{Int}}()
plan_ichebyshevtransform(x::AbstractArray{T,N}, ::Val{kind}, dims...; kws...) where {T,N,kind} =
    IChebyshevTransformPlan{T,kind,Nothing,false,N,UnitRange{Int}}()

plan_chebyshevtransform!(x::AbstractArray{T,N}, ::Val{kind}, dims...; kws...) where {T,N,kind} =
    ChebyshevTransformPlan{T,kind,Nothing,true,N,UnitRange{Int}}()
plan_ichebyshevtransform!(x::AbstractArray{T,N}, ::Val{kind}, dims...; kws...) where {T,N,kind} =
    IChebyshevTransformPlan{T,kind,Nothing,true,N,UnitRange{Int}}()

#following Chebfun's @Chebtech1/vals2coeffs.m and @Chebtech2/vals2coeffs.m
function *(P::ChebyshevTransformPlan{T,1,Nothing,false}, x::AbstractVector{T}) where T
    n = length(x)
    if n == 1
        x
    else
        w = [2exp(im*convert(T,π)*k/2n) for k=0:n-1]
        ret = w.*ifft([x;reverse(x)])[1:n]
        ret = T<:Real ? real(ret) : ret
        ret[1] /= 2
        ret
    end
end

# function *(P::ChebyshevTransformPlan{T,1,K,Nothing,false}, x::AbstractVector{T}) where {T,K}
#     n = length(x)
#     if n == 1
#         x
#     else
#         ret = ifft([x;x[end:-1:2]])[1:n]
#         ret = T<:Real ? real(ret) : ret
#         ret[2:n-1] *= 2
#         ret
#     end
# end

*(P::ChebyshevTransformPlan{T,1,Nothing,true,N,R}, x::AbstractVector{T}) where {T,N,R} =
    copyto!(x, ChebyshevTransformPlan{T,1,Nothing,false,N,R}() * x)
# *(P::ChebyshevTransformPlan{T,2,true,Nothing}, x::AbstractVector{T}) where T =
#     copyto!(x, ChebyshevTransformPlan{T,2,false,Nothing}() * x)

#following Chebfun's @Chebtech1/vals2coeffs.m and @Chebtech2/vals2coeffs.m
function *(P::IChebyshevTransformPlan{T,1,Nothing,false}, x::AbstractVector{T}) where T
    n = length(x)
    if n == 1
        x
    else
        w = [exp(-im*convert(T,π)*k/2n)/2 for k=0:2n-1]
        w[1] *= 2;w[n+1] *= 0;w[n+2:end] *= -1
        ret = fft(w.*[x;one(T);x[end:-1:2]])
        ret = T<:Real ? real(ret) : ret
        ret[1:n]
    end
end

# function *(P::IChebyshevTransformPlan{T,2,K,Nothing,true}, x::AbstractVector{T}) where {T,K}
#     n = length(x)
#     if n == 1
#         x
#     else
#         x[1] *= 2; x[end] *= 2
#         chebyshevtransform!(x, Val(2))
#         x[1] *= 2; x[end] *= 2
#         lmul!(convert(T,n-1)/2, x)
#         x
#     end
# end

*(P::IChebyshevTransformPlan{T,1,Nothing,true,N,R}, x::AbstractVector{T}) where {T,N,R} =
    copyto!(x, IChebyshevTransformPlan{T,1,Nothing,false,N,R}() * x)
# *(P::IChebyshevTransformPlan{T,SECONDKIND,false,Nothing}, x::AbstractVector{T}) where T =
#     IChebyshevTransformPlan{T,SECONDKIND,true,Nothing}() * copy(x)


text:
using ContinuousWavelet: cutoff_time, eval_analysis

@testset "cutoff-time" begin
    @testset "Cauchy scale=$scale" for scale in [10, 20, 50, 100, 200, 500, 1000]
        wav = CauchyWavelet(10; ϵ=1e-4)
        t = Int32(ceil(cutoff_time(wav)*scale))
        P0 = sum(map(t -> abs(eval_analysis(wav, t/scale))^2, 0:t))
        P1 = sum(map(t -> abs(eval_analysis(wav, t/scale))^2, (t+1):(10*t)))
        @test P1/P0 < 1e-6
    end

    @testset "Morlet scale=$scale" for scale in [10, 20, 50, 100, 200, 500, 1000]
        wav = MorletWavelet(0.5; ϵ=1e-4)
        t = Int32(ceil(cutoff_time(wav)*scale))
        P0 = sum(map(t -> abs(eval_analysis(wav, t/scale))^2, 0:t))
        P1 = sum(map(t -> abs(eval_analysis(wav, t/scale))^2, (t+1):(10*t)))
        @test P1/P0 < 1e-6
    end
end

ext: jl


text:
"""
    struct SamplingEnsemble{T}
        recordings::Vector{T}
    end

A collection of `SamplingPair`s, can be used for smoothing or inference
problems.

    SamplingEnsemble(
        aux_laws, recordings, tts, args=tuple();
        aux_laws_blocking=aux_laws, artificial_noise=1e-11,
        solver_choice_blocking=args
    )

Base constructor.
"""
struct SamplingEnsemble{T}
    recordings::Vector{T}

    function SamplingEnsemble(
            aux_laws, recordings, tts, args=tuple();
            aux_laws_blocking=aux_laws, artificial_noise=1e-11,
            solver_choice_blocking=args
        )
        num_rec = length(recordings)
        aux_laws = _vec_me(aux_laws, num_rec)
        args = _vec_me(args, num_rec)
        aux_laws_blocking = _vec_me(aux_laws_blocking, num_rec)
        artificial_noise = _vec_me(artificial_noise, num_rec)
        solver_choice_blocking = _vec_me(solver_choice_blocking, num_rec)
        recs = map(1:num_rec) do i
            SamplingPair(
                aux_laws[i], recordings[i], tts[i], args[i];
                aux_laws_blocking = aux_laws_blocking[i],
                artificial_noise = artificial_noise[i],
                solver_choice_blocking = solver_choice_blocking[i]
            )
        end
        new{eltype(recs)}(recs)
    end
end

_vec_me(val, N) = (typeof(val) <: AbstractArray ? val : fill(val, N))

OBS.num_recordings(se::SamplingEnsemble) = length(se.recordings)


text:
data = (
    [7, 1, 2, 4, 10],
    [7, 1, 2, 4, 10],
    [7, 1, 2, 4, 10, 15],
    [1, 2, 4, 7, 10, 15],
    [0, 10, 20, 30],
    [1, 2, 3, 4, 5],
    [1, 2, 3, 4, 5],
    [30, 40, 50, 60, 35],
    [2, 0.6, 1.3, 0.3, 0.3, 1.7, 0.7, 1.7, 0.4],
    [3.7, 3.3, 3.5, 2.8],
    [100, 125, 123, 60, 45, 56, 66],
    [2, 2, 2, 2, 2, 2],
    [2.3],
    [-2, -3, 1, 2, -10],
    [1, 2, 3, 4, 5],
)
weights = (
    [1, 1/3, 1/3, 1/3, 1],
    [1, 1, 1, 1, 1],
    [1, 1/3, 1/3, 1/3, 1, 1],
    [1/3, 1/3, 1/3, 1, 1, 1],
    [30, 191, 9, 0],
    [10, 1, 1, 1, 9],
    [10, 1, 1, 1, 900],
    [1, 3, 5, 4, 2],
    [2, 2, 0, 1, 2, 2, 1, 6, 0],
    [5, 5, 4, 1],
    [30, 56, 144, 24, 55, 43, 67],
    [0.1, 0.2, 0.3, 0.4, 0.5, 0.6],
    [12],
    [7, 1, 1, 1, 6],
    [1, 0, 0, 0, 2],
)
median_answers = (7.0,   4.0,  8.5,
                  8.5,  10.0,  2.5,
                  5.0,  50.0,  1.7,
                  3.5, 100.0,  2.0,
                  2.3,  -2.0,  5.0)
mean_answers = (6.444444,  4.800000, 8.583333,
                8.583333,  9.086956, 2.909091,
                4.949617, 47.333333, 1.275000,
                3.453333, 91.782816, 2.000000,
                2.300000, -4.625000, 3.666667)

num_tests = length(data)

# Weighted mean tests
@test_throws MethodError weighted_mean(data[1])
@test_throws MethodError weighted_mean("string", "input")
for i = 1:num_tests
    @test_approx_eq_eps weighted_mean(data[i], weights[i]) mean_answers[i] 1e-6
end
@test isnan(weighted_mean([4, 3, 2, 1], [0, 0, 0, 0]))

# Weighted median tests
@test_throws MethodError weighted_median(data[1])
@test_throws MethodError weighted_median("string", "input")
for i = 1:num_tests
    @test weighted_median(data[i], weights[i]) == median_answers[i]
end
@test_throws ErrorException weighted_median([4, 3, 2, 1], [0, 0, 0, 0])
@test_throws ErrorException weighted_median((Float64)[], (Float64)[])
v = [4, 3, 2, 1]
wt = [1, 2, 3, 4, 5]
@test_throws ErrorException weighted_median(v, wt)
@test_throws MethodError weighted_median([4 3 2 1 0], wt)
@test_throws MethodError weighted_median([[1 2];[4 5];[7 8];[10 11];[13 14]], wt)
v = [1, 3, 2, NaN, 2]
@test isnan(weighted_median(v, wt))
wt = [1, 2, NaN, 4, 5]
@test isnan(weighted_median(v, wt))
v = [5, 4, 3, 2, 1]
wt = [1, 2, -3, 4, -5]
@test weighted_median(v, wt) == 2.0
v = [-10, 1, 1, -10, -10]
wt = [-1, -1, -1, -1, -1]
@test weighted_median(v, wt) == -10


text:
# ps3000_open_unit_progress.jl

export ps3000_open_unit_progress

function ps3000_open_unit_progress()
  handle = Ref{Int16}()
  progress_percent = Ref{Int16}()
  open_unit_progress_status = ccall((:ps3000_open_unit_progress, ps3000driver), Int16,
    (Ref{Int16}, Ref{Int16}),
    handle, progress_percent)
  if open_unit_progress_status == -1
    error("ps3000 Error: unit failed to open or was not found")
  end
  return (handle[], progress_percent[])
end


text:
using ViewerGL, LinearAlgebraicRepresentation
GL = ViewerGL
Lar = LinearAlgebraicRepresentation

# input of primitive shape
V, (VV,EV,FV) = Lar.cuboid([1,1], true)
square = (V,EV)
model = Lar.Struct([ square,
			Lar.r(π), square ])
V,EV = Lar.struct2lar(model)
VV = [[k] for k=1:size(V,2)]

GL.VIEW( GL.numbering(.4)((V,[VV, EV]),GL.COLORS[1]) );

# arrangement of input data
W = convert(Lar.Points, V')
cop_EV = Lar.coboundary_0(EV)
V, copEV, copFE = Lar.planar_arrangement(W, cop_EV)

# compute containment graph of components
bicon_comps = Lar.Arrangement.biconnected_components(copEV)

# compute euler characteristic
χ = Lar.euler_characteristic(V, copEV, copFE)
println("χ = $χ ; bicon_comps = $(length(bicon_comps))");

EW = Lar.cop2lar(copEV)
W = convert(Lar.Points, V')
comps = [ GL.GLLines(W,EW[comp],GL.COLORS[(k-1)%12+1]) for (k,comp) in enumerate(bicon_comps) ];
GL.VIEW(comps);

# final solid visualization
triangulated_faces = Lar.triangulate2D(V, [copEV, copFE])
V = convert(Lar.Points, V')
FVs = convert(Array{Lar.Cells}, triangulated_faces)
GL.VIEW(GL.GLExplode(V,FVs,1.2,1.2,1.2,99,1));

# polygonal face boundaries
EVs = Lar.FV2EVs(copEV, copFE)
EVs = convert(Array{Array{Array{Int64,1},1},1}, EVs)
GL.VIEW(GL.GLExplode(V,EVs,1.2,1.2,1.2,1,1));


text:
# %% Demonstration of delay embeddings
using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
using DynamicalSystems, PyPlot, Random

lo = Systems.lorenz([0, 10, 0.0])
tr = trajectory(lo, 1000; Ttr=10, reltol = 1e-12, abstol = 1e-12)
x, y, z = columns(tr)
w = x
τ = estimate_delay(w, "mi_min")
R = embed(w, 3, τ)

close("all")
N = 5000
fig = figure()
ax1 = subplot(131, projection = "3d")
ax1.plot3D(x[1:N], y[1:N], z[1:N], lw = 1.0, color = "C0")
ax1.set_title("original set \$A\$", pad = 0)
ax = ax1
for a in (ax.xaxis, ax.yaxis, ax.zaxis); a.set_ticklabels([]); end
# ax1.text(-20, -20, 0, "\$D_0(A)\$ = $(round(D_A;digits=3))", color = "C1")

ax2 = subplot(132)
wx = 2400:3200
ax2.plot(wx, w[wx])
midpoint = (wx[1] + wx[end])/2
tau = midpoint:(midpoint+τ)
ax2.plot(tau, fill(10, length(tau)), color = "C3")
ax2.text(midpoint, 7.5, "\$\\tau\$", color = "C3", size = 32)
ax2.set_title("measurement \$w=x\$")
# make fancy axis
ax2.spines["bottom"].set_position("center")
ax2.spines["right"].set_color("none")
ax2.spines["top"].set_color("none")
ax2.set_xticks([])
ax2.set_yticks([])
ax2.set_xlabel("\$t\$")
ax2.set_ylabel("\$w\$", rotation = 0)
ax2.xaxis.set_label_coords(1.0, 0.5)
ax2.yaxis.set_label_coords(-0.05, 0.9)

ax3 = subplot(133, projection = "3d")
ax3.plot(R[1:N, 1], R[1:N, 2], R[1:N, 3], color = "C1", lw = 1.0)
ax = ax3
for a in (ax.xaxis, ax.yaxis, ax.zaxis); a.set_ticklabels([]); end
ax3.set_title("reconstruction \$R\$", pad = 0)
# ax3.set_title("reconstruction \$R\$\n\$(\\gamma=$(γ),\\tau=$(τ))\$")
# ax3.text(-5,-25,-10, "\$D_0(R)\$ = $(round(D_R;digits=3))", color = "C2")
ax1.dist = 9
ax3.dist = 9

fig.tight_layout(pad = 0.3)
wsave(plotsdir("6", "delayembedding"), fig)

max_stars_repo_name: JuliaDynamics/NonlinearDynamicsTextbook

max_issues_repo_name: JuliaDynamics/NonlinearDynamicsTextbook

max_forks_repo_name: JuliaDynamics/NonlinearDynamicsTextbook


text:
"This module is created to build MPPDC with quanity bids, for all UL agents,
Definitation of which UL problem is being built is based on the 'current' string passed in"
function build_MPPDC_quantity(t_steps, non_str_gens, non_str_res, current, optimizer, objective_type)
    ηch = 0.95 #Needs to be passed in
    ηdch = 0.95 #Needs to be passed in
    soc_init = dat_str.ESS_STR.E_max[1] * 0.5
    non_str_agents = vcat(generators, renewables)

    #############FIRST INIT THE ABSTRACT MODEL##########
    if current == "ESS_STR"
        ####INIT ABSTRACT MODEL######
        function init_MPPDC_ESS()
            ############INIT THE MODEL INSTANCE############
            MPPDC =  Model()
            MPPDC.ext[:param] = Dict()
            param = MPPDC.ext[:param]
            MPPDC.ext[:eq] = Dict()
            eq = MPPDC.ext[:eq]
            ##############PARAMETERS IN ESS MPPDC###############
            #OTHER AGENT QUANTITY BIDS
            param[:G_ST] = @NLparameter(MPPDC, G_ST[t in t_steps] == 0) #Generation quantity bid
            param[:W_ST] = @NLparameter(MPPDC, W_ST[t in t_steps] == 0) #Wind quantity bid
            #MAX ENERGY CONTENT
            param[:E_max] = @NLparameter(MPPDC, E_max[t in t_steps] == 0) #max capacity of the ESS
            #PRICE-TAKING AGENTS QUANTITY BIDS
            param[:G] = @NLparameter(MPPDC, G[i in non_str_gens, t in t_steps] == 0) #Generation capacity of non-str generator
            param[:W] = @NLparameter(MPPDC, W[k in non_str_res, t in t_steps]  == 0) #Generation capacity of non-str RES
            param[:D] = @NLparameter(MPPDC, D[t in t_steps]  == 0) #Max load

            #PRICE BIDS, AS QUANTITY BIDDING ONLY
            #########PRICE BIDS#############
            param[:c_g_st] = @NLparameter(MPPDC, c_g_st[t in t_steps] == 0) #Generation price bid str
            param[:c_w_st] = @NLparameter(MPPDC, c_w_st[t in t_steps]  == 0)#Wind price bid str
            # @NLparameter(MPPDC, c_dch_st[t in t_steps] == 0) #ESS discharge price bid str
            # @NLparameter(MPPDC, c_ch_st[t in t_steps] == 0) #ESS charge price bid str
            param[:c_g] = @NLparameter(MPPDC, c_g[i in non_str_gens, t in t_steps] == 0) #Generation price bid nonstr
            param[:c_w] = @NLparameter(MPPDC, c_w[k in non_str_res, t in t_steps] == 0) #Wind price bid nonstr
            param[:c_d] = @NLparameter(MPPDC, c_d[t in t_steps] == 0) #WTP of the load

            ########CONSTANT TERMS FOR RAMPING###########
            param[:ramp_limit] = @NLparameter(MPPDC, ramp_limit[t in t_steps] == 1.0) #Set to 100% as default

            for t in t_steps
                #RAMPING LIMIT OF STR GENERATOR
                JuMP.set_value(ramp_limit[t], Rᴳ * dat_str.GEN_STR.P_max[t])
                #MAXIMUM ENERGY CONTENT OF ESS
                JuMP.set_value(E_max[t], dat_str.ESS_STR.E_max[t]) #By this the upper bound var. on SoC is set

                for i in non_str_gens
                    JuMP.set_value(c_g[i,t], dat_nonstr[i].c_g[t]) #MC OF GEN
                    JuMP.set_value(G[i,t], dat_nonstr[i].G[t]) #MAX CAPACITY OF GEN
                end
                for k in non_str_res
                    JuMP.set_value(c_w[k,t], dat_nonstr[k].c_g[t]) #COST OF WIND
                    JuMP.set_value(W[k,t], dat_nonstr[k].G[t]) #MAX WIND CAPACITY
                end
                JuMP.set_value(c_d[t], p_cap) #WTP SET TO PRICE CAP
                JuMP.set_value(D[t], load[t]) #MAX LOAD
                #W.R.T OTHER STRATEGIC AGENTS
                #QUANTITIES ARE EQUAL TO MAX CAPACITY
                JuMP.set_value(G_ST[t], dat_str.GEN_STR.P_max[t])
                JuMP.set_value(W_ST[t], dat_str.WIND_STR.P_max[t])
                #PRICES ARE EQUAL TO THE MARGINAL PRICES
                JuMP.set_value(c_g_st[t], dat_str.GEN_STR.MC_g[t])
                JuMP.set_value(c_w_st[t], dat_str.WIND_STR.MC_g[t])
                # JuMP.set_value(c_ch_st[t], dat_str.ESS_STR.MC_ch[t])
                # JuMP.set_value(c_dch_st[t], dat_str.ESS_STR.MC_dch[t])
            end

            @variables(MPPDC, begin
                #REGULARIZATION A PARAMETER
                ϵ == 0.0001
                r_dch_st[t in t_steps]
                r_ch_st[t in t_steps]
                ############UL DECISION VARS################
                ############PRICE BIDS###########
                c_dch_st[t in t_steps]
                c_ch_st[t in t_steps]
                #########QUANTITY BIDS############
                0 <= DCH_ST[t in t_steps] #ESS discharge quantity bid
                0 <= CH_ST[t in t_steps] #ESS charge quantity bid
                0 <= SOC[t in t_steps] #ESS State of Charge var
                #############LL DECISION VARS###########
                ######PRIMALS#######
                0 <= g_st[t in t_steps] #Dispatched generation of the strategic agent
                0 <= w_st[t in t_steps] #Dispatched wind of the strategic agent
                0 <= g[i in non_str_gens, t in t_steps] #Dispatched generation
                0 <= w[k in non_str_res, t in t_steps] #Dispatched wind
                0 <= dch_st[t in t_steps] #Dispatched strategic discharge
                0 <= ch_st[t in t_steps] #Dispatched strategic charge
                0 <= d[t in t_steps] #Dispatched demand
                #######DUALS#########
                0 <= β⁺g[i in non_str_gens, t in t_steps] #Dual of the upper bound of the generation
                0 <= β⁻g[i in non_str_gens, t in t_steps] #Dual of the lower bound of the generation
                0 <= β⁺w[k in non_str_res, t in t_steps] #Dual of the upper bound of the RE generation
                0 <= β⁻w[k in non_str_res, t in t_steps] #Dual of the lower bound of the RE generation
                0 <= β⁺d[t in t_steps] #Dual of the upper bound of the demand
                0 <= β⁻d[t in t_steps] #Dual of the lower bound of the demand

                0 <= β⁺g_st[t in t_steps] #Dual of the upper bound of the str generation
                0 <= β⁻g_st[t in t_steps] #Dual of the lower bound of the str generation
                0 <= β⁺w_st[t in t_steps] #Dual of the upper bound of the str RE generation
                0 <= β⁻w_st[t in t_steps] #Dual of the lower bound of the str RE generation
                0 <= β⁺ch_st[t in t_steps] #Dual of the upper bound of the str charging
                0 <= β⁻ch_st[t in t_steps] #Dual of the lower bound of the str charging
                0 <= β⁺dch_st[t in t_steps] #Dual of the upper bound of the str discharging
                0 <= β⁻dch_st[t in t_steps] #Dual of the lower bound of the str discharging
                λ[t in t_steps] #Market price, dual of the energy balance cstr of LL
            end)

            ##########ENERGY BALANCE##########
            eq[:en_balance] = balance_eq = @expression(MPPDC, [t in t_steps],
            - g_st[t] - w_st[t] -dch_st[t] + ch_st[t]
            - sum(g[i,t] for i in non_str_gens) - sum(w[k,t] for k in non_str_res) + d[t])

            #######STRONG DUALITY EQ##########
            #PRIMAL OBJ - DUAL OBJ
            eq[:SDE_eq] = SDE_eq = @NLexpression(MPPDC,
                                sum(
                                g_st[t] * c_g_st[t] +
                                w_st[t] * c_w_st[t] +
                                dch_st[t] * c_dch_st[t] +
                                - ch_st[t] * c_ch_st[t] +
                                sum(w[k,t] * c_w[k,t] for k in non_str_res) +
                                sum(g[i,t] * c_g[i,t] for i in non_str_gens) -
                                d[t] * c_d[t] +
                                sum(β⁺g[i,t] * G[i,t] for i in non_str_gens) +
                                sum(β⁺w[k,t] * W[k,t] for k in non_str_res) +
                                β⁺g_st[t] * G_ST[t] +
                                β⁺w_st[t] * W_ST[t] +
                                r_ch_st[t] +
                                r_dch_st[t] +
                                β⁺d[t] * D[t]
                                for t in t_steps)
            )

            ##########OBJECTIVES OF THE VARIOUS UL AGENTS#########
            # #ESS-STR
            # obj_ESS = @NLexpression(MPPDC, sum(- dch_st[t] * λ[t]
            # + ch_st[t] * λ[t] for t in t_steps)
            # )
            #GEN-STR
            # obj_GEN = @NLexpression(MPPDC, sum(c_g_st[t] * g_st[t] -
            # g_st[t] * λ[t] for t in t_steps)
            # )
            # #WIND-STR
            # obj_WIND = @NLexpression(MPPDC, sum(c_w_st[t] * w_st[t]
            # - w_st[t] * λ[t] for t in t_steps)
            # )

            #################ADDING CONSTRAINTS#############
            @constraints(MPPDC, begin
                # ENERGY BALANCE CSTR
                en_balance[t in t_steps], balance_eq[t] == 0
                #SOC RULES
                soc_rule_0[t = 1], SOC[end] - SOC[t] + ch_st[t] * ηch - (dch_st[t]/ηdch) == 0
                soc_rule[t = 2:t_steps[end]], SOC[t-1] - SOC[t] + ch_st[t] * ηch - (dch_st[t]/ηdch) == 0
                soc_init, SOC[end] == soc_init
            end)

            @NLconstraints(MPPDC, begin
                #NOTE:DUE TO THE NL PARAMETERS MOST CONSTRAINTS HAS TO BE HANDELED AS NON-LINEAR
                ############UL CONSTRAINTS############
                #NOTE:AGENT SPECIFIC CONSTRAINTS NEED TO BE FIXED FOR EACH AGENT INDIVIDUALLY
                #RAMPING CSTRS
                # #Ramping up constraint at time step 1
                # ramp_up_0[t = 1], g_st[t] - g_st[end] <= ramp_limit[t]
                # #Ramping up constraint
                # ramp_up[t = 2:t_steps[end]], g_st[t] - g_st[t-1] <= ramp_limit[t]
                # #Ramping down constraint at time step 1
                # ramp_down_0[t = 1], g_st[t] - g_st[end] >= - ramp_limit[t]
                # #Ramping down constraint
                # ramp_down[t = 2:t_steps[end]], g_st[t] - g_st[t-1] >= - ramp_limit[t]
                #ENFORCE THAT NOBODY GETS FORCE TO NEGATIVE PROFITS BY OTHER STR AGENTS
                # - obj_ESS >= 0
                # - obj_GEN >= 0
                # - obj_WIND >= 0

                #LIMITS OF THE DISPATCHED QUANTITIES OF THE STRATEGIC AGENTS, THEY CANT BE SET FROM OUTSIDE THE MODEL
                [t in t_steps], g_st[t] <= G_ST[t]
                [t in t_steps], w_st[t] <= W_ST[t]
                [t in t_steps], dch_st[t] <= DCH_ST[t]
                [t in t_steps], ch_st[t] <= CH_ST[t]
                [t in t_steps], SOC[t] <= E_max[t]

                #FOR THE NON STR AGENTS
                [i in non_str_gens, t in t_steps], g[i,t] <= G[i,t]
                [k in non_str_res, t in t_steps], w[k,t] <= W[k,t]

                [t in t_steps], d[t] <= D[t]
                #LAGRANGIAN STATIONARITY
                #w.r.t g_{i,t}:
                [i in non_str_gens, t in t_steps], c_g[i,t] - λ[t] - β⁻g[i,t] + β⁺g[i,t] == 0
                #w.r.t g_st_{i,t}:
                [t in t_steps], c_g_st[t] - λ[t] - β⁻g_st[t] + β⁺g_st[t] == 0
                #w.r.t w_{k,t}:
                [k in non_str_res, t in t_steps], c_w[k,t] - λ[t] - β⁻w[k,t] + β⁺w[k,t] == 0
                #w.r.t w_st_{t}:
                [t in t_steps], c_w_st[t] - λ[t] - β⁻w_st[t] + β⁺w_st[t] == 0
                #w.r.t dch_st_{t}:
                [t in t_steps], c_dch_st[t] - λ[t] - β⁻dch_st[t] + β⁺dch_st[t] == 0
                #w.r.t ch_st_{t}:
                [t in t_steps], -c_ch_st[t] + λ[t] - β⁻ch_st[t] + β⁺ch_st[t] == 0
                #w.r.t d_{t}:
                [t in t_steps], -c_d[t] + λ[t] - β⁻d[t] + β⁺d[t] == 0

                #ENFORCING STRONG DUALITY
                SDE, SDE_eq == 0
                [t in t_steps], β⁺ch_st[t] * CH_ST[t] - r_ch_st[t] <= ϵ
                [t in t_steps], -β⁺ch_st[t] * CH_ST[t] + r_ch_st[t] <= ϵ
                [t in t_steps], β⁺dch_st[t] * DCH_ST[t] - r_dch_st[t]  <= ϵ
                [t in t_steps], -β⁺dch_st[t] * DCH_ST[t] + r_dch_st[t]  <= ϵ
                #NOTE: ESS CAN RESPECT IT BY USING ECONOMIC SENSE, FOR OTHERS NEEDED
                #EXCLUSIVE CH/DCH
                # excl[t in t_steps], dch_st[t] * ch_st[t] == 0
            end)

            return MPPDC
        end
        ####BUILD ABSTRACT MODEL#####
        MPPDC = init_MPPDC_ESS()

    elseif current == "GEN_STR"
        ####INIT ABSTRACT MODEL######
        function init_MPPDC_GEN()
            ############INIT THE MODEL INSTANCE############
            MPPDC =  Model()
            MPPDC.ext[:param] = Dict()
            param = MPPDC.ext[:param]
            MPPDC.ext[:eq] = Dict()
            eq = MPPDC.ext[:eq]
            ##############PARAMETERS IN ESS MPPDC###############
            #OTHER AGENT QUANTITY BIDS
            # @NLparameter(MPPDC, G_ST[t in t_steps] == 0) #Generation quantity bid
            param[:W_ST] = @NLparameter(MPPDC, W_ST[t in t_steps] == 0) #Wind quantity bid
            param[:DCH_ST] = @NLparameter(MPPDC, DCH_ST[t in t_steps] == 0) #ESS discharge quantity bid
            param[:CH_ST] = @NLparameter(MPPDC, CH_ST[t in t_steps] == 0) #ESS charge quantity bid
            #MAX ENERGY CONTENT
            # param[:E_max] = @NLparameter(MPPDC, E_max[t in t_steps] == 0) #max capacity of the ESS
            #PRICE TAKING AGENTS QUANTITY BIDS
            param[:G] = @NLparameter(MPPDC, G[i in non_str_gens, t in t_steps] == 0) #Generation capacity of non-str generator
            param[:W] = @NLparameter(MPPDC, W[k in non_str_res, t in t_steps]  == 0) #Generation capacity of non-str RES
            param[:D] = @NLparameter(MPPDC, D[t in t_steps]  == 0) #Max load

            #PRICE BIDS, AS QUANTITY BIDDING ONLY
            #########PRICE BIDS#############
            param[:c_g_st] = @NLparameter(MPPDC, c_g_st[t in t_steps] == 0) #Generation price bid str
            param[:c_w_st] = @NLparameter(MPPDC, c_w_st[t in t_steps]  == 0)#Wind price bid str
            param[:c_dch_st] = @NLparameter(MPPDC, c_dch_st[t in t_steps] == 0) #ESS discharge price bid str
            param[:c_ch_st] = @NLparameter(MPPDC, c_ch_st[t in t_steps] == 0) #ESS charge price bid str
            param[:c_g] = @NLparameter(MPPDC, c_g[i in non_str_gens, t in t_steps] == 0) #Generation price bid nonstr
            param[:c_w] = @NLparameter(MPPDC, c_w[k in non_str_res, t in t_steps] == 0) #Wind price bid nonstr
            param[:c_d] = @NLparameter(MPPDC, c_d[t in t_steps] == 0) #WTP of the load

            ########CONSTANT TERMS FOR RAMPING###########
            param[:ramp_limit] = @NLparameter(MPPDC, ramp_limit[t in t_steps] == 1.0) #Set to 100% as default

            for t in t_steps
                #RAMPING LIMIT OF STR GENERATOR
                JuMP.set_value(ramp_limit[t], Rᴳ * dat_str.GEN_STR.P_max[t])
                #MAXIMUM ENERGY CONTENT OF ESS
                # JuMP.set_value(E_max[t], dat_str.ESS_STR.E_max[t]) #By this the upper bound var. on SoC is set

                for i in non_str_gens
                    JuMP.set_value(c_g[i,t], dat_nonstr[i].c_g[t]) #MC OF GEN
                    JuMP.set_value(G[i,t], dat_nonstr[i].G[t]) #MAX CAPACITY OF GEN
                end
                for k in non_str_res
                    JuMP.set_value(c_w[k,t], dat_nonstr[k].c_g[t]) #COST OF WIND
                    JuMP.set_value(W[k,t], dat_nonstr[k].G[t]) #MAX WIND CAPACITY
                end
                JuMP.set_value(c_d[t], p_cap) #WTP SET TO PRICE CAP
                JuMP.set_value(D[t], load[t]) #MAX LOAD
                #W.R.T OTHER STRATEGIC AGENTS
                #QUANTITIES ARE EQUAL TO MAX CAPACITY
                # JuMP.set_value(G_ST[t], dat_str.GEN_STR.P_max[t])
                JuMP.set_value(W_ST[t], dat_str.WIND_STR.P_max[t])
                JuMP.set_value(DCH_ST[t], dat_str.ESS_STR.P_max[t])
                JuMP.set_value(CH_ST[t], dat_str.ESS_STR.P_max[t])
                # JuMP.set_value(DCH_ST[t], 0)
                # JuMP.set_value(CH_ST[t], 0)
                #PRICES ARE EQUAL TO THE MARGINAL PRICES
                JuMP.set_value(c_g_st[t], dat_str.GEN_STR.MC_g[t])
                JuMP.set_value(c_w_st[t], dat_str.WIND_STR.MC_g[t])
                #NOTE: THIS MAY HAVE TO BE REMOVED AND LET THE SOLVER CHOSE
                JuMP.set_value(c_dch_st[t], dat_str.ESS_STR.MC_dch[t])
                JuMP.set_value(c_ch_st[t], dat_str.ESS_STR.MC_ch[t])
            end

            @variables(MPPDC, begin
                #REGULARIZATION A PARAMETER
                ϵ == 0.0001
                r_g_st[t in t_steps]
                ############UL DECISION VARS################
                #########QUANTITY BIDS############
                0 <= G_ST[t in t_steps]
                # 0 <= SOC[t in t_steps] #ESS State of Charge var
                #############LL DECISION VARS###########
                ######PRIMALS#######
                0 <= g_st[t in t_steps] #Dispatched generation of the strategic agent
                0 <= w_st[t in t_steps] #Dispatched wind of the strategic agent
                0 <= g[i in non_str_gens, t in t_steps] #Dispatched generation
                0 <= w[k in non_str_res, t in t_steps] #Dispatched wind
                0 <= dch_st[t in t_steps] #Dispatched strategic discharge
                0 <= ch_st[t in t_steps] #Dispatched strategic charge
                0 <= d[t in t_steps] #Dispatched demand
                #######DUALS#########
                0 <= β⁺g[i in non_str_gens, t in t_steps] #Dual of the upper bound of the generation
                0 <= β⁻g[i in non_str_gens, t in t_steps] #Dual of the lower bound of the generation
                0 <= β⁺w[k in non_str_res, t in t_steps] #Dual of the upper bound of the RE generation
                0 <= β⁻w[k in non_str_res, t in t_steps] #Dual of the lower bound of the RE generation
                0 <= β⁺d[t in t_steps] #Dual of the upper bound of the demand
                0 <= β⁻d[t in t_steps] #Dual of the lower bound of the demand

                0 <= β⁺g_st[t in t_steps] #Dual of the upper bound of the str generation
                0 <= β⁻g_st[t in t_steps] #Dual of the lower bound of the str generation
                0 <= β⁺w_st[t in t_steps] #Dual of the upper bound of the str RE generation
                0 <= β⁻w_st[t in t_steps] #Dual of the lower bound of the str RE generation
                0 <= β⁺ch_st[t in t_steps] #Dual of the upper bound of the str charging
                0 <= β⁻ch_st[t in t_steps] #Dual of the lower bound of the str charging
                0 <= β⁺dch_st[t in t_steps] #Dual of the upper bound of the str discharging
                0 <= β⁻dch_st[t in t_steps] #Dual of the lower bound of the str discharging
                λ[t in t_steps] #Market price, dual of the energy balance cstr of LL
            end)

            ##########ENERGY BALANCE##########
            eq[:en_balance] = balance_eq = @expression(MPPDC, [t in t_steps],
            - g_st[t] - w_st[t] -dch_st[t] + ch_st[t]
            - sum(g[i,t] for i in non_str_gens) - sum(w[k,t] for k in non_str_res) + d[t])

            #######STRONG DUALITY EQ##########
            #PRIMAL OBJ - DUAL OBJ
            eq[:SDE_eq] = SDE_eq = @NLexpression(MPPDC,
                                sum(
                                g_st[t] * c_g_st[t] +
                                w_st[t] * c_w_st[t] +
                                dch_st[t] * c_dch_st[t] +
                                - ch_st[t] * c_ch_st[t] +
                                sum(w[k,t] * c_w[k,t] for k in non_str_res) +
                                sum(g[i,t] * c_g[i,t] for i in non_str_gens) -
                                d[t] * c_d[t] +
                                sum(β⁺g[i,t] * G[i,t] for i in non_str_gens) +
                                sum(β⁺w[k,t] * W[k,t] for k in non_str_res) +
                                r_g_st[t] +
                                β⁺w_st[t] * W_ST[t] +
                                β⁺ch_st[t] * CH_ST[t] +
                                β⁺dch_st[t] * DCH_ST[t] +
                                β⁺d[t] * D[t]
                                for t in t_steps)
            )

            ##########OBJECTIVES OF THE VARIOUS UL AGENTS#########
            #ESS-STR
            # obj_ESS = @NLexpression(MPPDC, sum(- dch_st[t] * λ[t]
            # + ch_st[t] * λ[t] for t in t_steps)
            # )
            # #GEN-STR
            # obj_GEN = @NLexpression(MPPDC, sum(c_g_st[t] * g_st[t] -
            # g_st[t] * λ[t] for t in t_steps)
            # )
            # #WIND-STR
            # obj_WIND = @NLexpression(MPPDC, sum(c_w_st[t] * w_st[t]
            # - w_st[t] * λ[t] for t in t_steps)
            # )

            #################ADDING CONSTRAINTS#############
            @constraints(MPPDC, begin
                # ENERGY BALANCE CSTR
                en_balance[t in t_steps], balance_eq[t] == 0
                #SOC RULES
                # soc_rule_0[t = 1], SOC[end] - SOC[t] + ch_st[t] * ηch - (dch_st[t]/ηdch) == 0
                # soc_rule[t = 2:t_steps[end]], SOC[t-1] - SOC[t] + ch_st[t] * ηch - (dch_st[t]/ηdch) == 0
            end)

            @NLconstraints(MPPDC, begin
                #NOTE:DUE TO THE NL PARAMETERS MOST CONSTRAINTS HAS TO BE HANDELED AS NON-LINEAR
                ############UL CONSTRAINTS############
                #NOTE:AGENT SPECIFIC CONSTRAINTS NEED TO BE FIXED FOR EACH AGENT INDIVIDUALLY
                #RAMPING CSTRS
                # #Ramping up constraint at time step 1
                # ramp_up_0[t = 1], g_st[t] - g_st[end] <= ramp_limit[t]
                # #Ramping up constraint
                # ramp_up[t = 2:t_steps[end]], g_st[t] - g_st[t-1] <= ramp_limit[t]
                # #Ramping down constraint at time step 1
                # ramp_down_0[t = 1], g_st[t] - g_st[end] >= - ramp_limit[t]
                # #Ramping down constraint
                # ramp_down[t = 2:t_steps[end]], g_st[t] - g_st[t-1] >= - ramp_limit[t]
                #ENFORCE THAT NOBODY GETS FORCE TO NEGATIVE PROFITS BY OTHER STR AGENTS
                # - obj_ESS >= 0
                # - obj_GEN >= 0
                # - obj_WIND >= 0

                #LIMITS OF THE DISPATCHED QUANTITIES OF THE STRATEGIC AGENTS, THEY CANT BE SET FROM OUTSIDE THE MODEL
                [t in t_steps], g_st[t] <= G_ST[t]
                [t in t_steps], w_st[t] <= W_ST[t]
                [t in t_steps], dch_st[t] <= DCH_ST[t]
                [t in t_steps], ch_st[t] <= CH_ST[t]
                # [t in t_steps], SOC[t] <= E_max[t]

                #FOR THE NON STR AGENTS
                [i in non_str_gens, t in t_steps], g[i,t] <= G[i,t]
                [k in non_str_res, t in t_steps], w[k,t] <= W[k,t]

                [t in t_steps], d[t] <= D[t]
                #LAGRANGIAN STATIONARITY
                #w.r.t g_{i,t}:
                [i in non_str_gens, t in t_steps], c_g[i,t] - λ[t] - β⁻g[i,t] + β⁺g[i,t] == 0
                #w.r.t g_st_{i,t}:
                [t in t_steps], c_g_st[t] - λ[t] - β⁻g_st[t] + β⁺g_st[t] == 0
                #w.r.t w_{k,t}:
                [k in non_str_res, t in t_steps], c_w[k,t] - λ[t] - β⁻w[k,t] + β⁺w[k,t] == 0
                #w.r.t w_st_{t}:
                [t in t_steps], c_w_st[t] - λ[t] - β⁻w_st[t] + β⁺w_st[t] == 0
                #w.r.t dch_st_{t}:
                [t in t_steps], c_dch_st[t] - λ[t] - β⁻dch_st[t] + β⁺dch_st[t] == 0
                #w.r.t ch_st_{t}:
                [t in t_steps], -c_ch_st[t] + λ[t] - β⁻ch_st[t] + β⁺ch_st[t] == 0
                #w.r.t d_{t}:
                [t in t_steps], -c_d[t] + λ[t] - β⁻d[t] + β⁺d[t] == 0

                #ENFORCING STRONG DUALITY
                SDE, SDE_eq == 0
                [t in t_steps], β⁺g_st[t] * G_ST[t] - r_g_st[t] <= ϵ
                [t in t_steps], -β⁺g_st[t] * G_ST[t] + r_g_st[t] <= ϵ
                #NOTE: ESS CAN RESPECT IT BY USING ECONOMIC SENSE, FOR OTHERS NEEDED
                #EXCLUSIVE CH/DCH
                #NOTE: THIS MAY HAVE TO BE ADDED
                # excl[t in t_steps], dch_st[t] * ch_st[t] == 0
            end)

            return MPPDC
        end
        ####BUILD ABSTRACT MODEL#####
        MPPDC = init_MPPDC_GEN()

    elseif current == "WIND_STR"
        ####INIT ABSTRACT MODEL######
        function init_MPPDC_RES()
            ############INIT THE MODEL INSTANCE############
            MPPDC =  Model()
            MPPDC.ext[:param] = Dict()
            param = MPPDC.ext[:param]
            MPPDC.ext[:eq] = Dict()
            eq = MPPDC.ext[:eq]
            ##############PARAMETERS IN ESS MPPDC###############
            #OTHER AGENT QUANTITY BIDS
            # @NLparameter(MPPDC, G_ST[t in t_steps] == 0) #Generation quantity bid
            param[:G_ST] = @NLparameter(MPPDC, G_ST[t in t_steps] == 0) #Wind quantity bid
            param[:DCH_ST] = @NLparameter(MPPDC, DCH_ST[t in t_steps] == 0) #ESS discharge quantity bid
            param[:CH_ST] = @NLparameter(MPPDC, CH_ST[t in t_steps] == 0) #ESS charge quantity bid
            #MAX ENERGY CONTENT
            # param[:E_max] = @NLparameter(MPPDC, E_max[t in t_steps] == 0) #max capacity of the ESS
            #PRICE TAKING AGENTS QUANTITY BIDS
            param[:G] = @NLparameter(MPPDC, G[i in non_str_gens, t in t_steps] == 0) #Generation capacity of non-str generator
            param[:W] = @NLparameter(MPPDC, W[k in non_str_res, t in t_steps]  == 0) #Generation capacity of non-str RES
            param[:D] = @NLparameter(MPPDC, D[t in t_steps]  == 0) #Max load

            #PRICE BIDS, AS QUANTITY BIDDING ONLY
            #########PRICE BIDS#############
            param[:c_g_st] = @NLparameter(MPPDC, c_g_st[t in t_steps] == 0) #Generation price bid str
            param[:c_w_st] = @NLparameter(MPPDC, c_w_st[t in t_steps]  == 0)#Wind price bid str
            param[:c_dch_st] = @NLparameter(MPPDC, c_dch_st[t in t_steps] == 0) #ESS discharge price bid str
            param[:c_ch_st] = @NLparameter(MPPDC, c_ch_st[t in t_steps] == 0) #ESS charge price bid str
            param[:c_g] = @NLparameter(MPPDC, c_g[i in non_str_gens, t in t_steps] == 0) #Generation price bid nonstr
            param[:c_w] = @NLparameter(MPPDC, c_w[k in non_str_res, t in t_steps] == 0) #Wind price bid nonstr
            param[:c_d] = @NLparameter(MPPDC, c_d[t in t_steps] == 0) #WTP of the load

            ########CONSTANT TERMS FOR RAMPING###########
            param[:ramp_limit] = @NLparameter(MPPDC, ramp_limit[t in t_steps] == 1.0) #Set to 100% as default

            for t in t_steps
                #RAMPING LIMIT OF STR GENERATOR
                JuMP.set_value(ramp_limit[t], Rᴳ * dat_str.GEN_STR.P_max[t])
                #MAXIMUM ENERGY CONTENT OF ESS
                # JuMP.set_value(E_max[t], dat_str.ESS_STR.E_max[t]) #By this the upper bound var. on SoC is set

                for i in non_str_gens
                    JuMP.set_value(c_g[i,t], dat_nonstr[i].c_g[t]) #MC OF GEN
                    JuMP.set_value(G[i,t], dat_nonstr[i].G[t]) #MAX CAPACITY OF GEN
                end
                for k in non_str_res
                    JuMP.set_value(c_w[k,t], dat_nonstr[k].c_g[t]) #COST OF WIND
                    JuMP.set_value(W[k,t], dat_nonstr[k].G[t]) #MAX WIND CAPACITY
                end
                JuMP.set_value(c_d[t], p_cap) #WTP SET TO PRICE CAP
                JuMP.set_value(D[t], load[t]) #MAX LOAD
                #W.R.T OTHER STRATEGIC AGENTS
                #QUANTITIES ARE EQUAL TO MAX CAPACITY
                # JuMP.set_value(G_ST[t], dat_str.GEN_STR.P_max[t])
                JuMP.set_value(G_ST[t], dat_str.GEN_STR.P_max[t])
                JuMP.set_value(DCH_ST[t], dat_str.ESS_STR.P_max[t])
                JuMP.set_value(CH_ST[t], dat_str.ESS_STR.P_max[t])
                # JuMP.set_value(DCH_ST[t], 0)
                # JuMP.set_value(CH_ST[t], 0)
                #PRICES ARE EQUAL TO THE MARGINAL PRICES
                JuMP.set_value(c_g_st[t], dat_str.GEN_STR.MC_g[t])
                JuMP.set_value(c_w_st[t], dat_str.WIND_STR.MC_g[t])
                #NOTE: THIS MAY HAVE TO BE REMOVED AND LET THE SOLVER CHOSE
                JuMP.set_value(c_dch_st[t], dat_str.ESS_STR.MC_dch[t])
                JuMP.set_value(c_ch_st[t], dat_str.ESS_STR.MC_ch[t])
            end

            @variables(MPPDC, begin
                #REGULARIZATION A PARAMETER
                ϵ == 0.0001
                r_w_st[t in t_steps]
                ############UL DECISION VARS################
                #########QUANTITY BIDS############
                0 <= W_ST[t in t_steps]
                # 0 <= SOC[t in t_steps] #ESS State of Charge var
                #############LL DECISION VARS###########
                ######PRIMALS#######
                0 <= g_st[t in t_steps] #Dispatched generation of the strategic agent
                0 <= w_st[t in t_steps] #Dispatched wind of the strategic agent
                0 <= g[i in non_str_gens, t in t_steps] #Dispatched generation
                0 <= w[k in non_str_res, t in t_steps] #Dispatched wind
                0 <= dch_st[t in t_steps] #Dispatched strategic discharge
                0 <= ch_st[t in t_steps] #Dispatched strategic charge
                0 <= d[t in t_steps] #Dispatched demand
                #######DUALS#########
                0 <= β⁺g[i in non_str_gens, t in t_steps] #Dual of the upper bound of the generation
                0 <= β⁻g[i in non_str_gens, t in t_steps] #Dual of the lower bound of the generation
                0 <= β⁺w[k in non_str_res, t in t_steps] #Dual of the upper bound of the RE generation
                0 <= β⁻w[k in non_str_res, t in t_steps] #Dual of the lower bound of the RE generation
                0 <= β⁺d[t in t_steps] #Dual of the upper bound of the demand
                0 <= β⁻d[t in t_steps] #Dual of the lower bound of the demand

                0 <= β⁺g_st[t in t_steps] #Dual of the upper bound of the str generation
                0 <= β⁻g_st[t in t_steps] #Dual of the lower bound of the str generation
                0 <= β⁺w_st[t in t_steps] #Dual of the upper bound of the str RE generation
                0 <= β⁻w_st[t in t_steps] #Dual of the lower bound of the str RE generation
                0 <= β⁺ch_st[t in t_steps] #Dual of the upper bound of the str charging
                0 <= β⁻ch_st[t in t_steps] #Dual of the lower bound of the str charging
                0 <= β⁺dch_st[t in t_steps] #Dual of the upper bound of the str discharging
                0 <= β⁻dch_st[t in t_steps] #Dual of the lower bound of the str discharging
                λ[t in t_steps] #Market price, dual of the energy balance cstr of LL
            end)

            ##########ENERGY BALANCE##########
            eq[:en_balance] = balance_eq = @expression(MPPDC, [t in t_steps],
            - g_st[t] - w_st[t] -dch_st[t] + ch_st[t]
            - sum(g[i,t] for i in non_str_gens) - sum(w[k,t] for k in non_str_res) + d[t])

            #######STRONG DUALITY EQ##########
            #PRIMAL OBJ - DUAL OBJ
            eq[:SDE_eq] = SDE_eq = @NLexpression(MPPDC,
                                sum(
                                g_st[t] * c_g_st[t] +
                                w_st[t] * c_w_st[t] +
                                dch_st[t] * c_dch_st[t] +
                                - ch_st[t] * c_ch_st[t] +
                                sum(w[k,t] * c_w[k,t] for k in non_str_res) +
                                sum(g[i,t] * c_g[i,t] for i in non_str_gens) -
                                d[t] * c_d[t] +
                                sum(β⁺g[i,t] * G[i,t] for i in non_str_gens) +
                                sum(β⁺w[k,t] * W[k,t] for k in non_str_res) +
                                β⁺g_st[t] * G_ST[t] +
                                r_w_st[t] +
                                β⁺ch_st[t] * CH_ST[t] +
                                β⁺dch_st[t] * DCH_ST[t] +
                                β⁺d[t] * D[t]
                                for t in t_steps)
            )

            ##########OBJECTIVES OF THE VARIOUS UL AGENTS#########
            #ESS-STR
            # obj_ESS = @NLexpression(MPPDC, sum(- dch_st[t] * λ[t]
            # + ch_st[t] * λ[t] for t in t_steps)
            # )
            # #GEN-STR
            # obj_GEN = @NLexpression(MPPDC, sum(c_g_st[t] * g_st[t] -
            # g_st[t] * λ[t] for t in t_steps)
            # )
            # #WIND-STR
            # obj_WIND = @NLexpression(MPPDC, sum(c_w_st[t] * w_st[t]
            # - w_st[t] * λ[t] for t in t_steps)
            # )

            #################ADDING CONSTRAINTS#############
            @constraints(MPPDC, begin
                # ENERGY BALANCE CSTR
                en_balance[t in t_steps], balance_eq[t] == 0
                #SOC RULES
                # soc_rule_0[t = 1], SOC[end] - SOC[t] + ch_st[t] * ηch - (dch_st[t]/ηdch) == 0
                # soc_rule[t = 2:t_steps[end]], SOC[t-1] - SOC[t] + ch_st[t] * ηch - (dch_st[t]/ηdch) == 0
            end)

            @NLconstraints(MPPDC, begin
                #NOTE:DUE TO THE NL PARAMETERS MOST CONSTRAINTS HAS TO BE HANDELED AS NON-LINEAR
                ############UL CONSTRAINTS############
                #NOTE:AGENT SPECIFIC CONSTRAINTS NEED TO BE FIXED FOR EACH AGENT INDIVIDUALLY
                #RAMPING CSTRS
                # #Ramping up constraint at time step 1
                # ramp_up_0[t = 1], g_st[t] - g_st[end] <= ramp_limit[t]
                # #Ramping up constraint
                # ramp_up[t = 2:t_steps[end]], g_st[t] - g_st[t-1] <= ramp_limit[t]
                # #Ramping down constraint at time step 1
                # ramp_down_0[t = 1], g_st[t] - g_st[end] >= - ramp_limit[t]
                # #Ramping down constraint
                # ramp_down[t = 2:t_steps[end]], g_st[t] - g_st[t-1] >= - ramp_limit[t]
                # #ENFORCE THAT NOBODY GETS FORCE TO NEGATIVE PROFITS BY OTHER STR AGENTS
                # - obj_ESS >= 0
                # - obj_GEN >= 0
                # - obj_WIND >= 0

                #LIMITS OF THE DISPATCHED QUANTITIES OF THE STRATEGIC AGENTS, THEY CANT BE SET FROM OUTSIDE THE MODEL
                [t in t_steps], g_st[t] <= G_ST[t]
                [t in t_steps], w_st[t] <= W_ST[t]
                [t in t_steps], dch_st[t] <= DCH_ST[t]
                [t in t_steps], ch_st[t] <= CH_ST[t]
                # [t in t_steps], SOC[t] <= E_max[t]

                #FOR THE NON STR AGENTS
                [i in non_str_gens, t in t_steps], g[i,t] <= G[i,t]
                [k in non_str_res, t in t_steps], w[k,t] <= W[k,t]

                [t in t_steps], d[t] <= D[t]
                #LAGRANGIAN STATIONARITY
                #w.r.t g_{i,t}:
                [i in non_str_gens, t in t_steps], c_g[i,t] - λ[t] - β⁻g[i,t] + β⁺g[i,t] == 0
                #w.r.t g_st_{i,t}:
                [t in t_steps], c_g_st[t] - λ[t] - β⁻g_st[t] + β⁺g_st[t] == 0
                #w.r.t w_{k,t}:
                [k in non_str_res, t in t_steps], c_w[k,t] - λ[t] - β⁻w[k,t] + β⁺w[k,t] == 0
                #w.r.t w_st_{t}:
                [t in t_steps], c_w_st[t] - λ[t] - β⁻w_st[t] + β⁺w_st[t] == 0
                #w.r.t dch_st_{t}:
                [t in t_steps], c_dch_st[t] - λ[t] - β⁻dch_st[t] + β⁺dch_st[t] == 0
                #w.r.t ch_st_{t}:
                [t in t_steps], -c_ch_st[t] + λ[t] - β⁻ch_st[t] + β⁺ch_st[t] == 0
                #w.r.t d_{t}:
                [t in t_steps], -c_d[t] + λ[t] - β⁻d[t] + β⁺d[t] == 0

                #ENFORCING STRONG DUALITY
                SDE, SDE_eq == 0
                [t in t_steps], β⁺w_st[t] * W_ST[t] - r_w_st[t] <= ϵ
                [t in t_steps], -β⁺w_st[t] * W_ST[t] + r_w_st[t] <= ϵ
                #NOTE: ESS CAN RESPECT IT BY USING ECONOMIC SENSE, FOR OTHERS NEEDED
                #EXCLUSIVE CH/DCH
                #NOTE: THIS MAY HAVE TO BE ADDED
                # excl[t in t_steps], dch_st[t] * ch_st[t] == 0
            end)

            return MPPDC
        end
        ####BUILD ABSTRACT MODEL#####
        MPPDC = init_MPPDC_RES()
    else
        println("ERROR: The current agent is not part of the list of str agents!!!")
    end

    ###############SETTING VARIABLE BOUNDS#############
    function set_UL_bounds()
        if current == "ESS_STR"
            for t in t_steps
                JuMP.set_upper_bound(MPPDC[:DCH_ST][t], dat_str.ESS_STR.P_max[t])
                JuMP.set_upper_bound(MPPDC[:CH_ST][t], dat_str.ESS_STR.P_max[t])
                JuMP.set_upper_bound(MPPDC[:c_ch_st][t], p_cap)
                JuMP.set_lower_bound(MPPDC[:c_ch_st][t], p_floor)
                JuMP.set_upper_bound(MPPDC[:c_dch_st][t], p_cap)
                JuMP.set_lower_bound(MPPDC[:c_dch_st][t], p_floor)
            end

        elseif current == "GEN_STR"
            for t in t_steps
                JuMP.set_upper_bound(MPPDC[:G_ST][t], dat_str.GEN_STR.P_max[t])
            end

        elseif current == "WIND_STR"
            for t in t_steps
                JuMP.set_upper_bound(MPPDC[:W_ST][t], dat_str.WIND_STR.P_max[t])
            end
        else
            println("ERROR: The current agent is not part of the list of str agents!!!")
        end
    end

    setting_UL_bounds = set_UL_bounds()

    ##########SET THE OBJECTIVE ACCORDING TO THE UL AGENT##########
    function return_objective()
        #DECLARATION OF THE CURRENT OBJECTIVE
        if current == "ESS_STR"
            if objective_type == "primal"
                obj = sum(- MPPDC[:dch_st][t] * MPPDC[:λ][t]
                + MPPDC[:ch_st][t] * MPPDC[:λ][t] for t in t_steps)
            elseif objective_type == "dual"
                obj =
                - (- sum(MPPDC[:g_st][t] * dat_str.GEN_STR.MC_g[t] for t in t_steps) #GEN_STR
                - sum(MPPDC[:w_st][t] * dat_str.WIND_STR.MC_g[t] for t in t_steps) #WIND_STR
                - sum(sum(MPPDC[:g][i,t] * dat_nonstr[i].c_g[t] for t in t_steps) for i in non_str_gens) #GENs
                - sum(sum(MPPDC[:w][k,t] * dat_nonstr[k].c_g[t] for t in t_steps) for k in non_str_res) #GENs
                + sum(MPPDC[:d][t] .* p_cap for t in t_steps)
                - sum(MPPDC[:β⁺g_st][t] * dat_str.GEN_STR.P_max[t] for t in t_steps) #GEN_STR
                - sum(MPPDC[:β⁺w_st][t] * dat_str.WIND_STR.P_max[t] for t in t_steps) #WIND_STR
                - sum(sum(MPPDC[:β⁺g][i,t] * dat_nonstr[i].G[t] for t in t_steps) for i in non_str_gens) #GENs
                - sum(sum(MPPDC[:β⁺w][k,t] * dat_nonstr[k].G[t] for t in t_steps) for k in non_str_res) #GENs
                - sum(MPPDC[:β⁺d][t] * load[t] for t in t_steps)
                )
            else
                println("ERROR: Correct objective type has to be chosen!!!")
            end

        elseif current == "GEN_STR"
            if objective_type == "primal"
                obj = sum(dat_str.GEN_STR.MC_g[t] * MPPDC[:g_st][t] -
                MPPDC[:g_st][t] * MPPDC[:λ][t] for t in t_steps)
            elseif objective_type == "dual"
                obj =
                - (
                # - sum(MPPDC[:g_st][t] * dat_str.GEN_STR.MC_g[t] for t in t_steps) #GEN_STR
                - sum(MPPDC[:dch_st][t] * dat_str.ESS_STR.c_dch[t] for t in t_steps) #DCH_STR
                + sum(MPPDC[:ch_st][t] * dat_str.ESS_STR.c_ch[t] for t in t_steps) #DCH_STR
                - sum(MPPDC[:w_st][t] * dat_str.WIND_STR.MC_g[t] for t in t_steps) #WIND_STR
                - sum(sum(MPPDC[:g][i,t] * dat_nonstr[i].c_g[t] for t in t_steps) for i in non_str_gens) #GENs
                - sum(sum(MPPDC[:w][k,t] * dat_nonstr[k].c_g[t] for t in t_steps) for k in non_str_res) #GENs
                + sum(MPPDC[:d][t] .* p_cap for t in t_steps)
                # - sum(MPPDC[:β⁺g_st][t] * dat_str.GEN_STR.P_max[t] for t in t_steps) #GEN_STR
                - sum(MPPDC[:β⁺dch_st][t] * dat_str.ESS_STR.P_max[t] for t in t_steps) #WIND_STR
                - sum(MPPDC[:β⁺ch_st][t] * dat_str.ESS_STR.P_max[t] for t in t_steps) #WIND_STR
                - sum(MPPDC[:β⁺w_st][t] * dat_str.WIND_STR.P_max[t] for t in t_steps) #WIND_STR
                - sum(sum(MPPDC[:β⁺g][i,t] * dat_nonstr[i].G[t] for t in t_steps) for i in non_str_gens) #GENs
                - sum(sum(MPPDC[:β⁺w][k,t] * dat_nonstr[k].G[t] for t in t_steps) for k in non_str_res) #GENs
                - sum(MPPDC[:β⁺d][t] * load[t] for t in t_steps)
                )
            else
                println("ERROR: Correct objective type has to be chosen!!!")
            end

        elseif current == "WIND_STR"

            if objective_type == "primal"
                obj = sum(dat_str.WIND_STR.MC_g[t] * MPPDC[:w_st][t]
                - MPPDC[:w_st][t] * MPPDC[:λ][t] for t in t_steps)
            elseif objective_type == "dual"
                obj =
                - (
                - sum(MPPDC[:g_st][t] * dat_str.GEN_STR.MC_g[t] for t in t_steps) #GEN_STR
                - sum(MPPDC[:dch_st][t] * dat_str.ESS_STR.c_dch[t] for t in t_steps) #DCH_STR
                + sum(MPPDC[:ch_st][t] * dat_str.ESS_STR.c_ch[t] for t in t_steps) #DCH_STR
                # - sum(MPPDC[:w_st][t] * dat_str.WIND_STR.MC_g[t] for t in t_steps) #WIND_STR
                - sum(sum(MPPDC[:g][i,t] * dat_nonstr[i].c_g[t] for t in t_steps) for i in non_str_gens) #GENs
                - sum(sum(MPPDC[:w][k,t] * dat_nonstr[k].c_g[t] for t in t_steps) for k in non_str_res) #GENs
                + sum(MPPDC[:d][t] .* p_cap for t in t_steps)
                - sum(MPPDC[:β⁺g_st][t] * dat_str.GEN_STR.P_max[t] for t in t_steps) #GEN_STR
                - sum(MPPDC[:β⁺dch_st][t] * dat_str.ESS_STR.P_max[t] for t in t_steps) #WIND_STR
                - sum(MPPDC[:β⁺ch_st][t] * dat_str.ESS_STR.P_max[t] for t in t_steps) #WIND_STR
                # - sum(MPPDC[:β⁺w_st][t] * dat_str.WIND_STR.P_max[t] for t in t_steps) #WIND_STR
                - sum(sum(MPPDC[:β⁺g][i,t] * dat_nonstr[i].G[t] for t in t_steps) for i in non_str_gens) #GENs
                - sum(sum(MPPDC[:β⁺w][k,t] * dat_nonstr[k].G[t] for t in t_steps) for k in non_str_res) #GENs
                - sum(MPPDC[:β⁺d][t] * load[t] for t in t_steps)
                )

            else
                println("ERROR: Correct objective type has to be chosen!!!")
            end

        else
            println("ERROR: The current agent is not part of the list of str agents!!!")
        end

        return obj
    end

    obj = return_objective()

    JuMP.set_objective_function(MPPDC, obj)
    JuMP.set_objective_sense(MPPDC, MOI.MIN_SENSE)

    ##########SETTING THE OPTIMIZER#########
    set_optimizer(MPPDC, optimizer)

    return MPPDC
end


text:
abstract type AbstractHyperedgeCutModel end

@inline (mdl::AbstractHyperedgeCutModel)(n_vertices, n_pins, n_self_nets, n_cut_nets, k) = mdl(n_vertices, n_pins, n_self_nets, n_cut_nets)

struct AffineHyperedgeCutModel{Tv} <: AbstractHyperedgeCutModel
    α::Tv
    β_vertex::Tv
    β_pin::Tv
    β_self_net::Tv
    β_cut_net::Tv
end

function AffineHyperedgeCutModel(; α = false, β_vertex = false, β_pin = false, β_self_net = false, β_cut_net = false)
    AffineHyperedgeCutModel(promote(α, β_vertex, β_pin, β_self_net, β_cut_net)...)
end

@inline cost_type(::Type{AffineHyperedgeCutModel{Tv}}) where {Tv} = Tv

(mdl::AffineHyperedgeCutModel)(n_vertices, n_pins, n_self_nets, n_cut_nets) = mdl.α + n_vertices * mdl.β_vertex + n_pins * mdl.β_pin + n_self_nets * mdl.β_self_net + n_cut_nets * mdl.β_cut_net

struct HyperedgeCutOracle{Ti, Net, SelfNet, Mdl} <: AbstractOracleCost{Mdl}
    pos::Vector{Ti}
    net::Net
    selfnet::SelfNet
    mdl::Mdl
end

oracle_model(ocl::HyperedgeCutOracle) = ocl.mdl

function oracle_stripe(hint::AbstractHint, mdl::AbstractHyperedgeCutModel, A::SparseMatrixCSC{Tv, Ti}; net=nothing, adj_A=nothing, kwargs...) where {Tv, Ti}
    @inbounds begin
        m, n = size(A)
        pos = A.colptr
        if net === nothing
            net = netcount(hint, A; kwargs...)
        end
        selfnet = selfnetcount(hint, A; kwargs...)
        return HyperedgeCutOracle(pos, net, selfnet, mdl)
    end
end

@inline function (cst::HyperedgeCutOracle{Ti, Mdl})(j, j′, k...) where {Ti, Mdl}
    @inbounds begin
        w = cst.pos[j′] - cst.pos[j]
        d = cst.net[j, j′]
        l = cst.selfnet[j, j′]
        return cst.mdl(j′ - j, w, l, d - l, k...)
    end
end

@inline function (stp::Step{Ocl})(_j, _j′, _k...) where {Ti, Mdl, Ocl <: HyperedgeCutOracle{Ti, Mdl}}
    @inbounds begin
        cst = stp.ocl
        j = destep(_j)
        j′ = destep(_j′)
        k = maptuple(destep, _k...)
        w = cst.pos[j′] - cst.pos[j]
        d = Step(cst.net)(_j, _j′)
        l = Step(cst.selfnet)(_j, _j′)
        return cst.mdl(j′ - j, w, l, d - l, k...)
    end
end


text:
### A Pluto.jl notebook ###
# v0.15.1

using Markdown
using InteractiveUtils

# ╔═╡ 1dffac12-ef71-11eb-3d44-537b4ed77dc9
using PlutoUI

# ╔═╡ 63456a8a-6821-446c-9279-8a9fc269b047
md"""# Pokémon Fights In Julia
Welcome great adventurer, in this notebook we will take you on a `Pokémon` journey. We will try to take adnvantage of the **multiple dispatch** system of Julia by implementing a very efficient Pokémon fight system.
"""

# ╔═╡ 8efc6f7a-1336-4118-acec-29f751cf7aac
md"""Something to get you in the right mood:\
$(Resource("https://vgmsite.com/soundtracks/pokemon-gameboy-sound-collection/gbhogmtx/107-battle%20%28vs%20wild%20pokemon%29.mp3"))"""

# ╔═╡ 0f6fc6b1-6d79-4ab7-9263-92adecb6e495
md"""## The Mechanics
Let us first take a look at the fighting mechanics of Pokémon.
As you can see in the image below there are different types and each type is effective against some types but weak against others.

$(Resource("https://img.pokemondb.net/images/typechart.png"))
*Image from https://img.pokemondb.net/images/typechart.png*

So let us first start by defining a general PokéType.
"""

# ╔═╡ fc131461-7d14-4301-aad5-1c91ed54c972
abstract type PokéType end

# ╔═╡ d53461a0-8124-4108-b8a9-909313c01b55
md"Next we will define all the separate types that we will need later on."

# ╔═╡ 7fed909f-d803-4059-b253-8b1f1fe930a3
begin
	struct Normal <: PokéType end
	struct Fire <: PokéType end
	struct Water <: PokéType end
	struct Electric <: PokéType end
	struct Grass <: PokéType end
	struct Ice <: PokéType end
	struct Fighting <: PokéType end
	struct Poison <: PokéType end
	struct Ground <: PokéType end
	struct Flying <: PokéType end
	struct Psychic <: PokéType end
	struct Bug <: PokéType end
	struct Rock <: PokéType end
	struct Ghost <: PokéType end
	struct Dragon <: PokéType end
	struct Dark <: PokéType end
	struct Steel <: PokéType end
	struct Fairy <: PokéType end
end

# ╔═╡ 78ca9717-a44c-4289-8827-4cd67274059f
md"""## The Core Function
Now it's time to create our effect function. This function will have as an input the type of the attacker and the type of the defender and then determine how effective the attack was.
"""

# ╔═╡ 8fd10c7d-e3e3-4575-8694-22b10f9a9c41
begin
	NO_EFFECT = 0
	NOT_VERY_EFFECTIVE = 0.5
	NORMAL_EFFECTIVE   = 1.0
	SUPER_EFFECTIVE    = 2.0
end;

# ╔═╡ 6fd02628-ccf5-4f85-a03c-2591fbdf3eb5
md"First we'll define the most general case, this way our function will always have an output for every Pokémon battle."

# ╔═╡ c4632665-51f2-46bf-b2aa-4849d635de43
function eff(atk::T1,def::T2) where {T1 <: PokéType, T2 <: PokéType}
	return NORMAL_EFFECTIVE
end

# ╔═╡ a3e53c87-5f15-47d0-a80c-4d5388b31eb8
md"Now we need to define all the other cases. Normally we this would mean defining 18x18 = $(18*18) cases. Luckily for us there are some patterns that make it so we don't need to define all of them. We will start by defining the cases where the attacker and defender are of the same type, then move on to all the `SUPER_EFFECTIVE` moves and so on."

# ╔═╡ 20ec0a99-9b6e-40a4-aa44-144765b86a3d
begin
	eff(atk::T,def::T) where {T <: PokéType} = NOT_VERY_EFFECTIVE
	eff(atk::Normal,def::Normal) = NORMAL_EFFECTIVE
	eff(atk::Fighting,def::Fighting) = NORMAL_EFFECTIVE
	eff(atk::Ground,def::Ground) = NORMAL_EFFECTIVE
	eff(atk::Flying,def::Flying) = NORMAL_EFFECTIVE
	eff(atk::Bug,def::Bug) = NORMAL_EFFECTIVE
	eff(atk::Rock,def::Rock) = NORMAL_EFFECTIVE
	eff(atk::Fairy,def::Fairy) = NORMAL_EFFECTIVE
	eff(atk::Ghost,def::Ghost) = SUPER_EFFECTIVE
	eff(atk::Dragon,def::Dragon) = SUPER_EFFECTIVE
end

# ╔═╡ af7579be-e3ee-408b-bb7e-2c1362a0c427
md"`SUPER_EFFECTIVE` fights:"

# ╔═╡ 9997973b-2992-4df2-ad71-b25fb80a3655
begin
	eff(atk::Fire,def::Grass) = SUPER_EFFECTIVE
	eff(atk::Fire,def::Ice) = SUPER_EFFECTIVE
	eff(atk::Fire,def::Bug) = SUPER_EFFECTIVE
	eff(atk::Fire,def::Steel) = SUPER_EFFECTIVE
	eff(atk::Water,def::Fire) = SUPER_EFFECTIVE
	eff(atk::Water,def::Ground) = SUPER_EFFECTIVE
	eff(atk::Water,def::Rock) = SUPER_EFFECTIVE
	eff(atk::Electric,def::Water) = SUPER_EFFECTIVE
	eff(atk::Electric,def::Flying) = SUPER_EFFECTIVE
	eff(atk::Grass,def::Water) = SUPER_EFFECTIVE
	eff(atk::Grass,def::Ground) = SUPER_EFFECTIVE
	eff(atk::Grass,def::Rock) = SUPER_EFFECTIVE
	eff(atk::Ice,def::Grass) = SUPER_EFFECTIVE
	eff(atk::Ice,def::Ground) = SUPER_EFFECTIVE
	eff(atk::Ice,def::Flying) = SUPER_EFFECTIVE
	eff(atk::Ice,def::Dragon) = SUPER_EFFECTIVE
	eff(atk::Fighting,def::Normal) = SUPER_EFFECTIVE
	eff(atk::Fighting,def::Ice) = SUPER_EFFECTIVE
	eff(atk::Fighting,def::Rock) = SUPER_EFFECTIVE
	eff(atk::Fighting,def::Dark) = SUPER_EFFECTIVE
	eff(atk::Fighting,def::Steel) = SUPER_EFFECTIVE
	eff(atk::Poison,def::Grass) = SUPER_EFFECTIVE
	eff(atk::Poison,def::Fairy) = SUPER_EFFECTIVE
	eff(atk::Ground,def::Fire) = SUPER_EFFECTIVE
	eff(atk::Ground,def::Electric) = SUPER_EFFECTIVE
	eff(atk::Ground,def::Poison) = SUPER_EFFECTIVE
	eff(atk::Ground,def::Rock) = SUPER_EFFECTIVE
	eff(atk::Ground,def::Steel) = SUPER_EFFECTIVE
	eff(atk::Flying,def::Grass) = SUPER_EFFECTIVE
	eff(atk::Flying,def::Fighting) = SUPER_EFFECTIVE
	eff(atk::Flying,def::Bug) = SUPER_EFFECTIVE
	eff(atk::Psychic,def::Fighting) = SUPER_EFFECTIVE
	eff(atk::Psychic,def::Poison) = SUPER_EFFECTIVE
	eff(atk::Bug,def::Grass) = SUPER_EFFECTIVE
	eff(atk::Bug,def::Psychic) = SUPER_EFFECTIVE
	eff(atk::Bug,def::Dark) = SUPER_EFFECTIVE
	eff(atk::Rock,def::Fire) = SUPER_EFFECTIVE
	eff(atk::Rock,def::Ice) = SUPER_EFFECTIVE
	eff(atk::Rock,def::Flying) = SUPER_EFFECTIVE
	eff(atk::Rock,def::Bug) = SUPER_EFFECTIVE
	eff(atk::Ghost,def::Psychic) = SUPER_EFFECTIVE
	eff(atk::Dark,def::Psychic) = SUPER_EFFECTIVE
	eff(atk::Dark,def::Ghost) = SUPER_EFFECTIVE
	eff(atk::Steel,def::Ice) = SUPER_EFFECTIVE
	eff(atk::Steel,def::Rock) = SUPER_EFFECTIVE
	eff(atk::Steel,def::Fairy) = SUPER_EFFECTIVE
	eff(atk::Fairy,def::Fighting) = SUPER_EFFECTIVE
	eff(atk::Fairy,def::Dragon) = SUPER_EFFECTIVE
	eff(atk::Fairy,def::Dark) = SUPER_EFFECTIVE

end

# ╔═╡ 1e075548-6898-457b-8cec-dbccc0fd8ec7
md"`NOT_VERY_EFFECTIVE` fights:"

# ╔═╡ eb578e80-f4fe-4c14-91b7-d7b267bd9046
begin
	eff(atk::Normal,def::Rock) = NOT_VERY_EFFECTIVE
	eff(atk::Normal,def::Steel) = NOT_VERY_EFFECTIVE
	eff(atk::Fire,def::Water) = NOT_VERY_EFFECTIVE
	eff(atk::Fire,def::Rock) = NOT_VERY_EFFECTIVE
	eff(atk::Fire,def::Dragon) = NOT_VERY_EFFECTIVE
	eff(atk::Water,def::Grass) = NOT_VERY_EFFECTIVE
	eff(atk::Water,def::Dragon) = NOT_VERY_EFFECTIVE
	eff(atk::Electric,def::Grass) = NOT_VERY_EFFECTIVE
	eff(atk::Electric,def::Dragon) = NOT_VERY_EFFECTIVE
	eff(atk::Grass,def::Fire) = NOT_VERY_EFFECTIVE
	eff(atk::Grass,def::Poison) = NOT_VERY_EFFECTIVE
	eff(atk::Grass,def::Bug) = NOT_VERY_EFFECTIVE
	eff(atk::Grass,def::Dragon) = NOT_VERY_EFFECTIVE
	eff(atk::Grass,def::Steel) = NOT_VERY_EFFECTIVE
	eff(atk::Grass,def::Flying) = NOT_VERY_EFFECTIVE
	eff(atk::Ice,def::Fire) = NOT_VERY_EFFECTIVE
	eff(atk::Ice,def::Water) = NOT_VERY_EFFECTIVE
	eff(atk::Ice,def::Steel) = NOT_VERY_EFFECTIVE
	eff(atk::Fighting,def::Poison) = NOT_VERY_EFFECTIVE
	eff(atk::Fighting,def::Flying) = NOT_VERY_EFFECTIVE
	eff(atk::Fighting,def::Psychic) = NOT_VERY_EFFECTIVE
	eff(atk::Fighting,def::Bug) = NOT_VERY_EFFECTIVE
	eff(atk::Fighting,def::Fairy) = NOT_VERY_EFFECTIVE
	eff(atk::Poison,def::Ground) = NOT_VERY_EFFECTIVE
	eff(atk::Poison,def::Rock) = NOT_VERY_EFFECTIVE
	eff(atk::Poison,def::Ghost) = NOT_VERY_EFFECTIVE
	eff(atk::Ground,def::Grass) = NOT_VERY_EFFECTIVE
	eff(atk::Ground,def::Bug) = NOT_VERY_EFFECTIVE
	eff(atk::Flying,def::Electric) = NOT_VERY_EFFECTIVE
	eff(atk::Flying,def::Rock) = NOT_VERY_EFFECTIVE
	eff(atk::Flying,def::Steel) = NOT_VERY_EFFECTIVE
	eff(atk::Psychic,def::Steel) = NOT_VERY_EFFECTIVE
	eff(atk::Bug,def::Fire) = NOT_VERY_EFFECTIVE
	eff(atk::Bug,def::Poison) = NOT_VERY_EFFECTIVE
	eff(atk::Bug,def::Fighting) = NOT_VERY_EFFECTIVE
	eff(atk::Bug,def::Flying) = NOT_VERY_EFFECTIVE
	eff(atk::Bug,def::Steel) = NOT_VERY_EFFECTIVE
	eff(atk::Bug,def::Fairy) = NOT_VERY_EFFECTIVE
	eff(atk::Bug,def::Ghost) = NOT_VERY_EFFECTIVE
	eff(atk::Rock,def::Fighting) = NOT_VERY_EFFECTIVE
	eff(atk::Rock,def::Ground) = NOT_VERY_EFFECTIVE
	eff(atk::Rock,def::Steel) = NOT_VERY_EFFECTIVE
	eff(atk::Ghost,def::Dark) = NOT_VERY_EFFECTIVE
	eff(atk::Dragon,def::Steel) = NOT_VERY_EFFECTIVE
	eff(atk::Dark,def::Fighting) = NOT_VERY_EFFECTIVE
	eff(atk::Dark,def::Fairy) = NOT_VERY_EFFECTIVE
	eff(atk::Steel,def::Fire) = NOT_VERY_EFFECTIVE
	eff(atk::Steel,def::Water) = NOT_VERY_EFFECTIVE
	eff(atk::Steel,def::Electric) = NOT_VERY_EFFECTIVE
	eff(atk::Fairy,def::Fire) = NOT_VERY_EFFECTIVE
	eff(atk::Fairy,def::Poison) = NOT_VERY_EFFECTIVE
	eff(atk::Fairy,def::Steel) = NOT_VERY_EFFECTIVE
end

# ╔═╡ f6c4ed81-0251-4973-bdc7-75951a2657c7
md"`NO_EFFECT` fights:"

# ╔═╡ 2e80f947-1447-4809-b31a-337abaedd955
begin
	eff(atk::Normal,def::Ghost) = NO_EFFECT
	eff(atk::Electric,def::Ground) = NO_EFFECT
	eff(atk::Fighting,def::Ghost) = NO_EFFECT
	eff(atk::Poison,def::Steel) = NO_EFFECT
	eff(atk::Ground,def::Flying) = NO_EFFECT
	eff(atk::Psychic,def::Dark) = NO_EFFECT
	eff(atk::Ghost,def::Normal) = NO_EFFECT
	eff(atk::Dragon,def::Fairy) = NO_EFFECT
end

# ╔═╡ db9ba5b4-0573-4bd0-9359-24cdc2885082
md"""Now that we have defined all of the effects, it's time to take a small brake and heal up at the PokéCenter!\
$(Resource("https://vgmsite.com/soundtracks/pokemon-gameboy-sound-collection/znqhqixi/111-pokemon%20recovery.mp3"))\
$(Resource("http://25.media.tumblr.com/tumblr_lzm9prI6UJ1qeh39oo1_500.gif"))\
*Gif from http://pokemondailylr.blogspot.com/2012/12/pokemon-center.html* """

# ╔═╡ 4c5e5e24-aea6-4573-b9a2-f25d83c22f19
md"""## The Actual Fight Function
Now we will define the last couple of functions that we need to implement the fighting system. First we'll make a function that makes a nice string based on the effectiveness of an attack, next we will use that function to complete our attack function.
"""

# ╔═╡ 7539d614-67d5-4a2a-b03a-00c5f873c8ae
function eff_string(effectiveness)
	if effectiveness == 0
		return "not effective"
	elseif effectiveness < 1.0
		return "not very effective ($effectiveness × damage)"
	elseif effectiveness == 1
		return "effective"
	else
		return "very effective  ($effectiveness × damage)"
	end
end

# ╔═╡ e9b22426-0c7f-4f15-8bd3-361b3b5ed5cf
md"We also want to correctly display the type of our Pokémon. For this reason we overload the `Base.show()` function."

# ╔═╡ 0be3dd43-188f-460d-8df8-3ae9ac8adae1
Base.string(x::PokéType) = split(string(typeof(x)),".")[end]

# ╔═╡ a5ebeeca-349e-4777-9acf-491da20efd88
Base.show(io::IO, x::PokéType) = string(x)

# ╔═╡ 2326a03a-3f53-420f-896b-cf3620f06a60
md"So now it's time for a small test:"

# ╔═╡ ae08b2e9-f864-458d-89e5-ebe07320f322
md"""## Dual Types
So now our function works for Pokémons with only one type but as we all know there are a lot of Pokémon with two types. Defining a function that lets us use our `attack!()` function for dual type Pokémon is actually very easy.

First we define what a dualtype is:
"""

# ╔═╡ b0f76cc7-0fb4-48ca-9844-c0fe186b32eb
struct DualType <: PokéType
	Type1::PokéType
	Type2::PokéType
end

# ╔═╡ c8f1021f-f230-428c-9685-0eb03a63def8
md"Let us also display the `DualType` correctly."

# ╔═╡ 0ce4e7e0-3d99-4ae4-8a11-905ce6e7708f
Base.show(io::IO, x::DualType) = "$(string(x.Type1))-$(string(x.Type2))"

# ╔═╡ e4870042-9c91-41d8-8770-47940cffce0a
md"Now we adjust our `effect` function so that it knows what to do with Dualtypes. When the Dual type is the attacker, it will choose its most effective attack."

# ╔═╡ 7ff0187e-12c2-497d-b5d1-29dd0770a8e1
eff(atk::DualType, def::PokéType) = max(eff(atk.Type1, def), eff(atk.Type2, def))

# ╔═╡ 33cb3990-d126-41e1-ac9f-c9f9d6b85ae5
md"When the dual type is the defender, we have to compute the correct effect of the attack."

# ╔═╡ bda2dba7-4be5-4ad0-bfdc-ad02a64297a4
eff(atk::PokéType, def::DualType) = eff(atk, def.Type1) * eff(atk, def.Type2)

# ╔═╡ aa5abb6c-2289-4c34-a062-2e84187559dd
function attack!(atk::PokéType, def::PokéType)
    effectiveness = eff(atk, def)
	println("A $(show(atk)) Pokémon used an attack")
    println("against a $(show(def)) Pokémon")
    println("it was $(eff_string(effectiveness))")
end

# ╔═╡ a5ecc6c7-d08b-4cf8-8a3b-580ada462edc
begin
	Squirtle = Water()
	Charmander = Fire()
	with_terminal() do
		attack!(Squirtle,Charmander)
	end
end

# ╔═╡ fd2d5874-46a0-47ae-9f13-45079f42cfab
md"So now we can very easily define a Pokémon with two types!"

# ╔═╡ faa0448f-7470-4d2e-84bf-f2dcaa92fc37
begin
	Venusaur = DualType(Grass(),Poison())

	with_terminal() do
		println("Venusaur vs Charmander:")
		attack!(Venusaur, Charmander)
		attack!(Charmander, Venusaur)
		println("")
		println("Venusaur vs Squirtle:")
		attack!(Venusaur, Squirtle)
		attack!(Squirtle, Venusaur)
	end
end

# ╔═╡ 4d67eb11-e859-4213-ae0b-9298d5c79bdf
show(Venusaur)

# ╔═╡ 88d51607-8d4f-4087-ab00-2113a9c65ad4
md"""As you can see the sky is the limit with these types and take note of the fact that it's all calculated very efficiently!\
For now this notebook is over but the Pokémon adventure never stops!\
$(Resource("https://vgmsite.com/soundtracks/pokemon-gameboy-sound-collection/gjqttmbp/108-victory%20%28vs%20wild%20pokemon%29.mp3"))
"""

# ╔═╡ 6209b50b-5ded-4529-a895-2ae533ef63b6
md"""[^1]This notebook was based on a great blogpost about Pokémon and Julia. You can find it here: [https://www.moll.dev/projects/effective-multi-dispatch/?s=03](https://www.moll.dev/projects/effective-multi-dispatch/?s=03).\
\
[^2]The sounds used in this notebook where all gathered from: [https://downloads.khinsider.com/game-soundtracks/album/pokemon-gameboy-sound-collection](https://downloads.khinsider.com/game-soundtracks/album/pokemon-gameboy-sound-collection)."""

# ╔═╡ 00000000-0000-0000-0000-000000000001
PLUTO_PROJECT_TOML_CONTENTS = """
[deps]
PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8"

[compat]
PlutoUI = "~0.7.9"
"""

# ╔═╡ 00000000-0000-0000-0000-000000000002
PLUTO_MANIFEST_TOML_CONTENTS = """
# This file is machine-generated - editing it directly is not advised

[[Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"

[[InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.1"

[[Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

[[Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"

[[Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"

[[Parsers]]
deps = ["Dates"]
git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "1.1.0"

[[PlutoUI]]
deps = ["Base64", "Dates", "InteractiveUtils", "JSON", "Logging", "Markdown", "Random", "Reexport", "Suppressor"]
git-tree-sha1 = "44e225d5837e2a2345e69a1d1e01ac2443ff9fcb"
uuid = "7f904dfe-b85e-4ff6-b463-dae2292396a8"
version = "0.7.9"

[[Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[Random]]
deps = ["Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[[Reexport]]
git-tree-sha1 = "5f6c21241f0f655da3952fd60aa18477cf96c220"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
version = "1.1.0"

[[Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[[Suppressor]]
git-tree-sha1 = "a819d77f31f83e5792a76081eee1ea6342ab8787"
uuid = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
version = "0.2.0"

[[Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
"""

# ╔═╡ Cell order:
# ╟─1dffac12-ef71-11eb-3d44-537b4ed77dc9
# ╟─63456a8a-6821-446c-9279-8a9fc269b047
# ╟─8efc6f7a-1336-4118-acec-29f751cf7aac
# ╟─0f6fc6b1-6d79-4ab7-9263-92adecb6e495
# ╠═fc131461-7d14-4301-aad5-1c91ed54c972
# ╟─d53461a0-8124-4108-b8a9-909313c01b55
# ╠═7fed909f-d803-4059-b253-8b1f1fe930a3
# ╟─78ca9717-a44c-4289-8827-4cd67274059f
# ╠═8fd10c7d-e3e3-4575-8694-22b10f9a9c41
# ╟─6fd02628-ccf5-4f85-a03c-2591fbdf3eb5
# ╠═c4632665-51f2-46bf-b2aa-4849d635de43
# ╟─a3e53c87-5f15-47d0-a80c-4d5388b31eb8
# ╠═20ec0a99-9b6e-40a4-aa44-144765b86a3d
# ╟─af7579be-e3ee-408b-bb7e-2c1362a0c427
# ╠═9997973b-2992-4df2-ad71-b25fb80a3655
# ╟─1e075548-6898-457b-8cec-dbccc0fd8ec7
# ╠═eb578e80-f4fe-4c14-91b7-d7b267bd9046
# ╟─f6c4ed81-0251-4973-bdc7-75951a2657c7
# ╠═2e80f947-1447-4809-b31a-337abaedd955
# ╟─db9ba5b4-0573-4bd0-9359-24cdc2885082
# ╟─4c5e5e24-aea6-4573-b9a2-f25d83c22f19
# ╠═7539d614-67d5-4a2a-b03a-00c5f873c8ae
# ╠═aa5abb6c-2289-4c34-a062-2e84187559dd
# ╟─e9b22426-0c7f-4f15-8bd3-361b3b5ed5cf
# ╠═0be3dd43-188f-460d-8df8-3ae9ac8adae1
# ╠═a5ebeeca-349e-4777-9acf-491da20efd88
# ╟─2326a03a-3f53-420f-896b-cf3620f06a60
# ╠═a5ecc6c7-d08b-4cf8-8a3b-580ada462edc
# ╟─ae08b2e9-f864-458d-89e5-ebe07320f322
# ╠═b0f76cc7-0fb4-48ca-9844-c0fe186b32eb
# ╟─c8f1021f-f230-428c-9685-0eb03a63def8
# ╠═0ce4e7e0-3d99-4ae4-8a11-905ce6e7708f
# ╠═4d67eb11-e859-4213-ae0b-9298d5c79bdf
# ╟─e4870042-9c91-41d8-8770-47940cffce0a
# ╠═7ff0187e-12c2-497d-b5d1-29dd0770a8e1
# ╟─33cb3990-d126-41e1-ac9f-c9f9d6b85ae5
# ╠═bda2dba7-4be5-4ad0-bfdc-ad02a64297a4
# ╟─fd2d5874-46a0-47ae-9f13-45079f42cfab
# ╠═faa0448f-7470-4d2e-84bf-f2dcaa92fc37
# ╟─88d51607-8d4f-4087-ab00-2113a9c65ad4
# ╟─6209b50b-5ded-4529-a895-2ae533ef63b6
# ╟─00000000-0000-0000-0000-000000000001
# ╟─00000000-0000-0000-0000-000000000002


text:
using DTMarkovChain
using Base.Test

# write your own tests here
@test 2 == 2


text:
struct Session{H<:Union{Nothing, Handler}}
    name::String
    handler::H
    objects::DefaultDict
end

function Session(name::String)
     # Create our objects dictionary which defaults to returning
    # an empty JLSOFile
    handler = CHECKPOINTS[name]

    objects = DefaultDict{AbstractPath, JLSO.JLSOFile}() do
        JLSO.JLSOFile(Dict{Symbol, Vector{UInt8}}(); handler.settings...)
    end

    Session{typeof(handler)}(name, handler, objects)
end

Session(prefix::Union{Module, String}, name::String) = Session(join([prefix, name], "."))

function Session(f::Function, args...)
    session = Session(args...)
    f(session)
    commit!(session)
end

function Session(f::Function, names::Vector{String})
    sessions = Session.(names)
    f(sessions...)
    commit!.(sessions)
end

function Session(f::Function, prefix::Union{Module, String}, names::Vector{String})
    Session(f, map(n -> "$prefix.$n", names))
end

"""
    commit!(session)

Write all staged JLSOFiles to the respective paths.
"""
function commit!(session::Session)
    # No-ops skip when handler is nothing
    session.handler === nothing && return nothing

    for (p, jlso) in session.objects
        commit!(session.handler, p, jlso)
    end
end

function checkpoint(session::Session, data::Dict{Symbol}; tags...)
    checkpoint_deprecation(tags...)
    with_checkpoint_tags(tags...) do
        # No-ops skip when handler is nothing
        session.handler === nothing && return nothing

        p = path(session.handler, session.name)
        jlso = session.objects[p]
        session.objects[p] = stage!(session.handler, jlso, data)
    end
end

function checkpoint(s::Session, data::Pair...; tags...)
    checkpoint_deprecation(tags...)
    with_checkpoint_tags(tags...) do
        checkpoint(s, Dict(data...))
    end
end

function checkpoint(s::Session, data; tags...)
    checkpoint_deprecation(tags...)
    with_checkpoint_tags(tags...) do
        checkpoint(s, Dict(:data => data))
    end
end