@with_kw mutable struct TTCIntersectionDriver <: DriverModel{LonAccelDirection}
    a::LonAccelDirection = LonAccelDirection(0., 1)
    navigator::RouteFollowingIDM = RouteFollowingIDM()
    intersection::Vector{Lane{Float64}} = Lane{Float64}[]
    intersection_pos::VecSE2 = VecE2(0., 0., 0.)
    ttc_threshold::Float64 = 4.5
    horizon::Float64 = 20.0 # distance from the intersection to start the logic
    stop_delta::Float64 = 0. # precision for stopping slightly before the line
    accel_tol::Float64 = 1e-2 # if |a| < accel_tol then a = 0.
    priorities::Dict{Tuple{LaneTag, LaneTag}, Bool} = Dict{Tuple{LaneTag, LaneTag}, Bool}()
    # states
    priority::Bool = false
    stop::Bool = false # switch to true when stopped at the intersection, stays true until the end
end

get_name(::TTCIntersectionDriver) = "TTCIntersectionDriver"
Base.rand(model::TTCIntersectionDriver) = model.a

function AutomotiveDrivingModels.set_desired_speed!(model::TTCIntersectionDriver, v_des::Float64)
    model.navigator.v_des = v_des
    model
end

function AutomotiveDrivingModels.reset_hidden_state!(model::TTCIntersectionDriver)
    model.priority = false
    model.stop = false
    model
end

function AutomotiveDrivingModels.observe!(model::TTCIntersectionDriver, scene::Scene, roadway::Roadway, egoid::Int)
    ego = scene[findfirst(egoid, scene)]
    AutomotiveDrivingModels.observe!(model.navigator, scene, roadway, egoid) # set the direction
    dir = model.navigator.dir
    a_lon =0.
    a_lon_idm = model.navigator.a
    passed = has_passed(model, scene, roadway, egoid)
    is_engaged = engaged(model, scene, roadway, egoid)
    right_of_way = model.priorities[(model.navigator.route[1].tag,model.navigator.route[end].tag)]
    is_clogged = is_intersection_clogged(model, scene, roadway, egoid)
    ttc = ttc_check(model, scene, roadway, egoid)
    # if !model.stop
    #     model.stop = isapprox(ego.state.v, 0.)
    # end

    if isempty(model.intersection) || passed
        a_lon = a_lon_idm
    elseif !passed
        if right_of_way
            if is_clogged && !passed && is_engaged && !isapprox(ego.state.v, 0.)
                # println("Vehicle $egoid : emergency break")
                a_lon = -model.navigator.d_max
            else
                a_lon = a_lon_idm
            end
        else # left turn
            if !ttc && !is_engaged  # before left turn
                a_lon = min(a_lon_idm, stop_at_end(model, ego, roadway))
            elseif is_clogged && !passed && is_engaged && !isapprox(ego.state.v, 0.) #!ttc && !passed && is_engaged || (is_clogged && is_engaged)
                # println("Vehicle $egoid : emergency break")
                a_lon = -model.navigator.d_max
            elseif ttc
                a_lon = a_lon_idm
            end
        end
    end
    # println("veh $egoid | clogged $is_clogged | passed $passed | engaged $is_engaged | stop $(isapprox(ego.state.v, 0.))| right of way $right_of_way")
    # println("veh $egoid: ttc $ttc")
    # println(" ID ", egoid, " stop ", model.stop, " priority ", model.priority)
    model.a = LonAccelDirection(a_lon, dir) # add noise to break ties #XXX remove with priorities
    model
end

function engaged(model::TTCIntersectionDriver, scene::Scene, roadway::Roadway, egoid::Int)
    ego = scene[findfirst(egoid, scene)]
    lane = get_lane(roadway, ego)
    inter_width = 7.5 #todo parameterized
    if normsquared(VecE2(model.intersection_pos - ego.state.posG)) < inter_width^2
        return true
    end
    return false
    # if isempty(lane.entrances)
    #     return false
    # end
    # return true
end

function has_passed(model::TTCIntersectionDriver, scene::Scene, roadway::Roadway, egoid::Int)
    ego = scene[findfirst(egoid, scene)]
    lane = get_lane(roadway, ego)
    inter_to_car = ego.state.posG - model.intersection_pos
    car_vec = get_front(ego) - ego.state.posG
    has_passed = dot(inter_to_car, car_vec) > 0. || lane ∈ get_exit_lanes(roadway)
    return has_passed
end

"""
Check if the time to collision is above some threshold
"""
function ttc_check(model::TTCIntersectionDriver, scene::Scene, roadway::Roadway, egoid::Int)
    min_ttc = Inf
    inter_width = 6.0 #todo parameterized
    otherid = -1
    ego = scene[findfirst(egoid, scene)]
    for veh in scene
        if veh.id != egoid && veh.def.class != AgentClass.PEDESTRIAN && !is_behind(ego, veh, roadway)
            posF = veh.state.posF
            int_x, int_y, int_θ = model.intersection_pos
            lane = get_lane(roadway, veh)
            int_proj = Frenet(proj(model.intersection_pos, lane, roadway, move_along_curves=false), roadway)
            if normsquared(VecE2(model.intersection_pos - veh.state.posG)) < inter_width^2 # vehicle is in the middle
                ttc = 0.
            else
                ttc = (int_proj.s - posF.s)/veh.state.v
            end
            if 0 <= ttc < min_ttc
                otherid = veh.id
                min_ttc = ttc
            end
        end
    end
    # println("veh id ", egoid, "min_ttc ", min_ttc, " threshold ", model.ttc_threshold, " other id ", otherid)
    if 0 <= min_ttc < model.ttc_threshold
        return false
    else
        return true
    end
end

function is_intersection_clogged(model::TTCIntersectionDriver, scene::Scene, roadway::Roadway, egoid::Int64)
    inter_width = 5.0 #todo parameterized
    ego = scene[findfirst(egoid, scene)]
    for veh in scene
        if veh.id == egoid
            continue
        end
        # println("veh id $(veh.id) : ", sqrt(normsquared(VecE2(model.intersection_pos - veh.state.posG))))
        if normsquared(VecE2(model.intersection_pos - veh.state.posG)) < inter_width^2 && !is_behind(ego, veh, roadway)
            return true
        end
    end
    return false
end

function is_behind(veh1::Vehicle, veh2::Vehicle, roadway::Roadway)
    lane1 = get_lane(roadway, veh1)
    # project veh2 on lane2
    veh2_l1 = Frenet(proj(veh2.state.posG, lane1, roadway, move_along_curves=false), roadway)
    return veh2_l1.s < veh1.state.posF.s
end


"""
Represents the processed spectral data necessary to efficiently filter-fit one or more unknown spectra.
A `FilterFitPacket{S<:Detector, T<:AbstractFloat}` contains the data necessary to filter the unknown and to
apply pre-filtered references.  The type `T <: AbstractFloat` allows you to specify the bit-depth used to
perform subsequent calculations using this `FilterFitPacket`.  `Float32` uses less memory and is a little
faster than `Float64`.  If there are duplicate `FilteredReference` for an elemental ROI, the preference
is for the first one.  This allows you to fill in unavailable "FilteredReference" elemental ROIs with more
general ones.
"""
struct FilterFitPacket{S<:Detector, T<:AbstractFloat}
    detector::S
    filter::TopHatFilter{T}
    references::Vector{FilteredReference{T}}

    function FilterFitPacket(det::S, filt::TopHatFilter{T}, refs::Vector{FilteredReference{T}})  where {S<:Detector, T<:AbstractFloat}
        # Only permit one FilteredReference for each type of label for a set of X-rays
        filtrefs = FilteredReference{T}[]
        for ref in refs
            exists = false
            for fr in filtrefs
                # If a reference already exists of this type for these X-rays reassign it
                if (typeof(fr.label) == typeof(ref.label))  && (fr.label.xrays==ref.label.xrays)
                    # filtrefs[i] = ref
                    exists = true
                    break
                end
            end
            if !exists
                push!(filtrefs, ref)
            end
        end
        new{S,T}(det, filt, filtrefs)
    end
end # struct

function Base.show(io::IO, ffp::FilterFitPacket)
    xrays = join(map(r -> "\t$(r.label),", ffp.references), "\n")
    print(io, "References[\n\t$(ffp.detector), \n$xrays\n]")
end

"""
    NeXLUncertainties.asa(::Type{DataFrame}, ffp::FilterFitPacket)

Summarize the `FilteredReference` structs within a `FilterFitPacket` as a `DataFrame`.
"""
function NeXLUncertainties.asa(::Type{DataFrame}, ffp::FilterFitPacket)
    function p2b(fref)
        # charonly over roi, data over ffroi
        croi = max(1,first(fref.roi)-first(fref.ffroi)+1):min(length(fref.data), last(fref.roi)-first(fref.ffroi)+1)
        @assert length(croi)==length(fref.roi)
        sum(fref.charonly) / (sum(fref.data[croi])-sum(fref.charonly))
    end
    function s2n(fref)
        croi = max(1,first(fref.roi)-first(fref.ffroi)+1):min(length(fref.data), last(fref.roi)-first(fref.ffroi)+1)
        @assert length(croi)==length(fref.roi)
        sum(fref.charonly) / sqrt(sum(fref.data[croi])-sum(fref.charonly))
    end
    DataFrame(
        :Spectrum => [ name(fr.label.spectrum) for fr in ffp.references ],
        Symbol("Beam Energy (keV)") => [ get(fr.label.spectrum, :BeamEnergy, missing)/1000.0 for fr in ffp.references ],
        Symbol("Probe Current (nA)") => [ get(fr.label.spectrum, :ProbeCurrent, missing) for fr in ffp.references ],
        Symbol("Live Time (s)") => [ get(fr.label.spectrum, :LiveTime, missing) for fr in ffp.references ],
        :Material => [ get(fr.label.spectrum, :Composition, nothing) for fr in ffp.references],
        :Lines => [ fr.label.xrays for fr in ffp.references],
        :ROI => [ fr.roi for fr in ffp.references],
        # :FullROI => [ fr.ffroi for fr in ffp.references],
        Symbol("P-to-B") => p2b.(ffp.references),
        Symbol("S-to-N") => s2n.(ffp.references)
    )
end

"""
    missingReferences(ffp::FilterFitPacket, elms::Vector{Element}, e0::Float64, ampl=1.0e-5)

Returns a `Vector{Tuple{Vector{CharXRay}, UnitRange{Int64}}}` containing the ROIs for which a
`FilteredReference` is missing from the `FilterFitPacket`.
"""
function missingReferences(ffp::FilterFitPacket, elms::Vector{Element}, e0::Float64, ampl=1.0e-5)
    # Find all collections of X-rays for which we'll need references
    allXrays = mapreduce(elm->NeXLSpectrum.labeledextents(elm, ffp.detector, ampl, e0), append!, elms)
    # Remove those for which there are references
    filter(allXrays) do xr
        isnothing(findfirst(ref->ref.label.xrays == xr[1], ffp.references))
    end
end

"""
Returns a tuple containing the unfiltered spectra associated with the references.
"""
spectra(ffp::FilterFitPacket) = (ref.label.spectrum for ref in ffp.references)

"""
A list of elements for which there are filtered references.
"""
NeXLCore.elms(ffp::FilterFitPacket) = union( [ element(ref.label) for ref in filter(r->r.label isa CharXRayLabel, ffp.references) ] )

struct ReferencePacket
    spectrum::Spectrum
    element::Element
    material::Material
end

