export Bellhop

"""
$(TYPEDEF)
A propagation model based on an external FORTRAN Bellhop executable.
"""
struct Bellhop{T} <: PropagationModel{T}
  env::T
  nbeams::Int
  minangle::Float32
  maxangle::Float32
  gaussian::Bool
  debug::Bool
  function Bellhop(env, nbeams, minangle, maxangle, gaussian, debug)
    nbeams < 0 && (nbeams = 0)
    -π/2 ≤ minangle ≤ π/2 || throw(ArgumentError("minangle should be between -π/2 and π/2"))
    -π/2 ≤ maxangle ≤ π/2 || throw(ArgumentError("maxangle should be between -π/2 and π/2"))
    minangle < maxangle || throw(ArgumentError("maxangle should be more than minangle"))
    new{typeof(env)}(check(Bellhop, env), nbeams, Float32(minangle), Float32(maxangle), gaussian, debug)
  end
end

"""
    Bellhop(env; gaussian=false, debug=false)
    Bellhop(env, nbeams, minangle, maxangle, gaussian, debug)

Create a Bellhop propagation model.
"""
Bellhop(env; gaussian=false, debug=false) = Bellhop(env, 0, -80°, 80°, gaussian, debug)

### interface functions

function check(::Type{Bellhop}, env::Union{<:UnderwaterEnvironment,Missing})
  if env === missing
    mktempdir(prefix="bellhop_") do dirname
      try
        bellhop(dirname, false)
      catch e
        e isa BellhopError && e.details == ["Unable to execute bellhop.exe"] && throw(e)
      end
    end
  else
    seabed(env) isa RayleighReflectionCoef || throw(ArgumentError("Seabed type not supported"))
    seasurface(env) === Vacuum || throw(ArgumentError("Only vacuum seasurface supported"))
  end
  env
end

function arrivals(model::Bellhop, tx1::AcousticSource, rx1::AcousticReceiver)
  mktempdir(prefix="bellhop_") do dirname
    writeenv(model, [tx1], [rx1], "A", dirname)
    bellhop(dirname, model.debug)
    readarrivals(joinpath(dirname, "model.arr"))
  end
end

function transfercoef(model::Bellhop, tx1::AcousticSource, rx::AcousticReceiverGrid2D; mode=:coherent)
  if mode === :coherent
    taskcode = "C"
  elseif mode === :incoherent
    taskcode = "I"
  elseif mode === :semicoherent
    taskcode = "S"
  else
    throw(ArgumentError("Unknown mode :" * string(mode)))
  end
  mktempdir(prefix="bellhop_") do dirname
    writeenv(model, [tx1], rx, taskcode, dirname)
    bellhop(dirname, model.debug)
    readshd(joinpath(dirname, "model.shd"))
  end
end

function transfercoef(model::Bellhop, tx1::AcousticSource, rx1::AcousticReceiver; mode=:coherent)
  if mode === :coherent
    taskcode = "C"
  elseif mode === :incoherent
    taskcode = "I"
  elseif mode === :semicoherent
    taskcode = "S"
  else
    throw(ArgumentError("Unknown mode :" * string(mode)))
  end
  mktempdir(prefix="bellhop_") do dirname
    writeenv(model, [tx1], [rx1], taskcode, dirname)
    bellhop(dirname, model.debug)
    readshd(joinpath(dirname, "model.shd"))[1]
  end
end

function eigenrays(model::Bellhop, tx1::AcousticSource, rx1::AcousticReceiver)
  mktempdir(prefix="bellhop_") do dirname
    writeenv(model, [tx1], [rx1], "E", dirname)
    bellhop(dirname, model.debug)
    readrays(joinpath(dirname, "model.ray"))
  end
end

function rays(model::Bellhop, tx1::AcousticSource, θ::AbstractArray, rmax)
  θ isa AbstractRange || length(θ) == 1 || throw(ArgumentError("Bellhop only supports uniformly spaced angles"))
  all(-π/2 .< θ .< π/2) || throw(ArgumentError("θ must be between -π/2 and π/2"))
  mktempdir(prefix="bellhop_") do dirname
    writeenv(model, [tx1], [AcousticReceiver(rmax, 0.0)], "R", dirname; minangle=minimum(θ), maxangle=maximum(θ), nbeams=length(θ))
    bellhop(dirname, model.debug)
    readrays(joinpath(dirname, "model.ray"))
  end
end

rays(model::Bellhop, tx1::AcousticSource, θ, rmax) = rays(model, tx1, [θ], rmax)[1]

### helper functions

struct BellhopError <: Exception
  details::Vector{String}
end

function Base.show(io::IO, e::BellhopError)
  if length(e.details) == 1
    println(io, e.details[1])
  else
    println(io, "Bellhop said:")
    for s ∈ e.details
      println(io, "  ", s)
    end
  end
end

function bellhop(dirname, debug)
  infilebase = joinpath(dirname, "model")
  outfilename = joinpath(dirname, "output.txt")
  try
    run(pipeline(ignorestatus(`bellhop.exe $infilebase`); stdout=outfilename, stderr=outfilename))
    if debug
      @info "Bellhop run completed in $dirname, press ENTER to delete intermediate files..."
      readline()
    end
  catch
    throw(BellhopError(["Unable to execute bellhop.exe"]))
  end
  err = String[]
  checkerr!(err, outfilename)
  checkerr!(err, joinpath(dirname, "model.prt"))
  if length(err) > 0
    throw(BellhopError(err))
  end
end

function checkerr!(err, filename)
  output = false
  open(filename) do f
    for s in eachline(f)
      if output || occursin("ERROR", uppercase(s))
        push!(err, s)
        output = true
      end
    end
  end
end

function writeenv(model::Bellhop, tx::Vector{<:AcousticSource}, rx::AbstractArray{<:AcousticReceiver}, taskcode, dirname; minangle=model.minangle, maxangle=model.maxangle, nbeams=model.nbeams)
  all(location(tx1)[1] == 0.0 for tx1 ∈ tx) || throw(ArgumentError("Bellhop requires transmitters at (0, 0, z)"))
  all(location(tx1)[2] == 0.0 for tx1 ∈ tx) || throw(ArgumentError("Bellhop 2D requires transmitters in the x-z plane"))
  all(location(rx1)[1] >= 0.0 for rx1 ∈ rx) || throw(ArgumentError("Bellhop requires receivers to be in the +x halfspace"))
  all(location(rx1)[2] == 0.0 for rx1 ∈ rx) || throw(ArgumentError("Bellhop 2D requires receivers in the x-z plane"))
  env = model.env
  name = split(basename(dirname), "_")[end]
  filename = joinpath(dirname, "model.env")
  open(filename, "w") do io
    println(io, "'", name, "'")
    flist = [nominalfrequency(tx1) for tx1 ∈ tx]
    f = sum(flist) / length(flist)
    maximum(abs.(flist .- f))/f > 0.2 && @warn("Source frequency varies by more than 20% from nominal frequency")
    @printf(io, "%0.6f\n", f)
    println(io, "1")
    if length(rx) == 1
      maxr = location(rx[1])[1]
    elseif rx isa AcousticReceiverGrid2D
      maxr = maximum(rx.xrange)
    else
      throw(ArgumentError("Receivers must be on a 2D grid"))
    end
    ss = ssp(env)
    sspi = "S"
    ss isa SampledSSP1D && ss.interp === :linear && (sspi = "C")
    print(io, "'", sspi, "VWT")
    alt = altimetry(env)
    if !(alt isa FlatSurface)
      print(io, "*")
      createadfile(joinpath(dirname, "model.ati"), alt, (p...) -> -altitude(p...), maxr, f)
    end
    println(io, "'")
    bathy = bathymetry(env)
    waterdepth = maxdepth(bathy)
    @printf(io, "1 0.0 %0.6f\n", waterdepth)
    if ss isa IsoSSP
      @printf(io, "0.0 %0.6f /\n", soundspeed(ss, 0.0, 0.0, 0.0), )
      @printf(io, "%0.6f %0.6f /\n", waterdepth, soundspeed(ss, 0.0, 0.0, 0.0))
    elseif ss isa SampledSSP1D
      for i ∈ 1:length(ss.z)
        @printf(io, "%0.6f %0.6f /\n", -ss.z[i], ss.c[i])
      end
    else
      for d ∈ range(0.0, waterdepth; length=recommendlength(waterdepth, f))
        @printf(io, "%0.6f %0.6f /\n", d, soundspeed(ss, 0.0, 0.0, -d))
      end
      floor(waterdepth) != waterdepth && @printf(io, "%0.6f %0.6f /\n", waterdepth, soundspeed(ss, 0.0, 0.0, -waterdepth))
    end
    print(io, "'A")
    if !(bathy isa ConstantDepth)
      print(io, "*")
      createadfile(joinpath(dirname, "model.bty"), bathy, depth, maxr, f)
    end
    println(io, "' 0.0") # bottom roughness = 0
    bed = seabed(env)
    c2 = soundspeed(ss, 0.0, 0.0, -waterdepth) * bed.cᵣ
    α = bed.δ * 40π / log(10)      # based on APL-UW TR 9407 (1994), IV-9 equation (4)
    @printf(io, "%0.6f %0.6f 0.0 %0.6f %0.6f /\n", waterdepth, c2, bed.ρᵣ, α)
    printarray(io, [-location(tx1)[3] for tx1 ∈ tx])
    if length(rx) == 1
      printarray(io, [-location(rx[1])[3]])
      printarray(io, [maxr / 1000.0])
    elseif rx isa AcousticReceiverGrid2D
      printarray(io, reverse(-rx.zrange))
      printarray(io, rx.xrange ./ 1000.0)
    end
    println(io, "'", taskcode, model.gaussian ? "B'" : "'")
    @printf(io, "%d\n", nbeams)
    @printf(io, "%0.6f %0.6f /\n", rad2deg(minangle), rad2deg(maxangle))
    @printf(io, "0.0 %0.6f %0.6f\n", 1.01*waterdepth, 1.01 * maxr / 1000.0)
  end
end

function printarray(io, a::AbstractVector)
  println(io, length(a))
  for a1 ∈ a
    @printf(io, "%0.6f ", a1)
  end
  println(io, "/")
end

function recommendlength(x, f)
  # recommendation based on nominal half-wavelength spacing
  λ = 1500.0 / f
  clamp(round(Int, 2x / λ) + 1, 25, 1000)
end

function createadfile(filename, data, func, maxr, f)
  open(filename, "w") do io
    interp = "L"
    if data isa SampledDepth || data isa SampledAltitude
      x = data.x
      data.interp !== :linear && (interp = "C")
    else
      x = range(0.0, maxr; length=recommendlength(maxr, f))
    end
    println(io, "'", interp, "'")
    println(io, length(x))
    for i ∈ 1:length(x)
      @printf(io, "%0.6f %0.6f\n", x[i]/1000.0, func(data, x[i], 0.0))
    end
  end
end

function readrays(filename)
  rays = RayArrival{Float64,Float64}[]
  open(filename, "r") do io
    [readline(io) for i ∈ 1:7]
    while !eof(io)
      s = strip(readline(io))
      length(s) == 0 && break
      aod = parse(Float64, s)
      pts, sb, bb = parse.(Int, split(strip(readline(io)) ,r" +"))
      raypath = Array{NTuple{3,Float64}}(undef, pts)
      for k ∈ 1:pts
        x, d = parse.(Float64, split(strip(readline(io)) ,r" +"))
        raypath[k] = (x, 0.0, -d)
      end
      push!(rays, RayArrival(NaN64, NaN64, sb, bb, -deg2rad(aod), NaN64, raypath))
    end
  end
  rays
end

