module Pseudospectra
#=
Eigenvalue and Pseudospectrum Analysis for Julia

The Pseudospectra.jl package is a translation of EigTool, but no endorsement
or promotion by the authors of EigTool is implied.

This package is released under a BSD license, as described in the LICENSE file.

Julia code and supplements
Copyright (c) 2017-2019, Ralph Smith

Portions derived from EigTool:

 Copyright (c) 2002-2014, The Chancellor, Masters and Scholars
 of the University of Oxford, and the EigTool Developers. All rights reserved.
 EigTool is maintained on GitHub:  https://github.com/eigtool

SPDX-License-Identifier: BSD-3-Clause
License-Filename: LICENSES/BSD-3-Clause_Eigtool
=#

using ProgressMeter

using LinearAlgebra, SparseArrays, Arpack, Printf
using Requires

export new_matrix, driver!, spectralportrait
export psa_compute, psa_radius, psa_abscissa
export numerical_range, numerical_abscissa
export modeplot, mtxexpsplot, mtxpowersplot, isheadless, iscomputed
export PSAStruct, ArpackOptions, Portrait, GUIState

# Plotting packages should probably extend these:
export zoomin!, zoomout!

# Not exported, but may be used by plotting packages:
# vec2ax, expandlevels, isvalidax
# oneeigcond, psmode_inv_lanczos, transient_bestlb, set_method!

# Associated plotting packages should provide these, specialized on their
# own GUIState types:
# redrawcontour, surfplot, arnoldiplotter!, ewsplotter, plotmode,
# replzdlg, addmark

const smallσ = 1e-150

"""
by default, sparse matrices of this size or smaller are converted to full
for pseudospectra computation.
"""
const nmax4autofull = 200
"""
by default, iterative methods are used for computing pseudospectra of dense
matrices of this size or larger.
"""
const nmin4autoiter = 1600

const myname = "PSA"

include("types.jl")

# Placeholders for plot-specific code implemented elsewhere
function redrawcontour end
function surfplot end
function arnoldiplotter! end
function _portrait end

"""
    ewsplotter(gs::GUIState, ews::Vector, zoom)

plot eigenvalues

So we have something to look at while waiting for the compute engines.
"""
function ewsplotter end
function plotmode end
function replzdlg end

function addmark end
"""
    mtxexpsplot(ps_data,dt=0.1,nmax=50; gs::GUIState = defaultgs(), gradual=false)

plot the evolution of `∥e^(tA)∥`.

This is useful for analyzing linear initial value problems `∂x/∂t = Ax`.
"""
function mtxexpsplot(ps_data::PSAStruct, dt=0.1, nmax=50;
                     gs::GUIState=defaultgs(), kws...)
    mtxexpsplot(gs, ps_data, dt=dt, nmax=nmax; kws...)
end

"""
    mtxpowersplot(ps_data, nmax=50; gs::GUIState = defaultgs(), gradual=false)

plot norms of powers of a matrix `∥A^k∥`

This is useful for analyzing iterative linear algebra methods.
"""
function mtxpowersplot(ps_data::PSAStruct, nmax=50;
                       gs::GUIState = defaultgs(), kws...)
    mtxpowersplot(gs, ps_data, nmax=nmax; kws...)
end

function fillopts end
function isheadless end

include("utils.jl")
include("compute.jl")
include("xeigs.jl")
include("modes.jl")
include("abscissa.jl")
include("radius.jl")
include("numrange.jl")
include("transients.jl")
include("plotter.jl")
include("zooming.jl")

include("plots/PSAPlots.jl")

"""
    new_matrix(A::AbstractMatrix, opts::Dict{Symbol,Any}=()) -> ps_data

process a matrix into the auxiliary data structure used by Pseudospectra.

# Options
- `:direct::Bool`: force use of a direct algorithm?
- `:keep_sparse::Bool`: use sparse matrix code even if `A` is not large?
- `:real_matrix::Bool`: treat `A` as unitarily equivalent to a real matrix?
- `:verbosity::Int`: obvious
- `:eigA`: eigenvalues of `A`, if already known
- `:proj_lev`: projection level (see `psa_compute`)
- `:npts`: edge length of grid for computing and plotting pseudospectra
- `:arpack_opts::ArpackOptions`: (see type description)
- `:levels::Vector{Real}`: contour levels
- `:ax::Vector{Real}(4)`: bounding box for computation `[xmin,xmax,ymin,ymax]`
- `:scale_equal::Bool`: force isotropic axes for spectral portraits?
"""
function new_matrix(A::AbstractMatrix,
                    opts::Dict{Symbol,Any}=Dict{Symbol,Any}())
    m,n=size(A)
    (m >= n) || throw(ArgumentError(
        "Only square or tall rectangular matrices are supported."))
    (issparse(A) && (m != n)) && throw(ArgumentError(
        "Only square sparse matrices are supported."))
    (any(isnan.(A)) || any(isinf.(A))) && throw(ArgumentError(
        "Input matrix has infinite or invalid entries."))

    # flag for the M x (M-1) Hessenberg form
    # Presumably intended for the case where projection is done
    # by a Krylov scheme outside this package.
    AisHess = ((m == (n+1)) && all([x == 0 for x in tril(A,-2)]))

    # User may specify that A is unitarily equivalent to a real matrix
    # even if it is complex
    Aisreal = get(opts,:real_matrix, !(eltype(A) <: Complex))

    verbosity = get(opts,:verbosity,1)

    convert2full = issparse(A) & (n <= nmax4autofull) &
        !get(opts,:keep_sparse,false)
    if haskey(opts,:direct)
        direct = opts[:direct]
    else
        if issparse(A)
            direct = convert2full
        else
            direct = (n <= nmin4autoiter)
            if (verbosity > 0) && (n > nmin4autoiter)
                # Might merit a warning here since it is most likely not
                # expected so iteration options are probably inappropriate.
                # For now, unspec. iteration options already => a warning.
                println("defaulting to iterative for large dense mtx")
            end
        end
    end

    # TODO: check for correctness of
    # proj_lev, levels, ax, arpack stuff, etc. from opts

    # define as placeholder if not provided
    eigA = get(opts,:eigA,Vector{complex(eltype(A))}())

    input_unitary_mtx = get(opts,:unitary_mtx,I)
    proj_lev = get(opts,:proj_lev,Inf)
    npts = get(opts,:npts,setgridsize(n,24,80,!Aisreal))
    Aissquare = (m == n)

    local Tschur, U
    haveschur = false
    if Aissquare && !issparse(A) && direct
        # if small, delay is negligible
        (verbosity > 1) && (m > 100) &&
            println("Attempting initial decomposition...")
        # If square, dense, & direct, we prefer a Schur factorization.
        # Checking for schurfact! method should work,
        # but that's just asking for surprises. This should be robust.
        try
            if eltype(A) <: Complex
                F = schur(A)
            else
                # For some reason Julia devs think a real Schur decomp
                # should shadow the true (not real!) thing
                F = schur(A .+ zero(eltype(A))*im)
            end
            Tschur,U,eigA  = F.T,F.Z,F.values
            haveschur = true
        catch JE
            isa(JE,MethodError) || rethrow(JE)
        end
        if !haveschur && isempty(eigA)
            try
                eigA = eigvals(A)
            catch JE
                isa(JE,MethodError) || rethrow(JE)
                # ME: maybe trap algorithmic errors too; what are they?

                # Warning is needed here since it explains why we need axes
                # for the driver.
                @warn("Failed to compute eigenvalues; proceeding without.")
                if verbosity > 0
                    # If we display(JE) we get the whole damn matrix too
                    println("Exception was method error: ",JE.f,
                            " for ",typeof(A))
                end
            end
        end
        (verbosity > 1) && (m > 100) && println("...done.")
    end
    if haveschur
        ps_dict = Dict{Symbol,Any}(:Aisreal => Aisreal,
                                   :isHessenberg => AisHess,
                                   :schur_mtx => Tschur,
                                   :schur_unitary_mtx => U,
                                   :direct => true,
                                   :projection_on => true,
                                   :proj_lev => proj_lev,
                                   :ews => eigA)
        ps_data = PSAStruct(UpperTriangular(Tschur), input_unitary_mtx * U,
                            A, input_unitary_mtx, ps_dict)

    elseif issparse(A) || AisHess || !direct || Aissquare
        # sparse, Hessenberg, iterative, or needing special handling
        # CHECKME: previously seemed to force
        #   direct |= Aissquare
        #
        ps_dict = Dict{Symbol,Any}(:Aisreal => Aisreal,
                                   :isHessenberg => AisHess,
                                   :projection_on => false,
                                   :proj_lev => Inf,
                                   :ews => eigA)
        ps_data = PSAStruct(A, input_unitary_mtx, A, input_unitary_mtx,
                            ps_dict)
        if !direct
            if !haskey(opts,:arpack_opts)
                @info("setting default ARPACK options")
                ps_dict[:arpack_opts] = ArpackOptions{eltype(A)}()
            else
                isa(opts[:arpack_opts],ArpackOptions) || throw(
                    ArgumentError("type of opts[:arpack_options] must "
                                  * "be ArpackOptions"))
                ps_dict[:arpack_opts] = opts[:arpack_opts]
            end
        elseif issparse(A) && convert2full
            (verbosity > 0) &&
                println("converting to full for direct computation")
            Atmp = full(ps_data.matrix)
            try
                F  = schur(Atmp+complex(eltype(Atmp))(0))
                ps_dict[:schur_mtx] = F.T
                ps_dict[:schur_unitary_mtx] = F.Z
                ps_data.matrix = UpperTriangular(F.T)
                ps_data.unitary_mtx = ps_data.input_unitary_mtx * F.T
                ps_dict[:projection_on] = true
                ps_dict[:ews] = F.values
                ps_dict[:orig_ews] = copy(F.values)
            catch
                ps_data.matrix = Atmp
            end
        end
    else # dense, non-square (but not Hessenberg), and direct
        rfS,rfT = rect_fact(A)
        ps_dict = Dict{Symbol,Any}(:Aisreal => Aisreal,
                                   :isHessenberg => false,
                                   :projection_on => false,
                                   :proj_lev => Inf,
                                   :matrix2 => rfT,
                                   :ews => eigA)
        ps_data = PSAStruct(rfS,input_unitary_mtx,A,input_unitary_mtx,
                            ps_dict)
    end
    ps_dict[:orig_ews] = eigA
    ps_dict[:ew_estimates] = false

    if !ps_dict[:isHessenberg] && !isa(ps_data.unitary_mtx,UniformScaling)
        if size(ps_data.unitary_mtx,2) ∉ [m,1]
            ps_data.unitary_mtx = I
        end
    end

    lev = get(opts,:levels,zeros(0))
    zoom = Portrait(zeros(0),zeros(0),zeros(0,0),
                    npts, get(opts,:ax,zeros(0)),
                    LevelDesc(lev),
                    isempty(lev), proj_lev,
                    size(ps_data.matrix), false,
                    get(opts,:scale_equal,false))
    push!(ps_data.zoom_list,zoom)
    ps_data.zoom_pos = 1
    # save for use when returning to initial plot
    ps_dict[:init_opts] = deepcopy(zoom)
    ps_dict[:init_direct] = direct
    ps_dict[:direct] = direct
    ps_dict[:verbosity] = verbosity

    # DEVNOTE: if direct, upstream constructs axes and calls origplot/redraw
    return ps_data
end

