"""
`module  JAC.RayleighCompton`
    ... a submodel of JAC that contains all methods for computing elastic Rayleigh and inelastic Compton photon scattering cross sections.
"""
module RayleighCompton

    using Printf, ..AngularMomentum, ..AtomicState, ..Basics, ..Defaults, ..ManyElectron, ..Radial, ..TableStrings

    """
    `struct  RayleighCompton.Settings`  ... defines a type for the settings in estimating lastic Rayleigh and inelastic Compton
                                            photon scattering cross sections

        + multipoles              ::Array{EmMultipole}      ... Specifies the multipoles of the radiation field that are to be included.
        + gauges                  ::Array{UseGauge}         ... Specifies the gauges to be included into the computations.
        + photonEnergies          ::Array{Float64,1}        ... List of photon energies in units defined by the user.
        + green                   ::Array{GreenChannel,1}   ... Precalculated and user-specified Green function of the ion.
        + calcAngular             ::Bool                    ... Calculate the angular distribution of the fluorescence photon.
        + calcStokes              ::Bool                    ... Calculate the Stokes parameters of the Rayleigh-scattered photon.
        + printBefore             ::Bool                    ... True, if all energies and lines are printed before their evaluation.
        + incidentStokes          ::ExpStokes               ... Stokes parameters of the incident radiation.
        + solidAngles             ::Array{SolidAngle,1}     ... List of solid angles [(theta_1, pho_1), ...].
        + lineSelection           ::LineSelection           ... Specifies the selected levels, if any.

    """
    struct Settings
        multipoles                ::Array{EmMultipole}
        gauges                    ::Array{UseGauge}
        photonEnergies            ::Array{Float64,1}
        green                     ::Array{AtomicState.GreenChannel,1}
        calcAngular               ::Bool
        calcStokes                ::Bool
        printBefore               ::Bool
        incidentStokes            ::ExpStokes
        solidAngles               ::Array{SolidAngle,1}
        lineSelection             ::LineSelection
    end

    """
    `RayleighCompton.Settings()`  ... constructor for the default values of Rayleigh-Compton photon-scattering estimates.
    """
    function Settings()
        Settings(EmMultipole[], UseGauge[], GreenChannel[], 0., false, false, false, ExpStokes(), SolidAngle[], LineSelection() )
    end

    # `Base.show(io::IO, settings::RayleighCompton.Settings)`  ... prepares a proper printout of the variable settings::RayleighCompton.Settings.
    function Base.show(io::IO, settings::RayleighCompton.Settings)
        println(io, "multipoles:               $(settings.multipoles)  ")
        println(io, "gauges:                   $(settings.gauges)  ")
        println(io, "photonEnergies:           $(settings.photonEnergies)  ")
        println(io, "green:                     (settings.green)  ")
        println(io, "calcAngular:              $(settings.calcAngular)  ")
        println(io, "calcStokes:               $(settings.calcStokes)  ")
        println(io, "printBefore:              $(settings.printBefore)  ")
        println(io, "incidentStokes:           $(settings.incidentStokes)  ")
        println(io, "solidAngles:              $(settings.solidAngles)  ")
        println(io, "lineSelection:            $(settings.lineSelection)  ")
    end

    """
    `struct  RayleighCompton.ReducedChannel`
        ... defines a type for a Rayleigh-Compton reduced channel/ U^(K, Compton) amplitude to help characterize the scattering of
            a single photon with well-defined multipolarities; in JAC, a Rayleigh-Compton channel is characterized by a reduced amplitude
            U^(K, Compton) (level_f, multipole_2, J_nu, omega, multipole_1, level_i).

        + K              ::AngularJ64           ... Rank K of the channel.
        + multipole1     ::EmMultipole          ... Multipole of the incoming photon.
        + multipole2     ::EmMultipole          ... Multipole of the outgoing, scattered photon.
        + gauge          ::EmGauge              ... Gauge for dealing with the (coupled) radiation field.
        + omega1         ::Float64              ... Photon frequency of multipole1.
        + omega2         ::Float64              ... Photon frequency of multipole2.
        + omega          ::Float64              ... Photon frequency in denominator.
        + Jsym           ::LevelSymmetry        ... Symmetry J_nu of the intermediate levels.
        + amplitude      ::Complex{Float64}     ... Two-photon reduced amplitude associated with the given channel.
   """
    struct  ReducedChannel
        K                ::AngularJ64
        multipole1       ::EmMultipole
        multipole2       ::EmMultipole
        gauge            ::EmGauge
        omega1           ::Float64
        omega2           ::Float64
        omega            ::Float64
        Jsym             ::LevelSymmetry
        amplitude        ::Complex{Float64}
    end

    """
    `struct  RayleighCompton.Line`  ... defines a type for a Rayleigh-Compton scattering line that may include the definition of channels.

        + initialLevel     ::Level              ... initial-(state) level
        + finalLevel       ::Level              ... final-(state) level
        + inOmega          ::Float64            ... Energy of the incoming photon.
        + outOmega         ::Float64            ... Energy of the outgoing, scattered photon.
        + crossSection     ::EmProperty         ... Cross section for this photoionization.
        + channels         ::Array{RayleighCompton.ReducedChannel,1}    ... List of (reduced) RayleighCompton.Channels of this line.
    """
    struct  Line
        initialLevel       ::Level
        finalLevel         ::Level
        inOmega            ::Float64
        outOmega           ::Float64
        crossSection       ::EmProperty
        channels           ::Array{RayleighCompton.ReducedChannel,1}
    end

    # `Base.show(io::IO, line::RayleighCompton.Line)`  ... prepares a proper printout of the variable line::RayleighCompton.Line.
    function Base.show(io::IO, line::RayleighCompton.Line)
        println(io, "initialLevel:      $(line.initialLevel)  ")
        println(io, "finalLevel:        $(line.finalLevel)  ")
        println(io, "inOmega:           $(line.inOmega)  ")
        println(io, "outOmega:          $(line.outOmega)  ")
        println(io, "crossSection:      $(line.crossSection)  ")
        println(io, "channels:          $(line.channels)  ")
    end

    """
    `RayleighCompton.computeAmplitudesProperties(line::RayleighCompton.Line, grid::Radial.Grid, settings::RayleighCompton.Settings)`
        ... to compute all amplitudes and properties of the given line; a line::RayleighCompton.Line is returned for which
            the amplitudes and properties are now evaluated.
    """
    function  computeAmplitudesProperties(line::RayleighCompton.Line, grid::Radial.Grid, settings::RayleighCompton.Settings)
        newChannels = RayleighCompton.ReducedChannel[]
        for ch in line.channels
            amplitude = RayleighCompton.computeReducedAmplitude(ch.K, line.finalLevel, ch.multipole2, ch.omega2, ch.Jsym, ch.omega,
                                                                ch.multipole1, ch.omega1, line.initialLevel, ch.gauge, grid, settings.green)
            push!( newChannels, ReducedChannel(ch.K, ch.multipole1, ch.multipole2, ch.gauge, ch.omega1, ch.omega2, ch.omega, ch.Jsym, amplitude) )
        end
        cs_Cou       = 0.;   cs_Bab = 0.
        crossSection = EmProperty(cs_Cou, cs_Bab)
        newLine      = RayleighCompton.Line(line.initialLevel, line.finalLevel, line.inOmega, line.outOmega, crossSection, newChannels)
        return( newLine )
    end

    """
    `RayleighCompton.computeLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, grid::Radial.Grid, settings::RayleighCompton.Settings; output=true)`
        ... to compute the Rayleigh-Compton transition amplitudes and all properties as requested by the given settings. A list of
            lines::Array{RayleighCompton.Lines} is returned.
    """
    function  computeLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, grid::Radial.Grid, settings::RayleighCompton.Settings; output=true)
        println("")
        printstyled("RayleighCompton.computeLines(): The computation of Rayleigh-Compton reduced amplitudes starts now ... \n", color=:light_green)
        printstyled("----------------------------------------------------------------------------------------------------- \n", color=:light_green)
        println("")
        #
        lines = RayleighCompton.determineLines(finalMultiplet, initialMultiplet, settings)
        # Display all selected pathways before the computations start
        if  settings.printBefore    RayleighCompton.displayLines(stdout, lines)    end
        # Calculate all reduced amplitudes and requested properties
        newLines = RayleighCompton.Line[]
        for  line in lines
            newLine = RayleighCompton.computeAmplitudesProperties(line, grid, settings)
            push!( newLines, newLine)
        end
        # Print all results to screen
        RayleighCompton.displayCrossSections(stdout, newLines, settings)
        if  settings.calcAngular        RayleighCompton.displayAngular(stdout, newLines, settings)             end
        if  settings.calcStokes         RayleighCompton.displayStokesParameters(stdout, newLines, settings)    end
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary
            if  settings.calcAngular    RayleighCompton.displayAngular(stdout, newLines, settings)             end
            if  settings.calcStokes     RayleighCompton.displayStokesParameters(stdout, newLines, settings)    end
        end

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

    """
    `RayleighCompton.computeReducedAmplitude(K::AngularJ64, finalLevel::Level, multipole2::EmMultipole, omega2::Float64, Jsym::LevelSymmetry,
                                             omega::Float64, multipole1::EmMultipole, omega1::Float64, initialLevel::Level, gauge::EmGauge,
                                             grid::Radial.Grid, greenChannels::Array{AtomicState.GreenChannel,1})`
        ... to compute the reduced amplitudes U^(K, Compton) as well as all properties of the given line;
            a line::RayleighCompton.Line is returned for which the reduced amplitudes and properties are now evaluated.
    """
    function  computeReducedAmplitude(K::AngularJ64, finalLevel::Level, multipole2::EmMultipole, omega2::Float64, Jsym::LevelSymmetry,
                                      omega::Float64, multipole1::EmMultipole, omega1::Float64, initialLevel::Level, gauge::EmGauge,
                                      grid::Radial.Grid, greenChannels::Array{AtomicState.GreenChannel,1})
        amplitude = 0.0im
        return( amplitude )
    end

    """
    `RayleighCompton.determineChannels(finalLevel::Level, initialLevel::Level, inOmega::Float64, outOmega::Float64, settings::RayleighCompton.Settings)`
        ... to determine a list of Rayleigh-Compton ReducedChannelChannel's for a transitions from the initial to final level and by taking
            into account the particular settings of for this computation; an Array{RayleighCompton.ReducedChannelChannel,1} is returned.
    """
    function determineChannels(finalLevel::Level, initialLevel::Level, inOmega::Float64, outOmega::Float64, settings::RayleighCompton.Settings)
        channels = RayleighCompton.ReducedChannel[];
        symi = LevelSymmetry(initialLevel.J, initialLevel.parity);    symf = LevelSymmetry(finalLevel.J, finalLevel.parity)
        for  mp1 in settings.multipoles
            for  mp2 in settings.multipoles
                symmetries  = AngularMomentum.allowedTotalSymmetries(symf, mp2, mp1, symi)
                Klist       = oplus(symf.J, symi.J)
                for  symn in symmetries
                    for  gauge in settings.gauges
                        # Include further restrictions if appropriate
                        if     string(mp1)[1] == 'E' || string(mp2)[1] == 'E'  &&   gauge == Basics.UseCoulomb
                            for K in Klist  push!(channels, ReducedChannel(K, mp1, mp2, Basics.Coulomb,   inOmega, outOmega, inOmega, symn, 0.) )  end
                        elseif string(mp1)[1] == 'E' || string(mp2)[1] == 'E'  &&   gauge == Basics.UseBabushkin
                            for K in Klist  push!(channels, ReducedChannel(K, mp1, mp2, Basics.Babushkin, inOmega, outOmega, inOmega, symn, 0.) )  end
                        elseif string(mp1)[1] == 'M' && string(mp2)[1] == 'M'
                            for K in Klist  push!(channels, ReducedChannel(K, mp1, mp2, Basics.Magnetic,  inOmega, outOmega, inOmega, symn, 0.) )  end
                        end
                    end
                end
            end
        end
        # Unify channels
        return( channels )
    end

    """
    `RayleighCompton.determineLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, settings::RayleighCompton.Settings)`
        ... to determine a list of RayleighCompton Line's for elastic scattering or Raman-type transitions between the levels from
            the given initial- and final-state multiplets and by taking into account the particular selections and settings for
            this computation; an Array{RayleighCompton.Line,1} is returned. Apart from the level specification, all physical
            properties are set to zero during the initialization process.
    """
    function  determineLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, settings::RayleighCompton.Settings)
        lines = RayleighCompton.Line[]
        for  iLevel  in  initialMultiplet.levels
            for  fLevel  in  finalMultiplet.levels
                if  Basics.selectLevelPair(iLevel, fLevel, settings.lineSelection)
                    for  omega in settings.photonEnergies
                        inOmega  = Defaults.convertUnits("energy: to atomic", omega)
                        outOmega = inOmega - (fLevel.energy - iLevel.energy);       if  outOmega <= 0.    continue   end
                        channels = RayleighCompton.determineChannels(fLevel, iLevel, inOmega, outOmega, settings)
                        if   length(channels) == 0   continue   end
                        push!( lines, RayleighCompton.Line(iLevel, fLevel, inOmega, outOmega, EmProperty(0., 0.), channels) )
                    end
                end
            end
        end
        return( lines )
    end

    """
    `RayleighCompton.displayAngular(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)`
        ... to display a list of lines and the angle-differential cross sections for the selected solid angles.
            A neat table of all selected transitions, energies and solid angles is printed but nothing is returned otherwise.
    """
    function  displayAngular(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)
        nx = 127
        println(stream, " ")
        println(stream, "  Angular distribution of the selected Rayleigh-Compton scattering lines at seleced solid angles: ... to be adapted !!")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "   ";   sb = "   ";   sc = "  "
        sa = sa * TableStrings.center(16, "i-level-f"; na=2);                         sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(16, "i--J^P--f"; na=7);                         sb = sb * TableStrings.hBlank(23)
        sa = sa * TableStrings.center(20, "i -- Energy -- f"; na=3);
        sb = sb * TableStrings.center(20, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(24, "omega-in   omega-out"; na=2);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=1)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=6)
        println(stream, sa[1:end]);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  line in lines
            sa  = "";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                         fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.initialLevel.energy)) * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.finalLevel.energy))   * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.inOmega))      * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.outOmega))     * "       "
            println(stream, sa )
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `RayleighCompton.displayCrossSections(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)`
        ... to display a list of lines and their cross sections as they were selected by the settings. A neat table of all selected
            transitions, energies and cross sections is printed but nothing is returned otherwise.
    """
    function  displayCrossSections(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)
        nx = 127
        println(stream, " ")
        println(stream, "  Cross sections of the selected Rayleigh-Compton scattering lines:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "   ";   sb = "   ";   sc = "  "
        sa = sa * TableStrings.center(16, "i-level-f"; na=2);                         sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(16, "i--J^P--f"; na=7);                         sb = sb * TableStrings.hBlank(23)
        sa = sa * TableStrings.center(20, "i -- Energy -- f"; na=3);
        sb = sb * TableStrings.center(20, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(24, "omega-in   omega-out"; na=2);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=1)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=6)
        sa = sa * TableStrings.center(28, "Cou -- cross section -- Bab"; na=0);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("cross section"); na=3)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("cross section"); na=4)
        println(stream, sa[1:end]);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  line in lines
            sa  = "";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                         fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.initialLevel.energy)) * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.finalLevel.energy))   * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.inOmega))      * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.outOmega))     * "       "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("cross section: from atomic", line.crossSection.Coulomb))       * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("cross section: from atomic", line.crossSection.Babushkin))     * "   "
            println(stream, sa )
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `RayleighCompton.displayLines(stream::IO, lines::Array{RayleighCompton.Line,1})`
        ... to display a list of lines and channels that have been selected due to the prior settings. A neat table of all selected
            transitions and energies is printed but nothing is returned otherwise.
    """
    function  displayLines(stream::IO, lines::Array{RayleighCompton.Line,1})
        nx = 157
        println(stream, " ")
        println(stream, "  Selected Rayleigh-Compton scacttering lines and (reduced) channels:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "   ";   sb = "   ";   sc = "  "
        sa = sa * TableStrings.center(16, "i-level-f"; na=2);                         sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(16, "i--J^P--f"; na=7);                         sb = sb * TableStrings.hBlank(23)
        sa = sa * TableStrings.center(20, "i -- Energy -- f"; na=3);
        sb = sb * TableStrings.center(20, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(24, "omega-in   omega-out"; na=0);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=1)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=4)
        sa = sa * TableStrings.center(11, "No channels"; na=2);                       sb = sb * TableStrings.hBlank(11)
        sa = sa * TableStrings.flushleft(60, "List of multipoles & intermediate level symmetries";   na=1)
        sb = sb * TableStrings.flushleft(60, "(K-rank, multipole_1, Jsym, multipole_2, gauge), ..."; na=1)
        println(stream, sa[1:end]);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  line in lines
            sa  = "";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                         fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.initialLevel.energy)) * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.finalLevel.energy))   * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.inOmega))      * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.outOmega))     * "   "
            sc = "         " * string(length(line.channels)) * "      "
            sa = sa * sc[end-12:end]
            mpGaugeList = Tuple{AngularJ64, Basics.EmMultipole, LevelSymmetry, Basics.EmMultipole, Basics.EmGauge}[]
            for  channel in  line.channels
                push!( mpGaugeList, (channel.K, channel.multipole1, channel.Jsym, channel.multipole2, channel.gauge) )
            end
            wa = TableStrings.twoPhotonGaugeTupels(60, mpGaugeList)
            if  length(wa) > 0    sb = sa * wa[1];    println( sb )    end
            for  i = 1:length(wa)
                sb = TableStrings.hBlank( length(sa) );    sb = sb * wa[i];    println(stream, sb )
            end
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

    """
    `RayleighCompton.displayStokesParameters(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)`
        ... to display a list of lines and the Stokes parameters at the selected solid angles.
            A neat table of all selected transitions, energies and solid angles is printed but nothing is returned otherwise.
    """
    function  displayStokesParameters(stream::IO, lines::Array{RayleighCompton.Line,1}, settings::RayleighCompton.Settings)
        nx = 127
        println(stream, " ")
        println(stream, "  StokesParameters of the selected Rayleigh-Compton scattering lines at seleced solid angles: ... to be adapted !!")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(nx))
        sa = "   ";   sb = "   ";   sc = "  "
        sa = sa * TableStrings.center(16, "i-level-f"; na=2);                         sb = sb * TableStrings.hBlank(18)
        sa = sa * TableStrings.center(16, "i--J^P--f"; na=7);                         sb = sb * TableStrings.hBlank(23)
        sa = sa * TableStrings.center(20, "i -- Energy -- f"; na=3);
        sb = sb * TableStrings.center(20, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(24, "omega-in   omega-out"; na=2);
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=1)
        sb = sb * TableStrings.center(10, TableStrings.inUnits("energy"); na=6)
        println(stream, sa[1:end]);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(nx))
        #
        for  line in lines
            sa  = "";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                         fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.initialLevel.energy)) * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.finalLevel.energy))   * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.inOmega))      * " "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", line.outOmega))     * "       "
            println(stream, sa )
        end
        println("  ", TableStrings.hLine(nx))
        #
        return( nothing )
    end