function readarrivals(filename)
  arrivals = RayArrival{Float64,Missing}[]
  open(filename, "r") do io
    s = strip(readline(io))
    if occursin("2D", s)
      f = parse(Float64, strip(readline(io)))
      v = split(strip(readline(io)) ,r" +")
      n = parse(Int, v[1])
      txdepth = parse.(Float64, v[2:end])
      n == length(txdepth) || error("Wrong number of txdepth entries in arrivals")
      v = split(strip(readline(io)) ,r" +")
      n = parse(Int, v[1])
      rxdepth = parse.(Float64, v[2:end])
      n == length(rxdepth) || error("Wrong number of rxdepth entries in arrivals")
      v = split(strip(readline(io)) ,r" +")
      n = parse(Int, v[1])
      rxrange = parse.(Float64, v[2:end])
      n == length(rxrange) || error("Wrong number of rxrange entries in arrivals")
    else
      v = split(s ,r" +")
      f = parse(Float64, v[1])
      n1, n2, n3 = parse.(Int, v[2:4])
      txdepth = parse.(Float64, split(strip(readline(io)) ,r" +"))
      rxdepth = parse.(Float64, split(strip(readline(io)) ,r" +"))
      rxrange = parse.(Float64, split(strip(readline(io)) ,r" +"))
      n1 == length(txdepth) || error("Wrong number of txdepth entries in arrivals")
      n2 == length(rxdepth) || error("Wrong number of rxdepth entries in arrivals")
      n3 == length(rxrange) || error("Wrong number of rxrange entries in arrivals")
    end
    for j ∈ 1:length(txdepth)
      readline(io)
      for k ∈ 1:length(rxdepth)
        for m ∈ 1:length(rxrange)
          count = parse(Int, strip(readline(io)))
          for n ∈ 1:count
            v = split(strip(readline(io)) ,r" +")
            length(v) == 8 || error("Wrong number of data entries in arrivals")
            A, ph, t, _, aod, aoa = parse.(Float64, v[1:6])
            sb, bb = parse.(Int, v[7:8])
            push!(arrivals, RayArrival(t, A * cis(deg2rad(ph)), sb, bb, -deg2rad(aod), deg2rad(aoa)))
          end
        end
      end
    end
  end
  sort(arrivals; by = a -> a.time)
end

function readshd(filename)
  open(filename, "r") do io
    r = read(io, UInt32)
    seek(io, 4r)
    b = Array{UInt8}(undef, 10)
    read!(io, b)
    strip(String(b)) == "rectilin" || error("Bad shd file format: incorrect ptype")
    seek(io, 8r)
    nfreq = read(io, UInt32)
    nfreq == 1 || error("Bad shd file format: incorrect nfreq")
    nθ = read(io, UInt32)
    nθ == 1 || error("Bad shd file format: incorrect nθ")
    nsx = read(io, UInt32)
    nsy = read(io, UInt32)
    nsd = read(io, UInt32)
    nsd == 1 || error("Bad shd file format: incorrect nsd")
    nrd = read(io, UInt32)
    nrr = read(io, UInt32)
    pressure = Array{ComplexF32}(undef, nrr, nrd)
    for ird ∈ 0:nrd-1
      recnum = 10 + ird
      seek(io, recnum * 4r)
      temp = Array{ComplexF32}(undef, nrr)
      read!(io, temp)
      pressure[:,ird+1] .= -temp    # negative because Bellhop seems to have a 180° phase inversion
    end
    reverse(pressure; dims=2)
  end
end


"""
INI method:

    run_omniscape(path::String)

In-memory method:

    run_omniscape(
        cfg::Dict{String, String}
        resistance::Array{Union{Float64, Missing}, 2};
        reclass_table = Array{Union{Float64, Missing}, 2}(undef, 1, 2),
        source_strength = source_from_resistance(resistance, cfg, reclass_table),
        condition1 = Array{Union{Float64, Missing}, 2}(undef, 1, 1),
        condition2 = Array{Union{Float64, Missing}, 2}(undef, 1, 1),
        condition1_future = Array{Union{Float64, Missing}, 2}(undef, 1, 1),
        condition2_future = Array{Union{Float64, Missing}, 2}(undef, 1, 1),
        wkt = "",
        geotransform = [0.0, 1.0, 0.0, 0.0, 0.0, -1.0],
        write_outputs = false
    )

Compute omnidirectional current flow. All array inputs for the in-memory method
should be of type `Array{Union{T, Missing}, 2} where T <: Number`, with
`missing` used for NoData pixels.

# Parameters
**`path`**: The path to an INI file containing run parameters. See the
[Settings and Options](@ref) section of the User Guide for descriptions of the run
parameters.

**`cfg`**: A dictionary of Omniscape run parameters. See the [Settings and Options](@ref)
section of the User Guide for descriptions of the run parameters and their
default values. The in-memory method of `run_omniscape` ignores the following
keys: `resistance_file`, `source_file`, `reclass_table`, `condition1_file`,
`condition2_file`, `condition1_future_file`, and `condition2_future_file`. These
all specify file paths, so they do not apply to the in-memory method
of `run_omniscape`.

**`resistance`**: A 2D, north-oriented array of resistance values. Use
`missing` for NoData (infinite resistance). `resistance` cannot contain zeros or
negative values.

# Keyword Arguments

**`reclass_table`**:  A two column array. The first column contains the original
resistance values in the resistance surface, and the second column specifies
what those values should be changed to. You can reclassify values to `missing`
to replace them with infinite resistance (NoData).

**`source_strength`**: A 2D, north-oriented array (with size equal to
`size(resistance)`) of source strength values. `source_strength` is only
required if `source_from_resistance` in `cfg` is set to "false"
(the default value).

**`condition1`**: Optional. Required if `conditional` in`cfg` is set to "true".
A 2D, north-oriented array (with size equal to `size(resistance)`). See
[Climate Connectivity](@ref) and [Conditional Connectivity Options](@ref) for
more information.

**`condition2`**: Optional. Required if `conditional` in`cfg` is set to "true"
and `n_conditions` in `cfg` is set to "2". A 2D, north-oriented array (with size
equal to `size(resistance)`). See [Climate Connectivity](@ref) and
[Conditional Connectivity Options](@ref) for more information.

**`condition1_future`**: Optional. Required if `conditional` in `cfg` is set
to "true" and `compare_to_future` in `cfg` is set to "1" or "both".
A 2D, north-oriented array (with size equal to `size(resistance)`). See
[Climate Connectivity](@ref) and [Conditional Connectivity Options](@ref) for
more information.

**`condition2_future`**: Optional. A 2D, north-oriented array (with size equal to
`size(resistance)`). See [Climate Connectivity](@ref) and
[Conditional Connectivity Options](@ref) for more information. Required if
`conditional` in`cfg` is set to "true", `n_conditions` in `cfg` is set to "2",
and `compare_to_future` in `cfg` is set to "2" or "both".

**`wkt`**: Optionally specify a Well Known Text representation of the projection
used for your spatial data inputs. Only used if Omniscape writes raster
outputs to disk.

**`geotransform`**: In addition to `wkt`, optionally specify a geotransform.
The geotransform is a 6-element vector with elements as follows for a north up
oriented image: `[<x coord of upper left orner>, <pixel width>,
<row rotation (typically 0)>, <y coord of upper left corner>,
<column rotation (typically 0)>, <pixel height (negative number)>]`.
Only used when writing raster outputs to disk.

**`write_outputs`**: Boolean specifying if outputs should be written to disk.
Defaults to `false`. If `true`, `cfg` must contain a value for the `project_name`
key.

"""
function run_omniscape(
        cfg::Dict{String, String},
        resistance::Array{Union{T, Missing}, 2} where T <: Number;
        reclass_table::Array{Union{T, Missing}, 2} where T <: Number = Array{Union{Float64, Missing}, 2}(undef, 1, 2),
        source_strength::Array{Union{T, Missing}, 2} where T <: Number = source_from_resistance(resistance, cfg, reclass_table),
        condition1::Array{Union{T, Missing}, 2} where T <: Number = Array{Union{Float64, Missing}, 2}(undef, 1, 1),
        condition2::Array{Union{T, Missing}, 2} where T <: Number = Array{Union{Float64, Missing}, 2}(undef, 1, 1),
        condition1_future::Array{Union{T, Missing}, 2} where T <: Number = condition1,
        condition2_future::Array{Union{T, Missing}, 2} where T <: Number = condition2,
        wkt::String = "",
        geotransform::Array{Float64, 1} = [0., 1., 0., 0., 0., -1.0],
        write_outputs::Bool = false)

    start_time = time()
    n_threads = nthreads()
    cfg_user = cfg

    # Check for unsupported or missing arguments
    check_unsupported_args(cfg)
    check_missing_args_dict(cfg_user) && return

    cfg = init_cfg()
    update_cfg!(cfg, cfg_user)

    ## Parse commonly called integer arguments
    int_arguments = Dict{String, Int64}()

    int_arguments["block_size"] = Int64(round(parse(Float64,
                                                    cfg["block_size"])))

    check_block_size!(int_arguments)

    int_arguments["block_radius"] = Int64((int_arguments["block_size"] - 1) / 2)
    int_arguments["radius"] = Int64(round(parse(Float64, cfg["radius"])))
    int_arguments["buffer"] = Int64(round(parse(Float64, cfg["buffer"])))
    int_arguments["n_conditions"] = Int64(round(parse(Float64, cfg["n_conditions"])))

    ## Parse other arguments
    compare_to_future = lowercase(cfg["compare_to_future"])
    precision = cfg["precision"] in SINGLE ? Float32 : Float64

    # flags
    os_flags = get_omniscape_flags(cfg)

    # other
    source_threshold = parse(Float64, cfg["source_threshold"])
    project_name = cfg["project_name"]
    file_format = os_flags.write_as_tif ? "tif" : "asc"

    check_solver!(cfg)
    solver = cfg["solver"]

    ## Set number of BLAS threads to 1 when parallel processing
    if os_flags.parallelize && nthreads() != 1
        BLAS.set_num_threads(1)
    end

    check_resistance_values(resistance) && return

    # Reclassify resistance layer
    if os_flags.reclassify
        reclassify_resistance!(resistance, reclass_table)
    end

    int_arguments["nrows"] = size(source_strength, 1)
    int_arguments["ncols"] = size(source_strength, 2)

    # Set up conditional connctivity stuff
    conditions = Conditions(cfg["comparison1"],
                            cfg["comparison2"],
                            parse(Float64, cfg["condition1_lower"]),
                            parse(Float64, cfg["condition1_upper"]),
                            parse(Float64, cfg["condition2_lower"]),
                            parse(Float64, cfg["condition2_upper"]))

    condition_layers = ConditionLayers(condition1, condition1_future, condition2, condition2_future)

    ## Setup Circuitscape configuration
    cs_cfg_dict = init_csdict(cfg)
    cs_cfg_dict["solver"] = solver
    cs_cfg = Circuitscape.init_config()
    Circuitscape.update!(cs_cfg, cs_cfg_dict)

    ## Calculate targets
    targets = get_targets(source_strength,
                          int_arguments,
                          precision)

    ## Circuitscape calls in loop over targets
    n_targets = size(targets, 1)

    # Permute targets randomly (to get better estimate of ProgressMeter ETA)
    targets = targets[randperm(n_targets), :]

    ## Define parameters for cs
    # Get flags
    o = Circuitscape.OutputFlags(false, false,
                                 false, false,
                                 false, false,
                                 false, false)

    precision_name = precision == Float64 ? "double" : "single"
    ## Add parallel workers
    if os_flags.parallelize
        @info("Starting up Omniscape with $(n_threads) workers and $(precision_name) precision")
        @info("Using Circuitscape with the $(uppercase(solver)) solver...")

        cum_currmap = fill(convert(precision, 0.),
                           int_arguments["nrows"],
                           int_arguments["ncols"],
                           n_threads)

        if os_flags.calc_flow_potential || os_flags.calc_normalized_current
            fp_cum_currmap = fill(convert(precision, 0.),
                                  int_arguments["nrows"],
                                  int_arguments["ncols"],
                                  n_threads)
        else
            # Hacky fix -- a later function needs fp_cum_currmap to be an array
            fp_cum_currmap = Array{precision, 3}(undef, 1, 1, 1)
        end
    else
        @info("Starting up Omniscape with 1 worker and $(precision_name) precision")
        @info("Using Circuitscape with the $(uppercase(solver)) solver...")
        cum_currmap = fill(convert(precision, 0.),
                          int_arguments["nrows"],
                          int_arguments["ncols"],
                          1)

        if os_flags.calc_flow_potential || os_flags.calc_normalized_current
            fp_cum_currmap = fill(convert(precision, 0.),
                                  int_arguments["nrows"],
                                  int_arguments["ncols"],
                                  1)
        else
            fp_cum_currmap = Array{precision, 3}(undef, 1, 1, 1)
        end
    end

    cs_flags = Circuitscape.RasterFlags(true, false, true,
                                        false, false,
                                        false, Symbol("rmvsrc"),
                                        cfg["connect_four_neighbors_only"] in TRUELIST,
                                        false, solver, o)

    if os_flags.correct_artifacts && !(int_arguments["block_size"] == 1)
        @info("Calculating block artifact correction array...")
        correction_array = calc_correction(int_arguments,
                                           os_flags,
                                           cs_cfg,
                                           cs_flags,
                                           condition_layers,
                                           conditions,
                                           precision)

    else
        correction_array = Array{precision, 2}(undef, 1, 1)
    end

    ## Calculate and accumulate currents on each worker
    @info("Solving moving window targets...")

    ## Create progress object
    p = Progress(n_targets; dt = 0.25, barlen = min(50, displaysize(stdout)[2] - length("Progress: 100%  Time: 00:00:00")))

    if os_flags.parallelize
        parallel_batch_size = Int64(round(parse(Float64, cfg["parallel_batch_size"])))
        n_batches = Int(ceil(n_targets / parallel_batch_size))

        @threads for i in 0:(n_batches - 1)
            start_ind = parallel_batch_size * i + 1
            end_ind = min(n_targets, start_ind + parallel_batch_size - 1)

            for j in start_ind:end_ind
                target = Target(Int64(targets[j, 1]), Int64(targets[j, 2]), float(targets[j, 3]))
                try
                    solve_target!(target,
                                  int_arguments,
                                  source_strength,
                                  resistance,
                                  os_flags,
                                  cs_cfg,
                                  cs_flags,
                                  condition_layers,
                                  conditions,
                                  correction_array,
                                  cum_currmap,
                                  fp_cum_currmap,
                                  precision)
                catch error
                    println("Omniscape failed on the moving window centered on row $(target.y_coord) column $(target.x_coord)")
                    throw(error)
                end
                next!(p)
            end
        end
    else
        for i in 1:n_targets
            target = Target(Int64(targets[i, 1]), Int64(targets[i, 2]), float(targets[i, 3]))
            try
                solve_target!(target,
                            int_arguments,
                            source_strength,
                            resistance,
                            os_flags,
                            cs_cfg,
                            cs_flags,
                            condition_layers,
                            conditions,
                            correction_array,
                            cum_currmap,
                            fp_cum_currmap,
                            precision)
            catch error
                println("Omniscape failed on the moving window centered on row $(target.y_coord) column $(target.x_coord)")
                throw(error)
            end
            next!(p)
        end
    end

    ## Set some objects to nothing to free up memory
    source_strength = nothing
    condition1 = nothing
    condition1_future = nothing
    condition2 = nothing
    condition2_future = nothing
    GC.gc()

    ## Collapse 3-dim cum current arrays to 2-dim via sum
    cum_currmap = dropdims(sum(cum_currmap, dims = 3), dims = 3)

    if os_flags.calc_flow_potential || os_flags.calc_normalized_current
        fp_cum_currmap = dropdims(sum(fp_cum_currmap, dims = 3), dims = 3)
    end

    ## Normalize by flow potential
    if os_flags.calc_normalized_current
        normalized_cum_currmap = cum_currmap ./ fp_cum_currmap
        # replace NaNs with 0's
        normalized_cum_currmap[isnan.(normalized_cum_currmap)] .= 0
    end

    if write_outputs
        ## create new directory if project_name already exists
        dir_suffix = 1
        while isdir(string(project_name, "_$(dir_suffix)"))
            dir_suffix+=1
        end
        isdir(project_name) && (project_name = string(project_name, "_$(dir_suffix)"))
        mkpath(project_name)
    end

    ## Overwrite no data
    if os_flags.mask_nodata
        if os_flags.calc_normalized_current
            normalized_cum_currmap[ismissing.(resistance)] .= -9999
        end
        if os_flags.calc_flow_potential
            fp_cum_currmap[ismissing.(resistance)] .= -9999
        end
        cum_currmap[ismissing.(resistance)] .= -9999
    end

    GC.gc()

    ## Write outputs
    if write_outputs
        write_cfg(cfg_user, project_name)

        if os_flags.reclassify && os_flags.write_reclassified_resistance
            resistance[ismissing.(resistance)] .= -9999
            write_raster("$(project_name)/classified_resistance",
                         convert(Array{precision, 2}, resistance),
                         wkt,
                         geotransform,
                         file_format)
        end

        if os_flags.write_raw_currmap
            write_raster("$(project_name)/cum_currmap",
                         cum_currmap,
                         wkt,
                         geotransform,
                         file_format)
        end

        if os_flags.calc_flow_potential
            write_raster("$(project_name)/flow_potential",
                         fp_cum_currmap,
                         wkt,
                         geotransform,
                         file_format)
        end

        if os_flags.calc_normalized_current
            write_raster("$(project_name)/normalized_cum_currmap",
                         normalized_cum_currmap,
                         wkt,
                         geotransform,
                         file_format)
        end
    end

    resistance = nothing

    @info("Time taken to complete job: $(round(time() - start_time; digits = 4)) seconds")

    if write_outputs
        @info("Outputs written to $(string(pwd(),"/",project_name))")
    end
    ## Return outputs, depending on user options
    # convert arrays, replace -9999's with missing
    cum_currmap = convert_and_fill_missing(cum_currmap, precision)

    if os_flags.calc_normalized_current && !os_flags.calc_flow_potential
        normalized_cum_currmap = convert_and_fill_missing(normalized_cum_currmap, precision)
        return cum_currmap, normalized_cum_currmap
    elseif !os_flags.calc_normalized_current && os_flags.calc_flow_potential
        fp_cum_currmap = convert_and_fill_missing(fp_cum_currmap, precision)
        return cum_currmap, fp_cum_currmap
    elseif os_flags.calc_normalized_current && os_flags.calc_flow_potential
        fp_cum_currmap = convert_and_fill_missing(fp_cum_currmap, precision)
        normalized_cum_currmap = convert_and_fill_missing(normalized_cum_currmap, precision)
        return cum_currmap, fp_cum_currmap, normalized_cum_currmap
    else
        return cum_currmap
    end
