"""
`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, ..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.
    """
    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

    """
    `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.
        + printBefore           ::Bool                 ... True, if all the requested amplitudes are printed before the computation start.
        + printAmplitudes       ::Bool                 ... True, if the amplitudes are to be printed.
    """
    struct Settings
        multipoles              ::Array{EmMultipole,1}
        gauges                  ::Array{UseGauge}
        printBefore             ::Bool
        printAmplitudes         ::Bool
    end

    """
    `StrongField.Settings()`
        ... constructor for the default values of SFA computations.
    """
    function Settings()
        Settings([E1], [UseCoulomb], false, false)
    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)  ")
    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), Level(), Level(),
                     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 energy 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

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

    ####################################################################################################################
    ######The following four functions will be replaced later and serve only for comparison with previous results#######
    ####################################################################################################################

    """
    `StrongField.HydrogenPnl(epsiloni::Float64, n::Int, l::Int, rGrid::Array{Float64,1})`
        ... returns the non-relativistic hydrogen-like radial wave function with modified binding energy epsiloni
                at the radial grid points rGrid
    """
    function  HydrogenPnl(epsiloni::Float64, n::Int, l::Int, rGrid::Array{Float64,1})
        Pnl = Float64[]
        Z = n*sqrt(-2*epsiloni)

        for j = 1:length(rGrid)
            r = rGrid[j]
            p = r * sqrt( (2*Z/n)^3 * factorial(n-l-1)/(2*n*factorial(n+l)) ) * exp(-Z*r/n) * (2*Z*r/n)^l * GSL.sf_laguerre_n(n-l-1,2*l+1,2*Z*r/n)
            push!( Pnl, p )
        end

        return( Pnl )
    end

    """
    `StrongField.HydrogenDPnlDr(epsiloni::Float64, n::Int, l::Int, rGrid::Array{Float64,1})`
        ... returns the r-derivative of the non-relativistic hydrogen-like radial wave function with modified binding energy epsiloni
                at the radial grid points rGrid
    """
    function  HydrogenDPnlDr(epsiloni::Float64, n::Int, l::Int, rGrid::Array{Float64,1})
        DPnl = Float64[]
        Z = n*sqrt(-2*epsiloni)

        for j = 1:length(rGrid)
            r = rGrid[j]
            if  n-l-2 >= 0
                p = 1/n * 2^(l+1) * exp(-r*Z/n) * (r*Z/n)^l * sqrt( Z^3 * factorial(n-l-1) / (n^4*factorial(l+n)) ) * ( (n+l*n-r*Z) * GSL.sf_laguerre_n(n-l-1,2*l+1,2*r*Z/n) - 2*r*Z * GSL.sf_laguerre_n(n-l-2,2+2*l,2*r*Z/n) )
            elseif  n == 1 && l == 0
                p = -2 * exp(-r*Z) * sqrt(Z^3) * (r*Z - 1)
            else #not implemented
                p = 0
            end
            push!( DPnl, p )
        end

        return( DPnl )
    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.pReducedME(epsilonp::Float64, lp::Int, n::Int, l::Int, epsiloni::Float64, volkov::AbstractVolkovState)`
        ... computes the reduced matrix elements of the momentum operator <epsilonp lp ||p||n l> in the one-particle picture
    """
    function  pReducedME(epsilonp::Float64, lp::Int, n::Int, l::Int, epsiloni::Float64, volkov::AbstractVolkovState)
        rmax = 100.
        orderGL = 1000
        gaussLegendre = Radial.GridGL("Finite",0.0,rmax,orderGL)
        rgrid = gaussLegendre.t
        weights = gaussLegendre.wt

        Pnl = HydrogenPnl( epsiloni, n, l, rgrid )
        if  typeof(volkov) == FreeVolkov
            Pepsplp = VolkovP( epsilonp, lp, rgrid )
        elseif  typeof(volkov) == CoulombVolkov
            Pepsplp = CoulombVolkovP( epsilonp, lp, volkov.Z, rgrid )
        end

        DPnl = HydrogenDPnlDr( epsiloni, n, l, rgrid )

        integral = 0. * im

        #Sum over grid and compute Gauss-Legendre sum
        for    j = 1:orderGL
            r = rgrid[j]
            integrand = conj( Pepsplp[j] )/r * ( r*DPnl[j] - ((lp-l)*(lp+l+1))/2 * Pnl[j] )

            #Gauss-Legendre sum
            integral = integral + weights[j] * 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( integral )
    end

    """
    `StrongField.scalarProdBoundCont(epsilonp::Float64, lp::Int, n::Int, l::Int, epsiloni::Float64, volkov::AbstractVolkovState)`
        ... computes the scalar product of the bound (hydrogenic) and continuum (Volkov) states in the one-particle picture
    """
    function  scalarProdBoundCont(epsilonp::Float64, lp::Int, n::Int, l::Int, m::Int, epsiloni::Float64, volkov::AbstractVolkovState)
        rmax = 1000.
        orderGL = 1000
        gaussLegendre = Radial.GridGL("Finite",0.0,rmax,orderGL)
        rgrid = gaussLegendre.t
        weights = gaussLegendre.wt

        Pnl = HydrogenPnl( epsiloni, n, l, rgrid )

        if  typeof(volkov) == FreeVolkov
            Pepsplp = VolkovP( epsilonp, lp, rgrid )
        elseif  typeof(volkov) == CoulombVolkov
            Pepsplp = CoulombVolkovP( epsilonp, lp, volkov.Z, rgrid )
        end

        DPnl = HydrogenDPnlDr( epsiloni, n, l, rgrid )

        integral = 0. * im

        #Sum over grid and compute Gauss-Legendre sum
        for    j = 1:orderGL
            r = rgrid[j]
            integrand = conj( Pepsplp[j] ) * Pnl[j]

            #Gauss-Legendre sum
            integral = integral + weights[j] * integrand
        end

        return((-im)^l * integral)
    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 an $(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,1})`
        ... computes the requested photoelectron energy distribution and returns the results in the variable
            outcome::StrongField.OutcomeEnergyDistribution
    """
    function  computeOutcome(obs::StrongField.SfaEnergyDistribution, amplitudes::Array{SphericalAmplitude,1})
        probabilities = Float64[]
        for  amp  in  amplitudes   pp = sqrt(2*amp.energy);  push!(probabilities, pp * (amp.value * conj(amp.value)) )      end
        outcome = OutcomeEnergyDistribution(obs.theta, obs.phi, obs.energies, probabilities)
        return( outcome )
    end

    """
    `StrongField.computeOutcome(observable::StrongField.SfaMomentumDistribution, amplitudes::Array{SphericalAmplitude,1})`
        ... computes the requested photoelectron momentum distribution and returns the results in the variable
            outcome::StrongField.OutcomeMomentumDistribution
    """
    function  computeOutcome(obs::StrongField.SfaMomentumDistribution, amplitudes::Array{SphericalAmplitude,1})
        probabilities = Float64[]
        momenta       = Array{Float64}(undef,0,2)
        for  amp  in  amplitudes
            pp = sqrt(2*amp.energy)
            px = pp*sin(amp.theta)*cos(amp.phi)
            py = pp*sin(amp.theta)*sin(amp.phi)
            momenta = [momenta ; [px py]]
            push!( probabilities, pp * (amp.value * conj(amp.value)) )
        end
        outcome = OutcomeMomentumDistribution(obs.theta, momenta, probabilities)
        return( outcome )
    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. A newAmplitudes::Array{SphericalAmplitude,1} is returned.
    """
    function  computeSphericalAmplitudes(comp::StrongField.Computation)
        newAmplitudes = SphericalAmplitude[]
        #
        # First determine which spherical SFA amplitudes need to be computed before the actual computation starts
        sfaAmplitudes = StrongField.determineSphericalAmplitudes(comp.observable)
        # Determine quantum numbers of the initial and final state
        nqn = 1;    lqn = 0;    n = 1;      l = 0;      m = 0;    initialEn = convertUnits("energy: from eV to atomic", comp.initialLevel.energy)
        #

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

        # Compute the requested amplitudes
        for  amp in  sfaAmplitudes
            thetap        = amp.theta;   phip = amp.phi;     energyp = amp.energy
            envIntegrals  = StrongField.computeEnvelopeIntegrals(comp.envelope, comp.beam, comp.polarization, thetap, phip, energyp, initialEn)
            fVolkovPlus, fVolkovMinus, fVolkovSquared = envIntegrals
            #
            reducedMEArray = ComplexF64[]
            for lp = lpMin:lpMax
                push!( reducedMEArray, pReducedME(energyp, lp, n, l, initialEn, comp.volkov) )
            end
            #

            wminus = 0.0im;    wplus = 0.0im
            # Collect contributions from all l_p, q terms; this summation will change in a (nljm) representation
            for  lp = lpMin:lpMax
                reducedME = reducedMEArray[lp-lpMin+1]
                for  q in [-1, 0, 1]
                    wplus = wplus + AngularMomentum.sphericalYlm(lp, m-q, amp.theta, amp.phi) * (-1)^q  *
                             Basics.determinePolarizationVector(q, comp.polarization, star=false)         *
                             AngularMomentum.ClebschGordan(l, m, 1, -q, lp, m-q) * reducedME
                    wminus = wminus + AngularMomentum.sphericalYlm(lp, m+q, amp.theta, amp.phi)           *
                             Basics.determinePolarizationVector(q, comp.polarization, star=true)          *
                             AngularMomentum.ClebschGordan(l, m, 1, q, lp, m+q) * reducedME
                end
            end
            scalarProd = scalarProdBoundCont(energyp, l, n, l, m, initialEn, comp.volkov)
            wa = -im * sqrt(2/pi) * (fVolkovPlus * wminus + fVolkovMinus * wplus) - im / sqrt(2*pi) * fVolkovSquared * AngularMomentum.sphericalYlm(l, m, amp.theta, amp.phi) * scalarProd
            push!(newAmplitudes, SphericalAmplitude(amp.energy, amp.theta, amp.phi, wa))

            println(">> $(SphericalAmplitude(amp.energy, amp.theta, amp.phi, wa))")
        end

        return( newAmplitudes )
    end

    """
    `StrongField.determineSphericalAmplitudes(observable::StrongField.SfaEnergyDistribution)`
        ... 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)
        amplitudes = StrongField.SphericalAmplitude[]
        for  energy in observable.energies      push!(amplitudes, SphericalAmplitude(energy, observable.theta, observable.phi, 0.))     end

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

    """
    `StrongField.determineSphericalAmplitudes(observable::StrongField.SfaMomentumDistribution)`
        ... 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)
        amplitudes = StrongField.SphericalAmplitude[]
        for  energy in observable.energies
            for  phi in observable.phis
                push!(amplitudes, SphericalAmplitude(energy, observable.theta, phi, 0.))
            end
        end

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

    """
    `StrongField.plot( comp::StrongField.Computation, results::Dict{String,Any}, energyScale::String = "atomic", probabilityScaling::String = "linear" )`
        ... generates a graphical representation of the observable (either StrongField.SfaEnergyDistribution or StrongField.SfaMomentumDistribution).
        energyScale determines the scaling of the energy axis either in atomic units (energyScale = "atomic") or in units of hbar*omega (energyScale = "omega").
        The y-axis is scaled either linearly (probabilityScaling = "linear") or logarithmically (probabilityScaling = "log")
    """
    function plot( comp::StrongField.Computation, results::Dict{String,Any}, energyScale::String = "atomic", probabilityScaling::String = "linear" )

        #---Photoelectron energy spectrum---
        if  typeof(comp.observable) == StrongField.SfaEnergyDistribution
                #prepare data and rescale the x-axis if neccessary
                if  energyScale == "atomic"
                    energyLabel = "E (a.u.)"
                    energies = results["energy distribution"].energies
                elseif  energyScale == "omega"
                    energyLabel = "E/omega"
                    energies = results["energy distribution"].energies / comp.beam.omega
                end
                probabilities = results["energy distribution"].probabilities
                gr() #sets the plotting backend to the package "GR"

                #set scaling of the y-axis
                scaling = :identity
                if  probabilityScaling == "log"
                            scaling = :log10
                end

                #generate the plot
                p = Plots.plot(energies, probabilities,
                                title = "Photoelectron energy spectrum",
                                xlabel = energyLabel,
                                ylabel = "P(E)",
                                xscale = :identity,
                                yscale = scaling,
                                framestyle = :box,
                                legend = :none,
                                #markershape = :circle,
                                #markershape = :none,
                                line = 2,
                                linecolor = :black,
                                gridlinewidth = 2,
                                tickfontsize = 10,
                                labelfontsize = 10,
                                labelfontfamily = "Latin Modern Roman",
                                titlefontfamily = "Latin Modern Roman"

                              )

                #export the plot as png-file
                png("energy_spectrum")
        #-----------------------------------

        #---Photoelectron momentum distribution---
        elseif  typeof(comp.observable) == StrongField.SfaMomentumDistribution
                #prepare data
                pList = results["momentum distribution"].momenta
                probList = results["momentum distribution"].probabilities

                #generate the plot
                #pyplot()
                #r = range(0,stop=10,length=11)
                #theta = range(0,stop=360,length=361)
                #f(r,theta) = r^2
                #println("$(f.(r,theta'))")
                #Plots.plot( heatmap( f.(r,theta'), proj=:polar ) )
                #println("$(transpose(probList))")
                #Plots.plot( heatmap( pList, transpose(probList), proj=:polar ) )
                #imshow(pList,transpose(probList))
                #probList = probList[2:-1]
                #matplotlib.pyplot.pcolormesh(pList[:,1], pList[:,2], transpose(probList))
                contourf( pList[:,1], pList[:,2], probList )
                #matplotlib.pyplot.imshow(pList)
                #heatmap( pList[:,1], pList[:,2], probList, proj=:polar, legend=true
                                #heatmap( probList
                #                )

                #p = Plots.plot( #heatmap( pList[:,1], pList[:,2], probList
                #               Plots.GR.polarheatmap( probList
                            ##heatmap( probList
                #                 )
                #              )

                #export the plot as png-file
                png("momentum_distribution")
        #-----------------------------------------

        #---Not a valid obserable---
        else
                error("Undefined observable for strong-field computations.")
        end
        #---------------------------

    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
            sfaAmplitudes = StrongField.computeSphericalAmplitudes(comp)
            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
            sfaAmplitudes = StrongField.computeSphericalAmplitudes(comp)
            sfaOutcome    = StrongField.computeOutcome(comp.observable, sfaAmplitudes)
            if output    results = Base.merge( results, Dict("computation" => comp, "momentum distribution" => sfaOutcome) )  end
        else     error("Undefined observable for strong-field computations.")
        end

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