Base.show(io::IO, rp::ReferencePacket) = print(io, "ReferencePacket[$(symbol(rp.element)), $(name(rp.material)), $(name(rp.spectrum))]")

"""
    reference( elm::Element, spec::Spectrum, mat::Material=spec[:Composition]; pc = nothing, lt = nothing, e0 = nothing, coating = nothing)::ReferencePacket
    reference(els::AbstractVector{Element}, spec::Spectrum, mat::Material = spec[:Composition]; pc = nothing, lt = nothing, e0 = nothing, coating = nothing)::Vector{ReferencePacket}

Construct a `ReferencePacket` from a `Spectrum` collected from the specified `Material` for the specified `Element`.
Often used with `references(...)` to build `FilterFitPacket`s.

Optional named arguments `pc`, `lt`, `e0`, `coating` allow you to specify the probe current, live time, beam energy and
sample coating.
"""
function reference(
    elm::Element,
    spec::Spectrum,
    mat::Material = spec[:Composition];
    pc = nothing,
    lt = nothing,
    e0 = nothing,
    coating = nothing,
)::ReferencePacket
    if !isnothing(lt)
        @assert lt > 0.0 "The live time must be larger than zero for $(spec[:Name])."
        spec[:LiveTime] = lt
    end
    if !isnothing(pc)
        @assert pc > 0.0 "The probe current must be larger than zero for $(spec[:Name])."
        spec[:ProbeCurrent] = pc
    end
    if !isnothing(e0)
        @assert e0 >= 1.0e3 "The beam energy ($(e0/1000.0)) keV is too low for $(spec[:Name])."
        spec[:BeamEnergy] = e0
    end
    if !isnothing(coating)
        @assert coating isa Film
        spec[:Coating] = coating
    end
    spec[:Composition] = mat
    @assert haskey(spec, :LiveTime) "The :LiveTime property must be defined for $(spec[:Name]).  (Use the `lt` keyword argument)"
    @assert haskey(spec, :ProbeCurrent) "The :ProbeCurrent property must be defined for $(spec[:Name]).  (Use the `pc` keyword argument)"
    @assert haskey(spec, :BeamEnergy) "The :BeamEnergy property must be defined for $(spec[:Name]).  (Use the `e0` keyword argument)"
    @assert haskey(mat, elm) "$(Symbol(elm)) is not present in $mat."
    return ReferencePacket(spec, elm, mat)
end

reference(elm::Element, filename::AbstractString, mat::Material; kwargs...) =
    reference(elm, loadspectrum(filename), mat; kwargs...)

reference(elm::Element, filename::AbstractString; kwargs...) =
    reference(elm, loadspectrum(filename); kwargs...)

function reference(
    els::AbstractVector{Element},
    spec::Spectrum,
    mat::Material = spec[:Composition];
    kwargs...)
    map(el->reference(el, spec, mat; kwargs...), els)
end
reference(elm::AbstractVector{Element}, filename::AbstractString, mat::Material; kwargs...) =
    reference(elm, loadspectrum(filename), mat; kwargs...)

"""
    references(refs::AbstractVector{ReferencePacket}, det::EDSDetector)::FilterFitPacket
    references(refs::AbstractVector{ReferencePacket}, fwhm::Float64)::FilterFitPacket

Constructs a FilterFitPacket from a vector of `ReferencePackets`.  Each `ReferencePacket` represents a
single ROI for an element.  It is possible more than one `ReferencePacket` might be defined for an
elemental ROI.  In this case, the `ReferencePacket` with the lower index will take preference over
later ones.  This allows you to fill in only the missing elemental ROIs using spectra collected from
alternative materials.  For example, a spectrum from F₂Fe is suitable for the Fe K-lines but not the
Fe L-lines. So we might specify F₂Fe first to specify the references for the Fe K-lines and then fill
in the L-lines with a spectrum from pure Fe.
"""
function references(
    refs::AbstractVector{ReferencePacket},
    det::EDSDetector;
    ftype::Type{<:AbstractFloat} = Float64
)::FilterFitPacket
    chcount = det.channelcount
    @assert all(length(r.spectrum) == chcount for r in refs) "The number of spectrum channels must match the detector for all spectra."
    @assert length(refs) > 0 "Please provide at least one ReferencePacket in references(...)"
    # Build the top-hat filter for det
    ff = buildfilter(ftype, det)
    # Apply the top-hat filter to all refs. Trying to thread this fails. :-(
    frefs = mapreduce(append!, refs) do ref
        frefs = filterreference(ff, ref.spectrum, ref.element, ref.material)
        length(frefs)==0 && @warn "Unable to create any filtered ROI references for $(ref.element) from $(name(ref.material))."
        frefs
    end
    return FilterFitPacket(det, ff, frefs)
end
references(refs::AbstractVector{ReferencePacket}, fwhm::Float64; ftype::Type{<:AbstractFloat}=Float64) =
    references(refs, matching(first(refs).spectrum, fwhm), ftype=ftype)

fit_spectrum(spec::Spectrum, ffp::FilterFitPacket{S, T}) where { S<:Detector, T<: AbstractFloat } =
    fit_spectrum(FilteredUnknownW{T}, spec, ffp.filter, ffp.references)

fit_spectrum(specs::AbstractVector{<:Spectrum}, ffp::FilterFitPacket{S, T}) where { S<:Detector, T<: AbstractFloat } =
    ThreadsX.map(spec -> fit_spectrum(FilteredUnknownW{T}, spec, ffp.filter, ffp.references), specs)

"""
    fit_spectrum(spec::Spectrum, ffp::FilterFitPacket)::FilterFitResult
    fit_spectrum(specs::AbstractVector{<:Spectrum}, ffp::FilterFitPacket)::Vector{FilterFitResult}

Fit a `Spectrum` or a vector of `Spectrum` using the specified `FilterFitPacket`.  The result is a
`FilterFitResult` structure which contains k-ratios, residuals, etc.

    fit_spectrum(hs::HyperSpectrum, ffp::FilterFitPacket; mode::Symbol=:Fast, zero = x -> max(0.0, x))::Array{KRatios}

  * `mode = :Fast` - Uses precomputed, filtered "vector" fit method.  No uncertainties are available.
  * `mode = :Intermediate` - Uses an optimized full fit without refits for negative k-ratios.
  * `mode = :Full` - Uses the full single spectrum fit algorithm including refitting when one or more k-ratio is less than zero.

Performs a filtered fit on a hyperspectrum returning an `Array{KRatios}`.

Selecting a mode:
  :Fast is good for generating k-ratio maps or exploratory analysis of a k-ratio map. :Full is best when a
  quantitative map of a high count hyperspectrum is desired.  Fit results for major elements are similar for
  all three but differ for minor and trace elements.  Particularly when a k-ratio is slightly negative. This
  negative k-ratio can effect the other k-ratios.  :Fast also works less well when many elements (>>10) (particularly
  interfering elements) are included in the fit. Unfortunately, :Intermediate and :Full slow down when many elements
  are fit - O(n(elements)²).

The following timing on a 512 x 512 x 2048 hyperspectrum fitting 21 elements with 33 distinct ROIs on a fast 6-core
laptop with 16 GiB memory give a relative feel for the speed of each algorithm.  Yes, :Fast is approximately 30 ×
faster than :Intermediate and used almost 80x less memory.  (64-bit references, 32-bit references use about 1/2
the memory and take about 4/5 the time.)

| mode=         | Threads  |Run time (s)   | Allocations  | Allocated (GiB) | GC time  |
|---------------|----------|---------------|--------------|-----------------|----------|
| :Fast         | 6        | 11.4          | 2.11 M       | 4.72            | 3.0%     |
| :Intermediate | 6        | 342.7         | 13.11 M      | 364.4           | 4.2%     |
| :Full         | 6        | 574.6         | 6.2 G        | 862.9           | 5.5%     |
| :Fast         | 1        | 25.5          | 2.1 M        | 4.72            | 1.8%     |
| :Intermediate | 1        | 1064.6        | 13.1 M       | 364.4           | 0.9%     |
| :Full         | 1        | 2186.2        | 6.2 G        | 862.1           | 2.8%     |
"""
function fit_spectrum(
    hs::HyperSpectrum,
    ffp::FilterFitPacket{S, T};
    mode::Symbol = :Fast,
    zero = x -> max(Base.zero(T), x),
    sigma = Base.zero(T)
)::Array{KRatios} where { S <: Detector, T <: AbstractFloat }
    @assert matches(hs[CartesianIndices(hs)[1]], ffp.detector) "The detector for the hyper-spectrum must match the detector for the filtered references."
    @assert (mode==:Fast) || (mode==:Intermediate) || (mode==:Full) "The mode argument must be :Fast, :Intermediate or :Full."
    if mode == :Fast
        vq = VectorQuant(ffp.references, ffp.filter)
        return fit_spectrum(hs, vq, zero)
    elseif mode == :Intermediate
        return fit_spectrum_int(hs, ffp, zero)
    elseif mode == :Full
        return fit_spectrum_full(hs, ffp, sigma)
    end
    # Should never get here...
    return  Array{KRatio}[]
end

function fit_spectrum_int(
    hs::HyperSpectrum,
    ffp::FilterFitPacket{S, T},
    zero::Function
)::Array{KRatios} where { S <: Detector, T <: AbstractFloat }
    unklabel = UnknownLabel(hs)
    function _tophatfilterhs(unklabel, data, thf, scale)
        @assert length(data) <= length(thf) "The reference spectra must have more channels than the hyperspectrum data."
        filtered = [filtereddatum(thf, data, i) for i in eachindex(data)]
        dp = [max(x, one(T)) for x in data] # To ensure covariance isn't zero or infinite precision
        covar = [filteredcovar(thf, dp, i, i) for i in eachindex(data)]
        FilteredUnknownW{T}(unklabel, scale, 1:length(data), data, filtered, covar)
    end
    fitcontiguousx(unk, ffs, chs) = #
        _buildscale(unk, ffs) * pinv(_buildmodel(ffs, chs), rtol = convert(T, 1.0e-6)) * extract(unk,chs)
    function _filterfitx(unk, ffs, fitrois)
        Iterators.flatten(map(fitrois) do fr
            fitcontiguousx(
                unk,
                filter(ff -> length(intersect(fr, ff.ffroi)) > 0, ffs),
                fr,
            )
        end)
    end
    @assert matches(hs[CartesianIndices(hs)[1]], ffp.detector) "The detector for the hyper-spectrum must match the detector for the filtered references."
    krs = zeros(Float32, length(ffp.references), size(hs)...)
    len = 1:min(depth(hs),length(ffp.filter))
    data = zeros(T, length(ffp.filter))
    fitrois = ascontiguous(map(fd -> fd.ffroi, ffp.references))
    cdata = counts(hs)
    ThreadsX.foreach(CartesianIndices(hs)) do ci
        data[len] .= T.(cdata[len,ci])
        unk = _tophatfilterhs(unklabel, data, ffp.filter, convert(T,1/dose(hs,ci)))
        krs[:, ci] .= zero.(_filterfitx(unk, ffp.references, fitrois))
    end
    return ThreadsX.map(filter(ii -> ffp.references[ii].label isa CharXRayLabel, eachindex(ffp.references))) do i
        lbl = ffp.references[i].label
        rprops = properties(spectrum(lbl))
        KRatios(xrays(lbl), properties(hs), rprops, rprops[:Composition], krs[i, :, :])
    end
end