end

function run_omniscape(path::String)
    cfg_user = parse_cfg(path)
    check_missing_args_ini(cfg_user) && return

    cfg = init_cfg()
    update_cfg!(cfg, cfg_user)

    ## flags and other cfg args
    os_flags = get_omniscape_flags(cfg)
    compare_to_future = lowercase(cfg["compare_to_future"])
    precision = cfg["precision"] in SINGLE ? Float32 : Float64
    n_conditions = Int64(round(parse(Float64, cfg["n_conditions"])))
    allow_different_projections = cfg["allow_different_projections"] in TRUELIST
    source_threshold = parse(precision, cfg["source_threshold"])

    ## Load resistance
    resistance_raster = read_raster("$(cfg["resistance_file"])", precision)
    resistance = resistance_raster[1]

    wkt = resistance_raster[2]
    geotransform = resistance_raster[3]

    check_resistance_values(resistance) && return

    ## Load reclass table if applicable
    if os_flags.reclassify
        reclass_table = read_reclass_table("$(cfg["reclass_table"])", precision)
    else
        reclass_table = Array{Union{precision, Missing}, 2}(undef, 1, 2)
    end

    ## Load source strengths
    if !os_flags.source_from_resistance
        sources_raster = read_raster("$(cfg["source_file"])", precision)
        source_strength = sources_raster[1]

        # Check for raster alignment
        check_raster_alignment(resistance_raster, sources_raster,
                               "resistance_file", "source_file",
                               allow_different_projections) && return

        # get rid of unneeded raster to save memory
        sources_raster = nothing

        # overwrite nodata with 0
        source_strength[ismissing.(source_strength)] .= 0.0

        # Set values < user-specified threshold to 0
        source_strength[source_strength .< source_threshold] .= 0.0
    else
        source_strength = source_from_resistance(resistance, cfg, reclass_table)
    end

    ## Load condition rasters
    if os_flags.conditional
        condition1_raster = read_raster("$(cfg["condition1_file"])", precision)
        condition1 = condition1_raster[1]

        # Check for raster alignment
        check_raster_alignment(resistance_raster, condition1_raster,
                               "resistance_file", "condition1_file",
                               allow_different_projections) && return

        # get rid of unneedecheck_rasterd raster to save memory
        condition1_raster = nothing

        if compare_to_future == "1" || compare_to_future == "both"
            condition1_future_raster = read_raster("$(cfg["condition1_future_file"])", precision)
            condition1_future = condition1_future_raster[1]

            # Check for raster alignment
            check_raster_alignment(resistance_raster, condition1_future_raster,
                                   "resistance_file", "condition1_future_file",
                                   allow_different_projections) && return

            # get rid of unneeded raster to save memory
            condition1_future_raster = nothing
        else
            condition1_future = condition1
        end

        if n_conditions == 2
            condition2_raster = read_raster("$(cfg["condition2_file"])", precision)
            condition2 = condition2_raster[1]

            # Check for raster alignment
            check_raster_alignment(resistance_raster, condition2_raster,
                                   "resistance_file", "condition2_file",
                                   allow_different_projections) && return

            # get rid of unneeded raster to save memory
            condition2_raster = nothing

            if compare_to_future == "2" || compare_to_future == "both"
                condition2_future_raster = read_raster("$(cfg["condition2_future_file"])", precision)
                condition2_future = condition2_future_raster[1]

                # Check for raster alignment
                check_raster_alignment(resistance_raster, condition2_future_raster,
                                       "resistance_file", "condition2_future_file",
                                       allow_different_projections) && return

                # get rid of unneeded raster to save memory
                condition2_future_raster = nothing
            else
                condition2_future = condition2
            end

        else
            condition2 = Array{Union{Missing, precision}, 2}(undef, 1, 1)
            condition2_future = condition2
        end
    else
        condition1 = Array{Union{Missing, precision}, 2}(undef, 1, 1)
        condition2 = Array{Union{Missing, precision}, 2}(undef, 1, 1)
        condition1_future = condition1
        condition2_future = condition2
    end

    resistance_raster = nothing

    run_omniscape(
        cfg,
        resistance;
        source_strength = source_strength,
        condition1 = condition1,
        condition2 = condition2,
        condition1_future = condition1_future,
        condition2_future = condition2_future,
        geotransform = geotransform,
        wkt = wkt,
        reclass_table = reclass_table,
        write_outputs = true
    )
end


#   This file is part of UnitSystems.jl. It is licensed under the MIT license
#   UnitSystems Copyright (C) 2021 Michael Reed

export Universe, coupling, finestructure, electronunit, protonunit, protonelectron
#const Mu,Ru,SB,hh,cc,m0,e0,ke,me,mp,mu,ee,FF,Z0,G0,Eh,a0,re,g0,lP,ϵ₀,mB = Mᵤ,Rᵤ,σ,𝘩,𝘤,μ₀,ε₀,kₑ,mₑ,mₚ,mᵤ,𝘦,𝔉,Z₀,G₀,Eₕ,a₀,rₑ,g₀,ℓP,ε₀,μB
export slug, ft, KJ1990, KJ2014, RK1990, RK2014, mₑ1990, mₑ2014, temp, units
export slugs, kilograms, lbm, meters, feet, rankine, kelvin, moles, molecules
export UnitSystem, US, SI, MKS, CGS, CGS2019, CGSm, CGSe, HLU, FFF