end # module


include("default_params.jl")
include("model_def.jl")

ks_errors = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
l1_errors = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
l1_cell_probs_errors = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
ks_error_row = zeros(4)
l1_error_row = zeros(4)
l1_cell_probs_row = zeros(4)

sim_cdf = CSV.read(
    (@__DIR__)*"/data/sims_cdf_evaluated.csv",
    DataFrame,
)

for o in orders
    cols = [1;3;4]
    for (c,m) in enumerate(string.(approx_types[[1;3;4]]).*string.([1;3;4]))
        approx_data = CSV.read(
            (@__DIR__)*"/data/first_return_cdf_approximations/order_"*string(o)*"_model_"*m*".csv",
            DataFrame,
        )
        ks_error = max(
            maximum(abs.(approx_data.phase_2-sim_cdf.phase_2)),
            maximum(abs.(approx_data.phase_4-sim_cdf.phase_4)),
        )
        ks_error_row[cols[c]] = ks_error
        l1_cdf = sum(abs.(approx_data.phase_2[1:end-1]+approx_data.phase_2[2:end]-
                sim_cdf.phase_2[1:end-1]-sim_cdf.phase_2[2:end]).*diff(approx_data.x))/2 +
            sum(abs.(approx_data.phase_4[1:end-1]+approx_data.phase_4[2:end]-
                sim_cdf.phase_4[1:end-1]-sim_cdf.phase_4[2:end]).*diff(approx_data.x))/2
        l1_error_row[cols[c]] = l1_cdf
        cell_edges_idx = findall(mod.(approx_data.x,0.4).==0.0)
        sim_cell_probs = DataFrame(
            x=[approx_data.x[1]; approx_data.x[cell_edges_idx[2:end]] - approx_data.x[cell_edges_idx[1:end-1]]],
            phase_2=[sim_cdf.phase_2[1]; sim_cdf.phase_2[cell_edges_idx[2:end]] - sim_cdf.phase_2[cell_edges_idx[1:end-1]]],
            phase_4=[sim_cdf.phase_4[1]; sim_cdf.phase_4[cell_edges_idx[2:end]] - sim_cdf.phase_4[cell_edges_idx[1:end-1]]],
        )
        approx_cell_probs = DataFrame(
            x=[approx_data.x[1]; approx_data.x[cell_edges_idx[2:end]] - approx_data.x[cell_edges_idx[1:end-1]]],
            phase_2=[approx_data.phase_2[1]; approx_data.phase_2[cell_edges_idx[2:end]] - approx_data.phase_2[cell_edges_idx[1:end-1]]],
            phase_4=[approx_data.phase_4[1]; approx_data.phase_4[cell_edges_idx[2:end]] - approx_data.phase_4[cell_edges_idx[1:end-1]]],
        )
        l1_cell_probs_row[cols[c]] = sum(abs.(approx_cell_probs.phase_2-sim_cell_probs.phase_2)) +
            sum(abs.(approx_cell_probs.phase_4-sim_cell_probs.phase_4))
    end
    push!(ks_errors,ks_error_row)
    push!(l1_errors,l1_error_row)
    push!(l1_cell_probs_errors,l1_cell_probs_row)
end

CSV.write((@__DIR__)*"/data/errors/ks_error.csv",ks_errors)
CSV.write((@__DIR__)*"/data/errors/l1_error.csv",l1_errors)
CSV.write((@__DIR__)*"/data/errors/l1_cell_probs_error.csv",l1_cell_probs_errors)

ks_errors_lwr = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
l1_errors_lwr = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
l1_cell_probs_errors_lwr = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
ks_error_row_lwr = zeros(4)
l1_error_row_lwr = zeros(4)
l1_cell_probs_error_row_lwr = zeros(4)

ks_errors_upr = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
l1_errors_upr = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
l1_cell_probs_errors_upr = DataFrame(DG=[],DG_limit=[],Order_1=[],QBDRAP=[])
ks_error_row_upr = zeros(4)
l1_error_row_upr = zeros(4)
l1_cell_probs_error_row_upr = zeros(4)