function fit_spectrum_full(
    hs::HyperSpectrum,
    ffp::FilterFitPacket{S, T},
    sigma::T
)::Array{KRatios} where { S <: Detector, T <: AbstractFloat }
    unklabel = UnknownLabel(hs)
    function _tophatfilterhs(unklabel, data, thf, scale)
        @assert length(data) <= length(thf) "The reference spectra must have more channels than the hyperspectrum data."
        filtered = T[filtereddatum(thf, data, i) for i in eachindex(data)]
        dp = T[max(x, one(T)) for x in data] # To ensure covariance isn't zero or infinite precision
        covar = T[filteredcovar(thf, dp, i, i) for i in eachindex(data)]
        FilteredUnknownW{T}(unklabel, scale, 1:length(data), data, filtered, covar)
    end
    @assert matches(hs[CartesianIndices(hs)[1]], ffp.detector) "The detector for the hyper-spectrum must match the detector for the filtered references."
    krs = zeros(Float32, length(ffp.references), size(hs)...)
    len = 1:depth(hs)
    data = zeros(T, length(ffp.filter))
    cdata = counts(hs)
    ThreadsX.foreach(CartesianIndices(hs)) do ci
        data[len] .= view(cdata, len, ci)
        unk = _tophatfilterhs(unklabel, data, ffp.filter, convert(T,1/dose(hs,ci)))
        uvs = _filterfit(unk, ffp.references, true)
        krs[:, ci] = map(ffp.references) do ref
            id = ref.label
            isnan(uvs, id) || (value(uvs, id) < sigma*σ(uvs, id)) ? Base.zero(eltype(krs)) : convert(eltype(krs), value(uvs, id))
        end
    end
    return ThreadsX.map(filter(ii -> ffp.references[ii].label isa CharXRayLabel, eachindex(ffp.references))) do i
        lbl = ffp.references[i].label
        rprops = properties(spectrum(lbl))
        KRatios( xrays(lbl), properties(hs), rprops, rprops[:Composition], krs[i, :, :])
    end
end


using Random
function initqtree!(wc, i::Integer; backgroundcolor=(0, 0, 0, 0), spacing=getparameter(wc, :spacing))
    img = wc.imgs[i]
    mimg = wordmask(img, backgroundcolor, spacing)
    t = qtree(mimg, wc.params[:groundsize])
    c = isassigned(wc.qtrees, i) ? getcenter(wc.qtrees[i]) : wc.params[:groundsize] ÷ 2
    setcenter!(t, c)
    wc.qtrees[i] = t
end
initqtree!(wc, i; kargs...) = initqtree!.(wc, index(wc, i); kargs...)
"Initialize word's images and other resources with specified style"
function initwords!(wc, i::Integer; backgroundcolor=(0, 0, 0, 0), spacing=getparameter(wc, :spacing),
                    fontsize=getfontsizes(wc, i), color=wc.params[:colors][i],
                    angle=wc.params[:angles][i], font=getfonts(wc, i))
    img, svg = prepareword(wc.words[i], fontsize, color, angle,
        font=font, backgroundcolor=backgroundcolor, border=spacing)
    wc.imgs[i] = img
    wc.svgs[i] = svg
    initqtree!(wc, i, backgroundcolor=backgroundcolor, spacing=spacing)
    nothing
end
initwords!(wc, i; kargs...) = initword!.(wc, index(wc, i); kargs...)
function initwords!(wc::WC; maxiter=5, tolerance=0.02)
    params = wc.params
    weights = wc.weights
    wc.weights .= weights ./ √(sum(weights.^2 .* length.(wc.words)) / length(weights))
    si = sortperm(wc.weights, rev=true)
    wc.words = @view wc.words[si]
    wc.weights = @view wc.weights[si]
    wc.imgs = @view wc.imgs[si]
    wc.svgs = @view wc.svgs[si]
    wc.qtrees = @view wc.qtrees[si]
    wc.params[:colors] = @view wc.params[:colors][si]
    wc.params[:angles] = @view wc.params[:angles][si]
    wc.params[:wordids] = @view wc.params[:wordids][si]
    wc.params[:indsmap] = nothing
    println("set density = $(params[:density])")
    find_weight_scale!(wc, density=params[:density], maxiter=maxiter, tolerance=tolerance)
    printfontsizes(wc)
    initword!.(wc, 1:length(wc.words))
    setstate!(wc, nameof(initwords!))
    wc
end
initword! = initwords!
function printfontsizes(wc)
    nsmall = findlast(i->getfontsizes(wc, i)<=wc.params[:minfontsize], length(wc):-1:1)
    nsmall === nothing && (nsmall = 0)
    println("fontsize ∈ [$(getfontsizes(wc, length(wc))), $(getfontsizes(wc, 1))]")
    if nsmall > 0
        perc = round(Int, nsmall/length(wc)*100)
        println("$nsmall words($perc%) are limited to the minimum font size.")
        if perc > 70
            msg = "It seems too crowded. Word size may be seriously distorted. You need to reduce the number of words or set a larger mask."
            ratio = contentsize_proposal(wc.words, wc.weights) / √getparameter(wc, :contentarea)
            if ratio > 1.1
                msg = msg * " Recommended mask scaling: ratio=$(round(ratio, digits=3))."
            end
            @warn msg
        end

    end
    if getfontsizes(wc, 1) == wc.params[:maxfontsize]
        @warn "Some words are limited to the maximum font size. Please set a `maxfontsize` in `wordcloud` or set a `maxweight` in `processtext`."
    end
end
"""
* placewords!(wc)
* placewords!(wc, style=:uniform)
* placewords!(wc, style=:gathering)
* placewords!(wc, style=:gathering, level=5) #`level` controls the intensity of gathering, typically between 4 and 6, defaults to 5.
* placewords!(wc, style=:gathering, level=6, rt=0) #rt=0, rectangle; rt=1, ellipse; rt=2, rhombus. defaults to 1.
There is also a bool keyword argument `centerlargestword`, which can be set to center the largest word.
When you have set `style=:gathering`, you should disable repositioning in `generate!` at the same time, especially for big words. e.g. `generate!(wc, reposition=0.7)`.
The keyword argument `reorder` is a function to reorder the words, which affects the order of placement. Like `reverse`, `WordCloud.shuffle`.
"""
function placewords!(wc::WC; style=:uniform, rt=:auto, centerlargestword=:auto, reorder=identity, callback=x->x, kargs...)
    if STATEIDS[getstate(wc)] < STATEIDS[:initwords!]
        initwords!(wc)
    end
    @assert style in [:uniform, :gathering]
    if centerlargestword == :auto
        c = wc.params[:groundsize] ÷ 2 # can't wc.maskqtree[1][end÷2]. 1D index goes wrong.
        kernelsize = Stuffing.kernelsize
        centerlargestword = wc.maskqtree[1][c, c] == QTrees.EMPTY && (
            length(wc.qtrees) < 2
            || (length(wc.qtrees) >= 2
                && wc.weights[2] / wc.weights[1] < 0.5
                && prod(kernelsize(wc.qtrees[2])) / prod(kernelsize(wc.qtrees[1])) < 0.5))
        if centerlargestword
            println("center the largest word $(repr(getwords(wc, 1)))")
        end
    end
    arg = ()
    if centerlargestword
        setcenter!(wc.qtrees[1],  wc.params[:groundsize] .÷ 2)
        arg = (2:length(wc.qtrees) |> collect,)
        callback(1)
    end
    qtrees = reorder(wc.qtrees)
    if length(wc.qtrees) > 0 + centerlargestword
        if style == :gathering
            if rt == :auto
                if hasparameter(wc, :rt)
                    rt = getparameter(wc, :rt)
                    println("use wordcloud's parameter `:rt=>$rt` to place words")
                else
                    rt = 1
                    println("rt = 1, ellipse")
                end
            end
            p = min(50, 2 / rt)
            ind = Stuffing.place!(deepcopy(wc.maskqtree), qtrees, arg...;
                    roomfinder=findroom_gathering, p=p, callback=callback, kargs...)
        else
            ind = Stuffing.place!(deepcopy(wc.maskqtree), qtrees, arg...;
                    roomfinder=findroom_uniform, callback=callback, kargs...)
        end
        if ind === nothing error("no room for placement") end
    end
    setstate!(wc, nameof(placewords!))
    setparameter!(wc, 0, :epoch)
    wc
end

"rescale!(wc::WC, ratio::Real)\nRescale all words's size. set `ratio`<1 to shrink, set `ratio`>1 to expand."
function rescale!(wc::WC, ratio::Real)
    qts = wc.qtrees
    centers = getcenter.(qts)
    wc.params[:scale] *= ratio
    initword!.(wc, 1:length(wc))
    setcenter!.(wc.qtrees, centers)
    wc
end

recolor_reset!(wc, i::Integer) = initword!(wc, i)
recolor_reset!(wc, w=:; kargs...) = recolor_reset!.(wc, index(wc, w); kargs...)
function counter(iter; C=Dict{eltype(iter),Int}())
    for e in iter
        C[e] = get(C, e, 0) + 1
    end
    C
end
function mostfrequent(iter; C=Dict{eltype(iter),Int}())
    C = counter(iter; C=C)
    argmax(C)
end
function recolor_main!(wc, i::Integer; background=getmask(wc))
    bg = ARGB.(background)
    img = getimages(wc, i)
    x, y = getpositions(wc, i)
    bg, img = Render.overlappingarea(bg, img, x, y)
    m = wordmask(img, (0, 0, 0, 0), 0)
    bkv = @view bg[m]
    c = mostfrequent(bkv)
    initword!(wc, i, color=c)
end
recolor_main!(wc, w=:; kargs...) = recolor_main!.(wc, index(wc, w); kargs...)
function recolor_average!(wc, i::Integer; background=getmask(wc))
    bg = ARGB.(background)
    img = getimages(wc, i)
    x, y = getpositions(wc, i)
    bg, img = Render.overlappingarea(bg, img, x, y)
    m = wordmask(img, (0, 0, 0, 0), 0)
    bkv = @view bg[m]
    c = sum(bkv) / length(bkv)
    initword!(wc, i, color=c)
end
recolor_average!(wc, w=:; kargs...) = recolor_average!.(wc, index(wc, w); kargs...)

function recolor_blending!(wc, i::Integer; alpha=0.5, background=getmask(wc))
    bg = background
    img = getimages(wc, i)
    x, y = getpositions(wc, i)
    bg, img = Render.overlappingarea(bg, img, x, y)
    m = wordmask(img, (0, 0, 0, 0), 0)
    bg = @view bg[m]
    img = @view img[m]
    alphas = Colors.alpha.(img)
    img .= Render.overlay.(img, convert.(eltype(img), Colors.alphacolor.(bg, alpha)))
    img .= Colors.alphacolor.(img, alphas)
    nothing
end
recolor_blending!(wc, w=:; kargs...) = recolor_blending!.(wc, index(wc, w); kargs...)

function recolor_clipping!(wc, i::Integer; background=getmask(wc))
    bg = background
    img = getimages(wc, i)
    x, y = getpositions(wc, i)
    bg, img = Render.overlappingarea(bg, img, x, y)
    m = wordmask(img, (0, 0, 0, 0), 0)
    bg = @view bg[m]
    img = @view img[m]
    img .= convert.(eltype(img), Colors.alphacolor.(bg, Colors.alpha.(img)))
    nothing