# == Metric is different
const κ = einstein(SI2019)
const σ = stefan(SI2019) #
const μB = magneton(SI2019) #
const ε₀ = permittivity(SI2019) #
const kₑ = coulomb(SI2019) #
const mₚ = protonmass(SI2019)
const mᵤ = atomicmass(SI2019)
const Mᵤ = molarmass(SI2019)
const 𝔉 = faraday(SI2019) #
const Φ₀ = magneticflux(SI2019) #
const Z₀ = impedance(SI2019) #
const G₀ = conductance(SI2019) #
const Eₕ = hartree(SI2019)
const a₀ = bohr(SI2019)
const rₑ = electronradius(SI2019)
const RK = klitzing(SI2019) #
const KJ = josephson(SI2019) #
const RH,Ry = R∞*mₚ/(mₑ+mₚ),𝘩*𝘤*R∞

const ℓP = length(PlanckGauss,SI2019)
const tP = time(PlanckGauss,SI2019)
const TP = temperature(PlanckGauss,SI2019)

const lS = length(Stoney,SI2019)
const tS = time(Stoney,SI2019)
const mS = mass(Stoney,SI2019)
const qS = charge(Stoney,SI2019)

const lA = length(Hartree,SI2019)
const tA = time(Hartree,SI2019)
const mA = mass(Hartree,SI2019)
const qA = charge(Hartree,SI2019)

const lQCD = length(QCD,SI2019)
const tQCD = time(QCD,SI2019)
const mQCD = mass(QCD,SI2019)

# non standard units

const BTUftlb = 3600/0.5778thermalconductivity(English) # BTU⋅ft⁻¹⋅lb⁻¹
const BTUJ = energy(English)*BTUftlb # BTU⋅J⁻¹

# constant aliases

const mpe, meu, mpu, ainv, aG = μₚₑ, μₑᵤ, μₚᵤ, αinv, αG
const Mu,Ru,SB,hh,cc,m0,e0,ke,me,mp,mu,ee,FF,Z0,G0,Eh,a0,re,g0,lP,aL,ϵ₀ = Mᵤ,Rᵤ,σ,𝘩,𝘤,μ₀,ε₀,kₑ,mₑ,mₚ,mᵤ,𝘦,𝔉,Z₀,G₀,Eₕ,a₀,rₑ,g₀,ℓP,αL,ε₀
export κ, GG, NA, kB, Rᵤ, σ, 𝘩, ħ, 𝘤, μ₀, ε₀, kₑ, mₑ, mₚ, mᵤ, 𝘦, 𝔉, Φ₀, Z₀, G₀, Eₕ, R∞, a₀, rₑ, KJ, RK, Ru, SB, hh, cc, m0, e0, ke, me, mp, mu, ee, FF, Z0, G0, Eh, a0, re, μB
export αG, αinv, μₚₑ, μₑᵤ, μₚᵤ, mpe, meu, mpu, mP, δμ₀, Mᵤ, Mu, RH, Ry, ΔνCs, Kcd, ainv
export cal, kcal, calₜₕ, kcalₜₕ, calᵢₜ, kcalᵢₜ, ℓP, g₀, g0, atm, lbm, BTUJ, BTUftlb, aG
export lP, tP, TP, lS, tS, mS, qS, lA, tA, mA, qA, lQCD, tQCD, mQCD, ϵ₀, αL, aL

# engineering unit systems docs

cgstext(US,AMP,cgs=eval(US)) = """
```Julia
julia> boltzmann($US) # erg⋅K⁻¹
$(boltzmann(cgs))

julia> planckreduced($US) # erg⋅s⋅rad⁻¹
$(planckreduced(cgs))

julia> lightspeed($US) # cm⋅s⁻¹
$(lightspeed(cgs))

julia> permeability($US) # statH⋅cm⁻¹
$(permeability(cgs))

julia> electronmass($US) # g
$(electronmass(cgs))

julia> rationalization($US)
$(rationalization(cgs))
```
"""

for U ∈ (:CGSm,:CGSe,:EMU,:ESU)
    (EU,AMP) = QuoteNode.(U ∉ (:CGSe,:ESU) ? (:EMU,:Bi) : (:ESU,:statA))
@eval @doc """
    $($(QuoteNode(U)))::UnitSystem{1e10*Rᵤ*mₑ/μₑᵤ,1e7*ħ,100𝘤,$($EU≠:EMU ? "(100𝘤)^-2" : 1),1000mₑ,4π}

Centimetre-gram-second `UnitSystem` variant based on `$($EU)` (non-rationalized).

$(cgstext($(QuoteNode(U)),$AMP))
""" $U

U ∉ (:CGSm,:CGSe) && @eval @doc """
    $(Symbol($(QuoteNode(U)),:2019))::UnitSystem{1e7*kB,1e7*ħ,100𝘤,$($EU≠:EMU ? "1e3*μ₀/𝘤^2" : "1e7*μ₀"),1000mₑ}

Centimetre-gram-second `UnitSystem` variant of tuned `SI2019` based on `$($EU)` (rationalized).

$(cgstext(Symbol($(QuoteNode(U)),:2019),$AMP))
""" $(Symbol(U,:2019))
end

@doc """
    Thomson::UnitSystem{1e10*Rᵤ*mₑ/μₑᵤ,1e7*ħ,100𝘤,1,1000mₑ,4π,1/2}

Centimetre-gram-second `UnitSystem` variant `Thomson` (EMU-Lorentz, non-rationalized).

```Julia
julia> boltzmann(Thomson) # erg⋅K⁻¹
$(boltzmann(Thomson))

julia> planckreduced(Thomson) # erg⋅s⋅rad⁻¹
$(planckreduced(Thomson))

julia> lightspeed(Thomson) # cm⋅s⁻¹
$(lightspeed(Thomson))

julia> permeability(Thomson) # abH⋅cm⁻¹
$(permeability(Thomson))

julia> electronmass(Thomson) # g
$(electronmass(Thomson))

julia> rationalization(Thomson)
$(rationalization(Thomson))

julia> lorentz(Thomson)
$(lorentz(Thomson))
```
""" Thomson

@doc """
    Gauss::UnitSystem{1e10*Rᵤ*mₑ/μₑᵤ,1e7*ħ,100𝘤,1,1000mₑ,4π,0.01/𝘤}

Centimetre-gram-second `UnitSystem` variant `CGS` (Gauss-Lorentz, non-rationalized).

```Julia
julia> boltzmann(Gauss) # erg⋅K⁻¹
$(boltzmann(Gauss))

julia> planckreduced(Gauss) # erg⋅s⋅rad⁻¹
$(planckreduced(Gauss))

julia> lightspeed(Gauss) # cm⋅s⁻¹
$(lightspeed(Gauss))

julia> permeability(Gauss) # statH⋅cm⁻¹
$(permeability(Gauss))

julia> electronmass(Gauss) # g
$(electronmass(Gauss))

julia> rationalization(Gauss)
$(rationalization(Gauss))

julia> lorentz(Gauss)
$(lorentz(Gauss))
```
""" Gauss, CGS

@doc """
    LorentzHeaviside::UnitSystem{1e10*Rᵤ*mₑ/μₑᵤ,1e7*ħ,100𝘤,1,1000mₑ,1,0.01/𝘤}

Centimetre-gram-second `UnitSystem` variant `HLU` (Heaviside-Lorentz, rationalized).

```Julia
julia> boltzmann(LorentzHeaviside) # erg⋅K⁻¹
$(boltzmann(LorentzHeaviside))

julia> planckreduced(LorentzHeaviside) # erg⋅s⋅rad⁻¹
$(planckreduced(LorentzHeaviside))

julia> lightspeed(LorentzHeaviside) # cm⋅s⁻¹
$(lightspeed(LorentzHeaviside))

julia> permeability(HLU) # hlH⋅cm⁻¹
$(permeability(LorentzHeaviside))

julia> electronmass(LorentzHeaviside) # g
$(electronmass(LorentzHeaviside))

julia> rationalization(LorentzHeaviside)
$(rationalization(LorentzHeaviside))

julia> lorentz(LorentzHeaviside)
$(lorentz(LorentzHeaviside))
```
""" LorentzHeaviside, HLU

@doc """
    MTS::UnitSystem{1e6*Rᵤ*mₑ/μₑᵤ,1000ħ,𝘤,4π/1e4,mₑ/1000}

Metre-tonne-second `UnitSystem` variant of `Metric` system.

```Julia
julia> boltzmann(MTS) # kJ⋅K⁻¹
$(boltzmann(MTS))

julia> planckreduced(MTS) # kJ⋅s⋅rad⁻¹
$(planckreduced(MTS))

julia> lightspeed(MTS) # m⋅s⁻¹
$(lightspeed(MTS))

julia> permeability(MTS) # kH⋅m⁻¹
$(permeability(MTS))

julia> electronmass(MTS) # t
$(electronmass(MTS))
```
""" MTS

@doc """
    Metric::UnitSystem{Rᵤ*mₑ/μₑᵤ/0.001,ħ,𝘤,4π*1e-7,mₑ}

Systeme International d'Unites (the SI units) adopted as the preferred `UnitSystem`.

```Julia
julia> boltzmann(Metric) # J⋅K⁻¹
$(boltzmann(Metric))

julia> planckreduced(Metric) # J⋅s⋅rad⁻¹
$(planckreduced(Metric))

julia> lightspeed(Metric) # m⋅s⁻¹
$(lightspeed(Metric))

julia> permeability(Metric) # H⋅m⁻¹
$(permeability(Metric))

julia> electronmass(Metric) # kg
$(electronmass(Metric))
```
""" Metric

@doc """
    SI2019::UnitSystem{kB,ħ,𝘤,μ₀,mₑ}

Systeme International d'Unites (the SI units) with `μ₀` for a tuned `charge` exactly.

```Julia
julia> boltzmann(SI2019) # J⋅K⁻¹
$(boltzmann(SI2019))

julia> planckreduced(SI2019) # J⋅s⋅rad⁻¹
$(planckreduced(SI2019))

julia> lightspeed(SI2019) # m⋅s⁻¹
$(lightspeed(SI2019))

julia> permeability(SI2019) # H⋅m⁻¹
$(permeability(SI2019))

julia> electronmass(SI2019) # kg
$(electronmass(SI2019))
```
""" SI2019, SI

@doc """
    CODATA::UnitSystem{Rᵤ*mₑ2014/μₑᵤ/0.001,2/RK2014/KJ2014^2/π,𝘤,2RK2014/𝘤/αinv,mₑ2014}

Metric `UnitSystem` based on Committee on Data of the International Science Council.

```Julia
julia> boltzmann(CODATA) # J⋅K⁻¹
$(boltzmann(CODATA))

julia> planckreduced(CODATA) # J⋅s⋅rad⁻¹
$(planckreduced(CODATA))

julia> lightspeed(CODATA) # m⋅s⁻¹
$(lightspeed(CODATA))

julia> permeability(CODATA) # H⋅m⁻¹
$(permeability(CODATA))

julia> electronmass(CODATA) # kg
$(electronmass(CODATA))
```
""" CODATA

@doc """
    Conventional::UnitSystem{Rᵤ*mₑ1990/μₑᵤ/0.001,2/RK1990/KJ1990^2/π,𝘤,2RK1990/𝘤/αinv,mₑ1990}

Conventional electronic `UnitSystem` with 1990 tuned `josephson` and `klitzing` constants.

```Julia
julia> boltzmann(Conventional) # J⋅K⁻¹
$(boltzmann(Conventional))

julia> planckreduced(Conventional) # J⋅s⋅rad⁻¹
$(planckreduced(Conventional))

julia> lightspeed(Conventional) # m⋅s⁻¹
$(lightspeed(Conventional))

julia> permeability(Conventional) # H⋅m⁻¹
$(permeability(Conventional))

julia> electronmass(Conventional) # kg
$(electronmass(Conventional))
```
""" Conventional

@doc """
    IAU::UnitSystem{Rᵤ*mₑ/μₑᵤ/0.001/Jₛ,ħ*day/Jₛ,day*𝘤/au,4π*1e-7*day^2/Jₛ,mₑ/mₛ}

Astronomical (solar) `UnitSystem` defined by International Astronomical Union.

```Julia
julia> boltzmann(IAU) # M⊙⋅au²⋅D⁻²⋅K⁻¹
$(boltzmann(IAU))

julia> planckreduced(IAU) # M⊙⋅au²⋅D⁻¹⋅rad⁻¹
$(planckreduced(IAU))

julia> lightspeed(IAU) # au⋅D⁻¹
$(lightspeed(IAU))

julia> permeability(IAU) # M⊙⋅au²⋅C⁻²
$(permeability(IAU))

julia> electronmass(IAU) # M⊙
$(electronmass(IAU))
```
""" IAU