ks_error_boot = zeros(n_boot)
l1_error_boot = zeros(n_boot)
l1_cell_probs_error_boot = zeros(n_boot)
for o in orders
    cols = [1;3;4]
    for (c,m) in enumerate(string.(approx_types[[1;3;4]]).*string.([1;3;4]))
        approx_data = CSV.read(
            (@__DIR__)*"/data/first_return_cdf_approximations/order_"*string(o)*"_model_"*m*".csv",
            DataFrame,
        )
        cell_edges_idx = findall(mod.(approx_data.x,0.4).==0.0)
        approx_cell_probs = DataFrame(
                x=[approx_data.x[1]; approx_data.x[cell_edges_idx[2:end]] - approx_data.x[cell_edges_idx[1:end-1]]],
                phase_2=[approx_data.phase_2[1]; approx_data.phase_2[cell_edges_idx[2:end]] - approx_data.phase_2[cell_edges_idx[1:end-1]]],
                phase_4=[approx_data.phase_4[1]; approx_data.phase_4[cell_edges_idx[2:end]] - approx_data.phase_4[cell_edges_idx[1:end-1]]],
            )
        for n in 1:n_boot
            sim_cdf = CSV.read(
                (@__DIR__)*"/data/bootstrap_first_return/sample_"*string(n)*".csv",
                DataFrame,
            )
            ks_error = max(
                maximum(abs.(approx_data.phase_2-sim_cdf.phase_2)),
                maximum(abs.(approx_data.phase_4-sim_cdf.phase_4)),
            )
            ks_error_boot[n] = ks_error
            l1_cdf = sum(abs.(approx_data.phase_2[1:end-1]+approx_data.phase_2[2:end]-
                    sim_cdf.phase_2[1:end-1]-sim_cdf.phase_2[2:end]).*diff(approx_data.x))/2 +
                sum(abs.(approx_data.phase_4[1:end-1]+approx_data.phase_4[2:end]-
                    sim_cdf.phase_4[1:end-1]-sim_cdf.phase_4[2:end]).*diff(approx_data.x))/2
            l1_error_boot[n] = l1_cdf
            sim_cell_probs = DataFrame(
                x=[approx_data.x[1]; approx_data.x[cell_edges_idx[2:end]] - approx_data.x[cell_edges_idx[1:end-1]]],
                phase_2=[sim_cdf.phase_2[1]; sim_cdf.phase_2[cell_edges_idx[2:end]] - sim_cdf.phase_2[cell_edges_idx[1:end-1]]],
                phase_4=[sim_cdf.phase_4[1]; sim_cdf.phase_4[cell_edges_idx[2:end]] - sim_cdf.phase_4[cell_edges_idx[1:end-1]]],
            )
            l1_cell_probs_error_boot[n] = sum(abs.(approx_cell_probs.phase_2-sim_cell_probs.phase_2)) +
                sum(abs.(approx_cell_probs.phase_4-sim_cell_probs.phase_4))
        end
        ks_error_row_lwr[cols[c]] = quantile(ks_error_boot,qtiles[1])
        l1_error_row_lwr[cols[c]] = quantile(l1_error_boot,qtiles[1])
        l1_cell_probs_error_row_lwr[cols[c]] = quantile(l1_cell_probs_error_boot,qtiles[1])
        ks_error_row_upr[cols[c]] = quantile(ks_error_boot,qtiles[2])
        l1_error_row_upr[cols[c]] = quantile(l1_error_boot,qtiles[2])
        l1_cell_probs_error_row_upr[cols[c]] = quantile(l1_cell_probs_error_boot,qtiles[2])
    end
    push!(ks_errors_lwr,ks_error_row_lwr)
    push!(l1_errors_lwr,l1_error_row_lwr)
    push!(l1_cell_probs_errors_lwr,l1_cell_probs_error_row_lwr)
    push!(ks_errors_upr,ks_error_row_upr)
    push!(l1_errors_upr,l1_error_row_upr)
    push!(l1_cell_probs_errors_upr,l1_cell_probs_error_row_upr)
end

CSV.write((@__DIR__)*"/data/errors/ks_error_lwr.csv",ks_errors_lwr)
CSV.write((@__DIR__)*"/data/errors/l1_error_lwr.csv",l1_errors_lwr)
CSV.write((@__DIR__)*"/data/errors/l1_cell_probs_error_lwr.csv",l1_cell_probs_errors_lwr)
CSV.write((@__DIR__)*"/data/errors/ks_error_upr.csv",ks_errors_upr)
CSV.write((@__DIR__)*"/data/errors/l1_error_upr.csv",l1_errors_upr)
CSV.write((@__DIR__)*"/data/errors/l1_cell_probs_error_upr.csv",l1_cell_probs_errors_upr)


module TIFFtoRegression

############################################################################
# Reading the values of the TIFF according to TIFF v0.6

function FieldValues(ValueOffset::Int64,FileNumber::Array{Int16},FieldCount::Int64,FieldType=4::Int64) #v2
# =========================== Value or Pointer ? ===============================
ValuePointer=0::Int64
if FieldCount==1 # Simplified criterion of "can hold that much info on 4 bytes"
  Value=ValueOffset
else
  ValuePointer=ValueOffset+1 # correction due to the first being 0 and not 1
end

if ValuePointer==0
  DesiredV=Value
else
  #println("$ValuePointer, Count=$FieldCount, Type=$FieldType")
  DesiredV=zeros(1,FieldCount)
  for i=1:FieldCount
    Vi=0
    if FieldType == 2 || FieldType==1 #ASCII or BYTE
      FieldTypeByte=1
    elseif FieldType==3 # Short
      FieldTypeByte=2
    elseif FieldType==4 # Long
      FieldTypeByte=4
    elseif FieldType==5 # Rational
      FieldTypeByte=8
    end

    for jj=1:FieldTypeByte
      Vjj=FileNumber[ValuePointer+FieldTypeByte*(i-1)+jj-1]*256^(jj-1)
      Vi=Vi+Vjj
    end
    DesiredV[i]=convert(Int64,Vi)
  end
end

return DesiredV
end
#############################################################################

# Reading a TIFF (also able to read special Dantec TIFF that are pseudo Std)

function AquiTiff(FilePath::String)
  # Opening File
  FileString=read(FilePath)
  L=length(FileString)
  FileNumber=zeros(Int16,L,1)
  for i=1:L
    NumberI=convert(Int16,FileString[i])
    FileNumber[i]=NumberI
  end
  # ============================ Reading Header ================================
  IFD=FileNumber[5]+1+FileNumber[6]*256+FileNumber[7]*256^2+FileNumber[8]*256^3 # indicates where the Image File Directory starts (i e where the meta data start)
  # +1 in the indexes is a consequence of the starting count from 0 to 1 in tiff ==> JuliaVector
  if FileNumber[1]==73 & FileNumber[2]==73 # Small endian (determines the order of the bites from bigger to smaller)
  # ======================== Reading Metadata: ================================
  # ------------------------- Opening Values ----------------------------------
    Entries=FileNumber[IFD]+256*FileNumber[IFD+1] # Number of Entries in the IFD
    # Opening Values that might be in the Metadata
    Height=0::Int64
    Width=0::Int64
    BPS=0::Int64 #Bit per Sample
    CompType=0::Int64 #Type of Compression
    BlackWhite=0::Int64 #Photometric Interpretation # 0=White is Zero; 1<=Black is Zero
    StripOffsets=0::Int64 #Places where data is stored
    NFragData=0::Int64 # Number of those Places
    Orientation=0::Int64 # 1=Start Top-left
    SPPx=0::Int64 # Sample per Pixel
    RowsPStrip=0::Int64 # Rows per data strip: ceilling(Height/RowsPStrip)=Number of strip (p19)
    StripByteCount=0::Int64 #For each strip the number of bytes in that strip AFTER compression
    XRes=0::Int64 # X-Resolution : nbr of px per Resolution unit (cm/inch see 296)
    YRes=0::Int64 # Y-Resolution
    PlanConfig=0::Int64 # Planar Configuration ???
    ResUnit=0::Int64 # Resolution Unit : abstract (=0), cm (=1) or inch (=2)
    Software=0::Int64 # Software used to create the Tiff
    SBCType=0::Int64
    StripOffsets=[]
    NBytesPPx=0::Int64 # Spec Dantec Dynamics
    # --------------------------------------------------------------------------

    for i=1:Entries # All MetaData there are
      # ----------------------- Initiating Entry--------------------------------
      # Delimiting entry
      StartE=IFD+12*(i-1)+2
      EndE=StartE+11
      EntryN=FileNumber[StartE:EndE] #Taking one entry of 12 hexa bytes
      # Reading Entry
      FieldID=EntryN[1]+EntryN[2]*256
      FieldType=EntryN[3]+EntryN[4]*256
      FieldCount=EntryN[5]+EntryN[6]*256+EntryN[7]*65536+EntryN[8]*16777216
      ValueOffset=EntryN[9]+EntryN[10]*256+EntryN[11]*65536+EntryN[12]*16777216
      # Is either the value of the entry or a pointer towards the place where the values are kept if the entry can't be held on 8bits
      # ----------------------- Switch on FieldID ------------------------------
      if FieldID==254 # NewSubFileType
        NewSubFileType=FieldValues(ValueOffset,FileNumber,FieldCount)
        # 2 : Multipage
        # 4 : Defines a transparency mask
      elseif FieldID==256 # Image Width= n° of column in the Image
        Width=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==257 # Height
        Height=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==258 # Bits/sample ==> number of shade of gray allowable
        BPS=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==259 # Compression Type
        CompType=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==262 # Photometric Interpretation
        # 0=White is Zero; 1<=Black is Zero
        BlackXhite=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==273 # StripOffsets= Position of all data StripOffsets
        NFragData=FieldCount
        StripOffsets=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==274 # Orientation of the Picture
        # 1=Start Top-left
        Orientation=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==277 # Sample/px (3 or more ==> Number of colors allowable)
        SPPx=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==278 # Rows per Strip
        RowsPStrip=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==279 # StripByteCount
        StripByteCount=FieldValues(ValueOffset,FileNumber,FieldCount)
        SBCType=FieldType
      elseif FieldID==282 # X-Resolution
        XRes=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==283 # Y-Resolution
        YRes=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==284 # Planar Configuration
        PlanConfig=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==285
        PageName=FieldValues(ValueOffset,FileNumber,FieldCount,FieldType)
      elseif FieldID==296 # Resolution Unit (cm/inch)
        ResUnit=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==297
        PageNumber=FieldValues(ValueOffset,FileNumber,FieldCount,FieldType)
      elseif FieldID==305 # Software
        #Software=FieldValues(ValueOffset,FileNumber,FieldCount)
      elseif FieldID==32997
        #Specific to DynamicStudio, Dantec
        # Number of bytes/ px? TBC
        NBytesPPx=FieldValues(ValueOffset,FileNumber,FieldCount)
        if SPPx==0
          SPPx=2 #NBytesPPx/8 # Works?
          SPPx=convert(Int64,SPPx)
        end
      end  # Metadata Aquisition
    end
    EndIFD=IFD+12*(Entries-1)+13
  elseif FileNumber[1]==77 & FileNumber[2]==77 #big endian
    println("Big Endian Still to be implemented, nearly same as small endian")
  end
  # ======================= Creating the Img Matrix ============================
  # ----------------------- Finding and Ordering Px ---------------------------
  PixelString=[]
  PixelNumber=ones(1,length(FileNumber))
  CountLeng=0
  for i=1:NFragData
    PxStrip=FileNumber[convert(Int64,StripOffsets[i]):convert(Int64,StripOffsets[i]+StripByteCount[i])]
    l=length(PxStrip)
    PixelNumber[1+1+CountLeng:1+l+CountLeng]=PxStrip[1:l]
    CountLeng=CountLeng+l
  end
  PixelNumber=PixelNumber[1:CountLeng] # cutting the exedentary ones (due to the last strip being sometimes shorter than the others)
  # ------------------------ Making Order in the Px ----------------------------
  ImgArray=zeros(Int64,Height,Width,SPPx)
  for i=1:Height
    for  j=1:Width
      for k=1:SPPx
        if NBytesPPx==0
          Pxijk=((i-1)*Width+(j-1))*SPPx+k
        else # Why ? 171 dtmed empirically for Dynamic Studio Tif)
          #Pxijk=((i-1)*Width+(j-1))*SPPx+k
          Pxijk=((i-1)*Width+(j+convert(Int64,floor(171*i/Height))-1))*SPPx+k # ???????????????????????????????????????????????????
        end
        ImgArray[i,j,k]=PixelNumber[Pxijk]
      end
    end
  end
  #--------------------- Making it Gray Matrix ---------------------------------
  if NBytesPPx==0 # Suposedly "Normal Img or Color Img"
    #GrayArray=sum(ImgArray,3)/SPPx
    GrayArray=ImgArray[:,:,3]
  else
    GrayArray=ImgArray[:,:,1]*256+ImgArray[:,:,2] # Dantec Spec
  end
