#=
3D advection using FV
=#

### If the Finch package has already been added, use this line #########
using Finch # Note: to add the package, first do: ]add "https://github.com/paralab/Finch.git"

### If not, use these four lines (working from the examples directory) ###
# if !@isdefined(Finch)
#     include("../Finch.jl");
#     using .Finch
# end
##########################################################################

init_finch("FVadvection3d");

useLog("FVadvection3dlog")

# Configuration setup
domain(3)
solverType(FV)
timeStepper(RK4)

# Mesh
n = 10 # number of elements in each direction
mesh(HEXMESH, elsperdim=n, bids=2)

# Variables and BCs
u = variable("u", location=CELL)
boundary(u, 1, FLUX, "sin(pi*y)*sin(pi*z)*sin(2*pi*t)^2") # x=0
boundary(u, 2, NO_BC) # other

# Time interval and initial condition
T = 0.7;
timeInterval(T)
initial(u, "0")

# The flux and source terms of the conservation equation
# F and S in the following equation:
# Dt(int(u dx)) = int(S dx) - int(F.n ds)
coefficient("a", [1,0,0], type=VECTOR) # advection velocity
# The "upwind" function applies upwinding to the term (a.n)*u with flow velocity a.
# The optional third parameter is for tuning. Default upwind = 0, central = 1. Choose something between these.
flux(u, "upwind(a,u)")
# Note that there is no source() for this problem

#exportCode("fvad2dcode") # uncomment to export generated code to a file
#importCode("fvad2dcode") # uncomment to import code from a file

solve(u)

finalize_finch()


x = range(0, stop=2, length=25)
for α ∈ 0.01:0.05:0.8
    plot!(x, x.^α, lwidth=α/10, col=RGB(0.0,0.0,α), smooth=true)
end


reorder(M::Matrix, V::Vector{Int}) = M[V,V]

function upper_sum(array::Matrix)
    S, (H,W) = 0, size(array)
    @assert W == H
    for i=1:H, j=i+1:W  S += array[i,j] end
    return S
end

function score_mat_cum_zeros(score_mat::Matrix, permutation::Vector,
                             to_shift = Array{Pair{Int,Int},1}())
    N = size(score_mat)[1]
    @assert size(score_mat)[2] == N
    @assert size(permutation)[1] == N
    @assert isperm(permutation) == true

    for u = 1:N
        perm_u = permutation[u]
        cumsum = score_mat[perm_u,perm_u]
        @assert cumsum == 0
        for v = u-1:-1:1
            cumsum += score_mat[perm_u,permutation[v]]
            cumsum == 0 && push!(to_shift, u=>v)
        end
        cumsum = score_mat[perm_u,perm_u]
        for v = u+1:N
            cumsum += score_mat[perm_u,permutation[v]]
            cumsum == 0 && push!(to_shift, u=>v)
        end
    end
    return to_shift
end

function rotate_part!(transformed::Vector, pair::Pair{Int,Int})
    (u, v) = pair; to_move = transformed[u]
    if u > v
        for k = u:-1:v+1 transformed[k] = transformed[k-1] end
    else
        for k = u:v-1 transformed[k] = transformed[k+1] end
    end
    transformed[v] = to_move
end

function slide_on_level(score_mat::Matrix, some_perm::Vector)
    N = size(score_mat)[1]
    @assert size(score_mat)[2] == N
    @assert size(some_perm)[1] == N
    @assert isperm(some_perm)
    visited, perms = 0, Vector{Vector{Int}}()

    @info "Collecting equivalent permutations..."
    push!(perms, some_perm)

    amount = length(perms)
    while visited < amount
        for i = visited+1:amount
            next_perm = perms[i]
            pairs = score_mat_cum_zeros(score_mat, next_perm)
            for pair in pairs
                permutation = copy(next_perm)
                rotate_part!(permutation, pair)
                in(permutation, perms) && continue
                push!(perms, permutation)
            end
        end
        visited = amount
        amount = length(perms)
        @debug "$amount\r"
        if amount > 20000
            @warn "More than 20k permutations!"
            break
        end
    end
    return sort(perms)
end

function is_refinable(array::Matrix)
    (N,M) = size(array)
    @assert N == M

    for i=1:N
        cumsum = 0
        for j=i-1:-1:1
            cumsum += array[i,j]
            if cumsum < 0
                array[i,j] == 0 && continue
                @info "$i=>$j : $cumsum"
            end
        end
        cumsum = 0
        for j=i+1:N
            cumsum += array[i,j]
            if cumsum > 0
                array[i,j] == 0 && continue
                @info "$i=>$j : $cumsum"
            end
        end
    end
    nothing
end

function dive_into(score_mat::Matrix, some_perm::Vector)
    (N,M) = size(score_mat)
    @assert N == M
    @assert isperm(some_perm)
    @assert N == size(some_perm)[1]
    steps = Dict{Pair{Int,Int},Int}()

    for i=1:N
        cumsum = 0
        for j=i-1:-1:1
            elem = reorder(score_mat,some_perm)[i,j]
            cumsum += elem
            if cumsum < 0
                elem == 0 && continue
                steps[i=>j] = cumsum
		@debug "$i=>$j : $cumsum"
            end
        end
        cumsum = 0
        for j=i+1:N
            elem = reorder(score_mat,some_perm)[i,j]
            cumsum += elem
            if cumsum > 0
                elem == 0 && continue
                steps[i=>j] = cumsum
		@debug "$i=>$j : $cumsum"
            end
        end
    end
    return steps
end

function find_key_of_maxvalue(divedict::Dict{Pair{Int,Int},Int})
    maxvalue = 0
    maxvaluekey = nothing
    for ((from,to),val) in divedict
        abs(val) < maxvalue && continue
        maxvalue = abs(val)
        maxvaluekey = from=>to
    end
    return maxvaluekey
end

function col_optimize(score_mat::Matrix)
    permutation = sortperm(col_negs_sums(score_mat))
    @debug "$permutation : $(upper_sum(reorder(score_mat, permutation)))"
    step = 1
    while true
        divedict = dive_into(score_mat, permutation)
        way = find_key_of_maxvalue(divedict)
        way == nothing && break
        rotate_part!(permutation, way)
        @debug "($step) $way -> $permutation : $(upper_sum(reorder(score_mat, permutation)))"
        step += 1
    end
    @info "col_optimize() stopped at $permutation."
    @info "Upper-triangle sum is $(upper_sum(reorder(score_mat, permutation)))."
    return slide_on_level(score_mat, permutation)
end

function row_optimize(score_mat::Matrix)
    permutation = reverse(sortperm(row_negs_sums(score_mat)))
    @debug "$permutation : $(upper_sum(reorder(score_mat, permutation)))"
    step = 1
    while true
        divedict = dive_into(score_mat, permutation)
        way = find_key_of_maxvalue(divedict)
        way == nothing && break
        rotate_part!(permutation, way)
        @debug "($step) $way -> $permutation : $(upper_sum(reorder(score_mat, permutation)))"
        step += 1
    end
    @info "row_optimize() stopped at $permutation."
    @info "Upper-triangle sum is $(upper_sum(reorder(score_mat, permutation)))."
    return slide_on_level(score_mat, permutation)
end

function consensus(score_mat::Matrix)
    @assert score_mat' == -score_mat
    solution = Vector{Vector{Int}}()
    @info "Sum of negative values is $(sum(score_mat[score_mat.<0]))."
    if is_straight(score_mat)
        @info "Unique permutation"
        push!(solution, sortperm(col_negs(score_mat)))
    else
        if is_crossing(score_mat)
            @warn "Unavoidable excess!"
            @show Δ⁺(score_mat)
        else
            @show Δ⁺(score_mat)
            @assert Δ⁺(score_mat) == 0
        end
        col_pack = col_optimize(score_mat)
        col_reached = upper_sum(reorder(score_mat, col_pack[1]))
        row_pack = row_optimize(score_mat)
        row_reached = upper_sum(reorder(score_mat, row_pack[1]))
        if col_reached > row_reached
            @info "Upper-triangle sum reached: $row_reached"
            solution = row_pack
        else
            @info "Upper-triangle sum reached: $col_reached"
            solution = col_pack
        end
    end
    return solution
    nothing
end


using MOCNeutronTransport
# Reactor-Physics-Benchmarks-Handbook/2015/LWR/CROCUS-LWR-RESR-001
# Model
# ----------------------------------------------------------------------------------------------
gmsh.initialize()
uo2_fuel_entities = Int32[]
um_fuel_entities = Int32[]
gap_entities = Int32[]
clad_entities = Int32[]
absorber_entities = Int32[]
# The external radius of the water is 65 cm
# How much water is needed to count as infinite reflector?
# What is the minimum distance from fuel to boundary?
# Don't want to simulate what we don't have to. Or could make the mesh towards the problem
# boundary really large to minimize computations on a larger problem.
# bb_apothem = 65.0
bb_apothem = 11.2*5
bb = (0.0, 2*bb_apothem, 0.0, 2*bb_apothem)

# UO2 pins
# ----------------------------------------------------------------------------------------------
# Fuel diameter = 10.52 mm (pg. 34)
# External cladding diameter = 12.6 mm (pg. 34)
# Cladding thickness = 0.85 mm (pg. 34)
# The document does not mention the gap thickness, so I believe we are left to compute that quantity
# Gap thickness = (External cladding diameter - 2(Clad thickness) - Fuel diameter)/2
# Gap thickness = (12.6 - 2(0.85) - 10.52)/2 = 0.19 mm
# Therefore
#   r_fuel = 10.52/2 = 5.26 mm = 0.526 cm
#   r_gap = 5.26 + 0.19 mm = 0.545 cm
#   r_clad = 12.6/2 = 6.3 mm = 0.63 cm
r_fuel = 0.526
r_gap = 0.545
r_clad = 0.63
pitch = 1.8370 # pg.34
# Sweet from left to right for each column in the uo2 pins.
# Place pins bottom to top for each row
for i = -11:11
    if i == 0
        continue
    end
    j_max = [11, 11, 11, 9, 9, 9, 6, 6, 6, 3, 3]
    for j = (-j_max[abs(i)] + 1):j_max[abs(i)]
        push!(uo2_fuel_entities,
              gmsh.model.occ.add_disk(i*pitch - sign(i)*pitch/2 + bb_apothem,
                                      j*pitch - pitch/2 + bb_apothem,
                                      0, r_fuel, r_fuel))
        push!(gap_entities,
              gmsh.model.occ.add_disk(i*pitch - sign(i)*pitch/2 + bb_apothem,
                                      j*pitch - pitch/2 + bb_apothem,
                                      0, r_gap,  r_gap))
        push!(clad_entities,
              gmsh.model.occ.add_disk(i*pitch - sign(i)*pitch/2 + bb_apothem,
                                      j*pitch - pitch/2 + bb_apothem,
                                      0, r_clad, r_clad))
    end
end
# U metal pins
# ----------------------------------------------------------------------------------------------
# Fuel diameter = 17.0 mm (pg. 34)
# External cladding diameter = 19.3 mm (pg. 34)
# Cladding thickness = 0.975 mm (pg. 34)
# Gap thickness = (External cladding diameter - 2(Clad thickness) - Fuel diameter)/2
# Gap thickness = (19.3 - 2(0.975) - 17.0)/2 = 0.175 mm
# Therefore
#   r_fuel = 17.0/2 = 8.5 mm = 0.85 cm
#   r_gap = 8.5 + 0.175 mm = 0.8675 cm
#   r_clad = 19.3/2 = 9.65 mm = 0.965 cm
# Note that the distribution of pins is not quarter symmetric.
r_fuel_m = 0.85
r_gap_m = 0.8675
r_clad_m = 0.965
pitch_m = 2.917 # pg.34
for i = -10:10
    if i == 0
        continue
    end
    j_max = [10, 10, 10, 9, 9, 8, 8, 7, 5, 3]
    j_min = [ 8,  8,  7, 7, 5, 5, 3, 1, 1, 1]
    for j = j_min[abs(i)]:j_max[abs(i)]
        push!(uo2_fuel_entities,
              gmsh.model.occ.add_disk(i*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                      sign(i)*j*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                      0, r_fuel_m, r_fuel_m))
        push!(gap_entities,
              gmsh.model.occ.add_disk(i*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                      sign(i)*j*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                      0, r_gap_m,  r_gap_m))
        push!(clad_entities,
              gmsh.model.occ.add_disk(i*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                      sign(i)*j*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                      0, r_clad_m, r_clad_m))
    end