end # module


################################################################################
#
#  Field access
#
################################################################################

function elem_type(::Type{RelOrdQuoRing{T1, T2, T3}}) where { T1, T2, T3 }
  S = elem_type(T1)
  return RelOrdQuoRingElem{T1, T2, T3, S}
end

function elem_type(::RelOrdQuoRing{T1, T2, T3}) where { T1, T2, T3 }
  S = elem_type(T1)
  return RelOrdQuoRingElem{T1, T2, T3, S}
end

base_ring(Q::RelOrdQuoRing) = Q.base_ring

ideal(Q::RelOrdQuoRing) = Q.ideal

basis_pmatrix(Q::RelOrdQuoRing) = Q.basis_pmatrix

parent(x::RelOrdQuoRingElem) = x.parent

parent_type(::Type{RelOrdQuoRingElem{T1, T2, T3, S}}) where { T1, T2, T3, S } = RelOrdQuoRing{T1, T2, T3}

################################################################################
#
#  Hashing
#
################################################################################

hash(x::RelOrdQuoRingElem, h::UInt) = hash(x.elem, h)

################################################################################
#
#  Copying
#
################################################################################

Base.deepcopy_internal(x::RelOrdQuoRingElem, dict::IdDict) =
        RelOrdQuoRingElem(parent(x), Base.deepcopy_internal(x.elem, dict))

################################################################################
#
#  I/O
#
################################################################################