@doc """
    English::UnitSystem{kB*rankine/slug/ft^2,ħ/slug/ft^2,𝘤/ft,4π,mₑ/slug}

Engineering `UnitSystem` historically used by Britain and United States.

```Julia
julia> boltzmann(English) # ft⋅lb⋅°R⁻¹
$(boltzmann(English))

julia> planckreduced(English) # ft⋅lb⋅s⋅rad⁻¹
$(planckreduced(English))

julia> lightspeed(English) # ft⋅s⁻¹
$(lightspeed(English))

julia> permeability(English) # slug⋅ft²⋅?⁻²
$(permeability(English))

julia> electronmass(English) # slugs
$(electronmass(English))
```
""" English

@doc """
    EnglishUS::UnitSystem{1000Rᵤ*mₑ/μₑᵤ*rankine/slug/ftUS^2,ħ/slug/ftUS^2,𝘤/ftUS,4π,mₑ/slug}

Engineering `UnitSystem` based on the geophysical US survey foot (1200/3937).

```Julia
julia> boltzmann(EnglishUS) # ftUS⋅lb⋅°R⁻¹
$(boltzmann(EnglishUS))

julia> planckreduced(EnglishUS) # ftUS⋅lb⋅s⋅rad⁻¹
$(planckreduced(EnglishUS))

julia> lightspeed(EnglishUS) # ftUS⋅s⁻¹
$(lightspeed(EnglishUS))

julia> permeability(EnglishUS) # slug⋅ftUS²⋅?⁻²
$(permeability(EnglishUS))

julia> electronmass(EnglishUS) # slugs
$(electronmass(EnglishUS))
```
""" EnglishUS

@doc """
    FFF::UnitSystem{Rᵤ*mₑ/μₑᵤ/0.001*rankine/Jf,ħ/14day/Jf,14day*𝘤/201.168,0,mₑ/mf}

Furlong–firkin–fortnight `FFF` is a humorous `UnitSystem` based on unusal impractical units.

```Julia
julia> boltzmann(FFF) # fir⋅fur²⋅ftn⁻²⋅F⁻¹
$(boltzmann(FFF))

julia> planckreduced(FFF) # fir⋅fur²⋅ftn⁻¹⋅rad⁻¹
$(planckreduced(FFF))

julia> lightspeed(FFF) # fur⋅ftn⁻¹
$(lightspeed(FFF))

julia> permeability(FFF) # fir⋅fur²⋅Inf⁻²
$(permeability(FFF))

julia> electronmass(FFF) # fir
$(electronmass(FFF))
```
""" FFF

@doc """
    Kennelly::UnitSystem{Rᵤ*mₑ/μₑᵤ/0.001,ħ,𝘤,1e-7,mₑ,4π}

Kennelly ? variant `UnitSystem` of the standard `Metric` units ???

```Julia
julia> boltzmann(Kennelly) # J⋅K⁻¹
$(boltzmann(Kennelly))

julia> planckreduced(Kennelly) # J⋅s⋅rad⁻¹
$(planckreduced(Kennelly))

julia> lightspeed(Kennelly) # m⋅s⁻¹
$(lightspeed(Kennelly))

julia> permeability(Kennelly) # H⋅m⁻¹
$(permeability(Kennelly))

julia> electronmass(Kennelly) # kg
$(electronmass(Kennelly))

julia> rationalization(Kennelly)
$(rationalization(Kennelly))
```
""" Kennelly

# natural unit system docs

textunits(U,S) = """
```Julia
julia> boltzmann($S)
$(boltzmann(U))

julia> planckreduced($S)
$(planckreduced(U))

julia> lightspeed($S)
$(lightspeed(U))

julia> permeability($S)
$(permeability(U))

julia> electronmass($S)
$(electronmass(U))
```
"""

@doc """
    Planck::UnitSystem{1,1,1,1,√(4π*αG)}

Planck `UnitSystem` with the `electronmass` value `√(4π*αG)` using gravitational coupling.

$(textunits(Planck,:Planck))
""" Planck

@doc """
    PlanckGauss::UnitSystem{1,1,1,4π,√αG}

Planck (Gauss) `UnitSystem` with `permeability` of `4π` and `electronmass` coupling `√αG`.

$(textunits(PlanckGauss,:PlanckGauss))

The well known `PlanckGauss` values for `length`, `time`, `mass`, and `temperature` are:
```Julia
julia> length(PlanckGauss,Metric) # ℓP
$(length(PlanckGauss,Metric))

julia> time(PlanckGauss,Metric) # tP
$(time(PlanckGauss,Metric))

julia> mass(PlanckGauss,Metric) # mP
$(mass(PlanckGauss,Metric))

julia> temperature(PlanckGauss,Metric) # TP
$(temperature(PlanckGauss,Metric))
```
""" PlanckGauss

@doc """
    Stoney::UnitSystem{1,αinv,1,4π,√(αG*αinv)}

Stoney `UnitSystem` with `permeability` of `4π` and `electronmass` coupling `√(αG*αinv)`.

$(textunits(Stoney,:Stoney))

The well known `Stoney` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(Stoney,Metric) # lS
$(length(Stoney,Metric))

julia> time(Stoney,Metric) # tS
$(time(Stoney,Metric))

julia> mass(Stoney,Metric) # mS
$(mass(Stoney,Metric))

julia> charge(Stoney,Metric) # qS
$(charge(Stoney,Metric))
```
""" Stoney

@doc """
    Hartree::UnitSystem{1,1,αinv,4π/αinv^2,1}

Hartree atomic `UnitSystem` with `lightspeed` of `αinv` and `permeability` of `4π/αinv^2`.

$(textunits(Hartree,:Hartree))

The well known `Hartree` atomic unit values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(Hartree,Metric) # lA
$(length(Hartree,Metric))

julia> time(Hartree,Metric) # tA
$(time(Hartree,Metric))

julia> mass(Hartree,Metric) # mA
$(mass(Hartree,Metric))

julia> charge(Hartree,Metric) # qA
$(charge(Hartree,Metric))
```
""" Hartree

@doc """
    Rydberg::UnitSystem{1,1,2αinv,π/αinv^2,1/2}

Rydberg `UnitSystem` with `lightspeed` of `2αinv` and `permeability` of `π/αinv^2`.

$(textunits(Rydberg,:Rydberg))
""" Rydberg

@doc """
    Schrodinger::UnitSystem{1,1,αinv,4π/αinv^2,√(αG*αinv)}

Schrodinger `UnitSystem` with `permeability` of `4π/αinv^2` and `electronmass` of `√(αG*αinv)`.

$(textunits(Schrodinger,:Schrodinger))
""" Schrodinger

@doc """
    Electronic::UnitSystem{1,αinv,1,4π,1}

Electronic `UnitSystem` with `planckreduced` of `αinv` and `permeability` of `4π`.

$(textunits(Electronic,:Electronic))
""" Electronic

@doc """
    Natural::UnitSystem{1,1,1,1,1}

Natural `UnitSystem` with all primary constants having unit value.

$(textunits(Natural,:Natural))

The well known `Natural` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(Natural,Metric)
$(length(Natural,Metric))

julia> time(Natural,Metric)
$(time(Natural,Metric))

julia> mass(Natural,Metric)
$(mass(Natural,Metric))

julia> charge(Natural,Metric)
$(charge(Natural,Metric))
```
""" Natural

@doc """
    NaturalGauss::UnitSystem{1,1,1,4π,1}

Natural (Gauss) `UnitSystem` with the Gaussian `permeability` value of `4π`.

$(textunits(NaturalGauss,:NaturalGauss))
""" NaturalGauss

@doc """
    QCD::UnitSystem{1,1,1,1,1/μₚₑ}

Qunatum chromodynamics `UnitSystem` with `electronmass` of `1/μₚₑ` or `1/$μₚₑ`.

$(textunits(QCD,:QCD))

The well known `QCD` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(QCD,Metric) # lQCD
$(length(QCD,Metric))

julia> time(QCD,Metric) # tQCD
$(time(QCD,Metric))

julia> mass(QCD,Metric) # mQCD
$(mass(QCD,Metric))

julia> charge(QCD,Metric)
$(charge(QCD,Metric))
```
""" QCD

@doc """
    QCDGauss::UnitSystem{1,1,1,4π,1/μₚₑ}

Qunatum chromodynamics (Gauss) `UnitSystem` with `electronmass` of `1/μₚₑ`.

$(textunits(QCDGauss,:QCDGauss))

The well known `QCDGauss` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(QCDGauss,Metric) # lQCD
$(length(QCDGauss,Metric))

julia> time(QCDGauss,Metric) # tQCD
$(time(QCDGauss,Metric))

julia> mass(QCDGauss,Metric) # mQCD
$(mass(QCDGauss,Metric))

julia> charge(QCDGauss,Metric)
$(charge(QCDGauss,Metric))
```
""" QCDGauss

@doc """
    QCDoriginal::UnitSystem{1,1,1,4π/αinv,1/μₚₑ}

Qunatum chromodynamics (original) `UnitSystem` with `permeability` of `4π/αinv`.

$(textunits(QCDoriginal,:QCDoriginal))

The well known `QCDoriginal` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(QCDoriginal,Metric) # lQCD
$(length(QCDoriginal,Metric))

julia> time(QCDoriginal,Metric) # tQCD
$(time(QCDoriginal,Metric))

julia> mass(QCDoriginal,Metric) # mQCD
$(mass(QCDoriginal,Metric))

julia> charge(QCDoriginal,Metric)
$(charge(QCDoriginal,Metric))
```
""" QCDoriginal