end
for i = -10:10
    if i == 0
        continue
    end
    j_max = [10, 10, 10, 9, 9, 9, 8, 7, 6, 4]
    j_min = [ 8,  8,  7, 7, 5, 5, 3, 1, 1, 1]
    for j = (j_min[abs(i)] - 1):(j_max[abs(i)] - 1)
        # Empty tube
        if (i == 6 && j == 5) || (i == -6 && j == 5)
            push!(gap_entities,
                  gmsh.model.occ.add_disk(i*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          -sign(i)*j*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          0, r_gap_m,  r_gap_m))
            push!(clad_entities,
                  gmsh.model.occ.add_disk(i*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          -sign(i)*j*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          0, r_clad_m, r_clad_m))
        else
            push!(uo2_fuel_entities,
                  gmsh.model.occ.add_disk(i*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          -sign(i)*j*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          0, r_fuel_m, r_fuel_m))
            push!(gap_entities,
                  gmsh.model.occ.add_disk(i*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          -sign(i)*j*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          0, r_gap_m,  r_gap_m))
            push!(clad_entities,
                  gmsh.model.occ.add_disk(i*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          -sign(i)*j*pitch_m - sign(i)*pitch_m/2 + bb_apothem,
                                          0, r_clad_m, r_clad_m))
        end
    end
end
# Absorber rod
# ----------------------------------------------------------------------------------------------
# Absorber diameter = 6.0 mm (pg. 32)
# Clad diameter = 8.0 mm (pg. 32)
push!(absorber_entities,
      gmsh.model.occ.add_disk(bb_apothem, bb_apothem, 0, 0.3, 0.3))
push!(clad_entities,
      gmsh.model.occ.add_disk(bb_apothem, bb_apothem, 0, 0.4, 0.4))

# Materials
# ----------------------------------------------------------------------------------------------
gmsh.model.occ.synchronize()
p = gmsh.model.add_physical_group(2, uo2_fuel_entities)
gmsh.model.set_physical_name(2, p, "MATERIAL_UO2")
p = gmsh.model.add_physical_group(2, gap_entities)
gmsh.model.set_physical_name(2, p, "MATERIAL_GAP")
p = gmsh.model.add_physical_group(2, clad_entities)
gmsh.model.set_physical_name(2, p, "MATERIAL_CLAD")
p = gmsh.model.add_physical_group(2, absorber_entities)
gmsh.model.set_physical_name(2, p, "MATERIAL_ABSORBER")
gmsh_group_preserving_fragment(gmsh.model.get_entities(2),
                               gmsh.model.get_entities(2);
                               material_hierarchy = ["MATERIAL_UO2",
                                                     "MATERIAL_ABSORBER",
                                                     "MATERIAL_GAP",
                                                     "MATERIAL_CLAD"])

# Overlay Grid
# ---------------------------------------------------------------------------------------------
# The preferable locations to place a grid division are the horizontal or vertical lines that do
# not intersect any pins. This way, we can potentially capture one pin per coarse cell, and easily
# show pin powers.
#
# If we define the center of the model at (0, 0)
# The valid x or y locations for the UO2 pins in the positive x, y quadrant are:
# [0, pitch/2 - r_clad)
# ( [(2i-1)/2]pitch + r_clad, [(2(i+1)-1)/2]pitch - r_clad)
# (23pitch/2 + r_clad, ∞)
# i = 1, 2, ..., 11
# The valid x or y locations for the U metal pins are
# [0, pitch_m/2 - r_clad_m)
# ( [(2i-1)/2]pitch_m + r_clad_m, [(2(i+1)-1)/2]pitch_m - r_clad_m)
# (21pitch_m/2 + r_clad_m, ∞)
# i = 1, 2, ..., 10
# Not that due to symmetry, -1*interval is also valid.
# We may compute the intersections of these sets.
uo2_sets = [(0.0, pitch/2 - r_clad)]
for i = 1:11
    push!(uo2_sets, ( ((2i-1)/2)pitch + r_clad, ((2(i+1)-1)/2)pitch - r_clad))
end
push!(uo2_sets, (23pitch/2 + r_clad, Inf))

um_sets = [(0.0, pitch_m/2 - r_clad_m)]
for i = 1:10
    push!(um_sets, ( ((2i-1)/2)pitch_m + r_clad_m, ((2(i+1)-1)/2)pitch_m - r_clad_m))
end
push!(um_sets, (21pitch_m/2 + r_clad_m, Inf))

set_intersections = Tuple{Float64, Float64}[]
for (uo2_min, uo2_max) in uo2_sets
    for (um_min, um_max) in um_sets
        # um set intersects on the left
        if um_min ≤ uo2_min ≤ min(um_max, uo2_max)
            push!(set_intersections, (uo2_min, min(um_max, uo2_max)))
        # um set intersects on the right
        elseif um_min ≤ uo2_max ≤ um_max
            push!(set_intersections, (max(um_min, uo2_min), uo2_max))
        end
    end
end
# Visualize the intervals in 1d
#for (ymin, ymax) in set_intersections
#    if ymax == Inf
#        ymax = bb_apothem
#    end
#    gmsh.model.occ.add_rectangle(bb_apothem, ymin + bb_apothem, 0.0, 30.0, ymax - ymin)
#end
#gmsh.model.occ.synchronize()
#
# Note that we may also use intervals of length 2*value, and divide the problem using an odd
# number of divisions
#
# Possible RT modules ± ϵ. Check the intervals for full range of valid values.
# 1 x 1 - (-65, 65)
# 2 x 2 - (-65, 0, 65)
# 3 x 3 - (-60, -20,  20, 60)
# 4 x 4 - (-65, -32.5, 0, 32.5, 65)
# 5 x 5 - (-56, -33.6, -11.2, 11.2, 33.6, 56) & (-73, -43.8, -14.6, 14.6, 43.8, 73)
# 6 x 6 - None
# 7 x 7 - None
# 8 x 8 - None
# ... None
nx = [5]
ny = [5]
grid_tags = gmsh_overlay_rectangular_grid(bb, "MATERIAL_MODERATOR", nx, ny)
gmsh.fltk.run()

gmsh.finalize()


function ChiSquaredDistribution(arg0::jdouble)
    return ChiSquaredDistribution((jdouble,), arg0)
end

function ChiSquaredDistribution(arg0::jdouble, arg1::jdouble)
    return ChiSquaredDistribution((jdouble, jdouble), arg0, arg1)
end

function cumulative_probability(obj::ChiSquaredDistribution, arg0::jdouble)
    return jcall(obj, "cumulativeProbability", jdouble, (jdouble,), arg0)
end

function density(obj::ChiSquaredDistribution, arg0::jdouble)
    return jcall(obj, "density", jdouble, (jdouble,), arg0)
end

function get_degrees_of_freedom(obj::ChiSquaredDistribution)
    return jcall(obj, "getDegreesOfFreedom", jdouble, ())
end

function get_numerical_mean(obj::ChiSquaredDistribution)
    return jcall(obj, "getNumericalMean", jdouble, ())
end

function get_numerical_variance(obj::ChiSquaredDistribution)
    return jcall(obj, "getNumericalVariance", jdouble, ())
end

function get_support_lower_bound(obj::ChiSquaredDistribution)
    return jcall(obj, "getSupportLowerBound", jdouble, ())
end

function get_support_upper_bound(obj::ChiSquaredDistribution)
    return jcall(obj, "getSupportUpperBound", jdouble, ())
end

function is_support_connected(obj::ChiSquaredDistribution)
    return jcall(obj, "isSupportConnected", jboolean, ())
end

function log_density(obj::ChiSquaredDistribution, arg0::jdouble)
    return jcall(obj, "logDensity", jdouble, (jdouble,), arg0)
end


using Test

@test 1 == 1

using LatticeBoltzmann

quadratures = [D2Q4(), D2Q5(), D2Q9(), D2Q13(), D2Q17(), D2Q21(), D2Q37()]
quadratures = LatticeBoltzmann.Quadratures

include("problems/taylor_green_vortex.jl")
include("problems/poiseuille.jl")
include("collision_models.jl")
include("initial_conditions.jl")

@testset "LatticeBoltzmann.jl" begin

    @testset "Multinomial hermite coefficients" begin

        ξ = [1.0 0.0]
        @test hermite(0, ξ) ≈ 1.0
        @test all(hermite(1, ξ) .≈ [1.0 0.0])
        @test all(hermite(2, ξ) .≈ [0.0 0.0; 0.0 -1.0])

        H_3 = hermite(3, ξ)
        @test all(H_3[:, :, 1] .≈ [-2.0 0.0; 0.0 -1.0])
        @test all(H_3[:, :, 2] .≈ [0.0 -1.0; -1.0 0.0])

        H_4 = hermite(4, ξ)

        # Check that our hermite polynomials obey the recurrance rule in 1 dimension
        ξ = 1.0
        for n in 3:4
            H = hermite(n, [ξ])[1]

            @test H ≈ ξ * hermite(n - 1, [ξ])[1] .- (n - 1) * hermite(n - 2, [ξ])[1]
        end

        # Check H4
        # H4 = [
        #     ξ[α_4] .* ξ[α_3] .* ξ[α_2] .* ξ[α_1] .- (
        #         # 6 terms
        #         (ξ[α_1] .* ξ[α_2]) .* δ(α_3, α_4) .+
        #         (ξ[α_1] .* ξ[α_3]) .* δ(α_2, α_4) .+
        #         (ξ[α_1] .* ξ[α_4]) .* δ(α_3, α_2) .+
        #         (ξ[α_2] .* ξ[α_3]) .* δ(α_1, α_4) .+
        #         (ξ[α_2] .* ξ[α_4]) .* δ(α_1, α_3) .+
        #         (ξ[α_3] .* ξ[α_4]) .* δ(α_1, α_2)
        #     ) .+ (δ(α_1, α_2) * δ(α_3, α_4) + δ(α_1, α_3) * δ(α_2, α_4) + δ(α_1, α_4) * δ(α_2, α_3))
        #     for α_1 = 1:D, α_2 = 1:D, α_3 = 1:D, α_4 = 1:D
        # ]
    end

    include("quadrature.jl")

    logspace(start, stop, length) = exp10.(range(start, stop = stop, length = length))
    @testset "moments of the equilibrium distribution of the $q lattice" for q in
                                                                             quadratures
        N = 1

        f = LatticeBoltzmann.equilibrium(q, 1.0, zeros(LatticeBoltzmann.dimension(q)), 1.0)

        @test LatticeBoltzmann.density(q, f) ≈ 1.0

        v = zeros(LatticeBoltzmann.dimension(q))
        LatticeBoltzmann.velocity!(q, f, 1.0, v)
        @test isapprox(v, zeros(LatticeBoltzmann.dimension(q)), atol = 1e-16)

        equilibrium(ρ, u) = begin
            T = 1.0
            # return LatticeBoltzmann.equilibrium(q, ρ, u, T)

            f = zeros(length(q.weights))
            LatticeBoltzmann.hermite_based_equilibrium!(q, ρ, u, T, f)
            f1 = copy(f)
            LatticeBoltzmann.equilibrium!(q, ρ, u, T, f)

            if !isapprox(LatticeBoltzmann.density(q, f), 1.0, rtol = 1e-10, atol = 1e-8)
                # @show f1 f
                # @show f1 - f
                LatticeBoltzmann.hermite_based_equilibrium!(q, ρ, u, T, f)
            end
            if !isapprox(LatticeBoltzmann.density(q, f1), 1.0, rtol = 1e-10, atol = 1e-8)
                # @show f1 f
                # @show f1 - f
                LatticeBoltzmann.hermite_based_equilibrium!(q, ρ, u, T, f)
            end
            # @show isapprox(LatticeBoltzmann.density(q, f), 1.0, rtol = 1e-10, atol = 1e-8) isapprox(LatticeBoltzmann.density(q, f1), 1.0, rtol = 1e-10, atol = 1e-8)
            # @show f - f1
            # return f1
            return f
        end
        density(ρ, u) = LatticeBoltzmann.density(q, equilibrium(ρ, u))
        momentum(ρ, u) = begin
            v = zeros(LatticeBoltzmann.dimension(q))
            LatticeBoltzmann.velocity!(q, equilibrium(ρ, u), 1.0, v)
            return v
        end

        pressure(ρ, u) = LATTICEBOLTZMANN.pressure(equilibrium(ρ, u))

        @test density(1.0, [0.0, 0.0]) ≈ 1.0
        @test isapprox(
            momentum(1.0, [0.0, 0.0]),
            fill(0.0, LatticeBoltzmann.dimension(q)),
            atol = 1e-16,
        )

        # Todo: analytically check the numerical error bounds

        for u in logspace(2, -9, 12)
            for v in [[0, 0], [u, 0.0], [0.0, u], [u, u]]
                @test isapprox(density(1.0, v), 1.0, rtol = 1e-10, atol = 1e-8)
                for d in 1:LatticeBoltzmann.dimension(q)
                    @test isapprox(momentum(1.0, v)[d], v[d], atol = 1e-11, rtol = 1e-6)
                end
            end
        end
    end
end


"""
The geometry consists of a “hook” in the form of a curved strip rigidly clamped
at one end and loaded with a unit in-plane shear along the width at the other
end. It has two circular segments that are connected at the tangent point. The
smaller segment has a mean radius of 0.3556 m (14 inches) and spans 60° from
the clamped end to the tangent point. The larger segment spans 150° from the
tangent point to the free end and has a mean radius of 1.1684 m (46 inches).
The hook is 0.0508 m (2 inches) thick and 0.508 m (20 inches) wide, modeled as
linear elastic with an elastic modulus of 22.77 MPa (3300 psi) and a Poisson's
ratio of 0.35. In most tests the shear force is applied through the use of a
distributing coupling constraint. The coupling constraint provides coupling
between a reference node on which the load is prescribed and the nodes located
on the free end. The distributed nodal loads on the free end are equivalent to
a uniformly distributed load of 8.7563 N/m (0.05 lb/in).

"""
module raasch_examples

