################################################################################
# This set of functions prepares OpenMendel to evaluate the likelihood
# of a collection of pedigrees by the Elston-Stewart algorithm.
################################################################################
#
# Required OpenMendel packages and modules.
#
# using DataStructures
# using GeneralUtilities
# using GeneticUtilities
# using ReadData

mutable struct Instruction
  instructions :: Int
  instructstart :: Vector{Int}
  instructfinish :: Vector{Int}
  operation :: Vector{Int}
  site :: Vector{Int}
  rankarray :: Vector{Int}
  space :: Vector{Int}
  extra :: Array{Vector{Int}, 1}
end # mutable struct Instruction

export Instruction
export allele_consolidation, genotype_elimination!
export orchestrate_likelihood, model_recombination_fractions

penetrance_and_prior_array = 1
transmission_array         = 2
pure_add                   = 3
multiply_and_add           = 4
pure_multiply              = 5
quit_processing_pedigree   = 6

export penetrance_and_prior_array, transmission_array, pure_add
export multiply_and_add, pure_multiply, quit_processing_pedigree

"""
Orchestrate allele lumping, genotype consolidation,
and creation of the Elston-Stewart instructions.
"""
function orchestrate_likelihood(pedigree::Pedigree, person::Person,
  nuclear_family::NuclearFamily, locus::Locus,
  keyword::Dict{AbstractString, Any})

  pedigrees = pedigree.pedigrees
  populations = person.populations
  model_loci = locus.model_loci
  eliminate_genotypes = keyword["eliminate_genotypes"]
  lump_alleles = keyword["lump_alleles"]
  product_mode = keyword["product_mode"]
  complexity_threshold = keyword["complexity_threshold"]
  #
  # Define the frequency of the lumped allele at each model locus.
  #
##
#   for i = 1:person.people
#     for l = 1:locus.loci
#       println(person.name[i],"  ",locus.name[l],"  ",person.genotype[i, l])
#     end
#   end
  if lump_alleles
    lumped_frequency = zeros(pedigrees, populations, model_loci)
    for l = 1:model_loci
      loc = locus.model_locus[l]
##
      if loc != locus.mutable_locus
##println(" Allele lumping at locus = ",locus.name[loc])
        lumped_frequency[:, :, l] =
          allele_consolidation(pedigree, person, locus, loc)
      end
    end
  else
    lumped_frequency = zeros(0, 0, 0)
  end
  #
  # Perform genotype elimination over the model loci.
  #
  if eliminate_genotypes
    for l = 1:model_loci
      loc = locus.model_locus[l]
##
      if loc != locus.mutable_locus
##println(" Genotype elimination at locus = ",locus.name[loc])
        genotype_elimination!(pedigree, person, nuclear_family, locus, loc)
      end
    end
  end
  #
  # Find an upper bound on the number of instructions.
  #
  bound = person.people
  if product_mode
    bound = bound * model_loci
  end
  bound = 6 * bound + pedigrees
  #
  # Allocate the components of the instruction vector and the first
  # and last instruction associated with each pedigree.
  #
  elston_stewart_count = zeros(pedigrees)
  instruction_start = zeros(Int, pedigrees)
  instruction_finish = zeros(Int, pedigrees)
  operation = zeros(Int, bound)
  site = zeros(Int, bound)
  rankarray = zeros(Int, bound)
  space = zeros(Int, bound)
  extra = Array{Array{Int, 1}}(undef, bound)
  for i = 1:bound
    extra[i] = zeros(Int, 0)
  end
  #
  # Find a good summation sequence.
  #
  instructions = prepare_elston_stewart(pedigree, person, locus,
    elston_stewart_count, instruction_start, instruction_finish, operation,
    site, rankarray, space, extra, bound, product_mode, complexity_threshold)
  #
  # Return the instructions for the Elston-Stewart algorithm.
  #
  instruction = Instruction(instructions, instruction_start,
    instruction_finish, operation, site, rankarray, space, extra)
  locus.lumped_frequency = lumped_frequency
  return (instruction, elston_stewart_count)
end  # function orchestrate_likelihood

"""
Consolidate alleles in each pedigree at locus number loc.
The lumped allele is numbered alleles + 1,
where alleles counts the number of alleles at the locus.
The frequency of the lumped allele and the lumping status
of each pedigree is returned along with the altered
genotype set for each person.
Warning: Allele consolidation should precede genotype elimination.
"""
function allele_consolidation(pedigree::Pedigree, person::Person,
  locus::Locus, loc::Int)
  #
  # Initialize variables.
  #
  xlinked = locus.xlinked[loc]
  pedigrees = pedigree.pedigrees
  (populations, alleles) = size(locus.frequency[loc])
  maximum_genotypes = alleles^2
  lumped_allele = alleles + 1
  lumped = falses(pedigrees)
  lumped_frequency = ones(pedigrees, populations)
  #
  # Find the alleles observed or partially observed in
  # the current pedigree.
  #
  for ped = 1:pedigrees
    observed = BitSet()
    for i = pedigree.pedstart[ped]:pedigree.twin_finish[ped]
      if xlinked && person.male[i]
        if length(person.genotype[i, loc]) < alleles
          for (gm, gp) in person.genotype[i, loc]
            push!(observed, gm)
          end
        end
      else
        if length(person.genotype[i, loc]) < maximum_genotypes
          for (gm, gp) in person.genotype[i, loc]
            push!(observed, gm)
            push!(observed, gp)
          end
        end
      end
    end
    #
    # Find the frequency of the lumped allele.
    #
    if length(observed) < alleles - 1
      lumped[ped] = true
      for a in observed
        f = locus.frequency[loc][:, a]
        lumped_frequency[ped, :] = lumped_frequency[ped, :] - f
      end
    else
      lumped_frequency[ped, :] .= 0.0
    end
    #
    # Construct a new genotype set for each person, replacing
    # each unobserved allele by the lumped allele.
    #
    for i = pedigree.pedstart[ped]:pedigree.twin_finish[ped]
      new_genotype_set = Set{Tuple{Int, Int}}()
      for (gm, gp) in person.genotype[i, loc]
        if in(gm, observed)
          hm = gm
        else
          hm = lumped_allele
        end
        if in(gp, observed)
          hp = gp
        else
          hp = lumped_allele
        end
        push!(new_genotype_set, (hm, hp))
      end
      person.genotype[i, loc] = deepcopy(new_genotype_set)
    end
  end
  #
  # Recount the number of homozygous genotypes per person and return
  # the lumped genotypes and lumped allele frequencies.
  #
  count_homozygotes!(person, loc)
  return lumped_frequency
end # function allele_consolidation

"""
Perform genotype elimination at locus number loc.
Genotypes for person i are stored in the set person.genotype[i, loc].
"""
function genotype_elimination!(pedigree::Pedigree, person::Person,
  nuclear_family::NuclearFamily, locus::Locus, loc::Int)

  people = person.people
  pedigrees = pedigree.pedigrees
  families = nuclear_family.families
  xlinked = locus.xlinked[loc]
  stable_pedigree = falses(pedigrees)
  stable_family = falses(families)
  saved_genotype = Array{Set{Tuple{Int, Int}}}(undef, people)
  #
  # Begin elimination for locus loc.
  #
  progress = true
  while progress
  #
  # Loop over all nuclear families.
  #
    for fam = 1:families
      stable_family[fam] = true
      ped = nuclear_family.pedigree[fam]
      if stable_pedigree[ped]; continue; end
      #
      # Initialize the saved sets of genotypes to be empty.
      #
      mom = nuclear_family.mother[fam]
      dad = nuclear_family.father[fam]
      saved_genotype[mom] = Set{Tuple{Int, Int}}()
      saved_genotype[dad] = Set{Tuple{Int, Int}}()
      for sib in nuclear_family.sib[fam]
        saved_genotype[sib] = Set{Tuple{Int, Int}}()
      end
      #
      # Loop over all combinations of maternal and paternal genotypes.
      #
      for (m1, m2) in person.genotype[mom, loc]
        for (d1, d2) in person.genotype[dad, loc]
          family_match = true
          #
          # Loop over all siblings.
          #
          for sib in nuclear_family.sib[fam]
            #
            # Loop over all genotypes of the current sib.
            #
            sib_match = false
            for (s1, s2) in person.genotype[sib, loc]
              #
              # Check for compatibility between the sib genotype and
              # the parental genotypes.
              #
              maternal_match = (s1 == m1 || s1 == m2)
              if !xlinked || !person.male[sib]
                paternal_match = (s2 == d1 || s2 == d2)
                sib_match = maternal_match && paternal_match
              else
                sib_match = maternal_match
              end
              #
              # As soon as a compatible sib genotype is found, stop looking.
              #
              if sib_match; break; end
            end  # sib genotype loop
            #
            # If no genotype of the current sib matches, exit.
            #
            family_match = family_match && sib_match
            if !family_match; break; end
          end  # sib loop
          #
          # If all sibs provide a match, save all matching parental and
          # sibling genotypes.
          #
          if !family_match; continue; end
          push!(saved_genotype[mom], (m1, m2))
          push!(saved_genotype[dad], (d1, d2))
          for sib in nuclear_family.sib[fam]
            for (s1, s2) in person.genotype[sib, loc]
              maternal_match = (s1 == m1 || s1 == m2)
              if !xlinked || !person.male[sib]
                paternal_match = (s2 == d1 || s2 == d2)
              else
                paternal_match = true
              end
              if maternal_match && paternal_match
                push!(saved_genotype[sib], (s1, s2))
              end
            end
          end  # sib loop
        end  # dad loop
      end  # mom loop
      #
      # Replace the current genotype sets in the family by the saved
      # genotype sets.
      #
      if length(saved_genotype[mom]) < length(person.genotype[mom, loc])
        stable_family[fam] = false
        person.genotype[mom, loc] = intersect(saved_genotype[mom],
          person.genotype[mom, loc])
      end
      if length(saved_genotype[dad]) < length(person.genotype[dad, loc])
        stable_family[fam] = false
        person.genotype[dad, loc] = intersect(saved_genotype[dad],
          person.genotype[dad, loc])
      end
      for sib in nuclear_family.sib[fam]
        if length(saved_genotype[sib]) < length(person.genotype[sib, loc])
          stable_family[fam] = false
          person.genotype[sib, loc] = intersect(saved_genotype[sib],
            person.genotype[sib, loc])
        end
      end  # sib loop
    end  # family loop
    #
    # Genotype elimination continues until genotypes stabilize
    # across all pedigrees.
    #
    fill!(stable_pedigree, true)
    for fam = 1:families
      ped = nuclear_family.pedigree[fam]
      stable_pedigree[ped] = stable_pedigree[ped] && stable_family[fam]
    end
    progress = !all(stable_pedigree)
  end # progress loop
  #
  # Recount the number of homozygous genotypes per person.
  #
  count_homozygotes!(person, loc)
end # function genotype_elimination!