function show(io::IO, Q::RelOrdQuoRing)
  print(io, "Quotient of $(base_ring(Q))")
end

function show(io::IO, x::RelOrdQuoRingElem)
  print(io, "$(x.elem)")
end

################################################################################
#
#  Parent object overloading
#
################################################################################

function (Q::RelOrdQuoRing{T1, T2, T3})(x::RelOrdQuoRingElem{T1, T2, T3, S}) where { T1, T2, T3, S }
  parent(x) !== Q && error("Cannot coerce element into quotient ring")
  return x
end

function (Q::RelOrdQuoRing{T1, T2, T3})(x::S) where { T1, T2, T3, S }
  parent(x) !== base_ring(Q) && error("Cannot coerce element into the quotient ring")
  return RelOrdQuoRingElem(Q, x)
end

function (Q::RelOrdQuoRing)(x::Integer)
  return Q(base_ring(Q)(x))
end

function (Q::RelOrdQuoRing)(x::fmpz)
  return Q(base_ring(Q)(x))
end

################################################################################
#
#  Quotient function
#
################################################################################

function quo(O::Union{NfRelOrd, AlgAssRelOrd}, I::Union{NfRelOrdIdl, AlgAssRelOrdIdl})
  @assert order(I) === O
  # We should check that I is not zero
  Q = RelOrdQuoRing(O, I)
  f = RelOrdQuoMap(O, Q)
  return Q, f
end

Nemo.ResidueRing(O::Union{NfRelOrd, AlgAssRelOrd}, I::Union{NfRelOrdIdl, AlgAssRelOrdIdl}) = RelOrdQuoRing(O, I)

################################################################################
#
#  Parent check
#
################################################################################