"""
    new_matrix(A, opts::Dict{Symbol,Any}=()) -> ps_data

process a linear operator object into the auxiliary data structure used by
Pseudospectra.

There must be methods with `A` for `eltype`, `size`, and `mul!`.
It is up to the user to make sure that `mul!` is consistent with any
options passed to the iterative solver (see documentation for [`xeigs`](@ref)).
"""
function new_matrix(A, opts::Dict{Symbol,Any}=Dict{Symbol,Any}())
    # CHECKME: can A be anything other than a LinearMap here?
    m,n=size(A)
    (m == n) || throw(ArgumentError(
        "Only square linear operators are supported."))
    Aisreal = get(opts,:real_matrix, !(eltype(A) <: Complex))

    verbosity = get(opts,:verbosity,1)
    direct = false
    eigA = get(opts,:eigA,Vector{complex(eltype(A))}())

    input_unitary_mtx = get(opts,:unitary_mtx,I)
    npts = get(opts,:npts,setgridsize(n,24,80,!Aisreal))
    proj_lev = get(opts,:proj_lev,Inf)

    ps_dict = Dict{Symbol,Any}(:Aisreal => Aisreal,
                                   :isHessenberg => false,
                                   :projection_on => false,
                                   :proj_lev => Inf,
                                   :ews => eigA)
    ps_data = PSAStruct(A, input_unitary_mtx, A, input_unitary_mtx,
                        ps_dict)
    if !haskey(opts,:arpack_opts)
        @info("setting default ARPACK options")
        ps_dict[:arpack_opts] = ArpackOptions{eltype(A)}()
    else
        isa(opts[:arpack_opts],ArpackOptions) || throw(
            ArgumentError("type of opts[:arpack_options] must "
                          * "be ArpackOptions"))
        ps_dict[:arpack_opts] = opts[:arpack_opts]
    end
    lev = get(opts,:levels,zeros(0))
    zoom = Portrait(zeros(0),zeros(0),zeros(0,0),
                    npts, get(opts,:ax,zeros(0)),
                    LevelDesc(lev),
                    isempty(lev), proj_lev,
                    size(ps_data.matrix), false,
                    get(opts,:scale_equal,false))
    push!(ps_data.zoom_list,zoom)

    ps_dict[:orig_ews] = eigA
    ps_dict[:ew_estimates] = false
    ps_data.zoom_pos = 1
    # save for use when returning to initial plot
    ps_dict[:init_opts] = deepcopy(zoom)
    ps_dict[:init_direct] = direct
    ps_dict[:direct] = direct
    ps_dict[:verbosity] = verbosity

    return ps_data
end

# for verifying that tests cover intended cases
const logging_algo = Ref{Bool}(false)

"""
    driver!(ps_data, opts, gs=defaultgs(); revise_method=false)

Compute pseudospectra and plot a spectral portrait.

If using an iterative method to get some eigenvalues and a projection, invokes
that first.

# Arguments
- `ps_data::PSAStruct`: ingested matrix, as processed by `new_matrix`
- `gs::GUIState`: object handling graphical output
- `opts::Dict{Symbol,Any}`:
  - `:ax`, axis limits (overrides value stored in `ps_data`).
  - other options passed to `redrawcontour`, `arnoldiplotter!`

When revising a spectral portrait (`revise_method==true`), the following
entries in `opts` also apply:
 - `:arpack_opts::ArpackOptions`,
 - `:direct::Bool`.
"""
function driver!(ps_data::PSAStruct,
                 optsin::Dict{Symbol,Any}=Dict{Symbol,Any}(),
                 gs::GUIState=defaultgs();
                 myprintln=println, logger=:default, revise_method=false)
    # DEVNOTE: mostly corresponds to switch_redraw.m in EigTool
    opts = fillopts(gs,optsin)
    ps_dict = ps_data.ps_dict
    verbosity = get(ps_dict,:verbosity,1)

    # For changing from direct to iterative, or vice versa,
    if revise_method & haskey(opts,:direct)
        set_method!(ps_data, opts[:direct])
    end

    if revise_method & haskey(opts,:arpack_opts) & !ps_dict[:direct]
        ao = opts[:arpack_opts]
        if !isa(ao,ArpackOptions)
            @mywarn(logger,"invalid :arpack_opts option")
            return nothing
        end
        if haskey(ps_dict,:arpack_opts)
            pvalid = (ao == ps_dict[:arpack_opts])
        else
            pvalid = false
        end
        ps_dict[:proj_valid] = pvalid
        ps_dict[:arpack_opts] = ao
    end

    # if caller specifies ax, use it or bust.
    if haskey(opts,:ax)
        if isvalidax(opts[:ax])
            new_ax = opts[:ax]
        else
            @mywarn(logger,"opts[:ax] is not a valid bounding box")
            return nothing
        end
    else
        new_ax = zeros(0)
    end

    if ps_dict[:direct] || get(ps_dict,:proj_valid,false)
        # for iterative methods, we get here on reentrance with the projection
        n,m = size(ps_data.matrix)
        A = ps_data.matrix
        B = get(ps_dict,:matrix2,I)
        eigA = ps_dict[:ews]
        zoom = ps_data.zoom_list[ps_data.zoom_pos]

        if isempty(new_ax)
            if !isempty(eigA)
                # This sets the default domain for the typical case
                isempty(zoom.ax) && (zoom.ax = vec2ax(eigA))
                if !isheadless(gs)
                    # show eigenvalues while waiting
                    ewsplotter(gs, eigA, zoom)
                end
            else
                if isempty(zoom.ax)
                    @mywarn(logger,"bounding box must be specified")
                    return nothing
                end
            end
        else
            zoom.ax = new_ax
        end

        if haskey(opts, :npts)
            new_npts = opts[:npts]
            if isa(new_npts, Integer) && (new_npts > 2) && (new_npts < 2049)
                zoom.npts = new_npts
            else
                @mywarn(logger,"opts[:npts] is not a valid number of points")
                return nothing
            end
        end

        psa_opts = Dict{Symbol,Any}(:levels=>expandlevels(zoom.levels),
                                    :recompute_levels=>zoom.autolev,
                                    :proj_lev=>zoom.proj_lev,
                                    :scale_equal=>zoom.scale_equal,
                                    :real_matrix=>ps_dict[:Aisreal],
                                    :verbosity=>verbosity)
        ss = size(A)
        Z,x,y,t_levels,err,Tproj,eigAproj,algo = psa_compute(A,zoom.npts,
                                                             zoom.ax,
                                                             eigA,psa_opts,
                                                             B,
                                                        myprintln=myprintln,
                                                        logger=logger)

        # FIXME: handle projection properly
        ps_dict[:proj_ews] = eigAproj

        if err != 0
            @mywarn(logger,"PSA computation failed")
            # FIXME: reset GUI if any
            return nothing
        end

        (logging_algo[] | (verbosity > 1)) && println("algorithm: ",algo)
        zoom = ps_data.zoom_list[ps_data.zoom_pos]
        if zoom.autolev
            (verbosity > 1) && println("levels: $t_levels")
            zoom.levels = LevelDesc(t_levels)
        end
        zoom.x = x
        zoom.y = y
        zoom.Z = Z
        zoom.computed = true
        zoom.dims = size(ps_data.matrix)

        redrawcontour(gs, ps_data, opts)
    else
        # Iterative method (uses ARPACK):
        # This performs implicitly restart Arnoldi steps to
        # project on a Krylov subspace, yielding a Hessenberg matrix
        # with approximately the same spectral properties (locally) as A.

        ps_data.matrix = ps_data.input_matrix
        m,n = size(ps_data.matrix)

        ao = ps_dict[:arpack_opts]

        function xeigsproducer(chnl)
            local ews,H,V
            local nconv,niter,nmult
            try
                ews,v,nconv,niter,nmult,resid,H,V = xeigs(ps_data.matrix,I,chnl,
                                                      nev=ao.nev,ncv=ao.ncv,
                                                      which=ao.which,tol=ao.tol,
                                                      maxiter=ao.maxiter,
                                                      v0=ao.v0,
                                                      sigma=ao.sigma,
                                                      options=opts)

           catch JE
            # FIXME: post a dialog, reset GUI if any
                @warn("xeigs throws:")
                display(JE)
                println()
                stuff = (:failure,nothing)
                put!(chnl,stuff)
                return nothing
            end
        end

        local ews,H,V
        local nconv,niter,nmult
        old_ax = zeros(0)
        (verbosity > 1) && println("calling xeigs w/ $ao")
        chnl = Channel(xeigsproducer)
        xeigs_result = take!(chnl)
        ap_state = nothing
        while xeigs_result[1] ∉ [:finale,:failure]
            the_key,dispvec,the_str,the_ews,the_shifts = xeigs_result
            if !isheadless(gs)
                ap_state = arnoldiplotter!(gs,old_ax,opts,dispvec,
                                           the_str,the_ews, the_shifts,
                                           ap_state)
            end # if gs
            xeigs_result = take!(chnl)
        end
        if xeigs_result[1] == :failure
            @mywarn(logger,"xeigs failed")
            return nothing
        end
        the_key,ews,v,nconv,niter,nmult,resid,H,V = xeigs_result
        if verbosity > 0
            println("xeigs: $nconv of $(ao.nev) converged in $niter iters "
                    * "($nmult MxV)")
        end
        if verbosity > 1
            println("xeigs ews:")
            display(ews); println()
        end
        ews = filter(x->!isnan(x), ews)

        # We basically replace A with H, saving some projection information,
        # and proceed with the dense matrix algorithms.

        ps_dict[:ew_estimates] = true
        ps_dict[:proj_matrix] = H
        ps_data.matrix = H
        ps_dict[:isHessenberg] = true
        ps_dict[:proj_unitary_mtx] = ps_data.input_unitary_mtx * V
        ps_data.unitary_mtx = ps_dict[:proj_unitary_mtx]
        ps_dict[:proj_valid] = true
        ps_dict[:ews] = ews
        # reset zoom list
        ps_data.zoom_pos = 1
        resize!(ps_data.zoom_list,1)
        # CHECKME: do we need remove() here?
        ps_dict[:mode_markers] = []
        zoom = ps_data.zoom_list[1]

        if isempty(new_ax)
            origax = ps_dict[:init_opts].ax # init_opts is a Portrait!
            if !isempty(origax) && isvalidax(origax) # && !ps_dict[:init_direct]
                copy!(zoom.ax,origax)
            else
                # CHECKME: maybe use init_ews if available?
                zoom.ax = vec2ax(ews)
          # elseif gs.mainph != nothing
          #    println("using eigvals for axis limits")
          #    copy!(zoom.ax,getxylims(gs.mainph))
            end
        else
            zoom.ax = new_ax
        end
        zoom.autolev = ps_dict[:init_opts].autolev
        zoom.levels = deepcopy(ps_dict[:init_opts].levels)

        delete!(ps_dict,:proj_axes)
        delete!(ps_dict,:comp_proj_lev)
        origplot!(ps_data,opts,gs) # WARNING: reenters driver!()
        # caller must reset GUI if appropriate
    end
    nothing
end

function iscomputed(ps_data::PSAStruct, idx=ps_data.zoom_pos)
    ps_data.zoom_list[idx].computed
end

iscomputed(zoom::Portrait) = zoom.computed