end
recolor_clipping!(wc, w=:; kargs...) = recolor_clipping!.(wc, index(wc, w); kargs...)
"""
recolor the words in `wc` in different styles with the background picture.
The styles supported are `:average`, `:main`, `:clipping`, `:blending`, and :reset (to undo all effects of others).
e.g.
* recolor!(wc, style=:average)
* recolor!(wc, style=:mian)
* recolor!(wc, style=:clipping, background=blur(getmask(wc))) # `background` is optional
* recolor!(wc, style=:blending, alpha=0.3) # `background` and `alpha` are optional
* recolor!(wc, style=:reset)

The effects of `:average`, `:main` and `:clipping` are only determined by the `background`. But the effect of `:blending` is also affected by the previous word color. Therefore, `:blending` can also be used in combination with others
The results of `clipping` and `blending` can not be exported as SVG files, use PNG instead.
"""
function recolor!(wc, args...; style=:average, kargs...)
    if style == :average
        recolor_average!(wc, args...; kargs...)
    elseif style == :main
        recolor_main!(wc, args...; kargs...)
    elseif style == :blending
        recolor_blending!(wc, args...; kargs...)
    elseif style == :clipping
        recolor_clipping!(wc, args...; kargs...)
    elseif style == :reset
        recolor_reset!(wc, args...; kargs...)
    else
        error("unknown style $style")
    end
    nothing
end
"""
# Positional Args
* wc: the wordcloud to fit
* nepoch: training epoch nums
# Keyword Args
* patient: number of epochs before repositioning
* reposition: a Bool value to turn on/off teleport, a Float number `p` between 0~1 indicating the repositioning ratio (Minimum `p`), a Int number `n` equivalent to `i -> i >= n`, a Function index::Int -> doteleport::Boll, or a white list collision.
* trainer: appoint a training engine
"""
function fit!(wc, args...; reposition=true, optimiser=SGD(), krags...)
    reposition isa Union{Function,Number} || (reposition = index(wc, reposition)) # Bool <: Number
    if STATEIDS[getstate(wc)] < STATEIDS[:placewords!]
        placewords!(wc)
    end
    qtrees = [wc.maskqtree, wc.qtrees...]
    ep, nc = train!(qtrees, args...; reposition=reposition, optimiser=optimiser, krags...)
    wc.params[:epoch] += ep
    if nc == 0
        setstate!(wc, nameof(fit!))
        # @assert isempty(outofkernelbounds(wc.maskqtree, wc.qtrees))
        # colllist = first.(totalcollisions(qtrees))
        # @assert length(colllist) == 0
    else
        setstate!(wc, nameof(placewords!))
    end
    wc
end
function printcollisions(wc)
    qtrees = [wc.maskqtree, wc.qtrees...]
    colllist = first.(totalcollisions(qtrees))
    get_text(i) = i > 1 ? wc.words[i - 1] : "#MASK#"
    collwords = [(get_text(i), get_text(j)) for (i, j) in colllist]
    if length(colllist) > 0
        @warn "Have $(length(colllist)) collisions. Try setting a larger `nepoch` and `retry`, or lower `density` in `wordcloud` to fix that"
        println("$collwords")
    end
end

"""
# Positional Args
* wc: the wordcloud to fit
* nepoch: training epoch nums
# Keyword Args
* retry: shrink & retrain times, defaults to 3, set to `1` to disable shrinking
* patient: number of epochs before repositioning
* reposition: a Bool value to turn on/off teleport, a Float number `p` between 0~1 indicating the repositioning ratio (Minimum `p`), a Int number `n` equivalent to `i -> i >= n`, a Function index::Int -> doteleport::Boll, or a white list collision.
* trainer: appoint a training engine
"""
function generate!(wc::WC, args...; retry=3, krags...)
    if STATEIDS[getstate(wc)] < STATEIDS[:placewords!]
        placewords!(wc)
    end
    for r in 1:retry
        if r != 1
            rescale!(wc, 0.97)
            dens = textoccupying(getwords(wc), getfontsizes(wc), getfonts(wc)) / wc.params[:contentarea]
            println("▸$r. try scale = $(wc.params[:scale]). The density is reduced to $dens")
            printfontsizes(wc)
        else
            println("▸$r. scale = $(wc.params[:scale])")
        end
        fit!(wc, args...; krags...)
        if getstate(wc) == :fit!
            break
        end
    end
    if STATEIDS[getstate(wc)] >= STATEIDS[:fit!]
        println("$(wc.params[:epoch]) epochs")
        setstate!(wc, nameof(generate!))
    else # check
        printcollisions(wc)
    end
    wc
end

STATES = nameof.([wordcloud, initwords!, placewords!, fit!, generate!])
STATEIDS = Dict([s => i for (i, s) in enumerate(STATES)])

"""
keep some words and ignore the others, then execute the function. It's the opposite of `ignore`.
* keep(fun, wc, ws::String) #keep a word
* keep(fun, wc, ws::Set{String}) #kepp all words in ws
* keep(fun, wc, ws::Vector{String}) #keep all words in ws
* keep(fun, wc, inds::Union{Integer, Vector{Integer}})
* keep(fun, wc::WC, mask::AbstractArray{Bool}) #keep words. length(mask)==length(wc)
"""
function keep(fun, wc::WC, mask::AbstractArray{Bool})
    mem = [wc.words, wc.weights, wc.imgs, wc.svgs, wc.qtrees,
            wc.params[:colors], wc.params[:angles], wc.params[:wordids], wc.params[:indsmap]]
    wc.words = @view wc.words[mask]
    wc.weights = @view wc.weights[mask]
    wc.imgs = @view wc.imgs[mask]
    wc.svgs = @view wc.svgs[mask]
    wc.qtrees = @view wc.qtrees[mask]
    wc.params[:colors] = @view wc.params[:colors][mask]
    wc.params[:angles] = @view wc.params[:angles][mask]
    wc.params[:wordids] = @view wc.params[:wordids][mask]
    wc.params[:indsmap] = nothing
    r = nothing
    try
        r = fun()
    finally
        wc.words = mem[1]
        wc.weights = mem[2]
        wc.imgs = mem[3]
        wc.svgs = mem[4]
        wc.qtrees = mem[5]
        wc.params[:colors] = mem[6]
        wc.params[:angles] = mem[7]
        wc.params[:wordids] = mem[8]
        wc.params[:indsmap] = mem[9]
    end
    r
end

"""
pin some words as if they were part of the background, then execute the function.
* pin(fun, wc, ws::String) #pin a word
* pin(fun, wc, ws::Set{String}) #pin all words in ws
* pin(fun, wc, ws::Vector{String}) #pin all words in ws
* pin(fun, wc, inds::Union{Integer, Vector{Integer}})
* pin(fun, wc::WC, mask::AbstractArray{Bool}) #pin words. length(mask)==length(wc)
"""
function pin(fun, wc::WC, mask::AbstractArray{Bool})
    maskqtree = wc.maskqtree
    wcmask = wc.mask
    contentarea = wc.params[:contentarea]

    maskqtree2 = deepcopy(maskqtree)
    Stuffing.overlap!(maskqtree2, wc.qtrees[mask])
    wc.maskqtree = maskqtree2
    resultpic = copy(wc.mask)
    wc.mask = overlay!(resultpic, wc.imgs[mask], getpositions(wc, mask))
    wc.params[:contentarea] = occupying(QTrees.kernel(wc.maskqtree[1]), QTrees.FULL)
    r = nothing
    try
        r = ignore(fun, wc, mask)
    finally
        wc.maskqtree = maskqtree
        wc.mask = wcmask
    wc.params[:contentarea] = contentarea
    end
    r
end

function keep(fun, wc, ind::Integer)
    mask = falses(length(wc))
    mask[ind] = 1
    keep(fun, wc, mask)
end
function keep(fun, wc, inds::AbstractArray{<:Integer})
    mask = falses(length(wc))
    mask[inds] .= 1
    keep(fun, wc, mask)
end
function keep(fun, wc, ws)
    keep(fun, wc, index(wc, ws))
end

"""
ignore some words as if they don't exist, then execute the function. It's the opposite of `keep`.
* ignore(fun, wc, ws::String) #ignore a word
* ignore(fun, wc, ws::Set{String}) #ignore all words in ws
* ignore(fun, wc, ws::Vector{String}) #ignore all words in ws
* ignore(fun, wc, inds::Union{Integer, Vector{Integer}})
* ignore(fun, wc::WC, mask::AbstractArray{Bool}) #ignore words. length(mask)==length(wc)
"""
function ignore(fun, wc::WC, mask::AbstractArray{Bool})
    keep(fun, wc, .!mask)
end
function ignore(fun, wc, ind::Integer)
    mask = falses(length(wc))
    mask[ind] = 1
    ignore(fun, wc, mask)
end
function ignore(fun, wc, inds::AbstractArray{<:Integer})
    mask = falses(length(wc))
    mask[inds] .= 1
    ignore(fun, wc, mask)
end
function ignore(fun, wc, ws)
    ignore(fun, wc, index(wc, ws))
end

function pin(fun, wc, ind::Integer)
    mask = falses(length(wc))
    mask[ind] = 1
    pin(fun, wc, mask)
end
function pin(fun, wc, inds::AbstractArray{<:Integer})
    mask = falses(length(wc))
    mask[inds] .= 1
    pin(fun, wc, mask)
end
function pin(fun, wc, ws)
    pin(fun, wc, index(wc, ws))
end


""" Normalize spectrum based on blaze model (also tryies to include black body function) from FITS file. """
function blaze_normalize_spectrum!(spectrum::ST) where { ST<:AbstractSpectra }
    @assert haskey(spectrum.metadata,:continuum)
    if spectrum.metadata[:normalization] == :raw
        blaze_smoothed = smooth_blaze(blaze)
        spectrum.flux ./= blaze_smoothed
        spectrum.var ./= (blaze_smoothed ) .^2
        spectrum.metadata[:normalization] = :blaze
    elseif spectrum.metadata[:normalization] == :continuum
        spectrum.flux .*= spectrum.metadata[:continuum]
        spectrum.var .*= (spectrum.metadata[:continuum] ) .^2
        spectrum.metadata[:normalization] = :blaze
    elseif spectrum.metadata[:normalization] == :blaze
        # do nothing
    else
        @error "Normalizing from " * string(spectrum.metadata.normalization) * " to blaze isn't implemented yet"
    end
    return spectrum
end

""" Normalize each spectrum based on blaze model (also tryies to include black body function) from FITS files. """
function blaze_normalize_spectra!(spectra::AS) where { ST<:AbstractSpectra, AS<:AbstractArray{ST} }
    for spectrum in spectra
        blaze_normalize_spectrum!(spectrum)
    end
    return spectra
end

""" Normalize spectrum based on continuum model from FITS file. """
function continuum_normalize_spectrum!(spectrum::ST) where { ST<:AbstractSpectra }
    @assert haskey(spectrum.metadata,:continuum)
    if spectrum.metadata[:normalization] == :raw
        blaze_smoothed = smooth_blaze(blaze)
        spectrum.flux ./= spectrum.metadata[:continuum] .* blaze_smoothed
        spectrum.var ./= (spectrum.metadata[:continuum].* blaze_smoothed ) .^2
        spectrum.metadata[:normalization] = :continuum
    elseif spectrum.metadata[:normalization] == :blaze
        spectrum.flux ./= spectrum.metadata[:continuum]
        spectrum.var ./= (spectrum.metadata[:continuum] ) .^2
        spectrum.metadata[:normalization] = :continuum
    elseif spectrum.metadata[:normalization] == :continuum
        # do nothing
    else
        @error "Normalizing from " * string(spectrum.metadata.normalization) * " to continuum isn't implemented yet"
    end
    return spectrum
end

""" Normalize each spectrum based on continuum model from FITS files. """
function continuum_normalize_spectra!(spectra::AS) where { ST<:AbstractSpectra, AS<:AbstractArray{ST} }
    for spectrum in spectra
        continuum_normalize_spectrum!(spectrum)
    end
    return spectra