using LinearAlgebra, Statistics
using FinEtools
using FinEtoolsDeforLinear
using FinEtoolsFlexStructures.FESetShellT3Module: FESetShellT3
using FinEtoolsFlexStructures.FESetShellQ4Module: FESetShellQ4
using FinEtoolsFlexStructures.FEMMShellT3FFModulew
using FinEtoolsFlexStructures.RotUtilModule: initial_Rfield, update_rotation_field!
using VisualStructures: plot_nodes, plot_midline, render, plot_space_box, plot_midsurface, space_aspectratio, save_to_json
using FinEtools.MeshExportModule.VTKWrite: vtkwrite

function _execute(input = "raasch_s4_1x9.inp", drilling_stiffness_scale = 1.0, visualize = true, nL = 9, nW = 1)
    # The physical quantities are provided in english units, dimensions in inches.10
    E = 3300.0;
    nu = 0.35;
    thickness  =  2.0;
    tolerance = thickness/2
    # Reference solution for the vertical deflection under the load. Obtained with a refined 20-node solid model.
    ref_sol = 5.022012648671993;
    R = 46.0;
    formul = FEMMShellT3FFModule
    # formul = FEMMShellT3DSGMTModule
    @show formul

    if input == ""
        fens, fes = T3block(210.0, 20.0, nL, nW)
        fens.xyz = xyz3(fens)
        for k in 1:count(fens)
            a, z = fens.xyz[k, :]
            if a <= 60.0
                fens.xyz[k, :] .= (14*sin(a*pi/180), 14*(1-cos(a*pi/180)), z)
            else
                a = a - 60.0 + 30.0
                xc = 14*sin(60*pi/180) + 46*cos(30*pi/180)
                yc = 14*(1-cos(60*pi/180)) - 46*sin(30*pi/180)
                fens.xyz[k, :] .= (xc-46*cos(a*pi/180), yc+46*sin(a*pi/180), z)
            end
        end
        input = "raasch_$(nL)x$(nW)"
    else
        output = import_ABAQUS(joinpath(dirname(@__FILE__()), input))
        fens = output["fens"]
        fes = output["fesets"][1]

        connected = findunconnnodes(fens, fes);
        fens, new_numbering = compactnodes(fens, connected);
        fes = renumberconn!(fes, new_numbering);

        fens, fes = Q4toT3(fens, fes)

        # fens, fes = T3refine(fens, fes)# .
        # fens, fes = T3refine(fens, fes)# .
        # fens, fes = T3refine(fens, fes)# .
    end

    @show count(fens), count(fes)

    # plots = cat(plot_space_box([[0 0 -R/2]; [R/2 R/2 R/2]]),
    #     plot_nodes(fens),
    #     plot_midsurface(fens, fes);
    # dims = 1)
    # pl = render(plots)

    mater = MatDeforElastIso(DeforModelRed3D, E, nu)

        # Report

    @info "Mesh: $input"

    sfes = FESetShellT3()
    accepttodelegate(fes, sfes)
    femm = formul.make(IntegDomain(fes, TriRule(1), thickness), mater)
    femm.drilling_stiffness_scale = drilling_stiffness_scale
    stiffness = formul.stiffness
    associategeometry! = formul.associategeometry!

    # Construct the requisite fields, geometry and displacement
    # Initialize configuration variables
    geom0 = NodalField(fens.xyz)
    u0 = NodalField(zeros(size(fens.xyz,1), 3))
    Rfield0 = initial_Rfield(fens)
    dchi = NodalField(zeros(size(fens.xyz,1), 6))

    # Apply EBC's
    # Clamped end
    l1 = selectnode(fens; box = Float64[0 0 -Inf Inf -Inf Inf], inflate = tolerance)
    for i in [1,2,3,4,5,6]
        setebc!(dchi, l1, true, i)
    end

    applyebc!(dchi)
    numberdofs!(dchi);

    # Assemble the system matrix
    associategeometry!(femm, geom0)
    K = stiffness(femm, geom0, u0, Rfield0, dchi);

    # Load
    bfes = meshboundary(fes)
    l1 = selectelem(fens, bfes, box = [97.96152422706632 97.96152422706632 -16 -16 0 20], inflate = 1.0e-6)
    lfemm = FEMMBase(IntegDomain(subset(bfes, l1), GaussRule(1, 2)))
    fi = ForceIntensity(FFlt[0, 0, 0.05, 0, 0, 0]);
    F = distribloads(lfemm, geom0, dchi, fi, 3);
    @assert  isapprox(sum(F), 1.0)

    # @infiltrate
    # Solve
    U = K\F
    scattersysvec!(dchi, U[:])
    nl = selectnode(fens; box = Float64[97.96152422706632 97.96152422706632 -16 -16 0 20], inflate = 1.0e-6)
    targetu =  mean(dchi.values[nl, 3])
    @info "Solution: $(round(targetu, digits=8)),  $(round(targetu/ref_sol, digits = 4)*100)%"

    # formul._resultant_check(femm, geom0, u0, Rfield0, dchi)

    # Generate a graphical display of displacements and rotations
    scalars = []
    for nc in 1:6
        push!(scalars, ("dchi$nc", deepcopy(dchi.values[:, nc])))
    end
    vectors = []
    push!(vectors, ("U", deepcopy(dchi.values[:, 1:3])))
    push!(vectors, ("UR", deepcopy(dchi.values[:, 4:6])))
    vtkwrite("$input-dchi.vtu", fens, fes; scalars = scalars, vectors = vectors)

    # Generate a graphical display of resultants
    function csys!(csmatout::FFltMat, XYZ::FFltMat, tangents::FFltMat, fe_label::FInt)
        cross3!(view(csmatout, :, 3), view(tangents, :, 1), view(tangents, :, 2))
        r = view(csmatout, :, 3)
        csmatout[:, 3] .= vec(r)/norm(vec(r))
        csmatout[:, 2] .= (0.0, 0.0, 1.0)
        cross3!(view(csmatout, :, 1), view(csmatout, :, 2), view(csmatout, :, 3))
        return csmatout
    end
    ocsys = CSys(3, 3, csys!)
    scalars = []
    for nc in 1:3
        fld = fieldfromintegpoints(femm, geom0, dchi, :moment, nc, outputcsys = ocsys)
            # fld = elemfieldfromintegpoints(femm, geom0, dchi, :moment, nc)
        push!(scalars, ("m$nc", fld.values))
    end
    vtkwrite("$(input)-m.vtu", fens, fes; scalars = scalars, vectors = [("u", dchi.values[:, 1:3])])
    scalars = []
    for nc in 1:3
        fld = fieldfromintegpoints(femm, geom0, dchi, :membrane, nc, outputcsys = ocsys)
            # fld = elemfieldfromintegpoints(femm, geom0, dchi, :moment, nc)
        push!(scalars, ("n$nc", fld.values))
    end
    vtkwrite("$(input)-n.vtu", fens, fes; scalars = scalars, vectors = [("u", dchi.values[:, 1:3])])
    scalars = []
    for nc in 1:2
        fld = fieldfromintegpoints(femm, geom0, dchi, :shear, nc, outputcsys = ocsys)
            # fld = elemfieldfromintegpoints(femm, geom0, dchi, :moment, nc)
        push!(scalars, ("q$nc", fld.values))
    end
    vtkwrite("$(input)-q.vtu", fens, fes; scalars = scalars, vectors = [("u", dchi.values[:, 1:3])])

    # Visualization
    if visualize
        scattersysvec!(dchi, (R/2)/maximum(abs.(U)).*U)
        update_rotation_field!(Rfield0, dchi)
        plots = cat(plot_space_box([[0 0 -R]; [R R R]]),
            plot_nodes(fens),
            plot_midsurface(fens, fes; x = geom0.values, u = dchi.values[:, 1:3], R = Rfield0.values);
            dims = 1)
        pl = render(plots)
    end
    return targetu/ref_sol
end

function test_convergence()

    @info "Raasch hook"

    # for m in ["1x9", "3x18", "5x36", "10x72", "20x144"]
        # _execute("raasch_s4_" * m * ".inp", 1.0, false)
    for n in 1:7
        _execute("", 1.0, false, 9*2^(n-1), 1*2^(n-1))
    end
    return true
end

function test_dep_drilling_stiffness_scale()

    @info "Raasch hook"

    all_results = []
    all_drilling_stiffness_scale = [1000.0, 1.0, 0.1, 0.0001, 0.000001]
    for drilling_stiffness_scale in all_drilling_stiffness_scale
        results = Float64[]
        # for m in ["1x9", "3x18", "5x36", "10x72", "20x144"]
            # v = _execute("raasch_s4_" * m * ".inp", drilling_stiffness_scale, false)
        for n in 1:7
            v = _execute("", drilling_stiffness_scale, false, 9*2^(n-1), 1*2^(n-1))
                    push!(results, v)
        end
        push!(all_results, results)
    end
    return all_drilling_stiffness_scale, all_results
end

function allrun()
    println("#####################################################")
    println("# test_convergence ")
    test_convergence()
    println("#####################################################")
    println("# test_dep_drilling_stiffness_scale ")
    test_dep_drilling_stiffness_scale()
    return true
end # function allrun

@info "All examples may be executed with "
println("using .$(@__MODULE__); $(@__MODULE__).allrun()")

end # module
nothing


module Run
using LabMaze
using Plots

function deadends()
    grid = PeriodicRectGrid(4, 4)
    recursive_backtracker!(grid)
    deadends = get_deadends(grid)
    ndeadends = length(deadends)
    println("# of deadends $(ndeadends)")
    display(grid)
    remove_deadends!(grid, 0.2)
    deadends = get_deadends(grid)
    ndeadends = length(deadends)
    println("# of deadends $(ndeadends)")
    display(grid)
    return nothing
end

function triangle(; periodic=false, nrows=4, ncols=4)
    grid = !periodic ? TriaGrid(nrows, ncols) : PeriodicTriaGrid(nrows, ncols)
    recursive_backtracker!(grid)
    remove_deadends!(grid, 0.5)
    p = draw_grid(grid)
    display(p)
    return nothing
end

function solve_triangle_astar(; periodic=false)
    grid = !periodic ? TriaGrid(4, 4) : PeriodicTriaGrid(4, 4)
    recursive_backtracker!(grid)
    remove_deadends!(grid, 0.5)
    p = draw_grid(grid)
    function heuristic(x, goal)
        return (x.col - goal.col)^2 + (x.row - goal.row)^2
    end
    path, dists = astar(grid[1, 1], grid[4, 4], heuristic)
    draw_distance(Tria, dists)
    draw_solution(Tria, path)
    display(p)
    suffix = periodic ? "_periodic" : ""
    savefig(p, "imgs/triangle$(suffix).png")
    return nothing
end

function rectangle(; periodic=false, nrows=4, ncols=4)
    grid = !periodic ? RectGrid(nrows, ncols) : PeriodicRectGrid(nrows, ncols)
    recursive_backtracker!(grid)
    display(grid)
    p = draw_grid(grid)
    display(p)
    return nothing
end

function solve_rectangle(; periodic=false)
    grid = !periodic ? RectGrid(4, 4) : PeriodicRectGrid(4, 4)
    recursive_backtracker!(grid)
    remove_deadends!(grid, 0.5)
    p = draw_grid(grid)
    path, dists = dijkstra(grid[1, 1], grid[4, 4])
    draw_distance(Rect, dists)
    draw_solution(Rect, path)
    display(p)
    suffix = periodic ? "_periodic" : ""
    savefig(p, "imgs/rectangle$(suffix).png")
    return nothing
end

function hexagon(; periodic=false, nrows=3, ncols=4)
    grid = !periodic ? HexaGrid(nrows, ncols) : PeriodicHexaGrid(nrows, ncols)
    recursive_backtracker!(grid)
    p = draw_grid(grid)
    display(p)
    return nothing
end

function solve_hexagon(; periodic=false)
    grid = !periodic ? HexaGrid(4, 4) : PeriodicHexaGrid(4, 4)
    recursive_backtracker!(grid)
    remove_deadends!(grid, 0.5)
    p = draw_grid(grid)
    path, dists = dijkstra(grid[1, 1], grid[4, 4])
    draw_distance(Hexa, dists)
    draw_solution(Hexa, path)
    display(p)
    suffix = periodic ? "_periodic" : ""
    savefig(p, "imgs/hexagon$(suffix).png")
    return nothing
end

function solve_hexagon_astar(; periodic=false)
    grid = !periodic ? HexaGrid(4, 4) : PeriodicHexaGrid(4, 4)
    recursive_backtracker!(grid)
    remove_deadends!(grid, 0.5)
    p = draw_grid(grid)
    function heuristic(x, goal)
        return (x.col - goal.col)^2 + (x.row - goal.row)^2
    end
    path, dists = astar(grid[1, 1], grid[4, 4], heuristic)
    draw_distance(Hexa, dists)
    draw_solution(Hexa, path)
    display(p)
    return nothing