"""
Specify the steps of the Elston-Stewart algorithm for each pedigree.
"""
function prepare_elston_stewart(pedigree::Pedigree, person::Person,
  locus::Locus, elston_stewart_count::Vector{Float64},
  instruction_start::Vector{Int}, instruction_finish::Vector{Int},
  operation::Vector{Int}, site::Vector{Int}, rankarray::Vector{Int},
  space::Vector{Int}, extra::Vector{Vector{Int}}, bound::Int,
  product_mode::Bool, complexity_threshold::Float64)

  pedigrees = pedigree.pedigrees
  model_loci = locus.model_loci
  free_recombination = locus.free_recombination
  instructions = 0
  #
  # Record the homozygote counts at the model loci.
  #
  homozygotes = zeros(Int, person.people, model_loci)
  for l = 1:model_loci
    homozygotes[:, l] = person.homozygotes[:, locus.model_locus[l]]
  end
  #
  # Create the instructions pedigree by pedigree.
  #
  for ped = 1:pedigrees
    ped_start = pedigree.pedstart[ped]
    ped_finish = pedigree.pedfinish[ped]
    ped_size = ped_finish - ped_start + 1
    pedigree_product_mode = product_mode
    #
    # Set up the adjacency graph of the current pedigree. From the adjacency
    # graph, determine a summation sequence by the greedy algorithm.
    # Note that greedy changes the set array graph.
    #
    (nodes, graph, weight) = adjacency_graph(pedigree, person, locus,
      homozygotes, pedigree_product_mode, ped, model_loci, free_recombination)
    (lowest_cost_sum, permutation) = greedy(nodes, graph, weight, ped_size)
    #
    # When likelihood evaluation can proceed in product mode, check if
    # non-product mode will do better.
    #
    if product_mode && !free_recombination
      product_mode_cost = lowest_cost_sum
      pedigree_product_mode = false
      (nodes, graph, weight) = adjacency_graph(pedigree, person, locus,
        homozygotes, pedigree_product_mode, ped, model_loci, free_recombination)
      (lowest_cost_sum, permutation) = greedy(nodes, graph, weight, ped_size)
      #
      # When product mode is superior, revert to it.
      #
      if lowest_cost_sum > product_mode_cost
        pedigree_product_mode = true
        (nodes, graph, weight) = adjacency_graph(pedigree, person, locus,
          homozygotes, pedigree_product_mode, ped, model_loci,
          free_recombination)
        (lowest_cost_sum, permutation) = greedy(nodes, graph, weight, ped_size)
      end
    end
    #
    # Decide whether the current pedigree is too demanding for likelihood
    # computation. If not, set up the index sets corresponding to the
    # various arrays encountered in likelihood evaluation.
    #
    if lowest_cost_sum <= complexity_threshold
      (work_arrays, index_set) = create_index_set(pedigree, person, locus,
        nodes, ped, pedigree_product_mode)
      #
      # Prepare the instructions for the Elston-Stewart algorithm.
      #
      instruction_start[ped] = instructions + 1
      elston_stewart_count[ped] = 0.0
      instructions = prepare_instructions(pedigree, person, locus,
        elston_stewart_count, index_set, weight, permutation, extra,
        operation, rankarray, space, site, work_arrays, instructions, nodes,
        bound, ped, pedigree_product_mode)
      instruction_finish[ped] = instructions
    #
    # If the current pedigree is too complex, set the Elston-Stewart count
    # equal to the lowest cost sum.
    #
    else
      instructions = instructions + 1
      instruction_start[ped] = instructions
      elston_stewart_count[ped] = lowest_cost_sum
      instruction_finish[ped] = instructions
    end
  end
  return instructions
end # function prepare_elston_stewart

"""
Define the adjacency graph connecting the indices of summation.
"""
function adjacency_graph(pedigree::Pedigree, person::Person, locus::Locus,
  homozygotes::Matrix{Int}, pedigree_product_mode::Bool, ped::Int,
  model_loci::Int, free_recombination::Bool)

  ped_start = pedigree.pedstart[ped]
  ped_finish = pedigree.pedfinish[ped]
  ped_size = ped_finish - ped_start + 1
  offset = ped_start - 1
  #
  # In product mode, nodes of the adjacency graph are person-locus pairs.
  # In non-product mode, nodes are simply people.
  #
  if pedigree_product_mode
    nodes = ped_size * model_loci
  else
    nodes = ped_size
  end
  #
  # Each node has a set of neighbors graph[node]. The weight of a node equals
  # the number of its associated genotypes or multi-locus genotypes.
  #
  weight = ones(Int, nodes)
  graph = empties(nodes)
  #
  # In product mode, loop over all person-locus pairs. Number nodes by
  # putting them in a matrix with loci on rows and people on columns.
  # A node is numbered by the its column-major position in the matrix.
  #
  if pedigree_product_mode
    for i = ped_start:ped_finish
      mom = person.mother[i]
      dad = person.father[i]
      for j = 1:model_loci
        jloc = locus.model_locus[j]
        node = (i - ped_start) * model_loci + j
        weight[node] = length(person.genotype[i, jloc])
        for k = 1:model_loci
          kloc = locus.model_locus[k]
          self_connected = false
          if mom != 0
            if !loci_split(homozygotes[mom, :], j, k, free_recombination)
              if j != k; self_connected = true; end
              push!(graph[node], (mom - ped_start) * model_loci + k)
            end
          end
          if dad != 0
            if !loci_split(homozygotes[dad, :], j, k, free_recombination)
              if j != k; self_connected = true; end
              push!(graph[node], (dad - ped_start) * model_loci + k)
            end
          end
          if length(person.children[i]) > 0 &&
            !loci_split(homozygotes[i, :], j, k, free_recombination)
            if j != k; self_connected = true; end
            for kid in person.children[i]
              push!(graph[node], (kid - ped_start) * model_loci + k)
            end
          end
          if self_connected
            push!(graph[node], (i - ped_start) * model_loci + k)
          end
        end
      end
    end
  else
  #
  # In non-product mode, two people are neighbors if and only if one
  # is the parent of the other. If this is true, then they occur as
  # indices in the same transmission array. To insure that nodes are
  # numbered starting with 1, subtract an offset from each person number.
  #
    for node = 1:nodes
      i = node + offset
      for kid in person.children[i]
        push!(graph[node], kid - offset)
      end
      if person.mother[i] != 0
        push!(graph[node], person.mother[i] - offset)
        push!(graph[node], person.father[i] - offset)
      end
      for l = 1:model_loci
        loc = locus.model_locus[l]
        weight[node] = weight[node] * length(person.genotype[i, loc])
      end
    end
  end
  return (nodes, graph, weight)
end # function adjacency_graph

"""
Decide if two model loci are split within person i
in the sense of being separated by an obligate heterozygous locus.
Under free recombination, the likelihood factors over different loci.
"""
function loci_split(homozygotes, locus_1::Int, locus_2::Int,
                    free_recombination::Bool)

  if free_recombination
    split_loci = locus_1 != locus_2
  else
    split_loci = false
    for loc = min(locus_1, locus_2) + 1:max(locus_1, locus_2) - 1
      if homozygotes[loc] == 0
        return true
      end
    end
  end
  return split_loci
end # function loci_split

"""
Select a good summation sequence by the greedy method
and store it in a permutation. Observe that the cost
of removing a node from the adjacency graph is a simple surrogate
for the number of arithmetic operations involved in summing over
the genotypes possible for the node.
"""
function greedy(nodes::Int, graph::Vector{BitSet},
                weight::Vector{Int}, ped_size::Int)

  log_cost = zeros(nodes)
  log_weight = zeros(nodes)
  #
  # Initialize the permutation and the log weight attached to each node.
  #
  permutation = collect(1:nodes)
  for node = 1:nodes
    log_weight[node] = log(weight[node])
  end
  lowest_cost_sum = 0.0
  #
  # Compute the log cost of eliminating a node.
  #
  for node = 1:nodes
    log_sum = log_weight[node]
    for element in graph[node]
      log_sum = log_sum + log_weight[element]
    end
    log_cost[node] = log_sum
  end
  #
  # Find the lowest cost node to eliminate. Note that the variable last
  # limits the search to nodes representing the current locus when we are
  # in product mode. Nodes are therefore considered in clumps starting
  # with the leftmost locus and ending with the rightmost locus.
  #
  for i = 1:nodes
    lowest_cost = log_cost[permutation[i]]
    lowest_cost_index = i
    last = (div(i - 1, ped_size) + 1) * ped_size
    for j = i + 1:last
      competing_cost = log_cost[permutation[j]]
      if competing_cost < lowest_cost
        (lowest_cost, lowest_cost_index) = (competing_cost, j)
      end
    end
    low = exp(lowest_cost)
    lowest_cost_sum = lowest_cost_sum + min(low, 1e20)
    #
    # Insert the lowest cost node into the permutation.
    #
    (permutation[i], permutation[lowest_cost_index]) =
      (permutation[lowest_cost_index], permutation[i])
    best = permutation[i]
    #
    # Expand the neighborhood of each neighbor of the lowest cost node and
    # Delete the lowest cost node and neighbor from each revised neighborhood.
    #
    for neighbor in graph[best]
      graph[neighbor] = union(graph[neighbor], graph[best])
      delete!(graph[neighbor], best)
      delete!(graph[neighbor], neighbor)
      #
      # Recompute the log cost of removing each neighbor.
      #
      log_sum = log_weight[neighbor]
      for element in graph[neighbor]
        log_sum = log_sum + log_weight[element]
      end
      log_cost[neighbor] = log_sum
    end
  end
  return (lowest_cost_sum, permutation)
end  # function greedy

"""
Create the index sets for all initial arrays in likelihood computation.
"""
function create_index_set(pedigree::Pedigree, person::Person,
  locus::Locus, nodes::Int, ped::Int, pedigree_product_mode::Bool)
  #
  # Allocate the index sets and a variable n tracking the current index set.
  #
  model_loci = locus.model_loci
  free_recombination = locus.free_recombination
  ped_start = pedigree.pedstart[ped]
  ped_finish = pedigree.pedfinish[ped]
  offset = ped_start - 1
  index_set = empties(6 * nodes)
  n = 0
  #
  # In product mode, loop over all person-locus pairs. Prepare for the
  # penetrance-prior arrays first.
  #
  if pedigree_product_mode
    for i = ped_start:ped_finish
      for l = 1:model_loci
        n = n + 1
        push!(index_set[n], (i - ped_start) * model_loci + l)
      end
    end
    #
    # Next prepare for the transmission arrays by looping over all children.
    # Transmission arrays will be considered parent by parent.
    #
    for i = ped_start:ped_finish
      if person.mother[i] != 0
        for parent = 1:2
          if parent == 1
            j = person.mother[i]
          else
            j = person.father[i]
          end
          #
          # Find a first and last locus in a block of loci not split by
          # an obligate heterozygous locus in the parent.
          #
          (first, last) = (1, 0)
          while last < model_loci
          #
          # Under free recombination, each transmission arrays completely
          # factors.
          #
            if free_recombination
              last = first
            else
              last = model_loci
              for l = first + 1:model_loci
                loc = locus.model_locus[l]
                if person.homozygotes[j, loc] == 0
                  last = l
                  break
                end
              end
            end
            #
            # Insert the parent-locus pairs into the index set before the
            # child-locus pairs.
            #
            n = n + 1
            spread = last - first + 1
            for l = first:last
              push!(index_set[n], (j - ped_start) * model_loci + l)
              push!(index_set[n], (i - ped_start) * model_loci + l)
            end
            #
            # Exit if the last locus has been hit. Otherwise, reset the
            # first locus to the current last locus and find another block
            # of loci.
            #
            if last == model_loci; break; end
            if free_recombination; last = first + 1; end
            first = last
          end
        end
      end
    end
  else
    #
    # In nonproduct mode, prepare for the penetrance-prior arrays first.
    #
    for i = ped_start:ped_finish
      n = n + 1
      push!(index_set[n], i - offset)
    end
    #
    # Next prepare for the transmission arrays by looping over all children.
    #
    for i = ped_start:ped_finish
      if person.mother[i] != 0
        n = n + 1
        push!(index_set[n], person.mother[i] - offset)
        push!(index_set[n], i - offset)
        n = n + 1
        push!(index_set[n], person.father[i] - offset)
        push!(index_set[n], i - offset)
      end
    end
  end
  #
  # Return the initial index sets.
  #
  return (n, index_set)
end # function create_index_set