end

""" `filter_line_list( linelist, inst ; λmin, λmax )`
Return DataFrame based on linelist, but filtered to only include lines between λmin and λmax.
Defaults for λmin and λmax specified are in traits for EXPRES.
"""
function filter_line_list(df::DataFrame, inst::IT ;
                            λmin::Real = default_filter_line_list_λmin, λmax::Real = default_filter_line_list_λmax ) where {
                                   IT<:EXPRES.AnyEXPRES }
   df |> @filter(λmin <= _.lambda <= λmax) |>
    #    @filter( _.lambda < 6000.0 ) |>                       # Avoid tellurics at redder wavelengths
    #    @filter( _.lambda >6157 || _.lambda < 6155  ) |>   # Avoid "line" w/ large variability
    DataFrame
end

""" `find_worst_telluric_in_each_chunk( chunk_list_timeseries, spectra )`
Return an array of the worst (smallest) telluric for each chunk at any time for the provided ChunkListTimeseries.
"""
function find_worst_telluric_in_each_chunk( clt::AbstractChunkListTimeseries, data::AbstractArray{AS,1} )  where { T1<:Real, T2<:Real, T3<:Real, A1<:AbstractArray{T1,2}, A2<:AbstractArray{T2,2}, A3<:AbstractArray{T3,2}, IT<:AnyEXPRES, AS<:Spectra2DBasic{T1,T2,T3,A1,A2,A3,IT}  }
   num_lines = num_chunks(clt)
   num_obs = length(clt)
   min_telluric_model_one_obs = ones(num_lines, num_obs )
   min_telluric_model_all_obs  = ones(num_lines)
   for ch_idx in 1:num_chunks(clt)
       for t_idx in 1:num_obs
          view_indices = clt.chunk_list[t_idx].data[ch_idx].λ.indices
          cols = view_indices[1]
          order = view_indices[2]
          min_telluric_model_one_obs[ch_idx, t_idx] = minimum(view(data[t_idx].metadata[:tellurics], cols, order))
      end # times
      min_telluric_model_all_obs[ch_idx] = minimum(min_telluric_model_one_obs[ch_idx, :])
  end # lines
  return min_telluric_model_all_obs
end

"""
   `make_clean_line_list_from_tellurics(line_list, expres_data; Δv_to_avoid_tellurics )`
Returns a new line list that excludes lines with telluric contamination.
Inputs:
- `line_list`:  Dataframe containing field lambda
- `expres_data`:  Array of spectra
- `Δv_to_avoid_tellurics`:  in m/s
Outputs:
- `line_list_without_tellurics`:   DataFrame with fields: `lambda`, `weight`, `lambda_lo`, and `lambda_hi`.
Warning: Currently, assumes a tellurics value in metadata for each spectra, such as is provided by EXPRES.
"""
function make_clean_line_list_from_tellurics(line_list::DataFrame, expres_data::DT; Δv_to_avoid_tellurics::Real = default_Δv_to_avoid_tellurics,
            v_center_to_avoid_tellurics::Real = 0.0, telluric_threshold::Real = 1
               ) where { T1<:Real, A1<:AbstractArray{T1}, T2<:Real, A2<:AbstractArray{T2}, T3<:Real, A3<:AbstractArray{T3}, IT<:EXPRES.AnyEXPRES, ST<:Spectra2DBasic{T1,T2,T3,A1,A2,A3,IT}, DT<:AbstractArray{ST,1} }

   @assert 0.5*RvSpectMLBase.max_bc_earth_rotation <= Δv_to_avoid_tellurics <= 4*RvSpectMLBase.max_bc
   line_list_to_search_for_tellurics = copy(line_list)
   # TODO: Check sign convention for v_center_to_avoid_tellurics
   line_list_to_search_for_tellurics.lambda_lo = line_list_to_search_for_tellurics.lambda./calc_doppler_factor(Δv_to_avoid_tellurics).*calc_doppler_factor(v_center_to_avoid_tellurics)
   line_list_to_search_for_tellurics.lambda_hi = line_list_to_search_for_tellurics.lambda.*calc_doppler_factor(Δv_to_avoid_tellurics).*calc_doppler_factor(v_center_to_avoid_tellurics)
   chunk_list_timeseries = RvSpectMLBase.make_chunk_list_timeseries_around_lines(expres_data,line_list_to_search_for_tellurics)
   line_list_to_search_for_tellurics.min_telluric_model_all_obs = find_worst_telluric_in_each_chunk( chunk_list_timeseries, expres_data)
   line_list_no_tellurics_df = line_list_to_search_for_tellurics |> @filter(_.min_telluric_model_all_obs >= telluric_threshold ) |> DataFrame
end

default_min_Δv_clean = 8000.0

function find_ranges_with_tellurics_in_order(spectrum::ST, order::Integer; telluric_threshold::Real = 1, min_Δv_clean::Real = default_min_Δv_clean, verbose::Bool = false) where { T1<:Real, A1<:AbstractArray{T1}, T2<:Real, A2<:AbstractArray{T2}, T3<:Real, A3<:AbstractArray{T3}, IT<:EXPRES.AnyEXPRES, ST<:Spectra2DBasic{T1,T2,T3,A1,A2,A3,IT}  }

    @assert haskey(spectrum.metadata,:tellurics)
    telluric_ranges = Vector{Tuple{Int64,Int64}}(undef,0)

    c = RvSpectMLBase.speed_of_light_mps
    all_wavelengths = range(first(spectrum.λ[:,order]),stop=last(spectrum.λ[:,order]),length=size(spectrum.λ,1) )
    tellurics = spectrum.metadata[:tellurics]
    start = findfirst(x->!isnan(x),view(tellurics,:,order) )
    stop = findlast(x->!isnan(x),view(tellurics,:,order) )
    if isnothing(start) || isnothing(stop)
        if verbose   println("# The entire order ", order, " was NaNs!?!")   end
        return Vector{Tuple{eltype(spectrum.λ), eltype(spectrum.λ)} }(undef,0)
    end
    @assert stop >= start +1
    in_telluric = tellurics[start,order] < telluric_threshold
    idx_start_this_telluric = in_telluric ? start : 0
    idx_stop_this_telluric = 0
    for i in start+1:stop
        if in_telluric
            if !(tellurics[i,order] < telluric_threshold)  # exitted telluric
                idx_stop_this_telluric = i-1
                #println("# Order = " , order, "  Adding pixels = ", idx_start_this_telluric, " - ", idx_stop_this_telluric, " λ = ", spectrum.λ[idx_start_this_telluric,order], " - ", spectrum.λ[idx_stop_this_telluric,order] )
                push!(telluric_ranges, (idx_start_this_telluric,idx_stop_this_telluric) )
                #push!(telluric_ranges,(spectrum.λ[idx_start_this_telluric,order], spectrum.λ[idx_stop_this_telluric,order]) )
                in_telluric = false
            end
        else
            if (tellurics[i,order] < telluric_threshold)  # entered telluric
                idx_start_this_telluric = i
                if length(telluric_ranges) >= 1
                    idx_last_start = last(telluric_ranges)[1]
                    idx_last_stop = last(telluric_ranges)[2]
                    if spectrum.λ[idx_start_this_telluric,order] - spectrum.λ[idx_last_stop,order] <= min_Δv_clean/c * spectrum.λ[idx_start_this_telluric,order]
                        idx_start_this_telluric = first(last(telluric_ranges))
                        pop!(telluric_ranges)
                    end
                end
                in_telluric = true
            end
        end
    end
    if in_telluric
        idx_stop_this_telluric = stop
        #println("# Order = " , order, "  Adding pixels = ", idx_start_this_telluric, " - ", idx_stop_this_telluric, " λ = ", spectrum.λ[idx_start_this_telluric,order], " - ", spectrum.λ[idx_stop_this_telluric,order] )
        push!(telluric_ranges,(idx_start_this_telluric,idx_stop_this_telluric) )
    end
    #lambda_range = map(r->(spectrum.λ[first(r),order], spectrum.λ[last(r),order] ), telluric_ranges)
    DataFrame(:lambda_lo=>map(r->spectrum.λ[first(r),order], telluric_ranges), :lambda_hi=>map(r->spectrum.λ[last(r),order], telluric_ranges) )
end

import EchelleInstruments.merge_sorted_wavelength_ranges

function find_ranges_with_tellurics(spectrum::ST; min_order::Integer = 1, max_order::Integer = size(spectrum.λ,2), min_Δv_clean::Real = default_min_Δv_clean, telluric_threshold::Real = 1 ) where {
            T1<:Real, A1<:AbstractArray{T1}, T2<:Real, A2<:AbstractArray{T2}, T3<:Real, A3<:AbstractArray{T3}, IT<:EXPRES.AnyEXPRES, ST<:Spectra2DBasic{T1,T2,T3,A1,A2,A3,IT}  }
    @assert haskey(spectrum.metadata,:tellurics)
    c = RvSpectMLBase.speed_of_light_mps

    #list_of_ranges = Vector{Tuple{eltype(spectrum.λ),eltype(spectrum.λ)}}(undef,0)
    list_of_ranges = DataFrame()
    for ord in min_order:max_order
        ranges_for_order = find_ranges_with_tellurics_in_order(spectrum,ord, min_Δv_clean=min_Δv_clean, telluric_threshold=telluric_threshold)
        if size(ranges_for_order,1) >= 1
            append!(list_of_ranges,ranges_for_order)
        end
    end

    if size(list_of_ranges,1)<=1   return list_of_ranges   end
    sort!(list_of_ranges, :lambda_lo )
    non_overlapping_ranges = merge_sorted_wavelength_ranges(list_of_ranges,min_Δv_clean=min_Δv_clean)
    return non_overlapping_ranges
end

#import EchelleInstruments.choose_obs_idx_for_init_guess

function choose_obs_idx_for_init_guess(df::DataFrame, inst::IT ) where { IT<:EXPRES.AnyEXPRES }
   @assert size(df,1) >= 1
   @assert hasproperty(df,:snr_prelim)
   idx = findmax(df.snr_prelim)[2]
   @assert 1 <= idx <= size(df,1)
   return idx
end


using YaoBase
using Test

# mocked registers
struct TestRegister{B} <: AbstractRegister{B} end

TestRegister() = TestRegister{1}()

YaoBase.nqubits(::TestRegister) = 8
YaoBase.nactive(::TestRegister) = 2

export TestInterfaceRegister
struct TestInterfaceRegister{B} <: AbstractRegister{B} end

TestInterfaceRegister() = TestInterfaceRegister{1}()

@testset "Test general interface" begin
    @test_throws MethodError nactive(TestInterfaceRegister())
    @test_throws MethodError nqubits(TestInterfaceRegister())
    @test_throws MethodError nremain(TestInterfaceRegister())
end

@testset "Test default fallbacks" begin
    @test nbatch(TestInterfaceRegister()) == 1
end

@testset "adjoint register" begin
    @test adjoint(TestRegister()) isa AdjointRegister
    @test adjoint(adjoint(TestRegister())) isa TestRegister
    @test nqubits(adjoint(TestRegister())) == 8
    @test nactive(adjoint(TestRegister())) == 2
end

@testset "Test Printing" begin
    @test repr(TestRegister()) == """
    TestRegister{1}
        active qubits: 2/8"""
end

@testset "Test adjoint printing" begin
    @test repr(adjoint(TestRegister())) == """
        adjoint(TestRegister{1})
            active qubits: 2/8"""
end