end

end


__precompile__()
module LightML

using Gadfly
using DataFrames
using ForwardDiff
using Distributions
using PyCall
using PyPlot
using DataStructures
using Distances
using Clustering

@pyimport sklearn.datasets as dat

export

    test_LinearRegression,
    test_LogisticRegression,

    test_ClassificationTree,
    test_RegressionTree,

    test_GDA,
    test_HMM,

    test_kneast_regression,
    test_kneast_classification,

    test_label_propagation,

    test_LDA,
    test_LDA_reduction,

    test_naive,

    test_NeuralNetwork,

    test_svm,

    test_GaussianMixture,

    test_kmeans_random,
    test_kmeans_speed,

    test_PCA,

    test_spec_cluster,

    test_Adaboost,
    test_BoostingTree,
    test_randomForest,

    make_cla,
    make_reg,
    make_digits,
    make_blo,
    make_iris,

    train!,
    predict,

    RegressionTree,
    ClassificationTree,
    randomForest,
    Adaboost,
    LinearRegression,
    LogisticRegression,
    GDA,
    BoostingTree,
    KnnRegression,
    KnnClassifier,
    label_propagation,
    show_example,
    LDA,
    plot_in_2d,
    NaiveBayes,
    NeuralNetwork,
    svm,
    GaussianMixture,
    Kmeans,
    PCA,
    transform,
    spec_clustering

Features = Union{String, Real}

#Supervised_learning

include("supervised_learning/baseRegression.jl")
include("supervised_learning/decisionTree.jl")
include("supervised_learning/gaussianDiscriminantAnalysis.jl")
include("supervised_learning/hiddenMarkovModel.jl")
include("supervised_learning/kNearestNeighbors.jl")
include("supervised_learning/labelPropagation.jl")
include("supervised_learning/linearDiscriminantAnalysis.jl")
include("supervised_learning/naivdBayes.jl")
include("supervised_learning/neuralNetwork_bp.jl")
include("supervised_learning/support_vector_machine.jl")
include("supervised_learning/adaboost.jl")
include("supervised_learning/randomForests.jl")
include("supervised_learning/xgboost.jl")
include("supervised_learning/GradientBoostingTree.jl")

#Unsupervised_learning

include("unsupervised_learning/gaussianMixtureModel.jl")
include("unsupervised_learning/kMeans.jl")
include("unsupervised_learning/principalComponentAnalysis.jl")
include("unsupervised_learning/spectralCluster.jl")
include("unsupervised_learning/largeScaleSpectralClustering.jl")

#Utils

include("utils/utils.jl")
include("utils/demo.jl")

end


using Test
using Ripserer
using Suppressor
using StaticArrays

using Ripserer: circumcenter_radius2

include("interfacetest.jl")

@static if Sys.iswindows()
    @test_broken begin
        Alpha(points)
        true
    end
else
    @testset "circumcenter_radius2" begin
        c1, r2 = circumcenter_radius2([SA[1.0, 2.0, 3.0, 4.0]])
        @test c1 == [1.0, 2.0, 3.0, 4.0]
        @test r2 == 0

        c2, r2 = circumcenter_radius2([SA[1, 1, 1], SA[-1, -1, -1]])
        @test c2 == [0, 0, 0]
        @test r2 == 3.0

        c3, r3 = circumcenter_radius2([SA[1, 0], SA[-1, 0], SA[0, √3]])
        @test c3 ≈ [0, 1 / √3]
        @test r3 ≈ (2 / √3)^2

        c4, r4 = circumcenter_radius2([SA[1, 0], SA[0, 0], SA[-1, 0]])
        c4 = [NaN, NaN]
        r4 = Inf
    end

    @testset "Compare to Cechmate" begin
        # This test compares the results to cechmate: https://github.com/scikit-tda/cechmate/
        points = [(t * sinpi(t), t * cospi(t)) for t in range(0, 2; length=20)]

        d0_cechmate_deaths =
            2 .* [
                0.05263158,
                0.05805554,
                0.06761044,
                0.07982644,
                0.09366810,
                0.10851510,
                0.12400688,
                0.13992943,
                0.15615105,
                0.17258742,
                0.18918257,
                0.20589813,
                0.22270697,
                0.23958947,
                0.25653108,
                0.27352082,
                0.29055025,
                0.30761277,
                0.32470317,
                Inf,
            ]
        d1_cechmate_births = 2 .* [0.83075319, 0.86707034, 0.91920916, 0.89414800]
        d1_cechmate_deaths = 2 .* [0.83143060, 0.86803555, 0.92415392, 0.89985412]

        d0, d1, d2 = ripserer(Alpha(points); dim_max=2)
        @test all(iszero, birth.(d0))
        @test death.(d0) ≈ d0_cechmate_deaths atol = 1e-7
        @test birth.(d1) ≈ d1_cechmate_births atol = 1e-7
        @test death.(d1) ≈ d1_cechmate_deaths atol = 1e-7
        @test isempty(d2)
    end

    @testset "Interface" begin
        points = [(t * sinpi(t), t * cospi(t)) for t in range(0, 2; length=20)]
        test_filtration(Alpha, points)
    end
end


# UNICODE examples

# call variables with greek symbols
α = 4
Δt = 0.01

# instead of writing alpha_sq = alpha^2
α² = α^2

# some operators have unicode versions
a = 10÷3
b = √2

# Composition of functions
f(x) = 5x
g(x) = x+1
h = (f ∘ g)
h(2)


module Runoffs
"""
Module for the modelling of waterborne pollutants.
"""

using ArchGDAL
using Dates

include(".\\Utils\\Functions.jl")

const agd = ArchGDAL

function getFeatureByFid( features::Vector{agd.Feature}, fid )
    for f in features
        if agd.getfid(f) == fid
            return f
        end
    end
    return nothing
end

"""
    hash_adjacent( t::Tuple{Int64, Int64} )

Given a tuple with both values in `-1:1` (except `(0, 0)`) return a value in `1:8`, matching the cell displacement with its numeric direction reppresentation.
"""
function hash_adjacent( t::Tuple{Int64, Int64} )
    # 3x3 matrix linearization
     # Its necessary to sum 2 to both values of t as they reppresent index displacements from the center of the matrix
      # the linearization process is based on starting index 0, hence the need to sum 2 instead of 3
      # for the same reason 1 needs to be substracted from the first index, ending up with "t[1] + 1"
    res = 3(t[1] + 1) + (t[2] + 2)
    # The cell of index 5 is the center and it's not needed
    return res >= 5 ? res - 1 : res
end

"""
    hash_adjacent( r::Int64, c::Int64 )

Given a two values in `-1:1` (except `(0, 0)`) return a value in `1:8`, matching the cell displacement with its numeric direction reppresentation.
"""
function hash_adjacent( r::Int64, c::Int64 )
    # 3x3 matrix linearization
     # Its necessary to sum 2 to both values of t as they reppresent index displacements from the center of the matrix
      # the linearization process is based on starting index 0, hence the need to sum 2 instead of 3
      # for the same reason 1 needs to be substracted from the first index, ending up with "t[1] + 1"
    res = 3(r + 1) + (c + 2)
    # The cell of index 5 is the center and it's not needed
    return res >= 5 ? res - 1 : res
end

"""
    hash_adjacent( i::Int64 )

Given a value in `1:8` return the corresponding tuple with both values in `-1:1`, except `(0, 0)`, matching the direction reppresentaion to the cell displacement.
"""
function hash_adjacent( i::Int64 )
    # To account for the fact that "i" = 5 for the tuple "(0, 0)"
    if i > 4
        i += 1
    end
    r = ceil(Int64, i / 3)
    c = ((i - 1) % 3) + 1
    # Substract 2 to obtain the index displacement from the center
    return r - 2, c - 2
end

"""
    connectivity_batch!( mat, dem_band::AbstractArray{T}, noDataValue::Real ) where {T <: Number}

Given NxMx8 (N and M must be the same between `mat` and `dem_band`) matrix `mat`, for each group of 8 cells with indexes r and c,
store in them the differences in values between cell (r, c) of dem_band and its 8 adjacent cells, unless the value is equal to `noDataValue`.
"""
function connectivity_batch!( mat, dem_band::AbstractArray{T}, noDataValue::Real ) where {T <: Number}
    rows, cols = size(dem_band)
    # For each cell of the dem's band
    @inbounds for r in 1:rows, c in 1:cols
        if dem_band[r, c] != noDataValue
            # Indexes of adjacent cells
            for (i, j) in keys(indexes)
                if ( r+i >= 1 && r+i <= rows ) && ( c+j >= 1 && c+j <= cols ) && dem_band[r+i, c+j] != noDataValue
                    if mat[ r, c, hash_adjacent(i, j) ] == noDataValue
                        mat[ r, c, hash_adjacent(i, j) ] = dem_band[r, c] - dem_band[r+i, c+j]
                    end
                    if mat[ r+i, c+j, hash_adjacent(-i, -j) ] == noDataValue
                        mat[ r+i, c+j, hash_adjacent(-i, -j) ] = dem_band[r+i, c+j] - dem_band[r, c]
                    end
                end
            end
        end
    end
end

"""
    connectivity( dem_band::Matrix{T}, batch_size::Integer, noDataValue::Real ) where {T <: Number}

Generate a matrix with the same size as `dem_band` and 8 layers, containing for each cell (r, c) of dem_band the difference between the value of that cell and
its 8 adjacent cells, unless the have value equal to `noDataValue`.
The operation will be executed one `batch_size`x`batch_size` section of the matrix at a time.
"""
function connectivity( dem_band::Matrix{T}, batch_size::Integer, noDataValue::Real ) where {T <: Number}
    rows, cols = size(dem_band)
    mat = fill( convert(Float32, noDataValue) , rows, cols, 8 )
    if batch_size == 1
        connectivity_batch!(mat, dem_band, noDataValue)
        return mat
    end
    n, m = ceil.( Int64, [rows, cols] ./ (batch_size - 1) )
    @inbounds for i in 1:n, j in 1:m
        # Find the starting and ending indexes for the current slice of the matrix
         # if the batch is one of the ending ones its ending index will be the size of the matrix for that dimension
        rows_range::UnitRange{Int64} = ( (batch_size - 1) * (i - 1) + 1 ) : ( i != n ? (batch_size - 1) * i + 1 : rows )
        cols_range::UnitRange{Int64} = ( (batch_size - 1) * (j - 1) + 1 ) : ( j != m ? (batch_size - 1) * j + 1 : cols )
        # Use the indexes to run the function on a view of the matrix (passing also the corresponding view of the dem)
        connectivity_batch!( view(mat, rows_range, cols_range, :), view(dem_band, rows_range, cols_range), noDataValue )
    end
    return mat
end

"""
    createRasterizedConnectivity( file::AbstractString, dtm_path::AbstractString )

Create and save as `file` a raster with the same size as the one pointed at by `dtm_path` and 8 layers, containing for each cell (r, c) of  the latter the difference
between the value of that cell and its 8 adjacent cells, unless the have value equal to `noDataValue`.
The operation will be executed one `batch_size`x`batch_size` section of the matrix at a time.
"""
function createRasterizedConnectivity( file::AbstractString, dtm_path::AbstractString )
    mat = connectivity(band_mat, 2048, ndv)
    dtm = agd.read(dtm_path)
    rows, cols = size(mat)
    res = agd.create( file, driver=agd.getdriver("GTiff"), width=rows, height=cols, nbands=8, dtype=Float32 )
    agd.setgeotransform!( res, agd.getgeotransform(dtm) )
    agd.setproj!( res, agd.getproj(dtm) )
    valNoData = agd.getnodatavalue(dtm)
    bands = [ agd.getband( res, i ) for i in 1:8 ]
    agd.setnodatavalue!.(bands, valNoData)
    agd.fillraster!.(bands, valNoData)
    band_mats = agd.read.(bands)

    for r in 1:rows, c in 1:cols
        if !ismissing(mat[r, c]) && !isempty(mat[r, c])
            for (i, j, val) in mat[r, c]
                band_mats[hash_adjacent(i, j)][r, c] = val
            end
        end
    end

    for i in 1:8
        agd.write!( target_ds, band_mats[i], i )
    end

    agd.destroy(target_ds)
end

using Plots

import ArchGDAL as agd

include("../Library/Functions.jl")

# Find the direction with the greatest difference in height from (r, c)
function mindir( mat, r::Int64, c::Int64 )
    dir = 0
    min = Inf
    @inbounds for i in 1:8
        if mat[r, c, i] != -9999.0 && mat[r, c, i] < min
            dir = i
            min = mat[i][r, c]
        end
    end
    return (dir, min)
end
 #= MINDIR TEST VALUES
    arr = [
        [ 1  2  -9999.0; 1  2  3 ],
        [ 3  4  -9999.0; 4  5  6 ],
        [ 5  6  -9999.0; 7  8  9 ],
        [ 7  8  -9999.0; 10 11 12 ],
        [ 9  10 -9999.0; 13 14 15 ],
        [ 11 12 -9999.0; 16 17 18 ],
        [ 13 14 -9999.0; 19 20 21 ],
        [ 17 16 -9999.0; 22 23 24 ]
    ]
 =#