"""
Make sure zoom list is ok, then redraw (unless `ax_only`).

Note: truncates zoom list, so use for a new problem or for a reset.
"""
function origplot!(ps_data::PSAStruct, opts, gs; ax_only = false)
    ps_data.zoom_pos = 1
    ps_dict = ps_data.ps_dict
    resize!(ps_data.zoom_list,1)
    zoom = ps_data.zoom_list[1]
    if isempty(zoom.ax)
        if isempty(get(ps_dict,:ews,[]))
            @warn("origplot called w/o preset axes or eigenvalues")
        else
            zoom.ax = vec2ax(ps_dict[:ews])
        end
    end
    ax_only || driver!(ps_data,opts,gs)
    nothing
end

"""
possibly change from direct to iterative method or vice versa
"""
function set_method!(ps_data::PSAStruct, todirect::Bool)
    # this is the part of switch_method which pertains to ps_data
    ps_dict = ps_data.ps_dict
    (todirect == ps_dict[:direct]) && return
    m,n = size(ps_data.input_matrix)
    if todirect
        if haskey(ps_dict,:schur_matrix)
            ps_data.matrix = ps_dict[:schur_matrix]
            ps_data.ews = ps_dict[:orig_ews]
            ps_dict[:ew_estimates] = false
        elseif m==n && issparse(ps_data.input_matrix)
            ps_data.matrix = ps_data.input_matrix
        end
        if haskey(ps_dict,:schur_unitary_mtx)
            ps_data.unitary_mtx  = ps_data.input_unitary_mtx *
                ps_data.schur_unitary_mtx
        else
            ps_data.unitary_mtx = ps_data.input_unitary_mtx
        end
        ps_dict[:proj_valid] = false
        # if reverting to a square matrix, no longer have ARPACK projection
        ss = size(ps_data.matrix)
        (ss[1]==ss[2]) && (ps_dict[:isHessenberg] = false)
    else # switch to iterative
        (m == n) || throw(ArgumentError("Iterative method not implemented "
                                        * "for rectangular matrices"))
        # apparently that's all we need for now
    end
    ps_dict[:direct] = todirect
end

"""
    spectralportrait(A::AbstractMatrix; npts=100) => Plots object

compute pseudospectra of matrix `A` and display as a spectral portrait.

Pseudospectra are computed on a grid of `npts` by `npts` points in
the complex plane, including a neighborhood of the spectrum.
Contour levels are `log10(ϵ)` where `ϵ` is the inverse resolvent norm.
This is a convenience wrapper for simple cases; see the Pseudospectra
package documentation for more elaborate interfaces.
"""
function spectralportrait(A0 :: AbstractMatrix; npts=100)
    if _currentplotter[] == :undef
        setpsplotter()
    end
    local ps_data
    try
        ps_data = new_matrix(A0)
    catch JE
        @warn "The spectralportrait function only works for simple cases."
        rethrow(JE)
    end
    n,m = size(ps_data.matrix)
    A = ps_data.matrix
    ps_dict = ps_data.ps_dict
    B = get(ps_dict,:matrix2,I)
    eigA = ps_dict[:ews]
    zoom = ps_data.zoom_list[ps_data.zoom_pos]
    isempty(zoom.ax) && (zoom.ax = vec2ax(eigA))
    psa_opts = _basic_psa_opts(zoom,ps_dict)
    ss = size(A)
    Z,xs,ys,t_levels,err,Tproj,eigAproj,algo = psa_compute(A,npts,
                                                             zoom.ax,
                                                             eigA,psa_opts,
                                                             B)
    return _portrait(xs,ys,Z,eigA)
end

_basic_psa_opts(zoom,ps_dict) = Dict{Symbol,Any}(
    :levels=>expandlevels(zoom.levels),
    :recompute_levels=>zoom.autolev,
    :proj_lev=>zoom.proj_lev,
    :scale_equal=>zoom.scale_equal,
    :real_matrix=>ps_dict[:Aisreal],
    :verbosity=>0)

################################################################
# FIXME: until we think of a better way to handle this:
include("../examples/demo_mtx.jl")

function __init__()
    @require PyPlot="d330b81b-6aea-500a-939a-2ce795aea3ee" link_pyplot()
    @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" link_plots()
    # GLMakie="e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
    @require AbstractPlotting = "537997a7-5e4e-5d89-9595-2241ea00577e" link_makie()
end

end # module


## macro for argument checking

macro check_args(D, cond)
    quote
        if !($(esc(cond)))
            throw(ArgumentError(string(
                $(string(D)), ": the condition ", $(string(cond)), " is not satisfied.")))
        end
    end
end

##### Utility functions

isunitvec(v::AbstractVector) = (norm(v) - 1.0) < 1.0e-12

isprobvec(p::AbstractVector{<:Real}) =
    all(x -> x ≥ zero(x), p) && isapprox(sum(p), one(eltype(p)))

pnormalize!(v::AbstractVector{<:Real}) = (v ./= sum(v); v)

add!(x::AbstractArray, y::AbstractVector) = broadcast!(+, x, x, y)
add!(x::AbstractArray, y::Zeros) = x

multiply!(x::AbstractArray, c::Number) = (x .*= c; x)

exp!(x::AbstractArray) = (x .= exp.(x); x)

# get a type wide enough to represent all a distributions's parameters
# (if the distribution is parametric)
# if the distribution is not parametric, we need this to be a float so that
# inplace pdf calculations, etc. allocate storage correctly
@inline partype(::Distribution) = Float64

# for checking the input range of quantile functions
# comparison with NaN is always false, so no explicit check is required
macro checkquantile(p,ex)
    p, ex = esc(p), esc(ex)
    :(zero($p) <= $p <= one($p) ? $ex : NaN)
end

macro checkinvlogcdf(lp,ex)
    lp, ex = esc(lp), esc(ex)
    :($lp <= zero($lp) ? $ex : NaN)
end

# because X == X' keeps failing due to floating point nonsense
function isApproxSymmmetric(a::AbstractMatrix{Float64})
    tmp = true
    for j in 2:size(a, 1)
        for i in 1:(j - 1)
            tmp &= abs(a[i, j] - a[j, i]) < 1e-8
        end
    end
    return tmp
end

# because isposdef keeps giving the wrong answer for samples
# from Wishart and InverseWisharts
hasCholesky(a::Matrix{Float64}) = isa(trycholesky(a), Cholesky)

function trycholesky(a::Matrix{Float64})
    try cholesky(a)
    catch e
        return e
    end
end

"""
    ispossemdef(A, k) -> Bool
Test whether a matrix is positive semi-definite with specified rank `k` by
checking that `k` of its eigenvalues are positive and the rest are zero.
# Examples
```jldoctest; setup = :(using Distributions: ispossemdef)
julia> A = [1 0; 0 0]
2×2 Array{Int64,2}:
 1  0
 0  0
julia> ispossemdef(A, 1)
true
julia> ispossemdef(A, 2)
false
```
"""
function ispossemdef(X::AbstractMatrix, k::Int;
                     atol::Real=0.0,
                     rtol::Real=(minimum(size(X))*eps(real(float(one(eltype(X))))))*iszero(atol))
    _check_rank_range(k, minimum(size(X)))
    ishermitian(X) || return false
    dp, dz, dn = eigsigns(Hermitian(X), atol, rtol)
    return dn == 0 && dp == k
end
function ispossemdef(X::AbstractMatrix;
                     atol::Real=0.0,
                     rtol::Real=(minimum(size(X))*eps(real(float(one(eltype(X))))))*iszero(atol))
    ishermitian(X) || return false
    dp, dz, dn = eigsigns(Hermitian(X), atol, rtol)
    return dn == 0
end

function _check_rank_range(k::Int, n::Int)
    0 <= k <= n || throw(ArgumentError("rank must be between 0 and $(n) (inclusive)"))
    nothing
end

#  return counts of the number of positive, zero, and negative eigenvalues
function eigsigns(X::AbstractMatrix,
                  atol::Real=0.0,
                  rtol::Real=(minimum(size(X))*eps(real(float(one(eltype(X))))))*iszero(atol))
    eigs = eigvals(X)
    eigsigns(eigs, atol, rtol)
end
function eigsigns(eigs::Vector{<: Real}, atol::Real, rtol::Real)
    tol = max(atol, rtol * eigs[end])
    eigsigns(eigs, tol)
end
function eigsigns(eigs::Vector{<: Real}, tol::Real)
    dp = count(x -> tol < x, eigs)        #  number of positive eigenvalues
    dz = count(x -> -tol < x < tol, eigs) #  number of numerically zero eigenvalues
    dn = count(x -> x < -tol, eigs)       #  number of negative eigenvalues
    return dp, dz, dn
end