# Method specialisation singletons.
struct GeoPlot end
struct GeoZPlot end
# We only look at arrays with X, Y, Z dims here.
# Otherwise they fall back to DimensionalData.jl recipes
@recipe function f(A::AbstractGeoArray)
    ddplot(A) = DimArray(A; dims=_maybe_mapped(dims(A)))
    max_res = get(plotattributes, :max_res, 1000)
    if !(get(plotattributes, :seriestype, :none) in (:none, :heatmap, :contourf))
        DD.DimensionalPlot(), ddplot(A)
    elseif all(hasdim(A, (SpatialDim, SpatialDim)))
        # Heatmap or multiple heatmaps. Use GD recipes.
        A = _prepare(_subsample(A, max_res))
        GeoPlot(), A
    elseif hasdim(A, ZDim) && ndims(A) == 1
        # Z dim plot, but for spatial data we want Z on the Y axis
        GeoZPlot(), _prepare(A)
    else
        DD.DimensionalPlot(), ddplot(A)
    end
end
# Plot a sinlge 2d map
@recipe function f(::GeoPlot, A::GeoArray{T,2,<:Tuple{D1,D2}}) where {T,D1<:SpatialDim,D2<:SpatialDim}
    # If colorbar is close to symmetric (< 25% difference) use a symmetric
    # colormap and set symmetric limits so zero shows up as a neutral color.

    yguide, xguide = label(dims(A))

    y, x = map(_prepare, dims(A))

    rdt = DD.refdims_title(A; issingle=true)
    :title --> (rdt === "" ? _maybename(A) : _maybename(A) * "\n" * rdt)
    :xguide --> xguide
    :yguide --> yguide
    :grid --> true
    :gridalpha --> 0.2
    # :guidefontsize --> 10
    # :titlefontsize --> 10
    # :tickfontsize --> 6
    # :colorbar_title --> name(A)
    :linewidth --> 1
    :colorbar_titlefontsize --> 9
    :colorbar_tickfontcolor --> RGB(0.3)
    :tickfontcolor --> RGB(0.3)
    :tickcolor --> RGB(0.3)
    :tick_direction --> :out
    :framestyle --> :box
    :foreground_color_axis --> RGB(0.3)
    :foreground_color_border --> RGB(0.3)
    :seriescolor --> :curl

    if all(d -> lookup(d) isa Mapped, (x, y))
        :xlims --> mappedbounds(x)
        :ylims --> mappedbounds(y)
        :aspect_ratio --> :equal
    else
        :xlims --> bounds(A, x)
        :ylims --> bounds(A, y)
        bnds = bounds(A, (D1, D2))
        s1, s2 = map(((l, u),) -> (u - l), bnds) ./ (size(A, D1), size(A, D2))
        square_pixels = s2 / s1
        :aspect_ratio --> square_pixels
    end

    if get(plotattributes, :seriestype, :none) == :contourf
        clims = extrema(skipmissing(A))
        :levels --> range(clims[1], clims[2], length=20)
        index(x), index(y), clamp.(A, clims[1], clims[2])
    else
        :seriestype --> :heatmap
        index(x), index(y), parent(A)
    end
end

# Plot a vertical 1d line
@recipe function f(::GeoZPlot, A::GeoArray{T,1}) where T
    z_dim = dims(A, ZDim)
    yguide = label(z_dim)
    xguide = label(A)
    rdt = DD.refdims_title(A; issingle=true)
    :title --> (rdt == "" ? _maybename(A) : _maybename(A) * "\n" * rdt)
    :xguide --> xguide
    :yguide --> yguide
    :label --> ""
    z = map(_prepare, dims(A))
    parent(A), index(z)
end

# Plot 3d arrays as multiple tiled plots
@recipe function f(::GeoPlot, A::GeoArray{T,3,<:Tuple{<:SpatialDim,<:SpatialDim,D}}) where {T,D}
    nplots = size(A, 3)
    if nplots > 1
        ncols, nrows = _balance_grid(nplots)
        :layout --> (ncols, nrows)
        # link --> :both
        # :colorbar := false
        titles = string.(index(A, D))
        for r in 1:nrows, c in 1:ncols
            i = (r + (c - 1) * nrows)
            @series begin
                :titlefontsize := 9
                :tickfontsize := 7
                subplot := i
                if c != ncols || r != 1
                    :xformatter := _ -> ""
                    :yformatter := _ -> ""
                    :xguide := ""
                    :yguide := ""
                end
                if i <= nplots
                    title := titles[i]
                    GeoPlot(), A[:, :, i]
                else
                    :framestyle := :none
                    :legend := :none
                    []
                end
            end
        end
    else
        GeoPlot(), A[:, :, 1]
    end
end

function _balance_grid(nplots)
    ncols = (nplots - 1) ÷ ceil(Int, sqrt(nplots)) + 1
    nrows = (nplots - 1) ÷ ncols + 1
    return ncols, nrows
end

# We only look at arrays with X, Y, Z dims here.
# Otherwise they fall back to DimensionalData.jl recipes
@recipe function f(st::AbstractGeoStack)
    nplots = length(keys(st))
    if nplots > 1
        ncols, nrows = _balance_grid(nplots)
        :layout --> (ncols, nrows)
        # colorbar := false
        max_res = get(plotattributes, :max_res, 1000/(max(nrows, ncols)))

        l = DD.layers(st)
        for r in 1:nrows, c in 1:ncols
            i = (r + (c - 1) * nrows)
            @series begin
                :titlefontsize := 9
                :tickfontsize := 7
                subplot := i
                if c != ncols || r != 1
                    :xformatter := _ -> ""
                    :yformatter := _ -> ""
                    :xguide := ""
                    :yguide := ""
                end
                if i <= nplots
                    A = l[i]
                    title := string(keys(st)[i])
                    if length(dims(A, (XDim, YDim))) > 0
                        # Get a view of the first slice of the X/Y dimension
                        ods = otherdims(A, (X, Y))
                        if length(ods) > 0
                            od1s = map(d -> DD.basetypeof(d)(firstindex(d)), ods)
                            A = view(A, od1s...)
                        end
                        GeoPlot(), _prepare(_subsample(A, max_res))
                    else
                        framestyle := :none
                        legend := :none
                        []
                    end
                else
                    framestyle := :none
                    legend := :none
                    []
                end
            end
        end
    else
        first(st)
    end
end

# Plots.jl heatmaps pixels are centered.
# So we should center the index, and use the projected value.
_prepare(d::Dimension) = d |> _maybe_shift |> _maybe_mapped
# Convert arrays to a consistent missing value and Forward array order
function _prepare(A::AbstractGeoArray)
    reorder(A, DD.ForwardOrdered) |>
    a -> permutedims(a, DD.commondims(>:, (ZDim, YDim, XDim, TimeDim, Dimension), dims(A))) |>
    a -> replace_missing(a, missing)
end

function _subsample(A, max_res)
    ssdims = dims(A, (XDim, YDim, ZDim))[1:2]
    # Aggregate based on the number of pixels
    s1, s2 = size(A, ssdims[1]), size(A, ssdims[2])
    ag = floor(Int, max(s1, s2) / max_res) + 1
    if ag == 1
        return read(A)
    else
        return A[DD.basetypeof(ssdims[1])(1:ag:s1), DD.basetypeof(ssdims[2])(1:ag:s2)]
    end
end

_maybename(A) = _maybename(name(A))
_maybename(n::Name{N}) where N = _maybename(N)
_maybename(n::NoName) = ""
_maybename(n::Symbol) = string(n)

_maybe_replace_missing(A::AbstractArray{<:AbstractFloat}) = replace_missing(A, eltype(A)(NaN))
_maybe_replace_missing(A) = A

_maybe_shift(d) = _maybe_shift(sampling(d), d)
_maybe_shift(::Intervals, d) = DD.maybeshiftlocus(Center(), d)
_maybe_shift(sampling, d) = d

_maybe_mapped(dims::Tuple) = map(_maybe_mapped, dims)
_maybe_mapped(dim::Dimension) = _maybe_mapped(lookup(dim), dim)
_maybe_mapped(lookup::LookupArray, dim::Dimension) = dim
_maybe_mapped(lookup::Projected, dim::Dimension) = _maybe_mapped(mappedcrs(lookup), dim)
_maybe_mapped(::Nothing, dim::Dimension) = dim
_maybe_mapped(::GeoFormat, dim::Dimension) = convertlookup(Mapped, dim)

# We don't show the Band label for a single-band raster,
# it's just not interesting information.
function DD.refdims_title(refdim::Band; issingle=false)
    if issingle
        ""
    else
        string(name(refdim), ": ", DD.refdims_title(lookup(refdim), refdim))
    end
end


using JLD2

export obs_env
export AbstractLattice, SquareLattice
abstract type AbstractLattice end
struct SquareLattice <: AbstractLattice end

export VUMPSRuntime, SquareVUMPSRuntime
# NOTE: should be renamed to more explicit names
"""
    VUMPSRuntime{LT}

a struct to hold the tensors during the `vumps` algorithm, each is a `Ni` x `Nj` Matrix, containing
- `d × d × d × d'` `M[i,j]` tensor
- `D × d' × D` `AL[i,j]` tensor
- `D × D`     `C[i,j]` tensor
- `D × d' × D` `AR[i,j]` tensor
- `D × d' × D` `FL[i,j]` tensor
- `D × d' × D` `FR[i,j]` tensor
and `LT` is a AbstractLattice to define the lattice type.
"""
struct VUMPSRuntime{LT,T,N,AT <: AbstractArray{<:AbstractArray,2},CT,ET1,ET2}
    M::AT
    AL::ET1
    C::CT
    AR::ET1
    FL::ET2
    FR::ET2
    function VUMPSRuntime{LT}(M::AT, AL::ET1, C::CT, AR::ET1, FL::ET2, FR::ET2) where {LT <: AbstractLattice,AT <: AbstractArray{<:AbstractArray,2}, CT <: AbstractArray{<:AbstractArray,2}, ET1 <: AbstractArray{<:AbstractArray,2}, ET2 <: AbstractArray{<:AbstractArray,2}}
        T, N = eltype(M[1,1]), ndims(M[1,1])
        new{LT,T,N,AT,CT,ET1,ET2}(M, AL, C, AR, FL, FR)
    end
end

const SquareVUMPSRuntime{T,AT} = VUMPSRuntime{SquareLattice,T,4,AT}
function SquareVUMPSRuntime(M::AT, AL, C, AR, FL, FR) where {AT <: AbstractArray{<:AbstractArray,2}}
    ndims(M[1,1]) == 4 || throw(DimensionMismatch("M dimensions error, should be `4`, got $(ndims(M[1,1]))."))
    VUMPSRuntime{SquareLattice}(M, AL, C, AR, FL, FR)
end

@doc raw"
    SquareVUMPSRuntime(M::AbstractArray{T,4}, env::Val, χ::Int)

create a `SquareVUMPSRuntime` with M-tensor `M`. The `NixNj` `AL,C,AR,FL,FR`
tensors are initialized according to `env`. If `env = Val(:random)`,
the `A[i,j]` is initialized as a random `D×d×D` tensor,and `AL[i,j],C[i,j],AR[i,j]` are the corresponding
canonical form. `FL,FR` is the left and right environment.

# example

```jldoctest; setup = :(using VUMPS)
julia> Ni, Nj = 2, 2;

julia> M = Array{Array{ComplexF64,3},2}(undef, Ni, Nj);

julia> for j = 1:Nj, i = 1:Ni
           M[i,j] = rand(2,2,2,2)
       end

julia> rt = SquareVUMPSRuntime(M, Val(:random), 4);

julia> size(rt.AL) == (2,2)
true

julia> size(rt.AL[1,1]) == (4,2,4)
true
```
"
function SquareVUMPSRuntime(M::AbstractArray{<:AbstractArray,2}, env, χ::Int; verbose=false)
    return SquareVUMPSRuntime(M, _initializect_square(M, env, χ; verbose=verbose)...)