return GrayArray[:,:,1]
end

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

# Loading a Serie of TIFF from Dantec Dynamics

function AquiSerieD(Folder::String, File0::String, NbrImg=189::Int64)
  # ========================== Making Img Path =================================
  # ----------------Cutting File0 (Dantec DynamicsStudio Naming)----------------
  j=1
  while File0[j]!='.'
    j=j+1
  end
  p1=j
  #println(p1)
  BaseName=File0[1:p1-1] # Input by User exporting Img
  j=j+1
  while File0[j]!='.'
    j=j+1
  end
  p2=j
  FileSerie=File0[p1+1:p2-1] #Auto Serie Numbering
  NumberInSerie=File0[p2+1:end] # Auto Img Numbering
  N=NbrImg-1 # Numbering Starts at 0 not 1
  ImgArray=[]
  OneAdress=[]
  # ------------------------ Making Image Number String -------------------------
  for i=0:N
    # Finding k
    LNIS=length(NumberInSerie)
    k=LNIS
    while floor(i/(10.0^k))<1 && k>=0
      k=k-1
    end
    # completing the NumberIn Serie String
    NIS=""
    ik=i
    for kk=LNIS:-1:1
      if kk>k+1
        NIS="$(NIS)0"
      else
        V=convert(Int64,floor(ik/(10^(kk-1))))
        NIS="$(NIS)$V"
        ik=ik-V*10^(kk-1)
      end
    end
    OneAdress="$(Folder)\\$(BaseName).$(FileSerie).$(NIS).tif"
    #println(OneAdress)
    # ========================= Aquiring Images ==================================
    OneImg=AquiTiff(OneAdress)
    if isempty(ImgArray)
      ImgArray=zeros(Int64,size(OneImg,1),size(OneImg,2),N+1)
      ImgArray[:,:,1]=OneImg
    else
      ImgArray[:,:,i+1]=OneImg
    end
  end
return ImgArray # to assure Julia wont print the Array
end
#############################################################################

# Loading a Serie of TIFF automaticaly renamed with Windows 10

function AquiSerieW(Folder::String, File0::String, NbrImg=189::Int64)
  # ========================== Making Img Path =================================
  # ----------------Cutting File0 (Dantec DynamicsStudio Naming)----------------
  j=1
  while File0[j]!='('
    j=j+1
  end
  p1=j
  #println(p1)
  BaseName=File0[1:p1-1] # Input by User + 1 space (std windows renumerotation)
  j=j+1

  ImgArray=[]
  OneAdress=[]
  # ------------------------ Making Image Number String -------------------------
  for i=1:NbrImg
    OneAdress="$(Folder)\\$(BaseName)($i).tif"
    #println(OneAdress)
    # ========================= Aquiring Images ==================================
    OneImg=AquiTiff(OneAdress)
    if isempty(ImgArray)
      ImgArray=zeros(Int64,size(OneImg,1),size(OneImg,2),NbrImg)
      ImgArray[:,:,1]=OneImg
    else
      ImgArray[:,:,i]=OneImg
    end
  end
return ImgArray # to assure Julia wont print the Array
end

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

# Creating a Special type to stock the start of a line and its values
type Line # Creates type Line
  YValues::Vector
  XStart::Int64
end
##############################################################################

# Detecting the Lines in an image matrix (also retourning a Binary matrix)

function DetectLine(Img::Matrix)
  # only for bicolor ImgLine
  BiMat=zeros(Bool,size(Img))
  BiMat0=zeros(Bool,size(Img))
  Max=maximum(Img)
  min=minimum(Img)
  moy=(Max+min)/2
# --------------------------BiColoring----------------------------------
  k=0
  for i=1:size(Img,1) #BiLeveling
    for j=1:size(Img,2)
      if Img[i,j]<=moy
        BiMat[i,j]=0
        BiMat0[i,j]=0
      else
        BiMat[i,j]=1
        BiMat0[i,j]=1
      end
    end
  end

SumMat=sum(BiMat)
H=size(BiMat,1)
L=size(BiMat,2)
NPx=H*L
if SumMat<NPx/2
  # 1=Line, 0=Background
  LinePx=true
  BckGrnd=false #BckGrnd LinePx
else
  # 0=Line, 1=Background
  LinePx=false
  BckGrnd=true