"""
Prepare the instructions for performing the Elston-Stewart algorithm.
"""
function prepare_instructions(pedigree::Pedigree, person::Person, locus::Locus,
  elston_stewart_count::Vector{Float64}, index_set::Vector{BitSet},
  weight::Vector{Int}, permutation::Vector{Int}, extra::Vector{Vector{Int}},
  operation::Vector{Int}, rankarray::Vector{Int}, space::Vector{Int},
  site::Vector{Int}, work_arrays::Int, instructions::Int, nodes::Int,
  bound::Int, ped::Int, pedigree_product_mode::Bool)

  model_loci = locus.model_loci
  ped_start = pedigree.pedstart[ped]
  offset = ped_start - 1
  #
  # Let n be the index of the current instruction. Prepare a set
  # to hold which arrays are involved in the current operation.
  #
  n = instructions
  active_array = trues(6 * nodes)
  involved = BitSet()
  union_set = BitSet()
  #
  # Give the instructions for creating the initial arrays.
  #
  for i = 1:work_arrays
    n = n + 1
    if i <= nodes
      operation[n] = penetrance_and_prior_array
      extra[n] = zeros(Int, 3)
      #
      # Load the starting and finishing loci and the person involved
      # in a penetrance and prior array.
      #
      if pedigree_product_mode
        j = first(index_set[i]) - 1
        extra[n][1] = mod(j, model_loci) + 1
        extra[n][2] = extra[n][1]
        extra[n][3] = div(j, model_loci) + ped_start
      else
        extra[n][1] = 1
        extra[n][2] = model_loci
        extra[n][3] = first(index_set[i]) + offset
      end
    else
      operation[n] = transmission_array
      extra[n] = zeros(Int, 4)
      #
      # Load the starting and finishing loci and the parent and child
      # involved in a transmission array.
      #
      if pedigree_product_mode
        j = first(index_set[i]) - 1
        k = last(index_set[i]) - 1
        extra[n][1] = mod(j, model_loci) + 1
        extra[n][2] = mod(k, model_loci) + 1
        extra[n][3] = div(j, model_loci) + ped_start
        extra[n][4] = div(k, model_loci) + ped_start
      else
        extra[n][1] = 1
        extra[n][2] = model_loci
        extra[n][3] = first(index_set[i]) + offset
        extra[n][4] = last(index_set[i]) + offset
      end
    end
    #
    # Load the site, the rank, and the size of the current array.
    #
    site[n] = i
    rankarray[n] = length(index_set[i])
    space[n] = product_weights(index_set[i], weight)
  end
  #
  # Give the instructions for creating and destroying arrays in computing
  # the likelihood of the current pedigree.
  #
  for node = 1:nodes
    i = permutation[node]
    #
    # Search through the index sets and find those containing the node i
    # to be eliminated.
    #
    for iteration = 1:100000
      involved = BitSet()
      for k = 1:work_arrays
        if active_array[k] && i in index_set[k]
          push!(involved, k)
        end
      end
      #
      # Exit the loop if no index containing i remains.
      #
      if isempty(involved); break; end
      #
      # Update the instruction index.
      #
      n = n + 1
      #
      # For a pure add, record the array to be summed.
      #
      if length(involved) == 1
        operation[n] = pure_add
        j = first(involved)
        site[n] = j
        rankarray[n] = length(index_set[j])
        space[n] = product_weights(index_set[j], weight)
        elston_stewart_count[ped] = elston_stewart_count[ped] + space[n]
        active_array[j] = false
        break
      #
      # For a pure multiply or a multiply and add,
      # choose a pair of arrays to combine.
      #
      else
        best_1 = 1
        best_2 = 2
        pivot = i
        #
        # For a pure multiply, find the best pair of arrays
        # to multiply by a greedy algorithm.
        #
        if length(involved) > 2
          pivot = 0
          best_size = Inf
          for j = 1:length(involved)
            k = select_set_element(involved, j)
            for l = j + 1:length(involved)
              m = select_set_element(involved, l)
              union_set = union(index_set[k], index_set[m])
              criterion = product_weights(union_set, weight)
              if criterion < best_size
                best_1 = j
                best_2 = l
                best_size = criterion
              end
            end
          end
        end
        #
        # Form the union of the index sets corresponding to
        # the only or best pair of arrays.
        #
        j = select_set_element(involved, best_1)
        k = select_set_element(involved, best_2)
        union_set = union(index_set[j], index_set[k])
        #
        # Combine nodes and insert the instructions for a pure multiply or
        # a multiply and add.
        #
        (rankarray[n], extra[n]) = combine_nodes(index_set[j], index_set[k],
          union_set, weight, j, k, n, pivot)
        #
        # Delete node i in a multiply and add. Also record the current
        # operation.
        #
        space[n] = product_weights(union_set, weight)
        if length(involved) == 2
          elston_stewart_count[ped] = elston_stewart_count[ped] + 2 * space[n]
          delete!(union_set, i)
          operation[n] = multiply_and_add
          space[n] = product_weights(union_set, weight)
        else
          elston_stewart_count[ped] = elston_stewart_count[ped] + space[n]
          operation[n] = pure_multiply
        end
        #
        # Activate the array created and determine its location and space
        # requirement.
        #
        work_arrays = work_arrays + 1
        index_set[work_arrays] = deepcopy(union_set)
        active_array[work_arrays] = true
        site[n] = work_arrays
        #
        # Deactivate the two arrays destroyed.
        #
        active_array[j] = false
        active_array[k] = false
      end
    end
  end
  #
  # Give the instruction for finishing the current pedigree and update the
  # number of instructions
  #
  n = n + 1
  operation[n] = quit_processing_pedigree
  return n
end # function prepare_instructions

"""
Consolidate the nodes for the multiplicand and product arrays into blocks.
Generate the instructions for a pure multiply or a multiply and add.
a and b are the index sets for the multiplicand arrays,
and a_union_b is the index set for the product array.
n is the current instruction index, and pivot is the node to
be eliminated in a multiply and add. a_location and b_location give
the locations of the multiplicand arrays in the evolving sequence of arrays.
"""
function combine_nodes(a::BitSet, b::BitSet, a_union_b::BitSet,
  weight::Vector{Int}, a_location::Int, b_location::Int, nodes::Int, pivot::Int)
  #
  # Initialize current positions in the original index sets and
  # the block index sets. Also initialize the position of the pivot
  # and the type indicator for block construction.
  #
  extent = zeros(Int, length(a_union_b))
  a_set_position = 1
  b_set_position = 1
  a_block_position = 0
  b_block_position = 0
  union_block_position = 0
  pivot_block_position = 0
  previous_type = -2
  a_stride = 0
  b_stride = 0
  #
  # Create and initialize arrays.
  #
  c = length(a_union_b)
  a_block = zeros(Int, c)
  b_block = zeros(Int, c)
  a_increment = zeros(Int, c)
  b_increment = zeros(Int, c)
  #
  # March through the union of the two original index sets, defining
  # blocks as we go. If the a set has been exhausted, flag its current
  # node as impossible; likewise for the b set.
  #
  while a_set_position <= length(a) || b_set_position <= length(b)
    if a_set_position > length(a)
      current_a_node = nodes + 1
    else
      current_a_node = select_set_element(a, a_set_position)
    end
    if b_set_position > length(b)
      current_b_node = nodes + 1
    else
      current_b_node = select_set_element(b, b_set_position)
    end
    #
    # A new block involving indices from the a set must be started.
    #
    if current_a_node < current_b_node
      new_type = 1
      current_node = current_a_node
      a_set_position = a_set_position + 1
    #
    # A new block involving indices from the b set must be started.
    #
    elseif current_a_node > current_b_node
      new_type = -1
      current_node = current_b_node
      b_set_position = b_set_position + 1
    #
    # The current block continues.
    #
    else
      new_type = 0
      current_node = current_a_node
      a_set_position = a_set_position + 1
      b_set_position = b_set_position + 1
    end
    #
    # A block boundary is hit whenever the pivot node for a multiply and
    # add is encountered.
    #
    if current_node == pivot
      previous_type = -2
      pivot_block_position = union_block_position + 1
    end
#
# Update the product of the weights associated with a continuing block.
#
    if new_type == previous_type
      extent[union_block_position] = extent[union_block_position] *
        weight[current_node]
    #
    # For a new block, record its position, initial weight, and its membership
    # in the a and/or b block index sets.
    #
    else
      union_block_position = union_block_position + 1
      extent[union_block_position] = weight[current_node]
      if new_type == 1 || new_type == 0
        a_block_position = a_block_position + 1
        a_block[a_block_position] = union_block_position
      end
      if new_type == -1 || new_type == 0
        b_block_position = b_block_position + 1
        b_block[b_block_position] = union_block_position
      end
      #
      # Record whether the current block involves a indices, b indices, or
      # both. The pivot marks the beginning of a new block.
      #
      previous_type = new_type
      if current_node == pivot; previous_type = -2; end
    end
  end
  #
  # Insert the rank of the product array into the current instruction.
  #
  rankarray = union_block_position
  a_blocks = a_block_position
  b_blocks = b_block_position
  #
  # Allocate the extra array for the current instruction. Record the
  # locations of the multiplicand arrays. Also load the pivot range, which
  # determines whether a pure multiply or a multiply and add is performed.
  #
  extra = zeros(Int, 5 + 3 * rankarray)
  extra[1] = a_location
  extra[2] = b_location
  if pivot_block_position == 0
    pivot_range = 0
  else
    pivot_range = extent[pivot_block_position] - 1
  end
  extra[3] = pivot_range
  #
  # Calculate the increments for the multiplicand arrays. Also calculate
  # their strides for a multiply and sum.
  #
  a_block_position = 1
  b_block_position = 1
  a_prod = 1
  b_prod = 1
  for union_block_position = 1:rankarray
    if union_block_position == pivot_block_position
      a_stride = a_prod
      b_stride = b_prod
    end
    if a_block_position <= a_blocks && a_block[a_block_position] == union_block_position
      a_prod = a_prod * extent[union_block_position]
      a_increment[union_block_position] = 1
      a_block_position = a_block_position + 1
    else
      a_increment[union_block_position] = 1 - a_prod
    end
    if b_block_position <= b_blocks && b_block[b_block_position] == union_block_position
      b_prod = b_prod * extent[union_block_position]
      b_increment[union_block_position] = 1
      b_block_position = b_block_position + 1
    else
      b_increment[union_block_position] = 1 - b_prod
    end
  end
  #
  # Load the strides into the current instruction.
  #
  extra[4] = a_stride
  extra[5] = b_stride
  #
  # Adjust the increment of each index pivot to correct for the add
  # operation in a multiply and add.
  #
  for i = 1:pivot_block_position - 1
    a_increment[i] = a_increment[i] - a_stride * pivot_range
    b_increment[i] = b_increment[i] - b_stride * pivot_range
  end
  #
  # Load the extents and increments into the current instruction.
  #
  true_rank = rankarray
  if pivot_block_position != 0; true_rank = rankarray - 1; end
  j = 5
  for i = 1:rankarray
    if i != pivot_block_position
      j = j + 1
      extra[j] = extent[i]
      extra[j + true_rank] = a_increment[i]
      extra[j + 2 * true_rank] = b_increment[i]
    end
  end
  rankarray = true_rank
  return (rankarray, extra)
end # function combine_nodes

"""
Find the product of the weights of the elements of the integer set S.
"""
function product_weights(s::BitSet, weight::Vector{Int})

  p = 1
  for element in s
    p = p * weight[element]
  end
  return p
end # function product_weights

"""
Compute recombination fractions between adjacent model loci.
"""
function model_recombination_fractions(locus::Locus,
  keyword::Dict{AbstractString, Any})

  n = locus.model_loci - 1
  theta = zeros(2, n)
  choice = keyword["genetic_map_function"]
  for l = 1:n
    j = locus.model_locus[l]
    k = locus.model_locus[l + 1]
    d = abs(locus.morgans[1, k] - locus.morgans[1, j])
    theta[1, l] = map_function(d, choice)
    d = abs(locus.morgans[2, k] - locus.morgans[2, j])
    theta[2, l] = map_function(d, choice)
  end
  return theta
end # function model_recombination_fractions


# __BEGIN_LICENSE__
#
# WaveOptics.jl
#
# Copyright (c) 2015, Stanford University
#
# All rights reserved.
#
# Redistribution and use in source and binary forms for academic and other
# non-commercial purposes with or without modification, are permitted provided
# that the following conditions are met:
#
# * Redistributions of source code, including modified source code, must retain
#   the above copyright notice, this list of conditions and the following
#   disclaimer.
#
# * Redistributions in binary form or a modified form of the source code 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.
#
# * Neither the name of The Leland Stanford Junior University, any of its
#   trademarks, the names of its employees, nor contributors to the source code
#   may be used to endorse or promote products derived from this software without
#   specific prior written permission.
#
# * Where a modified version of the source code is redistributed publicly in
#   source or binary forms, the modified source code must be published in a freely
#   accessible manner, or otherwise redistributed at no charge to anyone
#   requesting a copy of the modified source code, subject to the same terms as
#   this agreement.
#
# THIS SOFTWARE IS PROVIDED BY THE TRUSTEES OF THE LELAND STANFORD JUNIOR
# UNIVERSITY "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 LELAND STANFORD JUNIOR
# UNIVERSITY OR ITS TRUSTEES 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.
#
# __END_LICENSE__

using Interpolations

# -----------------------------------------------------------------------------
#           AMPLITUDE POINT SPREAD FUNCTION BASE TYPE & METHODS
# -----------------------------------------------------------------------------

# Base Type
#
# All PointSpreadFunctionModel subtypes will inherit from this base type.
abstract type PointSpreadFunctionModel end

# In a lateral shift invariant models, the image at each z plane can
# be computed using a convolution with a PSF for that z-plane.
#
# Examples: DebyeModel(), VectorialDebyeModel(), and DefocusModel().
#
abstract type LateralShiftInvariantModel <: PointSpreadFunctionModel end