function check_parent(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  if parent(x) !== parent(y)
    error("Elements must have same parents")
  end
  return true
end

################################################################################
#
#  Arithmetic
#
################################################################################

function +(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  check_parent(x, y)
  return parent(x)(x.elem + y.elem)
end

function -(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  check_parent(x, y)
  return parent(x)(x.elem - y.elem)
end

function -(x::RelOrdQuoRingElem)
  return parent(x)(-x.elem)
end

function *(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  check_parent(x, y)
  return parent(x)(x.elem * y.elem)
end

function mul!(z::RelOrdQuoRingElem, x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  z.elem = mul!(z.elem, x.elem, y.elem)
  z.elem = mod!(z.elem, parent(z))
  return z
end

function add!(z::RelOrdQuoRingElem, x::RelOrdQuoRingElem, y::RelOrdQuoRingElem)
  z.elem = add!(z.elem, x.elem, y.elem)
  z.elem = mod!(z.elem, parent(z))
  return z
end

function *(x::Integer, y::RelOrdQuoRingElem)
  return parent(y)(x * y.elem)
end

*(x::RelOrdQuoRingElem, y::Integer) = y*x

function *(x::fmpz, y::RelOrdQuoRingElem)
  return parent(y)(x * y.elem)
end

*(x::RelOrdQuoRingElem, y::fmpz) = y*x

function ^(a::RelOrdQuoRingElem, f::fmpz)
  if fits(Int, f)
    return a^Int(f)
  end
  f == 0 && return one(parent(a))
  f == 1 && return deepcopy(a)
  if f < 0
    f = -f
    a = inv(a)
  end
  b = a^(div(f, 2))
  b *= b
  if isodd(f)
    b *= a
  end
  return b
end

function ^(a::RelOrdQuoRingElem, b::Int)
  if b == 0
    return one(parent(a))
  elseif b == 1
    return deepcopy(a)
  else
    if b < 0
      a = inv(a)
      b = -b
    end
    bit = ~((~UInt(0)) >> 1)
    while (UInt(bit) & b) == 0
      bit >>= 1
    end
    z = deepcopy(a)
    bit >>= 1
    while bit != 0
      z = mul!(z, z, z)
      if (UInt(bit) & b) != 0
        z = mul!(z, z, a)
      end
      bit >>= 1
    end
    return z
  end
end

################################################################################
#
#  Special elements
#
################################################################################

iszero(x::RelOrdQuoRingElem) = iszero(x.elem)

isone(x::RelOrdQuoRingElem) = isone(x.elem)

function one(Q::RelOrdQuoRing)
  return Q(one(base_ring(Q)))
end

function zero(Q::RelOrdQuoRing)
  return Q(zero(base_ring(Q)))
end

################################################################################
#
#  Equality
#
################################################################################

function ==(x::RelOrdQuoRing, y::RelOrdQuoRing)
  return base_ring(x) === base_ring(y) && ideal(x) == ideal(y)
end

==(x::RelOrdQuoRingElem, y::RelOrdQuoRingElem) = x.elem == y.elem


#=
Final parsing of preparsing structures

Numbers and variables will finaly
=#
include("ofMeshDictPreParser.jl")

abstract type OfEntity end
abstract type AbstractOfVar <: OfEntity end
abstract type AbstractOfVal <: OfEntity end

struct OfDictionary <: OfEntity
    name::String
    entries::Vector{OfEntity}
end

# array list of same entities with length
struct OfArray <: OfEntity
    length::Int64
    entries::Vector{OfEntity}
end

struct OfList<: OfEntity
    length::Int64
    entries::Vector{OfEntity}
end

struct OfVariable{T} <: AbstractOfVar
    name::String
    value::T
end

struct OfValue{T} <: AbstractOfVal
    value::T
end

"""
    dfs_parse(parse_entities::Vector{PreParsedEntity}, tokens::Vector{Token})

parses pre parsed vector structure to finally parsed file structure
"""
function dfs_parse(parse_entities::Vector{PreParsedEntity}, tokens::Vector{Token})

    of_entities = Vector{OfEntity}(undef, 0)

    for parse_elem in parse_entities
        if typeof(parse_elem) == PreParsedVariable
            push!(of_entities, parse_of_variable(parse_elem, tokens))
        elseif typeof(parse_elem) == PreParsedFNum
            push!(of_entities, OfValue(parse(Float64,parse_elem.string)))
        elseif typeof(parse_elem) == PreParsedINum
            push!(of_entities, OfValue(parse(Int64,parse_elem.string)))
        elseif typeof(parse_elem) == PreParsedDictonary
            mypush!(of_entities, OfDictionary(parse_elem.name,
                        dfs_parse(parse_elem.entries, tokens)))
        elseif typeof(parse_elem) == PreParsedArray
            mypush!(of_entities, OfArray(parse_elem.length,
                        dfs_parse(parse_elem.entries, tokens)))
        elseif typeof(parse_elem) == PreParsedList
            parsed_list = dfs_parse(parse_elem.entries, tokens)
            mypush!(of_entities, OfList(length(parsed_list), parsed_list))
        end
    end

    return of_entities
end

function mypush!(of_entities::Vector{OfEntity}, OfEntity::T) where T <: OfEntity
    push!(of_entities, OfEntity)
end

function mypush!(of_entities::Vector{OfEntity},
                    entities::Vector{T}) where T <: OfEntity
    for e in entities
        push!(of_entities, e)
    end
end

"""
    parse_of_variable(pe::PreParsedEntity, tokens::Vector{Token})

Parsing of a PreParsedEntity to final type OfVariable, which completes parsing.
"""
# TODO: only works for numbers and strings by now
function parse_of_variable(pe::PreParsedEntity, tokens::Vector{Token})::OfVariable

    name = ""
    value = []

    if tokens[pe.id_token_from].type_id == TokenTypeID_Dict[:word]
        name = tokens[pe.id_token_from].string

        if ( (pe.id_token_to - pe.id_token_from) == 2 )
            if  (tokens[pe.id_token_from+1].type_id ==
                 TokenTypeID_Dict[:int_number])

                value = parse(Int64, tokens[pe.id_token_from+1].string)

            elseif (tokens[pe.id_token_from+1].type_id ==
                    TokenTypeID_Dict[:float_number])

                value = parse(Float64, tokens[pe.id_token_from+1].string)

            elseif (tokens[pe.id_token_from+1].type_id
                    == TokenTypeID_Dict[:word])

                value = tokens[pe.id_token_from+1].string

            elseif (tokens[pe.id_token_from+1].type_id
                == TokenTypeID_Dict[:string])

                value = tokens[pe.id_token_from+1].string

            else
                value = nothing
            end
        elseif ( ((pe.id_token_to - pe.id_token_from) == 3) &&
                 (tokens[pe.id_token_from+1].type_id ==
                        TokenTypeID_Dict[:minus]) )

            if  (tokens[pe.id_token_from+2].type_id ==
                TokenTypeID_Dict[:int_number])

                value = parse(Int64, tokens[pe.id_token_from+1].string *
                                        tokens[pe.id_token_from+2].string)

            elseif (tokens[pe.id_token_from+2].type_id ==
                    TokenTypeID_Dict[:float_number])

                 value = parse(Float64, tokens[pe.id_token_from+1].string *
                                        tokens[pe.id_token_from+2].string)
            else
                value =  nothing
            end

        else
            value = 0
        end
    else
        error("First Token of Variable should be a word!")
    end

    return OfVariable(name, value)
end

"""
    ofprint(ofvar::OfEntity, depth::Int64=0)

dfs print in case for single OfEntity
"""
function ofprint(ofvar::OfEntity, depth::Int64=0)
    ofprint([ofvar], depth)
end

"""
    ofprint(ofvars::Vector{OfEntity}, depth::Int64=0)

dfs print of Vector{OfEntity} to get a nicer view of finally parsed entities
"""
function ofprint(ofvars::Vector{OfEntity}, depth::Int64=0)
    for ofvar in ofvars
        if typeof(ofvar) <: AbstractOfVar
            println("\t"^depth,ofvar.name, ": ", ofvar.value)

        elseif typeof(ofvar) <: AbstractOfVal
            print(ofvar.value, " ")

        elseif typeof(ofvar) == OfDictionary
            println()
            println("\t"^depth,ofvar.name, " {")
            depth += 1
            ofprint(ofvar.entries, depth)
            depth -= 1
            println("\t"^depth,"}")
        elseif typeof(ofvar) == OfArray
            print("\t"^depth,ofvar.length, " (")
            depth += 1
            ofprint(ofvar.entries,depth)
            print(")\n")
            depth -= 1
        elseif typeof(ofvar) == OfList
            print("\t"^depth,"(")
            ofprint(ofvar.entries, depth)
            print(")\n")
        end
    end

end

"""
    get_dictionary(ofvars::Vector{OfEntity}, dict_name::string)

Returns Dictionary in first layer of ofvars with .name == dict_name
"""
function get_dictionary(ofvars::Vector{OfEntity}, dict_name::String)

    for f in ofvars
        if typeof(f) == OfDictionary && f.name == dict_name
            return f
        end
    end

    return nothing
end

"""
    get_variable(ofvars::Vector{OfEntity}, var_name::string)

Returns Variable in first layer of ofvars with .name == var_name
"""
function get_variable(ofvars::Vector{OfEntity}, var_name::String)

    for f in ofvars
        if typeof(f) <: AbstractOfVar && f.name == var_name
            return f
        end
    end

    return nothing
end

"""
    get_variable_value(ofvars::Vector{OfEntity}, var_name::string)

Returns Variable value in first layer of ofvars with .name == var_name
"""
function get_variable_value(ofvars::Vector{OfEntity}, var_name::String)

    var = get_variable(ofvars::Vector{OfEntity}, var_name::String)

    if !isnothing(var)
        return var.value
    else
        error("Variable ", var_name, " cannot be found")
    end

end


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

const _default_float_WT = Float64 # Default type for float weights
const _default_int_WT = Int # Default type for int weights
const _default_LDT = Float64 # Default type for log-density values

"""
    struct DensitySample

A weighted sample drawn according to an statistical density,
e.g. a [`BAT.AbstractDensity`](@ref).

Fields:
    * `v`: Multivariate parameter vector
    * `logd`: log of the value of the density at `v`
    * `weight`: Weight of the sample
    * `info`: Additional info on the provenance of the sample. Content depends
       on the sampling algorithm.
    * aux: Custom user-defined information attatched to the sample.

Constructors:

```julia
DensitySample(
    v::AbstractVector{<:Real},
    logd::Real,
    weight::Real,
    info::Any,
    aux::Any
)
```

Use [`DensitySampleVector`](@ref) to store vectors of multiple samples with
an efficient column-based memory layout.
"""
struct DensitySample{
    P,
    T<:Real,
    W<:Real,
    R,
    Q
}
    v::P
    logd::T
    weight::W
    info::R
    aux::Q
end

export DensitySample

# DensitySample behaves as a scalar type under broadcasting:
@inline Base.Broadcast.broadcastable(shape::DensitySample) = Ref(shape)

import Base.==
function ==(A::DensitySample, B::DensitySample)
    A.v == B.v && A.logd == B.logd &&
        A.weight == B.weight && A.info == B.info && A.aux == B.aux
end

function Base.similar(s::DensitySample{P,T,W,R,Q}) where {P<:AbstractVector{<:Real},T,W,R,Q}
    v = fill!(similar(s.v), eltype(s.v)(NaN))
    logd = convert(T, NaN)
    weight = zero(W)
    info = R()
    aux = Q()
    P_new = typeof(v)
    DensitySample{P_new,T,W,R,Q}(v, logd, weight, info, aux)
end

function _apply_shape(shape::AbstractValueShape, s::DensitySample)
    DensitySample(
        stripscalar(shape(s.v)),
        s.logd,
        s.weight,
        s.info,
        s.aux,
    )
end

(shape::AbstractValueShape)(s::DensitySample) = _apply_shape(shape, s)

"""
    DensitySampleVector

Type alias for `StructArrays.StructArray{<:DensitySample,...}`.

Constructors:

```julia
    DensitySampleVector(
        (
            v::AbstractVector{<:AbstractVector{<:Real}},
            logd::AbstractVector{<:Real},
            weight::AbstractVector{<:Real},
            info::AbstractVector{<:Any},
            aux::AbstractVector{<:Any}
        )
    )
```

```julia
    DensitySampleVector(
            v::AbstractVector,
            logval::AbstractVector{<:Real};
            weight::Union{AbstractVector{<:Real}, Symbol} = fill(1, length(eachindex(v))),
            info::AbstractVector = fill(nothing, length(eachindex(v))),
            aux::AbstractVector = fill(nothing, length(eachindex(v)))
        )
```
    For `weight = :RepetitionWeight`, repeated samples will be dropped and the weight of the corresponding sample will be increased.
"""
const DensitySampleVector{
    P,T<:AbstractFloat,W<:Real,R,Q,
    PV<:AbstractVector{P},TV<:AbstractVector{T},WV<:AbstractVector{W},RV<:AbstractVector{R},QV<:AbstractVector{Q}
} = StructArray{
    DensitySample{P,T,W,R,Q},
    1,
    NamedTuple{(:v, :logd, :weight, :info, :aux), Tuple{PV,TV,WV,RV,QV}}
}

export DensitySampleVector

function StructArray{DensitySample}(
    contents::Tuple{
        AbstractVector{P},
        AbstractVector{T},
        AbstractVector{W},
        AbstractVector{R},
        AbstractVector{Q},
    }
) where {P,T<:AbstractFloat,W<:Real,R,Q}
    v, logd, weight, info, aux = contents
    StructArray{DensitySample{P,T,W,R,Q}}(contents)
end

DensitySampleVector(contents::NTuple{5,Any}) = StructArray{DensitySample}(contents)

_create_undef_vector(::Type{T}, len::Integer) where T = Vector{T}(undef, len)

function DensitySampleVector{P,T,W,R,Q}(::UndefInitializer, len::Integer, npar::Integer) where {
    PT<:Real, P<:AbstractVector{PT}, T<:AbstractFloat, W<:Real, R, Q
}
    contents = (
        VectorOfSimilarVectors(ElasticArray{PT}(undef, npar, len)),
        Vector{T}(undef, len),
        Vector{W}(undef, len),
        _create_undef_vector(R, len),
        _create_undef_vector(Q, len)
    )

    DensitySampleVector(contents)
end

DensitySampleVector(::Type{S}, varlen::Integer) where {P<:AbstractVector{<:Real},T<:AbstractFloat,W<:Real,R,Q,S<:DensitySample{P,T,W,R,Q}} =
    DensitySampleVector{P,T,W,R,Q}(undef, 0, varlen)

function DensitySampleVector(
    v::AbstractVector,
    logval::AbstractVector{<:Real};
    weight::Union{AbstractVector{<:Real}, Symbol} = fill(1, length(eachindex(v))),
    info::AbstractVector = fill(nothing, length(eachindex(v))),
    aux::AbstractVector = fill(nothing, length(eachindex(v)))
)
    if weight == :RepetitionWeight
        idxs, weight = repetition_to_weights(v)
        return DensitySampleVector((ArrayOfSimilarArrays(v[idxs]), logval[idxs], weight, info[idxs], aux[idxs]))
    else
        return DensitySampleVector((ArrayOfSimilarArrays(v), logval, weight, info, aux))
    end
end

# Specialize getindex to properly support ArraysOfArrays and similar, preventing
# conversion to exact element type:

@inline Base.getindex(A::StructArray{<:DensitySample}, I::Int...) =
    DensitySample(A.v[I...], A.logd[I...], A.weight[I...], A.info[I...], A.aux[I...])

@inline Base.getindex(A::StructArray{<:DensitySample}, I::Union{Int,AbstractArray,Colon}...) =
    DensitySampleVector((A.v[I...], A.logd[I...], A.weight[I...], A.info[I...], A.aux[I...]))

# Specialize IndexStyle, current default for StructArray seems to be IndexCartesian()
Base.IndexStyle(::StructArray{<:DensitySample, 1}) = IndexLinear()

# Specialize comparison, currently StructArray seems fall back to `(==)(A::AbstractArray, B::AbstractArray)`
import Base.==
function(==)(A::DensitySampleVector, B::DensitySampleVector)
    A.v == B.v &&
    A.logd == B.logd &&
    A.weight == B.weight &&
    A.info == B.info &&
    A.aux == B.aux
end

function Base.merge!(X::DensitySampleVector, Xs::DensitySampleVector...)
    for Y in Xs
        append!(X, Y)
    end
    X
end

Base.merge(X::DensitySampleVector, Xs::DensitySampleVector...) = merge!(deepcopy(X), Xs...)

Base.@propagate_inbounds function _bcasted_apply_to_params(f, A::DensitySampleVector)
    DensitySampleVector((
        f.(A.v),
        A.logd,
        A.weight,
        A.info,
        A.aux
    ))
end

Base.copy(
    instance::Base.Broadcast.Broadcasted{
        <:Base.Broadcast.AbstractArrayStyle{1},
        <:Any,
        <:Union{AbstractValueShape,typeof(unshaped)},
        <:Tuple{DensitySampleVector}
    }
) = _bcasted_apply_to_params(instance.f, instance.args[1])

ValueShapes.varshape(A::DensitySampleVector) = elshape(A.v)

function _get_statw(f::Function, samples::DensitySampleVector, resultshape::AbstractValueShape)
    shape = varshape(samples)
    X = unshaped.(samples.v)
    w = FrequencyWeights(samples.weight)
    r_unshaped = f(X, w)
    resultshape(r_unshaped)
end

Statistics.mean(samples::DensitySampleVector) = _get_statw(mean, samples, varshape(samples))
Statistics.var(samples::DensitySampleVector) = _get_statw(var, samples, replace_const_shapes(ValueShapes.const_zero_shape, varshape(samples)))
Statistics.std(samples::DensitySampleVector) = _get_statw(std, samples, replace_const_shapes(ValueShapes.const_zero_shape, varshape(samples)))

function Statistics.median(samples::DensitySampleVector)
    shape = varshape(samples)
    flat_samples = flatview(unshaped.(samples.v))
    n_params = size(flat_samples)[1]
    median_params = Vector{Float64}()

    for param in Base.OneTo(n_params)
        median_param = quantile(flat_samples[param,:], FrequencyWeights(samples.weight), 0.5)
        push!(median_params, median_param)
    end
    shape(median_params)
end

function _get_stat(f::Function, samples::DensitySampleVector)
    shape = varshape(samples)
    X = unshaped.(samples.v)
    r_unshaped = f(X)
    shape(r_unshaped)
end

Base.minimum(samples::DensitySampleVector) = _get_stat(minimum, samples)
Base.maximum(samples::DensitySampleVector) = _get_stat(maximum, samples)

Statistics.cov(samples::DensitySampleVector{<:AbstractVector{<:Real}}) = cov(samples.v, FrequencyWeights(samples.weight))
Statistics.cor(samples::DensitySampleVector{<:AbstractVector{<:Real}}) = cor(samples.v, FrequencyWeights(samples.weight))

function _get_mode(samples::DensitySampleVector)
    shape = varshape(samples)
    i = findmax(samples.logd)[2]
    v_unshaped = unshaped.(samples.v)[i]
    v = copy(shape(v_unshaped))
    (v, i)
end

StatsBase.mode(samples::DensitySampleVector) = _get_mode(samples)[1]

"""
    drop_low_weight_samples(
        samples::DensitySampleVector,
        fraction::Real = 10^-4
    )

*BAT-internal, not part of stable public API.*

Drop `fraction` of the total probability mass from samples to filter out the
samples with the lowest weight.
"""
function drop_low_weight_samples(samples::DensitySampleVector, fraction::Real = 10^-5; threshold::Real=10^-2)
    W = float(samples.weight)
    if minimum(W) / maximum(W) > threshold
        samples
    else
        W_s = sort(W)
        Q = cumsum(W_s)
        Q ./= maximum(Q)
        @assert last(Q) ≈ 1
        ind = searchsortedlast(Q, fraction)
        if ind !== 0
            thresh = W_s[searchsortedlast(Q, fraction)]
            idxs = findall(x -> x >= thresh, samples.weight)
            samples[idxs]
        else
            samples
        end
    end
end

"""
    repetition_to_weights(v::AbstractVector)

*BAT-internal, not part of stable public API.*

Drop (subsequently) repeated samples by adding weights.
"""
function repetition_to_weights(v::AbstractVector)
    idxs = Vector{Int}()
    counts = Vector{Int}()
    push!(idxs, 1)
    push!(counts, 1)
    for i in 2:length(v)
        if v[i] == v[i-1]
            counts[end] += 1
        else
            push!(idxs, i)
            push!(counts, 1)
        end
    end
    return (idxs, counts)
end


include(string(PROJECT_HOME,"/simulation_code/updatefuns.jl"))
include(string(PROJECT_HOME,"/simulation_code/create_hydraulics_layered.jl"))

function make_layers(_totaldepth)
	linear_layers = collect(0:0.1:0.8);
	toadd = [0,0,1,2,3,4,5,6,7];
	quad_layers = cumsum(toadd)/sum(toadd);

	if _totaldepth < 0.8
		lower_layer_thick = (_totaldepth-0.1)/Int(floor((_totaldepth-0.1) / 0.1));
		layers = vcat(0, 0.1:lower_layer_thick:_totaldepth);
	end

	if _totaldepth >= 0.8
		layers = linear_layers + quad_layers*(_totaldepth-0.8);
	end
	return convert(Array{FT}, -1*layers)
end

function create_spac(
            sm::AbstractStomatalModel = OSMWang{FT}(),
            vcmax::FT = FT(30),
            kmax::FT = FT(0.3),
		z_soil::FT = FT(1000),
     chl::FT = FT(40),
	 N_slice::Int = 1,
	alpha::FT = FT(1.368),
	n::FT = FT(2.6257)
) where {FT<:AbstractFloat}
(

	#_soil_hs = create_soil_VC(VanGenuchten{FT}(), "Silt Loam");

	_soil_hs = VanGenuchten{FT}(stype = "Ozark",
									α = alpha,
									n = n,
								   Θs = 0.45,
								   Θr = 0.067);

	#_soil_hs = BrooksCorey{FT}(stype= "Ozark", Θs = FT(porosity), Θr = FT(0.05), ϕs = psi_sat, b = b_soil);

	_totaldepth = FT(z_soil/1000);
	_rootdepth = FT(z_soil/1000);
	#_soil_bounds = collect(FT,[0,-0.1,-0.25,-0.45,-0.7]);
	#_soil_bounds = collect(FT,[0,-0.1,-0.2,-0.3,-0.4]);
	#_soil_bounds = collect(FT,[0,-0.1,-0.2,-0.3,-0.4,-0.5,-0.6,-0.7]);

	#_soil_bounds = collect(FT,[0,-0.1,-0.15,-0.2,-0.25,-0.3,-0.35,-0.4]);
	#_soil_bounds[3:end] *= _totaldepth/FT(0.4);

	#_soil_bounds = collect(FT,[0,-0.1,-0.25,-0.5,-0.75,-1.0,-1.5,-2]);
	#_soil_bounds *= _totaldepth/FT(2);

	#_soil_bounds = collect(FT,[0,-0.1,-0.2, -0.3, -0.4, -0.5, -0.6, -0.7]);
	#_soil_bounds *= _totaldepth/FT(0.7);

	_soil_bounds = make_layers(_totaldepth);

	_totaldepth = -1*_soil_bounds[end];
	_rootdepth = -1*_soil_bounds[end];

	_air_bounds = convert(Array{FT},[0,5,9,12,16,18.5,20] );

	_tree_hs = create_tree2(FT(-1*_rootdepth), FT(9), FT(18.5), _soil_bounds,
						   _air_bounds, N_slice);
	for _root in _tree_hs.roots
		_root.sh = deepcopy(_soil_hs);
	end;

	_node = SPACMono{FT}(soil_bounds = _soil_bounds,
						  air_bounds = _air_bounds,
							  z_root = FT(-1*_rootdepth),
							z_canopy = 18.5,
							plant_hs = _tree_hs,
								  ga = 413.223,
								  la = 1735.537,
							latitude = 38.74,
						   longitude = -92.2,
						   elevation = 219.4,
					   stomata_model = sm);
	_node.swc = collect(FT,0.4*ones(length(_soil_bounds)-1));

	n_canopy = length(_node.plant_hs.canopy_index_in_air);

	#=
	incl_grid =  collect(FT,[22.5,45+22.5]);
	azi_grid = collect(FT,[90,270]);
	incl_grid_bnd = [collect(FT,[0,45]) collect(FT,[45,90])];
	=#

	incl_grid =  collect(FT,[45]);
	azi_grid = collect(FT,[180]);
	incl_grid_bnd = [collect(FT,[0]) collect(FT,[90])];

	nleaf = length(incl_grid)*length(azi_grid) + 1;

	_node.plant_ps = [CanopyLayer{FT}(n_leaf=nleaf) for
										  i in 1:n_canopy];

	_node.canopy_rt = Canopy4RT{FT}(nLayer=n_canopy, LAI=_node.la/_node.ga, litab  = incl_grid, litab_bnd = incl_grid_bnd, lazitab = azi_grid);

	#"RT dimensions"
	_node.rt_dim = create_rt_dims(_node.canopy_rt, _node.wl_set);
	#"CanopyRads container"
	_node.can_rad = create_canopy_rads(FT, _node.rt_dim);
	#"CanopyOpticals container"
	_node.can_opt = create_canopy_opticals(FT, _node.rt_dim);
	#"Array of LeafBios container"
	_node.leaves_rt = [create_leaf_bios(FT, _node.rt_dim) for
										 i in 1:n_canopy];

	#"RT container"
	_node.rt_con = create_rt_cache(FT, _node.rt_dim);
	#"Container for sunlit leaf area fraction in each layer"
	_node.f_SL = repeat(_node.canopy_rt.lidf, outer=[ _node.canopy_rt.nAzi ]) /
						_node.canopy_rt.nAzi;

	#=
	for _iPS in _node.plant_ps
		_iPS.g_min   = 0.001;
		_iPS.g_min25 = 0.001;
		_iPS.g_max   = 0.08; #0.50;
		_iPS.g_max25 = 0.08; #0.50;
	end;
	=#

	for _iPS in _node.plant_ps
		_iPS.g_min   = 0.001;
		_iPS.g_min25 = 0.001;
		_iPS.g_max   = 0.12; #0.50;
		_iPS.g_max25 = 0.12; #0.50;
	end;

	_node.canopy_rt.Ω       = 0.69;
	_node.canopy_rt.clump_a = 0.69;
	update_LAI!(_node, FT(4.2));
	update_Weibull!(_node, FT(5.703), FT(0.953));

	#update_Weibull!(_node, FT(4.0), FT(3.0));
	#for _leaf in _node.plant_hs.leaves
        #_leaf.vc.b = 1.0;
        #_leaf.vc.c = 3.0;
    #end;

	#for _leaf in _node.plant_hs.leaves
    #    _leaf.vc.b = 2.0;
        #_leaf.vc.c = c;
    #end;

    # update fitted Vcmax, Kmax, and Chlrophyll content
    update_VJRWW!(_node, vcmax);
    update_Kmax!(_node, kmax);
    update_Cab!(_node, chl);
    initialize_spac_canopy!(_node);

    return _node
)
end


# Different ways to simulate molecules

export
    SteepestDescentMinimizer,
    simulate!,
    VelocityVerlet,
    Verlet,
    StormerVerlet,
    Langevin

"""
    SteepestDescentMinimizer(; <keyword arguments>)

Steepest descent energy minimization.

# Arguments
- `step_size::D=0.01u"nm"`: the initial maximum displacement.
- `max_steps::Int=1000`: the maximum number of steps.
- `tol::F=1000.0u"kJ * mol^-1 * nm^-1"`: the maximum force below which to
    finish minimization.
- `run_loggers::Bool=false`: whether to run the loggers during minimization.
- `log_stream::L=devnull`: stream to print minimization progress to.
"""
struct SteepestDescentMinimizer{D, F, L}
    step_size::D
    max_steps::Int
    tol::F
    run_loggers::Bool
    log_stream::L
end

function SteepestDescentMinimizer(;
                                    step_size=0.01u"nm",
                                    max_steps=1_000,
                                    tol=1000.0u"kJ * mol^-1 * nm^-1",
                                    run_loggers=false,
                                    log_stream=devnull)
    return SteepestDescentMinimizer(step_size, max_steps, tol,
                                    run_loggers, log_stream)
end

"""
    simulate!(system, simulator, n_steps; parallel=true)
    simulate!(system, simulator; parallel=true)

Run a simulation on a system according to the rules of the given simulator.
Custom simulators should implement this function.
"""
function simulate!(sys,
                    sim::SteepestDescentMinimizer;
                    parallel::Bool=true)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    sim.run_loggers && run_loggers!(sys, neighbors, 0; parallel=parallel)
    E = potential_energy(sys, neighbors)
    println(sim.log_stream, "Step 0 - potential energy ",
            E, " - max force N/A - N/A")
    hn = sim.step_size

    for step_n in 1:sim.max_steps
        F = forces(sys, neighbors; parallel=parallel)
        max_force = maximum(norm.(F))

        coords_copy = sys.coords
        sys.coords += hn * F ./ max_force
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))

        neighbors_copy = neighbors
        neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                    parallel=parallel)
        E_trial = potential_energy(sys, neighbors)
        if E_trial < E
            hn = 6 * hn / 5
            E = E_trial
            println(sim.log_stream, "Step ", step_n, " - potential energy ",
                    E_trial, " - max force ", max_force, " - accepted")
        else
            sys.coords = coords_copy
            neighbors = neighbors_copy
            hn = hn / 5
            println(sim.log_stream, "Step ", step_n, " - potential energy ",
                    E_trial, " - max force ", max_force, " - rejected")
        end

        sim.run_loggers && run_loggers!(sys, neighbors, step_n;
                                        parallel=parallel)

        if max_force < sim.tol
            break
        end
    end
    return sys
end

# Forces are often expressed per mol but this dimension needs removing for use in the integrator
function remove_molar(x)
    fx = first(x)
    if dimension(fx) == u"𝐋 * 𝐍^-1 * 𝐓^-2"
        T = typeof(ustrip(fx))
        return x / T(Unitful.Na)
    else
        return x
    end
end

"""
    VelocityVerlet(; <keyword arguments>)

The velocity Verlet integrator.

# Arguments
- `dt::T`: the time step of the simulation.
- `coupling::C=NoCoupling()`: the coupling which applies during the simulation.
- `remove_CM_motion::Bool=true`: whether to remove the centre of mass motion
    every time step.
"""
struct VelocityVerlet{T, C}
    dt::T
    coupling::C
    remove_CM_motion::Bool
end

function VelocityVerlet(; dt, coupling=NoCoupling(), remove_CM_motion=true)
    return VelocityVerlet(dt, coupling, remove_CM_motion)
end

function simulate!(sys,
                    sim::VelocityVerlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    run_loggers!(sys, neighbors, 0; parallel=parallel)
    accels_t = accelerations(sys, neighbors; parallel=parallel)
    accels_t_dt = zero(accels_t)
    sim.remove_CM_motion && remove_CM_motion!(sys)

    for step_n in 1:n_steps
        sys.coords += sys.velocities .* sim.dt .+ (remove_molar.(accels_t) .* sim.dt ^ 2) ./ 2
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))

        accels_t_dt = accelerations(sys, neighbors; parallel=parallel)

        sys.velocities += remove_molar.(accels_t .+ accels_t_dt) .* sim.dt / 2

        sim.remove_CM_motion && remove_CM_motion!(sys)
        apply_coupling!(sys, sim, sim.coupling)

        run_loggers!(sys, neighbors, step_n; parallel=parallel)

        if step_n != n_steps
            neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                        parallel=parallel)
            accels_t = accels_t_dt
        end
    end
    return sys