# Return the amount of water flowing along the path formed by the points in "flowpoints" at "instant" and update the value corresponding to the volume of water at each point
function moving_water!( flowpoints::Vector, instant::Int64, rain::Matrix, permeability::Matrix )
    v_moving = 0 # Water flowing from already visited cells following the path
    for (rₚ, cₚ, Δhminₚ, vₚ) in flowpoints
        vₚ = (vₚ + rain[instant][rₚ, cₚ] + v_moving) * (1 - permeability[rₚ, cₚ])
        if Δhmin <= 0 # If the following cell in the path is below the current one
            # All the water on the cell flows to the next one
            v_moving = vₚ
            vₚ = 0
        else # If the cell is below all the surrounding cells
            # The water accumulates on the cell, if the volume of the water added to the height of the cell exceeds the height of the lowest adjacent cell
             # the water overflows
            if vₚ > Δhminₚ
                v_moving = vₚ - Δhminₚ
                vₚ -= v_moving
            else
                v_moving = 0
            end
        end
    end
    return v_moving
end

function flow( flowpoints::Vector, r::Int64, c::Int64, rows::Int64, cols::Int64, instants::Int64, band::Matrix{Float32}, rev_rain_band::Matrix, permeability_band::Matrix )
    while 1 < r < rows && 1 < c < cols
        # While its still raining
        v_moving = 0 # Volume of water flowing FROM the current cell to the next in the current instant
         # "moving_water!" computes the water flowing TO the current cell and so is an instant behind accounting for "v_moving"
        while instants > 0
            # Ad ogni istante ricalcoliamo il volume d'acqua che sta scorrendo lungo il percorso del flusso, in questo modo il volume d'acqua
             # di ogni nuova cella tiene conto del fatto che la pioggia influisce su tutte le celle appertenenti al percorso
            # Volume of water on the current cell
            v = ( sum(rain_band[instants:end][r, c]) + v_moving + moving_water!(flowpoints, instant, rev_rain_band, permeability_band) ) * (1 - permeability_band[r, c])
            dir, Δhmin = mindir(band, r, c)
            if Δhmin <= 0 # If there is a lower adjacent cell all the water flows there
                v_moving = v
                v = 0
                instants -= 1
            else # If the cell is a local minimum the water keeps accumulating untill it overflows
                while instants > 0 && Δhmin > v
                    v = ( v + rain_band[instants][r, c] + moving_water!(flowpoints, instant, rev_rain_band, permeability_band) ) * (1 - permeability[r, c])
                    instants -= 1
                end
                # If the rain stops before the water overflows the stream alts on the cell
                v_moving = instants == 0 ? 0 : v - Δhmin
                v -= v_moving
            end

            push!(flowpoints, (r, c, Δhmin, v))

            # If there is no water flowing the flow stops
            if v_moving == 0 && instants == 0
                break
            end

            # Flow to the next cell
            Δr, Δc = hash_adjacent(dir)
            r += Δr
            c += Δc
        end
        # After the rain stops but there is still water flowing
        while v_moving > 0
        end
    end
end

function mindir( mat, r::Int64, c::Int64, noDataValue::Real )
    dir = [0]
    min = Inf
    @inbounds for i in 1:8
        #   println( "$i) $(mat[i][r, c])" )
        if mat[r, c, i] != noDataValue
            if mat[r, c, i] < min
                dir = [i]
                min = mat[r, c, i]
                continue
            end
            if mat[r, c, i] == min
                push!(dir, i)
            end
        end
    end
    return (dir, min)
end

function flow( band::Matrix{Float32}, rain, permeability, noDataValue::Real )
    rows, cols = size(band)
    water = zeros(Float64, rows, cols, 2) # water[:,:,1]: water volume, water[:,:,2]: volume of incoming water
    flow = Array{ Union{ Vector{Float64}, Float64 } }(noDataValue, rows, cols)

    for rain_epoch in rain
        # Compute the amount of water for every cell
        water[:,:,1] = (water[:,:,1] + rain_epoch + water[:,:,2]) .* (1 .- permeability)

        # For every cell compute the water flowing from that cell to another and update the volume of water in the cell
        @inbounds for r in 1:rows, c in 1:cols
            if band[r,c] == noDataValue
                water[r, c, 1] = water[r, c, 2] = noDataValue
                flow[r, c] = noDataValue
                continue
            end
            # Get the minimum difference in height with the neighbouring cells and the directions to the cells at that height
            dir, Δhmin = mindir(band, r, c)
            # Get the displacements to reach said cells from the current one
            flow[r, c] = hash_adjacent.(dir)

            # If the volume of water is greater than the difference in height between the current cell and the lowest adjacent, the water flows
            if water[r, c, 1] > Δhmin
                ld = length(flow[r,c])
                # The volume of flowing water depends on the difference of height between the cell and the adjacent one
                v = Δhmin <= 0 ? water[r, c, 1] : water[r, c, 1] - Δhmin
                # To the incoming water of cell "(r+Δr, c+Δc)" is added a portion of the water of cell "(r, c)" depending on the number of paths
                for delta in flow[r,c]
                    water[r+delta[1], c+delta[2], 2] += v / ld
                end
                # The remaining water is equal to the original volume less the amount that passed to the adjacent cell
                water[r, c, 1] -= v
            end
        end
    end

    return flow, water
end

#=
Problema:
Trovare il percorso seguito dall'acqua tenendo conto che se il percorso trova un'avvallamento e si ferma li, l'acqua si accumula e con il
    passare del tempo potrebbe riuscire a fuoriuscire e proseguire la corsa.

