"""
`module  JAC.StrongField
    ... a submodel of JAC that contains all methods to set-up and perform strong-field computations.
"""
module StrongField

    using    GSL, HypergeometricFunctions, Plots, Printf, SpecialFunctions, ..AngularMomentum, ..Basics, ..Continuum, ..Defaults, ..InteractionStrength, ..Radial, ..ManyElectron,
             ..Nuclear, ..Pulse, ..TableStrings

    export   aaaa

    """
    `abstract type StrongField.AbstractSFAObservable`
        ... defines an abstract and a number of types for the observables that can be computed with SFA amplitudes.

        + struct SfaNoObservable            ... an empty instance of an observable that does not help compute anything.
        + struct SfaEnergyDistribution      ... to compute the energy distribution of the photoelectrons.
        + struct SfaMomentumDistribution    ... to compute the momentum distribution of the photoelectrons.
        + struct SfaAzimuthalAngularDistribution     ... to compute the angular distribution (azimuthal angle phi) of photoelectrons with fixed energy and fixed polar angle.
        + struct SfaPolarAngularDistribution     ... to compute the angular distribution (polar angle theta) of photoelectrons with fixed energy and fixed azimuthal angle.
    """
    abstract type  AbstractSFAObservable                                 end
    struct   SfaNoObservable     <: StrongField.AbstractSFAObservable    end

    """
    `struct  StrongField.SfaEnergyDistribution  <: StrongField.AbstractSFAObservable`
        ... to compute in SFA the energy distribution of photoelectrons at given energies and angles.

        + theta         ::Float64             ... polar angle of the energy distribution
        + phi           ::Float64             ... azimuthal angle of the energy distribution
        + energies      ::Array{Float64,1}    ... specifies the photoelectron energy in the current units.
    """
    struct   SfaEnergyDistribution  <: StrongField.AbstractSFAObservable
        theta           ::Float64
        phi             ::Float64
        energies        ::Array{Float64,1}
    end

    """
    `StrongField.SfaEnergyDistribution(theta::Float64, phi::Float64, NoEnergies::Int64, maxEnergy::Float64)`
        ... defines an energy distribution for given (theta,phi) and for NoEnergies between 0 < energy <= maxEnergy;
            a dist::SfaEnergyDistribution is returned.
    """
    function SfaEnergyDistribution(theta::Float64, phi::Float64, NoEnergies::Int64, maxEnergy::Float64)
        energies = Float64[];      for  i=1:NoEnergies   push!(energies, i*maxEnergy / NoEnergies)   end
        SfaEnergyDistribution(theta, phi, energies)
    end

    function Base.string(obs::SfaEnergyDistribution)
        sa = "Compute photo-electron energy distribution at angles (theta,phi) = ($(obs.theta), $(obs.phi)) for the energies [a.u.]:"
        return( sa )
    end

    function Base.show(io::IO, obs::SfaEnergyDistribution)
        sa = string(obs);       print(io, sa, "\n");     print(io, "   ", obs.energies)
    end

    """
    `struct  StrongField.SfaMomentumDistribution  <: StrongField.AbstractSFAObservable`
        ... to compute in SFA the momentum distribution of photoelectrons for given polar angle theta and at given energies and azimuthal angles.

        + theta         ::Float64             ... polar angle of the energy distribution
        + phi           ::Array{Float64,1}    ... specifies the azimuthal angles
        + energies      ::Array{Float64,1}    ... specifies the photoelectron energy in the current units.
    """
    struct   SfaMomentumDistribution  <: StrongField.AbstractSFAObservable
        theta           ::Float64
        phis            ::Array{Float64,1}
        energies        ::Array{Float64,1}
    end

    """
    `StrongField.SfaMomentumDistribution(theta::Float64, NoPhi::Int64, NoEnergies::Int64, maxEnergy::Float64)`
        ... defines a momentum distribution for given theta and for NoEnergies between 0 < energy <= maxEnergy and NoPhi between 0 <= phi < 2pi;
            a dist::SfaMomentumDistribution is returned.
    """
    function SfaMomentumDistribution(theta::Float64, NoPhi::Int64, NoEnergies::Int64, maxEnergy::Float64)
        energies = Float64[];      for  i=1:NoEnergies   push!(energies, i*maxEnergy / NoEnergies)   end
        phis = Float64[];          for  i=1:NoPhi        push!(phis, i*2pi / NoPhi)                  end
        SfaMomentumDistribution(theta, phis, energies)
    end

    function Base.string(obs::SfaMomentumDistribution)
        sa = "Compute photo-electron momentum distribution at polar angle theta = $(obs.theta) for (energy [a.u.], phi):"
        return( sa )
    end

    function Base.show(io::IO, obs::SfaMomentumDistribution)
        sa = string(obs);       print(io, sa, "\n");     print(io, "   ", obs.energies);    print(io, "   ", obs.phi)
    end

    """
    `struct  StrongField.SfaAzimuthalAngularDistribution  <: StrongField.AbstractSFAObservable`
        ... to compute in SFA the angular distribution of photoelectrons for given polar angle theta and at given azimuthal angles for a fixed energy

        + theta         ::Float64             ... polar angle of the energy distribution
        + phi           ::Array{Float64,1}    ... specifies the azimuthal angles
        + energy        ::Float64             ... specifies the photoelectron energy in the current units.
    """
    struct   SfaAzimuthalAngularDistribution  <: StrongField.AbstractSFAObservable
        theta           ::Float64
        phis            ::Array{Float64,1}
        energy          ::Float64
    end

    """
    `StrongField.SfaAzimuthalAngularDistribution(theta::Float64, NoPhi::Int64, energy::Float64)`
        ... defines a angular distribution for given theta and for NoPhi between 0 <= phi < 2pi and a fixed energy
            a dist::SfaAzimuthalAngularDistribution is returned.
    """
    function SfaAzimuthalAngularDistribution(theta::Float64, NoPhi::Int64, energy::Float64)
        phis = Float64[];          for  i=1:NoPhi        push!(phis, i*2pi / NoPhi)                  end
        SfaAzimuthalAngularDistribution(theta, phis, energy)
    end

    function Base.string(obs::SfaAzimuthalAngularDistribution)
        sa = "Compute photoelectron angular distribution at polar angle theta = $(obs.theta) and energy $(obs.energy) for phi:"
        return( sa )
    end

    function Base.show(io::IO, obs::SfaAzimuthalAngularDistribution)
        sa = string(obs);       print(io, sa, "\n");    print(io, "   ", obs.phi)
    end

    """
    `struct  StrongField.SfaPolarAngularDistribution  <: StrongField.AbstractSFAObservable`
        ... to compute in SFA the angular distribution of photoelectrons for given azimuthal angle phi and at given polar angles for a fixed energy

        + phi         ::Float64             ... phi angle of the energy distribution
        + theta       ::Array{Float64,1}    ... specifies the polar angles
        + energy      ::Float64             ... specifies the photoelectron energy in the current units.
    """
    struct   SfaPolarAngularDistribution  <: StrongField.AbstractSFAObservable
        phi           ::Float64
        thetas        ::Array{Float64,1}
        energy        ::Float64
    end

    """
    `StrongField.SfaPolarAngularDistribution(phi::Float64, NoTheta::Int64, energy::Float64)`
        ... defines a angular distribution for given phi and for NoPhi between 0 <= phi < 2pi and a fixed energy
            a dist::SfaPolarAngularDistribution is returned.
    """
    function SfaPolarAngularDistribution(phi::Float64, NoTheta::Int64, energy::Float64)
        thetas = Float64[];          for  i=1:NoTheta        push!(thetas, i*2pi / NoTheta)                  end
        SfaPolarAngularDistribution(phi, thetas, energy)
    end

    function Base.string(obs::SfaPolarAngularDistribution)
        sa = "Compute photoelectron angular distribution at azimuthal angle phi = $(obs.phi) and energy $(obs.energy) for theta:"
        return( sa )
    end

    function Base.show(io::IO, obs::SfaPolarAngularDistribution)
        sa = string(obs);       print(io, sa, "\n");    print(io, "   ", obs.theta)
    end

    """
    `abstract type StrongField.AbstractVolkovState`
        ... defines an abstract and a number of types for dealing with the Volkov states in the computation of the SFA amplitudes.

        + struct FreeVolkov       ... to apply the free-Volkov states.
        + struct CoulombVolkov    ... apply the Coulomb-Volkov states.
        + struct DistortedVolkov  ... apply the Distorted-Volkov states.
    """
    abstract type  AbstractVolkovState                         end
    struct         FreeVolkov        <:  AbstractVolkovState   end

    """
    `struct StrongField.CoulombVolkov     <:  StrongField.AbstractVolkovState`

        + Z ::Float64   ... Charge that generates the Coulomb potential
    """
    struct  CoulombVolkov   <: StrongField.AbstractVolkovState
        Z   ::Float64
    end

    struct         DistortedVolkov   <:  AbstractVolkovState   end

    function Base.string(volkov::FreeVolkov)         return( "Free-Volkov states" )         end
    function Base.string(volkov::CoulombVolkov)      return( "Coulomb-Volkov states in potential of charge Z = $(volkov.Z)" )      end
    function Base.string(volkov::DistortedVolkov)    return( "Distorted-Volkov states" )    end

    """
    `struct  StrongField.Settings`
        ... defines a type for the details and parameters of computing SFA amplitudes.

        + multipoles            ::Array{EmMultipoles}  ... Multipoles of the radiation field that are to be included.
        + gauges                ::Array{UseGauge}      ... Specifies the gauges to be included into the computations.
        + printAmplitudes       ::Bool                 ... True: the amplitudes are printed during computation
        + coupledBasis          ::Bool                 ... True: compute in |j l mj> angular momentum basis; False: compute in |l ml> angular momentum basis
        + hydrogenic            ::Bool                 ... True: Use hydrogenic wave functions for the initial state
        + hydrogenic1s          ::Bool                 ... True: Use hydrogenic wave functions for the initial state
        + mAverage              ::Bool                 ... True: Average over initialState projections mj or ml and sum over continuum spin projection msp
    """
    struct Settings
        multipoles              ::Array{EmMultipole,1}
        gauges                  ::Array{UseGauge}
        printAmplitudes         ::Bool
        coupledBasis            ::Bool
        hydrogenic              ::Bool
        hydrogenic1s            ::Bool
        mAverage                ::Bool
    end

    """
    `StrongField.Settings()`
        ... constructor for the default values of SFA computations.
    """
    function Settings()
        Settings([E1], [UseCoulomb], true, true, false, false, true)
    end

    # `Base.show(io::IO, settings::StrongField.Settings)`  ... prepares a proper printout of the variable settings::StrongField.Settings.
    function Base.show(io::IO, settings::StrongField.Settings)
        println(io, "multipoles:                 $(settings.multipoles)  ")
        println(io, "use-gauges:                 $(settings.gauges)  ")
        println(io, "printBefore:                $(settings.printBefore)  ")
        println(io, "printAmplitudes:            $(settings.printAmplitudes)  ")
        println(io, "coupledBasis:               $(settings.coupledBasis)  ")
        println(io, "hydrogenic:                 $(settings.hydrogenic)  ")
        println(io, "mAverage:                   $(settings.mAverage)  ")
    end

    """
    `struct  StrongField.Computation`
        ... defines a type for the computation of strong-field amplitudes and observables (properties).

        + observable         ::AbstractSFAObservable        ... The obserable to be calculated in SFA.
        + nuclearModel       ::Nuclear.Model                ... Model, charge and parameters of the nucleus.
        + grid               ::Radial.Grid                  ... The radial grid to be used for the computation.
        + initialLevel       ::Level                        ... Initial level of the atom
        + finalLevel         ::Level                        ... Final level of the atom
        + beam               ::Pulse.AbstractBeam           ... Beam character and properties of the incident light pulse.
        + envelope           ::Pulse.AbstractEnvelope       ... Envelope of the incident light pulse.
        + polarization       ::Basics.AbstractPolarization  ... Envelope of the incident light pulse.
        + volkov             ::AbstractVolkovState          ... Specify the treatment/approach of the Volkov states.
        + settings           ::StrongField.Settings         ... Settings for the controlling the SFA amplitudes.
    """
    struct  Computation
        observable           ::AbstractSFAObservable
        nuclearModel         ::Nuclear.Model
        grid                 ::Radial.Grid
        initialLevel         ::Level
        finalLevel           ::Level
        beam                 ::Pulse.AbstractBeam
        envelope             ::Pulse.AbstractEnvelope
        polarization         ::Basics.AbstractPolarization
        volkov               ::AbstractVolkovState
        settings             ::StrongField.Settings
    end

    """
    `StrongField.Computation()`  ... constructor for an `empty` instance of StrongField.Computation().
    """
    function Computation()
        Computation( SfaNoObservable(), Nuclear.Model(1.0), Radial.Grid(true), Orbital("1s_1/2",0.5), Orbital("1s_1/2",0.5),
                     Pulse.PlaneWaveBeam(), Pulse.InfiniteEnvelope(), Basics.RightCircular(), FreeVolkov(), Settings()  )
    end

    # `Base.string(comp::StrongField.Computation)`  ... provides a String notation for the variable comp::StrongField.Computation.
    function Base.string(comp::StrongField.Computation)
        sa = "Strong-field computation:  " * string(comp.observable) * " for Z = $(comp.nuclearModel.Z)  with " * string(comp.volkov) * "\n"
        sa = sa * " initial level with (J, P, energy) = ($(comp.initialLevel.J), $(comp.initialLevel.parity), $(comp.initialLevel.energy)) \n"
        sa = sa * " final level with   (J, P, energy) = ($(comp.finalLevel.J), $(comp.finalLevel.parity), $(comp.finalLevel.energy))   \n\n"
        sa = sa * "The incident laser pulse is described by the: \n "
        sa = sa * string(comp.beam) * "\n " * string(comp.polarization) * "\n " * string(comp.envelope)
        return( sa )
    end

    # `Base.show(io::IO, comp::StrongField.Computation)`  ... prepares a proper printout of the variable comp::StrongField.Computation.
    function Base.show(io::IO, comp::StrongField.Computation)
        sa = Base.string(comp);             print(io, sa, "\n\n")
        println(io, "Settings:              \n$(comp.settings)    ")
        println(io, "nuclearModel:          $(comp.nuclearModel)  ")
        println(io, "grid:                  $(comp.grid)  ")
    end

    """
    `struct  StrongField.SphericalAmplitude`
        ... to keep the amplitude at a given  energy-angular point (energy, theta, phi) in momentum space.

        + energy        ::Float64      ... Kinetic energy of the (outgoing) photoelectron.
        + theta         ::Float64      ... Polar angle of the (outgoing) photoelectron.
        + phi           ::Float64      ... Azimuthal angle of the (outgoing) photoelectron.
        + value         ::ComplexF64   ... (Total) Amplitude.
    """
    struct   SphericalAmplitude
        energy          ::Float64
        theta           ::Float64
        phi             ::Float64
        value           ::ComplexF64
    end

    function Base.string(amp::StrongField.SphericalAmplitude)
        sa = "Total SFA amplitude at (energy, theta, phi) = ($(amp.energy),$(amp.theta),$(amp.phi)) is $(amp.value)."
        return( sa )
    end

    function Base.show(io::IO, amp::StrongField.SphericalAmplitude)
        sa = string(amp);       print(io, sa)
    end

    """
    `struct  StrongField.OutcomeEnergyDistribution`
        ... to comprise the energy distribution of photoelectrons at given energies and angles, for instance,
            for later graphical representation.

        + theta         ::Float64             ... polar angle of the energy distribution
        + phi           ::Float64             ... azimuthal angle of the energy distribution
        + energies      ::Array{Float64,1}    ... selected energies of the distribution.
        + probabilities ::Array{Float64,1}    ... calculated probabilities of the energy distribution.
    """
    struct   OutcomeEnergyDistribution
        theta           ::Float64
        phi             ::Float64
        energies        ::Array{Float64,1}
        probabilities   ::Array{Float64,1}
    end

    # `Base.show(io::IO, outcome::StrongField.OutcomeEnergyDistribution)`  ... prepares a proper printout of the variable outcome::StrongField.OutcomeEnergyDistribution.
    function Base.show(io::IO, outcome::StrongField.OutcomeEnergyDistribution)
        println(io, "theta:             $(outcome.theta)  ")
        println(io, "phi:               $(outcome.phi)  ")
        println(io, "energies:          $(outcome.energies)  ")
        println(io, "probabilities:     $(outcome.probabilities)  ")
    end

    """
    `struct  StrongField.OutcomeMomentumDistribution`
        ... to comprise the momentum distribution of photoelectrons at given energies and angles, for instance,
            for later graphical representation.

        + theta         ::Float64             ... polar angle of the momentum distribution
        + momenta       ::Array{Float64}      ... pairs [px py] of momenta of the momentum distribution
        + probabilities ::Array{Float64,1}    ... calculated probabilities of the momentum distribution.
    """
    struct   OutcomeMomentumDistribution
        theta           ::Float64
        momenta         ::Array{Float64}
        probabilities   ::Array{Float64,1}
    end

    # `Base.show(io::IO, outcome::StrongField.OutcomeMomentumDistribution)`  ... prepares a proper printout of the variable outcome::StrongField.OutcomeMomentumDistribution.
    function Base.show(io::IO, outcome::StrongField.OutcomeMomentumDistribution)
        println(io, "theta:             $(outcome.theta)  ")
        println(io, "momenta:           $(outcome.momenta)  ")
        println(io, "probabilities:     $(outcome.probabilities)  ")
    end

    """
    `struct  StrongField.OutcomeAzimuthalAngularDistribution`
        ... to comprise the angular distribution of photoelectrons at given energy and angles,
            for later graphical representation.

        + theta         ::Float64             ... polar angle of the angular distribution
        + energy        ::Float64             ... photoelectron energy
        + phis          ::Array{Float64,1}      ... azimuthal angles phi
        + probabilities ::Array{Float64,1}    ... calculated probabilities of the angular distribution.
    """
    struct   OutcomeAzimuthalAngularDistribution
        theta           ::Float64
        energy          ::Float64
        phis            ::Array{Float64,1}
        probabilities   ::Array{Float64,1}
    end

    # `Base.show(io::IO, outcome::StrongField.OutcomeAzimuthalAngularDistribution)`  ... prepares a proper printout of the variable outcome::StrongField.OutcomePolarAngularDistribution.
    function Base.show(io::IO, outcome::StrongField.OutcomeAzimuthalAngularDistribution)
        println(io, "theta:             $(outcome.theta)  ")
        println(io, "energy:            $(outcome.energy)  ")
        println(io, "phis:              $(outcome.phis)  ")
        println(io, "probabilities:     $(outcome.probabilities)  ")
    end

    """
    `struct  StrongField.OutcomePolarAngularDistribution`
        ... to comprise the angular distribution of photoelectrons at given energy and angles,
            for later graphical representation.

        + phi           ::Float64             ... azimuthal angle of the angular distribution
        + energy        ::Float64             ... photoelectron energy
        + thetas        ::Array{Float64,1}      ... polar angles phi
        + probabilities ::Array{Float64,1}    ... calculated probabilities of the angular distribution.
    """
    struct   OutcomePolarAngularDistribution
        phi             ::Float64
        energy          ::Float64
        thetas          ::Array{Float64,1}
        probabilities   ::Array{Float64,1}
    end

    # `Base.show(io::IO, outcome::StrongField.OutcomePolarAngularDistribution)`  ... prepares a proper printout of the variable outcome::StrongField.OutcomePolarAngularDistribution.
    function Base.show(io::IO, outcome::StrongField.OutcomePolarAngularDistribution)
        println(io, "phi:             $(outcome.phi)  ")
        println(io, "energy:            $(outcome.energy)  ")
        println(io, "thetas:              $(outcome.thetas)  ")
        println(io, "probabilities:     $(outcome.probabilities)  ")
    end

    ##################################################################################################################################################
    ##################################################################################################################################################
    ##################################################################################################################################################

    """
    `StrongField.VolkovP(epsilonp::Float64, lp::Int, rGrid::Array{Float64,1})`
        ... returns the (plane-wave) Volkov radial wave function
                at the radial grid points rGrid
    """
    function  VolkovP(epsilonp::Float64, lp::Int, rGrid::Array{Float64,1})
        P = Float64[]

        for j = 1:length(rGrid)
            r = rGrid[j]
            p = r * GSL.sf_bessel_jl( lp, sqrt(2*epsilonp)*r )
            push!( P, p )
        end

        return( P )
    end

    """
    `StrongField.CoulombVolkovP(epsilonp::Float64, lp::Int, ZContinuum::Float64, rGrid::Array{Float64,1})`
        ... returns the Coulomb-Volkov radial wave function
                at the radial grid points rGrid
    """
    function  CoulombVolkovP(epsilonp::Float64, lp::Int, ZContinuum::Float64, rGrid::Array{Float64,1})
        sqrtTwoEps = sqrt(2*epsilonp)

        P = ComplexF64[]

        for j = 1:length(rGrid)
            r = rGrid[j]
            rho = sqrtTwoEps*r
            eta = -ZContinuum/sqrtTwoEps
            GammaValue = gamma( lp + 1 + eta * im )
            sigma = angle( GammaValue )
            Fl = 2.0^lp * exp(-0.5*pi*eta) * abs( GammaValue ) / factorial(2*lp+1) * rho^(lp+1) * exp( rho * im ) * HypergeometricFunctions.drummond1F1(lp+1+eta*im,2*lp+2,-2*rho*im)

            p = Fl * exp( sigma * im ) / sqrtTwoEps

            push!( P, p )
        end

        return( P )
    end

    ####################################################################################################################
    ####################################################################################################################

    """
    `StrongField.computeEnvelopeIntegrals(envelope::Pulse.AbstractEnvelope, beam::AbstractBeam, polarization::Basics.AbstractPolarization,
                                          thetap::Float64, phip::Float64, energyp::Float64, initialEn::Float64)`
        ... computes the pulse-envelope integrals for the given laser pulse; this pulse is completely specified by its beam character
            and parameters, the pulse-envelope parameters and the polarization of the light. A tuple of the two integrals
            (fVolkov, fVolkovSquared)::Tuple{Complex{Float64},Complex{Float64}} is returned.
    """
    function computeEnvelopeIntegrals(envelope::Pulse.AbstractEnvelope, beam::AbstractBeam, polarization::Basics.AbstractPolarization,
                                      thetap::Float64, phip::Float64, energyp::Float64, initialEn::Float64)
        fVolkovPlus    = Pulse.envelopeVolkovIntegral(true,  envelope, beam, polarization, thetap::Float64, phip::Float64, energyp::Float64, initialEn::Float64)
        fVolkovMinus   = Pulse.envelopeVolkovIntegral(false, envelope, beam, polarization, thetap::Float64, phip::Float64, energyp::Float64, initialEn::Float64)
        fVolkovSquared = Pulse.envelopeQuadVolkovIntegral(envelope, beam, polarization,    thetap::Float64, phip::Float64, energyp::Float64, initialEn::Float64)

        #println("> Envelope integrals for a $(string(envelope)) with A0=$(beam.A0), omega=$(beam.omega) [a.u.], cep=$(beam.cep)" *
        #        " and for $(string(polarization)) light are:" )
        #println("    F^(Volkov) [+; omega; f^(env); A]   = $fVolkovPlus" )
        #println("    F^(Volkov) [-; omega; f^(env); A]   = $fVolkovMinus" )
        #println("    F^(quad, Volkov) [f^(env); A]       = $fVolkovSquared" )

        return( (fVolkovPlus, fVolkovMinus, fVolkovSquared) )
    end

    """
    `StrongField.computeOutcome(observable::StrongField.SfaEnergyDistribution, amplitudes::Array{SphericalAmplitude,3})`
        ... computes the requested photoelectron energy distribution and returns the results in the variable
            outcome::StrongField.OutcomeEnergyDistribution
    """
    function  computeOutcome(obs::StrongField.SfaEnergyDistribution, amplitudes::Array{SphericalAmplitude,3})
        probabilities = Float64[]
        dimensions = size(amplitudes)
        for  jAmp  = 1:dimensions[3]
            pp = sqrt(2*amplitudes[1,1,jAmp].energy)
            amp = 0.
            for jmi = 1:dimensions[1]
                for jmf = 1:dimensions[2]
                    amp += amplitudes[jmi,jmf,jAmp].value * conj(amplitudes[jmi,jmf,jAmp].value) #sum of probabilities
                end
            end
            push!(probabilities, pp * amp )
        end
        outcome = OutcomeEnergyDistribution(obs.theta, obs.phi, obs.energies, probabilities)
        return( outcome )
    end

    """
    `StrongField.computeOutcome(observable::StrongField.SfaMomentumDistribution, amplitudes::Array{SphericalAmplitude,3})`
        ... computes the requested photoelectron momentum distribution and returns the results in the variable
            outcome::StrongField.OutcomeMomentumDistribution
    """
    function  computeOutcome(obs::StrongField.SfaMomentumDistribution, amplitudes::Array{SphericalAmplitude,3})
        probabilities   = Float64[]
        dimensions      = size(amplitudes)
        momenta         = Array{Float64}(undef,0,2)

        for  jAmp  = 1:dimensions[3]
            pp = sqrt(2*amplitudes[1,1,jAmp].energy)
            px = pp*sin(amplitudes[1,1,jAmp].theta)*cos(amplitudes[1,1,jAmp].phi)
            py = pp*sin(amplitudes[1,1,jAmp].theta)*sin(amplitudes[1,1,jAmp].phi)
            momenta = [momenta ; [px py]]
            amp = 0.
            for jmi = 1:dimensions[1]
                for jmf = 1:dimensions[2]
                    amp += amplitudes[jmi,jmf,jAmp].value * conj(amplitudes[jmi,jmf,jAmp].value) #sum of probabilities
                end
            end
            push!(probabilities, pp * amp )
        end
        outcome = OutcomeMomentumDistribution(obs.theta, momenta, probabilities)
        return( outcome )
    end

    """
    `StrongField.computeOutcome(observable::StrongField.SfaAzimuthalAngularDistribution, amplitudes::Array{SphericalAmplitude,3})`
        ... computes the requested photoelectron angular distribution and returns the results in the variable
            outcome::StrongField.OutcomeAzimuthalAngularDistribution
    """
    function  computeOutcome(obs::StrongField.SfaAzimuthalAngularDistribution, amplitudes::Array{SphericalAmplitude,3})
        probabilities = Float64[]
        dimensions = size(amplitudes)
        for  jAmp  = 1:dimensions[3]
            pp = sqrt(2*amplitudes[1,1,jAmp].energy)
            amp = 0.
            for jmi = 1:dimensions[1]
                for jmf = 1:dimensions[2]
                    amp += amplitudes[jmi,jmf,jAmp].value * conj(amplitudes[jmi,jmf,jAmp].value) #sum of probabilities
                end
            end
            push!(probabilities, pp * amp )
        end
        outcome = OutcomeAzimuthalAngularDistribution(obs.theta, obs.energy, obs.phis, probabilities)
        return( outcome )
    end

    """
    `StrongField.computeOutcome(observable::StrongField.SfaPolarAngularDistribution, amplitudes::Array{SphericalAmplitude,3})`
        ... computes the requested photoelectron angular distribution and returns the results in the variable
            outcome::StrongField.OutcomePolarAngularDistribution
    """
    function  computeOutcome(obs::StrongField.SfaPolarAngularDistribution, amplitudes::Array{SphericalAmplitude,3})
        probabilities = Float64[]
        dimensions = size(amplitudes)
        for  jAmp  = 1:dimensions[3]
            pp = sqrt(2*amplitudes[1,1,jAmp].energy)
            amp = 0.
            for jmi = 1:dimensions[1]
                for jmf = 1:dimensions[2]
                    amp += amplitudes[jmi,jmf,jAmp].value * conj(amplitudes[jmi,jmf,jAmp].value) #sum of probabilities
                end
            end
            push!(probabilities, pp * amp )
        end
        outcome = OutcomePolarAngularDistribution(obs.phi, obs.energy, obs.thetas, probabilities)
        return( outcome )
    end

    #---------------------------------------------------------------------------------------------------
    #--------------------------------------FORMULATION in j-l-mj-BASIS:---------------------------------
    #---------------------------------------------------------------------------------------------------

    """
    `StrongField.pReducedME(Pepsplp::Array{ComplexF64,1}, lp::Int, jp::Float64, n::Int, l::Int, j::Float64, initialOrbital::Orbital, grid::Radial.Grid)`
        ... computes the reduced matrix elements of the momentum operator <epsilonp lp jp||p||n l j> in the one-particle picture
            where the radial wave function Pepsplp of |epsilonp lp jp> needs to be provided as an argument on the grid rGrid
    """
    function  pReducedME(Pepsplp::Array{ComplexF64,1}, lp::Int, jp::Float64, l::Int, j::Float64, initialOrbital::Orbital, grid::Radial.Grid)

        #---- Compute the factor in <epsilonp lp jp||p||n l j> = fac * <epsilonp lp||p||n l>-----
        fac = 0.
        for    kms = -1:0 #ms = 1/2 + kms
            ms = 1/2 + kms
            for    ml = -l:l
                mj = ml + ms
                for    mlp = -lp:lp
                    mjp = mlp + ms
                    fac = fac + AngularMomentum.ClebschGordan_old(  AngularJ64(Rational(lp)),   AngularM64(Rational(mlp)) ,
                                                                    AngularJ64(1//2),           AngularM64(Rational(ms)),
                                                                    AngularJ64(Rational(jp)),   AngularM64(Rational(mjp))       )       *
                                AngularMomentum.ClebschGordan_old(  AngularJ64(Rational(l)),    AngularM64(Rational(ml)),
                                                                    AngularJ64(1//2),           AngularM64(Rational(ms)),
                                                                    AngularJ64(Rational(j)),    AngularM64(Rational(mj))       )
                end
            end
        end
        #----------------------------------------------------------------------------------------

        #--------------------------------Compute <epsilonp lp||p||n l>---------------------------
        rvalues = grid.r
        rweights = grid.wr
        orderGL = min(size(Pepsplp, 1), size(initialOrbital.P, 1))

        integral = 0. * im

        #Sum over grid and compute Gauss-Legendre sum
        for    k = 1:orderGL
            r = rvalues[k]
            integrand = conj( Pepsplp[k] )/r * ( r*initialOrbital.Pprime[k] - ((lp-l)*(lp+l+1))/2 * initialOrbital.P[k] )

            #Gauss-Legendre sum
            integral = integral + rweights[k] * integrand
        end

        #Note that GSL.sf_coupling_3j takes takes the input (2*j1,2*j2,2*j3,2*m1,2*m2,2*m3)
        integral = integral * (-im)^(lp+1) * (-1)^lp * GSL.sf_coupling_3j( 2*lp, 2*1, 2*l, 0, 0, 0 )
        #----------------------------------------------------------------------------------------

        return( fac * integral )
    end

    """
    `StrongField.scalarProdBoundCont(Pepsplp::Array{ComplexF64,1}, l::Int, initialOrbital::Orbital, grid::Radial.Grid)`
        ... computes the scalar product of the bound and continuum states in the one-particle picture
            where the radial wave function Pepsplp of |epsilonp lp jp> needs to be provided as an argument on the grid rGrid
    """
    function  scalarProdBoundCont(Pepsplp::Array{ComplexF64,1}, l::Int, initialOrbital::Orbital, grid::Radial.Grid)
        rvalues = grid.r
        rweights = grid.wr

        orderGL = min(size(Pepsplp, 1), size(initialOrbital.P, 1))

        integral = 0. * im

        #Sum over grid and compute Gauss-Legendre sum
        for    k = 1:orderGL
            r = rvalues[k]
            integrand = conj( Pepsplp[k] ) * initialOrbital.P[k]

            #Gauss-Legendre sum
            integral = integral + rweights[k] * integrand
        end

        return((-im)^l * integral)
    end

    """
    `StrongField.computeSphericalAmplitudes(comp::StrongField.Computation)`
        ... computes all necessary spherical amplitudes for the given initial and final levels, the Volkov states
            and polarization and by using the pulse envelope integrals - using a coupled angular-momentum basis for the active electron,
            A sfaAmplitudes::Array{SphericalAmplitude,3} is returned.
    """
    function  computeSphericalAmplitudes(comp::StrongField.Computation)

        #Extract the initial orbital of the active electron from the many-electron comp.initialLevel and set quantum numbers
        initialOrbitals = comp.initialLevel.basis.orbitals

        #Find highest lying orbital (smallest ionization potential)
        defaultSubshell = [sh for (sh,or) in initialOrbitals][1] #This is not nice; must be a better way to simply get a default element from a Dict
        initialOrbital = initialOrbitals[defaultSubshell]
        minIonPotential = abs(initialOrbital.energy)
        for (subshell,orbital) in initialOrbitals
            if   abs(orbital.energy) < minIonPotential
                initialOrbital = orbital
                minIonPotential = abs(orbital.energy)
            end
        end

        initialEneV = convertUnits("energy: from atomic to eV", initialOrbital.energy)
        println("")
        println("Selected orbital $(initialOrbital.subshell) as active electron for the strong-field ionization process. Binding energy: $(initialEneV) eV.")
        println("")

        ls = LevelSymmetry(initialOrbital.subshell)
        n = initialOrbital.subshell.n;      l = (ls.J.num+1)/2;    j = ls.J.num/2;     initialEn = initialOrbital.energy

        if  (sign((-1)^l) == -1 && ls.parity == plus::Parity) || (sign((-1)^l) == 1 && ls.parity == minus::Parity)
            l = l - 1
        end
        l = floor(Int,l)

        if comp.settings.hydrogenic && comp.settings.hydrogenic1s
            n = 1
            l = 0
            j = 1/2
        end

        lpMin = abs(l-1)
        lpMax = l+1

        #Determine which spherical SFA amplitudes need to be computed before the actual computation starts
        mjNum = 1; mspNum = 1    #If comp.Average == false
        mjAverageFactor = 1.     #If comp.Average == false
        if( comp.settings.mAverage )
            mjNum = Int(2*j + 1)
            mspNum = 2
            mjAverageFactor = 1.0/sqrt( mjNum )
        end

        sfaAmplitudes = StrongField.determineSphericalAmplitudes(comp.observable,mjNum,mspNum)

        #-----Generate continuum wave functions for all energies and values of lp that are needed below-----

        println("")
        println("Computing continuum wave functions...")

        energies = Array{Float64}(undef,length(sfaAmplitudes[1,1,:]))
        for  jAmp = 1:length(sfaAmplitudes[1,1,:])  #Get all different energy values
            energies[jAmp] = sfaAmplitudes[1,1,jAmp].energy
        end
        energies = unique(energies)
        lpArray = unique( [lpMin l lpMax] )

        Pepsplp = Array{ComplexF64}(undef,size(energies)[1],size(lpArray)[1],size(comp.grid.r)[1])

        for jlp = 1:size(lpArray)[1]

            lp = lpArray[jlp]

            for jE = 1:size(energies)[1]

                epsilonp = energies[jE]

                if  typeof(comp.volkov) == FreeVolkov

                    Pepsplp[jE,jlp,:] = VolkovP( epsilonp, lp, comp.grid.r )

                elseif  typeof(comp.volkov) == CoulombVolkov

                    Pepsplp[jE,jlp,:] = CoulombVolkovP( epsilonp, lp, comp.volkov.Z, comp.grid.r )

                elseif  typeof(comp.volkov) == DistortedVolkov

                    eta = (-1)^(lp+1)
                    kappa = eta*(lp+1/2)-1/2

                    nrContinuum = Continuum.gridConsistency(epsilonp, comp.grid)
                    contSettings = Continuum.Settings(false, comp.grid.NoPoints)

                    newiLevel = Basics.generateLevelWithSymmetryReducedBasis(comp.initialLevel, comp.initialLevel.basis.subshells)
                    newfLevel = Basics.generateLevelWithSymmetryReducedBasis(comp.finalLevel, newiLevel.basis.subshells)
                    newiLevel = Basics.generateLevelWithExtraSubshell(Subshell(101, kappa), newiLevel)
                    cOrbital, phase  = Continuum.generateOrbitalForLevel(epsilonp, Subshell(101, kappa), newfLevel, comp.nuclearModel, comp.grid, contSettings)

                    jr = size(cOrbital.P)[1]
                    while jr < size(comp.grid.r)[1]
                        push!(cOrbital.P,0)
                        jr += 1
                    end
                    Pepsplp[jE,jlp,:] = cOrbital.P * exp(im*phase)

                end

            end #end for energy

        end #end for lp

        #---------------------------------------------------------------------------------------------------

        println("")
        println("Computing SFA amplitudes...")
        println("")

        # Compute the requested amplitudes
        for jmj = 1:mjNum
            mj = -j + (jmj-1)
            for jmsp = 1:mspNum
                msp = -1/2 + (jmsp-1)
                for  jAmp = 1:length(sfaAmplitudes[jmj,jmsp,:]) #amp in  sfaAmplitudes[jmj,jmsp]
                    amp = sfaAmplitudes[jmj,jmsp,jAmp]
                    thetap        = amp.theta;   phip = amp.phi;
                    energyp = amp.energy; jE = findall(x->x==energyp, vec(energies))[1]
                    envIntegrals  = StrongField.computeEnvelopeIntegrals(comp.envelope, comp.beam, comp.polarization, thetap, phip, energyp, initialEn)
                    fVolkovPlus, fVolkovMinus, fVolkovSquared = envIntegrals
                    #

                    wminus = 0.0im;    wplus = 0.0im
                    # Collect contributions from all l_p, j_p, q terms
                    for  jlp = 1:size(lpArray)[1]
                        lp = lpArray[jlp]

                        kjpMin = floor(Int, ( 2 * abs(lp-1/2) - 1 ) / 2) # jp = (2*kjp + 1)/2, so that the sum below only runs over the correct values. THERE IS A BETTER WAY
                        kjpMax = floor(Int, ( 2 * abs(lp+1/2) - 1 ) / 2)

                        #Compute the sum over total angular momentum jp
                        for kjp = kjpMin:kjpMax
                            jp = 1/2 * (2 * kjp + 1)

                            KWignerEckart = 1.0/sqrt(2*jp+1) #Factor in the Wigner-Eckart theorem (CONVENTION that needs to fit the reduced ME!)

                            if comp.settings.hydrogenic
                                reducedME = pReducedMEHydrogenic(Pepsplp[jE,jlp,:], lp, jp, initialEn, n, l, j, comp.grid)
                            else
                                reducedME = pReducedME(Pepsplp[jE,jlp,:], lp, jp, l, j, initialOrbital, comp.grid)
                            end

                            #Compute the sum over spherical basis components
                            #Note: sphericalYlm needs an integer in the second argument. mj-msp-q is always a whole number, so we can convert to Int using floor without problems
                            for  q in [-1, 0, 1]
                                wplus = wplus +  KWignerEckart * (-1)^q * Basics.determinePolarizationVector(q, comp.polarization, star=false)   *
                                                    AngularMomentum.sphericalYlm(lp, floor(Int,mj-msp-q), amp.theta, amp.phi)                           *
                                                    AngularMomentum.ClebschGordan_old(  AngularJ64(Rational(lp)),   AngularM64(Rational(mj-msp-q)),
                                                                                AngularJ64(1//2),           AngularM64(Rational(msp)),
                                                                                AngularJ64(Rational(jp)),   AngularM64(Rational(mj-q)) )        *
                                                    AngularMomentum.ClebschGordan_old(  AngularJ64(Rational(j)),    AngularM64(Rational(mj)),
                                                                                AngularJ64(1),              AngularM64(Rational(-q)),
                                                                                AngularJ64(Rational(jp)),   AngularM64(Rational(mj-q)) )         *
                                                    reducedME

                                wminus = wminus +  KWignerEckart * Basics.determinePolarizationVector(q, comp.polarization, star=true)           *
                                                    AngularMomentum.sphericalYlm(lp, floor(Int,mj-msp+q), amp.theta, amp.phi)               *
                                                    AngularMomentum.ClebschGordan_old(  AngularJ64(Rational(lp)),   AngularM64(Rational(mj-msp+q)),
                                                                                AngularJ64(1//2),           AngularM64(Rational(msp)),
                                                                                AngularJ64(Rational(jp)),   AngularM64(Rational(mj+q)) )        *
                                                    AngularMomentum.ClebschGordan_old(  AngularJ64(Rational(j)),    AngularM64(Rational(mj)),
                                                                                AngularJ64(1),              AngularM64(Rational(q)),
                                                                                AngularJ64(Rational(jp)),   AngularM64(Rational(mj+q)) )         *
                                                    reducedME
                            end

                        end # for jp

                    end # for lp

                    #Compute the scalar product in the term proportional to F_2 in the spherical amplitude
                    if comp.settings.hydrogenic
                        jlp = findall(x->x==l, vec(lpArray))[1]
                        scalarProd = scalarProdBoundContHydrogenic(Pepsplp[jE,jlp,:], initialEn, n, l, comp.grid)
                    else
                        jlp = findall(x->x==l, vec(lpArray))[1]
                        scalarProd = scalarProdBoundCont(Pepsplp[jE,jlp,:], l, initialOrbital, comp.grid)
                    end

                    #Compute the total amplitude
                    wa = (fVolkovPlus * wminus + fVolkovMinus * wplus) + fVolkovSquared * AngularMomentum.sphericalYlm(l, floor(Int,mj-msp), amp.theta, amp.phi) *
                                                                            AngularMomentum.ClebschGordan_old(    AngularJ64(Rational(l)),    AngularM64(Rational(mj-msp)),
                                                                                AngularJ64(1//2),           AngularM64( Rational(msp) ),
                                                                                AngularJ64(Rational(j)),    AngularM64(Rational(mj))       )  *
                                                                            scalarProd

                    wa = - im/sqrt(2*pi) * wa
                    wa = mjAverageFactor * wa

                    sfaAmplitudes[jmj,jmsp,jAmp] = SphericalAmplitude(amp.energy, amp.theta, amp.phi, wa)

                    if( comp.settings.printAmplitudes )
                        println(">> $(SphericalAmplitude(amp.energy, amp.theta, amp.phi, wa))")
                    end
                end #for amp
            end #for jmsp
        end #for jmj

        return( sfaAmplitudes )
    end

    #---------------------------------------------------------------------------------------------------
    #---------------------------------------------------------------------------------------------------
    #----------------------------------------------------------------------------------------------------

    """
    `StrongField.determineSphericalAmplitudes(observable::StrongField.SfaEnergyDistribution, miNum::Int64, mfNum::Int64)`
        ... determines which direct (and other) SFA amplitudes need to be computed; these amplitudes are not yet computed here but can be arranged
            so that only a minimum number of Volkov states and/or reduced many-electron matrix elements need to be computed.
            A list of amplitudes::Array{StrongField.SphericalAmplitude,1} is returned.
    """
    function  determineSphericalAmplitudes(observable::StrongField.SfaEnergyDistribution, miNum::Int64, mfNum::Int64 )
        ENum = length(observable.energies)
        amplitudes = Array{StrongField.SphericalAmplitude}(undef, miNum, mfNum, ENum)

        for jmi = 1:miNum
            for jmf = 1:mfNum
                for jE = 1:ENum
                    energy = observable.energies[jE]
                    amplitudes[jmi,jmf,jE] = SphericalAmplitude(observable.energies[jE], observable.theta, observable.phi, 0.)
                end
            end
        end

        println("> A total of $(length(amplitudes)) spherical amplitudes need to be calculated.")
        return( amplitudes )
    end

    """
    `StrongField.determineSphericalAmplitudes(observable::StrongField.SfaMomentumDistribution, miNum::Int64, mfNum::Int64)`
        ... determines which direct (and other) SFA amplitudes need to be computed; these amplitudes are not yet computed here but can be arranged
            so that only a minimum number of Volkov states and/or reduced many-electron matrix elements need to be computed.
            A list of amplitudes::Array{StrongField.SphericalAmplitude,1} is returned.
    """
    function  determineSphericalAmplitudes(observable::StrongField.SfaMomentumDistribution, miNum::Int64, mfNum::Int64)
        ENum = length(observable.energies)
        phiNum = length(observable.phis)

        amplitudes = Array{StrongField.SphericalAmplitude}(undef, miNum, mfNum, ENum*phiNum)

        for jmi = 1:miNum
            for jmf = 1:mfNum
                ctr = 1
                for  jE = 1:ENum
                    for  jphi = 1:phiNum
                        amplitudes[jmi,jmf,ctr] = SphericalAmplitude(observable.energies[jE], observable.theta, observable.phis[jphi], 0.)
                        ctr = ctr + 1
                    end
                end
            end
        end

        println("> A total of $(length(amplitudes)) spherical amplitudes need to be calculated.")
        return( amplitudes )
    end

    """
    `StrongField.determineSphericalAmplitudes(observable::StrongField.SfaAzimuthalAngularDistribution, miNum::Int64, mfNum::Int64)`
        ... determines which direct (and other) SFA amplitudes need to be computed; these amplitudes are not yet computed here but can be arranged
            so that only a minimum number of Volkov states and/or reduced many-electron matrix elements need to be computed.
            A list of amplitudes::Array{StrongField.SphericalAmplitude,1} is returned.
    """
    function  determineSphericalAmplitudes(observable::StrongField.SfaAzimuthalAngularDistribution, miNum::Int64, mfNum::Int64)
        phiNum = length(observable.phis)
        amplitudes = Array{StrongField.SphericalAmplitude}(undef, miNum, mfNum, phiNum)

        for jmi = 1:miNum
            for jmf = 1:mfNum
                for  jphi = 1:phiNum
                    amplitudes[jmi,jmf,jphi] = SphericalAmplitude(observable.energy, observable.theta, observable.phis[jphi], 0.)
                end
            end
        end

        println("> A total of $(length(amplitudes)) spherical amplitudes need to be calculated.")
        return( amplitudes )
    end

    """
    `StrongField.determineSphericalAmplitudes(observable::StrongField.SfaPolarAngularDistribution, miNum::Int64, mfNum::Int64)`
        ... determines which direct (and other) SFA amplitudes need to be computed; these amplitudes are not yet computed here but can be arranged
            so that only a minimum number of Volkov states and/or reduced many-electron matrix elements need to be computed.
            A list of amplitudes::Array{StrongField.SphericalAmplitude,1} is returned.
    """
    function  determineSphericalAmplitudes(observable::StrongField.SfaPolarAngularDistribution, miNum::Int64, mfNum::Int64)
        thetaNum = length(observable.thetas)
        amplitudes = Array{StrongField.SphericalAmplitude}(undef, miNum, mfNum, thetaNum)

        for jmi = 1:miNum
            for jmf = 1:mfNum
                for  jtheta = 1:thetaNum
                    amplitudes[jmi,jmf,thetaNum] = SphericalAmplitude(observable.energy, observable.thetas[jtheta], observable.phi, 0.)
                end
            end
        end

        println("> A total of $(length(amplitudes)) spherical amplitudes need to be calculated.")
        return( amplitudes )
    end

    """
    `StrongField.perform(comp::StrongField.Computation; output::Bool=false)`
        ... to perform a computation of (one) selected observable that is related to SFA, such as a photoelectron energy
            distribution, momentum distribution or the computation of sidebands.
    """
    function perform(comp::StrongField.Computation; output::Bool=false)
        if  output    results = Dict{String, Any}()    else    results = nothing    end
        nModel = comp.nuclearModel

        if       typeof(comp.observable) == StrongField.SfaEnergyDistribution
            if  comp.settings.coupledBasis
                sfaAmplitudes = StrongField.computeSphericalAmplitudes(comp)
            else
                sfaAmplitudes = StrongField.computeSphericalAmplitudesUncoupled(comp)
            end
            sfaOutcome    = StrongField.computeOutcome(comp.observable, sfaAmplitudes)
            if output    results = Base.merge( results, Dict("computation" => comp, "energy distribution" => sfaOutcome) )  end
        elseif   typeof(comp.observable) == StrongField.SfaMomentumDistribution
            if  comp.settings.coupledBasis
                sfaAmplitudes = StrongField.computeSphericalAmplitudes(comp)
            else
                sfaAmplitudes = StrongField.computeSphericalAmplitudesUncoupled(comp)
            end
            sfaOutcome    = StrongField.computeOutcome(comp.observable, sfaAmplitudes)
            if output    results = Base.merge( results, Dict("computation" => comp, "momentum distribution" => sfaOutcome) )  end
        elseif   typeof(comp.observable) == StrongField.SfaAzimuthalAngularDistribution
            if  comp.settings.coupledBasis
                sfaAmplitudes = StrongField.computeSphericalAmplitudes(comp)
            else
                sfaAmplitudes = StrongField.computeSphericalAmplitudesUncoupled(comp)
            end
            sfaOutcome    = StrongField.computeOutcome(comp.observable, sfaAmplitudes)
            if output    results = Base.merge( results, Dict("computation" => comp, "angular distribution" => sfaOutcome) )  end
        elseif   typeof(comp.observable) == StrongField.SfaPolarAngularDistribution
            if  comp.settings.coupledBasis
                sfaAmplitudes = StrongField.computeSphericalAmplitudes(comp)
            else
                sfaAmplitudes = StrongField.computeSphericalAmplitudesUncoupled(comp)
            end
            sfaOutcome    = StrongField.computeOutcome(comp.observable, sfaAmplitudes)
            if output    results = Base.merge( results, Dict("computation" => comp, "angular distribution" => sfaOutcome) )  end
        else     error("Undefined observable for strong-field computations.")
        end

        if  output   return( results )   else    return( nothing )   end
    end

    #######################################################################################################################################
    #######################################################################################################################################

    include("module-StrongField-inc-hydrogenic.jl") #StrongField routines for hydrogenic initial states
    include("module-StrongField-inc-uncoupled.jl")  #StrongField routines for computations in the basis of "uncoupled" angular momentum l,ml instead of j,l,mj
    ## include("module-StrongField-inc-postProcessing.jl")       #StrongField.plot(...) routine to plot results of StrongField computations

end # module


    export isless

    """
    `Basics.interpolateOnGridGrasp92((F::Array{Float64,1}, oldGrid::Radial.Grid), (G::Void, newGrid::Radial.Grid) )`
        ... to interpolate the (radial) function F from the oldgrid to newgrid, by a call to the Grasp92 Fortran procedures;
            an function G::Array{Float64,1} is returned. **Not yet implemented !**
    """
    function Basics.interpolateOnGridGrasp92(from::Tuple{Array{Float64,1},Radial.Grid}, to::Tuple{DataType,Radial.Grid})
        # First prepare all fields and settings to call quad_grasp92()
        error("Not yet implemented !")
    end

    """
    `Basics.interpolateOnGridTrapezRule((F::Array{Float64,1}, oldGrid::Radial.Grid), (G::Void, newGrid::Radial.Grid)`
        ... to integrate the (radial) function F from the oldgrid to newgrid, by using a trapez rule from the Numerical Recipies.
            A function G::Array{Float64,1} is returned. **Not yet implemented !**.
    """
    function Basics.interpolateOnGridTrapezRule(from::Tuple{Array{Float64,1},Radial.Grid}, to::Tuple{DataType,Radial.Grid})
        error("Not yet implemented !")
    end

    """
    `Basics.integrate()`
        ... integrates a one- or two-dimensional function by different numerical methods, either on a given grid or for a general function.

    + `("function: on radial grid, Newton-Cotes", F::Array{Float64,1}, grid::Radial.Grid)`
        ... to integrate the (radial) function F over the given (radial) grid, int_0^infinity dr F(r) by using a 5-point Newton-Cotes
            integration formula; a value::Float64 is returned.

    + `("function: on radial grid, Simpson rule", F::Array{Float64,1}, grid::Radial.Grid)`
        ... to integrate the (radial) function F over the given (radial) grid, int_0^infinity dr F(r) by using Simpson's rule;
            a value::Float64 is returned.

    + `("function: on radial grid, trapez rule", F::Array{Float64,1}, grid::Radial.Grid)`
        ... to integrate the (radial) function F over the given (radial) grid, int_0^infinity dr F(r), by using a simple trapez rule;
            a value::Float64 is returned.
    """
    function Basics.integrate(sa::String, F::Array{Float64,1}, grid::Radial.Grid)
        if       sa == "function: on radial grid, Newton-Cotes"     wa = integrateOnGridNewtonCotes(F, grid)
        elseif   sa == "function: on radial grid, Simpson rule"     wa = integrateOnGridSimpsonRule(F, grid)
        elseif   sa == "function: on radial grid, trapez rule"      wa = integrateOnGridTrapezRule( F, grid)
        else     error("Unsupported keystring = $sa.")
        end

    end

    """
    `Basics.integrateOnGridNewtonCotes()`  ... integrates by using a 5-point Newton-Cotes integration formula; see Basic.integrate().
    """
    function Basics.integrateOnGridNewtonCotes(F::Array{Float64,1}, grid::Radial.Grid)
        coefficients = Array{Float64}([2 * 7, 32, 12, 32, 7]) * 2 / 45 * grid.h
        n = size(coefficients, 1)

        result = 0.;    i0     = 1

        while  abs(F[i0]) == 0
            i0 += 1
        end
        gamma  = log(F[i0]/F[i0+1] * grid.rp[i0+1]/grid.rp[i0]) / log(grid.r[i0]/grid.r[i0+1])
        result = 1/(gamma + 1) * grid.r[i0] * F[i0] / grid.rp[i0]

        result += F[i0] * coefficients[n]
        for i = i0+1:size(F, 1)
            result += coefficients[(i - i0) % (n - 1) + 1] * F[i]
        end

        if (size(F, 1) - i0) % (n - 1) + 1 == 1
            result -= coefficients[n] * F[size(F, 1)]
        end

        return( result )
    end

    """
    `Basics.integrateOnGridSimpsonRule()`  ... integrates by using Simpson's rule; see Basics.integrate().
    """
    function Basics.integrateOnGridSimpsonRule(F::Array{Float64,1}, grid::Radial.Grid)
        coefficients = Array{Float64}([2 * 1, 4, 1]) / 3 * grid.h
        n = size(coefficients, 1)

        result = 0.
        result += F[1] * coefficients[n]
        for i = 2:size(F, 1)
            result += coefficients[(i - 1) % (n - 1) + 1] * F[i]
        end

        if (size(F, 1) - 1) % (n - 1) + 1 == 1
            result -= coefficients[n] * F[size(F, 1)]
        end

        return( result )
    end

    """
    `Basics.integrateOnGridTrapezRule()`  ... integrates by using a simple trapez rule; see Basics.integrate().
    """
    function Basics.integrateOnGridTrapezRule(F::Array{Float64,1}, grid::Radial.Grid)
        coefficients = Array{Float64}([2 * 1, 1]) / 2 * grid.h
        n = size(coefficients, 1)

        result = 0.

        result += F[1] * coefficients[n]
        for i = 2:size(F, 1)
            result += coefficients[(i - 1) % (n - 1) + 1] * F[i]
        end

        if (size(F, 1) - 1) % (n - 1) + 1 == 1
            result -= coefficients[n] * F[size(F, 1)]
        end

        return( result )
    end

    function Base.isless(x::ManyElectron.Level, y::ManyElectron.Level)
        return x.energy < y.energy
    end

    function Base.isless(x::Hfs.IJF_Level, y::Hfs.IJF_Level)
        return x.energy < y.energy
    end

    function Base.isless(x::Basics.AngularJ64, y::Basics.AngularJ64)
        return x.num < y.num
    end

    function Base.isless(x::Cascade.AbsorptionCrossSection, y::Cascade.AbsorptionCrossSection)
        return x.photonEnergy < y.photonEnergy
    end

    """
    `Basics.isSimilar()`  ... returns true if two instances are similar to each other, and false otherwise.

    + `(keya::LevelKey, keyb::LevelKey, relAcc::Float64)`
        ... returns true if two level keys refer to the same level, i.e. level with the same symmetry and
            if the relative energy abs( (E_a - E_b)/E_a ) < relAcc. It returns false otherwise.
    """
    function Basics.isSimilar(keya::LevelKey, keyb::LevelKey, relAcc::Float64)
        if  keya.sym == keyb.sym   &&   abs( (keya.energy - keyb.energy)/keya.energy ) < relAcc    return(true)
        else                                                                                return(false)
        end
    end

    """
    `Basics.isSymmetric(matrix::Array{Float64,2})`
        ... returns true if the matrix is symmetric and false otherwise; if not symmetric, the function prints the
            first 10 pairs  M_ij, M_ji  which violate the symmetry condition.
    """
    function Basics.isSymmetric(matrix::Array{Float64,2})
        symmetric = true;    na = 0
        if  size(matrix,1) !=  size(matrix,2)  error("not quadratic")   end
        #
        for j = 1:size(matrix,2)
            for i = j+1:size(matrix,1)
                if abs( matrix[i,j] - matrix[j,i]) > 1.0e-10
                    na = na + 1;   symmetric = false;   println("*** $i  $j  $(matrix[i,j])  $(matrix[j,i]) ")
                    if  na >= 10   return( symmetric )  end
                end
            end
        end

        return( symmetric )
    end

    """
    `Basics.isZero(matrix::Array{Float64,2})`
        ... returns true if the matrix is zero and false otherwise; it sums up the |M[i,j]^2 and prints the results.
            If not zero, the function prints the first 10 matrix elements  M_ij  which violate this condition.
    """
    function Basics.isZero(matrix::Array{Float64,2})
        isz = true;    na = 0;   wa = 0.
        #
        for j = 1:size(matrix,2)
            for i = 1:size(matrix,1)
                wa = wa + abs( matrix[i,j])^2
                #
                if abs( matrix[i,j]) > 1.0e-10
                    na = na + 1;   isz = false;   println("*** $i  $j  $(matrix[i,j]) ")
                    if  na >= 10   return( isz )    end
                end
            end
        end
        println(">> isZero():   sum^2 = $(wa)")

        return( isz )
    end

    """
    `Basics.isViolated()`  ... returns true if some rule/limitation is violated, and false otherwise.

    + `(conf::Configuration, restriction::AbstractConfigurationRestriction)`
        ... returns true if restriction is violated by the given conf, and false otherwise.
    """
    function Basics.isViolated(conf::Configuration, restriction::AbstractConfigurationRestriction)
        wa = false;  ne = 0
        if      typeof(restriction) == RestrictNoElectronsTo
            for (sh,v) in  conf.shells
                if  sh.n >= restriction.nmin    ||    sh.l >= restriction.lmin    ne = ne + v   end
            end
            if  ne > restriction.ne                                   wa = true   end
        elseif  typeof(restriction) == RestrictParity
            if  Basics.determineParity(conf) != restriction.parity    wa = true   end
        elseif  typeof(restriction) == RestrictToShellDoubles
            for (sh,v) in  conf.shells
                if  sh.n >= restriction.nmin
                    if  sh.l >= restriction.lmin
                        println("restrictions:  $sh  $(sh.n)  $v  $(restriction.nmin)")
                        if !(v in [0,2])                              wa = true   end
                    end
                end
            end
        elseif  typeof(restriction) == RequestMinimumOccupation
            for (sh,v) in  conf.shells
                if  sh in restriction.shells   ne = ne + v     end
            end
            if  ne < restriction.ne                                   wa = true   end
        elseif  typeof(restriction) == RequestMaximumOccupation
            for (sh,v) in  conf.shells
                if  sh in restriction.shells   ne = ne + v     end
            end
            if  ne > restriction.ne                                   wa = true   end
        else    error("stop a")
        end

        return( wa )
    end

    """
    `Basics.isStandardSubshellList(basis::Basis)`
        ... returns true if the subshell list basis.subshells is in standard order, and false otherwise.
            It requests especially that both subshells of the same shell (n,l) occur in the sequence j = l-1/2, j = l+1/2
            and that l increases before n increases.
    """
    function Basics.isStandardSubshellList(basis::Basis)
        function  decimal(n,l,jnum)
            return( 10000n + 100l + jnum)
        end
        subshells = basis.subshells
        na = subshells[1].n;    la = Basics.subshell_l(subshells[1]);    ja = Basics.subshell_j(subshells[1])

        for  i = 2:length(subshells)
            nb = subshells[i].n;    lb = Basics.subshell_l(subshells[i]);    jb = Basics.subshell_j(subshells[1])
            if  decimal(nb,lb,jb.num) <= decimal(na,la,ja.num)    return( false )   end
        end

        return( true )
    end

    """
    Basics.merge(bases::Array{Basis,1})`
        ... to merge two (or more) atomic bases into a single basis::Basis. This method assumes the same number of electrons in all basis and
            that the subshell lists are the same or can be made 'consistent' to each other. Two bases have a consistent subshell list if all
            subshells, that appear in any of the two lists appear always in the same sequence (if they are not missing at all). In the merged
            basis, the radial orbitals are taken from the basis (in the bases-array}, from where they are found first.
    """
    function Basics.merge(bases::Array{Basis,1})

        if  length(bases) > 2
            println("Number of bases = $(length(bases)) ...")
            bs       = Basics.merge([ bases[1], bases[2] ])
            basesNew = [bs]
            for i = 3:length(bases)    push!( basesNew, bases[i])    end
            Basics.merge(basesNew)
        elseif  length(bases) == 1    return( basis[1] )
        elseif  length(bases) == 2
            # This is the essential step here; first create a consistent subshell list
            basisA = bases[1];    basisB = bases[2]
            wa = deepcopy( basisA.subshells );   wb = [ 100i  for i = 1:length(wa) ]
            # Add the subshells of basisB
            for  i = 1:length( basisB.subshells )
                if    basisB.subshells[i] in wa
                else  push!( wa, basisB.subshells[i] );    push!( wb, wb[i-1]+1 )
                end
            end
            # Now sort wb and create a new subshell list from wa due to this sorting
            wc = sortperm( wb )
            newSubshells = Subshell[]
            for  i in wc   push!( newSubshells, wa[i] )    end
            println("Sorted subshell list: $(newSubshells) ")
            error("Not yet fully implemented, see source code ")
            # Transform all CSF from basisA and basisB to newSubshells ... and apply CsfRExcludeDouble()
            # return new basis
            #
        else    error("stop a")
        end
    end

    """
    `Basics.merge(multiplets::Array{Multiplet,1})`
        ... to merge two (or more) atomic multiplets into a single multiplet::Multiplet. This method assumes (and checks) that all
            levels have level.hasStateRep = true and that all levels refer to the same basis.
    """
    function Basics.merge(multiplets::Array{Multiplet,1})

    if      length(multiplets) == 1    return( multiplets[1] )
        elseif  length(multiplets) == 2
            levels = copy(multiplets[1].levels);   basis = multiplets[1].levels[1].basis
            for  lev  in  multiplets[2].levels
                if  !(lev.hasStateRep)   ||   lev.basis != basis    error("Levels of multiplets do not refer to the same basis.")   end
                push!(levels, lev)
            end
            mp = Multiplet( multiplets[1].name * "+" * multiplets[2].name, levels )
            return( mp )
        elseif  length(multiplets) > 2
            mp = Basics.merge([ multiplets[1], multiplets[2] ])
            for  k = 3:length(multiplets)
                mp = Basics.merge([ mp, multiplets[k] ])
            end
            return( mp )
        else    error("stop a")
        end
    end

    """
    `Basics.modifyLevelEnergies(multiplet::Multiplet)`
        ... to shift the total energies of the atomic levels in multiplet interactively; a (new) multiplet::Multiplet is
            returned in which the total energies are just modified accordingly. **Not yet implemented !**
    """
    function Basics.modifyLevelEnergies(multiplet::Multiplet)
        error("Not yet implemented !")

        return( nothing )
    end

    # using Plots
    # pyplot()

    using RecipesBase

    """
    `Basics.plot()`  ... plots various quantities, often in a new window.

    + `("radial potentials", potentials::Array{Radial.Potential,1}, grid::Radial.Grid; N::Int64 = 0)`
        ... to plot one or more radial potentials, and where N::Int64 describes the number of grid points to be considered.
            call:  using Plots; pyplot()    ... to access this method by plot(...)
    """
    function Basics.plot(sa::String, potentials::Array{Radial.Potential,1}, grid::Radial.Grid; N::Int64 = 0)
        error("call instead:  using Plots; pyplot()    ... to access this method simply by plot(...)")
    end

    @recipe function f(sa::String, potentials::Array{Radial.Potential,1}, grid::Radial.Grid; N = 0)
        !(sa == "radial potentials")   &&   error("Unsupported keystring = $sa")
        wa = Float64[];   wc = [NaN for i=1:N];   labels = String[];   np = length(potentials)
        for  pot in potentials
            wb = wc
            nx = min(length(pot.Zr), N);   wb[1:nx] = pot.Zr[1:nx]
            append!(wa, wb)
            push!(labels, pot.name)
        end
        x = grid.r[1:N];     y = reshape(wa, (N, np))
        label --> reshape(labels, (1,np))
        x, y
    end

    """
    + `("radial orbitals: large", orbitals::Array{Radial.Orbital,1}, grid::Radial.Grid; N::Int64 = 0)`
        ... to plot the large component of one or more radial orbitals, and where N::Int64 describes the number of grid
            points to be considered.
    + `("radial orbitals: small", orbitals::Array{Radial.Orbital,1}, grid::Radial.Grid; N::Int64 = 0)`
        ... to plot the small component of one or more radial orbitals.
    + `("radial orbitals: both",  orbitals::Array{Radial.Orbital,1}, grid::Radial.Grid; N::Int64 = 0)`
        ... to plot the large and small component of one or more radial orbitals.

        call:  using Plots; pyplot()    ... to access this method by plot(...)
    """
    function Basics.plot(sa::String, orbitals::Array{Radial.Orbital,1}, grid::Radial.Grid; N = 0)
        error("call instead:  using Plots; pyplot()    ... to access this method simply by plot(...)")
    end

    @recipe function f(sa::String, orbitals::Array{Radial.Orbital,1}, grid::Radial.Grid; N = 0)
        wa = Float64[];   wc = [NaN for i=1:N];   labels = String[];   np = length(orbitals)

        if       sa == "radial orbitals: large"
            for  orb in orbitals
                wb = wc
                nx = min(length(orb.P), N);   wb[1:nx] = orb.P[1:nx]
                append!(wa, wb)
                push!(labels, "$(orb.subshell):large")
            end
            x = grid.r[1:N];     y = reshape(wa, (N, np))

        elseif   sa == "radial orbitals: small"
            for  orb in orbitals
                wb = wc
                nx = min(length(orb.Q), N);   wb[1:nx] = orb.Q[1:nx]
                append!(wa, wb)
                push!(labels, "$(orb.subshell):small")
            end
            x = grid.r[1:N];     y = reshape(wa, (N, np))

        elseif   sa == "radial orbitals: both"
            for  orb in orbitals
                wb = wc;    nx = min(length(orb.P), N);   wb[1:nx] = orb.P[1:nx];    append!(wa, wb)
                wb = wc;    nx = min(length(orb.Q), N);   wb[1:nx] = orb.Q[1:nx];    append!(wa, wb)
                push!(labels, "$(orb.subshell):large");   push!(labels, "$(orb.subshell):small")
            end
             x = grid.r[1:N];     y = reshape(wa, (N, 2np))

        else   error("Unsupported keystring = $sa")
        end
        label --> permutedims(labels)
        x, y
    end

    """
    + `("spectrum: transition rates over energy", lines::Array{PhotoEmission.Line,1})`
        ... to plot the transition rates of all lines as function of their transition energies. The plot is shown in a new
            window but nothing is returned otherwise. **Not yet implemented !**

    + `("spectrum: oscillator strength over energy, emission", lines::Array{PhotoEmission.Line,1})` or
        `("spectrum: oscillator strength over energy, absorption", lines::Array{PhotoEmission.Line,1})`
        ... to plot the absorption oscillator strength of all lines as function of their transition energies. Again, a new
            window is opened but nothing returned by this method. **Not yet implemented !**
    """
    function Basics.plot(sa::String, lines::Array{PhotoEmission.Line,1})
        error("call instead:  using Plots; pyplot()    ... to access this method simply by plot(...) ... not yet implemented !")
    end

    """
    + `("spectrum: transition rates over energy, Gaussian", lines::Array{PhotoEmission.Line,1}; widths=value::Float64)` or
        `("spectrum: transition rates over energy, Lorentzian", lines::Array{PhotoEmission.Line,1}; widths=value::Float64)`
        ... to plot the transition rates of all lines as function of their transition energies but with a Gaussian or Lorentzian
            distribution. Again, a new window is opened but nothing returned by this method. It still need to be decided how
            the widths (and, perhaps, other parameters) will be communicated to the method. **Not yet implemented !**
    """
    function Basics.plot(sa::String, lines::Array{PhotoEmission.Line,1}, widths::Float64)
        error("call instead:  using Plots; pyplot()    ... to access this method simply by plot(...) ... not yet implemented !")
    end


@testset "runtestWarningCountdown" begin
    println("Testing runtestWarningCountdown")
    include("../src/bfgaWarningCountdown.jl")
    using .bfgaWarningCountdown

    include("../src/Bf.jl")
    using .BfInterpreter

    @time model = test_serial()
    #@time model = test_parallel()
    prems = model.population[length(model.population)]
    println("Generation : $(model.params.currentGeneration)")
    println("& fitness : $(model.specific_fitness.fitness(prems, model.instructionsSet))")
    #bfcode :: Array{Float64,1}  = prems.dna
    println("code :  $(prems.program)") #$(join(genesToBfInstructions(bfcode), "") ) ")

    for elt in _trainingExamples
        output, _ = execute(prems.program, elt , model.instructionsSet)
        @show join(output, "")
        @test length(output) >= l_length * (target_length +1)
    end

    # do the test_parallel after ...
    #test_parallel()
end


# testing

using StaticArrays

abstract type AbstractTet end

struct BoundaryTet <: AbstractTet
end

struct Tetrahedron <: AbstractTet
    vertices::MVector{4,Int}
    neighbors::MVector{4,Int}
end

Tetrahedron() = Tetrahedron([-1,-1,-1,-1],[-1,-1,-1,-1])


## This file contains the exchange and exch_UV functions
# along with grid-specific methods (exch_T_N.jl, etc.)

## User Front Ends

"""
    exchange(fld::MeshArray)

Exchange / transfer data between neighboring arrays. Other methods are

    exchange(fld::MeshArray,N::Integer)
    exchange(u::MeshArray,v::MeshArray)
    exchange(u::MeshArray,v::MeshArray,N::Integer)
"""
function exchange(fld::MeshArray)
  FLD=exch_T_N(fld,1);
end

function exchange(fld::MeshArray,N::Integer)
  FLD=exch_T_N(fld,N);
end

function exchange(u::MeshArray,v::MeshArray)
  (uex,vex)=exch_UV_N(u,v,1);
end

function exchange(u::MeshArray,v::MeshArray,N::Integer)
  (uex,vex)=exch_UV_N(u,v,N);
end

## dispatch over grid types

#note: the "CubeSphere" implementation covers both cs and llc

function exch_T_N(fld,N)

if fld.grid.class=="LatLonCap"
  FLD=exch_T_N_cs(fld,N)
elseif fld.grid.class=="CubeSphere"
  FLD=exch_T_N_cs(fld,N)
elseif fld.grid.class=="PeriodicChannel"
  FLD=exch_T_N_ll(fld,N)
elseif fld.grid.class=="PeriodicDomain"
  FLD=exch_T_N_dpdo(fld,N)
else
  error("unknown grid.class case")
end

return FLD

end

function exch_UV_N(u,v,N)

if u.grid.class=="LatLonCap"
  (uex,vex)=exch_UV_N_cs(u,v,N)
elseif u.grid.class=="CubeSphere"
  (uex,vex)=exch_UV_N_cs(u,v,N)
elseif u.grid.class=="PeriodicChannel"
  (uex,vex)=exch_UV_N_ll(u,v,N)
elseif u.grid.class=="PeriodicDomain"
  (uex,vex)=exch_UV_N_dpdo(u,v,N)
else
  error("unknown grid.class case")
end

return uex,vex

end

function exch_UV(u,v)

if u.grid.class=="LatLonCap"
  (uex,vex)=exch_UV_cs(u,v)
elseif u.grid.class=="CubeSphere"
  (uex,vex)=exch_UV_cs(u,v)
elseif u.grid.class=="PeriodicChannel"
  (uex,vex)=exch_UV_ll(u,v)
elseif u.grid.class=="PeriodicDomain"
  (uex,vex)=exch_UV_dpdo(u,v)
else
  error("unknown grid.class case")
end

return uex,vex

end

## Grid-specific implementations: PeriodicDomain case

function exch_T_N_dpdo(fld::MeshArray,N::Integer)

fillval=0.0

ni,nj=Int.(fld.grid.ioSize[:]./fld.grid.fSize[1][:])
s=fld.fSize
FLD=similar(fld;m=fld.meta);

for i=1:ni
  for j=1:nj
    k=i+ni*(j-1)
    kS=j-1; kS==0 ? kS=nj : nothing; kS=i+ni*(kS-1)
    kN=j+1; kN==nj+1 ? kN=1 : nothing; kN=i+ni*(kN-1)
    kW=i-1; kW==0 ? kW=ni : nothing; kW=kW+ni*(j-1)
    kE=i+1; kE==ni+1 ? kE=1 : nothing; kE=kE+ni*(j-1)

    #step 1

    FLD.f[k]=fill(fillval,s[k].+2N);
    @views FLD.f[k][N+1:N+s[k][1],N+1:N+s[k][2]]=fld.f[k];

    #step 2

    iW=(s[k][1]-N+1:s[k][1],1:s[k][2]);
    iE=(1:N,1:s[k][2]);
    jW=(1:N,N+1:N+s[k][2]);
    jE=(N+1+s[k][1]:2N+s[k][1],N+1:N+s[k][2]);
    FLD.f[k][jW[1],jW[2]]=view(fld.f[kW],iW[1],iW[2])
    FLD.f[k][jE[1],jE[2]]=view(fld.f[kE],iE[1],iE[2])

    #step 3

    iS=(1:s[k][1],s[k][2]-N+1:s[k][2]);
    iN=(1:s[k][1],1:N);
    jS=(N+1:N+s[k][1],1:N);
    jN=(N+1:N+s[k][1],N+1+s[k][2]:2N+s[k][2]);
    FLD.f[k][jS[1],jS[2]]=view(fld.f[kS],iS[1],iS[2])
    FLD.f[k][jN[1],jN[2]]=view(fld.f[kN],iN[1],iN[2])

  end
end

return FLD

end

##

function exch_UV_N_dpdo(fldU,fldV,N);

FLDU=exch_T_N_dpdo(fldU,N);
FLDV=exch_T_N_dpdo(fldV,N);

return FLDU,FLDV

end

##

function exch_UV_dpdo(fldU,fldV);

fillval=0.0

ni,nj=Int.(fldU.grid.ioSize[:]./fldU.grid.fSize[1][:])
s=fldU.fSize
FLDU=similar(fldU;m=fldU.meta)
FLDV=similar(fldV;m=fldV.meta)

for i=1:ni
  for j=1:nj
    k=i+ni*(j-1)
    kS=j-1; kS==0 ? kS=nj : nothing; kS=i+ni*(kS-1)
    kN=j+1; kN==nj+1 ? kN=1 : nothing; kN=i+ni*(kN-1)
    kW=i-1; kW==0 ? kW=ni : nothing; kW=kW+ni*(j-1)
    kE=i+1; kE==ni+1 ? kE=1 : nothing; kE=kE+ni*(j-1)

    #step 1

    FLDU.f[k]=fill(fillval,s[k][1]+1,s[k][2]);
    FLDV.f[k]=fill(fillval,s[k][1],s[k][2]+1);
    @views FLDU.f[k][1:s[k][1],1:s[k][2]]=fldU.f[k];
    @views FLDV.f[k][1:s[k][1],1:s[k][2]]=fldV.f[k];

    #step 2

    FLDU.f[k][s[k][1]+1,1:s[k][2]]=view(fldU.f[kE],1,1:s[k][2])

    #step 3

    FLDV.f[k][1:s[k][1],s[k][2]+1]=view(fldV.f[kN],1:s[k][1],1)

  end
end

return FLDU,FLDV

end

## Grid-specific implementations: PeriodicChannel case

function exch_T_N_ll(fld::MeshArray,N::Integer)

fillval=0.0

#step 1

s=size.(fld.f);
FLD=similar(fld;m=fld.meta);
FLD.f[1]=fill(fillval,s[1].+2N);
@views FLD.f[1][N+1:N+s[1][1],N+1:N+s[1][2]]=fld.f[1];

#step 2

iW=(s[1][1]-N+1:s[1][1],1:s[1][2]);
iE=(1:N,1:s[1][2]);
jW=(1:N,N+1:N+s[1][2]);
jE=(N+1+s[1][1]:2N+s[1][1],N+1:N+s[1][2]);
FLD.f[1][jW[1],jW[2]]=view(fld.f[1],iW[1],iW[2])
FLD.f[1][jE[1],jE[2]]=view(fld.f[1],iE[1],iE[2])

return FLD

end

##

function exch_UV_N_ll(fldU,fldV,N);

FLDU=exch_T_N_ll(fldU,N);
FLDV=exch_T_N_ll(fldV,N);

return FLDU,FLDV

end

##

function exch_UV_ll(fldU,fldV);

fillval=0.0

#step 1

s=size.(fldU.f);
FLDU=similar(fldU;m=fldU.meta);
FLDV=similar(fldV;m=fldV.meta);

FLDU.f[1]=fill(fillval,s[1][1]+1,s[1][2]);
FLDV.f[1]=fill(fillval,s[1][1],s[1][2]+1);
@views FLDU.f[1][1:s[1][1],1:s[1][2]]=fldU.f[1];
@views FLDV.f[1][1:s[1][1],1:s[1][2]]=fldV.f[1];

#step 2

FLDU.f[1][s[1][1]+1,1:s[1][2]]=view(fldU.f[1],1,1:s[1][2])

return FLDU,FLDV

end

## Grid-specific implementations: CubeSphere & LatLonCap case

#note: the "CubeSphere" implementation covers both cs and llc

function exch_T_N_cs(fld::MeshArray,N::Integer)

fillval=0.0

#step 1

s=size.(fld.f)
nf=fld.grid.nFaces
nf==5 ? s=vcat(s,s[3]) : nothing
tp=fld.grid.class
FLD=similar(fld;m=fld.meta)

for i=1:nf; FLD.f[i]=fill(fillval,s[i].+2N); end;
#code below yields strange, seemingly incorrect results:
#for i=1:nf; FLD.f[i]=Array{eltype(fld.f[i])}(undef,s[i].+2N); end;

#all versions below yield same @time and memory (despite diff in allocs)
for i=1:nf;
# FLD.f[i][N+1:end-N,N+1:end-N]=fld.f[i];
 @views FLD.f[i][N+1:N+s[i][1],N+1:N+s[i][2]]=fld.f[i];
end;

#step 2

(ovfW,ovfE,ovfS,ovfN,evfW,evfE,evfS,evfN)=exch_cs_viewfunctions();

for a=1:nf
(jW, jE, jS, jN)=exch_cs_target(s[a],N)
(aW,aE,aS,aN,iW,iE,iS,iN)=exch_cs_sources(a,s,N)
if !iseven(a)
 aW <= nf ? FLD.f[a][jW[1],jW[2]]=ovfW(fld.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLD.f[a][jE[1],jE[2]]=ovfE(fld.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLD.f[a][jS[1],jS[2]]=ovfS(fld.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLD.f[a][jN[1],jN[2]]=ovfN(fld.f[aN],iN[1],iN[2]) : nothing
else
 aW <= nf ? FLD.f[a][jW[1],jW[2]]=evfW(fld.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLD.f[a][jE[1],jE[2]]=evfE(fld.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLD.f[a][jS[1],jS[2]]=evfS(fld.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLD.f[a][jN[1],jN[2]]=evfN(fld.f[aN],iN[1],iN[2]) : nothing
end
end

return FLD

end

##

function exch_UV_N_cs(fldU::MeshArray,fldV::MeshArray,N::Integer)

fillval=0.0

#step 1

s=size.(fldU.f)
nf=fldU.grid.nFaces
nf==5 ? s=vcat(s,s[3]) : nothing
tp=fldU.grid.class
FLDU=similar(fldU;m=fldU.meta)
FLDV=similar(fldV;m=fldV.meta)

for i=1:nf;
 FLDU.f[i]=fill(fillval,s[i].+2N);
 FLDV.f[i]=fill(fillval,s[i].+2N);
 @views FLDU.f[i][N+1:N+s[i][1],N+1:N+s[i][2]]=fldU.f[i];
 @views FLDV.f[i][N+1:N+s[i][1],N+1:N+s[i][2]]=fldV.f[i];
end;

#step 2

(ovfW,ovfE,ovfS,ovfN,evfW,evfE,evfS,evfN)=exch_cs_viewfunctions();

for a=1:nf
(jW, jE, jS, jN)=exch_cs_target(s[a],N)
(aW,aE,aS,aN,iW,iE,iS,iN)=exch_cs_sources(a,s,N)
if !iseven(a)
 aW <= nf ? FLDU.f[a][jW[1],jW[2]]=ovfW(fldV.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLDU.f[a][jE[1],jE[2]]=ovfE(fldU.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLDU.f[a][jS[1],jS[2]]=ovfS(fldU.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLDU.f[a][jN[1].+1,jN[2]]=-ovfN(fldV.f[aN],iN[1],iN[2]) : nothing
 aW <= nf ? FLDV.f[a][jW[1],jW[2].+1]=-ovfW(fldU.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLDV.f[a][jE[1],jE[2]]=ovfE(fldV.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLDV.f[a][jS[1],jS[2]]=ovfS(fldV.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLDV.f[a][jN[1],jN[2]]=ovfN(fldU.f[aN],iN[1],iN[2]) : nothing
else
 aW <= nf ? FLDU.f[a][jW[1],jW[2]]=evfW(fldU.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLDU.f[a][jE[1],jE[2]]=evfE(fldV.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLDU.f[a][jS[1].+1,jS[2]]=-evfS(fldV.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLDU.f[a][jN[1],jN[2]]=evfN(fldU.f[aN],iN[1],iN[2]) : nothing
 aW <= nf ? FLDV.f[a][jW[1],jW[2]]=evfW(fldV.f[aW],iW[1],iW[2]) : nothing
 aE <= nf ? FLDV.f[a][jE[1],jE[2].+1]=-evfE(fldU.f[aE],iE[1],iE[2]) : nothing
 aS <= nf ? FLDV.f[a][jS[1],jS[2]]=evfS(fldU.f[aS],iS[1],iS[2]) : nothing
 aN <= nf ? FLDV.f[a][jN[1],jN[2]]=evfN(fldV.f[aN],iN[1],iN[2]) : nothing
end
end

return FLDU,FLDV

end

##

function exch_UV_cs(fldU::MeshArray,fldV::MeshArray)

fillval=0.0

#step 1

s=size.(fldU.f)
nf=fldU.grid.nFaces
nf==5 ? s=vcat(s,s[3]) : nothing
tp=fldU.grid.class
FLDU=similar(fldU;m=fldU.meta)
FLDV=similar(fldV;m=fldV.meta)

for i=1:nf
  FLDU.f[i]=fill(fillval,s[i][1]+1,s[i][2]);
  FLDV.f[i]=fill(fillval,s[i][1],s[i][2]+1);
  @views FLDU.f[i][1:s[i][1],1:s[i][2]]=fldU.f[i];
  @views FLDV.f[i][1:s[i][1],1:s[i][2]]=fldV.f[i];
end

 #step 2

(ovfW,ovfE,ovfS,ovfN,evfW,evfE,evfS,evfN)=exch_cs_viewfunctions();

for a=1:nf
(jW, jE, jS, jN)=exch_cs_target(s[a],1)
(aW,aE,aS,aN,iW,iE,iS,iN)=exch_cs_sources(a,s,1)
if !iseven(a)
 aE <= nf ? FLDU.f[a][jE[1].-1,jE[2].-1]=ovfE(fldU.f[aE],iE[1],iE[2]) : nothing
 aN <= nf ? FLDV.f[a][jN[1].-1,jN[2].-1]=ovfN(fldU.f[aN],iN[1],iN[2]) : nothing
else
 aE <= nf ? FLDU.f[a][jE[1].-1,jE[2].-1]=evfE(fldV.f[aE],iE[1],iE[2]) : nothing
 aN <= nf ? FLDV.f[a][jN[1].-1,jN[2].-1]=evfN(fldV.f[aN],iN[1],iN[2]) : nothing
end
end

return FLDU,FLDV

end

## Convenience functions used in the cs & llc case

function exch_cs_target(sa::Tuple{Int64,Int64},N::Integer)

    #target array indices
    jW=(1:N,N+1:N+sa[2]);
    jE=(N+1+sa[1]:2N+sa[1],N+1:N+sa[2]);
    jS=(N+1:N+sa[1],1:N);
    jN=(N+1:N+sa[1],N+1+sa[2]:2N+sa[2]);

    return jW, jE, jS, jN

end

function exch_cs_sources(a::Integer,s::Array{Tuple{Int64,Int64},1},N::Integer)

#source array IDs
aW=0; aE=0; aS=0; aN=0;
if a==1;     aW=5; aE=2; aS=6; aN=3;
elseif a==2; aW=1; aE=4; aS=6; aN=3;
elseif a==3; aW=1; aE=4; aS=2; aN=5;
elseif a==4; aW=3; aE=6; aS=2; aN=5;
elseif a==5; aW=3; aE=6; aS=4; aN=1;
elseif a==6; aW=5; aE=2; aS=4; aN=1;
else; error("Array index is out of bounds.");
end;

if !iseven(a)
    #source array indices
    iW=(1:s[aW][1],s[aW][2]-N+1:s[aW][2]);
    iE=(1:N,1:s[aE][2]);
    iS=(1:s[aS][1],s[aS][2]-N+1:s[aS][2]);
    iN=(1:N,1:s[aN][2]);
else
    #source array indices
    iW=(s[aW][1]-N+1:s[aW][1],1:s[aW][2]);
    iE=(1:s[aE][1],1:N);
    iS=(s[aS][1]-N+1:s[aS][1],1:s[aS][2]);
    iN=(1:s[aN][1],1:N);
end

return aW,aE,aS,aN,iW,iE,iS,iN
end

function exch_cs_viewfunctions()
#view functions for odd numbered arrays
ovfW(x,i,j)=PermutedDimsArray(view(x,reverse(i),j),(2,1))
ovfE(x,i,j)=view(x,i,j)
ovfS(x,i,j)=view(x,i,j)
ovfN(x,i,j)=PermutedDimsArray(view(x,i,reverse(j)),(2,1))
#view functions for even numbered arrays
evfW(x,i,j)=view(x,i,j)
evfE(x,i,j)=PermutedDimsArray(view(x,reverse(i),j),(2,1))
evfS(x,i,j)=PermutedDimsArray(view(x,i,reverse(j)),(2,1))
evfN(x,i,j)=view(x,i,j)
#
return ovfW,ovfE,ovfS,ovfN,evfW,evfE,evfS,evfN
end

##


push!(LOAD_PATH, ".")

module Fitness

include("utility.jl")

using Individual
using RealIndividual

abstract type AbstractFitness{N} end

@define Fitness begin
  direction::NTuple{N, Int64} where N
end

function getSize(fit::AbstractFitness{N}) where N
  return N
end

function getDirection(fit::AbstractFitness)
  return fit.direction
end

end


struct StateSpaceSolution{T,N,uType,uType2,DType,tType,randType,P,A,IType,DE,PosteriorType,
                          logpdfType,zType} <: SciMLBase.AbstractRODESolution{T,N,uType}
    u::uType
    u_analytic::uType2
    errors::DType
    t::tType
    W::randType
    prob::P
    alg::A
    interp::IType
    dense::Bool
    tslocation::Int
    destats::DE
    retcode::Symbol
    P::PosteriorType
    logpdf::logpdfType
    z::zType
end

function build_solution(prob::AbstractStateSpaceProblem, alg, t, u; P = nothing, logpdf = nothing,
                        W = nothing, timeseries_errors = length(u) > 2, dense = false,
                        dense_errors = dense, calculate_error = true,
                        interp = SciMLBase.ConstantInterpolation(t, u), retcode = :Default,
                        destats = nothing, z = nothing, kwargs...)
    T = eltype(eltype(u))
    N = length((size(prob.u0)..., length(u)))

    # TODO: add support for has_analytic in the future
    sol = StateSpaceSolution{T,N,typeof(u),Nothing,Nothing,typeof(t),typeof(W),typeof(prob),
                             typeof(alg),typeof(interp),typeof(destats),typeof(P),typeof(logpdf),
                             typeof(z)}(u, nothing, nothing, t, W, prob, alg, interp, dense, 0,
                                        destats, retcode, P, logpdf, z)
    return sol
end

# Just using ConstantInterpolation for now.  Worth specializing?
# (sol::StateSpaceSolution)(t, ::Type{deriv} = Val{0}; idxs = nothing, continuity = :left) where {deriv} = _interpolate(sol, t, idxs)
# _interpolate(sol::StateSpaceSolution, t::Integer, idxs::Nothing) = sol.u[t]
# _interpolate(sol::StateSpaceSolution, t::Number, idxs::Nothing) = sol.u[Integer(round(t))]
# _interpolate(sol::StateSpaceSolution, t::Integer, idxs) = sol.u[t][idxs]

# For recipes
SciMLBase.getindepsym(sol::StateSpaceSolution) = :t
SciMLBase.getindepsym_defaultt(sol::StateSpaceSolution) = :t


# This file is a part of Julia. License is MIT: http://julialang.org/license

# The Computer Language Benchmarks Game
# http://shootout.alioth.debian.org/
#
# Contributed by David Campbell
# Based on the Go version

import Base.isless

function count(data::AbstractString, n::Int)
    counts = Dict{AbstractString, Int}()
    top = length(data) - n + 1
    for i = 1:top
        s = data[i : i+n-1]
        if haskey(counts, s)
            counts[s] += 1
        else
            counts[s] = 1
        end
    end
    counts
end

function count_one(data::AbstractString, s::AbstractString)
    count(data, length(s))[s]
end

type KNuc
    name::AbstractString
    count::Int
end

# sort down
function isless(x::KNuc, y::KNuc)
    if x.count == y.count
        return x.name > y.name
    end
    x.count > y.count
end

function sorted_array(m::Dict{AbstractString, Int})
    kn = Array(KNuc, length(m))
    i = 1
    for elem in m
        kn[i] = KNuc(elem...)
        i += 1
    end
    sort(kn)
end

function print_knucs(a::Array{KNuc, 1})
    sum = 0
    for kn in a
        sum += kn.count
    end
    for kn in a
        @printf("%s %.3f\n", kn.name, 100.0kn.count/sum)
    end
    println()
end

function k_nucleotide(infile="knucleotide-input.txt")
    input = open(infile, "r")
    three = ">THREE "
    while true
        line = readline(input)
        if length(line) >= length(three) && line[1:length(three)] == three
            break
        end
    end
    data = collect(readall(input))
    # delete the newlines and convert to upper case
    i, j = 1, 1
    while i <= length(data)
        if data[i] != '\n'
            data[j] = uppercase(data[i])
            j += 1
        end
        i += 1
    end
    str = join(data[1:j-1], "")

    arr1 = sorted_array(count(str, 1))
    arr2 = sorted_array(count(str, 2))
    close(input)
end


import DataFrames
import MLBase
import StatsBase

"""
"""
function singlelabelbinaryytrue(
        labels::AbstractVector,
        positive_class::AbstractString;
        inttype::Type=Int,
        )
    if !(inttype<:Integer)
        error("!(inttype<:Integer)")
    end
    result = inttype.(labels .== positive_class)
    return result
end

"""
"""
function singlelabelbinaryyscore(
        single_labelprobabilities::AbstractDict,
        positive_class::AbstractString;
        float_type::Type{<:AbstractFloat} = Float64,
        )
    result = float_type.(single_labelprobabilities[positive_class])
    return result
end

"""
"""
function singlelabelbinaryclassificationmetrics_tunableparam(
        kwargsassoc::AbstractDict,
        )
    tunableparams = [
        :threshold,
        :sensitivity,
        :specificity,
        :maximize,
        ]
    maximizableparams = [
        :f1score,
        :cohen_kappa,
        ]
    kwargshastunableparam = [
        haskey(kwargsassoc, x) for x in tunableparams
        ]
    if sum(kwargshastunableparam) != 1
        msg = "you must specify one (and only one) of the following: " *
            join(tunableparams, ", ")
        error(msg)
    end
    if length(tunableparams[kwargshastunableparam]) != 1
        error("oh boy you definitely should never see this error message")
    end
    selectedtunableparam =
        tunableparams[kwargshastunableparam][1]
    if selectedtunableparam == :maximize
        selectedparamtomax = kwargsassoc[:maximize]
        if !in(selectedparamtomax, maximizableparams)
            msg = "Cannot max $(selectedparamtomax). Select one " *
                "of the following: " * join(maximizableparams, ", ")
            error(msg)
        end
    else
        selectedparamtomax = :notapplicable
    end
    #
    metricdisplaynames = Dict()
    metricdisplaynames[:AUPRC] = string("AUPRC")
    metricdisplaynames[:AUROCC] = string("AUROCC")
    metricdisplaynames[:AveragePrecision] = string("Average precision")
    if selectedtunableparam == :threshold
        metricdisplaynames[:threshold] = string("[fix] * Threshold")
    else
        metricdisplaynames[:threshold] = string("* Threshold")
    end
    metricdisplaynames[:accuracy] = string("* Accuracy")
    if selectedtunableparam == :maximize && selectedparamtomax ==
            :cohen_kappa
        metricdisplaynames[:cohen_kappa] =
            string("[max] * Cohen's Kappa statistic")
    else
        metricdisplaynames[:cohen_kappa] =
            string("* Cohen's Kappa statistic")
    end
    if selectedtunableparam == :maximize && selectedparamtomax ==
            :f1score
        metricdisplaynames[:f1score] = string("[max] * F1 score")
    else
        metricdisplaynames[:f1score] = string("* F1 Score")
    end
    metricdisplaynames[:precision] =
        string("* Precision (positive predictive value)")
    metricdisplaynames[:negative_predictive_value] =
        string("* Negative predictive value")
    metricdisplaynames[:recall] =
        string("* Recall (sensitivity, true positive rate)")
    if selectedtunableparam == :sensitivity
        metricdisplaynames[:sensitivity] =
            string("[fix] * Sensitivity (recall, true positive rate)")
    else
        metricdisplaynames[:sensitivity] =
            string("* Sensitivity (recall, true positive rate)")
    end
    if selectedtunableparam == :specificity
        metricdisplaynames[:specificity] =
            string("[fix] * Specificity (true negative rate)")
    else
        metricdisplaynames[:specificity] =
            string("* Specificity (true negative rate)")
    end
    metricdisplaynames = fix_type(metricdisplaynames)
    return selectedtunableparam, selectedparamtomax, metricdisplaynames
end

"""
"""
function singlelabelbinaryclassificationmetrics_resultdict(
        estimator::AbstractFittable,
        features_df::DataFrames.AbstractDataFrame,
        labels_df::DataFrames.AbstractDataFrame,
        single_label_name::Symbol,
        positive_class::AbstractString;
        kwargs...
        )
    #
    kwargsdict = Dict(kwargs)
    kwargsdict = fix_type(kwargsdict)
    selectedtunableparam, selectedparamtomax, metricdisplaynames =
        singlelabelbinaryclassificationmetrics_tunableparam(kwargsdict)
    #
    predictedprobabilitiesalllabels = predict_proba(estimator, features_df)
    yscore = Float64.(
        singlelabelbinaryyscore(
            predictedprobabilitiesalllabels[single_label_name],
            positive_class,
            )
        )
    ytrue = Int.(
        singlelabelbinaryytrue(
            labels_df[single_label_name],
            positive_class,
            )
        )
    results = Dict()
    results[:ytrue] = ytrue
    results[:yscore] = yscore
    results[:AUROCC] = aurocc(ytrue, yscore)
    results[:AUPRC] = auprc(ytrue, yscore)
    results[:AveragePrecision] = averageprecisionscore(ytrue, yscore)
    if selectedtunableparam == :threshold
        additionalthreshold = kwargsdict[:threshold]
    else
        additionalthreshold = 0.5
    end
    allrocnums, allthresholds = getallrocnums(
        ytrue,
        yscore;
        additionalthreshold = additionalthreshold,
        )
    if selectedtunableparam == :threshold
        selectedthreshold = kwargsdict[:threshold]
        bestindex = argmin(abs.(allthresholds .- selectedthreshold))
    elseif selectedtunableparam == :sensitivity
        selectedsensitivity = kwargsdict[:sensitivity]
        allsensitivity = [sensitivity(x) for x in allrocnums]
        bestindex = argmin(abs.(allsensitivity .- selectedsensitivity))
    elseif selectedtunableparam == :specificity
        selectedspecificity = kwargsdict[:specificity]
        allspecificity = [specificity(x) for x in allrocnums]
        bestindex = argmin(abs.(allspecificity .- selectedspecificity))
    elseif selectedtunableparam == :maximize
        selectedparamtomax = kwargsdict[:maximize]
        if selectedparamtomax == :f1score
            allf1score = [fbetascore(x, 1) for x in allrocnums]
            bestindex = argmin(allf1score)
        elseif selectedparamtomax == :cohen_kappa
            allcohen_kappa = [cohen_kappa(x) for x in allrocnums]
            bestindex = argmin(allcohen_kappa)
        else
            error("this is an error that should never happen")
        end
    else
        error("this is another error that should never happen")
    end

    results[:allrocnums] = allrocnums
    results[:allthresholds] = allthresholds
    results[:bestindex] = bestindex
    bestrocnums = allrocnums[bestindex]
    bestthreshold = allthresholds[bestindex]
    results[:threshold] = bestthreshold
    results[:accuracy] = accuracy(bestrocnums)
    results[:sensitivity] = sensitivity(bestrocnums)
    results[:specificity] = specificity(bestrocnums)
    results[:precision] = precision(bestrocnums)
    results[:negative_predictive_value] =
        negative_predictive_value(bestrocnums)
    results[:recall] = recall(bestrocnums)
    results[:f1score] = f1score(bestrocnums)
    results[:cohen_kappa] = cohen_kappa(bestrocnums)
    results = fix_type(results)
    return results
end

"""
"""
function singlelabelbinaryclassificationmetrics(
        estimator::AbstractFittable,
        features_df::DataFrames.AbstractDataFrame,
        labels_df::DataFrames.AbstractDataFrame,
        single_label_name::Symbol,
        positive_class::AbstractString;
        kwargs...
        )
    vectorofestimators = AbstractFittable[estimator]
    result = singlelabelbinaryclassificationmetrics(
        vectorofestimators,
        features_df,
        labels_df,
        single_label_name,
        positive_class;
        kwargs...
        )
    return result
end

"""
"""
function singlelabelbinaryclassificationmetrics(
        vectorofestimators::AbstractVector{AbstractFittable},
        features_df::DataFrames.AbstractDataFrame,
        labels_df::DataFrames.AbstractDataFrame,
        single_label_name::Symbol,
        positive_class::AbstractString;
        kwargs...
        )
    kwargsdict = Dict(kwargs)
    kwargsdict = fix_type(kwargsdict)
    selectedtunableparam, selectedparamtomax, metricdisplaynames =
        singlelabelbinaryclassificationmetrics_tunableparam(kwargsdict)
    metricsforeachestimator = [
        singlelabelbinaryclassificationmetrics_resultdict(
            est,
            features_df,
            labels_df,
            single_label_name,
            positive_class;
            kwargs...
            )
            for est in vectorofestimators
        ]
    result = DataFrames.DataFrame()
    result[:metric] = [
        metricdisplaynames[:AUPRC],
        metricdisplaynames[:AUROCC],
        metricdisplaynames[:AveragePrecision],
        metricdisplaynames[:threshold],
        metricdisplaynames[:accuracy],
        metricdisplaynames[:cohen_kappa],
        metricdisplaynames[:f1score],
        metricdisplaynames[:precision],
        metricdisplaynames[:negative_predictive_value],
        metricdisplaynames[:recall],
        metricdisplaynames[:sensitivity],
        metricdisplaynames[:specificity],
        ]
    for i = 1:length(vectorofestimators)
        result[Symbol(vectorofestimators[i].name)] = [
            metricsforeachestimator[i][:AUPRC],
            metricsforeachestimator[i][:AUROCC],
            metricsforeachestimator[i][:AveragePrecision],
            metricsforeachestimator[i][:threshold],
            metricsforeachestimator[i][:accuracy],
            metricsforeachestimator[i][:cohen_kappa],
            metricsforeachestimator[i][:f1score],
            metricsforeachestimator[i][:precision],
            metricsforeachestimator[i][:negative_predictive_value],
            metricsforeachestimator[i][:recall],
            metricsforeachestimator[i][:sensitivity],
            metricsforeachestimator[i][:specificity],
            ]
    end
    return result
end