end

"""
    Verlet(; <keyword arguments>)

The leapfrog Verlet integrator.
This is a leapfrog integrator, so the velocities are offset by half a time step
behind the positions.

# Arguments
- `dt::T`: the time step of the simulation.
- `coupling::C=NoCoupling()`: the coupling which applies during the simulation.
- `remove_CM_motion::Bool=true`: whether to remove the centre of mass motion
    every time step.
"""
struct Verlet{T, C}
    dt::T
    coupling::C
    remove_CM_motion::Bool
end

function Verlet(; dt, coupling=NoCoupling(), remove_CM_motion=true)
    return Verlet(dt, coupling, remove_CM_motion)
end

function simulate!(sys,
                    sim::Verlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    run_loggers!(sys, neighbors, 0; parallel=parallel)
    sim.remove_CM_motion && remove_CM_motion!(sys)

    for step_n in 1:n_steps
        accels_t = accelerations(sys, neighbors; parallel=parallel)

        sys.velocities += remove_molar.(accels_t) .* sim.dt

        sys.coords += sys.velocities .* sim.dt
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))

        sim.remove_CM_motion && remove_CM_motion!(sys)
        apply_coupling!(sys, sim, sim.coupling)

        run_loggers!(sys, neighbors, step_n; parallel=parallel)

        if step_n != n_steps
            neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                        parallel=parallel)
        end
    end
    return sys