Idea:
Trovare il percorso fino al primo stop (primo avvallamento) calcolando il cammino minimo discendente (il cammino in cui da ongi punto al successivo si scende),
    trovato il primo stop, verificare se l'acqua ad un certo punto (temporale) può straripare (se l'avvallamento è profondo come il fosso delle marianne non succederà, o,
    qunatomeno, non in tempi abbastanza brevi da avere senso per lo studio), se ciò può avvenire (perchè la profondità dell'avvallamento lo consente), riapplicare il calcolo
    del cammino minimo da questo punto fino al prossimo, ripetere finchè non si trova uno stop forte (= buco molto profondo).
Calcolato questo percorso ideale utlizzando i dati della piovosità ecc calcolare l'effettivo flusso d'acqua (il flusso seguirà per forza questo percorso essendo il
    minimo, quindi la problematica principale sarà individuare il punto di stop del flusso).
=#

function flowpath!( mat::Matrix{Float32}, band::Matrix{Float32}, noDataValue::Real, row::Int64, col::Int64, )
    if band[row, col] != noDataValue
        r, c = row, col
        mat[r, c] = 1.f0
        directions, Δhmin = mindir(band, row, col)
        while Δhmin < 0
            if lenght(directions) > 1
                for dir in directions[2:end]
                    Δr, Δc = hash_adjacent(dir)
                    flowpath!( mat, band, noDataValue, r + Δr, c + Δc )
                end
            end
            r, c .+= hash_adjacent(directions[1])
            directions, Δhmin = mindir(band, r, c)
        end
    end
end

GC.gc()
# Per porre a 1 tutte le celle di un raster che stanno dentro and un poligono delimitato da uno shapefile
using Rasters
using Shapefile

dtm_file = split( @__DIR__ , "\\Porting\\")[1] * "\\Mappe\\DTM_wgs84.tiff"
# sat_file = split( @__DIR__ , "\\Porting\\")[1] * "\\Mappe\\sat WGS84\\sette_sorelle.shp"
csoil_file = "D:\\Z_Tirocinio_Dati\\Classi suolo WGS84\\Classi suolo.shp"
perm_file = "D:\\Z_Tirocinio_Dati\\Permeabilità suolo WGS84\\Permeabilità suolo.shp"
ccs_file = "D:\\Z_Tirocinio_Dati\\ccs WGS84\\ccs.shp"
ccs_file = "C:\\Users\\DAVIDE-FAVARO\\Desktop\\Dati\\ccs WGS84\\ccs.shp"

dtm = Raster(dtm_file)
# sat_shp = Shapefile.Handle(sat_file).shapes[1]
csoil_shp = Shapefile.Table(csoil_file)
perm_shp = Shapefile.Table(perm_file)
ccs_shp = Shapefile.Table(ccs_file)

# Creation of the final raster
originX = dtm.dims[1][1]
originY = dtm.dims[2][1]
stepX = dtm.dims[1][2] - originX
stepY = dtm.dims[2][2] - originY
dims =  X( Projected( originX:stepX:dtm.dims[1][end]+stepX; order=Rasters.ForwardOrdered(), span=Rasters.Regular(stepX), sampling=Rasters.Intervals(Rasters.Start()), crs=EPSG(4326) ) ),
        Y( Projected( originY:stepY:dtm.dims[2][end]+stepY; order=Rasters.ReverseOrdered(), span=Rasters.Regular(stepY), sampling=Rasters.Intervals(Rasters.Start()), crs=EPSG(4326) ) )

#   csoil = Raster( fill(dtm.missingval, dims); missingval=dtm.missingval )
#   perm = Raster( fill(dtm.missingval, dims); missingval=dtm.missingval )
ccs = Raster( fill(dtm.missingval, dims); missingval=dtm.missingval )

# CAMBIARE IL VALORE ASSOCIATO A "X" IN missingval O SIMILI, METTERE VALORI SENSATI E SISTEMARE I VALORI DELLE COPPIE
#   cdict = Dict( val => Float32(i) for (i, val) in enumerate(unique(csoil_shp.gr_idrolog)) )
#   for (shape, value) in zip(csoil_shp.geometry, csoil_shp.gr_idrolog)
#       rasterize!( csoil, shape; fill=cdict[value], order=(X, Y) )
#   end
#
#   Rasters.write( "D:\\Z_Tirocinio_Dati\\Classi suolo WGS84\\Classi suolo.tiff", csoil )

#   pdict = Dict( val => isnothing(tryparse(Float32, val)) ? 0.f0 : tryparse(Float32, val) for (i, val) in enumerate(unique(perm_shp.perm_clas)) )
#   for (shape, value) in zip(perm_shp.geometry, perm_shp.perm_clas)
#       rasterize!( perm, shape; fill=pdict[value], order=(X, Y) )
#   end
#
#   Rasters.write( "D:\\Z_Tirocinio_Dati\\Permeabilità suolo WGS84\\Permeabilità suolo.tiff", perm )

ccdict = Dict( desc => Float32(i) for (i, desc) in enumerate(unique(ccs_shp.legenda)) )
for (shape, value) in zip(ccs_shp.geometry[1:2], ccs_shp.legenda[1:2])
    rasterize!( ccs, shape; fill=ccdict[value], order=(X, Y) )
end

rasterize!( ccs, ccs_shp.geometry[1]; fill=1.f0, order=(X, Y) )
rasterize!( ccs, ccs_shp.geometry[394066]; fill=3.f0, order=(X, Y) )
rasterize!( ccs, ccs_shp.geometry[2]; fill=2.f0, order=(X, Y) )

Rasters.write( "D:\\Z_Tirocinio_Dati\\test.tiff", ccs )

# I poligoni 1 e 2 sono molto vicini ma non sono esattamente adiacenti (i loro mbr si intersecano) in mezzo a loro due c'è il poligono 394066.
#   Dopo la rasterizzazione i due poligoni si toccano coprendo il poligono che dovrebbe stare tra i due.
#   Rasterizzado tutti e tre non sembrano esserci sovrapposizioni nelle celle, ma i poligoni sono di piccole dimensioni e non sono rasterizzati con precisione.

using ArchGDAL
const agd = ArchGDAL

# DA "unique(ccs)" VENGONO 58 VALORI DIVERSI, AL POSTO DI 86 (un valore per ogni poligono più uno per il "-9999.0") QUINDI DEI POLIGONI VENGONO COPERTI DA ALTRI
res_ids = tryparse.( Int64, split( read("C:\\Users\\DAVIDE-FAVARO\\Desktop\\Dati\\res_ids.txt", String), "\n", keepempty=false ) )
#   ccs_shp = agd.read("D:\\Z_Tirocinio_Dati\\ccs WGS84\\ccs.shp")
#= IL SORTING VIENE EFFETUATO PRIMA DI SALVARE GLI ID
features =  collect( agd.getlayer( agd.read("C:\\Users\\DAVIDE-FAVARO\\Desktop\\Dati\\ccs WGS84\\ccs.shp"), 0 ) )
sort!(res_ids, lt=(x, y) -> isless( agd.geomarea.( agd.getgeom.(features[[x, y]]) )... ) )
=#
for (i, id) in enumerate(resids)
    rasterize!( ccs, ccs_shp.geometry[id], fill=Float32(i), order=(X,Y) )
end
Rasters.write( "C:\\Users\\DAVIDE-FAVARO\\Desktop\\Dati\\sat_polys.tiff", ccs )
Rasters.write( "D:\\Z_Tirocinio_Dati\\sat_polys.tiff", ccs )

using Rasters
using SpatialGraphs

dtm = Raster(split( @__DIR__ , "\\Porting\\")[1] * "\\Mappe\\DTM_wgs84.tiff")
csoil = Raster("C:\\Users\\DAVIDE-FAVARO\\Desktop\\Dati\\Classi suolo WGS84\\Classi suolo.tiff")
perm = Raster("C:\\Users\\DAVIDE-FAVARO\\Desktop\\Dati\\Permeabilità suolo WGS84\\Permeabilità suolo.tiff")
perm = Raster("D:\\Z_Tirocinio_Dati\\Permeabilità suolo WGS84\\Permeabilità suolo.tiff")

perm2 = Raster(
    replace(
        perm.data,
        0.f0 => -9999.f0,
        2.f0 => 0.036f0,
        3.f0 => 0.36f0,
        4.f0 => 3.6f0,
        5.f0 => 36.f0,
        6.f0 => 360.f0
    ),
    perm.dims,
    missingval = perm.missingval
)

# "weights" sarà il raster delle resistenze di ongi cella potrebbe essere semplicemente il raster della permeabilità, oppure una combinazione di permeabilità e altezza
wrg = weightedrastergraph(
    perm2,
    directed = true,
    condition_raster = dtm,
    condition = ( hs, hd ) -> hs != dtm.missingval && hd != dtm.missingval && hs >= hd
)

function run_runoff( dem_file::AbstractString, ccs_file::AbstractString, source_file::AbstractString, target_file::AbstractString, resolution::Int64, folder::AbstractString=".\\" )

    source = agd.read(source_file)

    if agd.geomdim(source) != 0
        throw(DomainError(source_file, "The source shapefile must contain a single point."))
    end

    target = agd.read(target_file)
    ccs = agd.read(ccs_file)
    target_layer, landcover_layer = agd.getlayer([target, ccs], 0)

    if agd.geomdim(target_layer) != 2
        throw(DomainError(target_file, " The target shapefile must contain a polygon."))
    end

    if agd.geomdim(landcover_layer) != 2
        throw(DomainError(ccs_file, "Not a valid landcover geometry."))
    end

    refsys = agd.getspatialref(source)

    dem = agd.read(dem_file)

    if agd.getspatialref(target_layer) != agd.getspatialref(source) || agd.getspatialref(landcover) != agd.getspatialref(source) || agd.getspatialref(dem) != agd.getspatialref(source)
        throw(DomainError("The reference systems are not uniform. Aborting analysis."))
    end

    path_temp_landcover = folder * "\\temp_lc.tiff"
    path_temp_soil = folder * "\\temp_soil.tiff"

    clc_df = Functions.cn_list_extract()

    soil_control = 0

 """ PRINT DI COSE
    messaggio+='ALGORITMO UTILIZZATO: calcolo della separazione delle componenti infiltrazione e ruscellamento tramite metodo SCS-CN; US Department of Agriculture Soil Conservation Service, 1972. National Engineering Handbook, Section 4, Hydrology. US Government Printing Office, Washington, DC, 544pp.\n\n'
 """

    output_path = folder * "\\runoff.tiff"
    intervallo = max(getCellDims(dem))

    # NON SO SE FUNZIONA
    bbox_src = agd.boundingbox(source)
    bbox_trgt = agd.boundingbox(target)
    # Geometry containing both the source and the target
    # area = agd.union( bbox_src, bbox_trgt )
    # Se union non ritorna una geometria unica ma un'unica geometria composta di due elementi disgiunti
    area = agd.boundingbox(agd.union( bbox_src, bbox_trgt ))

 """ PRENDE LA PORZIONE DI `landcover` RAPPRESENTATA DA `area`
    lc_clip_proc = processing.run('qgis:clip', {'INPUT':self.lc, 'OVERLAY':self.areastudio, 'OUTPUT':self.path_working+'/clip.gpkg'})
    lc_clip=QgsVectorLayer(lc_clip_proc['OUTPUT'], 'lc_clip', 'ogr')
    lc_clip.setCrs(self.source.crs())

    #path__layer_lc=lc_clip['OUTPUT'].dataProvider().dataSourceUri()
    path__layer_lc=lc_clip.dataProvider().dataSourceUri()
    path_lc=path__layer_lc.split("|")
    source_ds_lc = ogr.Open(path_lc[0])
    lc_layer = source_ds_lc.GetLayer()
 """
    # NON SONO CERTO PRESERVI LE INFORMAZIONI DI landcover
    landcover_clip = agd.intersection(landcover, area)
    landcover_layer = agd.getlayer(landcover_clip, 0)

    rows = agd.height(landcover_layer)
    cols = agd.width(landcover_layer)
    valNoData = -9999.0
    gtiff_driver = agd.getdriver("GTiff")

    landcover_ds = agd.create( path_temp_landcover, driver=gtiff_driver, width=rows, height=cols, nbands=1, dtype=Float32 )
    agd.setgeotransform!( landcover_ds, agd.getgeotransform(dem) )
    agd.setproj!(landcover_ds, refsys)
    band_lc = agd.getband(landcover_ds, 1)
    agd.setnodatavalue!(band_lc, valNoData)
    agd.fillraster!(band_lc, valNoData)
    bandlc = agd.read(band_lc)

 """ TRASFORMA IN RASTER LA PORZIONE DI `landcover` PRESA PRIMA """
    gdal.RasterizeLayer(lc_ds, [1], lc_layer,options=["ATTRIBUTE="+self.text_lcfield])
    lc_ds=None

    lc_layer=QgsRasterLayer(self.path_temp_lc,"lc_layer")
 """"""
    landcover_layer = agd.gdalrasterize( x -> x, landcover_ds )

    if soil_text == "Valore campo"
        soil_control = 1

        agd.getlayer(path_lc, 0)

        soil_ds = agd.create( path_temp_soil, driver=gtiff_driver, width=round(Int64, x_res), height=round(Int64, y_res), nbands=1, dtype=Float32 )
        agd.setgeotransform!( soil_ds, agd.getgeotransform(dem) )
        agd.setproj!(soil_ds, refsys)
        band_sl = agd.getband(soil_ds, 1)
        agd.setnodatavalue!( band_sl, Float64(valNoData) )
        bandsl = agd.read(band_sl)
        agd.fillraster!(bandsl, valNoData)

     """ RASTERIZZA IL  VETTORIALE DEL TIPO DI SUOLO """
        gdal.RasterizeLayer(lc_ds, [1], soil_layer,options=["ATTRIBUTE="+self.text_soilfield])
        soil_ds=None

        soil_layer=QgsRasterLayer(self.path_temp_soil,"soil_layer")
     """"""
        soil_layer = agd.gdalrasterize( x -> x, landcover_ds )
    end

 # VANNO PASSATI A flow I RANGE DELL'AREA IN CUI PIOVE

 # ASSUMENDO CHE target VENGA PASSATO COME FILE VETTORIALE
    target_layer = collect(agd.getlayer(target, 0))
    target_geom = agd.getgeom(agd.getgeom(target_layer[1], 0), 0)
 # SE target VIENE PASSATO COME GEOMETRIA ELIMINARE LE DUE RIGHE SOPRA
    length = agd.ngeom(target_geom)
    target_points = [ agd.getpoint(target_geom, i)[1:2] for i in 1:length ]

    x_source = agd.getx(source, 0)
    y_source = agd.gety(source, 0)
    mat = agd.read(".\\connectivity.tiff")
    res = flow(mat, x_source, y_source)

    volumes = []
    for point in res
        lc = landcover_layer[point...]
        if soil_control == 1
            soil = soil_layer[point...]
        else
            soil = text_soil
        end
        # IL BLOCCO TRY CATCH NON LO VEDO PARTICOLARMENTE NECESSARIO
        try
         # DA MODIFICARE TENENDO CONTO DEI VETTORIALI DEL SUOLO
         # PER LA CLASSE DEL SUOLO SI PUO' FAR RIFERIMENTO AL DIZIONARIO DEL FILE
         # CREDO STIA PRENDENDO LA RESISTENZA DEL SUOLO NEL PUNTO CORRENTE
            # Se passiamo come parameto un raster delle resisteze questa riga va sostituita
            cn = clc_df[lc, soil] # Classisoi ottenuto da "substance.db"
            S = 254.0((100 / cn) - 1)
        catch
            S = 0
        end
        pcheck = (p0 - 0.2S)^2 / (p0 - 0.2S + S)
        push!(volumes, pcheck > 0.2S ? pcheck : 0 )
    end

end

end # module

#=
function run_runoff( dem, source, target, landcover, soil_text::AbstractString, resolution::Integer, folder::AbstractString=".\\" )

    """ NON SO QUALE SIA L'EQUIVALENTE
       if not self.dem.isValid():
           QMessageBox.warning(self,"Warning", "The dem file is not valid" )
           return
    """

       if agd.geomdim(source) != 0
           throw(DomainError(source, "`source` must be a point"))
       end

       if agd.geomdim(targer) != 2
           throw(DomainError(source, "`target` must be a polygon"))
       end

       if agd.geomdim(landcover) != 2
           throw(DomainError(source, "Not a valid `landcover` geometry"))
       end

       if agd.getspatialref(target) != agd.getspatialref(source) || agd.getspatialref(landcover) != agd.getspatialref(source) || agd.getspatialref(dem) != agd.getspatialref(source)
           throw(DomainError("The reference systems are not uniform. Aborting analysis."))
       end

       refsys = agd.importEPSG(agd.fromWKT(agd.getspatialref(source)))

       path_temp_landcover = folder * "\\temp_lc.tiff"
       path_temp_soil = folder * "\\temp_soil.tiff"

    """ NON SO COSA SIANO QUESTE VARIABILI
       self.text_p = str(self.combo_fieldp.currentText())
    """

       clc_list = Functions.cn_list_extract()

       soil_control = 0

    """ PRINT DI COSE
       messaggio+='ALGORITMO UTILIZZATO: calcolo della separazione delle componenti infiltrazione e ruscellamento tramite metodo SCS-CN; US Department of Agriculture Soil Conservation Service, 1972. National Engineering Handbook, Section 4, Hydrology. US Government Printing Office, Washington, DC, 544pp.\n\n'
    """

       output_path = folder * "\\runoff.tiff"

       valNoData = -9999

       gtiff_driver = agd.getdriver("GTiff")
       target_ds = agd.create( output_path, gtiff_driver, round(Int64, ), round(Int64, ), 1, agd.GDAL.GDT_Float32 )
       agd.setgeotransform!(target_ds, [ , resolution, 0.0, , 0.0, -resolution ])
       agd.setproj!(target_ds, refsys)
    """ NON SO QUALE SIA IL COMANDO PER SETTARE I METADATI CON `ArchGDAL`
       target_ds.SetMetadata(
           Dict(
               "credits" => "Envifate - Francesco Geri, Oscar Cainelli, Paolo Zatelli, Gianluca Salogni, Marco Ciolli - DICAM Università degli Studi di Trento - Regione Veneto",
               "modulo" => "Analisi ruscellamento",
               "descrizione" => "Analisi di ruscellamento di un inquinante attraverso il metodo della separazione delle componenti",
               "srs" => refsys,
               "data" => today()
           )
       )
    """
       band1 = agd.getband(target_ds, 1)
       agd.setnodatavalue!( band1, Float64(valNoData) )
       band = agd.read(band1)
       agd.fillraster!(band, valNoData)
       xsize = agd.width(band)
       ysize = agd.height(band)

       intervallo = getCellDims(dem)

    """ PRENDE LA PORZIONE DI `landcover` RAPPRESENTATA DA `area` """
       lc_clip_proc = processing.run('qgis:clip', {'INPUT':self.lc, 'OVERLAY':self.areastudio, 'OUTPUT':self.path_working+'/clip.gpkg'})
       lc_clip=QgsVectorLayer(lc_clip_proc['OUTPUT'], 'lc_clip', 'ogr')
       lc_clip.setCrs(self.source.crs())

       #path__layer_lc=lc_clip['OUTPUT'].dataProvider().dataSourceUri()
       path__layer_lc=lc_clip.dataProvider().dataSourceUri()
       path_lc=path__layer_lc.split("|")
       source_ds_lc = ogr.Open(path_lc[0])
       lc_layer = source_ds_lc.GetLayer()
    """"""

       landcover_ds = agd.create( path_temp_landcover, gtiff_driver, round(Int64, x_res), round(Int64, y_res), 1, agd.GDAL.GDT_Float32 )
       agd.setgeotransform!(landcover_ds, [ x_min, 25.0, 0.0, y_max, 0.0, -25.0 ])
       agd.setproj!(landcover_ds, refsys)
       band_lc = agd.getband(landcover_ds, 1)
       agd.setnodatavalue!( band_lc, Float64(valNoData) )
       bandlc = agd.read(band_lc)
       agd.fillraster!(bandlc, valNoData)
       xsize = agd.width(band)
       ysize = agd.height(band)

    """ TRASFORMA IN RASTER LA PORZIONE DI `landcover` PRESA PRIMA """
       gdal.RasterizeLayer(lc_ds, [1], lc_layer,options=["ATTRIBUTE="+self.text_lcfield])
       lc_ds=None

       lc_layer=QgsRasterLayer(self.path_temp_lc,"lc_layer")
    """"""

       if soil_text == "Valore campo"
           soil_control = 1

        # NON SO SE SIA EQUIVALENTE
           #   source_ds_soil = ogr.Open(path_lc[0])
           #   soil_layer = source_ds_soil.GetLayer()
           agd.getlayer(path_lc, 0)

           soil_ds = agd.create( path_temp_soil, gtiff_driver, round(Int64, x_res), round(Int64, y_res), 1, agd.GDAL.GDT_Float32 )
           agd.setgeotransform!(soil_ds, [ x_min, 25.0, 0.0, y_max, 0.0, -25.0 ])
           agd.setproj!(soil_ds, refsys)
           band_sl = agd.getband(soil_ds, 1)
           agd.setnodatavalue!( band_sl, Float64(valNoData) )
           bandsl = agd.read(band_sl)
           agd.fillraster!(bandsl, valNoData)

        """ RASTERIZZA IL  VETTORIALE DEL TIPO DI SUOLO """
           gdal.RasterizeLayer(lc_ds, [1], soil_layer,options=["ATTRIBUTE="+self.text_soilfield])
           soil_ds=None

           soil_layer=QgsRasterLayer(self.path_temp_soil,"soil_layer")
        """"""
       end

    """ CALCOLA UN VETTORILE TRAMITE `r.drain` """
       grass_area=str(x_min)+','+str(x_max)+','+str(y_min)+','+str(y_max)+' ['+str(self.areastudio.crs().authid())+']'
       # grass_coord=str(x_source)+','+str(y_source)+' ['+str(self.source.crs().authid())+']'

       namewatershed=self.path_working+'/watershed'
       namedrain=self.path_working+'/wshed.shp'

       params = { 'GRASS_RASTER_FORMAT_OPT' : '','GRASS_REGION_CELLSIZE_PARAMETER' : 0, 'GRASS_REGION_PARAMETER' :grass_area,
                  'GRASS_VECTOR_EXPORT_NOCAT' : False, '-a' : False, 'start_coordinates' : None,  '-n' : False,
                  'input' : self.dem.dataProvider().dataSourceUri(),'-c' : True, 'drain' : namedrain, 'GRASS_MIN_AREA_PARAMETER' : 0.0001,
                  'start_points' : self.source.dataProvider().dataSourceUri(), 'output' : namewatershed }

       waterwshed_proc = processing.run('grass7:r.drain', params)

       #aggiungo per controllo la viewshed alla toc
       #iface.addVectorLayer(namedrain,'watershed','ogr')
       #watershed=QgsProject.instance().mapLayersByName('watershed.shp')

       vdrain = QgsVectorLayer(namedrain, 'vdrain', 'ogr')

       idxlevel = self.source.fields().indexFromName(self.text_p)
       idxcat = vdrain.fields().indexFromName('cat')

       idxtargetname = self.target.fields().indexFromName(self.text_targetfield)
    """"""

       features = agd.getgeom.(collect(agd.features(vdrain)))
       nfeat = 0
       polygons_t = collect(agd.getfeature(target))

       #   start_time = time.time()

       for f in features
           length = agd.geomlength(f)
           currentdistance = intervallo
           nfeat += 1
           featlines = []

           #   firstpoint=geom.interpolate(0)
           firstpoint = agd.getpoint(geom, 0)
           old_x = agd.getx(firstpoint, 0)
           old_y = agd.gety(firstpoint, 0)

           fileoutput = folder * "\\drain$nfeat.shp"

        """ NON SO COSA FACCIA STA ROBA """
           vline = QgsVectorLayer("LineString?crs=EPSG:"+self.refsys, "drain"+str(nfeat), "memory")

           prline = vline.dataProvider()
           prlfield=prline.addAttributes( [ QgsField("concentrazione", QVariant.Double) ] )

           idf=f.attributes()[idxcat]
           feat_drain = next(self.source.getFeatures(QgsFeatureRequest().setFilterFid(idf-1)))
           p00=feat_drain.attributes()[idxlevel]
        """"""

           index_progress = 0
           while currentdistance < length
               if index_progress == 0
                   p0 = p00
               else
                   p0 = pe
               end

               #   point = geom.interpolate(currentdistance)
               point = agd.getpoint(geom, currentdistance)
               x = agd.getx(point, 0)
               y = agd.gety(point, 1)
               clc = lc_layer[x, y]
               if soil_control == 1
                   r, c = toIndexes(soil_layer, x, y)
                   soil = soil_layer[r, c]
               else
                   soil = text_soil
               end
               try
                   cn = listaclc[clc][classisoil[soil]]
                   S = 254.0((100 / cn) - 1)
               catch
                   S = 0
               end

               pcheck = (p0 - 0.2S)^2 / (p0 - 0.2S + S)
               if pcheck > 0.2S
                   pe = pcheck

                """ NON SO COSA FACCIA STA ROBA """
                  fetline = QgsFeature()
                  fetline.setGeometry( QgsGeometry.fromPolyline( [QgsPoint(old_x,old_y),QgsPoint(x,y)] ))
                  fetline.initAttributes(1)
                  fetline.setAttribute(0,pe)
                  vline.updateFeature(fetline)

                  featlines.append(fetline)
                """"""

                   index_progress += 1

                   for polygon in polygons_t
                       p_geom = agd.getgeom(polygon)
                       if agd.within(point, p_geom)
                           nometarget = pol_t.attributes()[idxtargetname]
                        """ MESSAGIO
                           messaggio = "\nIl vettore drain$nfeat ha raggiunto l'area bersaglio denominata $nometarget con un volume pari a: $(round(pe,3))mm\n"
                           self.console.appendPlainText(messaggio)
                        """
                           currentdistance = length + 1
                       end
                   end
               else
                   pe = 0
                   currentdistance = length + 1
               end
                   old_x = x
                   old_y = y
                   currentdistance += intervallo
               end

            """ NON SO COSA FACCIA STA ROBA """
               prline.addFeatures(featlines)
               vline.updateFields()
               QgsProject.instance().addMapLayer(vline)
            """"""

       end
   end
=#


#=

This code is based on routines found in the scipy python library.

The license for scipy is included below:

Copyright (c) 2001, 2002 Enthought, Inc.
All rights reserved.

Copyright (c) 2003-2016 SciPy Developers.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  a. Redistributions of source code must retain the above copyright notice,
     this list of conditions and the following disclaimer.
  b. Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.
  c. Neither the name of Enthought nor the names of the SciPy Developers
     may be used to endorse or promote products derived from this software
     without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.

=#
mutable struct ConvergenceError <: Exception
    msg::AbstractString
end

"""
Given a function `f` and an initial guessed range `x1` to `x2`, the routine
expands the range geometrically until a root is bracketed by the returned values
`x1` and `x2` (in which case zbrac returns true) or until the range becomes
unacceptably large (in which case a `ConvergenceError` is thrown).

##### Arguments

- `f::Function`: The function you want to bracket
- `x1::T`: Initial guess for lower border of bracket
- `x2::T`: Initial guess ofr upper border of bracket
- `;ntry::Int(50)`: The maximum number of expansion iterations
- `;fac::Float64(1.6)`: Expansion factor (higher ⟶ larger interval size jumps)

##### Returns

- `x1::T`: The lower end of an actual bracketing interval
- `x2::T`: The upper end of an actual bracketing interval

##### References

This method is `zbrac` from numerical recipies in C++

##### Exceptions

- Throws a `ConvergenceError` if the maximum number of iterations is exceeded
"""
function expand_bracket(f::Function, x1::T, x2::T;
                        ntry::Int=50, fac::Float64=1.6) where T<:Number

    # x1 <= x2 || throw(ArgumentError("x1 must be less than x2"))

    f1 = f(x1)
    f2 = f(x2)

    for j=1:ntry
        if f1*f2 < 0.0
            return x1, x2
        end

        if abs(f1) < abs(f2)
            x1 += fac*(x1 - x2)
            f1 = f(x1)
        else
            x2 += fac*(x2 - x1)
            f2 = f(x2)
        end
    end

    throw(ConvergenceError("failed to find bracket in $ntry iterations"))
end

expand_bracket(f::Function, x1::T; ntry::Int=50, fac::Float64=1.6) where {T<:Number} =
    expand_bracket(f, 0.9x1, 1.1x1; ntry=ntry, fac=fac)

"""
Given a function `f` defined on the interval `[x1, x2]`, subdivide the
interval into `n` equally spaced segments, and search for zero crossings of the
function. `nroot` will be set to the number of bracketing pairs found. If it is
positive, the arrays `xb1[1..nroot]` and `xb2[1..nroot]` will be filled
sequentially with any bracketing pairs that are found.

##### Arguments

- `f::Function`: The function you want to bracket
- `x1::T`: Lower border for search interval
- `x2::T`: Upper border for search interval
- `n::Int(50)`: The number of sub-intervals to divide `[x1, x2]` into

##### Returns

- `x1b::Vector{T}`: `Vector` of lower borders of bracketing intervals
- `x2b::Vector{T}`: `Vector` of upper borders of bracketing intervals

##### References

This is `zbrack` from Numerical Recepies Recepies in C++
"""
function divide_bracket(f::Function, x1::T, x2::T, n::Int=50) where T<:Number
    x1 <= x2 || throw(ArgumentError("x1 must be less than x2"))

    xs = linspace(x1, x2, n)
    dx = xs[2] - xs[1]

    x1b = T[]
    x2b = T[]

    f1 = f(x1)

    for x in xs[2:end]
        f2 = f(x)

        if f1*f2 <= 0.0
            push!(x1b, x-dx)
            push!(x2b, x)
        end
        f1 = f2
    end

    return x1b, x2b
end

__zero_docstr_arg_ret = """
##### Arguments

- `f::Function`: The function you want to bracket
- `x1::T`: Lower border for search interval
- `x2::T`: Upper border for search interval
- `;maxiter::Int(500)`: Maximum number of bisection iterations
- `;xtol::Float64(1e-12)`: The routine converges when a root is known to lie
  within `xtol` of the value return. Should be >= 0. The routine modifies this to
  take into account the relative precision of doubles.
- `;rtol::Float64(2*eps())`:The routine converges when a root is known to lie
  within `rtol` times the value returned of the value returned. Should be ≥ 0

##### Returns

- `x::T`: The found root

##### Exceptions

- Throws an `ArgumentError` if `[x1, x2]` does not form a bracketing interval
- Throws a `ConvergenceError` if the maximum number of iterations is exceeded

"""

## Bisection

"""
Find the root of the `f` on the bracketing inverval `[x1, x2]` via bisection.

$__zero_docstr_arg_ret

##### References

Matches `bisect` function from scipy/scipy/optimize/Zeros/bisect.c
"""
function bisect(f::Function, x1::T, x2::T; maxiter::Int=500,
                xtol::Float64=1e-12, rtol::Float64=2*eps()) where T<:AbstractFloat

    tol = xtol + rtol*(abs(x1) + abs(x2))

    f1, f2 = f(x1), f(x2)

    if f1 * f2 > 0
        throw(ArgumentError("Root must be bracketed by [x1, x2]"))
    end

    # maybe we got lucky and either x1 or x2 is a root
    if f1 == 0.0
        return x1
    end

    if f2 == 0.0
        return x2
    end

    dm = x2 - x1

    for i=1:maxiter
        dm *= 0.5
        xm = x1 + dm
        fm = f(xm)

        # move bracketing interval up if sign(f(xm)) == sign(f(x1))
        if fm*f1 >= 0.0
            x1 = xm
        end

        if fm == 0.0 || abs(dm) < tol
            return xm
        end
    end

    throw(ConvergenceError("Failed to converge in $maxiter iterations"))
end

## Brent's algorithm
abstract type BrentExtrapolation end

struct BrentQuadratic <: BrentExtrapolation
end

struct BrentHyperbolic <: BrentExtrapolation
end

@inline evaluate(::BrentQuadratic, fcur, fblk, fpre, dpre, dblk) =
    -fcur*(fblk*dblk - fpre*dpre) / (dblk*dpre*(fblk - fpre))

@inline evaluate(::BrentHyperbolic, fcur, fblk, fpre, dpre, dblk) =
    -fcur*(fblk - fpre)/(fblk*dpre - fpre*dblk)

function _brent_body(BE::BrentExtrapolation, f::Function,
                     xa::T, xb::T, maxiter::Int=500,
                     xtol::Float64=1e-12,
                     rtol::Float64=2*eps()) where T<:AbstractFloat
    xpre, xcur = xa, xb
    xblk = fblk = spre = scur = 0.0

    fpre = f(xpre)
    fcur = f(xcur)

    if fpre*fcur > 0
        throw(ArgumentError("Root must be bracketed by [xa, xb]"))
    end

    # maybe we got lucky and x1 or x2 is a root of f
    if fpre == 0.0
        return xpre
    end

    if fcur == 0.0
        return xcur
    end

    for i=1:maxiter
        if fpre*fcur < 0
            xblk = xpre
            fblk = fpre
            spre = scur = xcur - xpre
        end

        if abs(fblk) < abs(fcur)
            xpre = xcur
            xcur = xblk
            xblk = xpre

            fpre = fcur
            fcur = fblk
            fblk = fpre
        end

        tol = xtol + rtol*abs(xcur)
        sbis = (xblk - xcur)/2;

        if fcur == 0.0 || abs(sbis) < tol
            return xcur
        end

        if abs(spre) > tol && abs(fcur) < abs(fpre)
            if xpre == xblk
                # interpolate
                stry = -fcur*(xcur - xpre)/(fcur - fpre)
            else
                # extrapolate
                dpre = (fpre-fcur) / (xpre-xcur)
                dblk = (fblk-fcur) / (xblk-xcur)
                stry = evaluate(BE, fcur, fblk, fpre, dpre, dblk)
            end

            if 2*abs(stry) < min(abs(spre), 3*abs(sbis) - tol)
                # good short step
                spre = scur
                scur = stry
            else
                # bisect
                spre = sbis
                scur = sbis
            end
        else
            # bisect
            spre = sbis
            scur = sbis
        end

        xpre = xcur
        fpre = fcur

        if abs(scur) > tol
            xcur += scur
        else
            xcur += copysign(tol, sbis)
        end

        fcur = f(xcur)
    end

    throw(ConvergenceError("Failed to converge in $maxiter iterations"))
end

"""
Find the root of the `f` on the bracketing inverval `[x1, x2]` via brent's algo.

$__zero_docstr_arg_ret

##### References

Matches `brentq` function from scipy/scipy/optimize/Zeros/bisectq.c
"""
function brent(f::Function, xa::T, xb::T; maxiter::Int=500,
               xtol::Float64=1e-12, rtol::Float64=2*eps()) where T<:AbstractFloat
    _brent_body(BrentQuadratic(), f, xa, xb, maxiter, xtol, rtol)
end

"""
Find a root of the `f` on the bracketing inverval `[x1, x2]` via modified brent

This routine uses a hyperbolic extrapolation formula instead of the standard
inverse quadratic formula. Otherwise it is the original Brent's algorithm, as
implemented in the `brent` function.

$__zero_docstr_arg_ret

##### References

Matches `brenth` function from scipy/scipy/optimize/Zeros/bisecth.c
"""
function brenth(f::Function, xa::T, xb::T; maxiter::Int=500,
                xtol::Float64=1e-12, rtol::Float64=2*eps()) where T<:AbstractFloat
    _brent_body(BrentHyperbolic(), f, xa, xb, maxiter, xtol, rtol)
end

## Ridder's method

"""
Find a root of the `f` on the bracketing inverval `[x1, x2]` via ridder algo

$__zero_docstr_arg_ret

##### References

Matches `ridder` function from scipy/scipy/optimize/Zeros/ridder.c
"""
function ridder(f::Function, xa::T, xb::T; maxiter::Int=500,
                xtol::Float64=1e-12, rtol::Float64=2*eps()) where T<:AbstractFloat
    tol = xtol + rtol*(abs(xa) + abs(xb))

    fa, fb = f(xa), f(xb)

    if fa * fb > 0
        throw(ArgumentError("Root must be bracketed by [xa, xb]"))
    end

    # maybe we got lucky and either xa or xb is a root
    if fa == 0.0
        return xa
    end

    if fb == 0.0
        return xb
    end

    for i=1:maxiter
        dm = 0.5*(xb - xa)
        xm = xa + dm
        fm = f(xm)
        dn = sign(fb - fa)*dm*fm/sqrt(fm*fm - fa*fb)
        xn = xm - sign(dn) * min(abs(dn), abs(dm) - .5*tol)
        fn = f(xn)

        if fn*fm < 0.0
            xa, fa, xb, fb = xn, fn, xm, fm
        elseif fn*fa < 0.0
            xb, fb = xn, fn
        else
            xa, fa = xn, fn
        end

        if fn == 0.0 || abs(xb - xa) < tol
            return xn
        end
    end

    throw(ConvergenceError("Failed to converge in $maxiter iterations"))
end


"""

```
norm_sqr(v::AbstractVector{T}) where T
```

Internal function or structure - interface may change.

# Extended help

`norm_sqr` from LinearAlgebra is not documented and is slower than this for
standard arrays.

"""
@inline function norm_sqr(v::AbstractVector{T}) where T
    n2 = zero(T)^2 # the square here is required for Units, for example
    @simd for x in v
        n2 += x^2
    end
    return n2
end

"""

```
norm(v::AbstractVector{T}) where T
```

Internal function or structure - interface may change.

# Extended help

`norm_sqr` from LinearAlgebra is not documented and is slower than this for
standard arrays. Thus we define our own `norm(x) = norm_sqr(x)`.

"""
@inline norm(v) = sqrt(norm_sqr(v))

"""

```
dot(x::AbstractVector{T1},y::AbstractVector{T2}) where {T1,T2}
```

Internal function or structure - interface may change.

# Extended help

`LinearAlgebra.dot` is slower than this for standard arrays (likely more accurate, but
that is not relevant here).

"""
@inline function dot(x::AbstractVector{T1},y::AbstractVector{T2}) where {T1,T2}
    @assert length(x) == length(y)
    d = zero(T1)*zero(T2)
    @inbounds @simd for i in eachindex(x)
        d += x[i]*y[i]
    end
    return d
end


using BayesLaplace
using BayesLaplace.Curvature
using BayesLaplace.Data
using Flux

@testset "Construction" begin

    # One layer:
    nn = Chain(Dense(2,1))
    la = Laplace(nn)
    @test la.n_params == 3

    # Multi-layer:
    nn = Chain(Dense(2,2,σ),Dense(2,1))
    la = Laplace(nn)
    @test la.n_params == 3
    la = Laplace(nn; subset_of_weights=:all)
    @test la.n_params == 9

end

@testset "Parameters" begin

    nn = Chain(Dense(2,2,σ),Dense(2,1))
    la = Laplace(nn)
    @test BayesLaplace.get_params(la) == collect(Flux.params(nn))[(end-1):end]

    la = Laplace(nn; subset_of_weights=:all)
    @test BayesLaplace.get_params(la) == collect(Flux.params(nn))

end

# We know the analytical expression for the gradient of logit binary cross entropy loss for a single-layer neural net with sigmoid activation just corresponds to the gradient in logistic regression (see for example: https://www.paltmeyer.com/blog/posts/bayesian-logit/): ∇ℓ=(μ-y)x. We can use this analytical expression to see if we get the expected results.

@testset "Hessian" begin

    nn = Chain(Dense([0 0]))
    la = Laplace(nn)

    # (always ignoring constant)
    @testset "Empirical Fisher - full" begin

        target = 1
        x = [1,1]
        grad = [-0.5,-0.5] # analytical solution for gradient
        @test BayesLaplace.hessian_approximation(la, (x,target))[1:2,1:2] == grad * grad'

        x = [-1,-1]
        grad = [0.5,0.5] # analytical solution for gradient
        @test BayesLaplace.hessian_approximation(la, (x,target))[1:2,1:2] == grad * grad'

        x = [0,0]
        grad = [0,0] # analytical solution for gradient
        @test BayesLaplace.hessian_approximation(la, (x,target))[1:2,1:2] == grad * grad'

    end

end

# We know the analytical expression for the Hessian of logit binary cross entropy loss for a single-layer neural net with sigmoid activation just corresponds to the Hessian in logistic regression (see for example: https://www.paltmeyer.com/blog/posts/bayesian-logit/): ∇ℓ=(μ-y)(μ(1-μ)xx'). With a weight-penalty (Gaussian prior), the Hessian becomes: ∇ℓ=∑(μ-y)(μ(1-μ)xx')+H₀. We can use this analytical expression to see if we get the expected results.

@testset "Fitting" begin

    nn = Chain(Dense([0 0]))
    la = Laplace(nn)

    hessian_exact(x,target) = (nn(x).-target).*(nn(x).*(1 .- nn(x)).*x*x') + la.H₀

    @testset "Empirical Fisher - full" begin

        target = [1]
        x = [[0,0]]
        fit!(la,zip(x,target))
        @test la.H[1:2,1:2] == hessian_exact(x[1],target[1])

    end

end


using Plots
using DataFrames
using LinearAlgebra
using Distributions

"""
    vowelPlot(f1, f2, cats; [meansOnly=false, addLabels=true, ell=false, ellPercent=0.67, nEllPts=500, markersize=1, linewidth=2, kw...])

Create an F1-by-F2 vowel plot. The `f1` values are displayed along the x-axis, and the `f2` values are displayed along the y-axis, with each unique vowel class in `cats` being represented with a new color. The series labels in the legend will take on the unique values contained in `cats`. The alternate display whereby reversed F2 is on the x-axis and reversed F1 is on the y-axis can be created by passing the F2 values in for the `f1` argument and F1 values in for the `f2` argument, and then using the `:flip` magic argument provided by the `Plots` package.

If `meansOnly` is set to true, only the mean values for each vowel category are plotted. Using `ell=true` will plot a data ellipse that approximately encompases the percentage of data specified by `ellPercent`. The ellipse is represented by a number of points specified with `nEllPts`. Other arguments to `plot` are passed in through the splatted `kw` argument. Setting the `addLabels` argument to `true` will add the text label of the vowel category above and to the right of the mean.

Args
======

* `f1` The F1 values, or otherwise the values to plot on the x-axis
* `f2` The F2 values, or otherwise the values to plot on the y-axis
* `cats` The vowel categories associated with each F1, F2 pair
* `meansOnly` (keyword) Plot only mean value for each category
* `addLabels` (keyword) Add labels for each category to the plot near the mean
* `ell` (keyword) Whether to add data ellipses to the plot
* `ellPercent` (keyword) How much of the data distribution the ellipse should cover (approximately)
* `nEllPts` (keyword) How many points should be used when plotting the ellipse
* `markersize` (keyword) How large the markers should be; passed directly to `plot`
* `linewidth` (keyword) How wide the line for the llipses should be; passed directly to `plot`
"""
function vowelPlot(f1, f2, cats; meansOnly=false, addLabels=false, ell=false, ellPercent=0.67, nEllPts=500, markersize=3, linewidth=3, kw...)
  d = DataFrame(f1=f1, f2=f2, cat=cats)
  groups = groupby(d, :cat)

  if meansOnly
    p = Plots.scatter([mean(groups[1].f1)], [mean(groups[1].f2)], label=groups[1].cat[1],
      markersize=markersize; kw...)
  else
    p = Plots.scatter(groups[1].f1, groups[1].f2, label=groups[1].cat[1],
      markersize=markersize; kw...)
  end

  jt1 = abs(mean(f1) / 20)
  jt2 = abs(mean(f2) / 20)

  if addLabels
    p = annotate!(mean(groups[1].f1) + jt1, mean(groups[1].f2) + jt2, text(groups[1].cat[1], color=palette(:auto)[1]))
  end

  if ell
    e = ellipsePts(groups[1].f1, groups[1].f2, percent=ellPercent, nPoints=nEllPts)
    p = Plots.plot!(e[:,1], e[:,2], label="", color=1, linewidth=linewidth)
  end

  for (i, g) in enumerate(groups)
    if i == 1 continue end

    if meansOnly
      p = Plots.scatter!([mean(g.f1)], [mean(g.f2)], label=g.cat[1], color=i,
        markersize=markersize)
    else
      p = Plots.scatter!(g.f1, g.f2, label=g.cat[1], color=i, markersize=markersize)
    end

    if addLabels
      p = annotate!(mean(g.f1) + jt1, mean(g.f2) + jt2, text(g.cat[1], color=palette(:auto)[i]))
    end

    if ell
      e = ellipsePts(g.f1, g.f2, percent=ellPercent, nPoints=nEllPts)
      p = Plots.plot!(e[:,1], e[:,2], label="", color=i, linewidth=linewidth)
    end
  end
  return p
end

"""
    ellipsePts(f1, f2; percent=0.95, nPoints=500)

Calculates `nPoints` points of the perimeter of a data ellipse for `f1` and `f2` with approximately the percent of the data spcified by `percent` contained within the ellipse. Points are returned in counter-clockwise order as the polar angle of rotation moves from 0 to 2π.

See Friendly, Monette, and Fox (2013, Elliptical insights: Understanding statistical methods through elliptical geometry, Statistical science 28(1), 1-39) for more information on the calculation process.

Args
======

* `f1` The F1 values or otherwise x-axis values
* `f2` The F2 values or otherwise y-axis values
* `percent` (keyword) Percent of the data distribution the ellipse should approximately cover
* `nPoints` (keyword) How many points to use when drawing the ellipse
"""
function ellipsePts(f1, f2; percent=0.95, nPoints=500)
  Σ = cov(hcat(f1, f2))
  L = cholesky(Σ).L

  # we have two dimensions (F1 and F2), so the chisq distribution has
  # two degrees of freedom
  r = sqrt(quantile(Chisq(2), percent))
  t = range(0, 2π, length=nPoints)
  S = hcat(cos.(t), sin.(t)) .* r

  pts = L * S'
  pts[1,:] .+= mean(f1)
  pts[2,:] .+= mean(f2)
  return Array(pts')
end