"""
`module  JAC.PhotoExcitationFluores`
    ... a submodel of JAC that contains all methods for computing photo-excitation-autoionization cross
        sections and rates.
"""
module PhotoExcitationFluores

    using Printf, ..AutoIonization, ..Basics, ..ManyElectron, ..Radial, ..PhotoEmission, ..TableStrings
    #x global JAC_counter = 0

    """
    `struct  PhotoExcitationFluores.Settings`
        ... defines a type for the details and parameters of computing photon-impact excitation-autoionization
            pathways |i(N)>  --> |m(N)>  --> |f(N-1)>.

        + multipoles              ::Array{EmMultipole,1}   ... Specifies the multipoles of the radiation field that are to be included.
        + gauges                  ::Array{UseGauge,1}      ... Specifies the gauges to be included into the computations.
        + printBeforeComputation  ::Bool                   ... True, if all energies and lines are printed before their evaluation.
        + selectPathways          ::Bool                   ... True if particular pathways are selected for the computations.
        + selectedPathways        ::Array{Tuple{Int64,Int64,Int64},1}  ... List of list of pathways, given by tupels (inital, inmediate, final).
    """
    struct Settings
        multipoles                ::Array{EmMultipole,1}
        gauges                    ::Array{UseGauge,1}
        printBeforeComputation    ::Bool
        selectPathways            ::Bool
        selectedPathways          ::Array{Tuple{Int64,Int64,Int64},1}
     end

    """
    `PhotoExcitationFluores.Settings()`
        ... constructor for the default values of photon-impact excitation-autoionizaton settings.
    """
    function Settings()
        Settings( Basics.EmMultipole[], UseGauge[], false,  false, Tuple{Int64,Int64,Int64}[])
    end

    # `Base.show(io::IO, settings::PhotoExcitationFluores.Settings)`
    # 	 ... prepares a proper printout of the variable settings::PhotoExcitationFluores.Settings.
    function Base.show(io::IO, settings::PhotoExcitationFluores.Settings)
        println(io, "multipoles:              $(settings.multipoles)  ")
        println(io, "gauges:                  $(settings.gauges)  ")
        println(io, "printBeforeComputation:  $(settings.printBeforeComputation)  ")
        println(io, "selectPathways:          $(settings.selectPathways)  ")
        println(io, "selectedPathways:        $(settings.selectedPathways)  ")
    end

    #==
    """
    `struct  PhotoExcitationFluores.Channel`
        ... defines a type for a photon-impact excitaton & autoionization channel that specifies
            all quantum numbers, phases and amplitudes.

        + excitationChannel  ::PhotoEmission.Channel       ... Channel that describes the photon-impact excitation process.
        + augerChannel       ::AutoIonization.Channel      ... Channel that describes the subsequent Auger/autoionization process.
    """
    struct  Channel
        excitationChannel    ::PhotoEmission.Channel
        augerChannel         ::AutoIonization.Channel
    end ==#

    """
    `struct  PhotoExcitationFluores.Pathway`  ... defines a type for a photon-impact excitation pathway that may include the definition
                                                      of different excitation and autoionization channels and their corresponding amplitudes.

        + initialLevel        ::Level                  ... initial-(state) level
        + intermediateLevel   ::Level                  ... intermediate-(state) level
        + finalLevel          ::Level                  ... final-(state) level
        + excitEnergy         ::Float64                ... photon excitation energy of this pathway
        + fluorEnergy         ::Float64                ... photon fluorescence energy of this pathway
        + crossSection        ::EmProperty             ... total cross section of this pathway
        + hasChannels         ::Bool                   ... Determines whether the individual excitation and fluorescence channels are defined
                                                           in terms of their multipole, gauge as well as the amplitude, or not.
        + excitChannels       ::Array{PhotoEmission.Channel,1}  ... List of excitation channels of this pathway.
        + fluorChannels       ::Array{PhotoEmission.Channel,1}  ... List of fluorescence channels of this pathway.
    """
    struct  Pathway
        initialLevel          ::Level
        intermediateLevel     ::Level
        finalLevel            ::Level
        excitEnergy           ::Float64
        fluorEnergy           ::Float64
        crossSection          ::EmProperty
        hasChannels           ::Bool
        excitChannels         ::Array{PhotoEmission.Channel,1}
        fluorChannels         ::Array{PhotoEmission.Channel,1}
    end

    """
    `PhotoExcitationFluores.Pathway()`
        ... 'empty' constructor for an photon-impact excitation-autoionization pathway between a specified initial,
            intermediate and final level.
    """
    function Pathway()
        Pathway(Level(), Level(), Level(), 0., 0., 0., false, PhotoExcitationFluores.Channel[] )
    end

    # `Base.show(io::IO, pathway::PhotoExcitationFluores.Pathway)`
    #		 ... prepares a proper printout of the variable pathway::PhotoExcitationFluores.Pathway.
    function Base.show(io::IO, pathway::PhotoExcitationFluores.Pathway)
        println(io, "initialLevel:               $(pathway.initialLevel)  ")
        println(io, "intermediateLevel:          $(pathway.intermediateLevel)  ")
        println(io, "finalLevel:                 $(pathway.finalLevel)  ")
        println(io, "excitEnergy                 $(pathway.excitEnergy)  ")
        println(io, "fluorEnergy                 $(pathway.fluorEnergy)  ")
        println(io, "crossSection:               $(pathway.crossSection)  ")
        println(io, "hasChannels:                $(pathway.hasChannels)  ")
        println(io, "excitChannels:              $(pathway.excitChannels)  ")
        println(io, "fluorChannels:              $(pathway.fluorChannels)  ")
    end

    """
    `PhotoExcitationFluores.computeAmplitudesProperties(pathway::PhotoExcitationFluores.Pathway, grid::Radial.Grid,
                                                            settings::PhotoExcitationFluores.Settings)`
        ... to compute all amplitudes and properties of the given line; a pathway::PhotoExcitationFluores.Pathway is
            returned for which the amplitudes and properties have now been evaluated.
    """
    function  computeAmplitudesProperties(pathway::PhotoExcitationFluores.Pathway, grid::Radial.Grid, settings::PhotoExcitationFluores.Settings)
        # Compute all excitation channels
        neweChannels = PhotoEmission.Channel[]
        for eChannel in pathway.excitChannels
            amplitude   = PhotoEmission.amplitude("absorption", eChannel.multipole, eChannel.gauge, pathway.excitEnergy,
                                                  pathway.intermediateLevel, pathway.initialLevel, grid)
             push!( neweChannels, PhotoEmission.Channel( eChannel.multipole, eChannel.gauge, amplitude))
        end
        # Compute all fluorescence channels
        newfChannels = PhotoEmission.Channel[]
        for fChannel in pathway.fluorChannels
            amplitude   = PhotoEmission.amplitude("emission", fChannel.multipole, fChannel.gauge, pathway.fluorEnergy,
                                                  pathway.finalLevel, pathway.intermediateLevel, grid)
            push!( newfChannels, PhotoEmission.Channel( fChannel.multipole, fChannel.gauge, amplitude))
        end
        crossSection = EmProperty(-1., -1.)
        pathway = PhotoExcitationFluores.Pathway( pathway.initialLevel, pathway.intermediateLevel, pathway.finalLevel, pathway.excitEnergy,
                                                  pathway.fluorEnergy, crossSection, true, neweChannels, newfChannels)
        return( pathway )
    end

    """
    `PhotoExcitationFluores.computePathways(finalMultiplet::Multiplet, intermediateMultiplet::Multiplet, initialMultiplet::Multiplet,
                                            grid::Radial.Grid, settings::PhotoExcitationFluores.Settings; output=true)`
        ... to compute the photo-excitation-fluorescence amplitudes and all properties as requested by the given settings. A list of
            lines::Array{PhotoExcitationFluores.Lines} is returned.
    """
    function  computePathways(finalMultiplet::Multiplet, intermediateMultiplet::Multiplet, initialMultiplet::Multiplet, grid::Radial.Grid,
                              settings::PhotoExcitationFluores.Settings; output=true)
        println("")
        printstyled("PhotoExcitationFluores.computePathways(): The computation of photo-excitation-fluorescence amplitudes starts now ... \n", color=:light_green)
        printstyled("-------------------------------------------------------------------------------------------------------------------- \n", color=:light_green)
        println("")
        pathways = PhotoExcitationFluores.determinePathways(finalMultiplet, intermediateMultiplet, initialMultiplet, settings)
        # Display all selected pathways before the computations start
        if  settings.printBeforeComputation    PhotoExcitationFluores.displayPathways(pathways)    end
        # Calculate all amplitudes and requested properties
        newPathways = PhotoExcitationFluores.Pathway[]
        ##x println("computePathways: NO-pathwayS = $(length(pathways)) ")
        for  pathway in pathways
            ##x println("computePathways: pathway = $pathway ")
            newPathway = PhotoExcitationFluores.computeAmplitudesProperties(pathway, grid, settings)
            push!( newPathways, newPathway)
        end
        # Print all results to screen
        PhotoExcitationFluores.displayResults(stdout, pathways, settings)
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        if  printSummary   PhotoExcitationFluores.displayResults(iostream, pathways, settings)     end
        #
        if    output    return( pathways )
        else            return( nothing )
        end
    end

    """
    `PhotoExcitationFluores.determinePathways(finalMultiplet::Multiplet, intermediateMultiplet::Multiplet, initialMultiplet::Multiplet,
                                              settings::PhotoExcitationFluores.Settings)`
        ... to determine a list of dielectronic-recombination pathways between the levels from the given initial-, intermediate- and
            final-state multiplets and by taking into account the particular selections and settings for this computation;
            an Array{PhotoExcitationFluores.Pathway,1} is returned. Apart from the level specification, all physical properties are
            set to zero during the initialization process.
    """
    function  determinePathways(finalMultiplet::Multiplet, intermediateMultiplet::Multiplet, initialMultiplet::Multiplet,
                                settings::PhotoExcitationFluores.Settings)
        if    settings.selectPathways    selectPathways = true;
              selectedPathways = Basics.determineSelectedPathways(settings.selectedPathways, initialMultiplet, intermediateMultiplet, finalMultiplet)
        else                             selectPathways = false
        end

        pathways = PhotoExcitationFluores.Pathway[]
        for  i = 1:length(initialMultiplet.levels)
            for  n = 1:length(intermediateMultiplet.levels)
                for  f = 1:length(finalMultiplet.levels)
                    if  selectPathways  &&  !((i,n,f) in selectedPathways )    continue   end
                    eEnergy = intermediateMultiplet.levels[n].energy - initialMultiplet.levels[i].energy
                    fEnergy = intermediateMultiplet.levels[n].energy - finalMultiplet.levels[f].energy
                    if  eEnergy < 0.   ||   fEnergy < 0.    continue    end

                    rSettings = PhotoEmission.Settings( settings.multipoles, settings.gauges, false, false, false, Tuple{Int64,Int64}[], 0., 0., 0.)
                    eChannels = PhotoEmission.determineChannels(intermediateMultiplet.levels[n], initialMultiplet.levels[i], rSettings)
                    fChannels = PhotoEmission.determineChannels(finalMultiplet.levels[f], intermediateMultiplet.levels[n], rSettings)
                    push!( pathways, PhotoExcitationFluores.Pathway(initialMultiplet.levels[i], intermediateMultiplet.levels[i], finalMultiplet.levels[f],
                                                                    eEnergy, fEnergy, EmProperty(0., 0.), true, eChannels, fChannels) )
                end
            end
        end
        return( pathways )
    end

    """
    `PhotoExcitationFluores.displayResults(stream::IO, pathways::Array{PhotoExcitationFluores.Pathway,1},
                                           settings::PhotoExcitationFluores.Settings)`
        ... to list all results, energies, cross sections, etc. of the selected lines. A neat table is printed but nothing
            is returned otherwise.
    """
    function  displayResults(stream::IO, pathways::Array{PhotoExcitationFluores.Pathway,1}, settings::PhotoExcitationFluores.Settings)
        println(stream, " ")
        println(stream, "  Partial excitation & fluorescence cross sections:")
        println(stream, " ")
        println(stream, "  ", TableStrings.hLine(150))
        sa = "    ";   sb = "    "
        sa = sa * TableStrings.center(23, "Levels"; na=2);            sb = sb * TableStrings.center(23, "i  --  e  --  f"; na=2);
        sa = sa * TableStrings.center(23, "J^P symmetries"; na=0);    sb = sb * TableStrings.center(23, "i  --  e  --  f"; na=2);
        sa = sa * TableStrings.center(30, "Energies  " * TableStrings.inUnits("energy"); na=4);
        sb = sb * TableStrings.center(30, "r--i        r--f  "; na=4)
        sa = sa * TableStrings.center(30, "Multipoles"; na=4);
        sb = sb * TableStrings.center(38, "i--r        r--f  "; na=4)
        sa = sa * TableStrings.center(36, "Cou -- cross sections -- Bab";        na=2)
        sb = sb * TableStrings.center(36, TableStrings.inUnits("cross section") * "          " *
                                              TableStrings.inUnits("cross section"); na=3)
        println(stream, sa);    println(stream, sb);    println(stream, "  ", TableStrings.hLine(150))
        #
        for  pathway in pathways
            sa  = " ";     isym = LevelSymmetry( pathway.initialLevel.J,      pathway.initialLevel.parity)
                           msym = LevelSymmetry( pathway.intermediateLevel.J, pathway.intermediateLevel.parity)
                           fsym = LevelSymmetry( pathway.finalLevel.J,        pathway.finalLevel.parity)
            sa = sa * TableStrings.center(23, TableStrings.levels_imf(pathway.initialLevel.index, pathway.intermediateLevel.index,
                                                                              pathway.finalLevel.index); na=3)
            sa = sa * TableStrings.center(23, TableStrings.symmetries_imf(isym, msym, fsym);  na=4)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", pathway.excitEnergy))      * "   "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("energy: from atomic", pathway.fluorEnergy))      * "   "
            #
            multipoles = EmMultipole[]
            for  ech in pathway.excitChannels
                multipoles = push!( multipoles, ech.multipole)
            end
            multipoles = unique(multipoles);   mpString = TableStrings.multipoleList(multipoles)            * "                   "
            sa = sa * TableStrings.flushleft(16, mpString[1:16];  na=2)
            #
            multipoles = EmMultipole[]
            for  fch in pathway.fluorChannels
                multipoles = push!( multipoles, fch.multipole)
            end
            multipoles = unique(multipoles);   mpString = TableStrings.multipoleList(multipoles)             * "                   "
            sa = sa * TableStrings.flushleft(16, mpString[1:16];  na=2)
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("cross section: from atomic", pathway.crossSection.Coulomb))     * "  "
            sa = sa * @sprintf("%.4e", Defaults.convertUnits("cross section: from atomic", pathway.crossSection.Babushkin))   * "  "
            println(stream, sa)
        end
        println(stream, "  ", TableStrings.hLine(150))
        #
        return( nothing )
    end