@testset "Simulations" begin
    T = Float64
    @testset "Flat Interface" begin
        sys = Swalbe.SysConst(Lx=25, Ly=25, Tmax=200, tdump=100)
        h = Swalbe.run_flat(sys, "CPU", verbos=false)
        @test all(h .== 1.0)
        @test sum(h) == 25*25
        h = Swalbe.run_flat(sys, "CPU", verbos=true)
        # One dim version
        sys = Swalbe.SysConst_1D(L=25, Tmax=200, tdump=100)
        h = Swalbe.run_flat(sys, verbos=false)
        @test all(h .== 1.0)
        @test sum(h) == 25
        h = Swalbe.run_flat(sys, verbos=true)
    end
    @testset "Random Interface" begin
        sys = Swalbe.SysConst(Lx=25, Ly=25, Tmax=10000, tdump=5000)
        h = Swalbe.run_random(sys, "CPU", ϵ=0.1, verbos=false)
        difference = maximum(h) - minimum(h)
        @test difference < 0.02
        sys = Swalbe.SysConst(Lx=25, Ly=25, Tmax=100, tdump=500)
        h = Swalbe.run_random(sys, "CPU", ϵ=0.1, verbos=true)
        # One dim version
        sys = Swalbe.SysConst_1D(L=25, Tmax=10000, tdump=5000)
        h = Swalbe.run_random(sys, ϵ=0.1, verbos=false)
        difference = maximum(h) - minimum(h)
        @test difference < 0.02
        sys = Swalbe.SysConst_1D(L=25, Tmax=100, tdump=500)
        h = Swalbe.run_random(sys, ϵ=0.1, verbos=true)
    end
    @testset "Rayleigh Taylor" begin
        sys = Swalbe.SysConst(Lx=100, Ly=100, Tmax=1000, tdump=500, g=-0.002)
        _, diff = Swalbe.run_rayleightaylor(sys, "CPU", kx=4, ky=5, ϵ=0.01, verbos=true)
        @test diff[1] < diff[end]
        # TODO: Write a test to check the growthrate
        # One dim case
        sys = Swalbe.SysConst_1D(L = 512, Tmax=1000, tdump=500, g=-0.002)
        _, diff = Swalbe.run_rayleightaylor(sys, k=4, ϵ=0.01, verbos=true)
        @test diff[1] < diff[end]
    end
    @testset "Relaxing droplet" begin
        rads = 35
        sys = Swalbe.SysConst(Lx=150, Ly=150, Tmax=10000, δ=3.0)
        h, A = Swalbe.run_dropletrelax(sys, "CPU", radius=rads, verbos=false)
        # Initial droplet volume
        vol = π/3 * rads^3 * (2+cospi(1/6)) * (1-cospi(1/6))^2
        R1 = cbrt((rads^3*(2+cospi(1/6))*(1-cospi(1/6))^2)/((2+cospi(1/9))*(1-cospi(1/9))^2))
        r1 = sinpi(1/9)*R1
        # Identify the droplet, below this height is precursor layer
        drop1d = findall(h[75,:] .> 0.055)
        # Get the radius and height
        droprad = length(drop1d)/2
        droph = maximum(h)
        # Compute the droplets shape after 10k time steps
        vnum = 1/6*π*droph*(3*droprad^2 + droph^2)
        # Test that the droplet volume has not changed too much
        @test vol ≈ vnum atol = vol/100*10
        @test r1 ≈ droprad atol = r1/100*10
        @test A[1] < A[end]
        sys = Swalbe.SysConst(Lx=150, Ly=150, Tmax=100, δ=3.0, tdump=50)
        h, A = Swalbe.run_dropletrelax(sys, "CPU", radius=rads, verbos=true)
        # One dim case
        thresh = 0.05026
        sys = Swalbe.SysConst_1D(L = 150, Tmax=10000, δ=3.0)
        h, A = Swalbe.run_dropletrelax(sys, radius=rads, verbos=false)
        # Initial droplet volume
        vol = π/3 * rads^3 * (2+cospi(1/6)) * (1-cospi(1/6))^2
        R1 = cbrt((rads^3*(2+cospi(1/6))*(1-cospi(1/6))^2)/((2+cospi(1/9))*(1-cospi(1/9))^2))
        r1 = sinpi(1/9)*R1
        # Identify the droplet, below this height is precursor layer
        drop1d = findall(h .> 0.055)
        # Drop area (summation of height)
        drop_ar = zeros(sys.L)
        for i in 1:sys.L
            if h[i] > thresh
                drop_ar[i] = h[i]
            end
        end
        # Get the radius and height
        droprad = length(drop1d)/2
        droph = maximum(h)
        # Compute the droplets shape after 10k time steps
        vnum = 1/6*π*droph*(3*droprad^2 + droph^2)
        sph_r = droph/2 + length(findall(h .> thresh))^2/(8*droph)
        ar = sph_r^2/2*(π/9 - sinpi(1/9))
        # Test that the droplet volume has not changed too much
        # @test vol ≈ vnum atol = vol/100*10 # This breaks in 1d I have to investigate at some point in time
        @test ar ≈ sum(drop_ar) atol = ar/100*10
        @test r1 ≈ droprad atol = r1/100*10
        @test A[1] < A[end]
        sys = Swalbe.SysConst_1D(L = 200, Tmax=100, δ=3.0, tdump=50)
        h, A = Swalbe.run_dropletrelax(sys, radius=35, verbos=true)
    end
    @testset "Patterned droplet" begin
        sys = Swalbe.SysConst(Lx=150, Ly=150, Tmax=10000, δ=3.0)
        h = Swalbe.run_dropletpatterned(sys, "CPU", radius=35, θₛ = fill(1/9,sys.Lx, sys.Ly), verbos=false)
        # Initial droplet volume
        vol = π/3 * 35^3 * (2+cospi(1/6)) * (1-cospi(1/6))^2
        R1 = cbrt((35^3*(2+cospi(1/6))*(1-cospi(1/6))^2)/((2+cospi(1/9))*(1-cospi(1/9))^2))
        r1 = sinpi(1/9)*R1
        # Identify the droplet, below this height is precursor layer
        drop1d = findall(h[75,:] .> 0.055)
        # Get the radius and height
        droprad = length(drop1d)/2
        droph = maximum(h)
        # Compute the droplets shape after 10k time steps
        vnum = 1/6*π*droph*(3*droprad^2 + droph^2)
        # Test that the droplet volume has not changed too much
        @test vol ≈ vnum atol = vol/100*10
        @test r1 ≈ droprad atol = r1/100*10
        sys = Swalbe.SysConst(Lx=150, Ly=150, Tmax=100, tdump=50, δ=3.0)
        h = Swalbe.run_dropletpatterned(sys, "CPU", radius=35, θₛ = fill(1/9,sys.Lx, sys.Ly))
        # One dim case
        thresh = 0.05026
        sys = Swalbe.SysConst_1D(L=150, Tmax=10000, δ=3.0)
        h = Swalbe.run_dropletpatterned(sys, radius=35, θₛ = fill(1/9,sys.L), verbos=false)
        # Initial droplet volume
        vol = π/3 * 35^3 * (2+cospi(1/6)) * (1-cospi(1/6))^2
        R1 = cbrt((35^3*(2+cospi(1/6))*(1-cospi(1/6))^2)/((2+cospi(1/9))*(1-cospi(1/9))^2))
        r1 = sinpi(1/9)*R1
        # Identify the droplet, below this height is precursor layer
        drop1d = findall(h .> 0.055)
        # Drop area (summation of height)
        drop_ar = zeros(sys.L)
        for i in 1:sys.L
            if h[i] > thresh
                drop_ar[i] = h[i]
            end
        end
        # Get the radius and height
        droprad = length(drop1d)/2
        droph = maximum(h)
        # Compute the droplets shape after 10k time steps
        vnum = 1/6*π*droph*(3*droprad^2 + droph^2)
        sph_r = droph/2 + length(findall(h .> thresh))^2/(8*droph)
        ar = sph_r^2/2*(π/9 - sinpi(1/9))
        @test ar ≈ sum(drop_ar) atol = ar/100*10
        @test r1 ≈ droprad atol = r1/100*10
        sys = Swalbe.SysConst_1D(L=150, Tmax=100, tdump=50, δ=3.0)
        h = Swalbe.run_dropletpatterned(sys, radius=35, θₛ = fill(1/9,sys.L))
    end

    @testset "Sliding droplet" begin
        sys = Swalbe.SysConst(Lx=150, Ly=150, Tmax=5000, δ=2.0)
        # This will return the hydrodynamic moments: h, u, v
        mom = Swalbe.run_dropletforced(sys, "CPU", radius=35, fx=1e-4, verbos=false)
        # Now check that the center has moved
        @test findmax(mom[1])[2][1] ≠ 75
        @test findmax(mom[1])[2][2] == 75
        # Further test that the velocity is not too large
        @test all(mom[2] .< 0.1)
        @test all(mom[3] .< 0.1)
        sys = Swalbe.SysConst(Lx=150, Ly=150, Tmax=100, tdump=50, δ=3.0)
        h = Swalbe.run_dropletforced(sys, "CPU", radius=35, fx=1e-4)
        # One dim case
        sys = Swalbe.SysConst_1D(L=150, Tmax=5000, δ=2.0)
        # This will return the hydrodynamic moments: h, u, v
        mom = Swalbe.run_dropletforced(sys, radius=35, f=1e-4, verbos=false)
        # Now check that the center has moved
        @test findmax(mom[1])[2] ≠ Int(sys.L/2)
        # Further test that the velocity is not too large
        @test all(mom[2] .< 0.1)
        sys = Swalbe.SysConst_1D(L=150, Tmax=100, tdump=50, δ=3.0)
        h = Swalbe.run_dropletforced(sys, radius=35, f=1e-4)
    end
end


export solve!

export LinearSystem

include("Utils.jl")
include("Partial.jl")
include("Curl.jl")
include("CurlCurl.jl")

mutable struct LinearSystem
∇ₛxμⁱ∇ₛx::SparseMatrixCSC{CFloat}
ϵᵣ::Vector{CFloat}
A::SparseMatrixCSC{CFloat}
Q::SparseMatrixCSC{CFloat}
b::Vector{CFloat}
end

function LinearSystem(sim::Simulation)
     ∇ₛxμⁱ∇ₛx_M =  sim(∇ₛxμⁱ∇ₛx)
     ϵᵣ =    sim(convert_to_vector,:ϵᵣ)
     k₀²ϵᵣ = spdiagm(0 => (2pi/sim.λ₀)^2 * ϵᵣ)
     A =     ∇ₛxμⁱ∇ₛx_M - k₀²ϵᵣ
     Q   =   sim(convert_to_diagonal_matrix,:Q)
     src  =   sim(convert_to_vector,:J)
     b =  sim.activate_tfsf ?   (Q*A - A*Q) * src   : src
     LinearSystem(∇ₛxμⁱ∇ₛx_M,ϵᵣ,A,Q,b)
end

function solve! end
function (sim::Simulation)(::typeof(solve!); solver = LU(), args...)
    ls =  LinearSystem(sim)
    linsolve!(sim.E,ls.A,ls.b,solver;args...)
end