end
CountLinePx=abs(SumMat-NPx) # nbr of Px that are part of a line
Lines=[] # need Array to stock the XStart
Xcount=1::Int64
Split=false
Ymean2=H
YmeanM1=H
Xcount2=1
while Xcount<L+1
  x=Xcount
 # 1 Line code  !
  if Split
    y=Ymean2
    x=Xcount2
    Xcount=Xcount-1
    Split=false
  else
    y=H
    x=Xcount
  end
  ConLine=true
  # ========================= Following the Line =========================
  while y>=2 && ConLine# going up to top
    # find [Background; Line]
    YmeanV=[]
    if [LinePx; BckGrnd]==[BiMat[y-1,x]; BiMat[y,x]] # Line Start Detect
      #Xcount=Xcount-1 #Stay in the same column and check further
      # Special Start in case of Split
      if YmeanM1 != H
        push!(YmeanV,YmeanM1)
        XStart=x-1
        YmeanM1=H
      else #End Spec Split
        XStart=x
      end
      y=y-1
      YStart=y
      # Start of a Line
      while BiMat[y,x]==LinePx
        y=y-1
      end
      YEnd=y+1
      # following that one line
      Width=abs(YStart-YEnd)+1
      BiMat[YEnd:YStart,x]=repmat([BckGrnd],Width,1)
      Ymean=(YStart+YEnd)/2
      push!(YmeanV,Ymean)
      while ConLine && x<L
        x=x+1
        if YStart<H && YEnd>1
          if sum(BiMat[YEnd-1:YStart+1,x].==repmat([BckGrnd],Width+2,1))==Width+2
            # Line Stop
            ConLine=false
            YStart=0
            YEnd=0
            x=Xcount
            Xcount=Xcount-1
          else
            # Line continue
            if BiMat[YEnd-1:YStart+1,x]==[BckGrnd;repmat([LinePx],Width,1);BckGrnd]
             # Line constant in Value and Width
            else
              # Line continue but changed
              if (BiMat[YStart:YStart+1,x]==[LinePx;LinePx])||(BiMat[YEnd-1:YEnd,x]==[LinePx;LinePx])
                # Line increase or decrease
                if (BiMat[YStart:YStart+1,x]==[LinePx;LinePx]) && !(BiMat[YEnd-1:YEnd,x]==[LinePx;LinePx])
                  # Decreasing
                  YEnd=YEnd+minimum(findin(BiMat[YEnd:YStart,x].==repmat([LinePx],Width,1),true))-1
                  yy=YStart
                  while BiMat[yy+1,x]==LinePx && yy<H
                    yy=yy+1
                  end
                  YStart=yy
                elseif !(BiMat[YStart:YStart+1,x]==[LinePx;LinePx]) && (BiMat[YEnd-1:YEnd,x]==[LinePx;LinePx])# Increasing
                  YStart=YEnd+maximum(findin(BiMat[YEnd:YStart,x].==repmat([LinePx],Width,1),true))-1
                  # YEnd
                  yy=YEnd
                  while BiMat[yy-1,x]==LinePx && yy>1
                    yy=yy-1
                  end
                  YEnd=yy
                elseif (BiMat[YStart:YStart+1,x]==[LinePx;LinePx]) && (BiMat[YEnd-1:YEnd,x]==[LinePx;LinePx]) # Increse in up and down
                  if sum(BiMat[YEnd-1:YStart+1,x].==LinePx)==(YStart+1-(YEnd-1)+1)# No hole
                    # Increse in Width in both side
                    ydown=YStart
                    while BiMat[ydown+1,x]==LinePx && ydown<H
                      ydown=ydown+1
                    end
                    YStart=ydown
                    yup=YEnd
                    while BiMat[yup-1,x]==LinePx && yup>1
                      yup=yup-1
                    end
                    YEnd=yup
                  else # there is a hole
                    # Split in two curves
                    Split= true
                    PosHole=findin(BiMat[YEnd-1:YStart+1,x].==BckGrnd,true)
                    # Down Part Continues
                    YEnd=(YEnd-1+minimum(PosHole))-1 # (below hole)
                    ydown=YStart
                    while BiMat[ydown+1,x]==LinePx && ydown<H
                      ydown=ydown+1
                    end
                    YStart=ydown
                    # Up Going part
                    YStart2=(YEnd-1+maximum(PosHole))+1
                    Ymean2=YStart2+1
                    Xcount2=x
                    YmeanM1=Ymean # kept for next line
                  end
                end
              else
                #Line Width decrease within previous limits
                PxPos=findin(BiMat[YEnd:YStart,x].==repmat([LinePx],Width,1),true)
                minPxPos=minimum(PxPos)
                MaxPxPos=maximum(PxPos)
                YStart=YEnd+MaxPxPos-1
                YEnd=YEnd+minPxPos-1
              end
            end
          end
        else
          # border effect
        end
        if !ConLine
          Width=0
        else
          Width=abs(YStart-YEnd)+1
          BiMat[YEnd:YStart,x]=repmat([BckGrnd],Width,1)
          Ymean=(YEnd+YStart)/2
          push!(YmeanV,Ymean)
        end
      end #start of a line detected
    else
      y=y-1 # No Line, go on climbing
    end
    if length(YmeanV)>1 #!isempty(YmeanV)
      Line1=Line(H+1-YmeanV,XStart) # Assuming left bottom axis =0
      #then complete Lines
      push!(Lines,Line1)
      ConLine=false # end current line
    end
  end
    Xcount=Xcount+1
end
# from bottom to top
# 1 First column -> detect LinePx
# 1b if no LinePx go to the right
# 2 continue climbing until Px!=LinePx
# 3 from there moove to the right following the line
# 3a check same Px and Px+1 and Px-1
# 3b if Px+-1 check also Px+-2, etc
#    if no Px: line stop
# 3c repeat 3 until line stop
# 4 remoove selected Px
# 5 repeat operation until top right
return BiMat0, Lines
end

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

# Allowing to plot the new type created, eventually on the matrix (thanks to PyPlot)

function PlotLine(Lines::Array{Any},H=0::Int64)
  for i=1:length(Lines)
    XS=Lines[i].XStart
    YV=Lines[i].YValues
    if H==0
      plot(XS:XS+length(YV)-1,YV,"-")
    else
      plot(XS-1:XS-1+length(YV)-1,H-YV,"-")
    end
  end
end

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

# Creating the next function for the Multiple Regression Function below

function NextFun(Xin::Vector,RegFuns="Poly"::String,i=1::Int64)
  n=length(Xin)
  if RegFuns=="Poly"
    Xout=Xin.^(i)
    XT=("Poly Order $i")
  elseif RegFuns=="Trigo"
    if isinteger(i/2)
      Xout=cos.(Xin/maximum(Xin)*(i/2))
      XT=("Standirdize cos($i x/2)")
    else
      Xout=cos.(Xin/maximum(Xin)/((i+1)/2))
      XT=("Standirdize cos($(1/(i+1)) x/2)")
    end
  elseif RegFuns=="Frac"
    F=findin(Xin,0)
    for j=1:length(F)
      X[F[j]]=mean(abs(Xin))*10^-5
    end
    Xout=1./Xin.^i
    if abs(minimum(Xout))< 2e-15 # all becoming too small
      println("Elements becoming too small X=ones")
      Xout=ones(size(Xin))
      XT=("Ones")
    else
      XT=("Frac Order $i")
    end
  elseif RegFuns=="Expo"
    Xout=exp.(i*Xin/maximum(Xin))
    XT=("Standardize exp($i x)")
  elseif RegFuns=="Loga"
    Xout=log.(abs(Xin+(i-2)*sqrt(maximum(Xin))))
    XT=("log(x+$(i-2) sqrt(Max))")
  elseif RegFuns=="Mixte"
    NFam=5 #nbr of family used (code written could be improoved)
    if isinteger((i+NFam-1)/NFam)
      k=(i+NFam-1)/NFam
      Xout=Xin.^k
      XT=("Poly Order $k") #Poly
    elseif isinteger((i+NFam-2)/NFam)
      k=(i+NFam-2)/NFam
      F=findin(Xin,0)
      for j=1:length(F)
        X[F[j]]=mean(abs(Xin))*10^-5
      end
      Xout=1./Xin.^k
      if abs(minimum(Xout))< 2e-15 # all becoming too small
        println("Elements becoming too small X=rand")
        Xout=rand(size(Xin))
      else
        XT=("Frac Order $k")
      end #Frac
    elseif isinteger((i+NFam-3)/NFam)
      k=(i+NFam-3)/NFam
      #NTrigo=4
      if isinteger(k/2)
        Xout=cos.(Xin/maximum(Xin)*(k/2))
        XT=("Standirdize cos($k x/2)")
      else
        Xout=cos.(Xin/maximum(Xin)/(k+1)/2)
        XT=("Standirdize cos($(1/(k+1)) x/2)")
      end #Trigo
    elseif isinteger((i+NFam-4)/NFam)
      k=(i+NFam-3)/NFam
      Xout=exp.(k*Xin/maximum(Xin))
      XT=("Standardize exp($k x)") #Expo
    elseif isinteger((i+NFam-5)/NFam)
      k=(i+NFam-5)/NFam
      Xout=log.(abs(Xin+(k-2)*sqrt(maximum(Xin))))
      XT=("log(x+$(k-2) sqrt(Max))")
    end
  elseif RegFuns=="Custom" # Would their be another interesting family
    println("What next fun do you want ? (Existing type: Poly, Trigo, Frac, Expo or Loga) \n Your choice is :")
    SwCu=input() #Switch Custom
    if SwCu=="Poly"
      println("Order of the wanted term : y=x^{Order} ? \n Order=")
      u=parse(Float64,input())
      if isinteger(u)
        Xu=Xin
      else
        Xu=Xin
        Xu[find(Xu.<0)]=zeros(size(find(Xu.<0)))
        println("All smaller than 0 elements have been replaced by zero for Poly Order $u")
      end
      Xout=Xu.^(u)
      XT=("Poly Order $u")
    elseif SwCu=="Trigo"
      println("Normal or Inverse ? \n Your choice is :")
      TTrig=input()
      if TTrig=="Normal"
        println("What type do you want ? (Possible choices: cos, sin, tan or cot) \n Your choice is :")
        TrigN=input()
        if TrigN=="sin"
          pritnln("What Angulat fequency (omega) do you want ? \n omega =")
          omega=parse(Float64,input())
          println("What Dephasage (phi) do you want ? \n phi =")
          phi=parse(Float64,input())
          Xout=sin.(omega*Xin+phi)
          XT=("sin($omega x+$phi)")
        elseif TrigN=="cos"
          pritnln("What Angulat fequency (omega) do you want ? \n omega =")
          omega=parse(Float64,input())
          println("What Dephasage (phi) do you want ? \n phi =")
          phi=parse(Float64,input())
          Xout=cos.(omega*Xin+phi)
          XT=("cos($omega x+$phi)")
        elseif TrigN=="tan"
          pritnln("What Angulat fequency (omega) do you want ? \n omega =")
          omega=parse(Float64,input())
          println("What Dephasage (phi) do you want ? \n phi =")
          phi=parse(Float64,input())
          Xout=tan.(omega*Xin+phi)
          XT=("tan($omega x+$phi)")
        elseif TrigN=="cot"
          pritnln("What Angulat fequency (omega) do you want ? \n omega =")
          omega=parse(Float64,input())
          println("What Dephasage (phi) do you want ? \n phi =")
          phi=parse(Float64,input())
          Xout=cot.(omega*Xin+phi)
          XT=("cot($omega x+$phi)")
        end
      elseif TTrig=="Inverse"
        println("What type do you want ? (Possible choices: acos, asin, atan or acot) \n Your choice is :")
        TrigN=input()
        if TrigN=="asin"
          pritnln("Vector is standardize (Divided by Max)")
          println("f ? : asin(XStd/f) (abs(f)>1)? \n f=")
          f=parse(Float64,input())
          if abs(f)<1
            f=1
          end
          Xout=asin.(Xin/maximum(abs(Xin))/f)
          XT=("asin(XStd/$f)")
        elseif TrigN=="acos"
          pritnln("Vector is standardize (Divided by Max)")
          println("f ? : acos(XStd/f) (abs(f)>1)? \n f=")
          f=parse(Float64,input())
          if abs(f)<1
            f=1
          end
          Xout=acos.(Xin/maximum(abs(Xin))/f)
          XT=("acos(XStd/$f)")
        elseif TrigN=="atan"
          println("What Factor f ? (atan(fX+t)) \n f=")
          f=parse(Float64,input())
          println("What terme ? \n t=")
          t=parse(Float64,input())
          Xout=atan.(f*Xin+t)
          XT=("atan(fX+t)")
        elseif TrigN=="acot"
          println("What Factor f ? (acot(fX+t)) \n f=")
          f=parse(Float64,input())
          println("What terme ? \n t=")
          t=parse(Float64,input())
          Xout=acot.(f*Xin+t)
          XT=("acot(fX+t)")
        end
      end
    elseif SwCu=="Frac"
      println("Order of the wanted term : y=x^-{Order} ? \n Order=")
      u=parse(Float64,input())
      if isinteger(u)
        Xu=Xin
      else
        Xu=Xin
        Xu[find(Xu.<=0)]=ones(size(find(Xu.<0)))
        println("All smaller than or equal to 0 elements have been replaced by one for Frac Order $u")
      end
      Xout=1./(Xu.^(u))
      XT=("Frac Order $u")
    elseif SwCu=="Expo"
      println("Order of the wanted Exponential ? : y=exp(x*{Order}) ? \n Order=")
      u=parse(Float64,input())
      Xout=exp.(u*Xin)
      XT=("exp($u x)")
    elseif SwCu=="Loga"
      println("Terme of the wanted Logarithme ? : y=log|x+t|) ? \n t=")
      t=parse(Float64,input())
      Xout=log.(abs(Xin+t))
      XT=("log(x+t)")
    end
  end