end

function _initializect_square(M::AbstractArray{<:AbstractArray,2}, env::Val{:random}, χ::Int; verbose=false)
    A = initialA(M, χ)
    AL, L = leftorth(A)
    R, AR = rightorth(AL)
    _, FL = leftenv(AL, AL, M)
    _, FR = rightenv(AR, AR, M)
    C = LRtoC(L,R)
    Ni, Nj = size(M)
    verbose && print("random initial $(Ni)×$(Nj) vumps_χ$(χ) environment-> ")
    AL, C, AR, FL, FR
end

function _initializect_square(M::AbstractArray{<:AbstractArray,2}, chkp_file::String, χ::Int; verbose=false)
    env = load(chkp_file)["env"]
    Ni, Nj = size(M)
    atype = _arraytype(M[1,1])
    verbose && print("vumps $(Ni)×$(Nj) environment load from $(chkp_file) -> ")
    AL, C, AR, FL, FR = env.AL, env.C, env.AR, env.FL, env.FR
    Zygote.@ignore begin
        AL, C, AR, FL, FR = Array{atype{ComplexF64,3},2}(env.AL), Array{atype{ComplexF64,2},2}(env.C), Array{atype{ComplexF64,3},2}(env.AR), Array{atype{ComplexF64,3},2}(env.FL), Array{atype{ComplexF64,3},2}(env.FR)
    end
    AL, C, AR, FL, FR
end

function vumps(rt::VUMPSRuntime; tol::Real=1e-10, maxiter::Int=10, miniter::Int=1, verbose=false, show_every = Inf)
    # initialize
    olderror = Inf
    vumps_counting = show_every_count(show_every)

    stopfun = StopFunction(olderror, -1, tol, maxiter, miniter)
    rt, err = fixedpoint(res -> vumpstep(res...;show_counting=vumps_counting), (rt, olderror), stopfun)
    verbose && println("vumps done@step: $(stopfun.counter), error=$(err)")
    return rt
end

function show_every_count(n::Number)
    i = 0
    counting() = (i += 1; mod(i,n)==0 ? i : 0)
    return counting
end

function vumpstep(rt::VUMPSRuntime, err; show_counting = show_every_count(Inf))
    temp = show_counting()
    temp != 0 && println("vumps@step: $(temp), error=$(err)")
    M, AL, C, AR, FL, FR = rt.M, rt.AL, rt.C, rt.AR, rt.FL, rt.FR
    AC = ALCtoAC(AL,C)
    _, ACp = ACenv(AC, FL, M, FR)
    _, Cp = Cenv(C, FL, FR)
    ALp, ARp, _, _ = ACCtoALAR(ACp, Cp)
    _, FL = leftenv(AL, ALp, M, FL)
    _, FR = rightenv(AR, ARp, M, FR)
    _, ACp = ACenv(ACp, FL, M, FR)
    _, Cp = Cenv(Cp, FL, FR)
    ALp, ARp, errL, errR = ACCtoALAR(ACp, Cp)
    erroverlap = error(ALp, Cp, ARp, FL, M, FR)
    err = erroverlap + errL + errR
    # @show error(ALp, Cp, ARp, FL, M, FR)
    # err = errL + errR
    err > 1e-8 && temp >= 10 && println("errL=$errL, errR=$errR, erroverlap=$erroverlap")
    return SquareVUMPSRuntime(M, ALp, Cp, ARp, FL, FR), err
end

"""
    uptodown(i,Ni,Nj)

````
i -> (i,j) -> (Nj +1 - i,j) -> ir
````
"""
function uptodown(i,Ni,Nj)
    Liner = LinearIndices((1:Ni,1:Nj))
    Cart = CartesianIndices((1:Ni,1:Nj))
    Index = Cart[i]
    i,j = Index[1],Index[2]
    ir = Ni + 1 - i
    Liner[ir,j]
end

"""
    env = vumps_env(M::AbstractArray; χ::Int, tol::Real=1e-10, maxiter::Int=10, miniter::Int=1, verbose = false, savefile = false, infolder::String="./data/", outfolder::String="./data/", direction::String= "up", downfromup = false, show_every = Inf)

sometimes the finally observable is symetric, so we can use the same up and down environment.
"""
function vumps_env(M::AbstractArray; χ::Int, tol::Real=1e-10, maxiter::Int=10, miniter::Int=1, verbose = false, savefile = false, infolder::String="./data/", outfolder::String="./data/", direction::String= "up", downfromup = false, show_every = Inf)
    verbose && (direction == "up" ? print("↑ ") : print("↓ "))
    downfromup && direction == "down" && (direction = "up")

    D = size(M[1,1],1)
    savefile && mkpath(outfolder)
    in_chkp_file = infolder*"/$(direction)_D$(D)_χ$(χ).jld2"

    if isfile(in_chkp_file)
        rtup = SquareVUMPSRuntime(M, in_chkp_file, χ; verbose = verbose)
    else
        rtup = SquareVUMPSRuntime(M, Val(:random), χ; verbose = verbose)
    end
    env = vumps(rtup; tol=tol, maxiter=maxiter, miniter=miniter, verbose = verbose, show_every = show_every)

    Zygote.@ignore savefile && begin
        out_chkp_file = outfolder*"/$(direction)_D$(D)_χ$(χ).jld2"
        ALs, Cs, ARs, FLs, FRs = Array{Array{ComplexF64,3},2}(env.AL), Array{Array{ComplexF64,2},2}(env.C), Array{Array{ComplexF64,3},2}(env.AR), Array{Array{ComplexF64,3},2}(env.FL), Array{Array{ComplexF64,3},2}(env.FR)
        envsave = SquareVUMPSRuntime(M, ALs, Cs, ARs, FLs, FRs)
        save(out_chkp_file, "env", envsave)
    end
    env
end

"""
    M, ALu, Cu, ARu, ALd, Cd, ARd, FL, FR, envup.FL, envup.FR = obs_env(M::AbstractArray; χ::Int, tol::Real=1e-10, maxiter::Int=10, miniter::Int=1, verbose=false, savefile= false, infolder::String="./data/", outfolder::String="./data/", updown = true, downfromup = false, show_every = Inf)

If `Ni,Nj>1` and `Mij` are different bulk tensor, the up and down environment are different. So to calculate observable, we must get ACup and ACdown, which is easy to get by overturning the `Mij`. Then be cautious to get the new `FL` and `FR` environment.
"""
function obs_env(M::AbstractArray; χ::Int, tol::Real=1e-10, maxiter::Int=10, miniter::Int=1, verbose=false, savefile= false, infolder::String="./data/", outfolder::String="./data/", updown = true, downfromup = false, show_every = Inf)
    M /= norm(M)
    envup = vumps_env(M; χ=χ, tol=tol, maxiter=maxiter, miniter=miniter, verbose=verbose, savefile=savefile, infolder=infolder,outfolder=outfolder, direction="up", downfromup=downfromup, show_every = show_every)
    ALu,ARu,Cu = envup.AL,envup.AR,envup.C

    D = size(M[1,1],1)
    atype = _arraytype(M[1,1])
    in_chkp_file_obs = infolder*"/obs_D$(D)_χ$(χ).jld2"
    if isfile(in_chkp_file_obs)
        verbose && println("←→ observable environment load from $(in_chkp_file_obs)")
        FL, FR = load(in_chkp_file_obs)["env"]
        Zygote.@ignore begin
            FL, FR = Array{atype{ComplexF64,3},2}(FL), Array{atype{ComplexF64,3},2}(FR)
        end
    else
        FL, FR = envup.FL, envup.FR
    end

    if updown
        Ni, Nj = size(ALu)
        Md = [permutedims(M[uptodown(i,Ni,Nj)], (1,4,3,2)) for i = 1:Ni*Nj]
        Md = reshape(Md, Ni, Nj)

        envdown = vumps_env(Md; χ=χ, tol=tol, maxiter=maxiter, miniter=miniter, verbose=verbose, savefile=savefile, infolder=infolder, outfolder=outfolder, direction="down", downfromup=downfromup, show_every = show_every)
        ALd, ARd, Cd = envdown.AL, envdown.AR, envdown.C
    else
        ALd, ARd, Cd = ALu, ARu, Cu
    end

    _, FL = obs_FL(ALu, ALd, M, FL)
    _, FR = obs_FR(ARu, ARd, M, FR)
    Zygote.@ignore savefile && begin
        out_chkp_file_obs = outfolder*"/obs_D$(D)_χ$(χ).jld2"
        envsave = (Array{Array{ComplexF64,3},2}(FL), Array{Array{ComplexF64,3},2}(FR))
        save(out_chkp_file_obs, "env", envsave)
    end
    return M, ALu, Cu, ARu, ALd, Cd, ARd, FL, FR, envup.FL, envup.FR
end


module UI

import GLFW
using ModernGL

include("gltools.jl")

GLFW.Init()

# OS X-specific GLFW hints to initialize the correct version of OpenGL
@osx_only begin
    GLFW.WindowHint(GLFW.CONTEXT_VERSION_MAJOR, 3)
    GLFW.WindowHint(GLFW.CONTEXT_VERSION_MINOR, 2)
    GLFW.WindowHint(GLFW.OPENGL_PROFILE, GLFW.OPENGL_CORE_PROFILE)
    GLFW.WindowHint(GLFW.OPENGL_FORWARD_COMPAT, GL_TRUE)
end

# Create a windowed mode window and its OpenGL context
window = GLFW.CreateWindow(600, 600, "OpenGL Example")

# Make the window's context current
GLFW.MakeContextCurrent(window)

# The data for our triangle
data = GLfloat[
    0.0, 0.5,
    0.5, -0.5,
    -0.5,-0.5
]

# Generate a vertex array and array buffer for our data
vao = glGenVertexArray()
glBindVertexArray(vao)

vbo = glGenBuffer()
glBindBuffer(GL_ARRAY_BUFFER, vbo)
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW)

# Create and initialize shaders
const vsh = """
    #version 330
    in vec2 position;

    void main() {
        gl_Position = vec4(position, 0.0, 1.0);
    }
"""

const fsh = """
    #version 330
    out vec4 outColor;

    void main() {
        outColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
"""

vertexShader = createShader(vsh, GL_VERTEX_SHADER)
fragmentShader = createShader(fsh, GL_FRAGMENT_SHADER)
program = createShaderProgram(vertexShader, fragmentShader)
glUseProgram(program)

positionAttribute = glGetAttribLocation(program, "position");

glEnableVertexAttribArray(positionAttribute)
glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, false, 0, 0)

t = 0

# Loop until the user closes the window
while !GLFW.WindowShouldClose(window)
    # Pulse the background blue
    t += 1
    glClearColor(0.0, 0.0, 0.5 * (1 + sin(t * 0.02)), 1.0)
    glClear(GL_COLOR_BUFFER_BIT)
    # Draw our triangle
    glDrawArrays(GL_TRIANGLES, 0, 3)

    # Swap front and back buffers
    GLFW.SwapBuffers(window)

    # Poll for and process events
    GLFW.PollEvents()
end

GLFW.Terminate()

end


module DrakeVisualizer

using LCMCore
using GeometryTypes
using FileIO
using LinearAlgebra

import MeshIO
import JSON