end # module


using ContinuousPOMDPTreeSearchExperiments
using ParticleFilters
using ARDESPOT
using BasicPOMCP
using POMCPOW
using POMDPs
using DiscreteValueIteration
using QMDP
using MCTS
using VDPTag2
using POMDPToolbox
using DataFrames
using Plots
using StatPlots
using Missings

@everywhere using POMDPToolbox
@everywhere using Missings

file_contents = readstring(@__FILE__())

# pomdp = VDPTagPOMDP(mdp=VDPTagMDP(agent_speed=2.0#=, dt=0.01=#))
pomdp = VDPTagPOMDP()
dpomdp = AODiscreteVDPTagPOMDP(pomdp, 30, 0.5)

@show max_time = 1.0
@show max_depth = 10

solvers = Dict{String, Union{Solver,Policy}}(
    "to_next" => ToNextML(mdp(pomdp)),
    "manage_uncertainty" => ManageUncertainty(pomdp, 0.01),

    "pomcpow" => begin
        rng = MersenneTwister(13)
        ro = ToNextMLSolver(rng)
        ro = RandomSolver(rng)
        solver = POMCPOWSolver(tree_queries=10_000_000,
                               criterion=MaxUCB(90.0),
                               final_criterion=MaxQ(),
                               max_depth=max_depth,
                               max_time=max_time,
                               k_action=20.0,
                               alpha_action=1/20,
                               k_observation=6.0,
                               alpha_observation=1/55,
                               estimate_value=FORollout(ro),
                               next_action=RootToNextMLFirst(rng),
                               check_repeat_obs=false,
                               check_repeat_act=false,
                               default_action=ReportWhenUsed(TagAction(false, 0.0)),
                               rng=rng
                              )
    end,

    "pft" => begin
        rng = MersenneTwister(13)
        m = 15
        node_updater = ObsAdaptiveParticleFilter(deepcopy(pomdp),
                                           LowVarianceResampler(m),
                                           0.05, rng)
        ro = ToNextML(mdp(pomdp), rng)
        ro = RandomSolver(rng)
        ev = SampleRollout(solve(ro, pomdp), rng)
        solver = DPWSolver(n_iterations=typemax(Int),
                           exploration_constant=90.0,
                           depth=max_depth,
                           max_time=max_time,
                           k_action = 20.0,
                           alpha_action = 1/20,
                           k_state = 6.0,
                           alpha_state = 1/55,
                           check_repeat_state=false,
                           check_repeat_action=false,
                           estimate_value=ev,
                           next_action=RootToNextMLFirst(rng),
                           default_action=ReportWhenUsed(TagAction(false, 0.0)),
                           rng=rng
                          )
        belief_mdp = GenerativeBeliefMDP(deepcopy(pomdp), node_updater)
        solve(solver, belief_mdp)
    end,
)

@show N=1000

alldata = DataFrame()

# for (k, solver) in solvers
test = ["pft", "pomcpow"]
# test = ["to_next", "manage_uncertainty"]
for (k, solver) in [(s, solvers[s]) for s in test]
    @show k
    if isa(solver, Solver)
        planner = solve(solver, pomdp)
    else
        planner = solver
    end
    sims = []
    for i in 1:N
        srand(planner, i+50_000)
        filter = SIRParticleFilter(deepcopy(pomdp), 100_000, rng=MersenneTwister(i+90_000))

        md = Dict(:solver=>k, :i=>i)
        hr = HistoryRecorder(max_steps=100, rng=MersenneTwister(i+70_000), capture_exception=true)
        sim = Sim(deepcopy(pomdp),
                  planner,
                  filter,
                  simulator=hr,
                  metadata=md,
                 )

        push!(sims, sim)
    end

    data = run_parallel(sims) do sim, hist
        if isnull(hist.exception)
            tq = [get(info, :tree_queries, missing) for info in eachstep(hist,"ai")]
            if isempty(tq)
                println("Empty history?")
                @show n_steps(hist)
                iters = missing
            else
                iters = mean(tq)
            end
            return [:steps=>n_steps(hist),
                    :reward=>discounted_reward(hist),
                    :iterations=>iters
                   ]
        else
            println("Simulator Caught a $(typeof(get(hist.exception)))")
            return [:exception_type => string(typeof(get(hist.exception)))]
        end
    end
    # data = run(sims)

    rs = data[:reward]
    println(@sprintf("reward: %6.3f ± %6.3f", mean(rs), std(rs)/sqrt(length(rs))))
    is = data[:iterations]
    println(@sprintf("iterations: mean: %6.3f", mean(is)))

    if isempty(alldata)
        alldata = data
    else
        alldata = vcat(alldata, data)
    end
end

# p = plot()
# for data in groupby(alldata, :solver)
#     histogram!(p, data[:steps], label=first(data[:solver]))
# end
#
# gui(p)

#=
datestring = Dates.format(now(), "E_d_u_HH_MM")
copyname = Pkg.dir("ContinuousPOMDPTreeSearchExperiments", "icaps_2018", "data", "subhunt_table_$(datestring).jl")
write(copyname, file_contents)
filename = Pkg.dir("ContinuousPOMDPTreeSearchExperiments", "icaps_2018", "data", "subhunt_$(datestring).csv")
println("saving to $filename...")
writetable(filename, alldata)
println("done.")
=#


function vertexbuffer(x, trans)
    pos = decompose(Point, x)
    return apply_transform(trans,  pos)
end

function vertexbuffer(x::Observable, p)
    return Buffer(lift(vertexbuffer, x, transform_func_obs(p)))
end

facebuffer(x) = facebuffer(GeometryBasics.faces(x))
facebuffer(x::Observable) = Buffer(lift(facebuffer, x))
function facebuffer(x::AbstractArray{GLTriangleFace})
    return x
end

function array2color(colors, cmap, crange)
    cmap = RGBAf0.(Colors.color.(to_colormap(cmap)), 1.0)
    return AbstractPlotting.interpolated_getindex.((cmap,), colors, (crange,))
end

function array2color(colors::AbstractArray{<:Colorant}, cmap, crange)
    return RGBAf0.(colors)
end

function converted_attribute(plot::AbstractPlot, key::Symbol)
    return lift(plot[key]) do value
        return convert_attribute(value, Key{key}(), Key{plotkey(plot)}())
    end
end

function create_shader(scene::Scene, plot::AbstractPlotting.Mesh)
    # Potentially per instance attributes
    mesh_signal = plot[1]
    mattributes = GeometryBasics.attributes
    get_attribute(mesh, key) = lift(x -> getproperty(x, key), mesh)
    data = mattributes(mesh_signal[])

    uniforms = Dict{Symbol,Any}()
    attributes = Dict{Symbol,Any}()

    for (key, default) in (:uv => Vec2f0(0), :normals => Vec3f0(0))
        if haskey(data, key)
            attributes[key] = Buffer(get_attribute(mesh_signal, key))
        else
            uniforms[key] = Observable(default)
        end
    end

    if haskey(data, :attributes) && data[:attributes] isa AbstractVector
        attr = get_attribute(mesh_signal, :attributes)
        attr_id = get_attribute(mesh_signal, :attribute_id)
        color = lift((c, id) -> c[Int.(id) .+ 1], attr_id)
        attributes[:color] = Buffer(color)
        uniforms[:uniform_color] = false
    else
        color_signal = converted_attribute(plot, :color)
        color = color_signal[]
        mesh_color = color_signal[]
        uniforms[:uniform_color] = Observable(false) # this is the default

        if color isa Colorant && haskey(data, :color)
            color_signal = get_attribute(mesh_signal, :color)
            color = color_signal[]
        end

        if color isa AbstractArray
            c_converted = if color isa AbstractArray{<:Colorant}
                color_signal
            elseif color isa AbstractArray{<:Number}
                lift(array2color, color_signal, plot.colormap, plot.colorrange)
            else
                error("Unsupported color type: $(typeof(color))")
            end
            if c_converted[] isa AbstractVector
                attributes[:color] = Buffer(c_converted) # per vertex colors
            else
                uniforms[:uniform_color] = Sampler(c_converted) # Texture
                !haskey(attributes, :uv) &&
                    @warn "Mesh doesn't use Texturecoordinates, but has a Texture. Colors won't map"
            end
        elseif color isa Colorant && !haskey(attributes, :color)
            uniforms[:uniform_color] = color_signal
        else
            error("Unsupported color type: $(typeof(color))")
        end
    end

    if !haskey(attributes, :color)
        uniforms[:color] = Vec4f0(0) # make sure we have a color attribute
    end

    uniforms[:shading] = plot.shading

    for key in (:ambient, :diffuse, :specular, :shininess)
        uniforms[key] = plot[key]
    end

    faces = facebuffer(mesh_signal)
    positions = vertexbuffer(mesh_signal, plot)
    instance = GeometryBasics.Mesh(GeometryBasics.meta(positions; attributes...), faces)

    get!(uniforms, :colorrange, true)
    get!(uniforms, :colormap, true)
    get!(uniforms, :model, plot.model)
    get!(uniforms, :lightposition, Vec3f0(1))

    uniforms[:normalmatrix] = map(scene.camera.view, plot.model) do v, m
        i = SOneTo(3)
        return transpose(inv(v[i, i] * m[i, i]))
    end

    return Program(WebGL(), lasset("mesh.vert"), lasset("mesh.frag"), instance; uniforms...)
end


export RainbowLearner

"""
    RainbowLearner(;kwargs...)

See paper: [Rainbow: Combining Improvements in Deep Reinforcement Learning](https://arxiv.org/abs/1710.02298)

# Keywords

- `approximator`::[`AbstractApproximator`](@ref): used to get Q-values of a state.
- `target_approximator`::[`AbstractApproximator`](@ref): similar to `approximator`, but used to estimate the target (the next state).
- `loss_func`: the loss function.
- `Vₘₐₓ::Float32`: the maximum value of distribution.
- `Vₘᵢₙ::Float32`: the minimum value of distribution.
- `n_actions::Int`: number of possible actions.
- `γ::Float32=0.99f0`: discount rate.
- `batch_size::Int=32`
- `update_horizon::Int=1`: length of update ('n' in n-step update).
- `min_replay_history::Int=32`: number of transitions that should be experienced before updating the `approximator`.
- `update_freq::Int=4`: the frequency of updating the `approximator`.
- `target_update_freq::Int=500`: the frequency of syncing `target_approximator`.
- `stack_size::Union{Int, Nothing}=4`: use the recent `stack_size` frames to form a stacked state.
- `default_priority::Float32=1.0f2.`: the default priority for newly added transitions. It must be `>= 1`.
- `n_atoms::Int=51`: the number of buckets of the value function distribution.
- `stack_size::Union{Int, Nothing}=4`: use the recent `stack_size` frames to form a stacked state.
- `rng = Random.GLOBAL_RNG`
"""
mutable struct RainbowLearner{
    Tq<:AbstractApproximator,
    Tt<:AbstractApproximator,
    Tf,
    Ts,
    R<:AbstractRNG,
} <: AbstractLearner
    approximator::Tq
    target_approximator::Tt
    loss_func::Tf
    sampler::NStepBatchSampler
    Vₘₐₓ::Float32
    Vₘᵢₙ::Float32
    n_actions::Int
    n_atoms::Int
    support::Ts
    delta_z::Float32
    min_replay_history::Int
    update_freq::Int
    target_update_freq::Int
    update_step::Int
    default_priority::Float32
    β_priority::Float32
    rng::R
    loss::Float32