return Xout,XT
end

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

# Statistics discrabing the Regression using ANOVA analysis thanks to distributions
# functions

function StatReg(Xin,Yin,XMat,XName,p)
  Yb=mean(Yin)
  SquareX=(transpose(XMat)*XMat)
  n=length(Yin)
  Bh=(SquareX^-1)*transpose(XMat)*Yin # Estimator for Inv(Mat) ?
  Yh=XMat*Bh
  SSE=sum((Yin-Yh).^2)
  SSR=sum((Yh-Yb).^2)
  SSTot=sum((Yin-Yb).^2)
  R2=SSR/SSTot # the biggest the best
  dfR=p-1
  MSR=SSR/dfR
  dfE=n-p
  MSE=SSE/dfE
  F=MSE/MSR
  # ---------------------- Calc p-Value Model --------------------------
  FD=FDist(dfR,dfE)
  pModel=cdf(FD,F) #Smallest the best
  # p- Values indie
  TD=TDist(dfE)
  pVect=ones(size(Bh))
  for i=1:length(Bh)
    Yh=XMat[:,i]*Bh[i]
    SSE=sum((Yin-Yh).^2)
    if XName[i]=="Constant"
      SX2=sum((Xin-mean(Xin)).^2)
      SEBh=sqrt(SSE*(1/n+mean(Xin)^2/SX2))
    else
      X=XMat[:,i]
      SX2=sum((X-mean(X)).^2)
      SEBh=sqrt(SSE/SX2)
    end
    t=Bh[i]*sqrt(dfE)/SEBh
    pVar=2*(1-cdf(TD,t))
    pVect[i]=pVar
  end

  return pVect,pModel,Bh
end

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

# Main function of Multiple Regression implementing ANOVA tests
# and a user friendly approach to regression

function MultiRegOpt(Xin::Vector, Yin::Vector, FunFamily="Poly"::String, p=3::Int64,Stepwise="None"::String, threshold=0.05::Float64)
  n=length(Xin)
  pVModels=[]
  XMatArray=[]
  BArray=[]
  YhA=[]
  XMatA=[]
  pVarA=[]
  XNameA=[]
  NbrModelTested=0
  if FunFamily=="Custom"
    NbrModel=p
    XMat=ones(Float64,n,NbrModel)
    XName=[]
    for Counti=0:NbrModel-1 # Nombre of function involved
      if Counti==0
        FX=ones(size(Xin))
        push!(XName,"Constant")
      else
        Xout=NextFun(Xin,FunFamily,Counti)
        FX=Xout[1]
        push!(XName,Xout[2])
      end
      XMat[:,Counti+1]=FX
    end
    pVect,pModel,Bh=StatReg(Xin,Yin,XMat,XName,p)
    # Stepwise, taking out not necessary variables
    OldXMat=XMat
    OldpVect=pVect
    OldBh=Bh
    OldXMat=XMat
    OldpModel=pModel
    while sum(pVect.>threshold)>0 && Stepwise=="Student"# There is pVect>0.05
      i=findmax(pVect)[2] #eliminated fun
      if (NbrModel-1)==0
        println("Impossible to find a acceptable regression with given number of function for threshold=$threshold, First Result given")
        XMat=OldXMat
        pVect=OldpVect
        Bh=OldBh
        XMat=OldXMat
        break
      else
        NewXMat=ones(Float64,n,NbrModel-1)
        NewXName=[]
        for j=1:NbrModel
          if j<i
            NewXMat[:,j]=XMat[:,j]
            push!(NewXName,XName[j])
          elseif j>i
            NewXMat[:,j-1]=XMat[:,j]
            push!(NewXName,XName[j])
          end
        end
        println("MultiReg New:NewXName=$NewXName")
        pVect,pModel,Bh=StatReg(Xin,Yin,NewXMat,NewXName,p)
        XMat=NewXMat
        XName=NewXName
        NbrModel=NbrModel-1
      end
    end
    push!(pVarA,pVect)
    push!(pVModels,pModel)
    push!(BArray,Bh)
    push!(XMatA,XMat)
    push!(XNameA,XName)
  else
    for NbrModel=1:p
      XMat=ones(Float64,n,NbrModel)
      XName=[]
      for Counti=0:NbrModel-1 # Nombre of function involved
        if Counti==0
          FX=ones(size(Xin))
          push!(XName,"Constant")
        else
          Xout=NextFun(Xin,FunFamily,Counti)
          FX=Xout[1]
          push!(XName,Xout[2])
        end
        XMat[:,Counti+1]=FX
      end
      pVect,pModel,Bh=StatReg(Xin,Yin,XMat,XName,p)
      # Stepwise, taking out not necessary variables
      OldpVect=pVect
      OldBh=Bh
      OldXMat=XMat
      OldpModel=pModel
      while sum(pVect.>threshold)>0 && Stepwise=="Student"# there is pVect>0.05
        i=findmax(pVect)[2] #eliminated fun
        if (NbrModel-1)==0
          println("Impossible to find a acceptable regression with given number of function for threshold=$threshold, First Result given")
          XMat=OldXMat
          pVect=OldpVect
          Bh=OldBh
          XMat=OldXMat
          break
        else
          NewXMat=ones(Float64,n,NbrModel-1)
          NewXName=[]
          for j=1:NbrModel
            if j<i
              NewXMat[:,j]=XMat[:,j]
              push!(NewXName,XName[j])
            elseif j>i
              NewXMat[:,j-1]=XMat[:,j]
              push!(NewXName,XName[j])
            end
          end
          pVect,pModel,Bh=StatReg(Xin,Yin,NewXMat,NewXName,p)
          XMat=NewXMat
          XName=NewXName
          NbrModel=NbrModel-1
        end
      end
      if Stepwise=="Fleussu"
        Yb=mean(Yin)
        if sum(abs(Bh).<Yb*10.0^-5)>0
          println(abs(Bh).<Yb*10.0^-5)
          Bh[abs(Bh).<Yb*10.0^-5]=maximum(Bh)*2
          i=findin(X,maximum(X))
          NewXName=[]
          NewBh=[]
          for j=1:length(Bh)
            if sum(findin(i,j))==0
              push!(NewXName,XName[j])
              push!(NewBh,Bh[j])
            end
          end
          XName=NewXName
          Bh=NewBh
          NbrModel=length(Bh)
        end
      end
      push!(pVarA,pVect)
      push!(pVModels,pModel)
      push!(BArray,Bh)
      push!(XMatA,XMat)
      push!(XNameA,XName)
    end
  end
  return BArray,pVModels,XMatA,pVarA,XNameA
end

############################################################################
end # module


# export PARAMETER_DEFAULT, PARAMETER_UNSTABLE_BUT_FAST, PARAMETER_STABLE_BUT_SLOW

using MathOptInterface
MOI = MathOptInterface
const MOIU = MOI.Utilities
const AFFEQ{T} = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}