using Meshing: MarchingTetrahedra
using Rotations: Rotation, Quat, rotation_between
using CoordinateTransformations: Transformation,
                                 transform_deriv,
                                 IdentityTransformation,
                                 AbstractAffineMap,
                                 AffineMap,
                                 LinearMap,
                                 Translation,
                                 compose
using ColorTypes: RGB, RGBA, Colorant, red, green, blue, alpha
using StaticArrays: SVector, StaticArray, SMatrix
using DataStructures: OrderedDict

import GeometryTypes: origin, radius, raw
import Base: convert, length, show, isempty, empty!, delete!

export GeometryData,
        Link,
        Robot,
        Visualizer,
        HyperRectangle,
        HyperEllipsoid,
        Cylinder,
        HyperSphere,
        HyperCube,
        MeshFile,
        PointCloud,
        Point,
        Vec,
        Triad,
        PolyLine,
        ArrowHead,
        contour_mesh,
        settransform!,
        setgeometry!,
        addgeometry!,
        load!,
        draw!,
        delete!,
        batch

const drake_visualizer_executable_name = "drake-visualizer"

function new_window(; script::Union{AbstractString, Nothing} = nothing)
    installed_visualizer_path = joinpath(dirname(@__FILE__), "..", "deps", "usr", "bin", "$drake_visualizer_executable_name")
    drake_visualizer = if isfile(installed_visualizer_path)
        # If we built drake-visualizer, then use it
        installed_visualizer_path
    else
        # Otherwise let the system try to find it
        drake_visualizer_executable_name
    end
    command = script == nothing ? `$drake_visualizer` : `$drake_visualizer --script $script`
    proc = open(command)
    proc
end

function any_open_windows()
    @static if Sys.isapple()
        return success(`pgrep $drake_visualizer_executable_name`)
    elseif Sys.islinux()
        return success(`pgrep -f $drake_visualizer_executable_name`)
    else
        warn("DrakeVisualizer.any_open_windows not implemented for $(Sys.KERNEL). This function will always return false.")
        return false
    end
end

function delete_director_binaries(skip_confirmation=false)
    root = joinpath(dirname(dirname(@__FILE__)), "deps")
    binary_paths = [
        joinpath(root, "usr"),
        joinpath(root, "downloads"),
        joinpath(root, "src"),
        joinpath(root, "builds"),
        joinpath(root, "deps.jl")
    ]
    if !skip_confirmation
        println("CAUTION: This will delete all downloaded binaries of Director.")
        println("The following paths will be deleted:")
        for path in binary_paths
            println(path)
        end
        print("After doing this, you will need to run 'using Pkg; Pkg.build(\"DrakeVisualizer\")'. Proceed? (y/n) ")
        choice = chomp(readline())
        if lowercase(choice[1]) != 'y'
            println("Canceled.")
            return
        end
    end
    for path in binary_paths
        println("Removing $path")
        rm(path, force=true, recursive=true)
    end
end

include("lcmtypes/comms_t.jl")
include("lazytree.jl")
include("contour_meshes.jl")
include("geometry_types.jl")
include("visualizer.jl")
include("serialization.jl")

end


function _precompile_()
    ccall(:jl_generating_output, Cint, ()) == 1 || return nothing
    precompile(Tuple{Core.kwftype(typeof(ITensors.eigen)),NamedTuple{(:ishermitian, :which_decomp, :tags, :maxdim, :mindim, :cutoff, :eigen_perturbation, :ortho),Tuple{Bool,Nothing,TagSet,Int64,Int64,Float64,Nothing,String}},typeof(eigen),ITensor{2},IndexSet{1,Index{Int64},Tuple{Index{Int64}}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}})
    precompile(Tuple{Core.kwftype(typeof(ITensors.factorize)),NamedTuple{(:which_decomp, :tags, :maxdim, :mindim, :cutoff, :eigen_perturbation, :ortho),Tuple{Nothing,TagSet,Int64,Int64,Float64,Nothing,String}},typeof(factorize),ITensor{3},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}})
    precompile(Tuple{Core.kwftype(typeof(ITensors.replacebond!)),NamedTuple{(:maxdim, :mindim, :cutoff, :eigen_perturbation, :ortho, :which_decomp),Tuple{Int64,Int64,Float64,Nothing,String,Nothing}},typeof(replacebond!),MPS,Int64,ITensor{3}})
    precompile(Tuple{Core.kwftype(typeof(KrylovKit.eigsolve)),NamedTuple{(:ishermitian, :tol, :krylovdim, :maxiter),Tuple{Bool,Float64,Int64,Int64}},typeof(KrylovKit.eigsolve),ProjMPO,ITensor{3},Int64,Symbol})
    precompile(Tuple{Core.kwftype(typeof(KrylovKit.expand!)),NamedTuple{(:verbosity,),Tuple{Int64}},typeof(KrylovKit.expand!),KrylovKit.LanczosIterator{ProjMPO,ITensor{3},KrylovKit.ModifiedGramSchmidt2},KrylovKit.LanczosFactorization{ITensor{3},Float64}})
    precompile(Tuple{Core.kwftype(typeof(NDTensors.eigen)),NamedTuple{(:ishermitian, :which_decomp, :tags, :maxdim, :mindim, :cutoff, :eigen_perturbation, :ortho),Tuple{Bool,Nothing,TagSet,Int64,Int64,Float64,Nothing,String}},typeof(eigen),LinearAlgebra.Hermitian{Float64,NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}}}})
    precompile(Tuple{Core.kwftype(typeof(NDTensors.svd)),NamedTuple{(:alg,),Tuple{String}},typeof(svd),NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}}})
    precompile(Tuple{Core.kwftype(typeof(NDTensors.svd)),NamedTuple{(:alg,),Tuple{String}},typeof(svd),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}}})
    precompile(Tuple{ProjMPO,ITensor{3}})
    precompile(Tuple{Type{MPO},AutoMPO,Array{Index{Int64},1}})
    precompile(Tuple{Type{TagSet},String})
    precompile(Tuple{typeof(*),Float64,NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}}})
    precompile(Tuple{typeof(*),ITensor{2},ITensor{3}})
    precompile(Tuple{typeof(*),ITensor{3},ITensor{1}})
    precompile(Tuple{typeof(*),ITensor{3},ITensor{2}})
    precompile(Tuple{typeof(*),ITensor{3},ITensor{3}})
    precompile(Tuple{typeof(*),ITensor{4},ITensor{1}})
    precompile(Tuple{typeof(*),ITensor{4},ITensor{4}})
    precompile(Tuple{typeof(==),Array{Index{Int64},1},Array{Index{Int64},1}})
    precompile(Tuple{typeof(==),IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}})
    precompile(Tuple{typeof(==),TagSet,TagSet})
    precompile(Tuple{typeof(Base.Broadcast.materialize!),ITensor{4},Base.Broadcast.Broadcasted{ITensors.ITensorStyle,Nothing,typeof(+),Tuple{ITensor{4},ITensor{4}}}})
    precompile(Tuple{typeof(Base.afoldl),typeof(*),ITensor{4},ITensor{3}})
    precompile(Tuple{typeof(ITensors.setstore!),ITensor{2},NDTensors.Dense{Float64,Array{Float64,1}}})
    precompile(Tuple{typeof(KrylovKit.orthogonalize!),ITensor{3},ITensor{3},KrylovKit.ModifiedGramSchmidt2})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Number,2,NDTensors.Combiner,IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Diag{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,0,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{0,Union{},Tuple{}}},Tuple{},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Number,2,NDTensors.Combiner,IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Diag{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,1,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}},Tuple{Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,1,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}},Tuple{Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(NDTensors.contract!!),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64}})
    precompile(Tuple{typeof(NDTensors.contraction_output),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},IndexSet{0,Union{},Tuple{}}})
    precompile(Tuple{typeof(NDTensors.contraction_output),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NDTensors.Tensor{Float64,1,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}})
    precompile(Tuple{typeof(NDTensors.getperm),IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}})
    precompile(Tuple{typeof(NDTensors.permutedims!!),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},Function})
    precompile(Tuple{typeof(NDTensors.permutedims!!),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},Function})
    precompile(Tuple{typeof(Random.randn!),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}}})
    precompile(Tuple{typeof(axpy!),Float64,ITensor{3},ITensor{3}})
    precompile(Tuple{typeof(combiner),Index{Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Number,2,NDTensors.Combiner,IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NTuple{4,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Number,2,NDTensors.Combiner,IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Number,2,NDTensors.Combiner,IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Diag{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Diag{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Diag{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,2,NDTensors.Diag{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,1,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}},Tuple{Int64},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,1,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}},Tuple{Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NTuple{4,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},Tuple{Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},Tuple{}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,1,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}},Tuple{Int64},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,1,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}},Tuple{Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,3,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}}},Tuple{Int64,Int64,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NTuple{4,Int64}})
    precompile(Tuple{typeof(contract),NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64},NDTensors.Tensor{Float64,4,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{4,Index{Int64},NTuple{4,Index{Int64}}}},NTuple{4,Int64}})
    precompile(Tuple{typeof(hassameinds),ITensor{3},Tuple{Index{Int64},Index{Int64},Index{Int64}}})
    precompile(Tuple{typeof(hastags),TagSet,String})
    precompile(Tuple{typeof(itensor),Array{Float64,2},Index{Int64},Index{Int64}})
    precompile(Tuple{typeof(itensor),Array{Float64,3},Index{Int64},Index{Int64},Vararg{Index,N} where N})
    precompile(Tuple{typeof(mul!),ITensor{3},ITensor{3},Float64})
    precompile(Tuple{typeof(mul!),ITensor{3},KrylovKit.OrthonormalBasis{ITensor{3}},SubArray{Float64,1,Array{Float64,2},Tuple{UnitRange{Int64},Int64},true}})
    precompile(Tuple{typeof(prime),ITensor{3},ITensors.Not{TagSet}})
    precompile(Tuple{typeof(prime),IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}},Int64,ITensors.Not{IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}}})
    precompile(Tuple{typeof(randomITensor),Index{Int64},Index{Int64},Index{Int64}})
    precompile(Tuple{typeof(randomMPS),Array{Index{Int64},1},Int64})
    precompile(Tuple{typeof(replaceind!),ITensor{2},Index{Int64},Index{Int64}})
    precompile(Tuple{typeof(replaceind!),ITensor{3},Index{Int64},Index{Int64}})
    precompile(Tuple{typeof(replaceind),IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}},Index{Int64},Index{Int64}})
    precompile(Tuple{typeof(replaceinds),IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}},Tuple{Index{Int64},Index{Int64}},Tuple{Index{Int64},Index{Int64}}})
    precompile(Tuple{typeof(replaceinds),IndexSet{3,Index{Int64},Tuple{Index{Int64},Index{Int64},Index{Int64}}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}},IndexSet{1,Index{Int64},Tuple{Index{Int64}}}})
    precompile(Tuple{typeof(setindex!),Dict{Array{ITensors.SiteOp,1},Array{Float64,2}},Array{Float64,2},Array{ITensors.SiteOp,1}})
    precompile(Tuple{typeof(svd),NDTensors.Tensor{Complex{Float64},2,NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}}})
    precompile(Tuple{typeof(svd),NDTensors.Tensor{Float64,2,NDTensors.Dense{Float64,Array{Float64,1}},IndexSet{2,Index{Int64},Tuple{Index{Int64},Index{Int64}}}}})
    precompile(Tuple{typeof(zip),IndexSet{1,Index{Int64},Tuple{Index{Int64}}},Vararg{IndexSet{1,Index{Int64},Tuple{Index{Int64}}},N} where N})
end