module SamplingTools

    using LinearAlgebra
    using Statistics
    using Base64
    using Interpolations

    function hj_info(h::Dict, J::Dict)
        n = length(h)
        M = zeros(Float64, (n, n))
        degrees = zeros(Int64, n)
        j_full = Dict()
        for ((x, y), val) in J
            M[sort([x, y])...] = val
            degrees[x] += 1
            degrees[y] += 1
        end
        M += M'
        for (key, val) in h
            M[key, key] = val
            j_full[(key, key)] = val
        end
        return Array{Float64, 2}(UpperTriangular(M)), merge(J, j_full), degrees
    end

    function hj_info!(M::Array{Float64, 2}, j_full::Dict,
                      degrees::Array{Int64, 1}, h::Dict, J::Dict)
        n = length(h)
        fill!(degrees, 0.0)
        for ((x, y), val) in J
            M[x, y] = val
            degrees[x] += 1
            degrees[y] += 1
        end
        M += M'
        for (key, val) in h
            M[key, key] = val
            j_full[(key, key)] = val
        end
        return M, j_full, degrees
    end

    function generate_nbr(nspins::Int,
                          J::Dict,
                          max_degree::Int)
        # define vars
        nbs_vals = zeros(Float64, (max_degree, nspins))
        nbs_idx = zeros(Int64, (max_degree, nspins))
        ispin = 1
        ipair = 1
        x, y = 1, 1
        val = 0.0
        for ispin = 1:nspins
            ipair = 1
            for ((x, y), val) in J
                if x == ispin
                    nbs_idx[ipair, ispin] = y
                    nbs_vals[ipair, ispin] = val
                    ipair += 1
                elseif y == ispin
                    nbs_idx[ipair, ispin] = x
                    nbs_vals[ipair, ispin] = val
                    ipair += 1
                end
            end
        end
        return nbs_idx, nbs_vals
    end

    function generate_nbr!(nbs_idx::Array{Int64, 2}, nbs_vals::Array{Float64, 2},
                           nspins::Int,
                           J::Dict,
                           max_degree::Int)
        # define vars
        ispin = 1
        ipair = 1
        x, y = 1, 1
        val = 0.0
        for ispin = 1:nspins
            ipair = 1
            for ((x, y), val) in J
                if x == ispin
                    nbs_idx[ipair, ispin] = y
                    nbs_vals[ipair, ispin] = val
                    ipair += 1
                elseif y == ispin
                    nbs_idx[ipair, ispin] = x
                    nbs_vals[ipair, ispin] = val
                    ipair += 1
                end
            end
        end
        return nbs_idx, nbs_vals
    end

    function generate_nbr_dict(h::Dict{Int64, Float64},
                               J::Dict{Tuple{Int64, Int64}, Float64})
        # define vars
        nbs = Dict{Int64, Dict{Int64, Float64}}()
        nbs_edges = Dict()
        nspins = length(h)

        for ispin = 1:nspins
            local_nbr = Dict{Int64, Float64}(ispin => h[ispin])
            local_edges = []
            for ((x, y), val) in J
                if x == ispin
                    push!(local_nbr, y => val)
                    push!(local_edges, (x, y))
                elseif y == ispin
                    push!(local_nbr, x => val)
                    push!(local_edges, (x, y))
                end
            end
            push!(nbs, ispin => local_nbr)
            push!(nbs_edges, ispin => local_edges)
        end
        return nbs, nbs_edges
    end

    function shuffle(sorted_array::Array{Int64, 1}, n::Int64)
        t = 0
        j = 0
        @fastmath @inbounds for i = n:-1:1
            j = (rand(sorted_array) % i) + 1
            t = sorted_array[i]
            sorted_array[i] = sorted_array[j]
            sorted_array[j] = t
        end
    end

    function default_temp_range(M::Array{Float64, 2})
        JM = map(abs, M)
        min_de = minimum(diag(JM))
        max_de = maximum(sum(JM, dims=2))

        hot_temp = max_de / log(2)
        cold_temp = min_de / log(1000)
        return hot_temp, cold_temp
    end

    function ising_energy(spin_vector::Array{Float64, 1},
                          JM::Array{Float64, 2}; b_field=1.0
        )::Float64
        b = diag(JM)
        return b_field * ((spin_vector'* ((JM - diagm(b)) * spin_vector)) + (b' * spin_vector))
    end

    function discrete_aggregation(spin_vectors::Array{Array{Float64, 1}, 1},
                                  num_reads, JM::Array{Float64, 2})
        # spin_vectors = [spin_vectors[:, i] for i =1:num_reads]
        unq_vectors = unique(spin_vectors)
        num_unique = size(unq_vectors, 1)
        energies_unq = zeros(Float64, num_unique)
        occurences_unq = zeros(Int, num_unique)
        for i = 1:num_unique
            uvec = unq_vectors[i]
            energies_unq[i] = ising_energy(uvec, JM)
            occurences_unq[i] = count(x -> x == uvec, spin_vectors)
        end
        return unq_vectors, energies_unq, occurences_unq
    end

    function rotor_aggregation(rotor_vectors::Array{Array{Float64, 1}, 1}, num_reads,
                               JM::Array{Float64, 2}; cluster=false)
        if cluster
            spin_vectors = map(x-> sign.(x), map(x-> sin.(x), rotor_vectors))
        else
            spin_vectors = map(x-> sign.(x), map(x-> cos.(x), rotor_vectors))
        end
        unq_vectors = unique(spin_vectors)
        num_unique = size(unq_vectors, 1)
        energies_unq = zeros(Float64, num_unique)
        occurences_unq = zeros(Int, num_unique)
        for i = 1:num_unique
            uvec = unq_vectors[i]
            energies_unq[i] = ising_energy(uvec, JM)
            occurences_unq[i] = count(x -> x == uvec, spin_vectors)
        end
        return unq_vectors, energies_unq, occurences_unq
    end

    function pimc_aggregation(spin_lattice::Array{Array{Float64, 2}, 1},
                              num_reads, JM::Array{Float64, 2})
        # spin_vectors = [spin_vectors[:, i] for i =1:num_reads]
        nslices = size(spin_lattice[1], 2)
        spin_vectors = Array{Array{Float64, 1}, 1}()
        for i = 1:num_reads
            v, e, o = discrete_aggregation([spin_lattice[i][:, j] for j =1:nslices], nslices, JM)
            push!(spin_vectors, collect(v[findmin(e)[2]]))
        end
        return discrete_aggregation(spin_vectors, num_reads, JM)
    end

    function bootstrap(sample_set::Array{Float64, 1},
                       bootstraps=1000, return_bounds=true)
        # for finding a bootstrap median
        n = length(sample_set)
        sample_set = repeat(sample_set, outer=(1,bootstraps))
        @inbounds bootstrap_samples = sample_set[rand(1:n, n, bootstraps)]
        medians = vcat(median(bootstrap_samples, dims=1)...)
        m = median(medians)
        if return_bounds
            bounds = quantile(medians, [0.05, 0.95])
            return m, bounds
        end
        return m
    end

    function add_cross_talk(h::Dict, j::Dict, susceptibility=-0.05)
        nbs, nbs_edges = generate_nbr_dict(h, j)
        j_p = merge(j, Dict([((y, x), val) for ((x, y), val) in j]))
        h_c = copy(h)
        j_c = copy(j)
        j_p_keys = collect(keys(j_p))
        j_c_keys = collect(keys(j_c))
        for (node, n_edges) in nbs_edges
            h_off = 0.0
            ne = length(n_edges)
            for i1 = 1:ne
                (x, y) = n_edges[i1]
                if node == y
                    k = x
                else
                    k = y
                end
                h_off += j[(x, y)] * h[k]
                for i2 = 1:ne
                    (u, v) = n_edges[i2]
                    if ((x, y) != (u, v)) & (i1 < i2)
                        if v == node
                            a = u
                        else
                            a = v
                        end
                        if k < a
                            jij = (k, a)
                        else
                            jij = (a, k)
                        end
                        j_off = susceptibility * get(j_p, (node, a), 0.0) * get(j_p, (node, k), 0.0)
                        if (jij in j_c_keys) == false
                            push!(j_c_keys, jij)
                            push!(j_c, jij => 0.0)
                        end
                        j_c[jij] += j_off
                    end
                end
            end
            h_c[node] += susceptibility * h_off
        end
        return h_c, j_c
    end

    function add_noise(h, j, h_std=0.023, j_std=0.03, h_mean=0.0, j_mean=0.0)
        h_noise = (h_std .* randn(length(h))) .+ h_mean
        j_noise = (j_std .* randn(length(j))) .+ j_mean
        h_new = Dict(Tuple(zip(keys(h), values(h) .+ h_noise)))
        j_new = Dict(Tuple(zip(keys(j), values(j) .+ j_noise)))
        return h_new, j_new
    end

    function PFC_dict(d::Float64; N=2, R=1.0)
        h = Dict{Int64, Float64}([(i, 0.0) for i = 1:(2*N)])
        j = Dict{Tuple, Float64}()
        @assert R > d
        for i = 1:N
            h[i] = R - d
            h[i+N] = -1.0 * R
            j[(i, i+N)] = -1.0 * R
            if i < N
                j[(i, i+1)] = -1.0 * R
            end
        end
        return h, j
    end

    function energy_ranges(h::Dict{Int64, Float64}, j::Dict{Tuple, Float64},
                           max_field=1.0)
        n = length(h)
        @assert n >= 1
        abs_h = []
        abs_dict = Dict{Int64, Float64}()
        abs_j = []
        # bias contrib
        for (key, hh) in h
            abs_h_dict[key] = abs(hh)
            if hh != 0.0
                push!(abs_h, abs(hh))
            end
        end
        # j contrib
        for ((i1, i2), jj) in j
            ajj = abs(jj)
            abs_dict[i1] += ajj
            abs_dict[i2] += ajj
            if jj != 0.0
                push!(abs_j, ajj)
            end
        end

        abs_vals = vcat(abs_h, abs_j)
        if length(abs_vals) == 0
            return [0.0, 0.0]
        end

        min_de = minimum(abs_vals)
        max_de = maximum(values(abs_dict))
        return [min_de, max_de]
    end

    function DQA_schedule(k::Int, nspins::Int, a_schedule::Array{Float64, 1},
                          b_schedule::Array{Float64, 1}; sx=0.2, cx=0.0, c1=0.0)
        num_steps = length(a_schedule)
        new_steps = convert(Int64, ceil(num_steps / (1.0 - sx)))
        s_old = LinRange(sx, 1.0, num_steps)
        s = collect(LinRange(0.0, 1.0, new_steps))
        ak_sch = collect(LinRange(cx, c1, num_steps))
        a_dqa_dict = Dict()
        b_dqa_dict = Dict()
        for i = 1:nspins
            a_sch = deepcopy(a_schedule)
            b_sch = deepcopy(b_schedule)
            if i == k
                afn = monotonic_interpolation(s_old, ak_sch)
                a_dqa_dict[i] = afn.(s)
                b_dqa_dict[i] = LinRange(-sx/(1.0-sx), 1.0, new_steps)
            else
                afn = monotonic_interpolation(s_old, a_sch)
                bfn = monotonic_interpolation(s_old, b_sch)
                a_dqa_dict[i] = afn.(s)
                b_dqa_dict[i] = bfn.(s)
            end
        end
        return a_dqa_dict, b_dqa_dict
    end

    function monotonic_interpolation(x, y)
        return extrapolate(interpolate(x, y, SteffenMonotonicInterpolation()), Flat())
    end

    export rotor_aggregation, discrete_aggregation, ising_energy,
            generate_nbr, default_temp_range, hj_info, shuffle,
            generate_nbr_dict, bootstrap, add_cross_talk, add_noise,
            hj_info!, generate_nbr!, pimc_aggregation, PFC_dict,
            energy_ranges, DQA_schedule, monotonic_interpolation
end


using Printf
using Logging
using Plots

using Oceananigans
using Oceananigans.Advection
using Oceananigans.OutputWriters
using Oceananigans.Utils

using Oceananigans.Grids: ynodes, znodes

include("stommel_gyre.jl")

ENV["GKSwstype"] = "100"
pyplot()

Logging.global_logger(OceananigansLogger())

#####
##### Initial (= final) conditions
#####

@inline ϕ_Gaussian(x, y; L, A, σˣ, σʸ) = A * exp(-(x-L/2)^2/(2σˣ^2) -(y-L/2)^2/(2σʸ^2))
@inline ϕ_Square(x, y; L, A, σˣ, σʸ)   = A * (-σˣ <= x-L/2 <= σˣ) * (-σʸ <= y-L/2 <= σʸ)

ic_name(::typeof(ϕ_Gaussian)) = "Gaussian"
ic_name(::typeof(ϕ_Square))   = "Square"

#####
##### Experiment functions
#####

function setup_simulation(N, T, CFL, ϕₐ, advection_scheme; u, v)
    topology = (Flat, Bounded, Bounded)
    domain = (x=(0, 1), y=(0, L), z=(0, L))
    grid = RectilinearGrid(topology=topology, size=(1, N, N), halo=(3, 3, 3); domain...)

    model = NonhydrostaticModel(
               grid = grid,
        timestepper = :RungeKutta3,
          advection = advection_scheme,
            tracers = :c,
           buoyancy = nothing,
            closure = ScalarDiffusivity(ν=0, κ=0)
    )

    set!(model, v=u, w=v, c=ϕₐ)

    v_max = maximum(abs, interior(model.velocities.v))
    w_max = maximum(abs, interior(model.velocities.w))
    Δt = CFL * min(grid.Δyᵃᶜᵃ, grid.Δzᵃᵃᶜ) / max(v_max, w_max)

    simulation = Simulation(model, Δt=Δt, stop_time=T, progress=print_progress, iteration_interval=1,
                            parameters = (v_Stommel=u, w_Stommel=v))

    filename = @sprintf("stommel_gyre_%s_%s_N%d_CFL%.2f.nc", ic_name(ϕₐ), typeof(advection_scheme), N, CFL)
    fields = Dict("v" => model.velocities.v, "w" => model.velocities.w, "c" => model.tracers.c)
    global_attributes = Dict("N" => N, "CFL" => CFL, "advection_scheme" => string(typeof(advection_scheme)))
    output_attributes = Dict("c" => Dict("longname" => "passive tracer"))

    simulation.output_writers[:fields] =
        NetCDFOutputWriter(model, fields, filename=filename, schedule=TimeInterval(0.01),
                           global_attributes=global_attributes, output_attributes=output_attributes)

    return simulation
end