# In a periodic shift invariant models, the point spread function
# displays some periodic lateral shift invariance, but is not completey
# shift invariant.  Projection operators for such models can still be
# computed fairly efficiently using a sequence of convolution operations.
#
# Subtypes of this type must define 'shift_invariance_subpixels' and
# 'shift_invariance_dimensions' to indicate the periodicity of that
# particular model.
#
# Examples: LightFieldModel()
#
abstract type PeriodicShiftInvariantModel <: PointSpreadFunctionModel end

# Shift variant models cannot be projected using convolution, since
# they allow for PSFs that change with every 3D point location.
#
# Examples: CodedApertureLightFieldModel()
#
abstract type ShiftVariantModel <: PointSpreadFunctionModel end

# --------------
# Shared Methods
#
function num_observations(model::PointSpreadFunctionModel)
    return Int64(model.num_observations)
end

function atf(model::PointSpreadFunctionModel, p, theta = Float64[]; na_limit = true, apply_tilt = true)
    error("The atf() method is not implemented for this model type.")
end

function otf_2d(model::PointSpreadFunctionModel, p, theta = Float64[])
    error("The atf() method is not implemented for this model type.")
end

function apsf(model::PointSpreadFunctionModel, p, theta = Float64[])
    error("The apsf() method is not implemented for this model type.")
end

function psf(model::PointSpreadFunctionModel, p, theta = Float64[])
    wf = WaveOptics.apsf(model, p, theta)
    return abs2.(wf)
end

"""
Estimate the diameter (in meters) of an image large enough to contain the point
spread function for a wide field microscope without any phase mask.  Most
(but not all) point spread functions with back aperture masks will fit in
this diameter.

Here, p = (x,y,z) is a tuple containing the point generating this PSF.
"""
function estimate_psf_size(model::PointSpreadFunctionModel, p)

        # Check length and unpack p
        assert(length(p) == 3)
        x,y,z = p

        objective_theta = asin(model.recipe.objective_na / model.recipe.medium_index)
        aperture_diameter = abs( 2 * z * tan(objective_theta) )
        rayleigh_limit = model.recipe.wavelength / (2 * model.recipe.objective_na)

        # Choose a PSF size that comfortably fits the full PSF, with special
        # handling for when z = 0. (The min size is 8 * rayleigh_limit,
        # otherwise it is 2 * aperture_diameter.)
        final_diameter_m = max(8*rayleigh_limit, 2*aperture_diameter)
        return final_diameter_m
end

function optimal_sampling(model::PointSpreadFunctionModel, sim_size_m; oversampling = nothing)
        """
        Given a desired simulation size, this function returns a sampling
        rate (in pixels) that will yield an accurate wave optics simulation.

        Note that this sampling rate is only an estimate based on the abbe
        limit. It may be too high in some cases, and too low in others. Use it
        as a starting point.
        """

        if oversampling == nothing
            oversampling = 1
        end

        # We use the abbe limit as an estimate of the optimal sampling rate. We
        # recommend sampling at 2x the Abbe limit.
        abbe_limit = model.recipe.wavelength / (2. * model.recipe.objective_na);
    return sim_size_m / (2*abbe_limit) * oversampling
end

# --------------------------------------------------------------------------
#                COMMON PHASE MASKS & APSF UTILITIES
# --------------------------------------------------------------------------

"""
This function takes an APSF lookup up table computed using apsf_scalar_debye
or apsf_defocus_atf() and uses the translational invariance property of the
APSF to interpolate and place it at any location x, y at the depth z for
which the apsf_template was computed.

Note, the translation could have been accomplished with a tilt in the back
aperture plane via translation_mask(), but this would have potentially led
to sampling problems if a large tilt (due to a large x or y translation)
exceeded the nyquist sampling rate of the back aperture. For general
translations which may be large, this method is more robust.
"""
function translate_apsf(sim_size_m, sim_size_px, apsf::AbstractArray{T, 2}, dx, dy) where T<:Number
    sample_period = sim_size_m / sim_size_px

    # Use 2D interpolation to shift the translationally invariant microscope PSF
    apsf_real_interp = interpolate(real(apsf), BSpline(Interpolations.Linear()))
    apsf_imag_interp = interpolate(imag(apsf), BSpline(Interpolations.Linear()))

    result = zeros(eltype(apsf), size(apsf))
    for c in 1:size(apsf,2)
        for r in 1:size(apsf,1)
            result[r,c] = apsf_real_interp(r + dy/sample_period, c - dx/sample_period) + im * apsf_imag_interp(r + dy/sample_period, c - dx/sample_period)
        end
    end
    return result
end

"""
Creates a phase mask in the back aperture plane corresponding to a
translation in the image plane. This translation mask is based on the
Fourier shift theorem. For small shifts, it should match with the
translate_apsf() function below.

fx, fy must be in 'normalized' cartesian coordinates (see fourier_plane_meshgrid()).
"""
function translation_mask(dx, dy, fx::Array{T, 2}, fy::Array{T, 2},
                          objective_na::Float64,
                          wavelength::Float64) where T<:AbstractFloat
    result = Array{ComplexF32}(size(fx))
    omega = -2. * im * pi * objective_na/wavelength
    for i in eachindex(result)
        result[i] = exp(omega * (fx[i] * dx + fy[i] * (-dy)))
    end
    return result
end

"""
Implements the defocus phase function described in

Hanser, B. M., Gustafsson, M., & Agard, D. A. (2004). Phase-retrieved pupil
functions in wide-field fluorescence microscopy. Journal of Microscopy. Vol.
216. pp. 32-48. 2004.

This phase function models roughly what happens if the microscope objective
were to be refocused to a different depth 'dz' in the object.

r, theta must be in 'normalized' polar coordinates (see fourier_plane_meshgrid()).
"""
function defocus_mask(dz::S, r::AbstractArray{T,2}, theta::AbstractArray{T,2},
                      objective_na::Float64, wavelength::Float64, medium_index::Float64) where T<:AbstractFloat where S<:Number
    M,N = size(r)
    k = medium_index/wavelength
    omega = T(k^2) .- T(objective_na/wavelength)^2 * r.^2

    # Ensure we don't try to take the sqrt() of a negative number. This should
    # only happen outside the boundary of the aperture, so this check amounts to
    # zeroing out the phase of mask pixels outside the numerical aperture.
    binary_mask = omega .>= 0

    # Since CuArrays.jl doesn't have a function to compute exp(ComplexFloat),
    # we manually compute it.
    phi = T(-2π * dz) * sqrt.(max.(omega, 0))
    phase =  (cos.(phi) + 1im .* sin.(phi)) .* binary_mask
    return phase
end

# --------------------------------------------------------------------------
#                       APODIZATION FUNCTIONS
# --------------------------------------------------------------------------

"""
Apply a the apodization function for the Abbe sine condition. This
implements eqn. 6.3.7 from Min Gu's "Advanced Optical Imaging Theory"

r, theta must be in "normalized" polar coordinates (see fourier_plane_meshgrid()).
"""
function abbe_sine_apodization(rho::Array{T}, theta::Array{T}, objective_na::AbstractFloat, medium_index::AbstractFloat) where T<:AbstractFloat

    result = zeros(ComplexF32, size(rho))
    omega = objective_na/medium_index
    for i in eachindex(rho)
        if rho[i] <= 1.0
            alpha = asin(omega * rho[i])    # Gross eq. 21-3 and 21-4
            apodization = sqrt(cos(alpha))                # Gu eq. 6.3.10
            result[i] = apodization + 0. * im
        else
            result[i] = 0.
        end
    end
    return result
end


#OVERVIEW
#----------------
#Any JuMP model that describes an optimization problem must have four parts:

#Model Object,
#Variables,
#Objective,
#Constraints.

#PACKAGE IMPORT
#----------------
println("Initializing...")

using JuMP  # Need to say it whenever we use JuMP

using GLPK

using CSV # Import data

using DataFrames # For results export to CSV

using Plots

#=
electrolyzer_tea.jl is a function used to evaluate the TEA of the electrochemical
system, including separations, stack cost, and other operational parameters.
This function takes a series of inputs specific to an electrochemical pathway,
as shown below:

function electrolyzer_TEA(
    flow, product flow in [kg/year]
    product_phase, phase of product ["Gas" or "Liquid"]
    product_mw, molecular weight of product [g/mol]
    product_density, product density [kg/m^3]
    electrons_product, [# e-]
    electrons_co2, [#e-/co2]
    system_potential, whole cell potential [V]
    selectivity, selectivity, or faradaic efficiency of CO2R system to product [%]
    hours_year, hours in a given year [hours/year]
    current_density, [A/cm^2]
    single_pass_conversion, single pass conversion of CO2 to product [%]
    electrolyzer_cost, electrolyzer capital cost [$/m^2]
    electricity_price, [$/kWh]
    water_price, process water cost [$/kg]
    water_rate, rate of water consusumption [kg water/kg product]
    crf capital recovery factor [1/years]
    )
=#
include("./electrolyzer_tea.jl")

#=
The electrolyzer tea function operates identically to the electrolyzer tea function
discussed above, however it returns an array of results for costs rather
than a single value. This allows for a more detailed analysis of costs
included in the TEA. Input values are identical, output values are as follows:

    return:
    electrolyzer_capex + electrolyzer_BOP, electrolyzer costs [$/year]
    PSA_capex + PSA_opex + distil_capex + distil_opex, separations costs [$/year]
    battery_capex, battery energy storage costs [$/year]
    maintenance_cost, system maintenance costs [$/year]
    water_opex annual process water costs [$/year]
=#
include("./electrolyzer_tea_array.jl")

__precompile__()

# Define function to perform optimization routine - decreases runtime
function co2_optimizer()

    #DATA IMPORT
    #--------------------
    input_filename = "input_data_full.csv"
    input_filepath = joinpath(@__DIR__, input_filename)

    # Import point source and point sink data
    data = DataFrame!(CSV.File(input_filepath))

    #=
    Option to exclude specific point types from either sink or
    source data in optimizer. To use, fill the array with the proper name
    of the point source to be excluded, e.g. ["COAL", "GAS"]
    =#
    #--------------------
    source_exclusions =  ["COAL","GAS"]#["None"]#["None"]#["None"]
    sink_exclusions = ["None"]#["Import","Export","Acetic Acid Plant", "Petroleum Terminal"]#["Ethane Crackers"]#["Import","Export"]#["None"]

    for i = 1:length(source_exclusions)

        data = data[data[!,8].!= source_exclusions[i] ,:]

    end

    for i = 1:length(sink_exclusions)

        data = data[data[!,8].!= sink_exclusions[i] ,:]

    end

    println("Source and sink points to be considered:")
    println(size(data,1))

    #VARIABLES and generic functions
    #---------

    # A variable is modeled using @defVar(name of the model object, variable name and bound, variable type)
    # Bound can be lower bound, upper bound or both. If no variable type is defined, then it is treated as
    #real. For binary variable write Bin and for integer use Int.

    # Sink/source point datasets
    #_____________________________________________________________________________

    #= Isolate consumption point data by including only points where
    demand for the molecule is not equal to zero =#
    sinks_C2H4 = data[data[:,4].!= 0  ,:]
    sinks_HCOO = data[data[:,5].!= 0  ,:]
    sinks_CO = data[data[:,6].!= 0  ,:]

    #= Isolate CO2 point source data by including only points where
    feedstock supply is not equal to zero =#
    source_pts = data[data[:,7].!= 0  ,:]
    sink_pts = data[data[:,7].== 0  ,:]

    # Number of chemical products considered
    num_species = 3

    # Find number of CO2 point sources
    num_source_pts = size(data[data[:,7].!= 0  ,:],1)

    # Find number of sink points
    num_sink_pts = size(data[data[:,7].== 0  ,:],1)

    # Find total number of points
    num_pts = size(data,1)

    # Faradays constant
    F = 96485 # [C/mol]

    # Sink/source distance evaluation datasets
    # Goal: build an array showing distance between sink and source points
    #_____________________________________________________________________________

    # Array of lat/long for sources
    lat_source = source_pts[:, 2]
    long_source = source_pts[:, 3]

    # Array of lat/long for sinks
    lat_sink = sink_pts[:, 2]
    long_sink = sink_pts[:, 3]

    # Molecular weights
    #_____________________________________________________________________________
    C_mw = 12.011 # [g/mol]
    CO2_mw = 44 # [g/mol]
    C2H4_mw = 28.05 # [g/mol]
    HCOO_mw = 45.017 # [g/mol]
    CO_mw = 28.01 # [g/mol]
    H2O_mw = 18 # [g/mol]

    product_mw = [C2H4_mw , HCOO_mw, CO_mw]

    # Molecule carbons
    #_____________________________________________________________________________
    C2H4_carbons = 2 # [carbons/molecule]
    HCOO_carbons = 1 # [carbons/molecule]
    CO_carbons = 1 # [carbons/molecule]

    carbons = [C2H4_carbons , HCOO_carbons, CO_carbons]

    # Electrons per mol product
    #_____________________________________________________________________________
    C2H4_electrons_product = 12  # [mol e- / mol product]
    HCOO_electrons_product = 2 # [mol e- / mol product]
    CO_electrons_product = 2 # [mol e- / mol product]
    H2O_electrons = 4 # [mol e- / mol product]

    electrons_product = [C2H4_electrons_product , HCOO_electrons_product, CO_electrons_product]

    # Product state
    #_____________________________________________________________________________
    C2H4_phase = "Gas"
    HCOO_phase = "Liquid"
    CO_phase = "Gas"

    product_phase = [C2H4_phase, HCOO_phase, CO_phase]

    # Product densities
    #_____________________________________________________________________________
    C2H4_density = 1.18 # [kg/m^3]
    HCOO_density = 1221 # [kg/m^3]
    CO_density = 1.14 # [kg/m^3]

    product_density = [C2H4_density , HCOO_density, CO_density]

    # Electrons per mol CO2
    #_____________________________________________________________________________
    C2H4_electrons_co2 = 6  # [mol e- / mol co2]
    HCOO_electrons_co2 = 2 # [mol e- / mol co2]
    CO_electrons_co2 = 2 # [mol e- / mol co2]

    electrons_co2 = [C2H4_electrons_co2 , HCOO_electrons_co2, CO_electrons_co2]

    # Current densities
    #_____________________________________________________________________________
    C2H4_cd = 1.55 # [A/cm^2] 10.1126/science.aay4217
    HCOO_cd = 0.5 # [A/cm^2] 10.1021/acsenergylett.0c00860
    CO_cd = 0.35 # [A/cm^2] http://xlink.rsc.org/?DOI=C9EE02410G

    current_density = [C2H4_cd , HCOO_cd, CO_cd]

    # single pass conversion
    #_____________________________________________________________________________
    C2H4_spc = 0.5 # [%] assumed
    HCOO_spc = 0.5 # [%] assumed
    CO_spc = 0.5 # [%] assumed

    single_pass_conversion = [C2H4_spc , HCOO_spc, CO_spc]

    # Selectivities
    #___________________________________________________________________________
    C2H4_selectivity = 0.6 # 10.1126/science.aay4217
    HCOO_selectivity = 0.93 #  10.1021/acsenergylett.0c00860
    CO_selectivity = 0.95  #  http://xlink.rsc.org/?DOI=C9EE02410G

    selectivity = [C2H4_selectivity , HCOO_selectivity, CO_selectivity]

    # Potentials
    #_____________________________________________________________________________
    C2H4_system_potential = 3.9 #[V] 10.1126/science.aay4217 taken from fig s33, durability study
    HCOO_system_potential = 5.8 #[V] 10.1021/acsenergylett.0c00860 table 1
    CO_system_potential = 3.0 #[V] 10.1021/acs.iecr.7b03514

    system_potential = [C2H4_system_potential , HCOO_system_potential, CO_system_potential]

    # Market Values
    #_____________________________________________________________________________
    C2H4_value = 0.555 # [$/kg] C2H4 value from PEP yearbook and: https://www.spglobal.com/platts/en/market-insights/podcasts/focus/040720-north-america-lng-exports-price-plunge
    HCOO_value = 0.735 # [$/kg] CO value https://pubs.acs.org/doi/10.1021/acs.iecr.7b03514
    CO_value = 0.6 # [$/kg] CO value https://pubs.acs.org/doi/10.1021/acs.iecr.7b03514

    value = [C2H4_value , HCOO_value, CO_value]

    # Market Demand
    #_____________________________________________________________________________
    C2H4_demand = sink_pts[:,4]  # consumption at each point in [kg/year]
    HCOO_demand = sink_pts[: ,5] # consumption at each point in [kg/year]
    CO_demand = sink_pts[: ,6] # consumption at each point in [kg/year]

    demand = hcat(C2H4_demand, HCOO_demand, CO_demand)

    # Water rate of consumption
    #_____________________________________________________________________________
    C2H4_water = 1.28 #[kg H2O/kg product]
    HCOO_water = 0.391 #[kg H2O/kg product]
    CO_water = 0.6426 #[kg H2O/kg product]

    water_rate = [C2H4_water, HCOO_water, CO_water]

    #_____________________________________________________________________________
    capacity = data[data[:,7].!= 0  ,7] * 1e3 * (C_mw / CO2_mw) # Feedstock carbon at each point in [kg C/year]

    transport_A = 1e-5 # Best fit line for CO2 transportation costs from IPCC (see Excel sheet)
    transport_B = -0.0002 # Best fit line for CO2 transportation costs from IPCC (see Excel sheet)

    #CO2 flue gas capture cost for each location [$/kg C]
    CO2_cap_cost = data[data[:,7].!= 0  ,12] / 1e3 * (C_mw / CO2_mw) #[$/kg C]

    # Spatially distinct electricity prices in [$/kWh] (sink pts)
    electricity_price = data[data[:,7].!= 0  ,15]

    # Economic assumptions
    #_____________________________________________________________________________

    hours_year = 8000 # [hours of operation annually]
    electrolyzer_cost = 10000 # [$/m^2]
    interest_rate = 0.08 # [%]
    facility_lifetime = 15 #[years]
    water_price = 8.929e-4 # [$/kg] https://www.energy.gov/sites/prod/files/2017/10/f38/water_wastewater_escalation_rate_study.pdf
    crf = (interest_rate * (1 + interest_rate)^facility_lifetime) / ((1 + interest_rate)^facility_lifetime - 1)

    # Define an array with rows = number of sources, columns = number of sinks, and sheets = number of products
    D = zeros(Float16, num_source_pts, num_sink_pts, num_species)

    # Distance constraint evaluation.
    #_____________________________________________________________________________
    #=
    Define an array with rows = source points, columns = sink points,
    sheets = number of species. Default values are zeros. The loop below changes
    the value to 1 if distance between sink/source is <1,000 km.
    This is then incorporated in the flow variable definition as an upper
    bound on the variable.
    =#
    up_bound = zeros(num_source_pts,num_sink_pts,num_species)
    for i in 1:num_source_pts
        for j in 1:num_sink_pts
            for k in 1:num_species
                pt1 = [long_source[i]; lat_source[i]]
                pt2 = [long_sink[j]; lat_sink[j]]

                D[i, j, k] = (abs(pt1[1] - pt2[1]) + abs(pt1[2] - pt2[2])) / 1e3

                if (abs(source_pts[i,3] - sink_pts[j,3]) + abs(source_pts[i,2] - sink_pts[j,2])) / 1e3 < 1000
                    up_bound[i, j, k] = 1.0

                end
            end
        end
    end

    #MODEL CONSTRUCTION
    #--------------------

    # Preparing an optimization model
    eco2RR = Model(GLPK.Optimizer)

    #OPTIMIZATION VARIABLES
    #--------------------
    #Initialize flow array [kg/year]
    @variable(eco2RR, flow[i = 1:num_source_pts , j = 1:num_sink_pts , k = 1:num_species] >= 0, upper_bound = up_bound[i, j, k].*1e10) # distance constraint
    #@variable(eco2RR, flow[i = 1:num_source_pts , j = 1:num_sink_pts , k = 1:num_species] >= 0)

    #OPTIMIZATION CONSTRAINTS
    #--------------------

    # Ship no more than plant capacity, in units of [kg carbon]
    @constraint(eco2RR, capacity_con[i in 1:num_source_pts],
    + sum(flow[i, : , 1]) * carbons[1] * C_mw / product_mw[1] / selectivity[1]
    + sum(flow[i, : , 2]) * carbons[2] * C_mw / product_mw[2] / selectivity[2]
    + sum(flow[i, : , 3]) * carbons[3] * C_mw / product_mw[3] / selectivity[3]
    <= capacity[i])

    #=
    Constrain total electrochemical system power consumption at a source point
    to less than 300MW. Provides a realistic constraint on system size

    Modifying this equality only applies when the system is feedstock constrained
    i.e. total demand is greater than carbon feedstock available

    Two key options are available:

    == equality - model required to export 300MW worth at each CO2 point.

    <= equality - model can provide anything up to 300MW system at a CO2 point source.

    =#

    @constraint(eco2RR, elec_con[i in 1:num_source_pts],
    sum(
    + sum(flow[i, :, 1]) / hours_year / 3600 * 1e3 / product_mw[1] * electrons_product[1] * F * system_potential[1] / selectivity[1] / 1e6
    + sum(flow[i, :, 2]) / hours_year / 3600 * 1e3 / product_mw[2] * electrons_product[2] * F * system_potential[2] / selectivity[2] / 1e6
    + sum(flow[i, :, 3]) / hours_year / 3600 * 1e3 / product_mw[3] * electrons_product[3] * F * system_potential[3] / selectivity[3] / 1e6
    )
    <= 500)

    #=
    Choose how the model attempts to meet demand for different molecules.
    This constraint can cause the model to be unable to find a SOLUTION
    if feedstock inputs are not sufficient to meet demand.

    Modifying this equality only applies when the system is demand constrained
    i.e. total demand is less than carbon feedstock available

    Two key options are available:

    == equality - model required to meet demand at each point. In this scenario,
        if feedstock inputs are not sufficient then optimizer will not find a
        solution

    <= equality - model can provide anything up to market demand at a point.
        Using this equality allows the optimizer to only make flows it deems
        economically sustainable (positive NPV).

    =#
    @constraint(eco2RR, demand_con[j in 1:num_sink_pts, k in 1:num_species],
    sum(flow[:, j , k]) == demand[j,k])

    println("Constraints Defined")

    #OPTIMIZATION OBJECTIVE FUNCTION
    #--------------------
    println("Beginning Optimization...")

    @objective(
        eco2RR, # Designate model name
        Max, # Tell JuMP to minimize/optimize
        sum(
        (

        - (D[i, j, k] * transport_A + transport_B) * flow[i, j, k] # Transportation cost
        - electrolyzer_TEA(
            flow[i, j, k],
            product_phase[k],
            product_mw[k],
            product_density[k],
            electrons_product[k],
            electrons_co2[k],
            system_potential[k],
            selectivity[k],
            hours_year,
            current_density[k],
            single_pass_conversion[k],
            electrolyzer_cost,
            electricity_price[i],
            water_price,
            water_rate[k],
            crf) # System TEA
        - flow[i, j, k] * carbons[k] * C_mw / product_mw[k] * CO2_cap_cost[i] # Carbon capture cost
        - flow[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F * system_potential[k] / selectivity[k] / 1e3 * hours_year * electricity_price[i] # Electrochemical system electricity cost
        + flow[i, j, k] * value[k] # Product value
        )

        for
            i in 1:num_source_pts, # Iterate over source points
            j in 1: num_sink_pts, # Iterate over sink points
            k in 1: num_species#, # Iterate over number of species
    ))

    # Solving the optimization problem
    optimize!(eco2RR)

    # Set objective value
    obj_value = JuMP.objective_value(eco2RR)

    #OPTIMIZATION SOLUTION WRANGLING
    #--------------------
    println("Optimizer termination status:")
    println(termination_status(eco2RR))
    println(primal_status(eco2RR))
    println("Objective function value:")
    println(obj_value)

    #Output filepath and variable name
    #--------------------

    C2H4_result_filepath = joinpath(@__DIR__, "results/flows/C2H4_flows.csv")
    HCOO_result_filepath = joinpath(@__DIR__, "results/flows/HCOO_flows.csv")
    CO_result_filepath = joinpath(@__DIR__, "results/flows/CO_flows.csv")

    C2H4_NPV_filepath = joinpath(@__DIR__, "results/npv/C2H4_NPV.csv")
    HCOO_NPV_filepath = joinpath(@__DIR__, "results/npv/HCOO_NPV.csv")
    CO_NPV_filepath = joinpath(@__DIR__, "results/npv/CO_NPV.csv")

    C2H4_trans_filepath = joinpath(@__DIR__, "results/econ/trans/C2H4_trans.csv")
    HCOO_trans_filepath = joinpath(@__DIR__, "results/econ/trans/HCOO_trans.csv")
    CO_trans_filepath = joinpath(@__DIR__, "results/econ/trans/CO_trans.csv")

    C2H4_tea_filepath = joinpath(@__DIR__, "results/econ/tea/C2H4_tea.csv")
    HCOO_tea_filepath = joinpath(@__DIR__, "results/econ/tea/HCOO_tea.csv")
    CO_tea_filepath = joinpath(@__DIR__, "results/econ/tea/CO_tea.csv")

    C2H4_ccu_filepath = joinpath(@__DIR__, "results/econ/ccu/C2H4_ccu.csv")
    HCOO_ccu_filepath = joinpath(@__DIR__, "results/econ/ccu/HCOO_ccu.csv")
    CO_ccu_filepath = joinpath(@__DIR__, "results/econ/ccu/CO_ccu.csv")

    C2H4_elec_filepath = joinpath(@__DIR__, "results/econ/electricity/C2H4_elec.csv")
    HCOO_elec_filepath = joinpath(@__DIR__, "results/econ/electricity/HCOO_elec.csv")
    CO_elec_filepath = joinpath(@__DIR__, "results/econ/electricity/CO_elec.csv")

    C2H4_prf_filepath = joinpath(@__DIR__, "results/econ/prf/C2H4_prf.csv")
    HCOO_prf_filepath = joinpath(@__DIR__, "results/econ/prf/HCOO_prf.csv")
    CO_prf_filepath = joinpath(@__DIR__, "results/econ/prf/CO_prf.csv")

    C2H4_water_rsrc_filepath = joinpath(@__DIR__, "results/resource/water/C2H4_water_rsrc.csv")
    HCOO_water_rsrc_filepath = joinpath(@__DIR__, "results/resource/water/HCOO_water_rsrc.csv")
    CO_water_rsrc_filepath = joinpath(@__DIR__, "results/resource/water/CO_water_rsrc.csv")

    C2H4_power_filepath = joinpath(@__DIR__, "results/resource/power/C2H4_power.csv")
    HCOO_power_filepath = joinpath(@__DIR__, "results/resource/power/HCOO_power.csv")
    CO_power_filepath = joinpath(@__DIR__, "results/resource/power/CO_power.csv")

    C2H4_electrolyzer_filepath = joinpath(@__DIR__, "results/econ/electrolyzer/C2H4_electrolyzer.csv")
    HCOO_electrolyzer_filepath = joinpath(@__DIR__, "results/econ/electrolyzer/HCOO_electrolyzer.csv")
    CO_electrolyzer_filepath = joinpath(@__DIR__, "results/econ/electrolyzer/CO_electrolyzer.csv")

    C2H4_separations_filepath = joinpath(@__DIR__, "results/econ/separations/C2H4_separations.csv")
    HCOO_separations_filepath = joinpath(@__DIR__, "results/econ/separations/HCOO_separations.csv")
    CO_separations_filepath = joinpath(@__DIR__, "results/econ/separations/CO_separations.csv")

    C2H4_battery_filepath = joinpath(@__DIR__, "results/econ/battery/C2H4_battery.csv")
    HCOO_battery_filepath = joinpath(@__DIR__, "results/econ/battery/HCOO_battery.csv")
    CO_battery_filepath = joinpath(@__DIR__, "results/econ/battery/CO_battery.csv")

    C2H4_maintenance_filepath = joinpath(@__DIR__, "results/econ/maintenance/C2H4_maintenance.csv")
    HCOO_maintenance_filepath = joinpath(@__DIR__, "results/econ/maintenance/HCOO_maintenance.csv")
    CO_maintenance_filepath = joinpath(@__DIR__, "results/econ/maintenance/CO_maintenance.csv")

    C2H4_water_filepath = joinpath(@__DIR__, "results/econ/water/C2H4_water.csv")
    HCOO_water_filepath = joinpath(@__DIR__, "results/econ/water/HCOO_water.csv")
    CO_water_filepath = joinpath(@__DIR__, "results/econ/water/CO_water.csv")

    # Initialize flow output array
    flow_results = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Populate each output array with JuMP varaible value found in optimizer
    for i = 1:num_source_pts
        for j = 1:num_sink_pts
            for k in 1:num_species
                flow_results[i, j, k] = JuMP.value(flow[i, j, k])
            end
        end
    end

    # Initialize NPV output arrays for each molecule
    results_NPV = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_elec = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_trans = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_ccu = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_tea = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_prf = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize water consumption output arrays for each molecule
    results_water = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize power consumption output arrays for each molecule
    results_power = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize electrolyzer cost output arrays for each molecule
    results_electrolyzer = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize separations cost output arrays for each molecule
    results_separations = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize battery cost output arrays for each molecule
    results_battery = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize electrolyzer cost output arrays for each molecule
    results_maintenance = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize water cost output arrays for each molecule
    results_water_rsrc = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Populate each output array with JuMP varaible value found in optimizer
    for i = 1:num_source_pts
        for j = 1:num_sink_pts
            for k = 1:num_species
                results_NPV[i, j, k] = sum(
                (
                - (D[i, j, k] * transport_A + transport_B) * flow_results[i, j, k] # Transportation cost
                -  electrolyzer_TEA(
                    flow_results[i, j, k],
                    product_phase[k],
                    product_mw[k],
                    product_density[k],
                    electrons_product[k],
                    electrons_co2[k],
                    system_potential[k],
                    selectivity[k],
                    hours_year,
                    current_density[k],
                    single_pass_conversion[k],
                    electrolyzer_cost,
                    electricity_price[i],
                    water_price,
                    water_rate[k],
                    crf)  # TEA system cost
                - flow_results[i, j, k] * carbons[k] * C_mw / product_mw[k] * CO2_cap_cost[i] # Carbon capture cost
                - flow_results[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F * system_potential[k] / selectivity[k] / 1e3 * hours_year * electricity_price[i] # Electrochemical electricity price
                + flow_results[i, j, k] * value[k] # Product value
                )
                )

                if flow_results[i, j, k] != 0
                    results_electrolyzer[i, j, k],
                    results_separations[i, j, k],
                    results_battery[i, j, k],
                    results_maintenance[i, j, k],
                    results_water[i, j, k] =
                    electrolyzer_TEA_array(
                        flow_results[i, j, k],
                        product_phase[k],
                        product_mw[k],
                        product_density[k],
                        electrons_product[k],
                        electrons_co2[k],
                        system_potential[k],
                        selectivity[k],
                        hours_year,
                        current_density[k],
                        single_pass_conversion[k],
                        electrolyzer_cost,
                        electricity_price[i],
                        water_price,
                        water_rate[k],
                        crf)  # TEA system cost
                    results_trans[i, j, k] = (D[i, j, k] * transport_A + transport_B) * flow_results[i, j, k] # Transportation cost
                    results_tea[i, j, k] =  electrolyzer_TEA(
                        flow_results[i, j, k],
                        product_phase[k],
                        product_mw[k],
                        product_density[k],
                        electrons_product[k],
                        electrons_co2[k],
                        system_potential[k],
                        selectivity[k],
                        hours_year,
                        current_density[k],
                        single_pass_conversion[k],
                        electrolyzer_cost,
                        electricity_price[i],
                        water_price,
                        water_rate[k],
                        crf)  # TEA system cost
                    results_ccu[i, j, k] =  flow_results[i, j, k] * carbons[k] * C_mw / product_mw[k] * CO2_cap_cost[i] # Carbon capture cost
                    results_elec[i, j, k] =  flow_results[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F * system_potential[k] / selectivity[k] / 1e3 * hours_year * electricity_price[i] # Electrochemical electricity price
                    results_prf[i, j, k] = flow_results[i, j, k] * value[k] # Product value
                    results_water_rsrc[i, j, k] = flow_results[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F / selectivity[k] / H2O_electrons / F * H2O_mw / 1e3 * hours_year * 3600
                    results_power[i, j, k] = flow_results[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F * system_potential[k] / selectivity[k] / 1e3 #kW
                end
            end
        end
    end

    # Write sink/source data to CSV files for data visualization
    # Useful for runs where points are excluded from optimization routine
    source_index_path = joinpath(@__DIR__, "results/index/source_index.csv")
    sink_index_path = joinpath(@__DIR__, "results/index/sink_index.csv")

    CSV.write(source_index_path, DataFrame(source_pts))
    CSV.write(sink_index_path, DataFrame(sink_pts))

    # Write output flow files to CSV
    CSV.write(C2H4_result_filepath, DataFrame(flow_results[:, :, 1]))
    CSV.write(HCOO_result_filepath, DataFrame(flow_results[:, :, 2]))
    CSV.write(CO_result_filepath, DataFrame(flow_results[:, :, 3]))

    # write output NPV dataframes to CSV
    CSV.write(C2H4_NPV_filepath, DataFrame(results_NPV[:, :, 1]))
    CSV.write(HCOO_NPV_filepath, DataFrame(results_NPV[:, :, 2]))
    CSV.write(CO_NPV_filepath, DataFrame(results_NPV[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_trans_filepath, DataFrame(results_trans[:, :, 1]))
    CSV.write(HCOO_trans_filepath, DataFrame(results_trans[:, :, 2]))
    CSV.write(CO_trans_filepath, DataFrame(results_trans[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_tea_filepath, DataFrame(results_tea[:, :, 1]))
    CSV.write(HCOO_tea_filepath, DataFrame(results_tea[:, :, 2]))
    CSV.write(CO_tea_filepath, DataFrame(results_tea[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_ccu_filepath, DataFrame(results_ccu[:, :, 1]))
    CSV.write(HCOO_ccu_filepath, DataFrame(results_ccu[:, :, 2]))
    CSV.write(CO_ccu_filepath, DataFrame(results_ccu[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_elec_filepath, DataFrame(results_elec[:, :, 1]))
    CSV.write(HCOO_elec_filepath, DataFrame(results_elec[:, :, 2]))
    CSV.write(CO_elec_filepath, DataFrame(results_elec[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_prf_filepath, DataFrame(results_prf[:, :, 1]))
    CSV.write(HCOO_prf_filepath, DataFrame(results_prf[:, :, 2]))
    CSV.write(CO_prf_filepath, DataFrame(results_prf[:, :, 3]))

    # write output water consumption dataframes to CSV
    CSV.write(C2H4_water_rsrc_filepath, DataFrame(results_water_rsrc[:, :, 1]))
    CSV.write(HCOO_water_rsrc_filepath, DataFrame(results_water_rsrc[:, :, 2]))
    CSV.write(CO_water_rsrc_filepath, DataFrame(results_water_rsrc[:, :, 3]))

    # write output power consumption dataframes to CSV
    CSV.write(C2H4_power_filepath, DataFrame(results_power[:, :, 1]))
    CSV.write(HCOO_power_filepath, DataFrame(results_power[:, :, 2]))
    CSV.write(CO_power_filepath, DataFrame(results_power[:, :, 3]))

    # write output electrolyzer cost dataframes to CSV
    CSV.write(C2H4_electrolyzer_filepath, DataFrame(results_electrolyzer[:, :, 1]))
    CSV.write(HCOO_electrolyzer_filepath, DataFrame(results_electrolyzer[:, :, 2]))
    CSV.write(CO_electrolyzer_filepath, DataFrame(results_electrolyzer[:, :, 3]))

    # write output separations cost dataframes to CSV
    CSV.write(C2H4_separations_filepath, DataFrame(results_separations[:, :, 1]))
    CSV.write(HCOO_separations_filepath, DataFrame(results_separations[:, :, 2]))
    CSV.write(CO_separations_filepath, DataFrame(results_separations[:, :, 3]))

    # write output battery cost dataframes to CSV
    CSV.write(C2H4_battery_filepath, DataFrame(results_battery[:, :, 1]))
    CSV.write(HCOO_battery_filepath, DataFrame(results_battery[:, :, 2]))
    CSV.write(CO_battery_filepath, DataFrame(results_battery[:, :, 3]))

    # write output maintenance cost dataframes to CSV
    CSV.write(C2H4_maintenance_filepath, DataFrame(results_maintenance[:, :, 1]))
    CSV.write(HCOO_maintenance_filepath, DataFrame(results_maintenance[:, :, 2]))
    CSV.write(CO_maintenance_filepath, DataFrame(results_maintenance[:, :, 3]))

    # write output water cost dataframes to CSV
    CSV.write(C2H4_water_filepath, DataFrame(results_water[:, :, 1]))
    CSV.write(HCOO_water_filepath, DataFrame(results_water[:, :, 2]))
    CSV.write(CO_water_filepath, DataFrame(results_water[:, :, 3]))

    println("Optimization Routine Completed!")

end

@time co2_optimizer()


# hash
hash(c::AbstractGray, hx::UInt) = hash(gray(c), hx)
hash(c::TransparentGray, hx::UInt) = hash(alpha(c), hash(gray(c), hx))
hash(c::AbstractRGB, hx::UInt) = hash(blue(c), hash(green(c), hash(red(c), hx)))
hash(c::TransparentRGB, hx::UInt) = hash(alpha(c), hash(blue(c), hash(green(c), hash(red(c), hx))))

Base.adjoint(c::Colorant) = c

# gamut{min,max}
gamutmax(::Type{T}) where {T<:HSV} = (360,1,1)
gamutmin(::Type{T}) where {T<:HSV} = (0,0,0)
gamutmax(::Type{T}) where {T<:HSL} = (360,1,1)
gamutmin(::Type{T}) where {T<:HSL} = (0,0,0)
gamutmax(::Type{T}) where {T<:Lab} = (100,128,128)
gamutmin(::Type{T}) where {T<:Lab} = (0,-127,-127)
gamutmax(::Type{T}) where {T<:LCHab} = (100,1,360)
gamutmin(::Type{T}) where {T<:LCHab} = (0,0,0)
gamutmax(::Type{T}) where {T<:YIQ} = (1,0.5226,0.5226)
gamutmin(::Type{T}) where {T<:YIQ} = (0,-0.5957,-0.5957)

gamutmax(::Type{T}) where {T<:AbstractGray} = (1,)
gamutmax(::Type{T}) where {T<:TransparentGray} = (1,1)
gamutmax(::Type{T}) where {T<:AbstractRGB} = (1,1,1)
gamutmax(::Type{T}) where {T<:TransparentRGB} = (1,1,1,1)
gamutmin(::Type{T}) where {T<:AbstractGray} = (0,)
gamutmin(::Type{T}) where {T<:TransparentGray} = (0,0)
gamutmin(::Type{T}) where {T<:AbstractRGB} = (0,0,0)
gamutmin(::Type{T}) where {T<:TransparentRGB} = (0,0,0,0)

# rand
for t in [Float16,Float32,Float64,N0f8,N0f16,N0f32]
    @eval _rand(::Type{T}) where {T<:Union{AbstractRGB{$t},AbstractGray{$t}}} =
          mapc(x->rand(eltype(T)), base_colorant_type(T)())
end

function _rand(::Type{T}) where T<:Colorant
    Gmax = gamutmax(T)
    Gmin = gamutmin(T)
    Mi = eltype(T) <: FixedPoint ? 1.0/typemax(eltype(T)) : 1.0
    A = rand(eltype(T), length(T))
    for j in eachindex(Gmax)
        A[j] = A[j] * (Mi * (Gmax[j]-Gmin[j])) + Gmin[j]
    end
    T(A...)
end

if Base.VERSION < v"1.6.0-DEV.1083"
    reinterpretc(::Type{T}, A::Array{C}) where {T<:Real,C<:AbstractGray} = reinterpret(T, A)
    reinterpretc(::Type{T}, A::Array{C}) where {T<:Real,C<:Colorant} = reshape(reinterpret(T, A), (sizeof(C)÷sizeof(T), size(A)...))
else
    reinterpretc(::Type{T}, A::Array{C}) where {T<:Real,C<:Colorant} = reinterpret(reshape, T, A)
end

function _rand(::Type{T}, sz::Dims) where T<:Colorant
    Mi = eltype(T) <: FixedPoint ? 1.0/typemax(eltype(T)) : 1.0
    A = Array{T}(undef, sz)
    Tr = eltype(T) <: FixedPoint ? FixedPointNumbers.rawtype(eltype(T)) : eltype(T)
    nchannels = sizeof(T)÷sizeof(eltype(T))
    Au = Random.UnsafeView(convert(Ptr{Tr}, pointer(A)), length(A)*nchannels)
    rand!(Au)

    Gmax = gamutmax(T)
    Gmin = gamutmin(T)
    s = (Gmax .- Gmin) .* Mi
    if !all(iszero, Gmin) || !all(==(1), s)
        Ar = reinterpretc(eltype(T), A)
        if T<:AbstractGray
            s1, offset1 = s[1], Gmin[1]
            for I in CartesianIndices(A)
                Ar[I] = Ar[I] * s1 + offset1
            end
        else
            for I in CartesianIndices(A)
                for j in eachindex(s)
                    Ar[j,I] = Ar[j,I] * s[j] + Gmin[j]
                end
            end
        end
    end
    return A
end

rand(::Type{T}, sz::Dims...) where {T<:Colorant} = _rand(ccolor(T, base_colorant_type(T){Float64}), sz...)

rand(::Type{Gray24}) = Gray24(rand(N0f8))
rand(::Type{Gray24}, sz::Dims) = Gray24.(rand(N0f8,sz))
rand(::Type{AGray32}) = AGray32(rand(N0f8),rand(N0f8))
rand(::Type{AGray32}, sz::Dims) = AGray32.(rand(N0f8,sz),rand(N0f8,sz))

# broadcast

# Mapping a function over color channels
"""
    mapc(f, rgb) -> rgbf
    mapc(f, rgb1, rgb2) -> rgbf

`mapc` applies the function `f` to each color channel of the input
color(s), returning an output color in the same colorspace.

# Examples:

    julia> mapc(x->clamp(x,0,1), RGB(-0.2,0.3,1.2))
    RGB{Float64}(0.0,0.3,1.0)

    julia> mapc(max, RGB(0.1,0.8,0.3), RGB(0.5,0.5,0.5))
    RGB{Float64}(0.5,0.8,0.5)

    julia> mapc(+, RGB(0.1,0.8,0.3), RGB(0.5,0.5,0.5))
    RGB{Float64}(0.6,1.3,0.8)
"""
@inline mapc(f, c::C) where {C<:ColorantN{1}} =
    base_colorant_type(C)(f(comp1(c)))
@inline mapc(f, c::C) where {C<:ColorantN{2}} =
    base_colorant_type(C)(f(comp1(c)), f(comp2(c)))
@inline mapc(f, c::C) where {C<:ColorantN{3}} =
    base_colorant_type(C)(f(comp1(c)), f(comp2(c)), f(comp3(c)))
@inline mapc(f, c::C) where {C<:ColorantN{4}} =
    base_colorant_type(C)(f(comp1(c)), f(comp2(c)), f(comp3(c)), f(comp4(c)))
@inline mapc(f, c::C) where {C<:ColorantN{5}} =
    base_colorant_type(C)(f(comp1(c)), f(comp2(c)), f(comp3(c)), f(comp4(c)), f(comp5(c)))

@inline mapc(f, x::Number) = f(x)

mapc(f::F, x, y) where F = _mapc(_same_colorspace(x,y), f, x, y)
_mapc(::Type{C}, f, x::ColorantN{1}, y::ColorantN{1}) where C =
    C(f(comp1(x), comp1(y)))
_mapc(::Type{C}, f, x::ColorantN{2}, y::ColorantN{2}) where C =
    C(f(comp1(x), comp1(y)), f(comp2(x), comp2(y)))
_mapc(::Type{C}, f, x::ColorantN{3}, y::ColorantN{3}) where C =
    C(f(comp1(x), comp1(y)), f(comp2(x), comp2(y)), f(comp3(x), comp3(y)))
_mapc(::Type{C}, f, x::ColorantN{4}, y::ColorantN{4}) where C =
    C(f(comp1(x), comp1(y)), f(comp2(x), comp2(y)), f(comp3(x), comp3(y)), f(comp4(x), comp4(y)))
_mapc(::Type{C}, f, x::ColorantN{5}, y::ColorantN{5}) where C =
    C(f(comp1(x), comp1(y)), f(comp2(x), comp2(y)), f(comp3(x), comp3(y)), f(comp4(x), comp4(y)), f(comp5(x), comp5(y)))

_same_colorspace(x::Colorant, y::Colorant) = _same_colorspace(base_colorant_type(x),
                                                              base_colorant_type(y))
_same_colorspace(::Type{C}, ::Type{C}) where {C<:Colorant} = C
@noinline _same_colorspace(::Type{C1}, ::Type{C2}) where {C1<:Colorant,C2<:Colorant} =
    throw(ArgumentError("$C1 and $C2 are from different colorspaces"))

"""
    reducec(op, v0, c)

Reduce across color channels of `c` with the binary operator
`op`. `v0` is the neutral element used to initiate the reduction. For
grayscale,

    reducec(op, v0, c::Gray) = op(v0, comp1(c))

whereas for RGB

    reducec(op, v0, c::RGB) = op(comp3(c), op(comp2(c), op(v0, comp1(c))))

If `c` has an alpha channel, it is always the last one to be folded into the reduction.
"""
@inline reducec(op, v0, c::ColorantN{1}) = op(v0, comp1(c))
@inline reducec(op, v0, c::ColorantN{2}) = op(comp2(c), op(v0, comp1(c)))
@inline reducec(op, v0, c::ColorantN{3}) = op(comp3(c), op(comp2(c), op(v0, comp1(c))))
@inline reducec(op, v0, c::ColorantN{4}) = op(comp4(c), op(comp3(c), op(comp2(c), op(v0, comp1(c)))))
@inline reducec(op, v0, c::ColorantN{5}) = op(comp5(c), op(comp4(c), op(comp3(c), op(comp2(c), op(v0, comp1(c))))))

@inline reducec(op, v0, x::Number) = op(v0, x)

"""
    mapreducec(f, op, v0, c)

Reduce across color channels of `c` with the binary operator `op`,
first applying `f` to each channel. `v0` is the neutral element used
to initiate the reduction. For grayscale,

    mapreducec(f, op, v0, c::Gray) = op(v0, f(comp1(c)))

whereas for RGB

    mapreducec(f, op, v0, c::RGB) = op(f(comp3(c)), op(f(comp2(c)), op(v0, f(comp1(c)))))

If `c` has an alpha channel, it is always the last one to be folded into the reduction.
"""
@inline mapreducec(f, op, v0, c::ColorantN{1}) = op(v0, f(comp1(c)))
@inline mapreducec(f, op, v0, c::ColorantN{2}) = op(f(comp2(c)), op(v0, f(comp1(c))))
@inline mapreducec(f, op, v0, c::ColorantN{3}) =
    op(f(comp3(c)), op(f(comp2(c)), op(v0, f(comp1(c)))))
@inline mapreducec(f, op, v0, c::ColorantN{4}) =
    op(f(comp4(c)), op(f(comp3(c)), op(f(comp2(c)), op(v0, f(comp1(c))))))
@inline mapreducec(f, op, v0, c::ColorantN{5}) =
    op(f(comp5(c)), op(f(comp4(c)), op(f(comp3(c)), op(f(comp2(c)), op(v0, f(comp1(c)))))))

@inline mapreducec(f, op, v0, x::Number) = op(v0, f(x))


export iauDtf2d
"""
Encode date and time fields into 2-part Julian Date (or in the case
of UTC a quasi-JD form that includes special provision for leap
seconds).

This function is part of the International Astronomical Union's
SOFA (Standards of Fundamental Astronomy) software collection.

Status:  support function.

Given:
   scale     char[]  time scale ID (Note 1)
   iy,im,id  int     year, month, day in Gregorian calendar (Note 2)
   ihr,imn   int     hour, minute
   sec       double  seconds

Returned:
   d1,d2     double  2-part Julian Date (Notes 3,4)

Returned (function value):
            int     status: +3 = both of next two
                              +2 = time is after end of day (Note 5)
                              +1 = dubious year (Note 6)
                              0 = OK
                              -1 = bad year
                              -2 = bad month
                              -3 = bad day
                              -4 = bad hour
                              -5 = bad minute
                              -6 = bad second (<0)

Notes:

   1. scale identifies the time scale.  Only the value "UTC" (in upper
   case) is significant, and enables handling of leap seconds (see
   Note 4).

   2. For calendar conventions and limitations, see iauCal2jd.

   3. The sum of the results, d1+d2, is Julian Date, where normally d1
   is the Julian Day Number and d2 is the fraction of a day.  In the
   case of UTC, where the use of JD is problematical, special
   conventions apply:  see the next note.

   4. JD cannot unambiguously represent UTC during a leap second unless
   special measures are taken.  The SOFA internal convention is that
   the quasi-JD day represents UTC days whether the length is 86399,
   86400 or 86401 SI seconds.  In the 1960-1972 era there were
   smaller jumps (in either direction) each time the linear UTC(TAI)
   expression was changed, and these "mini-leaps" are also included
   in the SOFA convention.

   5. The warning status "time is after end of day" usually means that
   the sec argument is greater than 60.0.  However, in a day ending
   in a leap second the limit changes to 61.0 (or 59.0 in the case
   of a negative leap second).

   6. The warning status "dubious year" flags UTCs that predate the
   introduction of the time scale or that are too far in the future
   to be trusted.  See iauDat for further details.

   7. Only in the case of continuous and regular time scales (TAI, TT,
   TCG, TCB and TDB) is the result d1+d2 a Julian Date, strictly
   speaking.  In the other cases (UT1 and UTC) the result must be
   used with circumspection;  in particular the difference between
   two such results cannot be interpreted as a precise time
   interval.

Called:
   iauCal2jd    Gregorian calendar to JD
   iauDat       delta(AT) = TAI-UTC
   iauJd2cal    JD to Gregorian calendar

This revision:  2013 July 26

SOFA release 2018-01-30

Copyright (C) 2018 IAU SOFA Board.  See notes at end.
"""
# int iauDtf2d(const char *scale, int iy, int im, int id,
#              int ihr, int imn, double sec, double *d1, double *d2)
function iauDtf2d(scale::String, iy::Real, im::Real, id::Real, ihr::Real, imn::Real, sec::Real)
   ref_d1 = Ref{Float64}(0.0)
   ref_d2 = Ref{Float64}(0.0)

   status = ccall((:iauDtf2d, libsofa_c), Cint,
                  (Cstring, Cint, Cint, Cint, Cint, Cint, Cdouble, Ref{Cdouble}, Ref{Cdouble}),
                  scale, convert(Int32, iy), convert(Int32, im), convert(Int32, id),
                  convert(Int32, ihr), convert(Int32, imn), convert(Float64, sec), ref_d1, ref_d2)

   if status != 0
      @warn "Non-zero return code form iauDtf2d"
   end

   return status, ref_d1[], ref_d2[]
end


"""
`module  JAC.Statistical`
    ... a submodel of JAC that contains all structs and methods to deal with time-dependent statistical tensors.
"""
module Statistical

    using ..ManyElectron

    """
    `struct  Statistical.ResonanceR`  ... defines a type for a resonance state in the continuum with a well-defined bound-ionic core,
             one or several electrons in the continuum, a widths as well as a loss rate due to additional decay processes.

        + isBound            ::Bool                ... True if it just refers to a bound state with no electron in the continuum.
        + ionLevel           ::Level               ... Level of the bound-state core.
        + widths             ::Float64             ... Widths of the resonance state.
        + lossRate           ::Float64             ... Loss rate due to processes that are not considered explicitly in the time evolution.
    """
    struct ResonanceR
        isBound              ::Bool
        ionLevel             ::Level
        widths               ::Float64
        lossRate             ::Float64
    end

    """
    `Statistical.ResonanceR()`  ... constructor for an `empty` instance of Statistical.ResonanceR().
    """
    function ResonanceR()
        ResonanceR( true, Level(), 0., 0. )
    end

    # `Base.show(io::IO, resonance::Statistical.ResonanceR)`  ... prepares a proper printout of the variable resonance::Statistical.ResonanceR.
    function Base.show(io::IO, resonance::Statistical.ResonanceR)
        println(io, "isBound:              $(resonance.isBound)  ")
        println(io, "ionLevel:             $(resonance.ionLevel)  ")
        println(io, "widths:               $(resonance.widths)  ")
        println(io, "lossRate:             $(resonance.lossRate)  ")
    end

    """
    `struct  Statistical.Tensor`  ... defines a type for a statistical tensor of given rank k, projection q and w.r.t. two resonances.

        + k                  ::AngularJ64          ... Rank of the tensor
        + q                  ::AngularM64          ... Projection of the tensor rank.
        + a                  ::ResonanceR          ... Level/resonance a with total symmetry alpha J.
        + b                  ::ResonanceR          ... Level/resonance a with total symmetry alpha' J'.
        + value              ::Complex{Float64}    ... value of the statistical tensor
    """
    struct Tensor
        k                    ::AngularJ64
        q                    ::AngularM64
        a                    ::ResonanceR
        b                    ::ResonanceR
        value                ::Complex{Float64}
    end

    """
    `Statistical.Tensor()`  ... constructor for an `empty` instance of Statistical.Tensor().
    """
    function Tensor()
        Tensor( AngularJ64(0), AngularM64(0), ResonanceR(), ResonanceR(), Complex(0.) )
    end

    # `Base.show(io::IO, tensor::Statistical.Tensor)`  ... prepares a proper printout of the variable tensor::Statistical.Tensor.
    function Base.show(io::IO, tensor::Statistical.Tensor)
        println(io, "k:              $(tensor.k)  ")
        println(io, "q:              $(tensor.q)  ")
        println(io, "a:              $(tensor.a)  ")
        println(io, "b:              $(tensor.b)  ")
        println(io, "value:          $(tensor.value)  ")
    end

end # module


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/Materials.jl/blob/master/LICENSE

module Materials

abstract type AbstractMaterial end
abstract type AbstractMaterialState end

export AbstractMaterial, AbstractMaterialState
export integrate_material!, update_material!, reset_material!

"""
    :+(a::T, b::T) where T <: AbstractMaterialState

Fieldwise addition for material states.
"""
@generated function Base.:+(a::T, b::T) where T <: AbstractMaterialState
   expr = [:(a.$p + b.$p) for p in fieldnames(T)]
   return :(T($(expr...)))
end

"""
    integrate_material!(material::AbstractMaterial)

Integrate one timestep. The input `material.variables` represents the old
problem state.

Abstract method. Must be implemented for each material type. When integration is
done, the method **must** write the new state into `material.variables_new`.

**Do not** write into `material.variables`; actually committing the timestep
(i.e. accepting that one step of time evolution and applying it permanently)
is the job of `update_material!`.
"""
function integrate_material!(material::M) where M <: AbstractMaterial
    error("One needs to define how to integrate material $M!")
end

"""
    update_material!(material::AbstractMaterial)

Commit the result of `integrate_material!`.

In `material`, we add `ddrivers` into `drivers`, `dparameters` into
`parameters`, and replace `variables` by `variables_new`. Then we
automatically invoke `reset_material!`.
"""
function update_material!(material::AbstractMaterial)
    material.drivers += material.ddrivers
    material.parameters += material.dparameters
    material.variables = material.variables_new
    reset_material!(material)
    return nothing
end

"""
    reset_material!(material::AbstractMaterial)

In `material`, we zero out `ddrivers`, `dparameters` and `variables_new`. This
clears out the tentative state produced when a timestep has been computed, but
has not yet been committed.

Used internally by `update_material!`.
"""
function reset_material!(material::AbstractMaterial)
    material.ddrivers = typeof(material.ddrivers)()
    material.dparameters = typeof(material.dparameters)()
    material.variables_new = typeof(material.variables_new)()
    return nothing
end

include("utilities.jl")
using .Utilities
export Symm2, Symm4
export delta, II, IT, IS, IA, IV, ID, isotropic_elasticity_tensor, isotropic_compliance_tensor
export lame, delame, debang, find_root

include("perfectplastic.jl")
using .PerfectPlasticModule
export PerfectPlastic, PerfectPlasticDriverState, PerfectPlasticParameterState, PerfectPlasticVariableState

include("chaboche.jl")
using .ChabocheModule
export Chaboche, ChabocheDriverState, ChabocheParameterState, ChabocheVariableState

include("memory.jl")
using .MemoryModule
export Memory, MemoryDriverState, MemoryParameterState, MemoryVariableState

include("DSA.jl")
using .DSAModule
export DSA, DSADriverState, DSAParameterState, DSAVariableState

include("increments.jl")
using .Increments
export uniaxial_increment!, biaxial_increment!, stress_driven_uniaxial_increment!,
       general_increment!, stress_driven_general_increment!, find_dstrain!

end


# tools for the regularization of the system.

# update regularization values in classic mode if there is a failure during factorization
function update_regu_trycatch!(regu, cnts, T, T0)
  T == Float32 && return 1
  T0 == Float128 && T == Float64 && return 1
  if cnts.c_pdd == 0 && cnts.c_catch == 0
    regu.δ *= T(1e2)
    regu.δ_min *= T(1e2)
    regu.ρ *= T(1e5)
    regu.ρ_min *= T(1e5)
  elseif cnts.c_pdd == 0 && cnts.c_catch != 0
    regu.δ *= T(1e1)
    regu.δ_min *= T(1e1)
    regu.ρ *= T(1e0)
    regu.ρ_min *= T(1e0)
  elseif cnts.c_pdd != 0 && cnts.c_catch == 0
    regu.δ *= T(1e5)
    regu.δ_min *= T(1e5)
    regu.ρ *= T(1e5)
    regu.ρ_min *= T(1e5)
  else
    regu.δ *= T(1e1)
    regu.δ_min *= T(1e1)
    regu.ρ *= T(1e1)
    regu.ρ_min *= T(1e1)
  end
  return 0
end

function update_regu!(regu)
  if regu.δ >= regu.δ_min
    regu.δ /= 10
  end
  if regu.ρ >= regu.ρ_min
    regu.ρ /= 10
  end
end

# update regularization, and corrects if the magnitude of the diagonal of the matrix is too high
function update_regu_diagK2!(regu, K_nzval, diagind_K, nvar, pdd, l_pdd, mean_pdd, cnts, T, T0)
  l_pdd[cnts.k % 6 + 1] = pdd
  mean_pdd = mean(l_pdd)

  if T == Float64 &&
     cnts.k > 10 &&
     mean_pdd != zero(T) &&
     std(l_pdd ./ mean_pdd) < T(1e-2) &&
     cnts.c_pdd < 5
    regu.δ_min /= 10
    regu.δ /= 10
    cnts.c_pdd += 1
  end
  if T == Float64 &&
     cnts.k > 10 &&
     cnts.c_catch <= 1 &&
     @views minimum(K_nzval[diagind_K[1:nvar]]) < -one(T) / regu.δ / T(1e-6)
    regu.δ /= 10
    regu.δ_min /= 10
    cnts.c_pdd += 1
  elseif T != T0 &&
         cnts.c_pdd < 2 &&
         @views minimum(K_nzval[diagind_K[1:nvar]]) < -one(T) / regu.δ / T(1e-5)
    return 1
  elseif T == Float128 &&
         cnts.k > 10 &&
         cnts.c_catch <= 1 &&
         @views minimum(K_nzval[diagind_K[1:nvar]]) < -one(T) / regu.δ / T(1e-15)
    regu.δ /= 10
    regu.δ_min /= 10
    cnts.c_pdd += 1
  end

  update_regu!(regu)

  return 0
end

function update_regu_diagK2_5!(regu, D, pdd, l_pdd, mean_pdd, cnts, T, T0)
  l_pdd[cnts.k % 6 + 1] = pdd
  mean_pdd = mean(l_pdd)

  if T == Float64 &&
     cnts.k > 10 &&
     mean_pdd != zero(T) &&
     std(l_pdd ./ mean_pdd) < T(1e-2) &&
     cnts.c_pdd < 5
    regu.δ_min /= 10
    regu.δ /= 10
    cnts.c_pdd += 1
  end
  if T == Float64 &&
     cnts.k > 10 &&
     cnts.c_catch <= 1 &&
     @views minimum(D) < -one(T) / regu.δ / T(1e-6)
    regu.δ /= 10
    regu.δ_min /= 10
    cnts.c_pdd += 1
  elseif T != T0 && cnts.c_pdd < 2 && @views minimum(D) < -one(T) / regu.δ / T(1e-5)
    return 1
  elseif T == Float128 &&
         cnts.k > 10 &&
         cnts.c_catch <= 1 &&
         @views minimum(D) < -one(T) / regu.δ / T(1e-15)
    regu.δ /= 10
    regu.δ_min /= 10
    cnts.c_pdd += 1
  end

  update_regu!(regu)

  return 0
end