abstract type AbstractBlockMatrix{T} <: AbstractMatrix{T} end
function nblocks end
function block end

function Base.size(bm::AbstractBlockMatrix)
    n = mapreduce(blk -> LinearAlgebra.checksquare(block(bm, blk)),
                  +, 1:nblocks(bm), init=0)
    return (n, n)
end
function Base.getindex(bm::AbstractBlockMatrix, i::Integer, j::Integer)
    (i < 0 || j < 0) && throw(BoundsError(i, j))
    for k in 1:nblocks(bm)
        blk = block(bm, k)
        n = size(blk, 1)
        if i <= n && j <= n
            return blk[i, j]
        elseif i <= n || j <= n
            return 0
        else
            i -= n
            j -= n
        end
    end
    i, j = (i, j) .+ size(bm)
    throw(BoundsError(i, j))
end
Base.getindex(A::AbstractBlockMatrix, I::Tuple) = getindex(A, I...)

abstract type BlockSolution{T} <: AbstractBlockMatrix{T} end
struct PrimalSolution{T} <: BlockSolution{T}
    blocks::Vector{Matrix{T}}
end
# getptr(X::PrimalSolution, blk) = getResultYMat(X.problem, blk)
struct VarDualSolution{T} <: BlockSolution{T}
    blocks::Vector{Matrix{T}}
end
# getptr(X::VarDualSolution, blk) = getResultXMat(X.problem, blk)
nblocks(X::BlockSolution) = length(X.blocks)
function block(X::BlockSolution, blk::Integer)
    return X.blocks[blk]
end
# Needed by MPB_wrapper
function Base.getindex(A::BlockSolution, i::Integer)
    block(A, i)
end

mutable struct Optimizer{T} <: MOI.AbstractOptimizer
    objconstant::T
    objsign::Int
    blockdims::Vector{Int}
    varmap::Vector{Tuple{Int, Int, Int}} # Variable Index vi -> blk, i, j
    b::Vector{T}
    solve_time::Float64
    silent::Bool
    options::Dict{Symbol, Any}
    y::Vector{T}
    X::PrimalSolution{T}
    Z::VarDualSolution{T}
    primalobj::T
    dualobj::T
    phasevalue::Symbol
    tempfile::String
    elemdata::Vector{Any}
	presolve::Bool
    function Optimizer{T}(; presolve::Bool = true) where T
		optimizer = new(
            zero(T), 1, Int[], Tuple{Int, Int, Int}[], T[],
            NaN, false, Dict{Symbol, Any}(), T[], PrimalSolution{T}(Matrix{T}[]), VarDualSolution{T}(Matrix{T}[]), zero(T), zero(T), :noINFO, mktempdir(), [], true)
		if !presolve
			optimizer.presolve = false
		end
		return optimizer
    end
end

varmap(optimizer::Optimizer, vi::MOI.VariableIndex) = optimizer.varmap[vi.value]

function MOI.supports(optimizer::Optimizer, param::MOI.RawParameter)
	return param.name in keys(SET_PARAM)
end
function MOI.set(optimizer::Optimizer, param::MOI.RawParameter, value)
	if !MOI.supports(optimizer, param)
		throw(MOI.UnsupportedAttribute(param))
	end
	optimizer.options[param.name] = value
end
function MOI.get(optimizer::Optimizer, param::MOI.RawParameter)
	# TODO: This gives a poor error message if the name of the parameter is invalid.
	return optimizer.options[param.name]
end

MOI.supports(::Optimizer, ::MOI.Silent) = true
function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool)
	optimizer.silent = value
end
MOI.get(optimizer::Optimizer, ::MOI.Silent) = optimizer.silent

MOI.get(::Optimizer, ::MOI.SolverName) = "SDPA-GMP"

# See https://www.researchgate.net/publication/247456489_SDPA_SemiDefinite_Programming_Algorithm_User's_Manual_-_Version_600
# "SDPA (SemiDefinite Programming Algorithm) User's Manual — Version 6.00" Section 6.2
const RAW_STATUS = Dict(
    :noINFO        => "The iteration has exceeded the maxIteration and stopped with no informationon the primal feasibility and the dual feasibility.",
    :pdOPT => "The normal termination yielding both primal and dual approximate optimal solutions.",
    :pFEAS => "The primal problem got feasible but the iteration has exceeded the maxIteration and stopped.",
    :dFEAS => "The dual problem got feasible but the iteration has exceeded the maxIteration and stopped.",
    :pdFEAS => "Both primal problem and the dual problem got feasible, but the iterationhas exceeded the maxIteration and stopped.",
    :pdINF => "At least one of the primal problem and the dual problem is expected to be infeasible.",
    :pFEAS_dINF => "The primal problem has become feasible but the dual problem is expected to be infeasible.",
    :pINF_dFEAS => "The dual problem has become feasible but the primal problem is expected to be infeasible.",
    :pUNBD => "The primal problem is expected to be unbounded.",
    :dUNBD => "The dual problem is expected to be unbounded.")

function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString)
	return RAW_STATUS[optimizer.phasevalue]
end
function MOI.get(optimizer::Optimizer, ::MOI.SolveTime)
	return optimizer.solve_time
end

function MOI.is_empty(optimizer::Optimizer)
    return iszero(optimizer.objconstant) &&
        optimizer.objsign == 1 &&
        isempty(optimizer.blockdims) &&
        isempty(optimizer.varmap) &&
        isempty(optimizer.b) &&
        optimizer.elemdata == []
end
function MOI.empty!(optimizer::Optimizer{T}) where T
    optimizer.objconstant = zero(Cdouble)
    optimizer.objsign = 1
    empty!(optimizer.blockdims)
    empty!(optimizer.varmap)
    empty!(optimizer.b)
    optimizer.X = PrimalSolution{T}(Matrix{T}[])
    optimizer.Z = VarDualSolution{T}(Matrix{T}[])
    optimizer.y = T[]
    optimizer.phasevalue = :noINFO
    optimizer.tempfile = mktempdir()
    optimizer.elemdata = []
    optimizer.primalobj = zero(T)
    optimizer.dualobj = zero(T)
end

function MOI.supports(
    optimizer::Optimizer,
    ::Union{MOI.ObjectiveSense,
            MOI.ObjectiveFunction{<:Union{MOI.SingleVariable,
                                          MOI.ScalarAffineFunction{T}}}}) where T
    return true
end

function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.VectorOfVariables}, ::Type{MOI.Reals})
    return false
end
const SupportedSets = Union{MOI.Nonnegatives, MOI.PositiveSemidefiniteConeTriangle}
function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.VectorOfVariables},
    ::Type{<:SupportedSets})
    return true
end
function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.ScalarAffineFunction{T}},
    ::Type{MOI.EqualTo{T}}) where T
    return true
end

function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...)
    return MOIU.automatic_copy_to(dest, src; kws...)
end
MOIU.supports_allocate_load(::Optimizer, copy_names::Bool) = !copy_names

function MOIU.allocate(optimizer::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense)
    # To be sure that it is done before load(optimizer, ::ObjectiveFunction, ...), we do it in allocate
    optimizer.objsign = sense == MOI.MIN_SENSE ? -1 : 1
end
function MOIU.allocate(::Optimizer, ::MOI.ObjectiveFunction, ::Union{MOI.SingleVariable, MOI.ScalarAffineFunction}) end

function MOIU.load(::Optimizer, ::MOI.ObjectiveSense, ::MOI.OptimizationSense) end
# Loads objective coefficient α * vi
function load_objective_term!(optimizer::Optimizer{T}, α, vi::MOI.VariableIndex) where {T}
    blk, i, j = varmap(optimizer, vi)
    coef = optimizer.objsign * α
    if i != j
        coef /= 2
    end
    # in SDP format, it is max and in MPB Conic format it is min
    inputElement(optimizer, 0, blk, i, j, convert(T, coef))
end
function MOIU.load(optimizer::Optimizer, ::MOI.ObjectiveFunction, f::MOI.ScalarAffineFunction)
    obj = MOIU.canonical(f)
    optimizer.objconstant = f.constant
    for t in obj.terms
        if !iszero(t.coefficient)
            load_objective_term!(optimizer, t.coefficient, t.variable_index)
        end
    end
end
function MOIU.load(optimizer::Optimizer{T}, ::MOI.ObjectiveFunction, f::MOI.SingleVariable) where T
    load_objective_term!(optimizer, one(T), f.variable)
end

function new_block(optimizer::Optimizer, set::MOI.Nonnegatives)
    push!(optimizer.blockdims, -MOI.dimension(set))
    blk = length(optimizer.blockdims)
    for i in 1:MOI.dimension(set)
        push!(optimizer.varmap, (blk, i, i))
    end
end

function new_block(optimizer::Optimizer, set::MOI.PositiveSemidefiniteConeTriangle)
    push!(optimizer.blockdims, set.side_dimension)
    blk = length(optimizer.blockdims)
    for i in 1:set.side_dimension
        for j in 1:i
            push!(optimizer.varmap, (blk, i, j))
        end
    end
end

function MOIU.allocate_constrained_variables(optimizer::Optimizer,
                                             set::SupportedSets)
    offset = length(optimizer.varmap)
    new_block(optimizer, set)
    ci = MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}(offset + 1)
    return [MOI.VariableIndex(i) for i in offset .+ (1:MOI.dimension(set))], ci
end

function MOIU.load_constrained_variables(
    optimizer::Optimizer, vis::Vector{MOI.VariableIndex},
    ci::MOI.ConstraintIndex{MOI.VectorOfVariables},
    set::SupportedSets)