end

Flux.functor(x::RainbowLearner) =
    (Q = x.approximator, Qₜ = x.target_approximator, S = x.support),
    y -> begin
        x = @set x.approximator = y.Q
        x = @set x.target_approximator = y.Qₜ
        x = @set x.support = y.S
        x
    end

function RainbowLearner(;
    approximator,
    target_approximator,
    loss_func,
    Vₘₐₓ,
    Vₘᵢₙ,
    n_actions,
    n_atoms = 51,
    support = collect(range(Float32(-Vₘₐₓ), Float32(Vₘₐₓ), length = n_atoms)),
    stack_size = 4,
    delta_z = Float32(support[2] - support[1]),
    γ = 0.99,
    batch_size = 32,
    update_horizon = 1,
    min_replay_history = 32,
    update_freq = 1,
    target_update_freq = 500,
    update_step = 0,
    default_priority = 1.0f2,
    β_priority = 0.5f0,
    traces = SARTS,
    rng = Random.GLOBAL_RNG,
)
    default_priority >= 1.0f0 || error("default value must be >= 1.0f0")
    copyto!(approximator, target_approximator)  # force sync
    support = send_to_device(device(approximator), support)
    sampler = NStepBatchSampler{traces}(;
        γ = γ,
        n = update_horizon,
        stack_size = stack_size,
        batch_size = batch_size,
    )
    RainbowLearner(
        approximator,
        target_approximator,
        loss_func,
        sampler,
        Vₘₐₓ,
        Vₘᵢₙ,
        n_actions,
        n_atoms,
        support,
        delta_z,
        min_replay_history,
        update_freq,
        target_update_freq,
        update_step,
        default_priority,
        β_priority,
        rng,
        0.0f0,
    )
end

function (learner::RainbowLearner)(env)
    s = send_to_device(device(learner.approximator), state(env))
    s = Flux.unsqueeze(s, ndims(s) + 1)
    logits = learner.approximator(s)
    q = learner.support .* softmax(reshape(logits, :, learner.n_actions))
    vec(sum(q, dims = 1)) |> send_to_host
end

function RLBase.update!(learner::RainbowLearner, batch::NamedTuple)
    Q = learner.approximator
    Qₜ = learner.target_approximator
    γ = learner.sampler.γ
    β = learner.β_priority
    loss_func = learner.loss_func
    n_atoms = learner.n_atoms
    n_actions = learner.n_actions
    support = learner.support
    delta_z = learner.delta_z
    update_horizon = learner.sampler.n
    batch_size = learner.sampler.batch_size
    D = device(Q)
    states = send_to_device(D, batch.state)
    rewards = send_to_device(D, batch.reward)
    terminals = send_to_device(D, batch.terminal)
    next_states = send_to_device(D, batch.next_state)

    actions = CartesianIndex.(batch.action, 1:batch_size)

    target_support =
        reshape(rewards, 1, :) .+
        (reshape(support, :, 1) * reshape((γ^update_horizon) .* (1 .- terminals), 1, :))

    next_logits = Qₜ(next_states)
    next_probs = reshape(softmax(reshape(next_logits, n_atoms, :)), n_atoms, n_actions, :)
    next_q = reshape(sum(support .* next_probs, dims = 1), n_actions, :)
    if haskey(batch, :next_legal_actions_mask)
        l′ = send_to_device(D, batch[:next_legal_actions_mask])
        next_q .+= ifelse.(l′, 0.0f0, typemin(Float32))
    end
    next_prob_select = select_best_probs(next_probs, next_q)

    target_distribution = project_distribution(
        target_support,
        next_prob_select,
        support,
        delta_z,
        learner.Vₘᵢₙ,
        learner.Vₘₐₓ,
    )

    is_use_PER = haskey(batch, :priority)  # is use Prioritized Experience Replay
    if is_use_PER
        updated_priorities = Vector{Float32}(undef, batch_size)
        weights = 1.0f0 ./ ((batch.priority .+ 1f-10) .^ β)
        weights ./= maximum(weights)
        weights = send_to_device(D, weights)
    end

    gs = gradient(Flux.params(Q)) do
        logits = reshape(Q(states), n_atoms, n_actions, :)
        select_logits = logits[:, actions]
        batch_losses = loss_func(select_logits, target_distribution)
        loss =
            is_use_PER ? dot(vec(weights), vec(batch_losses)) * 1 // batch_size :
            mean(batch_losses)
        ignore() do
            if is_use_PER
                updated_priorities .= send_to_host(vec((batch_losses .+ 1f-10) .^ β))
            end
            learner.loss = loss
        end
        loss
    end

    update!(Q, gs)

    is_use_PER ? updated_priorities : nothing
end

@inline function select_best_probs(probs, q)
    q_argmax = argmax(q, dims = 1)
    prob_select = @inbounds probs[:, q_argmax] # !!! without @inbounds it would be really slow
    reshape(prob_select, :, length(q_argmax))
end

function project_distribution(supports, weights, target_support, delta_z, vmin, vmax)
    batch_size, n_atoms = size(supports, 2), length(target_support)
    clampped_support = clamp.(supports, vmin, vmax)
    tiled_support = reshape(
        repeat(clampped_support; outer = (n_atoms, 1)),
        n_atoms,
        n_atoms,
        batch_size,
    )

    projection =
        clamp.(
            1 .- abs.(tiled_support .- reshape(target_support, 1, :)) ./ delta_z,
            0,
            1,
        ) .* reshape(weights, n_atoms, 1, batch_size)
    reshape(sum(projection, dims = 1), n_atoms, batch_size)
end


module PowerSimulations

#################################################################################
# Exports

#Network Relevant Exports
export StandardPTDFForm
export CopperPlatePowerModel

#operation_models
export UnitCommitment
export EconomicDispatch
export SCEconomicDispatch
export OptimalPowerFlow

#functions
export solve_op_model!

#################################################################################
# Imports
import JuMP
import ParameterJuMP
#using TimeSeries
import PowerSystems
import PowerModels
import MathOptInterface
import DataFrames
import LinearAlgebra
#import LinearAlgebra.BLAS #needed for the simulation stage
import AxisArrays
import Dates

#################################################################################
# Type Alias From other Packages
const PM = PowerModels
const PSY = PowerSystems
const PSI = PowerSimulations
const MOI = MathOptInterface
const MOIU = MathOptInterface.Utilities
const PJ = ParameterJuMP

#Type Alias for JuMP and PJ containers
const JuMPExpressionMatrix = Matrix{<:JuMP.AbstractJuMPScalar}
const PGAE{V} = PJ.ParametrizedGenericAffExpr{Float64,V} where V <: JuMP.AbstractVariableRef
const GAE{V} = JuMP.GenericAffExpr{Float64,V} where V <: JuMP.AbstractVariableRef
const JuMPAffineExpressionArray = Matrix{GAE{V}} where V <: JuMP.AbstractVariableRef

#Type Alias for Unions
const FixResource = Union{PSY.RenewableFix, PSY.HydroFix}

#Type Alias for long type signatures
const MinMax = NamedTuple{(:min, :max),Tuple{Float64,Float64}}
const NamedMinMax = Tuple{String, MinMax}
const UpDown = NamedTuple{(:up, :down),Tuple{Float64,Float64}}

#################################################################################
# Includes

#Abstract Models
include("network/network_models/networks.jl")
include("services/service_models/services.jl")

#Core Models and constructors
include("core/core_structs/device_model.jl")
include("core/core_structs/canonical_model.jl")
include("core/core_structs/service_model.jl")
include("core/core_structs/operation_model.jl")
include("core/device_constructor.jl")
include("core/canonical_constructor.jl")
include("core/operations_constructor.jl")
include("core/core_structs/results_model.jl")
#include("core/core_structs/simulation_model.jl")

#Device Modeling components
include("devices/device_models/common.jl")
include("devices/device_models/renewable_generation.jl")
include("devices/device_models/thermal_generation.jl")
include("devices/device_models/electric_loads.jl")
include("devices/device_models/branches.jl")
include("devices/device_models/storage.jl")
include("devices/device_models/hydro_generation.jl")

#Network models
include("network/network_models/copperplate_model.jl")
include("network/network_models/powermodels_interface.jl")
include("network/network_models/ptdf_model.jl")

#Device constructors
include("devices/device_constructors/device_constructors.jl")

#Network constructors
include("network/network_constructor.jl")

#Services Models
#include("service_models/reserves.jl")

#Services constructors
include("services/services_constructor.jl")

#Operational Model Constructors
include("operation_models/operation_models.jl")

#Utils
include("routines/solve_routines.jl")
include("routines/get_ini_cond.jl")
#include("routines/simulation_routines.jl")
#include("routines/device_retreval.jl")

#################################################################################
##### JuMP methods overloading
JuMP.Model(optimizer::Nothing; kwargs...) = JuMP.Model(kwargs...)

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

end


### Methods ###

function PVLVModel(config::Dict)
  numLanes = get(config, "numLanes", 4)
  szUS = get(config, "szUS", 8)
  szCS = get(config, "szCS", 8)
  szDA = get(config, "szDA", 8)
  szPV = get(config, "szPV", 8)
  szLV = get(config, "szLV", 8)

  agent = Agent()

  # TODO: Pull out US and DA
  for i = 1:numLanes
    agent_Lane = Agent()

    US = GenericNeurons(szUS)
    lUS = addnode!(agent_Lane, US; name="US")
    CS = GenericNeurons(szCS)
    lCS = addnode!(agent_Lane, CS; name="CS")
    DA = GenericNeurons(szDA) # TODO: Use profile with spontaneous activity
    lDA = addnode!(agent_Lane, DA; name="DA")
    PVE = GenericNeurons(szPV)
    lPVE = addnode!(agent_Lane, PVE; name="PVE")
    PVI = GenericNeurons(szPV)
    lPVI = addnode!(agent_Lane, PVI; name="PVI")
    LVE = GenericNeurons(szLV)
    lLVE = addnode!(agent_Lane, LVE; name="LVE")
    LVI = GenericNeurons(szLV)
    lLVI = addnode!(agent_Lane, LVI; name="LVI")

    # TODO: Add RL connections
    # FIXME: MNE
    addnode!(agent_Lane, GenericSynapses(szUS, szPV); name="US_PVE")
    addedge!(agent_Lane, agent_Lane["US_PVE"], lUS, lPVE)
    addnode!(agent_Lane, GenericSynapses(szUS, szPV); name="US_PVI")
    addedge!(agent_Lane, agent_Lane["US_PVE"], lUS, lPVI)
    addnode!(agent_Lane, GenericSynapses(szUS, szLV); name="US_LVE")
    addedge!(agent_Lane, agent_Lane["US_PVE"], lUS, lLVE)
    addnode!(agent_Lane, GenericSynapses(szCS, szLV); name="CS_LVE")
    addedge!(agent_Lane, agent_Lane["CS_LVE"], lCS, lLVE)
    addnode!(agent_Lane, GenericSynapses(szCS, szLV); name="CS_LVI")
    addedge!(agent_Lane, agent_Lane["CS_LVI"], lCS, lLVI)
    addnode!(agent_Lane, GenericSynapses(szPV, szDA); name="PVE_DA")
    addedge!(agent_Lane, agent_Lane["PVE_DA"], lPVE, lDA)
    addnode!(agent_Lane, GenericSynapses(szPV, szDA; outputMask = -1); name="PVI_DA")
    addedge!(agent_Lane, agent_Lane[], l, l)
    addnode!(agent_Lane, GenericSynapses(szLV, szDA); name="LVE_DA")
    addedge!(agent_Lane, agent_Lane[], l, l)
    addnode!(agent_Lane, GenericSynapses(szLV, szDA; outputMask = -1); name="LVI_DA")
    addedge!(agent_Lane, agent_Lane[], l, l)

    agent["$i"] = agent_Lane
  end

  return agent
end