end

"""
    StormerVerlet(; <keyword arguments>)

The Störmer-Verlet integrator.
Does not currently work with coupling methods that alter the velocity.

# Arguments
- `dt::T`: the time step of the simulation.
- `coupling::C=NoCoupling()`: the coupling which applies during the simulation.
"""
struct StormerVerlet{T, C}
    dt::T
    coupling::C
end

StormerVerlet(; dt, coupling=NoCoupling()) = StormerVerlet(dt, coupling)

function simulate!(sys,
                    sim::StormerVerlet,
                    n_steps::Integer;
                    parallel::Bool=true)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    run_loggers!(sys, neighbors, 0; parallel=parallel)
    coords_last = sys.coords

    for step_n in 1:n_steps
        accels_t = accelerations(sys, neighbors; parallel=parallel)

        coords_copy = sys.coords
        if step_n == 1
            # Use the velocities at the first step since there is only one set of coordinates
            sys.coords += sys.velocities .* sim.dt .+ (remove_molar.(accels_t) .* sim.dt ^ 2) ./ 2
        else
            sys.coords += vector.(coords_last, sys.coords, (sys.box_size,)) .+ remove_molar.(accels_t) .* sim.dt ^ 2
        end
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))

        # This is accurate to O(dt)
        sys.velocities = vector.(coords_copy, sys.coords, (sys.box_size,)) ./ sim.dt

        apply_coupling!(sys, sim, sim.coupling)

        run_loggers!(sys, neighbors, step_n; parallel=parallel)

        if step_n != n_steps
            neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                        parallel=parallel)
            coords_last = coords_copy
        end
    end
    return sys
end

"""
    Langevin(; <keyword arguments>)

The Langevin integrator, based on the Langevin Middle Integrator in OpenMM.
This is a leapfrog integrator, so the velocities are offset by half a time step
behind the positions.

# Arguments
- `dt::T`: the time step of the simulation.
- `temperature::K`: the temperature of the simulation.
- `friction::F`: the friction coefficient of the simulation.
- `remove_CM_motion::Bool=true`: whether to remove the centre of mass motion
    every time step.
"""
struct Langevin{S, K, F, T}
    dt::S
    temperature::K
    friction::F
    remove_CM_motion::Bool
    vel_scale::T
    noise_scale::T
end

function Langevin(; dt, temperature, friction, remove_CM_motion=true)
    vel_scale = exp(-dt * friction)
    noise_scale = sqrt(1 - vel_scale^2)
    return Langevin(dt, temperature, friction, remove_CM_motion, vel_scale, noise_scale)
end

function simulate!(sys,
                    sim::Langevin,
                    n_steps::Integer;
                    parallel::Bool=true,
                    rng=Random.GLOBAL_RNG)
    neighbors = find_neighbors(sys, sys.neighbor_finder; parallel=parallel)
    run_loggers!(sys, neighbors, 0; parallel=parallel)
    sim.remove_CM_motion && remove_CM_motion!(sys)

    for step_n in 1:n_steps
        accels_t = accelerations(sys, neighbors; parallel=parallel)

        sys.velocities += remove_molar.(accels_t) .* sim.dt

        sys.coords += sys.velocities .* sim.dt / 2
        noise = random_velocities(sys, sim.temperature; rng=rng)
        sys.velocities = sys.velocities .* sim.vel_scale .+ noise .* sim.noise_scale

        sys.coords += sys.velocities .* sim.dt / 2
        sys.coords = wrap_coords_vec.(sys.coords, (sys.box_size,))
        sim.remove_CM_motion && remove_CM_motion!(sys)

        run_loggers!(sys, neighbors, step_n; parallel=parallel)

        if step_n != n_steps
            neighbors = find_neighbors(sys, sys.neighbor_finder, neighbors, step_n;
                                        parallel=parallel)
        end
    end
    return sys
end


function converttopartitionedbernoullidbm(mdbm::MultimodalDBM)
   Vector{Union{BernoulliRBM, PartitionedRBM{BernoulliRBM}}}(mdbm)
end

"""
    addlayer!(dbm, x)
Adds a pretrained layer to the BasicDBM `dbm`, given the dataset `x` as input
for the visible layer of the `dbm`.

# Optional keyword arguments:
* `isfirst`, `islast`: indicating that the new RBM should be trained as
   first (assumed if `dbm` is empty) or last layer of the DBM, respectively.
   If those flags are not set, the added layer is trained as intermediate layer.
   This information is needed to determine the factor for the weights during
   pretraining.
* `epochs`, `nhidden`, `learningrate`/`learningrates`, `pcd`, `cdsteps`,
  `monitoring`: used for fitting the weights of the last layer, see `fitrbm(x)`
"""
function addlayer!(dbm::BasicDBM, x::Matrix{Float64};
      isfirst::Bool = isempty(dbm),
      islast::Bool = false,
      nhidden::Int = size(x,2),
      epochs::Int = 10,
      learningrate::Float64 = 0.005,
      learningrates::Vector{Float64} = learningrate * ones(epochs),
      pcd::Bool = true,
      cdsteps::Int = 1,
      monitoring::Function = nomonitoring)

   warn("`addlayer!`` is deprecated. Please use the options of `fitdbm` instead.")
   # propagate input x up to last hidden layer
   hh = x
   for i = 1:length(dbm)
      hh = hiddenpotential(dbm[i], hh, 2.0) # intermediate layer, factor is 2.0
   end

   upfactor = downfactor = 2.0
   if isfirst
      downfactor = 1.0
   end
   if islast
      upfactor = 1.0
   end

   rbm = fitrbm(hh, nhidden = nhidden, epochs = epochs,
         rbmtype = BernoulliRBM,
         learningrate = learningrate,
         learningrates = learningrates,
         pcd = pcd, cdsteps = 1,
         upfactor = upfactor, downfactor = downfactor,
         monitoring = monitoring)

   push!(dbm, rbm)
   dbm
end

function defaultfinetuninglearningrates(learningrate, epochs)
   learningrate * 11.0 ./ (10.0 .+ (1:epochs))
end