function print_progress(simulation)
    model = simulation.model

    v_Stommel, w_Stommel = simulation.parameters
    set!(model, v=v_Stommel, w=w_Stommel)

    progress = 100 * (model.clock.time / simulation.stop_time)

    v_max = maximum(abs, interior(model.velocities.v))
    w_max = maximum(abs, interior(model.velocities.w))
    c_min, c_max = extrema(interior(model.tracers.c))

    i, t = model.clock.iteration, model.clock.time
    @info @sprintf("[%06.2f%%] i: %d, t: %.4f, U_max: (%.2e, %.2e), c: (min=%.3e, max=%.3e)",
                   progress, i, t, v_max, w_max, c_min, c_max)
end

L = τ₀ = β = 1
r = 0.04β*L

A  = 1
σˣ = σʸ = 0.05L

T = 2

u = (x, y, z) -> u_Stommel(y, z, L=L, τ₀=τ₀, β=β, r=r)
v = (x, y, z) -> v_Stommel(y, z, L=L, τ₀=τ₀, β=β, r=r)

ϕ_λ_Gaussian = (x, y, z) -> ϕ_Gaussian(y, z, L=L, A=A, σˣ=σˣ, σʸ=σʸ)
ϕ_λ_Square   = (x, y, z) -> ϕ_Square(y, z, L=L, A=A, σˣ=σˣ, σʸ=σʸ)

ic_name(::typeof(ϕ_λ_Gaussian)) = ic_name(ϕ_Gaussian)
ic_name(::typeof(ϕ_λ_Square))   = ic_name(ϕ_Square)

ϕ_λs = (ϕ_λ_Gaussian, ϕ_λ_Square)
schemes = (CenteredSecondOrder(), CenteredFourthOrder(), WENO5())
Ns = (32, 256)
CFLs = (0.05,)

for ϕ in ϕ_λs, scheme in schemes, N in Ns, CFL in CFLs
    @info @sprintf("Running Stommel gyre advection [%s, %s, N=%d, CFL=%.2f]...", ic_name(ϕ), typeof(scheme), N, CFL)
    simulation = setup_simulation(N, T, CFL, ϕ, scheme, u=v, v=v)

    # simulation.stop_time = T/2
    run!(simulation)

    # Reverse velocity field
    # set!(simulation.model, v = (x, y, z) -> -u(x, y, z), w = (x, y, z) -> -v(x, y, z))

    # simulation.stop_time = T
    # run!(simulation)
end


## http://docs.sympy.org/0.7.2/modules/integrals/integrals.html

integrals_sympy_methods = (:integrate, :Integral,
                           :mellin_transform,
                           :inverse_mellin_transform,
                           :laplace_transform,
                           :inverse_laplace_transform,
                           :fourier_transform,
                           :inverse_fourier_transform,
                           :sine_transform,
                           :inverse_sine_transform,
                           :cosine_transform,
                           :inverse_cosine_transform,
                           :hankel_transform,
                           :inverse_hankel_transform,
                           :line_integrate,
                           :deltaintegrate,
                           :ratint,
                           :heurisch,
                           :trigintegrate,
                           :line_integrate
                           )

integrals_instance_methods = (:as_sum,
                              :limits,
                              :transform,
                              :variables)

"""
Create a parameterized curve for line integrals

```
@vars t x y
C = Curve([exp(t)+1, exp(t)-1], (t, 0, log(Sym(2))))
line_integrate(x + y, C, [x,y])
```

"""
Curve{T<:Sym}(exs::Vector{T}, p) = sympy_meth(:Curve, exs, p)
export Curve

"""
Dirac delta for integration

[SymPy Documentation](http://docs.sympy.org/dev/modules/functions/special.html)
"""
DiracDelta(x::Number) = convert(Function, sympy[:DiracDelta])(x)
export DiracDelta

"""
Heaviside function for integration.

`H(x) = x < 0 ? 0 : (x > 0 ? 1 : 1/2)`

[SymPy Documentation](http://docs.sympy.org/dev/modules/functions/special.html)
"""
Heaviside(x::Number) = convert(Function, sympy[:Heaviside])(x)
export Heaviside

## Alternate interface for simple integral
"""

The `integrate` function has its limits specified with tuples of the type `(var, a, b)`. This
profides a simpler interface for one-dimensional integrals: `integrate(ex, var, a, b)`

"""
integrate(s::Sym, x::Sym, from::SymOrReal, to::SymOrReal) = integrate(s, (x, convert(Sym,from), convert(Sym,to)))

"Symbolically integrate a function"
function integrate(f::Function)
    x = Sym("x")
    integrate(f(x), x)
end

"Symbolically integrate a function over `[a,b]`"
function integrate(f::Function, from::SymOrReal, to::SymOrReal)
    x = Sym("x")
    integrate(f(x), x, from, to)
end

## summation
summations_sympy_methods = (
                            :summation, :Sum
                            )

summations_instance_methods  = (
                                :euler_maclaurin,
                                )
summations_object_properties  = (:is_zero, :is_number)


# Just a simple lattice for examples and test.
# We note that there is package Lattices.jl but quite out-dated and poor-maintained. (does not support cubic lattices.)
export OBC, Cubic, graph, locations, Lattice, edgeline

using Reexport
@reexport using LightGraphs, GeometryBasics
abstract type Lattice end
abstract type AbstractBC end
abstract type OBC <: AbstractBC end

"""
    A Lattice has two components.
    G: a Graph including vertices and edges.
    L: Locations as Point for each vertices.
"""
struct Cubic{bc<:AbstractBC} <: Lattice
    graph::AbstractGraph
    locations::Array
end

graph(l::Lattice) = l.graph
locations(l::Lattice) = l.locations

function Cubic{OBC}(Lx::Int, Ly::Int, Lz::Int)
    dim = [Lx; Ly; Lz]
    N = prod(dim)
    D = 3
    g = SimpleGraph(N)
    locations = zeros(Float64, N, 3)
    f(i, j, k) = i + (j - 1) * Lx + (k - 1) * Lx * Ly

    function tadd!(g, pos, dir)
        if pos[dir] != dim[dir]
            tpos = [i == dir ? 1 : 0 for i = 1:D]
            add_edge!(g, f(pos...), f((pos + tpos)...))
        end
    end

    for i = 1:Lx
        for j = 1:Ly
            for k = 1:Lz
                for dir = 1:D
                    tadd!(g, [i; j; k], dir)
                end
                locations[f(i, j, k), :] = [i; j; k]
            end
        end
    end
    return Cubic{OBC}(g, locations)
end

"""
    Function edgeline
    create a Line from an edge
"""
edgeline(l2D::Array, edge) = Line(Point(l2D[src(edge), :]...), Point(l2D[dst(edge), :]...))

# """
#     obtain raw data
# """
# function raw(lattice::Lattice)

# end


using MyExample
using Test

@testset "MyExample.jl" begin
@test my_f(2,1) == 7
@test my_f(2,3) == 13
@test my_f(1,3) == 11
end

@testset "Derivative Tests" begin
@test derivative_of_my_f(2,1) == 2
end


function undef_copy(v::Array{T, d}) where {T, d}
    Array{T, d}(undef, size(v))
end

function make_volterra_kernel(K::Array{T, 2}) where {T <: Number}
    n = size(K, 1)

    V = LowerTriangular(Array{T, 2}(undef, (n, n)))

    V[1,1] = 0

    for i  = 2:n
        V[i,1] = K[i,1]
        V[i,i] = K[i,i]

        for j = 2:(i-1)
            V[i,j] = 2 * K[i,j]
        end
    end

    return(V)
end


###############################
### WUE metrics calculation ###
###############################

#' Water-Use Efficiency Metrics
#'
#' Calculation of various water use efficiency (WUE) metrics.
#'
#' - data      DataFrame or matrix containing all required variables
#' - GPP       Gross primary productivity (umol CO2 m-2 s-1)
#' - NEE       Net ecosystem exchange (umol CO2 m-2 s-1)
#' - LE        Latent heat flux (W m-2)
#' - VPD       Vapor pressure deficit (kPa)
#' - Tair      Air temperature (deg C)
#' - constants Cmol - molar mass of carbon (kg mol-1)
#'                  umol2mol - conversion micromole (umol) to mole (mol)
#'                  kg2g - conversion kilogram (kg) to gram (g)
#'
#' # Details
 the following metrics are calculated:
#'
#'          Water-use efficiency (WUE):
#'
#'            ``WUE = GPP / ET``
#'
#'          Water-use efficiency based on NEE (WUE_NEE):
#'
#'            ``WUE_NEE = NEE / ET``
#'
#'          Inherent water-use efficiency (IWUE; Beer et al. 2009):
#'
#'            ``IWUE = (GPP * VPD) / ET``
#'
#'          Underlying water-use efficiency (uWUE; Zhou et al. 2014):
#'
#'            ``uWUE= (GPP * sqrt(VPD)) / ET``
#'
#'          All metrics are calculated based on the median of all values. E_g.
#'          WUE = median(GPP/ET,na_rm=true)
#'
#' # Value
 a named vector with the following elements:
#'         - WUE: Water-use efficiency (gC (kg H20)-1)
#'         - WUE_NEE: Water-use efficiency based on NEE (gC (kg H20)-1)
#'         - IWUE: Inherent water-use efficiency (gC kPa (kg H20)-1)
#'         - uWUE: Underlying water-use efficiency (gC kPa^0.5 (kg H20)-1)
#'
#' # Note
#' Units for VPD can also be hPa. Units change accordingly.
#'       WUE_NEE is calculated based on the absolute value of NEE (the sign convention does not matter here).
#'
#' #References
#' Beer, C., et al., 2009: Temporal and among-site variability of inherent
#'             water use efficiency at the ecosystem level. Global Biogeochemical Cycles 23, GB2018.
#'
#'             Zhou, S., et al., 2014: The effect of vapor pressure deficit on water
#'             use efficiency at the sub-daily time scale. Geophysical Research Letters 41.
#'
#' #See also
#' [`stomatal_slope`](@ref) for a measure of intrinsic WUE
#'
#' ```@example; output = false
#' ```
#' ## filter data for dry periods and daytime at DE-Tha in June 2014
#' DE_Tha_Jun_2014_2 = filter_data(DE_Tha_Jun_2014,quality_control=false,
#'                                  vars_qc=c("Tair","precip","VPD","H","LE"),
#'                                  filter_growseas=false,filter_precip=true,
#'                                  filter_vars=c("Tair","PPFD","ustar"),
#'                                  filter_vals_min=c(5,200,0.2),
#'                                  filter_vals_max=c(missing,missing,missing),NA_as_invalid=true,
#'                                  quality_ext="_qc",good_quality=c(0,1),
#'                                  missing_qc_as_bad=true,GPP="GPP",doy="doy",
#'                                  year="year",tGPP=0.5,ws=15,min_int=5,precip="precip",
#'                                  tprecip=0.1,precip_hours=24,records_per_hour=2)
#'
#' ## calculate WUE metrics in the filtered periods
#' WUE_metrics(DE_Tha_Jun_2014_2)
#'
#' @importFrom stats median
"""
"""
function WUE_metrics(data,GPP="GPP",NEE="NEE",LE="LE",VPD="VPD",Tair="Tair",
                        constants=BigleafConstants())

  check_input(data,list(GPP,NEE,LE,VPD,Tair))

  ET  = LE_to_ET(LE,Tair)                 # kg H2O m-2 s-1
  GPP = (GPP * constants.umol2mol] * constants[:Cmol]) * constants[:kg2g  # gC m-2 s-1
  NEE = (NEE * constants.umol2mol] * constants[:Cmol]) * constants[:kg2g  # gC m-2 s-1

  WUE     = median(GPP/ET,na_rm=true)
  WUE_NEE = median(abs(NEE)/ET,na_rm=true)
  IWUE    = median((GPP*VPD)/ET,na_rm=true)
  uWUE    = median((GPP*sqrt(VPD))/ET,na_rm=true)

  return(c(WUE=WUE,WUE_NEE=WUE_NEE,IWUE=IWUE,uWUE=uWUE))
end


using DelimitedFiles
using WriteVTK
using MeshCore: AbsShapeDesc, P1, L2, T3, Q4, T4, H8
using MeshCore: VecAttrib
using MeshCore: shapedesc, nshapes, IncRel
using MeshCore: ShapeColl
using LinearAlgebra: norm

"""
    T4blockx(xs::Vector{T}, ys::Vector{T}, zs::Vector{T}, orientation::Symbol) where {T}

Generate a graded tetrahedral mesh  of a 3D block.

Four-node tetrahedra in a regular arrangement, with non-uniform given spacing
between the nodes, with a given orientation of the diagonals.
The mesh is produced by splitting each logical  rectangular cell into a certain number of
tetrahedra. Orientation may be chosen as `:a`, `:b` (six tetrahedra per rectangular cell),
or `:ca` or `:cb` (five tetrahedra per rectangular cell). Keyword argument

# Return
By convention the function returns an incidence relation (`connectivity`)
between the elements (`connectivity.left`) and the vertices
(`connectivity.right`). The geometry is stored as the attribute "geom" of the
vertices.
"""
function T4blockx(xs::Vector{T}, ys::Vector{T}, zs::Vector{T}, orientation::Symbol; intbytes = 8) where {T}
    inttype = Int64
    if intbytes == 4
        inttype = Int32
    end
    nL = length(xs)-1;
    nW = length(ys)-1;
    nH = length(zs)-1;
    nnodes = (nL+1)*(nW+1)*(nH+1);
    ncells = 6*(nL)*(nW)*(nH);
    xyzs = zeros(T, nnodes, 3);
    conns = zeros(inttype, ncells, 4);
    if (orientation==:a)
        t4ia = inttype[1  8  5  6; 3  4  2  7; 7  2  6  8; 4  7  8  2; 2  1  6  8; 4  8  1  2];
        t4ib =  inttype[1  8  5  6; 3  4  2  7; 7  2  6  8; 4  7  8  2; 2  1  6  8; 4  8  1  2];
    elseif (orientation==:b)
        t4ia = inttype[2 7 5 6; 1 8 5 7; 1 3 4 8; 2 1 5 7; 1 2 3 7; 3 7 8 1];
        t4ib =  [2 7 5 6; 1 8 5 7; 1 3 4 8; 2 1 5 7; 1 2 3 7; 3 7 8 1];
    elseif (orientation==:ca)
        t4ia = inttype[8  4  7  5; 6  7  2  5; 3  4  2  7; 1  2  4  5; 7  4  2  5];
        t4ib = inttype[7  3  6  8; 5  8  6  1; 2  3  1  6; 4  1  3  8; 6  3  1  8];
    elseif (orientation==:cb)
        t4ia = inttype[7  3  6  8; 5  8  6  1; 2  3  1  6; 4  1  3  8; 6  3  1  8];
        t4ib = inttype[8  4  7  5; 6  7  2  5; 3  4  2  7; 1  2  4  5; 7  4  2  5];
    else
        error("Unknown orientation")
    end

    f=1;
    for k=1:(nH+1)
        for j=1:(nW+1)
            for i=1:(nL+1)
                xyzs[f, 1] = xs[i]
                xyzs[f, 2] = ys[j]
                xyzs[f, 3] = zs[k];
                f=f+1;
            end
        end
    end

    function node_numbers(i, j, k, nL, nW, nH)
        f=(k-1)*((nL+1)*(nW+1))+(j-1)*(nL+1)+i;
        nn=[f (f+1)  f+(nL+1)+1 f+(nL+1)];
        return inttype[nn broadcast(+, nn, (nL+1)*(nW+1))];
    end

    gc=1;
    for i=1:nL
        for j=1:nW
            for k=1:nH
                nn=node_numbers(i, j, k, nL, nW, nH);
                if (mod(sum( [i, j, k] ), 2)==0)
                    t4i =t4ib;
                else
                    t4i =t4ia;
                end
                for r=1:size(t4i, 1)
                    for c1=1:size(t4i, 2)
                        conns[gc, c1] = nn[t4i[r, c1]];
                    end
                    gc=gc+1;
                end
            end
        end
    end

    C = inttype.(conns[1:gc-1, :]);
    N, TC = size(xyzs, 2), eltype(xyzs)
    locs =  VecAttrib([SVector{N, TC}(xyzs[i, :]) for i in 1:size(xyzs, 1)])
    vertices = ShapeColl(P1, length(locs), "vertices")
    vertices.attributes["geom"] = locs
    elements = ShapeColl(T4, size(C, 1), "elements")
    return IncRel(elements, vertices, C)
end

"""
    T4block(Length, Width, Height, nL, nW, nH, orientation = :a)

Generate a tetrahedral mesh  of the 3D block.

Four-node tetrahedra in a regular arrangement, with uniform spacing between
the nodes, with a given orientation of the diagonals.
The mesh is produced by splitting each logical  rectangular cell into six
tetrahedra. Range =<0, Length> x <0, Width> x <0, Height>.
Divided into elements: nL,  nW,  nH in the first,  second,  and
third direction (x, y, z).

See also: T4blockx
"""
function T4block(Length, Width, Height, nL, nW, nH, orientation = :a; kwargs...)
    return T4blockx(collect(linearspace(0.0, Length, nL+1)),
                    collect(linearspace(0.0, Width, nW+1)),
                    collect(linearspace(0.0, Height, nH+1)), orientation; kwargs...);
end


# Exercise 10-1
function nestedsum(t)
   return sum(sum.(t))
end

t = [[1, 2], [3], [4, 5, 6]]
# println(nestedsum(t))

# Exercise 10-2
function cumulsum(t)
    res = Array{Int64, 1}()
    for ele in t
        if ele == t[begin]
            push!(res, ele)
        else
            push!(res, ele + res[end])
        end
    end
    return res
end

function cumulsum!(t)
    for i in eachindex(t)
        if i == firstindex(t)
            continue
        else
            t[i] += t[prevind(t, i)]
        end
    end
    return t
end

t = [1, 2, 3, 4, 5, 6]
# println(cumulsum(t))
cumulsum!(t)
# println(t)

# Exercise 10-3
function interior(t)
    return t[begin+1:end-1]
end

t = [1, 2, 3, 4, 5, 6]
# println(interior(t))

# Exercise 10-4
function interior!(t)
    pop!(t)
    popfirst!(t)
end

interior!(t)
# println(t)

# Exercise 10-5
function issort(t)
    for i in eachindex(t)
        if i == firstindex(t)
            continue
        else
            if t[i] < t[prevind(t, i)]
                return false
            end
        end
    end
    return true
end

# println(issort([1, 2, 2, 3, 4, 5, 6]))
# println(issort(["abcdef", "bacdef", "cbadef"]))
# println(issort(["d", "bacdef", "cbadef"]))

# Exercise 10-6
function isanagram(a, b)
    return sort(collect(a)) == sort(collect(b))
end

# println(isanagram("abcdef", "acbdef"))
# println(isanagram("aaaa", "bbbb"))

# Exercise 10-7
function hasduplicates(t)
    return unique(t) != t
end

dup = [3, 4, 5, 1, 2, 3, 4, 5]
unq = [1, 2, 3, 4, 5]
# println(hasduplicates(dup))
# println(hasduplicates(unq))

# Exercise 10-8
function gensample()
    days = Array{Int64, 1}()
    for i = 1:23
        push!(days, rand(1:365))
    end
    return hasduplicates(days)
end

function ex10_8(n)
    count = 0
    for i = 1:n
        if gensample()
            count += 1
        end
    end
    return count / n
end

# println(ex10_8(10000))

# Exercise 10-9
# reader2 takes longer to run due to repeated creation and allocation of new arrays.
function reader1()
    fin = open("words.txt")
    words = Array{String, 1}()
    for line in eachline("words.txt")
        push!(words, line)
    end
    return words
end

function reader2()
    fin = open("words.txt")
    words = Array{String, 1}()
    for line in eachline("words.txt")
        words = [words..., line]
    end
    return words
end

function ex10_9()
    @time begin
        reader1()
    end
    @time begin
        reader2()
    end
end

# ex10_9()

# Exercise 10-10
function inbisect(arr, val)
    lo, hi = firstindex(arr), lastindex(arr)
    while lo <= hi
        mid = lo + (hi - lo) ÷ 2
        if arr[mid] < val
            lo = mid + 1
        elseif arr[mid] > val
            hi = mid - 1
        else
            return mid
        end
    end
    return -1
end

function ex10_10()
    arr = [1, 3, 5, 7, 9, 10, 12, 14, 17, 20]
    println(inbisect(arr, 17))
    println(inbisect(arr, 25))
    println(inbisect(arr, 11))
    println(inbisect(arr, 5))
    println(inbisect(arr, -5))
end

# ex10_10()

# Exercise 10-11
function inbisect(arr, val, lo, hi)
    while lo <= hi
        mid = lo + (hi - lo) ÷ 2
        if arr[mid] < val
            lo = mid + 1
        elseif arr[mid] > val
            hi = mid - 1
        else
            return mid
        end
    end
    return -1
end

function reversepairs()
    words = reader1()
    n = length(words)
    revpairs = Array{Tuple, 1}()
    for i = 1:n
        curr = words[i]
        index = inbisect(words, reverse(curr), i + 1, n)
        if index != -1
            push!(revpairs, (curr, words[index]))
        end
    end
    return revpairs
end

# println(reversepairs())

# Exercise 10-12
function interlock()
    words = reader1()
    n = length(words)
    interlocks = Array{Tuple, 1}()
    for i = 1:n
        curr = words[i]
        odd, even = curr[begin:2:end], curr[begin+1:2:end]
        if inbisect(words, odd) != -1 && inbisect(words, even) != -1
            push!(interlocks, (odd, even))
        end
    end
    return interlocks
end

# println(interlock())

function interlock3()
    words = reader1()
    n = length(words)
    interlocks = Array{Tuple, 1}()
    for i = 1:n
        curr = words[i]
        a, b, c = curr[begin:3:end], curr[begin+1:3:end], curr[begin+2:3:end]
        if inbisect(words, a) != -1 && inbisect(words, b) != -1 && inbisect(words, c) != -1
            push!(interlocks, (a, b, c))
        end
    end
    return interlocks
end

# println(interlock3())


include("./mt1d.jl");
using MT1D
using PyPlot

"""
Calculate the apparent resistivity and phase from a model and then
plot a comparison of the results with actual data.

## Arguments:
- `modelFile` -- The path to a file containing the resistivity model.
- `dataFile`  -- The path to a file containing a data matrix formatted
  as [T,ρ,Φ,σρ,σΦ]

## Returns:
- Nothing
"""
function plotMT1DCompare(modelFile::AbstractString, dataFile::AbstractString)
    modelFileHandle = open(modelFile);
    model = readdlm(modelFileHandle, Float64);
    close(modelFileHandle);

    dataFileHandle = open(dataFile);
    data = readdlm(dataFileHandle, Float64);
    close(dataFileHandle);

    Ts     = data[:,1];
    fs     = Ts.^-1;
    ρData  = data[:,2];
    ΦData  = data[:,3];

    ρa, Φa = mt1d(model, fs);
    σρData = data[:,4];
    σΦData = data[:,5];

    rmsΦ = rms(ΦData, Φa, σΦData);
    rmsρ = rms(ρData, ρa, σρData);
    rmsTotal = sqrt(rmsΦ^2 + rmsρ^2);
    titleStr = @sprintf("Total RMS: %.2f", rmsTotal);

    subplot(2,1,1);
    loglog(Ts, ρa);
    errorbar(Ts, ρData, yerr=σρData, fmt="x");
    xlabel("Period (S)");
    ylabel("Apparent Resistivity (Ohm-Metres)");
    title(titleStr);

    subplot(2,1,2);
    semilogx(Ts, Φa);
    errorbar(Ts, ΦData, yerr=σΦData, fmt="x");
    xlabel("Period (S)");
    ylabel("Phase (°)");

    legend(["Model", "Data"]);
end


using Gadfly, DataFrames

set_default_plot_size(6inch, 3inch)

xs = 0:0.1:20

df = DataFrame(
    x=xs,
    y=cos.(xs),
    ymin=cos.(xs) .- 0.5,
    ymax=cos.(xs) .+ 0.5,
)

plot(df, x=:x, y=:y, ymin=:ymin, ymax=:ymax, Geom.line, Geom.ribbon)


using DataFrames
using Gadfly
using Colors

include("theory.jl")
include("transitions.jl")

tr_chains_d1(θ, N_H, N_E, p_H, p_E) = tr_chains(θ, N_H, N_E, p_H, p_E, 1)
tr_chains_d2(θ, N_H, N_E, p_H, p_E) = tr_chains(θ, N_H, N_E, p_H, p_E, 2)
tr_chains_d10(θ, N_H, N_E, p_H, p_E) = tr_chains(θ, N_H, N_E, p_H, p_E, 10)
tr_chains_d50(θ, N_H, N_E, p_H, p_E) = tr_chains(θ, N_H, N_E, p_H, p_E, 50)
tr_chains_d100(θ, N_H, N_E, p_H, p_E) = tr_chains(θ, N_H, N_E, p_H, p_E, 100)
conj_chains_d1(λ_E, λ_H, p_H, p_E) = conj_chains(λ_E, λ_H, p_H, p_E, 1)
theory_chains_d1(λ_E, λ_H, p_H, p_E) = theory_chains_upper(λ_E, λ_H, p_H, p_E, 1)
conj_chains_d50(λ_E, λ_H, p_H, p_E) = conj_chains(λ_E, λ_H, p_H, p_E, 50)
theory_chains_d50(λ_E, λ_H, p_H, p_E) = theory_chains_upper(λ_E, λ_H, p_H, p_E, 50)

function run_sim(θ, p_E, p_H, T, transition)
    N_H = 0
    N_E = 0
    hist = DataFrame(time = Array{Float64}(0),
                     N_E = Array{Float64}(0),
                     N_H = Array{Float64}(0))
    for i in 1:T
        (N_H, N_E) = transition(θ, N_H, N_E, p_H, p_E)
        push!(hist, (i, N_E, N_H))
    end
    return hist #mean(hist[convert(3*T/4):T,:N_H])
end

#Runs markov chain, not the Jump Process.
function run_sim(λ_E, λ_H, p_E, p_H, T, transition)
    θ= λ_H/(λ_H + λ_E)
    if transition in [theory_prioE_lower, theory_prioE_upper,
                      conj_prioE, theory_chains_d1,
                      conj_chains_d1, theory_chains_d50,
                      conj_chains_d50]
        return transition(λ_E, λ_H, p_E, p_H)
    else
        h = run_sim(θ, p_E, p_H, T, transition)
        h[:w_H] = h[:N_H]/λ_H
        h[:w_E] = h[:N_E]/λ_E
        return mean(h[Int(3*T/4):T, :w_H])
    end
end

function run_sim_E(λ_E, λ_H, p_E, p_H, T, transition)
    θ= λ_H/(λ_H + λ_E)
    h = run_sim(θ, p_E, p_H, T, transition)
    h[:w_H] = h[:N_H]/λ_H
    h[:w_E] = h[:N_E]/λ_E
    return mean(h[Int(3*T/4):T, :w_E])
end

function run_multiple_sim(λ_E_range, λ_H_range, p_E_range, p_H_range, transitions_list; w_E = false)
    df = DataFrame(λ_H = Array{Float64}(0),
                   λ_E = Array{Float64}(0),
                   θ = Array{Float64}(0),
                   p_H = Array{Float64}(0),
                   p_E = Array{Float64}(0))
    for transition in transitions_list
        df[Symbol(transition)] = Array{Float64}(0)
    end
    v = zeros(5 + length(transitions_list))
    for λ_H in λ_H_range
        v[1] = λ_H
        for λ_E in λ_E_range
            v[2] = λ_E
            θ = λ_H/(λ_H + λ_E)
            v[3] = θ
            for p_H in p_H_range
                v[4] = p_H
                for p_E in p_E_range
                    v[5] = p_E
                    for (k, transition) in enumerate(transitions_list)
                        v[5 + k] = run_sim(λ_E, λ_H, p_E, p_H, T, transition)
                        if w_E
                            v[5 + k] = run_sim_E(λ_E, λ_H, p_E, p_H, T, transition)
                        end
                    end
                    push!(df, v)
                end
            end
        end
    end
    return df
end


############################################################
# Affinity Propagation
############################################################

type AffinityPropagationOpts
    max_iter     ::Int
    n_stop_check ::Int
    damp         ::Float64
    display      ::Symbol
end

function AffinityPropagationOpts(;
    max_iter::Integer = 500,    # max number of iterations
    n_stop_check::Integer = 10, # stop if exemplars not changed for this number of iterations
    damp::FloatingPoint = 0.5,  # damping factor for message updating, 0 means no damping
    display::Symbol = :iter     # verboseness: :iter, :final, :none
    )

    AffinityPropagationOpts(int(max_iter), int(n_stop_check), float(damp), display)
end

type AffinityPropagationResult
    exemplar_index ::Vector{Int} # index for exemplars (centers)
    assignments    ::Vector{Int} # assignments for each point
    iterations     ::Int         # number of iterations executed
    converged      ::Bool        # converged or not
end

function affinity_propagation{T<:FloatingPoint}(S::Matrix{T}, opts::AffinityPropagationOpts)
    n = size(S, 1)
    converged = false

    exemplars = falses(n, opts.n_stop_check)
    psi = zeros(eltype(S), n, n)
    phi = zeros(eltype(S), n, n)

    n_iter = 0
    IC = nothing

    for n_iter = 1:opts.max_iter
        affprop_update_message!(psi, phi, S, n, opts)

        IC = diag(psi) + diag(phi) .> 0
        exemplars[:, mod(n_iter-1, opts.n_stop_check)+1] = IC

        if opts.display == :iter
            @printf("%7d: %3d exemplars identified\n", n_iter, nnz(IC))
        end

        if n_iter > opts.n_stop_check
            converged = true
            for i = 1:opts.n_stop_check
                if IC != exemplars[:, i]
                    converged = false
                    break
                end
            end
            if converged
                break
            end
        end
    end

    assignment, exemplar_index = affprop_decide_assignment(phi, S, n, IC)
    if opts.display == :iter || opts.display == :final
        if converged
            println("affinity propagation converged with $n_iter iterations")
        else
            println("affinity propagation terminated without convergence after $n_iter iterations")
        end
    end

    return AffinityPropagationResult(exemplar_index, assignment, n_iter, converged)
end

affinity_propagation(x::Matrix; opts...) = affinity_propagation(x, AffinityPropagationOpts(;opts...))

############################################################
# Message Updating
#
# Loopy message passing sometimes can be tricky. Parallel
# updating vs. in-place updating; damping vs. no-damping;
# as well as the order of message updating can affect the
# convergence behavior drastically.
############################################################
function affprop_update_message!{T<:FloatingPoint}(psi::Matrix{T}, phi::Matrix{T}, S::Matrix{T}, n, opts::AffinityPropagationOpts)
    # update phi
    for i = 1:n
        SM = S[i,:] + psi[i,:]
        I = indmax(SM)
        Y = SM[I]
        SM[I] = -Inf
        Y2 = max(SM)
        val = S[i,:] - Y;
        val[I] = S[i,I] - Y2;
        phi[i,:] = opts.damp*phi[i,:] + (1-opts.damp)*val
    end

    # update psi
    for j = 1:n
        RP = phi[:,j]
        idx = 1:n .!= j
        RP[idx] = max(RP[idx], 0)
        val = sum(RP) - RP;
        val[idx] = min(val[idx], 0)
        psi[:,j] = opts.damp*psi[:,j] + (1 - opts.damp)*val
    end
end

function affprop_decide_assignment{T<:FloatingPoint}(phi::Matrix{T}, S::Matrix{T}, n::Integer, exemplar::BitArray{1})
    assignment = zeros(Int, n)
    Iexp = find(exemplar)

    for i = 1:n
        I = indmax(phi[i,Iexp] + S[i,Iexp])
        assignment[i] = Iexp[I]
    end

    # make sure exemplars are assigned to themselves
    assignment[Iexp] = Iexp

    return assignment, Iexp
end


using DynamicEnergyBudgets, Unitful, Test

reftemp = 310.0u"K"
arrtemp = 2000.0u"K"
lowerbound = 280.0u"K"
arrlower = 20000.0u"K"
upperbound = 315.0u"K"
arrupper = 70000.0u"K"

tc = TempCorrLowerUpper(reftemp, arrtemp, lowerbound, arrlower, upperbound, arrupper)
@test tempcorr(100.0u"°C", tc) ≈ 0.0 atol=1e-10
@test tempcorr(-100.0u"°C", tc) ≈ 0.0 atol=1e-10
@test tempcorr(reftemp, tc) == 1.0

tc = TempCorrLower(reftemp, arrtemp, lowerbound, arrlower)
@test tempcorr(100.0u"°C", tc) > 1.0
@test tempcorr(-100.0u"°C", tc) ≈ 0.0 atol=1e-10
@test tempcorr(reftemp, tc) == 1.0

tc = TempCorr(reftemp, arrtemp)
@test tempcorr(100.0u"°C", tc) > 1.0
@test tempcorr(-100.0u"°C", tc) > 0.0
@test tempcorr(-100.0u"°C", tc) < 1.0
@test tempcorr(reftemp, tc) == 1.0


function field_cache(v::Number,x)
  nx = length(x)
  c = zeros(typeof(v),nx)
  CachedArray(c)
end

function evaluate_field!(c,v::Number,x)
  nx = length(x)
  setsize!(c,(nx,))
  r = c.array
  for i in eachindex(x)
    @inbounds r[i] = v
  end
  r
end

function field_gradient(v::Number)
  T = typeof(v)
  E = eltype(T)
  zero(E)
end

function field_cache(v::AbstractArray{<:Number},x)
  nx = length(x)
  sv = size(v)
  s = (nx,sv...)
  c = zeros(eltype(v),s)
  CachedArray(c)
end

function evaluate_field!(c,v::AbstractArray{<:Number},x)
  nx = length(x)
  sv = size(v)
  s = (nx,sv...)
  setsize!(c,s)
  cis = CartesianIndices(v)
  r = c.array
  for i in eachindex(x)
    for ci in cis
      @inbounds r[i,ci] = v[ci]
    end
  end
  r
end

function field_gradient(v::AbstractArray{<:Number})
  T = eltype(v)
  E = eltype(T)
  Fill(zero(E),size(v))
end