end

function MOIU.allocate_variables(model::Optimizer, nvars)
end

function MOIU.load_variables(optimizer::Optimizer{T}, nvars) where T
    @assert nvars == length(optimizer.varmap)
    dummy = isempty(optimizer.b)
    if dummy
        optimizer.b = [one(T)]
        optimizer.blockdims = [optimizer.blockdims; -1]
    end
    if dummy
        inputElement(optimizer, 1, length(optimizer.blockdims), 1, 1, one(T))
    end
end

function MOIU.allocate_constraint(optimizer::Optimizer{T},
                                  func::MOI.ScalarAffineFunction,
                                  set::MOI.EqualTo) where T
    push!(optimizer.b, MOI.constant(set))
    return AFFEQ{T}(length(optimizer.b))
end

function MOIU.load_constraint(m::Optimizer{T}, ci::AFFEQ,
                              f::MOI.ScalarAffineFunction, s::MOI.EqualTo) where T
    if !iszero(MOI.constant(f))
        throw(MOI.ScalarFunctionConstantNotZero{
            T, MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}(
                MOI.constant(f)))
    end
    f = MOIU.canonical(f) # sum terms with same variables and same outputindex
    for t in f.terms
        if !iszero(t.coefficient)
            blk, i, j = varmap(m, t.variable_index)
            coef = t.coefficient
            if i != j
                coef /= 2
            end
            inputElement(m, ci.value, blk, i, j, convert(T, coef))
        end
    end
end

function MOI.optimize!(m::Optimizer)
	start_time = time()
    # SDPA.initializeUpperTriangle(m.problem, false)
    redundant_F = initializeSolve(m)
    # SDPA.solve(m)
    inputname = "input.dat-s"
    outputname = "output.dat"
    full_input_path = joinpath(m.tempfile, inputname)
    full_output_path = joinpath(m.tempfile, outputname)
    sdpa_gmp_binary_solve!(m, full_input_path, full_output_path, redundant_entries = redundant_F)
    m.solve_time = time() - start_time
end

function MOI.get(m::Optimizer, ::MOI.TerminationStatus)
    status = m.phasevalue
    if status == :noINFO
        return MOI.OPTIMIZE_NOT_CALLED
    elseif status == :pFEAS
        return MOI.SLOW_PROGRESS
    elseif status == :dFEAS
        return MOI.SLOW_PROGRESS
    elseif status == :pdFEAS
        return MOI.OPTIMAL
    elseif status == :pdINF
        return MOI.INFEASIBLE_OR_UNBOUNDED
    elseif status == :pFEAS_dINF
        return MOI.DUAL_INFEASIBLE
    elseif status == :pINF_dFEAS
        return MOI.INFEASIBLE
    elseif status == :pdOPT
        return MOI.OPTIMAL
    elseif status == :pUNBD
        return MOI.DUAL_INFEASIBLE
    elseif status == :dUNBD
        return MOI.INFEASIBLE
    end
end

function MOI.get(m::Optimizer, ::MOI.PrimalStatus)
    status = m.phasevalue
    if status == :noINFO
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == :pFEAS
        return MOI.FEASIBLE_POINT
    elseif status == :dFEAS
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == :pdFEAS
        return MOI.FEASIBLE_POINT
    elseif status == :pdINF
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == :pFEAS_dINF
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == :pINF_dFEAS
        return MOI.INFEASIBLE_POINT
    elseif status == :pdOPT
        return MOI.FEASIBLE_POINT
    elseif status == :pUNBD
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == :dUNBD
        return MOI.INFEASIBLE_POINT
    end
end

function MOI.get(m::Optimizer, ::MOI.DualStatus)
    status = m.phasevalue
    if status == :noINFO
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == :pFEAS
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == :dFEAS
        return MOI.FEASIBLE_POINT
    elseif status == :pdFEAS
        return MOI.FEASIBLE_POINT
    elseif status == :pdINF
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == :pFEAS_dINF
        return MOI.INFEASIBLE_POINT
    elseif status == :pINF_dFEAS
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == :pdOPT
        return MOI.FEASIBLE_POINT
    elseif status == :pUNBD
        return MOI.INFEASIBLE_POINT
    elseif status == :dUNBD
        return MOI.INFEASIBILITY_CERTIFICATE
    end
end

MOI.get(m::Optimizer, ::MOI.ResultCount) = 1
function MOI.get(m::Optimizer, ::MOI.ObjectiveValue)
    return m.objsign * m.primalobj + m.objconstant
end
function MOI.get(m::Optimizer, ::MOI.DualObjectiveValue)
    return m.objsign * m.dualobj + m.objconstant
end
struct PrimalSolutionMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::PrimalSolutionMatrix) = true
MOI.get(optimizer::Optimizer, ::PrimalSolutionMatrix) = optimizer.X

struct DualSolutionVector <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSolutionVector) = true
function MOI.get(optimizer::Optimizer, ::DualSolutionVector)
    return optimizer.y
end

struct DualSlackMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSlackMatrix) = true
MOI.get(optimizer::Optimizer, ::DualSlackMatrix) = optimizer.Z

function block(optimizer::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    return optimizer.varmap[ci.value][1]
end
function dimension(optimizer::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    blockdim = optimizer.blockdims[block(optimizer, ci)]
    if blockdim < 0
        return -blockdim
    else
        return MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(blockdim))
    end
end
function vectorize_block(M, blk::Integer, s::Type{MOI.Nonnegatives})
    return diag(block(M, blk))
end
function vectorize_block(M::AbstractMatrix{T}, blk::Integer, s::Type{MOI.PositiveSemidefiniteConeTriangle}) where T
    B = block(M, blk)
    d = LinearAlgebra.checksquare(B)
    n = MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(d))
    v = Vector{T}(undef, n)
    k = 0
    for j in 1:d
        for i in 1:j
            k += 1
            v[k] = B[i, j]
        end
    end
    @assert k == n
    return v
end

function MOI.get(optimizer::Optimizer, ::MOI.VariablePrimal, vi::MOI.VariableIndex)
    blk, i, j = varmap(optimizer, vi)
    return block(MOI.get(optimizer, PrimalSolutionMatrix()), blk)[i, j]
end

function MOI.get(optimizer::Optimizer, ::MOI.ConstraintPrimal,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, S}) where S<:SupportedSets
    return vectorize_block(MOI.get(optimizer, PrimalSolutionMatrix()), block(optimizer, ci), S)
end
function MOI.get(m::Optimizer, ::MOI.ConstraintPrimal, ci::AFFEQ)
    return m.b[ci.value]
end

function MOI.get(optimizer::Optimizer, ::MOI.ConstraintDual,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, S}) where S<:SupportedSets
    return vectorize_block(MOI.get(optimizer, DualSlackMatrix()), block(optimizer, ci), S)
end
function MOI.get(optimizer::Optimizer, ::MOI.ConstraintDual, ci::AFFEQ)
    return -MOI.get(optimizer, DualSolutionVector())[ci.value]
end


module DynamicsUtils

using NQCDynamics:
    AbstractSimulation,
    Simulation,
    RingPolymerSimulation,
    Calculators,
    masses

divide_by_mass!(dv, masses) = dv ./= masses'
velocity!(dr, v, r, sim, t) = dr .= v

"""
    apply_interbead_coupling!(du::DynamicalVariables, u::DynamicalVariables,
                              sim::RingPolymerSimulation)

Applies the force that arises from the harmonic springs between adjacent beads.

Only applies the force for atoms labelled as quantum within the `RingPolymerParameters`.
"""
function apply_interbead_coupling!(dr::AbstractArray{T,3}, r::AbstractArray{T,3}, sim::RingPolymerSimulation) where {T}
    for i in axes(dr, 3)
        iplus = mod1(i+1, sim.beads.n_beads)
        iminus = mod1(i-1, sim.beads.n_beads)
        for j in sim.beads.quantum_atoms
            for k in axes(dr, 1)
                dr[k,j,i] -= sim.beads.ω_n² * (2r[k,j,i] - r[k,j,iplus] - r[k,j,iminus])
            end
        end
    end
    return nothing
end

function classical_hamiltonian end

function classical_kinetic_energy(sim::Simulation, v::AbstractMatrix)
    kinetic = zero(eltype(v))
    for i in axes(v, 2)
        for j in axes(v, 1)
            kinetic += masses(sim, i) * v[j,i]^2
        end
    end
    return kinetic / 2
end

function classical_kinetic_energy(sim::RingPolymerSimulation, v::AbstractArray{T,3}) where {T}
    kinetic = zero(eltype(v))
    for k in axes(v, 3)
        for i in axes(v, 2)
            for j in axes(v, 1)
                kinetic += masses(sim, i) * v[j,i,k]^2
            end
        end
    end
    return kinetic / 2
end

function classical_potential_energy(sim::Simulation, r::AbstractMatrix)
    Calculators.evaluate_potential!(sim.calculator, r)
    sim.calculator.potential
end

function classical_potential_energy(sim::RingPolymerSimulation, r::AbstractArray{T,3}) where {T}
    Calculators.evaluate_potential!(sim.calculator, r)
    sum(sim.calculator.potential) + RingPolymers.get_spring_energy(sim.beads, masses(sim), r)
end

include("dynamics_variables.jl")
include("callbacks.jl")
export CellBoundaryCallback
export TerminatingCallback

include("density_matrix_dynamics.jl")
include("plot.jl")

end # module