"""
    fitdbm(x; ...)
Fits a (multimodal) DBM to the data set `x`.
The procedure consists of two parts:
First a stack of RBMs is pretrained in a greedy layerwise manner
(see `stackrbms(x)`). Then the weights of all layers are jointly
trained using the general Boltzmann Machine learning procedure
(see `traindbm!(dbm,x)`).

# Optional keyword arguments (ordered by importance):
* `nhiddens`: vector that defines the number of nodes in the hidden layers of
   the DBM. The default value specifies two hidden layers with the same size
   as the visible layer.
* `epochs`: number of training epochs for joint training
* `epochspretraining`: number of training epochs for pretraining,
   defaults to `epochs`
* `learningrate`/`learningrates`:
   learning rate(s) for joint training of layers (= fine tuning)
   using the learning algorithm for a general Boltzmann Machine.
   The learning rate for fine tuning is by default decaying with the number of epochs,
   starting with the given value for the `learningrate`.
   (For more details see `traindbm!`).
* `learningratepretraining`: learning rate for pretraining,
   defaults to `learningrate`
* `batchsizepretraining`: batchsize for pretraining, defaults to 1
* `nparticles`: number of particles used for sampling during joint training of
   DBM, default 100
* `pretraining`: The arguments for layerwise pretraining
   can be specified for each layer individually.
   This is done via a vector of `TrainLayer` objects.
   (For a detailed description of the possible parameters,
   see help for `TrainLayer`).
   If the number of training epochs and the learning rate are not specified
   explicitly for a layer, the values of `epochspretraining`,
   `learningratepretraining` and `batchsizepretraining` are used.
* `monitoring`: Monitoring function accepting a `dbm` and the number of epochs
   retuning nothing. Used for the monitoring of fine-tuning.
* `monitoringdatapretraining`: a `DataDict` that contains data used for
   monitoring the pretraining (see argument `monitoringdata` of `stackrbms`.)
* `optimizer`/`optimizers`: an optimizer or a vector of optimizers for each epoch
   (see `AbstractOptimizer`) used for fine-tuning.
* `optimizerpretraining`: an optimizer used for pre-training.
   Defaults to the `optimizer`.
"""
function fitdbm(x::Matrix{Float64};
      nhiddens::Vector{Int} = Vector{Int}(),
      epochs::Int = 10,
      nparticles::Int = 100,
      learningrate::Float64 = 0.005,
      learningrates::Vector{Float64} =
            defaultfinetuninglearningrates(learningrate, epochs),
      sdlearningrate::Float64 = 0.0,
      sdlearningrates::Vector{Float64} =
            defaultfinetuninglearningrates(sdlearningrate, epochs),
      learningratepretraining::Float64 = learningrate,
      epochspretraining::Int = epochs,
      batchsizepretraining::Int = 1,
      pretraining::AbstractTrainLayers = Vector{TrainLayer}(),
      monitoring::Function = nomonitoring,
      monitoringdatapretraining::DataDict = DataDict(),
      optimizer::AbstractOptimizer = NoOptimizer(),
      optimizers::Vector{<:AbstractOptimizer} = Vector{AbstractOptimizer}(),
      optimizerpretraining::AbstractOptimizer = optimizer)

   if isempty(pretraining) && isempty(nhiddens)
      # set default only if there is not any more detailed info
      nvariables = size(x,2)
      nhiddens = [nvariables; nvariables]
   end

   # Layerwise pre-training
   pretraineddbm = stackrbms(x, nhiddens = nhiddens,
         epochs = epochspretraining, predbm = true,
         batchsize = batchsizepretraining,
         learningrate = learningratepretraining,
         optimizer = optimizer,
         trainlayers = pretraining,
         monitoringdata = monitoringdatapretraining)
   # Fine-tuning using mean-field approximation in algorithm for
   # training a general Boltzmann machine
   traindbm!(pretraineddbm, x, epochs = epochs, nparticles = nparticles,
         learningrate = learningrate, learningrates = learningrates,
         sdlearningrate = sdlearningrate, sdlearningrates = sdlearningrates,
         optimizer = optimizer, optimizers = optimizers,
         monitoring = monitoring)
end

"""
    newparticleslike(particles)
Creates new and uninitialized particles of the same dimensions as the given
`particles`.
"""
function newparticleslike(particles::Particles)
   newparticles = Particles(undef, length(particles))
   for i in eachindex(particles)
      newparticles[i] = Matrix{Float64}(undef, size(particles[i]))
   end
   newparticles
end

"""
    meanfield(dbm, x)
    meanfield(dbm, x, eps)
Computes the mean-field approximation for the data set `x` and
returns a matrix of particles for the DBM.
The number of particles is equal to the number of samples in `x`.
`eps` is the convergence criterion for the fix-point iteration, default 0.001.
It is assumed that all nodes in in-between-layers are Bernoulli distributed.
"""
function meanfield(dbm::MultimodalDBM, x::Array{Float64,2}, eps::Float64 = 0.001)

   nlayers = length(dbm) + 1
   mu = Particles(undef, nlayers)

   # Initialization with single bottom-up pass using twice the weights for all
   # but the topmost layer (see [Salakhutdinov+Hinton, 2012], p. 1985)
   mu[1] = x
   for i=2:(nlayers-1)
      mu[i] = hiddenpotential(dbm[i-1], mu[i-1], 2.0)
   end
   mu[nlayers] = hiddenpotential(dbm[nlayers-1], mu[nlayers-1])

   newmu = newparticleslike(mu)
   newmu[1] = x
   input2 = newparticleslike(mu)

   # mean-field updates until convergence criterion is met
   delta = 1.0
   while delta > eps
      delta = 0.0

      for i = 2:(nlayers-1)
         # total input from layer below
         hiddeninput!(newmu[i], dbm[i-1], mu[i-1])
         # input from layer above
         visibleinput!(input2[i], dbm[i], mu[i+1])
         # combine input
         newmu[i] .+= input2[i]
         # By computing the potential,
         # the assumption is used that all nodes in in-between-layers
         # are Bernoulli-distributed:
         sigm!(newmu[i])

         delta = max(delta, maximum(abs.(mu[i] - newmu[i])))

         # swap new and old mu
         tmp = newmu[i]
         newmu[i] = mu[i]
         mu[i] = tmp
      end

      # last layer
      hiddenpotential!(newmu[end], dbm[end], mu[end-1])
      delta = max(delta, maximum(abs.(mu[end] - newmu[end])))

      # swap new and old mu
      tmp = newmu[end]
      newmu[end] = mu[end]
      mu[end] = tmp
   end

   mu
end

"""
    traindbm!(dbm, x; ...)
Trains the `dbm` (a `BasicDBM` or a more general `MultimodalDBM`) using
the learning procedure for a general Boltzmann Machine with the
training data set `x`.
A learning step consists of mean-field inference (positive phase),
stochastic approximation by Gibbs Sampling (negative phase) and the parameter
updates.

# Optional keyword arguments (ordered by importance):
* `epoch`: number of training epochs
* `learningrate`/`learningrates`: a vector of learning rates for each epoch to
   update the weights and biases. The learning rates should decrease with the
   epochs, e. g. like `a / (b + epoch)`. If only one value is given as
   `learningrate`, `a` and `b` are 11.0 and 10.0, respectively.
* `nparticles`: number of particles used for sampling, default 100
* `monitoring`: A function that is executed after each training epoch.
   It has to accept the trained DBM and the current epoch as arguments.
"""
function traindbm!(dbm::MultimodalDBM, x::Array{Float64,2};
      epochs::Int = 10,
      nparticles::Int = 100,
      learningrate::Float64 = 0.005,
      learningrates::Vector{Float64} =
            defaultfinetuninglearningrates(learningrate, epochs),
      sdlearningrate::Float64 = 0.0,
      sdlearningrates::Vector{Float64} =
            defaultfinetuninglearningrates(sdlearningrate, epochs),
      monitoring::Function = nomonitoring,
      optimizer::AbstractOptimizer = NoOptimizer(),
      optimizers::Vector{<:AbstractOptimizer} = Vector{AbstractOptimizer}())

   assert_enoughvaluesforepochs("learningrates", learningrates, epochs)

   optimizer = converttodbmoptimizer(optimizer, dbm)
   map!(opt -> converttodbmoptimizer(opt, dbm), optimizers, optimizers)
   optimizers = assertinitoptimizers(optimizer, optimizers, dbm,
         learningrates, sdlearningrates, epochs)

   particles = initparticles(dbm, nparticles)

   for epoch = 1:epochs
      traindbm!(dbm, x, particles, optimizers[epoch])

      # monitoring the learning process at the end of epoch
      monitoring(dbm, epoch)
   end

   dbm
end

"""
    traindbm!(dbm, x, particles, learningrate)
Trains the given `dbm` for one epoch.
"""
function traindbm!(dbm::MultimodalDBM, x::Array{Float64,2}, particles::Particles,
      optimizer::AbstractOptimizer)

   gibbssample!(particles, dbm)
   mu = meanfield(dbm, x)

   computegradient!(optimizer, mu, particles, dbm)
   updateparameters!(dbm, optimizer)

   dbm
end


module GuillotineCallback

include("./../../constraints/geometric/constants.jl")
using JuMP
using .Constants

export guillotine_callback, get_involved_boxes_flbx, find_all_guillotine_cuts_exceeding_limit

