"""
`module  JAC.IsotopeShift`
    ... a submodel of JAC that contains all methods for computing isotope-dependent properties for some level(s).
"""
module IsotopeShift

    using Printf, ..Basics, ..Defaults, ..InteractionStrength, ..ManyElectron, ..Nuclear, ..Radial, ..RadialIntegrals,
                  ..SpinAngular, ..TableStrings

    """
    `struct  IsotopeShift.Settings`
        ... defines a type for the details and parameters of computing isotope-shift M and F parameters.

        + calcNMS                  ::Bool             ... True if mass-shift parameters M_nmn need to be calculated, and false otherwise.
        + calcSMS                  ::Bool             ... True if mass-shift parameters M_sms need to be calculated, and false otherwise.
        + calcF                    ::Bool             ... True if the field-shift parameter need to be calculated, and false otherwise.
        + calcBoson                ::Bool             ... True if the boson-field shift parameter need to be calculated, and false otherwise.
        + printBefore              ::Bool             ... True if a list of selected levels is printed before the actual computations start.
        + bosonMass                ::Float64          ... mass of the scalar boson [e_electron].
        + levelSelection           ::LevelSelection   ... Specifies the selected levels, if any.
    """
    struct Settings
        calcNMS                    ::Bool
        calcSMS                    ::Bool
        calcF                      ::Bool
        calcBoson                  ::Bool
        printBefore                ::Bool
        bosonMass                  ::Float64
        levelSelection             ::LevelSelection
    end

    """
    `IsotopeShift.Settings(; calcNMS::Bool=true,` calcSMS::Bool=false, calcF::Bool=false, calcBoson::Bool=false,
                             printBefore::Bool=true, bosonMass::Float64=0., levelSelection::LevelSelection=LevelSelection())
        ... keyword constructor to overwrite selected value of isoshift computations.
    """
    function Settings(; calcNMS::Bool=true, calcSMS::Bool=false, calcF::Bool=false, calcBoson::Bool=false,
                             printBefore::Bool=true, bosonMass::Float64=0., levelSelection::LevelSelection=LevelSelection())
        Settings(calcNMS, calcSMS, calcF, calcBoson, printBefore, bosonMass, levelSelection)
    end

    # `Base.show(io::IO, settings::IsotopeShift.Settings)`  ... prepares a proper printout of the variable settings::IsotopeShift.Settings.
    function Base.show(io::IO, settings::IsotopeShift.Settings)
        println(io, "calcNMS:                  $(settings.calcNMS)  ")
        println(io, "calcSMS:                  $(settings.calcSMS)  ")
        println(io, "calcF:                    $(settings.calcF)  ")
        println(io, "calcBoson:                $(settings.calcBoson)  ")
        println(io, "printBefore:              $(settings.printBefore)  ")
        println(io, "bosonMass:                $(settings.bosonMass)  ")
        println(io, "levelSelection:           $(settings.levelSelection)  ")
    end

    """
    `struct  IsotopeShift.Outcome`
        ... defines a type to keep the outcome of a isotope-shift computation, such as the K and F parameters as well other
            results.

        + level                     ::Level              ... Atomic level to which the outcome refers to.
        + Knms                      ::Float64            ... K_nms parameter
        + Ksms                      ::Float64            ... K_sms parameter
        + Fme                       ::Float64            ... F parameter from matrix element.
        + Fdensity                  ::Float64            ... F parameter from density.
        + Xboson                    ::Float64            ... X boson-field shift constant.
        + amplitudeKnms             ::Complex{Float64}   ... K_nms amplitude
        + amplitudeKsmsA            ::Complex{Float64}   ... K_sms,A amplitude
        + amplitudeKsmsB            ::Complex{Float64}   ... K_sms,B amplitude
        + amplitudeKsmsC            ::Complex{Float64}   ... K_sms,C amplitude
    """
    struct Outcome
        level                       ::Level
        Knms                        ::Float64
        Ksms                        ::Float64
        Fme                         ::Float64
        Fdensity                    ::Float64
        Xboson                      ::Float64
        amplitudeKnms               ::Complex{Float64}
        amplitudeKsmsA              ::Complex{Float64}
        amplitudeKsmsB              ::Complex{Float64}
        amplitudeKsmsC              ::Complex{Float64}
    end

    """
    `IsotopeShift.Outcome()`  ... constructor for an `empty` instance of Hfs.Outcome for the computation of isotope-shift properties.
    """
    function Outcome()
        Outcome(Level(), 0., 0., 0., 0., 0.,   0., 0., 0., 0.)
    end

    # `Base.show(io::IO, outcome::IsotopeShift.Outcome)`  ... prepares a proper printout of the variable outcome::IsotopeShift.Outcome.
    function Base.show(io::IO, outcome::IsotopeShift.Outcome)
        println(io, "level:                   $(outcome.level)  ")
        println(io, "Knms:                    $(outcome.Knms)  ")
        println(io, "Ksms:                    $(outcome.Ksms)  ")
        println(io, "Fme:                     $(outcome.Fme)  ")
        println(io, "Fdensity:                $(outcome.Fdensity)  ")
        println(io, "Xboson:                  $(outcome.Xboson)  ")
        println(io, "amplitudeKnms:           $(outcome.amplitudeKnms)  ")
        println(io, "amplitudeKsmsA:          $(outcome.amplitudeKsmsA)  ")
        println(io, "amplitudeKsmsB:          $(outcome.amplitudeKsmsB)  ")
        println(io, "amplitudeKsmsC:          $(outcome.amplitudeKsmsC)  ")
    end

    """
    `IsotopeShift.amplitude(kind::String, rLevel::Level, sLevel::Level, nm::Nuclear.Model, grid::Radial.Grid)`
        ... to compute either the  H^(NMS),  H^(SMS,A),  H^(SMS,B) or  H^(SMS,C) normal and specific mass-shift amplitudes
            <alpha_r J_r || H^(A)) || alpha_s J_s>  for a given pair of levels. A value::ComplexF64 is returned.
    """
    function  amplitude(kind::String, rLevel::Level, sLevel::Level, nm::Nuclear.Model, grid::Radial.Grid)
        nr = length(rLevel.basis.csfs);    ns = length(sLevel.basis.csfs);    matrix = zeros(ComplexF64, nr, ns)
        printstyled("Compute mass-shift $(kind[1:9]) matrix of dimension $nr x $ns ... ", color=:light_green)
        #
        if  rLevel.parity != sLevel.parity   return( ComplexF64(0.) )   end
        #
        for  r = 1:nr
            for  s = 1:ns
                me = 0.
                if  rLevel.basis.csfs[r].parity  != rLevel.parity    ||  sLevel.basis.csfs[s].parity  != sLevel.parity  ||
                    rLevel.parity != sLevel.parity    continue
                end
                #
                if      kind == "H^(NMS)   amplitude"
                #------------------------------------
                    ##x wa = compute("angular coefficients: e-e, Ratip2013", rLevel.basis.csfs[r], sLevel.basis.csfs[s])
                    subshellList = rLevel.basis.subshells
                    op = SpinAngular.OneParticleOperator(0, plus, true)
                    wa = SpinAngular.computeCoefficients(op, rLevel.basis.csfs[r], sLevel.basis.csfs[s], subshellList)
                    for  coeff in wa
                        ja = Basics.subshell_2j(rLevel.basis.orbitals[coeff.a].subshell)
                        jb = Basics.subshell_2j(sLevel.basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.hamiltonian_nms(rLevel.basis.orbitals[coeff.a], sLevel.basis.orbitals[coeff.b], nm, grid)
                        me = me + coeff.T  * sqrt( ja + 1) * tamp
                    end
                #
                elseif  kind == "H^(SMS,A) amplitude"
                #------------------------------------
                    ##x wa = compute("angular coefficients: e-e, Ratip2013", rLevel.basis.csfs[r], sLevel.basis.csfs[s])
                    subshellList = rLevel.basis.subshells
                    op = SpinAngular.TwoParticleOperator(0, plus, true)
                    wa = SpinAngular.computeCoefficients(op, rLevel.basis.csfs[r], sLevel.basis.csfs[s], subshellList)
                    for  coeff in wa
                        if  coeff.nu != 1  continue   end
                        ja = Basics.subshell_2j(rLevel.basis.orbitals[coeff.a].subshell)
                        jb = Basics.subshell_2j(sLevel.basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.X_smsA(rLevel.basis.orbitals[coeff.a], rLevel.basis.orbitals[coeff.b],
                                                           sLevel.basis.orbitals[coeff.c], sLevel.basis.orbitals[coeff.d], nm, grid)
                        me = me + coeff.V * sqrt( ja + 1) * tamp
                    end
                #
                elseif  kind == "H^(SMS,B) amplitude"
                #------------------------------------
                    ##x wa = compute("angular coefficients: e-e, Ratip2013", rLevel.basis.csfs[r], sLevel.basis.csfs[s])
                    subshellList = rLevel.basis.subshells
                    op = SpinAngular.TwoParticleOperator(0, plus, true)
                    wa = SpinAngular.computeCoefficients(op, rLevel.basis.csfs[r], sLevel.basis.csfs[s], subshellList)
                    for  coeff in wa
                        if  coeff.nu != 1  continue   end
                        ja = Basics.subshell_2j(rLevel.basis.orbitals[coeff.a].subshell)
                        jb = Basics.subshell_2j(sLevel.basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.X_smsB(rLevel.basis.orbitals[coeff.a], rLevel.basis.orbitals[coeff.b],
                                                           sLevel.basis.orbitals[coeff.c], sLevel.basis.orbitals[coeff.d], nm, grid)
                        me = me + coeff.V * sqrt( ja + 1) * tamp
                    end
                #
                elseif  kind == "H^(SMS,C) amplitude"
                #------------------------------------
                    ##x wa = compute("angular coefficients: e-e, Ratip2013", rLevel.basis.csfs[r], sLevel.basis.csfs[s])
                    subshellList = rLevel.basis.subshells
                    op = SpinAngular.TwoParticleOperator(0, plus, true)
                    wa = SpinAngular.computeCoefficients(op, rLevel.basis.csfs[r], sLevel.basis.csfs[s], subshellList)
                    for  coeff in wa
                        if  coeff.nu != 1  continue   end
                        ja = Basics.subshell_2j(rLevel.basis.orbitals[coeff.a].subshell)
                        jb = Basics.subshell_2j(sLevel.basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.X_smsC(rLevel.basis.orbitals[coeff.a], rLevel.basis.orbitals[coeff.b],
                                                           sLevel.basis.orbitals[coeff.c], sLevel.basis.orbitals[coeff.d], nm, grid)
                        me = me + coeff.V * sqrt( ja + 1) * tamp
                    end
                #
                else    error("stop a")
                end
                #
                matrix[r,s] = me
            end
        end
        printstyled("done.\n", color=:light_green)
        amplitude = transpose(rLevel.mc) * matrix * sLevel.mc
        #
        return( amplitude )
    end

    """
    `IsotopeShift.amplitude(kind::String, rLevel::Level, sLevel::Level, potential::Array{Float64,1}, grid::Radial.Grid)`
        ... to compute the H^(field-shift) field-shift amplitude  <alpha_r J_r || H^(field-shift) || alpha_s J_s> or the
            H^(boson-field) shift amplitude  <alpha_r J_r || H^(boson-field) || alpha_s J_s>  for a given pair of levels.
            The potential has to provide delta V^(nuc) for the field-shift amplitudes and the effective potential for the
            boson-field shift. A value::ComplexF64 is returned.
    """
    function  amplitude(kind::String, rLevel::Level, sLevel::Level, potential::Array{Float64,1}, grid::Radial.Grid)
        nr = length(rLevel.basis.csfs);    ns = length(sLevel.basis.csfs);    matrix = zeros(ComplexF64, nr, ns)
        printstyled("Compute field-shift $kind matrix of dimension $nr x $ns ... ", color=:light_green)
        #
        if  rLevel.parity != sLevel.parity   return( ComplexF64(0.) )   end
        #
        for  r = 1:nr
            for  s = 1:ns
                me = 0.
                if  rLevel.basis.csfs[r].parity  != rLevel.parity    ||  sLevel.basis.csfs[s].parity  != sLevel.parity  ||
                    rLevel.parity != sLevel.parity    continue
                end
                #
                if      kind == "H^(field-shift) amplitude"
                #------------------------------------------
                    subshellList = rLevel.basis.subshells
                    op = SpinAngular.OneParticleOperator(0, plus, true)
                    wa = SpinAngular.computeCoefficients(op, rLevel.basis.csfs[r], sLevel.basis.csfs[s], subshellList)
                    for  coeff in wa
                        ja = Basics.subshell_2j(rLevel.basis.orbitals[coeff.a].subshell)
                        jb = Basics.subshell_2j(sLevel.basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.fieldShift(rLevel.basis.orbitals[coeff.a], sLevel.basis.orbitals[coeff.b], potential, grid)
                        me = me + coeff.T  * sqrt( ja + 1) * tamp
                    end
                #
                elseif  kind == "H^(boson-field) amplitude"
                #------------------------------------------
                    subshellList = rLevel.basis.subshells
                    op = SpinAngular.OneParticleOperator(0, plus, true)
                    wa = SpinAngular.computeCoefficients(op, rLevel.basis.csfs[r], sLevel.basis.csfs[s], subshellList)
                    for  coeff in wa
                        ja = Basics.subshell_2j(rLevel.basis.orbitals[coeff.a].subshell)
                        jb = Basics.subshell_2j(sLevel.basis.orbitals[coeff.b].subshell)
                        tamp  = InteractionStrength.bosonShift(rLevel.basis.orbitals[coeff.a], sLevel.basis.orbitals[coeff.b], potential, grid)
                        me = me + coeff.T  * sqrt( ja + 1) * tamp
                    end
                #
                else    error("stop b")
                end
                #
                matrix[r,s] = me
            end
        end
        printstyled("done.\n", color=:light_green)
        amplitude = transpose(rLevel.mc) * matrix * sLevel.mc
        #
        return( amplitude )
    end

    """
    `IsotopeShift.computeAmplitudesProperties(outcome::IsotopeShift.Outcome, nm::Nuclear.Model, grid::Radial.Grid,
                                              settings::IsotopeShift.Settings)
        ... to compute all amplitudes and properties for a given level; an outcome::IsotopeShift.Outcome is returned for
            which the amplitudes and properties are now evaluated explicitly.
    """
    function  computeAmplitudesProperties(outcome::IsotopeShift.Outcome, nm::Nuclear.Model, grid::Radial.Grid, settings::IsotopeShift.Settings)
        amplitudeKnms   = 0.0;   Ksms  = 0.0;    F  = 0.0;    Xboson = 0.0;
        amplitudeKsmsA  = 0.0;   amplitudeKsmsB  = 0.0;  amplitudeKsmsC  = 0.0;  amplitudeF  = 0.0;
        if  settings.calcNMS
            amplitudeKnms  = IsotopeShift.amplitude("H^(NMS)   amplitude", outcome.level, outcome.level, nm, grid)
        end

        if  settings.calcSMS
            amplitudeKsmsA = IsotopeShift.amplitude("H^(SMS,A) amplitude", outcome.level, outcome.level, nm, grid)
            amplitudeKsmsB = IsotopeShift.amplitude("H^(SMS,B) amplitude", outcome.level, outcome.level, nm, grid)
            amplitudeKsmsC = IsotopeShift.amplitude("H^(SMS,C) amplitude", outcome.level, outcome.level, nm, grid)
            Ksms = (amplitudeKsmsA + amplitudeKsmsB + amplitudeKsmsC)
        end

        if  settings.calcF
            nmp       = Nuclear.Model(nm.Z, nm.mass+1.0)
            deltaPot  = Nuclear.nuclearPotential(nm, grid).Zr - Nuclear.nuclearPotential(nmp, grid).Zr
            deltaPot  = deltaPot / (nm.radius^2 - nmp.radius^2)
            lastPoint = Basics.lastPoint(deltaPot, 1.0e-9);   @show lastPoint
            #
            Fme       = IsotopeShift.amplitude("H^(field-shift) amplitude", outcome.level, outcome.level, deltaPot, grid)
            #
            density   = Basics.computeDensity(outcome.level, grid)
            wb        = zeros( lastPoint )
            wb[1:lastPoint] = - density[1:lastPoint] .* deltaPot[1:lastPoint] ./ grid.r[1:lastPoint]
            Fdensity  = RadialIntegrals.V0(wb, lastPoint, grid)
        end

        if  settings.calcBoson
            potential = zeros( grid.NoPoints );   malpha = settings.bosonMass / Defaults.getDefaults("alpha")
            for     i = 1:length(potential)    potential[i] = exp( -malpha*grid.r[i] ) / grid.r[i]    end
            Xboson    = IsotopeShift.amplitude("H^(boson-field) amplitude", outcome.level, outcome.level, potential, grid)
        end

        newOutcome = IsotopeShift.Outcome( outcome.level, amplitudeKnms, Ksms, Fme, Fdensity, Xboson,
                                           amplitudeKnms, amplitudeKsmsA, amplitudeKsmsB, amplitudeKsmsC )
        return( newOutcome )
    end

    """
    `IsotopeShift.computeOutcomes(multiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid, settings::IsotopeShift.Settings; output=true)`
        ... to compute (as selected) the isotope-shift M and F parameters for the levels of the given multiplet and as
            specified by the given settings. The results are printed in neat tables to screen but nothing is returned otherwise.
    """
    function computeOutcomes(multiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid, settings::IsotopeShift.Settings; output=true)
        println("")
        printstyled("IsotopeShift.computeOutcomes(): The computation of the isotope-shift parameters starts now ... \n", color=:light_green)
        printstyled("-------------------------------------------------------------------------------------------------- \n", color=:light_green)
        #
        ##x @show settings
        outcomes = IsotopeShift.determineOutcomes(multiplet, settings)
        # Display all selected levels before the computations start
        if  settings.printBefore    IsotopeShift.displayOutcomes(outcomes)    end
        # Calculate all amplitudes and requested properties
        newOutcomes = IsotopeShift.Outcome[]
        for  outcome in outcomes
            newOutcome = IsotopeShift.computeAmplitudesProperties(outcome, nm, grid, settings)
            push!( newOutcomes, newOutcome)
        end
        # Print all results to screen
        IsotopeShift.displayResults(stdout, newOutcomes, settings)
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary    IsotopeShift.displayResults(iostream, newOutcomes, settings)   end
        #
        if    output    return( newOutcomes )
        else            return( nothing )
        end
    end

    """
    `IsotopeShift.determineOutcomes(multiplet::Multiplet, settings::IsotopeShift.Settings)`
        ... to determine a list of Outcomes's for the computation of the isotope-shift M and F parameters for the given multiplet.
            It takes into account the particular selections and settings. An Array{IsotopeShift.Outcome,1} is returned. Apart from
            the level specification, all physical properties are set to zero during the initialization process.
    """
    function  determineOutcomes(multiplet::Multiplet, settings::IsotopeShift.Settings)
        outcomes = IsotopeShift.Outcome[]
        @show settings.levelSelection
        for  level  in  multiplet.levels
            if  Basics.selectLevel(level, settings.levelSelection)
                push!( outcomes, IsotopeShift.Outcome(level, 0., 0., 0., 0., 0.,   0., 0., 0., 0.) )
            end
        end
        return( outcomes )
    end

    """
    `IsotopeShift.displayOutcomes(outcomes::Array{IsotopeShift.Outcome,1})`
        ... to display a list of levels that have been selected for the computations. A small neat table of all
            selected levels and their energies is printed but nothing is returned otherwise.
    """
    function  displayOutcomes(outcomes::Array{IsotopeShift.Outcome,1})
        nx = 43
        println(" ")
        println("  Selected IsotopeShift levels:")
        println(" ")
        println("  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(10, "Level"; na=2);                             sb = sb * TableStrings.hBlank(12)
        sa = sa * TableStrings.center(10, "J^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Energy"; na=4);
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=4)
        println(sa);    println(sb);    println("  ", TableStrings.hLine(nx))
        #
        for  outcome in outcomes
            sa  = "  ";    sym = Basics.LevelSymmetry( outcome.level.J, outcome.level.parity)
            sa = sa * TableStrings.center(10, TableStrings.level(outcome.level.index); na=2)
            sa = sa * TableStrings.center(10, string(sym); na=4)
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", outcome.level.energy)) * "    "
            println( sa )
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `IsotopeShift.displayResults(stream::IO, outcomes::Array{Hfs.Outcome,1}, settings::IsotopeShift.Settings)`
        ... to display the energies, M_ms and F-parameters, etc. for the selected levels. A neat table is printed but nothing
            is returned otherwise.
    """
    function  displayResults(stream::IO, outcomes::Array{IsotopeShift.Outcome,1}, settings::IsotopeShift.Settings)
        nx = 133
        println(stream, " ")
        println(stream, "  IsotopeShift parameters and amplitudes:  Just take the difference K_i - K_f for transition lines.")
        println(stream, " ")
        println(stream, "  Boson mass = $(settings.bosonMass)")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  ";   sc = "  "
        sa = sa * TableStrings.center(10, "Level"; na=2);                             sb = sb * TableStrings.hBlank(12)
        sa = sa * TableStrings.center(10, "J^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Energy"; na=5)
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=5);  sc = sc * TableStrings.hBlank(45)
        sa = sa * TableStrings.center(11, "K_nms"; na=4)
        sb = sb * TableStrings.center(11, "[GHz u]" ; na=4)
        sc = sc * TableStrings.center(11, "[a.u.]" ; na=4)
        sa = sa * TableStrings.center(11, "K_sms"; na=4)
        sb = sb * TableStrings.center(11, "[GHz u]" ; na=4)
        sc = sc * TableStrings.center(11, "[a.u.]" ; na=4)
        sa = sa * TableStrings.center(11, "K_ms "; na=4)
        sb = sb * TableStrings.center(11, "[GHz u]" ; na=4)
        sc = sc * TableStrings.center(11, "[a.u.]" ; na=4)
        sa = sa * TableStrings.center(11, " F [ME]";   na=4)
        sb = sb * TableStrings.center(11, "[MHz/fm^2]" ; na=4)
        sa = sa * TableStrings.center(11, " F [dens]";   na=5)
        sb = sb * TableStrings.center(11, "[MHz/fm^2]" ; na=5)
        sa = sa * TableStrings.center(11, "X^(boson)";   na=4)
        sb = sb * TableStrings.center(11, "a.u."; na=4)
        println(stream, sa);    println(stream, sb);    ## println(stream, sc);
        println(stream, "  ", TableStrings.hLine(nx))
        #
        for  outcome in outcomes
            sa  = "  ";    sym = LevelSymmetry( outcome.level.J, outcome.level.parity)
            sa = sa * TableStrings.center(10, TableStrings.level(outcome.level.index); na=2)
            sa = sa * TableStrings.center(10, string(sym); na=4)
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", outcome.level.energy))            * "    "
            sa = sa * @sprintf("%.5e", outcome.Knms * 3609.4824)                                                      * "    "
            sa = sa * @sprintf("%.5e", outcome.Ksms * 3609.4824)                                                      * "    "
            sa = sa * @sprintf("%.5e", (outcome.Knms + outcome.Ksms) * 3609.4824)                                     * "    "
            sa = sa * @sprintf("%.5e", Defaults.convertUnits("energy: from atomic to Hz", outcome.Fme)      * 1.0e-6) * "    "
            sa = sa * @sprintf("%.5e", Defaults.convertUnits("energy: from atomic to Hz", outcome.Fdensity) * 1.0e-6) * "    "
            sa = sa * @sprintf("%.5e", Defaults.convertUnits("energy: from atomic to Hz", outcome.Xboson) )
            println(stream, sa )
            ## sb = TableStrings.hBlank(47)
        end
        println(stream, "  ", TableStrings.hLine(nx), "\n\n")
        #
        #
        # Now printout the individual amplitudes in an extra table
        nx = 158
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(10, "Level"; na=2);                             sb = sb * TableStrings.hBlank(12)
        sa = sa * TableStrings.center(10, "J^P";   na=4);                             sb = sb * TableStrings.hBlank(14)
        sa = sa * TableStrings.center(14, "Energy"; na=4);
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=4)
        sa = sa * TableStrings.center(26, "H^(nms) amplitude"    ; na=3);             sb = sb * TableStrings.hBlank(35)
        sa = sa * TableStrings.center(26, "H^(sms,A) amplitude"  ; na=3);             sb = sb * TableStrings.hBlank(35)
        sa = sa * TableStrings.center(26, "H^(sms,B) amplitude"  ; na=3);             sb = sb * TableStrings.hBlank(35)
        sa = sa * TableStrings.center(26, "H^(sms,C) amplitude"  ; na=3);             sb = sb * TableStrings.hBlank(35)
        println(stream, sa);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  outcome in outcomes
            sa  = "  ";    sym = LevelSymmetry( outcome.level.J, outcome.level.parity)
            sa = sa * TableStrings.center(10, TableStrings.level(outcome.level.index); na=2)
            sa = sa * TableStrings.center(10, string(sym); na=4)
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", outcome.level.energy))          * "    "
            sa = sa * @sprintf("%.5e %s %.5e", outcome.amplitudeKnms.re,  " ", outcome.amplitudeKnms.im)  * "    "
            sa = sa * @sprintf("%.5e %s %.5e", outcome.amplitudeKsmsA.re, " ", outcome.amplitudeKsmsA.im) * "    "
            sa = sa * @sprintf("%.5e %s %.5e", outcome.amplitudeKsmsB.re, " ", outcome.amplitudeKsmsB.im) * "    "
            sa = sa * @sprintf("%.5e %s %.5e", outcome.amplitudeKsmsC.re, " ", outcome.amplitudeKsmsC.im) * "    "
            println(stream, sa )
        end
        println(stream, "  ", TableStrings.hLine(nx))
        #
        #
        # Now printout the transition isotope parameters
        nx = 144
        println(stream, " ")
        println(stream, "  IsotopeShift parameters for individual transitions:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "  ";   sb = "  ";   sc = "  "
        sa = sa * TableStrings.center(14, "Transition";    na=2);                        sb = sb * TableStrings.hBlank(16)
        sa = sa * TableStrings.center(14, "I -- J^P -- F"; na=4);                        sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(14, "Energy"; na=5)
        sb = sb * TableStrings.center(14, TableStrings.inUnits("energy"); na=5);  sc = sc * TableStrings.hBlank(45)
        sa = sa * TableStrings.center(11, "K_nms"; na=5)
        sb = sb * TableStrings.center(11, "[GHz u]" ; na=5)
        sc = sc * TableStrings.center(11, "[a.u.]" ; na=5)
        sa = sa * TableStrings.center(11, "K_sms"; na=5)
        sb = sb * TableStrings.center(11, "[GHz u]" ; na=5)
        sc = sc * TableStrings.center(11, "[a.u.]" ; na=5)
        sa = sa * TableStrings.center(11, "K_ms "; na=5)
        sb = sb * TableStrings.center(11, "[GHz u]" ; na=5)
        sc = sc * TableStrings.center(11, "[a.u.]" ; na=5)
        sa = sa * TableStrings.center(11, " F [ME]";   na=4)
        sb = sb * TableStrings.center(11, "[MHz/fm^2]" ; na=4)
        sa = sa * TableStrings.center(11, " F [dens]";   na=4)
        sb = sb * TableStrings.center(11, "[MHz/fm^2]" ; na=4)
        sa = sa * TableStrings.center(11, "X^(boson)";   na=4)
        sb = sb * TableStrings.center(11, "a.u."; na=4)
        println(stream, sa);    println(stream, sb);    ## println(stream, sc);
        println(stream, "  ", TableStrings.hLine(nx))
        #
        for  ioutcome in outcomes
            for  foutcome in outcomes
            if ioutcome.level.index >= foutcome.level.index     continue    end
            sa  = "";    isym = LevelSymmetry( ioutcome.level.J, ioutcome.level.parity)
                         fsym = LevelSymmetry( foutcome.level.J, foutcome.level.parity)
            sa = sa * TableStrings.center(14, TableStrings.levels_if(ioutcome.level.index, foutcome.level.index); na=2)
            sa = sa * TableStrings.center(18, string(isym) * "  --   " * string(fsym); na=2)
            en = abs(ioutcome.level.energy - foutcome.level.energy)
            sa = sa * @sprintf("%.8e", Defaults.convertUnits("energy: from atomic", en))                              * "     "
            sa = sa * @sprintf("%.5e", (ioutcome.Knms-foutcome.Knms) * 3609.4824)                                     * "    "
            sa = sa * @sprintf("%.5e", (ioutcome.Ksms-foutcome.Ksms) * 3609.4824)                                     * "     "
            sa = sa * @sprintf("%.5e", (ioutcome.Knms + ioutcome.Ksms - foutcome.Knms - foutcome.Ksms) * 3609.4824)   * "     "
            sa = sa * @sprintf("%.5e", Defaults.convertUnits("energy: from atomic to Hz", ioutcome.Fme-foutcome.Fme)  * 1.0e-6) * "    "
            sa = sa * @sprintf("%.5e", Defaults.convertUnits("energy: from atomic to Hz", ioutcome.Fdensity-foutcome.Fdensity)  * 1.0e-6) * "    "
            sa = sa * @sprintf("%.5e", Defaults.convertUnits("energy: from atomic to Hz", ioutcome.Xboson-foutcome.Xboson))
            println(stream, sa )
            ## sb = TableStrings.hBlank(47)
            end
        end
        println(stream, "  ", TableStrings.hLine(nx), "\n\n")

        return( nothing )
    end

end # module


export read_openeew, openeew2seisdata, OpenEEWRecord, splitjson

# structure for holding an individual OpenEEW structure
struct OpenEEWRecord
    country_code::String
    device_id::String
    x::Array{Float32,1}
    y::Array{Float32,1}
    z::Array{Float32,1}
    device_t::Float64
    cloud_t::Float64
    sr::Float64
end

function OpenEEWRecord(d::Dict)
    return OpenEEWRecord(
        d["country_code"],
        d["device_id"],
        d["x"],
        d["y"],
        d["z"],
        d["device_t"],
        d["cloud_t"],
        d["sr"],
)
end

"""
  read_openeew(filepath)

Read OpenEEW data using SeisIO.

# Arguments
- `filepath::String`: Path to OpenEEW .jsonl file.

# Returns
- `O::Array{OpenEEW}`: 3-channel SeisData structure.
"""
function read_openeew(filepath::String)
    # read JSON lines file
    records  = splitjson(read(filepath))

    # convert to Julia struct
    O = OpenEEWRecord.(JSON.parse.(records))
    return openeew2seisdata(O)
end

"""
  openeew2seisdata(O)

Convert `Array` `O` of `OpenEEWRecords` to SeisData structure.
"""
function openeew2seisdata(O::Array{OpenEEWRecord};
    vertical_axis::Union{String,Char}="x",
)
    N = length(O)
    T = eltype(vertical_axis)
    ax = T == Char ? first.(["x","y","z"]) : ["x","y","z"]
    horizontal_axes = setdiff(ax,[vertical_axis])
    vert = Symbol(vertical_axis)
    horiz1 = Symbol(horizontal_axes[1])
    horiz2 = Symbol(horizontal_axes[2])

    # allocate arrays
    x = vcat([getproperty(O[ii],horiz1) for ii = 1:N]...)
    y = vcat([getproperty(O[ii],horiz2) for ii = 1:N]...)
    z = vcat([getproperty(O[ii],vert) for ii = 1:N]...)

    # enforce constraint that channels must be same length
    Nx = length(x)
    Ny = length(y)
    Nz = length(z)
    if Nx != Ny != Nz
        throw(ArgumentError("x, y, and z channels must be same length."))
    end

    # check sample rate same for all records
    @assert length(Set(o.sr for o in O)) == 1
    sr = O[1].sr # sample rate

    # check country code same for all records
    @assert length(Set(o.country_code for o in O)) == 1
    country_code = O[1].country_code

    # check country code same for all records
    @assert length(Set(o.device_id for o in O)) == 1
    device_id = O[1].device_id

    # allocate time variable and re-sort by time
    t = vcat([sample(o.device_t,o.sr,length(o.x)) for o in O]...)
    ind = sortperm(t)
    t .= t[ind]
    x .= x[ind]
    y .= y[ind]
    z .= z[ind]

    # find gaps and interpolate data onto an even grid-spacing
    # due to time-delays, the last few points will be lost
    newt = resample_time(t,sr)
    bad = ceil(Int,(newt[end] - t[end]) * sr)
    bad = bad < 0 ? 0 : bad
    itpx = interpolate((t,), x, Gridded(Linear()))
    itpy = interpolate((t,), y, Gridded(Linear()))
    itpz = interpolate((t,), z, Gridded(Linear()))
    newx = itpx(newt[1:end-bad])
    newy = itpy(newt[1:end-bad])
    newz = itpz(newt[1:end-bad])
    newt = newt[1:end-bad]
    return openeew2seisdata(newx,newy,newz,newt,sr,country_code,device_id)
end

function sample(t::Real,sr::Real,N::Int)
    return round.(t .- (1/sr) .* collect(N-1:-1:0),digits=3)
end

function resample_time(t::AbstractArray,sr::Real)
    newt = Array{Float64}(undef,0)
    gaps = get_gaps(t,sr)
    for ii = 1:length(gaps)-1
            gapt = t[gaps[ii]+1:gaps[ii+1]]
            Ngap = length(gapt)
            resampt = round.(collect((0:Ngap-1) ./ sr .+ gapt[1]),digits=3)
            append!(newt,resampt)
    end
    return newt
end

function get_gaps(t::AbstractArray,sr::Real)
    tdiff = abs.(diff(t))
    gaps = vcat([0; findall(tdiff .>= 2 / sr )])
    append!(gaps,length(t))
    return gaps
end

"""
  openeew2seisdata(x,y,z,t,sr,country_code,device_id)

Convert `OpenEEW` data to SeisData structure.
"""
function openeew2seisdata(
    x::AbstractArray,
    y::AbstractArray,
    z::AbstractArray,
    t::AbstractArray,
    sr::Real,
    country_code::String,
    device_id::String,
)
    @assert length(x) == length(y) == length(z) == length(t)
    S = SeisData(3)

    # use SEED naming convetion
    S.id[1] = "$country_code.$device_id..ENX"
    S.id[2] = "$country_code.$device_id..ENY"
    S.id[3] = "$country_code.$device_id..ENZ"
    S.name[1] = "$country_code.$device_id..ENX"
    S.name[2] = "$country_code.$device_id..ENY"
    S.name[3] = "$country_code.$device_id..ENZ"

    # add sampling rate
    S.fs[1] = sr
    S.fs[2] = sr
    S.fs[3] = sr

    # check for gaps with t
    tmat = SeisIO.t_collapse(convert.(Int,t .* 1e6),sr)
    S.t[1] = tmat
    S.t[2] = tmat
    S.t[3] = tmat
    S.x[1] = x
    S.x[2] = y
    S.x[3] = z
    return S
end

function splitjson(A::AbstractArray)
    splits = split(String(A),"\n")

    # remove empty last record
    if splits[end] == ""
        deleteat!(splits,length(splits))
    end

    return splits
end


ClockCycle = Int

abstract type DispatchCostModel end

Base.@kwdef struct DefaultDispatchCostModel <: DispatchCostModel
    skip::ClockCycle
    static_dispatch::ClockCycle
    dynamic_dispatch::ClockCycle
end
const basemodel = DefaultDispatchCostModel(
    skip                = 3,
    static_dispatch     = 8,
    dynamic_dispatch    = 2000,
)

function costof(
            typefreqs, # iterable over Frequency-s
            fixtypes,
            costmodel::DispatchCostModel = basemodel
        )
    total = 0
    for f in typefreqs
        skipcost, isstatic = calc_skipcost(f.type, f.freq, fixtypes, costmodel)
        dispatchcost = f.freq * (isstatic ? costmodel.static_dispatch
                                          : costmodel.dynamic_dispatch)
        total += skipcost + dispatchcost
    end
    return total
end

# Return (skipcost, isstatic)
function calc_skipcost(type, freq, fixtypes, costmodel)
    idx = findfirst(type, fixtypes)
    perskip_cost = costmodel.skip * freq
    if isnothing(idx)
        return perskip_cost * length(fixtypes), false
    end
    return perskip_cost * idx, true
end


function apply_boundary_conditions_on_φ_axis!(  rbpot::AbstractArray{T, 4}, rbi::Int,
                                                ax::DiscreteAxis{T, :periodic, :periodic}, int::Interval{:closed, :open, T} )::Nothing where {T}
    @inbounds rbpot[:,   1, :, rbi] .= view(rbpot, :, size(rbpot, 2) - 1, :, rbi) # cycling boundary
    @inbounds rbpot[:, end, :, rbi] .= view(rbpot, :,                  2, :, rbi) # cycling boundary
    nothing
end

function apply_boundary_conditions_on_φ_axis!(  rbpot::AbstractArray{T, 4}, rbi::Int,
                                                ax::DiscreteAxis{T, :reflecting, :reflecting}, int::Interval{:closed, :closed, T} )::Nothing where {T}
    @inbounds rbpot[:,   1, :, rbi] .= view(rbpot, :,                  3, :, rbi) # cycling boundary
    @inbounds rbpot[:, end, :, rbi] .= view(rbpot, :, size(rbpot, 2) - 2, :, rbi) # cycling boundary
    nothing
end

function apply_boundary_conditions_on_r_axis!(  rbpot::AbstractArray{T, 4}, rbi::Int, ax::DiscreteAxis{T, :r0, :infinite}, int::Interval{:closed, :closed, T},
                                                grid_boundary_factors::NTuple{2, T})::Nothing where {T}
    @inbounds rbpot[:, :, end, rbi] .= grid_boundary_factors[2] .* view(rbpot, :, :, size(rbpot, 3) - 2, rbi) # infinity boundaries in r
    nothing
end
function apply_boundary_conditions_on_r_axis!(  rbpot::AbstractArray{T, 4}, rbi::Int, ax::DiscreteAxis{T, :r0, :reflecting}, int::Interval{:closed, :closed, T},
                                                grid_boundary_factors::NTuple{2, T})::Nothing where {T}
    @inbounds rbpot[:, :, end, rbi] .= view(rbpot, :, :, size(rbpot, 3) - 2, rbi) # infinity boundaries in r
    nothing
end
function apply_boundary_conditions_on_r_axis!(  rbpot::AbstractArray{T, 4}, rbi::Int, ax::DiscreteAxis{T, :r0, :fixed}, int::Interval{:closed, :closed, T},
                                                grid_boundary_factors::NTuple{2, T})::Nothing where {T}
    nothing
end

function apply_boundary_conditions!(pssrb::PotentialSimulationSetupRB{T, Cylindrical}, update_even_points::Val{even_points}, only2d::Val{only_2d}) where {T, even_points, only_2d}
    rbi::Int = even_points ? rb_even::Int : rb_odd::Int
    # Radial Axis
    apply_boundary_conditions_on_r_axis!( pssrb.potential, rbi, pssrb.grid.axes[1], pssrb.grid.axes[1].interval, pssrb.grid_boundary_factors[1])
    # # Azimutal-Axis
    if !only_2d
        apply_boundary_conditions_on_φ_axis!( pssrb.potential, rbi, pssrb.grid.axes[2], pssrb.grid.axes[2].interval)
    end
    # Cylindrical Z-Axis -> same as Cartesian X-Axis
    apply_boundary_conditions_on_x_axis!( pssrb.potential, rbi, pssrb.grid.axes[3], pssrb.grid.axes[3].interval, pssrb.grid_boundary_factors[3])
    nothing
end


#=

This file contains functions that help convert a parameters YAML file into a vSmartMOM_Parameters
object. This process includes converting data types, specifying default values, validating
the input, and producing the output object.

=#

"Check that a field exists in yaml file"
function check_yaml_field(dict::Dict{Any, Any}, full_keys::Array{String}, curr_keys::Array{String}, final_type::Type, valid_options::Array{String})

    # Should have at least one key
    @assert length(curr_keys) >= 1

    # Only one key (that should exist)
    if length(curr_keys) == 1
        @assert curr_keys[1] in keys(dict) "Missing key in parameters yaml: $(join(full_keys, '/'))"
        @assert dict[curr_keys[1]] isa final_type "Improper type for $(join(full_keys, '/')); must be a $(final_type)"
        if length(valid_options) > 0
            @assert dict[curr_keys[1]] in valid_options "Field $(join(full_keys, '/')) must be one of $(valid_options)"
        end
        return true

    # Recursive case, check sub-dictionary
    else
        @assert curr_keys[1] in keys(dict) "Missing key in parameters yaml: $(join(full_keys, '/'))"
        return check_yaml_field(dict[curr_keys[1]], full_keys, curr_keys[2:end], final_type, valid_options)
    end
end

"Given a list of aerosol parameter dictionaries, validate all of them"
function validate_aerosols(aerosols::Union{Array{Dict{Any, Any}}, Vector{Any}})

    @assert !isempty(aerosols) "Aerosols list shouldn't be empty if scattering block is included"

    fields = [(["τ_ref"], Real),
              (["μ"], Real),
              (["σ"], Real),
              (["nᵣ"], Real),
              (["nᵢ"], Real),
              (["p₀"], Real),
              (["p₀"], Real)]

    for aerosol in aerosols
        for i in 1:length(fields)
            key, elty = fields[i][1:2]
            @assert check_yaml_field(aerosol, key, key, elty, Array{String}([]))
        end
    end
end

"Convert the input dictionary of aerosols into a list of RT_aerosols"
function aerosol_params_to_obj(aerosols::Union{Array{Dict{Any, Any}}, Vector{Any}}, FT)

    rt_aerosol_obj_list = RT_Aerosol[]

    for aerosol in aerosols

        size_distribution = LogNormal(FT(aerosol["μ"]), FT(aerosol["σ"]))

        new_aerosol_obj = Aerosol(size_distribution,
                                  FT(aerosol["nᵣ"]),
                                  FT(aerosol["nᵢ"]))

        new_rt_aerosol_obj = RT_Aerosol(new_aerosol_obj, FT(aerosol["τ_ref"]), FT(aerosol["p₀"]), FT(aerosol["σp"]))

        push!(rt_aerosol_obj_list, new_rt_aerosol_obj)
    end

    return rt_aerosol_obj_list
end

"Check that the vmr's in the atmospheric profile match the molecules in the parameters"
function validate_vmrs(molecules::Array, vmr::Dict)
    for molec in unique(vcat(molecules...))
        @assert molec in keys(vmr) "$(molec) listed as molecule in parameters yaml, but no vmr given in atmospheric profile"
        @assert vmr[molec] isa Real || vmr[molec] isa Vector "The vmr for $(molec) in the atmospheric profile must either be a real-valued number, or an array of nodal points from surface to 0hPa (TOA)"
    end
end

"Given a parameter dictionary from a YAML file, validate the dictionary"
function validate_yaml_parameters(params)

    # All the fields that *should* exist and their types
    fields = [

              # radiative_transfer group
              (["radiative_transfer", "spec_bands"], Array{String}),
              (["radiative_transfer", "surface"], Array{String}),
              (["radiative_transfer", "quadrature_type"], String),
              (["radiative_transfer", "polarization_type"], String),
              (["radiative_transfer", "max_m"], Integer),
              (["radiative_transfer", "Δ_angle"], Real),
              (["radiative_transfer", "l_trunc"], Integer),
              (["radiative_transfer", "depol"], Real),
              (["radiative_transfer", "float_type"], String),
              (["radiative_transfer", "architecture"], String),

              # geometry group
              (["geometry", "sza"], Real),
              (["geometry", "vza"], Array{<:Real}),
              (["geometry", "vaz"], Array{<:Real}),
              (["geometry", "obs_alt"], Real),

              # atmospheric_profile group
              (["atmospheric_profile", "T"], Array{<:Real}),
              (["atmospheric_profile", "p"], Array{<:Real}),
              (["atmospheric_profile", "profile_reduction"], Union{Integer, Nothing}),

              # absorption group
              (["absorption", "molecules"], Array),
              (["absorption", "vmr"], Dict),
              (["absorption", "broadening"], String, ["Voigt()", "Doppler()", "Lorentz()"]),
              (["absorption", "CEF"], String, ["HumlicekWeidemann32SDErrorFunction()"]),
              (["absorption", "wing_cutoff"], Integer),

              # scattering group
              (["scattering", "aerosols"], Array),
              (["scattering", "r_max"], Real),
              (["scattering", "nquad_radius"], Real),
              (["scattering", "λ_ref"], Real),
              (["scattering", "decomp_type"], String, ["NAI2()", "PCW()"]),
              ]

    # Check that every field exists and is correctly typed
    for i in 1:length(fields)
        key, elty = fields[i][1:2]
        valid_options = length(fields[i]) == 3 ? fields[i][3] : Array{String}([])

        # Check if the section exists in the YAML file and is an optional section
        if ("absorption" in keys(params) && key[1] == "absorption") ||
           ("scattering" in keys(params) && key[1] == "scattering") ||
           (key[1] != "absorption" && key[1] != "scattering")
            @assert check_yaml_field(params, key, key, elty, valid_options)
        end
    end

    # Check the aerosols separately if specified
    if "scattering" in keys(params)
        validate_aerosols(params["scattering"]["aerosols"])
    end
end

"Given a path to a YAML parameters file, load all the parameters into a vSmartMOM_Parameters struct"
function parameters_from_yaml(file_path)

    # Load the YAML file
    params_dict = YAML.load_file(file_path)

    # Validate to make sure it has all necessary fields
    validate_yaml_parameters(params_dict)

    # #########################################################
    # Evaluate type-specifying fields to get their proper types
    # #########################################################

    # radiative_transfer group
    spec_bands = convert.(Array, map(x -> collect(eval(Meta.parse(x))), params_dict["radiative_transfer"]["spec_bands"]))
    BRDF_per_band = map(x -> eval(Meta.parse(x)), params_dict["radiative_transfer"]["surface"])
    quadrature_type = eval(Meta.parse(params_dict["radiative_transfer"]["quadrature_type"]))
    polarization_type = eval(Meta.parse(params_dict["radiative_transfer"]["polarization_type"]))
    FT = eval(Meta.parse(params_dict["radiative_transfer"]["float_type"]))
    architecture = eval(Meta.parse(params_dict["radiative_transfer"]["architecture"]))

    # atmospheric_profile group
    T = convert.(FT, params_dict["atmospheric_profile"]["T"]) # Level
    p = convert.(FT, params_dict["atmospheric_profile"]["p"]) # Boundaries
    q = "q" in keys(params_dict["atmospheric_profile"]) ? # Specific humidity, if it's specified.
            convert.(Float64, params_dict["atmospheric_profile"]["q"]) : zeros(length(T)) # Otherwise 0

    # absorption group
    if "absorption" in keys(params_dict)
        molecules = Array(params_dict["absorption"]["molecules"])
        vmr = convert(Dict{String, Union{Real, Vector}}, params_dict["absorption"]["vmr"])
        validate_vmrs(params_dict["absorption"]["molecules"], vmr)
        broadening_function = eval(Meta.parse(params_dict["absorption"]["broadening"]))
        CEF = eval(Meta.parse(params_dict["absorption"]["CEF"]))
        wing_cutoff = params_dict["absorption"]["wing_cutoff"]

        absorption_params = AbsorptionParameters(molecules, vmr, broadening_function,
                                                 CEF, wing_cutoff)
    else
        absorption_params = nothing
    end

    # scattering group
    if "scattering" in keys(params_dict)

        # Get aerosols from types
        aerosols = aerosol_params_to_obj(params_dict["scattering"]["aerosols"], FT)
        r_max = FT(params_dict["scattering"]["r_max"])
        nquad_radius = params_dict["scattering"]["nquad_radius"]
        λ_ref = FT(params_dict["scattering"]["λ_ref"])
        decomp_type = eval(Meta.parse(params_dict["scattering"]["decomp_type"]))

        scattering_params = ScatteringParameters(aerosols, r_max, nquad_radius,
                                                 λ_ref, decomp_type)
    else
        scattering_params = nothing
    end

    return vSmartMOM_Parameters(
                                # radiative_transfer group
                                spec_bands,
                                BRDF_per_band,
                                quadrature_type,
                                polarization_type,
                                params_dict["radiative_transfer"]["max_m"],
                                FT(params_dict["radiative_transfer"]["Δ_angle"]),
                                params_dict["radiative_transfer"]["l_trunc"],
                                FT(params_dict["radiative_transfer"]["depol"]),
                                FT,
                                architecture,

                                # geometry group
                                FT(params_dict["geometry"]["sza"]),
                                convert.(FT, params_dict["geometry"]["vza"]),
                                convert.(FT, params_dict["geometry"]["vaz"]),
                                FT(params_dict["geometry"]["obs_alt"]),

                                # atmospheric_profile group
                                T, p, q,
                                params_dict["atmospheric_profile"]["profile_reduction"],

                                # absorption group
                                absorption_params,

                                # scattering group
                                scattering_params
                                );
end


#!/usr/bin/env julia

using Compose

rawimg = read(joinpath(@__DIR__,"smiley.png"));

compose(context(),
    rectangle(0.1,0.1,0.1,0.1),
    circle(0.3,0.15,0.05),
    polygon([(0.45,0.1),(0.4,0.2),(0.5,0.2)]),
    ellipse(0.65,0.15,0.1,0.05),
    (context(), stroke("black"),
        line([(0.1,0.3),(0.2,0.3)]),
        curve((0.25,0.35),(0.25,0.25),(0.35,0.25),(0.35,0.35))),
    bitmap("image/png",rawimg,0.4,0.25,0.1,0.1),
    text(0.6,0.3,"hello")) |> SVG("primitives.svg")


#__precompile__() # this module is safe to precompile
module NeidSolarScripts
#using PyCall

#=
function __init__()
  pyimport("astropy")
end
=#

using Dates, LinearAlgebra

# For reading data from pyrheliometer files
include("pyroheliometer.jl")
export Pyroheliometer

#=
# For computing effects of apparent solar rotation rate
include("solar_rotation.jl")
export SolarRotation
export CalculateFWHMDifference_SolarRotation_from_loc_Ecliptic, CalculateFWHMDifference_SolarRotation_from_loc_Equatorial
export CalculateFWHMDifference_SolarRotation_from_long_lat_alt, CalculateFWHMDifference_SolarRotation_from_obs
#export get_wiyn_loc, get_harpsn_loc

# For computing differential extinction
include("diff_solar_extinction.jl")
export DifferentialExtinction
=#

#include("continuum_AFS.jl")
#export Continuum_AFS
include("continuum_rassine_like.jl")
export Continuum

end


include("partition.jl")
include("io.jl")

"""
    convergence(finame::String, foname::String; metric::String="snr", skipoverlap::Bool=false)

Compute the RMS error or SNR for progressively longer stacks of cross-correlations and (for RMS) a reference cross-correlation

# Arguments
- `finame::String,`    : Input file name for unstacked cross-correlations e.g. "./inputData/BPnetwork.jld2"
- `refname::String,`    : Input file name for reference cross-corrleations e.g. "./inputData/BPnetwork.jld2"
- `foname::String,`    : Output file name for fully stacked xcorrs e.g. "referenceXcorr.jld2"
- `metric::String`    : Method of tracking convergence: "snr" for peak signal-to-noise ratio, "rms" for root mean square misfit, or "cc" for correlation coefficient
- `skipoverlap::Bool`    : Whether or not to skip overlap in cross-correlation windows. If cross-correlations were computed
                           with no overlap, set this to false to ensure no time is skipped.
- `slice::Union{Bool, Float64, Array{Float64,1}}`    : whether to slice the cross-correlations before computing convergence. If so, a single Float64 specifies the
                                                       lag at which to slice to keep just the ballistic wave and an array of Float64 specifies the start and end lag
                                                       to keep only the coda.

# Output
- `foname.jld2`    : contains arrays of RMS errors for progressively longer stacks of cross-correlations

"""
function convergence(basefiname::String, refname::String, foname::String; ntimes::Int64=-1, metric::String="cc", skipoverlap::Bool=false, slice::Union{Bool, Float64, Array{Float64,1}}=false, thresh::Float64=-1)
    # input file contains metadata (stationlist, timestamplist)
    f = jldopen(basefiname*".jld2")
    tslist = f["info/timestamplist"] # time stamps
    close(f) # input file

    # read reference xcorrs
    f_ref = jldopen(refname)

    # dictionary to keep track of convergence vectors for each station pair
    conv = Dict()
    # dictionary to keep track of stacked data
    stackData = Dict{String, Array{Float64,2}}()

    if ntimes==-1
        ntimes=length(tslist)
    end
    nWins = Dict()
    nGood = Dict()
    # iterate over timestamps
    for tstamp in tslist[1:ntimes]
        println("Processing $tstamp")
        # read xcorrs for each time step
        f_cur = jldopen(basefiname*".$tstamp.jld2")
        stnpairs = sort(keys(f_cur[tstamp]))
        stnpairs = ["BP.CCRB..BP1.BP.SMNB..BP1"]

        # iterate over station pairs
        for stnpair in stnpairs
            # load unstacked cross-correlations
            data = try f_cur["$tstamp/$stnpair"] catch; continue end

            # load reference cross-correlation for the current station pair
            reference = f_ref[stnpair]

            # optionally slice reference to keep only coda or only ballistic
            ref = corr_slice(reference, 1:size(reference.corr, 2), slice)

            if haskey(nWins, stnpair)
                push!(nWins[stnpair], size(data.corr, 2))
            else
                nWins[stnpair] = [size(data.corr, 2)]
            end

            datacopy, cList = selective_stacking(data, reference, threshold=thresh)
            good_inds = findall(x->(x>thresh), cList)
            data.corr = data.corr[:, good_inds]

            if haskey(nGood, stnpair)
                push!(nGood[stnpair], length(good_inds))
            else
                nGood[stnpair] = [length(good_inds)]
            end

            # iterate over windowed cross-correlations
            for i=1:size(data.corr)[2]
                if skipoverlap i=2*i-1 end # assumes cc_step is 1/2 cc_len

                # slice current cross-correlation if necessary
                data_cur = corr_slice(data, i:i, slice)

                # build input to convergence functions
                # here we either create a progressively longer stack of cross-correlation
                # functions, or concatenate cross-correlation functions for snr
                if haskey(stackData, stnpair) && (metric=="rms" || metric=="cc")
                    # keep running sum of cross-correlations, stack if key exists, assign if not
                    stackData[stnpair] .+= data_cur
                elseif haskey(stackData, stnpair) && metric=="snr"
                    # create matrix of xcorrs
                    stackData[stnpair] = hcat(stackData[stnpair], data_cur)
                else
                    # reshape into 2D array (necessary for snr input)
                    if typeof(slice) == Array{Float64,1}
                        stackData[stnpair] = data_cur
                    else
                        stackData[stnpair] = reshape(data_cur, length(data_cur), 1)
                    end
                end

                # compute convergence data point and append to vector
                if metric=="rms"
                    # compute RMS error of the running sum with the reference
                    if haskey(conv, stnpair)
                        push!(conv[stnpair], rms(normalize(ref[:,1]), normalize(stackData[stnpair][:,1])))
                    else
                        conv[stnpair] = [rms(normalize(ref[:,1]), normalize(stackData[stnpair][:,1]))]
                    end
                elseif metric=="cc"
                    # compute Pearson correlation-coefficient between current and reference cross-correlation
                    if haskey(conv, stnpair)
                        if typeof(slice)==Array{Float64,1}
                            # compute correlation-coefficient for both positive and negative coda
                            # store in 2d array
                            neg_coda_cor = cor(ref[:, 1], stackData[stnpair][:, 1])
                            pos_coda_cor = cor(ref[:, 2], stackData[stnpair][:, 2])
                            push!(conv[stnpair], [neg_coda_cor, pos_coda_cor])
                        else
                            push!(conv[stnpair], cor(ref[:,1], stackData[stnpair][:,1]))
                        end
                    else
                        if typeof(slice)==Array{Float64,1}
                            # compute correlation-coefficient for both positive and negative coda
                            # store in 2d array
                            neg_coda_cor = cor(ref[:, 1], stackData[stnpair][:, 1])
                            pos_coda_cor = cor(ref[:, 2], stackData[stnpair][:, 2])
                            conv[stnpair] = [[neg_coda_cor, pos_coda_cor]]
                        else
                            conv[stnpair] = [cor(ref[:,1], stackData[stnpair][:,1])]
                        end
                    end
                elseif metric=="snr"
                    # compute peak signal-to-noise ratio of increasingly long stacks
                    if haskey(conv, stnpair)
                        # compute psnr for both positive and negative coda
                        if typeof(slice)==Array{Float64,1}
                            neg_coda = findall(x->(x%2!=0), collect(1:size(stackData[stnpair], 2)))
                            pos_coda = findall(x->(x%2==0), collect(1:size(stackData[stnpair], 2)))
                            neg_coda_snr = maximum(snr(stackData[stnpair][:, neg_coda], reference.fs))
                            pos_coda_snr = maximum(snr(stackData[stnpair][:, pos_coda], reference.fs))

                            push!(conv[stnpair], [neg_coda_snr, pos_coda_snr])
                        else
                            push!(conv[stnpair], maximum(snr(stackData[stnpair], reference.fs)))
                        end
                    else
                        if typeof(slice)==Array{Float64,1}
                            neg_coda = findall(x->(x%2!=0), collect(1:size(stackData[stnpair], 2)))
                            pos_coda = findall(x->(x%2==0), collect(1:size(stackData[stnpair], 2)))
                            neg_coda_snr = maximum(snr(stackData[stnpair][:, neg_coda], reference.fs))
                            pos_coda_snr = maximum(snr(stackData[stnpair][:, pos_coda], reference.fs))

                            conv[stnpair] = [[neg_coda_snr, pos_coda_snr]]
                        else
                            conv[stnpair] = [maximum(snr(stackData[stnpair], reference.fs))]
                        end
                    end
                end
            end
        end
        close(f_cur) # xcorrs for each time step
    end
    close(f_ref) # reference xcorrs

    # write convergence vectors to disk
    f_conv = jldopen(foname, "a+")
    for key in keys(conv)
        f_conv["$metric/$key"] = conv[key]
        f_conv["$metric/nWin/$key"] = nWins[key]
        f_conv["$metric/nGood/$key"] = nGood[key]
    end

    close(f_conv) # convergence file
end

function corr_slice(data::CorrData, range::UnitRange{Int64}, slice::Union{Bool, Float64, Array{Float64,1}}=false)
    if typeof(slice)==Float64
        # time vector
        tvec = -data.maxlag:1/data.fs:data.maxlag
        # find all times within [-slice,slice] time
        t_inds = findall(x->(x>=-slice && x<=slice), tvec)
        # slice reference (maintain 2d array)
        d = data.corr[t_inds, range]
    elseif typeof(slice)==Array{Float64,1}
        # convert startlag/endlag[s] to startlag/windowlength[samples]
        win_len = Int(diff(slice)[1] * data.fs)
        startlag = Int(slice[1] * data.fs)
        # partition reference to keep only the coda
        d = partition(data.corr[:, range], startlag, win_len)
    else
        println("Proceeding without slicing.")
        d = data.corr
    end
    return d
end


export gmm_probability

import ..GMM

using mlpack._Internal.io

import mlpack_jll
const gmm_probabilityLibrary = mlpack_jll.libmlpack_julia_gmm_probability

# Call the C binding of the mlpack gmm_probability binding.
function gmm_probability_mlpackMain()
  success = ccall((:gmm_probability, gmm_probabilityLibrary), Bool, ())
  if !success
    # Throw an exception---false means there was a C++ exception.
    throw(ErrorException("mlpack binding error; see output"))
  end
end

" Internal module to hold utility functions. "
module gmm_probability_internal
  import ..gmm_probabilityLibrary

import ...GMM

# Get the value of a model pointer parameter of type GMM.
function IOGetParamGMM(paramName::String)::GMM
  GMM(ccall((:IO_GetParamGMMPtr, gmm_probabilityLibrary), Ptr{Nothing}, (Cstring,), paramName))
end

# Set the value of a model pointer parameter of type GMM.
function IOSetParamGMM(paramName::String, model::GMM)
  ccall((:IO_SetParamGMMPtr, gmm_probabilityLibrary), Nothing, (Cstring, Ptr{Nothing}), paramName, model.ptr)
end

# Serialize a model to the given stream.
function serializeGMM(stream::IO, model::GMM)
  buf_len = UInt[0]
  buf_ptr = ccall((:SerializeGMMPtr, gmm_probabilityLibrary), Ptr{UInt8}, (Ptr{Nothing}, Ptr{UInt}), model.ptr, Base.pointer(buf_len))
  buf = Base.unsafe_wrap(Vector{UInt8}, buf_ptr, buf_len[1]; own=true)
  write(stream, buf)
end
# Deserialize a model from the given stream.
function deserializeGMM(stream::IO)::GMM
  buffer = read(stream)
  GMM(ccall((:DeserializeGMMPtr, gmm_probabilityLibrary), Ptr{Nothing}, (Ptr{UInt8}, UInt), Base.pointer(buffer), length(buffer)))
end
end # module

"""
    gmm_probability(input, input_model; [verbose])

This program calculates the probability that given points came from a given GMM
(that is, P(X | gmm)).  The GMM is specified with the `input_model` parameter,
and the points are specified with the `input` parameter.  The output
probabilities may be saved via the `output` output parameter.

So, for example, to calculate the probabilities of each point in `points` coming
from the pre-trained GMM `gmm`, while storing those probabilities in `probs`,
the following command could be used:

```julia
julia> using CSV
julia> points = CSV.read("points.csv")
julia> probs = gmm_probability(points, gmm)
```

# Arguments

 - `input::Array{Float64, 2}`: Input matrix to calculate probabilities
      of.
 - `input_model::GMM`: Input GMM to use as model.
 - `verbose::Bool`: Display informational messages and the full list of
      parameters and timers at the end of execution.  Default value `false`.

# Return values

 - `output::Array{Float64, 2}`: Matrix to store calculated probabilities
      in.

"""
function gmm_probability(input,
                         input_model::GMM;
                         verbose::Union{Bool, Missing} = missing,
                         points_are_rows::Bool = true)
  # Force the symbols to load.
  ccall((:loadSymbols, gmm_probabilityLibrary), Nothing, ());

  IORestoreSettings("GMM Probability Calculator")

  # Process each input argument before calling mlpackMain().
  IOSetParamMat("input", input, points_are_rows)
  gmm_probability_internal.IOSetParamGMM("input_model", convert(GMM, input_model))
  if verbose !== nothing && verbose === true
    IOEnableVerbose()
  else
    IODisableVerbose()
  end

  IOSetPassed("output")
  # Call the program.
  gmm_probability_mlpackMain()

  return IOGetParamMat("output", points_are_rows)
end


using RCall
using JellyMe4

using DataFrames
using MixedModels
using Test

const datasets = MixedModels.datasets
dataset(x) = DataFrame(MixedModels.dataset(x))

# this should only occur on CIs
# the Linux CI installs lme4 via apt beforehand
# and there are binary builds for Mac and Windows
reval("""
if(!require(lme4)){
    # use tmp for tests if lme4 isn't available
    .libPaths("/tmp")
    lib <- .libPaths()[1L]
    warning("installing lme4")
    warning(lib)
    install.packages("lme4",repos="https://cloud.r-project.org", libs=lib, type="binary")
    library(lme4)
}
if(!require(afex)){
    .libPaths("/tmp")
    lib <- .libPaths()[1L]
    install.packages("afex",repos="https://cloud.r-project.org", libs=lib, , type="binary")
}
""")

include("merMod.jl")
include("lmerMod.jl")
include("glmerMod.jl")


using aoc2019.computer: load_program, run

function nv(program)
    for i = 1:100
        for j = 1:100
            if nv(program, i, j)
                return 100 * i + j
            end
        end
    end
end

function nv(program, i, j)
    let new_program = copy(program)
        new_program[2] = i
        new_program[3] = j
        run(new_program)
        if first(new_program) == 19690720
            return true
        else
            return false
        end
    end
end

input = joinpath(@__DIR__, "input")
p = load_program(input)

program = copy(p)
run(program)
p1 = first(program)
@assert p1 == 3101844

p2 = nv(copy(p))
@assert p2 == 8478

print("-----------------------------------------------------------------------\n")
print("2012 program alarm -- part one\n    final value : $p1\n")
print("2012 program alarm -- part two\n    nounverb    : $p2\n")
print("-----------------------------------------------------------------------\n")


f(x) = x * (pi/180)

println("theta=60: ", f(60))
println("theta=80: ", f(80))


#== # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
#
#   Functions to verify if the satellite is within the station visibility
#   circle.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ==#

export ground_station_accesses, ground_station_gaps,
       list_ground_station_accesses, list_ground_station_gaps,
       ground_station_visible

"""
    ground_station_accesses(orbp, vrs_e,     Δt, ECI, ECEF, vargs...; kwargs...)
    ground_station_accesses(orbp, [(WGS84)], Δt, ECI, ECEF, vargs...; kwargs...)

Compute the accesses of a satellite with orbit propagator `orbp` (see
`init_orbit_propagator`) to the ground stations defined in the vector `vrs_e`.
The analysis interval begins in the propagator epoch and lasts `Δt` [s].

The ground stations can be specified by an array of 3×1 vectors describing the
ground stations position in an ECEF frame `vrs_e` or by an array of tuples
containing the WGS84 position of each ground station `[(WGS84)]`:

    (latitude [rad], longitude [rad], altitude [m])

# Args

* `ECI`: Earth-Centered Inertial frame in which the state vector of the
         propagator is represented.
* `ECEF`: Earth-Centered, Earth-fixed frame to be used for the analysis. It
          must be the same frame used to compute the ground station position
          vector.
* `vargs...`: list of additional arguments to be passed to the function
              `r_eci_to_ecef` when converting the ECI frame to the ECEF.

# Keywords

* `θ`: Minimum elevation angle for communication between the satellite and the
       ground stations [rad]. (**Default** = 10ᵒ)
* `reduction`: A function that receives a boolean vector with the visibility
               between the satellite and each ground station. It must return a
               boolean value indicating if the access must be computed or not.
               This is useful to merge access time between two or more stations.
               (**Default** = `v->|(v...)` *i.e.* compute the access if at least
               one ground station is visible)

"""
ground_station_accesses(orbp, gs_wgs84::Tuple, vargs...; kwargs...) =
    ground_station_accesses(orbp, [gs_wgs84], vargs...; kwargs...)

function ground_station_accesses(orbp, vgs_wgs84::AbstractVector{T}, vargs...;
                                 kwargs...) where T<:Tuple

    vrs_e = [geodetic_to_ecef(gs_wgs84...) for gs_wgs84 in vgs_wgs84]
    return ground_station_accesses(orbp, vrs_e, vargs...; kwargs...)
end

ground_station_accesses(orbp, rs_e::AbstractVector{T}, vargs...; kwargs...) where
    T<:Number = ground_station_accesses(orbp, [rs_e], vargs...; kwargs...)

function ground_station_accesses(orbp, vrs_e::AbstractVector{T}, Δt::Number,
                                 ECI::Union{T_ECIs, T_ECIs_IAU_2006},
                                 ECEF::Union{T_ECEFs, T_ECEFs_IAU_2006}, vargs...;
                                 θ::Number = 10*pi/180,
                                 reduction::Function = v->|(v...),
                                 step::Number = 60) where T<:AbstractVector

    # Time vector of the analysis.
    t = 0:step:Δt

    # Get the epoch of the propagator.
    JD₀ = get_epoch(orbp)

    # Matrix that will contain the accesses.
    accesses = Matrix{DateTime}(undef,0,2)

    # State to help the computation.
    state = :initial

    # Lambda function to check if the ground station is visible.
    f(t)::Bool = begin
        r_i, v_i   = propagate!(orbp, t)
        r_e        = r_eci_to_ecef(DCM, ECI, ECEF, JD₀ + t/86400, vargs...)*r_i
        visibility = [ground_station_visible(r_e, rs_e, θ) for rs_e in vrs_e]

        return reduction(visibility)
    end

    access_beg = DateTime(now())
    access_end = DateTime(now())

    for k in t
        # Check if the ground station is visible.
        gs_visible = f(k)

        # Handle the initial case.
        if state == :initial
            if gs_visible
                access_beg = jd_to_date(DateTime, JD₀)
                state = :visible
            else
                state = :not_visible
            end
        # Handle transitions.
        elseif (state == :not_visible) && gs_visible
            # Refine to find the edge.
            k₀ = k - step
            k₁ = k
            kc = find_crossing(f, k₀, k₁, false, true)

            state = :visible
            access_beg = jd_to_date(DateTime, JD₀ + kc/86400)

        elseif (state == :visible) && !gs_visible
            # Refine to find the edge.
            k₀ = k - step
            k₁ = k
            kc = find_crossing(f, k₀, k₁, true, false)

            state = :not_visible
            access_end = jd_to_date(DateTime, JD₀ + kc/86400)

            accesses = vcat(accesses, [access_beg access_end])
        end
    end

    # If the analysis finished during an access, then just add the end of the
    # interval as the end of the access.
    if state == :visible
        access_end = jd_to_date(DateTime, JD₀ + Δt/86400)
        accesses   = vcat(accesses, [access_beg access_end])
    end

    return accesses
end

"""
    ground_station_gaps(args...; kwargs...)

Compute the gaps between the accesses of ground stations. The arguments and
keywords are the same as the ones used in the function
`ground_station_accesses`.

Notice that the gap analysis starts in the orbit propagator epoch and ends in
the instant defined by the argument `Δt`.

"""
function ground_station_gaps(orbp, args...; kwargs...)
    # Get the epoch of the propagator.
    JD₀ = get_epoch(orbp)
    DT₀ = jd_to_date(DateTime, JD₀)

    # Compute the list of ground station accesses.
    accesses = ground_station_accesses(orbp, args...; kwargs...)

    # Get the analysis period, which must be the second argument in `args...`.
    Δt = args[2]

    # Compute the last propagation instant.
    JD₁ = JD₀ + Δt/86400
    DT₁ = jd_to_date(DateTime, JD₁)

    # Compute the gaps between accesses.
    gaps = Matrix{DateTime}(undef,0,2)

    # Check if the simulation did not start under the visibility of a ground
    # station.
    if accesses[1,1] != DT₀
        gaps = vcat(gaps, [jd_to_date(DateTime, JD₀) accesses[1,1]])
    end

    # Check if the simulation did not end under the visibility of a ground
    # station.
    if accesses[end,2] != DT₁
        aux  = jd_to_date(DateTime, JD₁)
        gaps = vcat(gaps, [ accesses[:,2] vcat(accesses[2:end,1], aux) ])
    else
        gaps = vcat(gaps, [ accesses[1:end-1,2] accesses[2:end,1] ])
    end

    return gaps
end

"""
    list_ground_station_accesses(io, vargs...; kwargs...)

Print the ground station accesses to the io `io`. The arguments `vargs...` and
keywords `kwargs...` are those of the function `ground_station_accesses`.

Additionally, the following keywords can be used to modify the behavior of this
function:

* `format`: If `:pretty`, then a formatted table will be printed. If `:csv`,
            then the access data will be printed using the CSV format.
            (**Default** = `:pretty`)
* `time_scale`: Select the time scale of the access duration (`:s` for seconds,
                `:m` for minutes, and `:h` for hours). (**Default** = `:m`)

"""
list_ground_station_accesses(vargs...; kwargs...) =
    list_ground_station_accesses(stdout, vargs...; kwargs...)

function list_ground_station_accesses(io::IO, vargs...; format = :pretty,
                                      time_scale::Symbol = :m, kwargs...)

    # Compute the accesses.
    accesses = ground_station_accesses(vargs...; kwargs...)

    # If no access was found, then inform the user and return.
    if isempty(accesses)
        println("No access found!")
        return nothing
    end

    # Check which scale to use when printing the access duration.
    if time_scale == :s
        ts    = 1
        label = "s"
    elseif time_scale == :m
        ts    = 60
        label = "min"
    else
        ts    = 3600
        label = "h"
    end

    # Compute the accesses duration.
    access_time = map((b,e)->(e-b).value/1000/ts,
                       @view(accesses[:,1]), @view(accesses[:,2]))
    num         = length(access_time)

    # Assemble the information to be printed.
    header = ["Access #", "Beginning (UTC)", "End (UTC)", "Duration [$label]"]
    mp     = hcat(string.(collect(1:1:num)), accesses, access_time)

    # Print the information depending on the format.

    if format == :csv
        writedlm(io, vcat(header,mp), ",")
    else
        # Statistics.
        min_access, id_min = findmin(access_time)
        max_access, id_max = findmax(access_time)
        mean_access        = mean(access_time)
        total_access       = sum(access_time)

        # Mark the minimum and maximum accesses.
        mp[id_min, 1] = "MIN " * mp[id_min,1]
        mp[id_max, 1] = "MAX " * mp[id_max,1]

        # Add the statistics to the end of the table.
        mp = vcat(mp, ["" "" "         Minimum access"  min_access;
                       "" "" "            Mean access"  mean_access;
                       "" "" "         Maximum access"  max_access;
                       "" "" "           Total access"  total_access;])

        # Highlighters for the minimum and maximum accesses.
        hl_min = Highlighter((data,i,j)->i == id_min, crayon"bold red")
        hl_max = Highlighter((data,i,j)->i == id_max, crayon"bold blue")

        # Highlighter for statistics labels.
        hl = Highlighter((data,i,j)->(j == 3) && (i-num ∈ (1,2,3,4)),
                         crayon"bold")

        # Print.
        pretty_table(io, mp;
                     alignment    = [:r, :l, :l, :r],
                     body_hlines  = [num,num+3],
                     crop         = :horizontal,
                     header       = header,
                     formatters   = ft_printf("%.3f",[4]),
                     highlighters = (hl,hl_min,hl_max))
    end

    return nothing
end

"""
    list_ground_station_gaps(io, vargs...; kwargs...)

Print the ground station gaps to the io `io`. The arguments `vargs...` and
keywords `kwargs...` are those of the function `ground_station_gaps`.

Additionally, the following keywords can be used to modify the behavior of this
function:

* `format`: If `:pretty`, then a formatted table will be printed. If `:csv`,
            then the access data will be printed using the CSV format.
            (**Default** = `:pretty`)
* `time_scale`: Select the time scale of the access duration (`:s` for seconds,
                `:m` for minutes, and `:h` for hours). (**Default** = `:m`)

"""
list_ground_station_gaps(vargs...; kwargs...) =
    list_ground_station_gaps(stdout, vargs...; kwargs...)

function list_ground_station_gaps(io::IO, vargs...; format = :pretty,
                                  time_scale::Symbol = :m, kwargs...)

    # Compute the gaps.
    gaps = ground_station_gaps(vargs...; kwargs...)

    # If no gap was found, then inform the user and return.
    if isempty(gaps)
        println("No gap found!")
        return nothing
    end

    # Check which scale to use when printing the gap duration.
    if time_scale == :s
        ts    = 1
        label = "s"
    elseif time_scale == :m
        ts    = 60
        label = "min"
    else
        ts    = 3600
        label = "h"
    end

    # Compute the gaps duration.
    gap_time = map((b,e)->(e-b).value/1000/ts, @view(gaps[:,1]), @view(gaps[:,2]))
    num         = length(gap_time)

    # Assemble the information to be printed.
    header = ["   Gap #", "Beginning (UTC)", "End (UTC)", "Duration [$label]"]
    mp     = hcat(string.(collect(1:1:num)), gaps, gap_time)

    # Print the information depending on the format.

    if format == :csv
        writedlm(io, vcat(header,mp), ",")
    else
        # Statistics.
        min_gap, id_min = findmin(gap_time)
        max_gap, id_max = findmax(gap_time)
        mean_gap        = mean(gap_time)
        total_gap       = sum(gap_time)

        # Mark the minimum and maximum gaps.
        mp[id_min, 1] = "MIN " * mp[id_min,1]
        mp[id_max, 1] = "MAX " * mp[id_max,1]

        # Add the statistics to the end of the table.
        mp = vcat(mp, ["" "" "            Minimum gap"  min_gap;
                       "" "" "               Mean gap"  mean_gap;
                       "" "" "            Maximum gap"  max_gap;
                       "" "" "              Total gap"  total_gap;])

        # Highlighters for the minimum and maximum gaps.
        hl_min = Highlighter((data,i,j)->i == id_min, crayon"bold red")
        hl_max = Highlighter((data,i,j)->i == id_max, crayon"bold blue")

        # Highlighter for statistics labels.
        hl = Highlighter((data,i,j)->(j == 3) && (i-num ∈ (1,2,3,4)),
                         crayon"bold")

        # Print.
        pretty_table(io, mp;
                     alignment    = [:r, :l, :l, :r],
                     body_hlines  = [num,num+3],
                     crop         = :horizontal,
                     header       = header,
                     formatters   = ft_printf("%.3f",[4]),
                     highlighters = (hl,hl_min,hl_max))
    end

    return nothing
end

"""
    ground_station_visible(r_e::AbstractVector, rs_e::AbstractVector, θ::Number)

Check if the satellite with position vector `r_e` (ECEF) is inside the
visibility circle of a ground station with position vector `rs_e` (ECEF) and a
minimum elevation angle of `θ` [rad].

Notice that `r_e` and `rs_e` must be represented in the same ECEF frame, and
must have the same unit.

Returns `true` if the satellite is inside the visibility circle, or `false`
otherwise.

"""
function ground_station_visible(r_e::AbstractVector, rs_e::AbstractVector,
                                θ::Number)
    # Check if the satellite is within the visibility circle of the station.
    dr_e = r_e - rs_e
    cos_beta = dot( dr_e/norm(dr_e), rs_e/norm(rs_e) )

    return cos_beta > cos(π/2-θ)
end

"""
    ground_station_visible(r_e::AbstractVector, lat_s::Number, lon_s::Number, h_s::Number, θ::Number)

Check if the satellite with position vector `r_e` (ECEF) is inside the
visibility circle of a ground station with latitude `lat_s` [rad], longitude
`lon_s` [rad], altitude `h_s` (WGS-84), and a minimum elevation angle of
`θ` [rad].

Notice that the units of `r_e` and `h_s` must be the same.

Returns `true` if the satellite is inside the visibility circle, or `false`
otherwise.

"""
function ground_station_visible(r_e::AbstractVector, lat_s::Number,
                                lon_s::Number, h_s::Number, θ::Number)
    # Convert the ground station LLA to the ECEF frame.
    rs_e = geodetic_to_ecef(lat_s, lon_s, h_s)

    return ground_station_visible(r_e, rs_e, θ)
end


### FieldTuple types

# FieldTuple is a thin wrapper around a Tuple or NamedTuple holding some Fields
# and behaving like a Field itself
struct FieldTuple{FS<:Union{Tuple,NamedTuple},B,T} <: Field{B,T}
    fs :: FS
    function FieldTuple(fs::FS) where {FS<:Union{Tuple,NamedTuple}}
        B = BasisProd{Tuple{map(basis,values(fs))...}}
        T = promote_type(map(eltype,values(fs))...)
        new{FS,B,T}(fs)
    end
end
# FieldTuple(args...) or FieldTuple(;kwargs...) calls the inner constructor
# which takes a single Tuple/NamedTuple:
(::Type{FT})(;kwargs...) where {FT<:FieldTuple} = FT((;kwargs...))
(::Type{FT})(f1::Field,f2::Field,fs::Field...) where {FT<:FieldTuple} = FT((f1,f2,fs...))

### printing
getindex(f::FieldTuple,::Colon) = vcat(getindex.(values(f.fs),:)...)[:]
getindex(D::DiagOp{<:FieldTuple}, i::Int, j::Int) = (i==j) ? D.diag[:][i] : diagzero(D, i, j)
typealias_def(::Type{<:FieldTuple{NamedTuple{Names,FS},T}}) where {Names,FS<:Tuple,T} =
    "Field-($(join(map(string,Names),",")))-$FS"
typealias_def(::Type{<:FieldTuple{FS,T}}) where {FS<:Tuple,T} =
    "Field-$(tuple_type_len(FS))-$FS"

### array interface
size(f::FieldTuple) = (mapreduce(length, +, f.fs, init=0),)
copy(f::FieldTuple) = FieldTuple(map(copy,f.fs))
iterate(ft::FieldTuple, args...) = iterate(ft.fs, args...)
getindex(f::FieldTuple, i::Union{Int,UnitRange}) = getindex(f.fs, i)
fill!(ft::FieldTuple, x) = (map(f->fill!(f,x), ft.fs); ft)
adapt_structure(to, f::FieldTuple) = FieldTuple(map(f->adapt(to,f),f.fs))
similar(ft::FieldTuple) = FieldTuple(map(similar,ft.fs))
similar(ft::FieldTuple, ::Type{T}) where {T<:Number} = FieldTuple(map(f->similar(f,T),ft.fs))
similar(ft::FieldTuple, Nbatch::Int) = FieldTuple(map(f->similar(f,Nbatch),ft.fs))
sum(f::FieldTuple; dims=:) = dims == (:) ? sum(sum, f.fs) : error("sum(::FieldTuple, dims=$dims not supported")

### broadcasting
# see base_fields.jl for more explanation of all these pieces, its the
# exact same principle
struct FieldTupleStyle{S,Names} <: AbstractArrayStyle{1} end
function BroadcastStyle(::Type{<:FieldTuple{TS}}) where {TS<:Tuple}
    FieldTupleStyle{Tuple{map_tupleargs(typeof∘BroadcastStyle,TS)...}, Nothing}()
end
function BroadcastStyle(::Type{<:FieldTuple{NamedTuple{Names,TS}}}) where {Names,TS<:Tuple}
    FieldTupleStyle{Tuple{map_tupleargs(typeof∘BroadcastStyle,TS)...}, Names}()
end
function BroadcastStyle(::FieldTupleStyle{S₁,Names}, ::FieldTupleStyle{S₂,Names}) where {S₁,S₂,Names}
    FieldTupleStyle{Tuple{map_tupleargs((s₁,s₂)->typeof(result_style(s₁(),s₂())), S₁, S₂)...}, Names}()
end
BroadcastStyle(S::FieldTupleStyle, ::DefaultArrayStyle{0}) = S

@generated function materialize(bc::Broadcasted{FieldTupleStyle{S,Names}}) where {S,Names}
    wrapper = Names == Nothing ? :tuple : :(NamedTuple{$Names})
    exprs = map_tupleargs(S, tuple(1:tuple_type_len(S)...)) do Sᵢ, i
        :(materialize(convert(Broadcasted{$Sᵢ}, preprocess(($(S.parameters[i])(),FieldTupleComponent{$i}()), bc))))
    end
    :(FieldTuple($wrapper($(exprs...))))
end
@generated function materialize!(dst::FieldTuple, bc::Broadcasted{FieldTupleStyle{S,Names}}) where {S,Names}
    exprs = map_tupleargs(S, tuple(1:tuple_type_len(S)...)) do Sᵢ, i
        :(materialize!(dst.fs[$i], convert(Broadcasted{$Sᵢ}, preprocess(($(S.parameters[i])(),FieldTupleComponent{$i}()), bc))))
    end
    :(begin $(exprs...) end; dst)
end

struct FieldTupleComponent{i} end

preprocess(::Tuple{<:Any,FieldTupleComponent{i}}, ft::FieldTuple) where {i} = ft.fs[i]

preprocess(dest::Tuple{FieldTupleStyle{S},<:Any}, bc::Broadcasted) where {S} =
    broadcasted(S(), bc.f, preprocess_args(dest, bc.args)...)

### promotion
function promote(ft1::FieldTuple, ft2::FieldTuple)
    fts = map(promote, ft1.fs, ft2.fs)
    FieldTuple(map(first,fts)), FieldTuple(map(last,fts))
end

### conversion
Basis(ft::FieldTuple) = ft
(::Type{B})(ft::FieldTuple) where {B<:Basis}     = FieldTuple(map(B, ft.fs))
(::Type{B})(ft::FieldTuple) where {B<:Basislike} = FieldTuple(map(B, ft.fs))
(::Type{B})(ft::FieldTuple{<:Tuple}) where {Bs,B<:BasisProd{Bs}} =
    FieldTuple(map_tupleargs((B,f)->B(f), Bs, ft.fs))
(::Type{B})(ft::FieldTuple{<:NamedTuple{Names}}) where {Names,Bs,B<:BasisProd{Bs}} =
    FieldTuple(NamedTuple{Names}(map_tupleargs((B,f)->B(f), Bs, values(ft.fs))))

### properties
getproperty(f::FieldTuple, s::Symbol) = getproperty(f, Val(s))
getproperty(f::FieldTuple, ::Val{:fs}) = getfield(f, :fs)
getproperty(f::FieldTuple, ::Val{s}) where {s} = getproperty(getfield(f,:fs), s)
propertynames(f::FieldTuple) = (:fs, propertynames(f.fs)...)

### simulation
white_noise(ξ::FieldTuple, rng::AbstractRNG) = FieldTuple(map(f -> white_noise(f, rng), ξ.fs))

### Diagonal-ops
# need a method specific for FieldTuple since we don't carry around
# the basis in a way that works with the default implementation
(*)(D::DiagOp{<:FieldTuple}, f::FieldTuple) = FieldTuple(map((d,f)->Diagonal(d)*f, D.diag.fs, f.fs))
(\)(D::DiagOp{<:FieldTuple}, f::FieldTuple) = FieldTuple(map((d,f)->Diagonal(d)\f, D.diag.fs, f.fs))

# # promote before recursing for these
dot(a::FieldTuple, b::FieldTuple) = reduce(+, map(dot, getfield.(promote(a,b),:fs)...), init=0)
hash(ft::FieldTuple, h::UInt64) = foldr(hash, (typeof(ft), ft.fs), init=h)

# logdet & trace
logdet(L::Diagonal{<:Union{Real,Complex}, <:FieldTuple}) = reduce(+, map(logdet∘Diagonal, L.diag.fs), init=0)
tr(L::Diagonal{<:Union{Real,Complex}, <:FieldTuple}) = reduce(+, map(tr∘Diagonal, L.diag.fs), init=0)

# misc
batch_length(ft::FieldTuple) = only(unique(map(batch_length, ft.fs)))
batch_index(ft::FieldTuple, I) = FieldTuple(map(f -> batch_index(f, I), ft.fs))
function global_rng_for(::Type{<:FieldTuple{<:Union{FS,NamedTuple{Names,FS}}}}) where {Names,FS<:Tuple}
    only(unique(map_tupleargs(global_rng_for, FS)))
end


using MRMP
import Random: seed!

solvers =
    ["MRMP.PP", "MRMP.CBS", "MRMP.RRT", "MRMP.RRT_connect", "MRMP.PRM_direct", "MRMP.SSSP"]

TIME_LIMIT = 1.0

ins_commons_params = Dict(:N => 3, :rad => 0.025, :num_obs => 3, :rad_obs => 0.05)

function test_all_solvers(ins_gen_fn)
    seed!(1)
    config_init, config_goal, obstacles, ins_params... = ins_gen_fn(; ins_commons_params...)
    connect = gen_connect(config_init[1], obstacles, ins_params...)
    collide = gen_collide(config_init[1], ins_params...)
    check_goal = MRMP.gen_check_goal(config_goal)

    for solver in solvers
        @info solver
        seed!(1)
        @time eval(Meta.parse(solver))(
            config_init,
            config_goal,
            connect,
            collide,
            check_goal;
            TIME_LIMIT = TIME_LIMIT,
            VERBOSE = 1,
        )
    end
end

@testset "point2d" begin
    test_all_solvers(MRMP.gen_random_instance_StatePoint2D)
end
@testset "point3d" begin
    test_all_solvers(MRMP.gen_random_instance_StatePoint3D)
end
@testset "line2d" begin
    test_all_solvers(MRMP.gen_random_instance_StateLine2D)
end
@testset "arm22" begin
    test_all_solvers(MRMP.gen_random_instance_StateArm22)
end
@testset "arm33" begin
    test_all_solvers(MRMP.gen_random_instance_StateArm33)
end
@testset "snake2d" begin
    test_all_solvers(MRMP.gen_random_instance_StateSnake2D)
end
@testset "dubins2d" begin
    test_all_solvers(MRMP.gen_random_instance_StateDubins)
end
@testset "capsule3d" begin
    test_all_solvers(MRMP.gen_random_instance_StateDubins)
end


libpath = "/home/hessammehr/harminv-1.4.1/.libs"

const C = Complex128
const F = Float64

struct HarminvNative
    c::Ptr{C}
    n::Int
    K::Int
    J::Int
    nfreqs::Int
    fmin::F
    fmax::F
    z::Ptr{C}
    U0::Ptr{C}
    U1::Ptr{C}
    G0::Ptr{C}
    G0_M::Ptr{C}
    D0::Ptr{C}
    B::Ptr{C}
    u::Ptr{C}
    amps::Ptr{C}
    errs::Ptr{F}
end

if !(libpath in Base.LD_LOAD_PATH)
    push!(Base.LD_LOAD_PATH, libpath)
end

ccall((:harminv_data_create, "libharminv"), )


function benchmark_carrier_replica()
    results_min = DataFrame(
        Samples = SAMPLES,
        sCPU_time = zeros(Float64,length(SAMPLES)),
        GPU_time = zeros(Float64,length(SAMPLES)),
        sGPU_time = zeros(Float64,length(SAMPLES)),
    )
    results_med = DataFrame(
        Samples = SAMPLES,
        sCPU_time = zeros(Float64,length(SAMPLES)),
        GPU_time = zeros(Float64,length(SAMPLES)),
        sGPU_time = zeros(Float64,length(SAMPLES)),
    )
    results_mean = DataFrame(
        Samples = SAMPLES,
        sCPU_time = zeros(Float64,length(SAMPLES)),
        GPU_time = zeros(Float64,length(SAMPLES)),
        sGPU_time = zeros(Float64,length(SAMPLES)),
    )
    rowpos = Int32(1)
    for N in SAMPLES
        # init signals
        cpucarrier = zeros(Complex{Float32}, N)
        gpucarrier = CuArray{ComplexF32}(cpucarrier)

        # init signals in StructArrays
        scpucarrier = StructArray{ComplexF32}(
            (real(cpucarrier),imag(cpucarrier))
        )
        sgpucarrier = StructArray{ComplexF32}(
            (real(gpucarrier),imag(gpucarrier))
        )

        # do the benchmark
        println("Benchmarking carrier_replica on CPU: StructArray{ComplexF32} ", N," samples...")
        result = @benchmark cpu_gen_carrier_replica!(
            $scpucarrier,
            1500,
            2.5e6,
            0,
            Val(7),
            1,
            $N
        )
        println(minimum(result).time)
        results_min.sCPU_time[rowpos] = minimum(result).time
        results_med.sCPU_time[rowpos] = median(result).time
        results_mean.sCPU_time[rowpos] = mean(result).time
        println("Benchmarking carrier_replica on GPU: CuArray{ComplexF32} ", N, " samples...")
        result = @benchmark CUDA.@sync gpu_gen_carrier_replica!(
            $gpucarrier,
            1500,
            2.5e6,
            0,
            Val(7),
            1,
            $N
        )
        println(minimum(result).time)
        results_min.GPU_time[rowpos] = minimum(result).time
        results_med.GPU_time[rowpos] = median(result).time
        results_mean.GPU_time[rowpos] = mean(result).time
        println("Benchmarking carrier_replica on GPU: StructArray{ComplexF32}(CuArray,CuArray) ", N, " samples...")
        result = @benchmark CUDA.@sync gpu_gen_carrier_replica!(
            $sgpucarrier,
            1500,
            2.5e6,
            0,
            Val(7),
            1,
            $N
        )
        println(minimum(result).time)
        results_min.sGPU_time[rowpos] = minimum(result).time
        results_med.sGPU_time[rowpos] = median(result).time
        results_mean.sGPU_time[rowpos] = mean(result).time
        rowpos += 1
    end
    CSV.write("data/carrierreplica_min.csv", results_min)
    CSV.write("data/carrierreplica_med.csv", results_med)
    CSV.write("data/carrierreplica_mean.csv", results_mean)
end