#Finds a guillotine cut that is larger than the allowed amount
function guillotine_callback(cb_data, m)

    max_cut = guillotine_ds_var * callback_value(cb_data, m[:maxHeight])

    fixed_x = []
    fixed_y = []

    #Check if there exists a cut higher than the allowed cuts
    for i in 1:length(boxes) - 1

        if length(fixed_x) > 0 || length(fixed_y) > 0
            print("Fixed X ", fixed_x, "\n")
            print("Fixed Y ", fixed_y, "\n")
        end

        print("Round ", i, "\n")

        #Find out where the cut is and pass information onwards
        if (callback_value(cb_data, m[:cut_flbx_left_i][i]) + 0.001 >= max_cut && check_if_fixed_x(cb_data, m, 1, i, fixed_x))
            print("Nr 1 \n")
            get_involved_boxes(cb_data, m, 1, i)
            x_value = callback_value(cb_data, m[:flbx_front_left_bot_x][i])
            push!(fixed_x, x_value)
        end
        if (callback_value(cb_data, m[:cut_flbx_j_plus_left_i][i]) + 0.001 >= max_cut && check_if_fixed_x(cb_data, m, 2, i, fixed_x))
            print("Nr 2 \n")
            get_involved_boxes(cb_data, m, 2, i)
            x_value = callback_value(cb_data, m[:flbx_front_left_bot_x][i])
            push!(fixed_x, x_value)
        end
        if (callback_value(cb_data, m[:cut_flbx_i_plus_right][i]) + 0.001 >= max_cut && check_if_fixed_x(cb_data, m, 3, i, fixed_x))
            print("Nr 3 \n")
            get_involved_boxes(cb_data, m, 3, i)
            x_value = callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + callback_value(cb_data, m[:box_x_axis_cover][i])
            push!(fixed_x, x_value)
        end
        if (callback_value(cb_data, m[:cut_flbx_ij_plus_right][i]) + 0.001 >= max_cut && check_if_fixed_x(cb_data, m, 4, i, fixed_x))
            print("Nr 4 \n")
            get_involved_boxes(cb_data, m, 4, i)
            x_value = callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + callback_value(cb_data, m[:box_x_axis_cover][i])
            push!(fixed_x, x_value)
        end
        if (callback_value(cb_data, m[:cut_flby_right][i]) + 0.001 >= max_cut && check_if_fixed_y(cb_data, m, 5, i, fixed_y))
            print("Nr 5 \n")
            get_involved_boxes(cb_data, m, 5, i)
            y_value = callback_value(cb_data, m[:flby_front_left_bot_y][i])
            push!(fixed_y, y_value)
        end
        if (callback_value(cb_data, m[:cut_flby_j_plus_right][i]) + 0.001 >= max_cut && check_if_fixed_y(cb_data, m, 6, i, fixed_y))
            print("Nr 6 \n")
            get_involved_boxes(cb_data, m, 6, i)
            y_value = callback_value(cb_data, m[:flby_front_left_bot_y][i])
            push!(fixed_y, y_value)
        end
        if (callback_value(cb_data, m[:cut_flby_i_plus_left][i]) + 0.001 >= max_cut && check_if_fixed_y(cb_data, m, 7, i, fixed_y))
            print("Nr 7 \n")
            get_involved_boxes(cb_data, m, 7, i)
            y_value = callback_value(cb_data, m[:flby_front_left_bot_y][i]) + callback_value(cb_data, m[:box_y_axis_cover][i])
            push!(fixed_y, y_value)
        end
        if (callback_value(cb_data, m[:cut_flby_ij_plus_left][i]) + 0.001 >= max_cut && check_if_fixed_y(cb_data, m, 8, i, fixed_y))
            print("Nr 8 \n")
            get_involved_boxes(cb_data, m, 8, i)
            y_value = callback_value(cb_data, m[:flby_front_left_bot_y][i]) + callback_value(cb_data, m[:box_y_axis_cover][i])
            push!(fixed_y, y_value)
        end
    end

    return
end

function check_if_fixed_x(cb_data, m, pos, i, fixed_x)
    fixed = false
    if pos == 1 || pos == 2
        for fixed_pos in 1:length(fixed_x)
            if (callback_value(cb_data, m[:flbx_front_left_bot_x][i]) - 0.0001 <=
                fixed_x[fixed_pos] <=
                callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + 0.0001)
                fixed = true
            end
        end
    elseif pos == 3 || pos == 4
        for fixed_pos in 1:length(fixed_x)
            if (callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + callback_value(cb_data, m[:box_x_axis_cover][i]) - 0.0001 <=
                fixed_x[fixed_pos] <=
                callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + callback_value(cb_data, m[:box_x_axis_cover][i]) + 0.0001)
                fixed = true
            end
        end
    end
    return ~fixed
end

function check_if_fixed_y(cb_data, m, pos, i, fixed_y)
    fixed = false
    if pos == 5 || pos == 6
        for fixed_pos in 1:length(fixed_y)
            if (callback_value(cb_data, m[:flby_front_left_bot_y][i]) - 0.0001 <=
                fixed_y[fixed_pos] <=
                callback_value(cb_data, m[:flby_front_left_bot_y][i]) + 0.0001)
                fixed = true
            end
        end
    elseif pos == 7 || pos == 8
        for fixed_pos in 1:length(fixed_y)
            if (callback_value(cb_data, m[:flby_front_left_bot_y][i]) + callback_value(cb_data, m[:box_y_axis_cover][i]) - 0.0001 <=
                fixed_y[fixed_pos] <=
                callback_value(cb_data, m[:flby_front_left_bot_y][i]) + callback_value(cb_data, m[:box_y_axis_cover][i]) + 0.0001)
                fixed = true
            end
        end
    end
    return ~fixed
end

#Takes a box number to check what boxes are involved in guillotine
function get_involved_boxes(cb_data, m, pos, i)
    involved = []
    push!(involved, i)
    # Check the position of the cut
    for j in 1:length(boxes) - 1
        if i!=j
            if (pos == 1)
                if (callback_value(cb_data, m[:guillotine_flbx][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 2)
                if (callback_value(cb_data, m[:guillotine_flbx_j_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 3)
                if (callback_value(cb_data, m[:guillotine_flbx_i_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 4)
                if (callback_value(cb_data, m[:guillotine_flbx_ij_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 5)
                if (callback_value(cb_data, m[:guillotine_flby][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 6)
                if (callback_value(cb_data, m[:guillotine_flby_j_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 7)
                if (callback_value(cb_data, m[:guillotine_flby_i_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 8)
                if (callback_value(cb_data, m[:guillotine_flby_ij_plus][i, j]) == 1)
                    push!(involved, j)
                end
            end
            break
        end
    end

    if (length(involved) >= 2)
        create_constraint(cb_data, m, involved, pos)
    end
end

function create_constraint(cb_data, m, involved, pos)
    print(involved)

    max_height = 0
    max_height_box = 0
    snd_max_height = 0
    snd_max_height_box = 0

    for i in 1:length(involved)
        height_of_i = callback_value(cb_data, m[:flbz_front_left_bot_z][involved[i]]) + callback_value(cb_data, m[:hob_height_of_box][involved[i]])
        if height_of_i >= max_height
            #update second highest
            snd_max_height = max_height
            snd_max_height_box = max_height_box

            #update highest
            max_height = height_of_i
            max_height_box = involved[i]
        elseif height_of_i >= snd_max_height
            #update second highest
            snd_max_height = height_of_i
            snd_max_height_box = involved[i]
        end
    end

    for i in 1:3
        print('\n')
        print(i)
        print('\n')
        @show callback_value(cb_data, m[:flbx_front_left_bot_x][i])
        @show callback_value(cb_data, m[:flby_front_left_bot_y][i])
        @show callback_value(cb_data, m[:flbz_front_left_bot_z][i])
    end

    print('\n')
    print("First ", max_height, " ", max_height_box, "\n")
    print("Second ", snd_max_height, " ", snd_max_height_box, "\n")

    #If same size as sentinel, then the new constraints can't apply.
    if ~((callback_value(cb_data, m[:lob_length_of_box][max_height_box]) >=
        (1 - guillotine_fix_var) * callback_value(cb_data, m[:lob_length_of_box][length(boxes)])) &&
        (callback_value(cb_data, m[:wob_width_of_box][max_height_box]) >=
        (1 - guillotine_fix_var) * callback_value(cb_data, m[:wob_width_of_box][length(boxes)])))

        #check if lowest dimension still same as width of sentinel
        if ~((callback_value(cb_data, m[:lob_length_of_box][max_height_box]) >=
            (1 - guillotine_fix_var) * callback_value(cb_data, m[:lob_length_of_box][length(boxes)])) &&
            (callback_value(cb_data, m[:wob_width_of_box][max_height_box]) >=
            (1 - guillotine_fix_var) * callback_value(cb_data, m[:lob_length_of_box][length(boxes)])))
            if pos == 1
                con = @build_constraint(
                    m[:flbx_front_left_bot_x][max_height_box] -
                    m[:flbx_front_left_bot_x][snd_max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 1 \n")
            elseif pos == 2
                con = @build_constraint(
                    (m[:flbx_front_left_bot_x][max_height_box] + m[:box_x_axis_cover][max_height_box]) -
                    m[:flbx_front_left_bot_x][snd_max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 2 \n")
            elseif pos == 3
                con = @build_constraint(
                    (m[:flbx_front_left_bot_x][snd_max_height_box] + m[:box_x_axis_cover][snd_max_height_box]) -
                    m[:flbx_front_left_bot_x][max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 3 \n")
            elseif pos == 4
                con = @build_constraint(
                    (m[:flbx_front_left_bot_x][snd_max_height_box] + m[:box_x_axis_cover][snd_max_height_box]) -
                    (m[:flbx_front_left_bot_x][max_height_box] + m[:box_x_axis_cover][max_height_box]) >= guillotine_fix_var
                    )
                print("added constraint 4 \n")
            end
            if pos == 1 || pos == 2 || pos == 3 || pos == 4
                MOI.submit(m, MOI.LazyConstraint(cb_data), con)
            end
        end
        if ~((callback_value(cb_data, m[:lob_length_of_box][max_height_box]) >=
            (1 - guillotine_fix_var) * callback_value(cb_data, m[:wob_width_of_box][length(boxes)])) &&
            (callback_value(cb_data, m[:wob_width_of_box][max_height_box]) >=
            (1 - guillotine_fix_var) * callback_value(cb_data, m[:wob_width_of_box][length(boxes)])))
            if pos == 5
                con = @build_constraint(
                    m[:flby_front_left_bot_y][max_height_box] -
                    m[:flby_front_left_bot_y][snd_max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 5 \n")
            elseif pos == 6
                con = @build_constraint(
                    (m[:flby_front_left_bot_y][max_height_box] + m[:box_y_axis_cover][max_height_box]) -
                    m[:flby_front_left_bot_y][snd_max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 6 \n")
            elseif pos == 7
                con = @build_constraint(
                    (m[:flby_front_left_bot_y][snd_max_height_box] + m[:box_y_axis_cover][snd_max_height_box]) -
                    m[:flby_front_left_bot_y][max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 7 \n")
            elseif pos == 8
                con = @build_constraint(
                    (m[:flby_front_left_bot_y][snd_max_height_box] + m[:box_y_axis_cover][snd_max_height_box]) -
                    (m[:flby_front_left_bot_y][max_height_box] + m[:box_y_axis_cover][max_height_box]) >= guillotine_fix_var
                    )
                print("added constraint 8 \n")
            end
            if pos == 5 || pos == 6 || pos == 7 || pos == 8
                MOI.submit(m, MOI.LazyConstraint(cb_data), con)
            end
        end
    end

end

end