### BasicContUnvParameter

# Guidelines for usage of inner constructors of continuous parameter types:
# 1) Function fields have higher priority than implicitly derived definitions via the pdf field
# 2) Target-related fields have higher priority than implicitly derived likelihood+prior fields
# 3) Upto-related fields have higher priority than implicitly derived Function tuples

### BasicContUnvParameter

mutable struct BasicContUnvParameter <: Parameter{Continuous, Univariate}
  key::Symbol
  index::Integer
  pdf::Union{ContinuousUnivariateDistribution, Void}
  prior::Union{ContinuousUnivariateDistribution, Void}
  setpdf::Union{Function, Void}
  setprior::Union{Function, Void}
  loglikelihood!::Union{Function, Void}
  logprior!::Union{Function, Void}
  logtarget!::Union{Function, Void}
  gradloglikelihood!::Union{Function, Void}
  gradlogprior!::Union{Function, Void}
  gradlogtarget!::Union{Function, Void}
  tensorloglikelihood!::Union{Function, Void}
  tensorlogprior!::Union{Function, Void}
  tensorlogtarget!::Union{Function, Void}
  dtensorloglikelihood!::Union{Function, Void}
  dtensorlogprior!::Union{Function, Void}
  dtensorlogtarget!::Union{Function, Void}
  uptogradlogtarget!::Union{Function, Void}
  uptotensorlogtarget!::Union{Function, Void}
  uptodtensorlogtarget!::Union{Function, Void}
  diffmethods::Union{DiffMethods, Void}
  diffopts::Union{DiffOptions, Void}
  states::VariableStateVector
  state::ParameterState{Continuous, Univariate}

  function BasicContUnvParameter(
    key::Symbol,
    index::Integer,
    pdf::Union{ContinuousUnivariateDistribution, Void},
    prior::Union{ContinuousUnivariateDistribution, Void},
    setpdf::Union{Function, Void},
    setprior::Union{Function, Void},
    ll::Union{Function, Void},
    lp::Union{Function, Void},
    lt::Union{Function, Void},
    gll::Union{Function, Void},
    glp::Union{Function, Void},
    glt::Union{Function, Void},
    tll::Union{Function, Void},
    tlp::Union{Function, Void},
    tlt::Union{Function, Void},
    dtll::Union{Function, Void},
    dtlp::Union{Function, Void},
    dtlt::Union{Function, Void},
    uptoglt::Union{Function, Void},
    uptotlt::Union{Function, Void},
    uptodtlt::Union{Function, Void},
    diffmethods::Union{DiffMethods, Void},
    diffopts::Union{DiffOptions, Void},
    states::VariableStateVector,
    state::ParameterState{Continuous, Univariate}
  )
    args = (setpdf, setprior, ll, lp, lt, gll, glp, glt, tll, tlp, tlt, dtll, dtlp, dtlt, uptoglt, uptotlt, uptodtlt)
    fnames = fieldnames(BasicContUnvParameter)[5:21]

    # Check that all generic functions have correct signature
    for i in 1:17
      if isa(args[i], Function) &&
        isa(args[i], Function) &&
        !method_exists(args[i], (BasicContUnvParameterState, VariableStateVector))
        error("$(fnames[i]) has wrong signature")
      end
    end

    new(
      key,
      index,
      pdf,
      prior,
      setpdf,
      setprior,
      ll,
      lp,
      lt,
      gll,
      glp,
      glt,
      tll,
      tlp,
      tlt,
      dtll,
      dtlp,
      dtlt,
      uptoglt,
      uptotlt,
      uptodtlt,
      diffmethods,
      diffopts,
      states,
      state
    )
  end
end

function BasicContUnvParameter!(
  parameter::BasicContUnvParameter,
  setpdf::Union{Function, Void},
  setprior::Union{Function, Void},
  ll::Union{Function, Void},
  lp::Union{Function, Void},
  lt::Union{Function, Void},
  gll::Union{Function, Void},
  glp::Union{Function, Void},
  glt::Union{Function, Void},
  tll::Union{Function, Void},
  tlp::Union{Function, Void},
  tlt::Union{Function, Void},
  dtll::Union{Function, Void},
  dtlp::Union{Function, Void},
  dtlt::Union{Function, Void},
  uptoglt::Union{Function, Void},
  uptotlt::Union{Function, Void},
  uptodtlt::Union{Function, Void}
)
  args = (setpdf, setprior, ll, lp, lt, gll, glp, glt, tll, tlp, tlt, dtll, dtlp, dtlt, uptoglt, uptotlt, uptodtlt)

  # Define setpdf (i = 1) and setprior (i = 2)
  for (i, setter, distribution) in ((1, :setpdf, :pdf), (2, :setprior, :prior))
    setfield!(
      parameter,
      setter,
      if isa(args[i], Function)
        (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          setfield!(parameter, distribution, args[i](_state, _states))
      else
        nothing
      end
    )
  end

  # Define loglikelihood! (i = 3) and gradloglikelihood! (i = 6)
  # plfield stands for parameter likelihood-related field respectively
  for (i, plfield) in ((3, :loglikelihood!), (6, :gradloglikelihood!))
    setfield!(
      parameter,
      plfield,
      if isa(args[i], Function)
        _state::BasicContUnvParameterState -> args[i](_state, parameter.states)
      else
        nothing
      end
    )
  end

  # Define logprior! (i = 4) and gradlogprior! (i = 7)
  # ppfield and spfield stand for parameter prior-related field and state prior-related field repsectively
  for (i , ppfield, spfield, f) in ((4, :logprior!, :logprior, logpdf), (7, :gradlogprior!, :gradlogprior, gradlogpdf))
    setfield!(
    parameter,
      ppfield,
      if isa(args[i], Function)
        _state::BasicContUnvParameterState -> args[i](_state, parameter.states)
      else
        if (
            isa(parameter.prior, ContinuousUnivariateDistribution) &&
            method_exists(f, (typeof(parameter.prior), eltype(parameter.prior)))
          ) ||
          isa(args[2], Function)
          _state::BasicContUnvParameterState -> setfield!(_state, spfield, f(getfield(parameter, :prior), _state.value))
        else
          nothing
        end
      end
    )
  end

  # Define logtarget! (i = 5) and gradlogtarget! (i = 8)
  # ptfield, plfield and ppfield stand for parameter target, likelihood and prior-related field respectively
  # stfield, slfield and spfield stand for state target, likelihood and prior-related field respectively
  for (i , ptfield, plfield, ppfield, stfield, slfield, spfield, f) in (
    (5, :logtarget!, :loglikelihood!, :logprior!, :logtarget, :loglikelihood, :logprior, logpdf),
    (8, :gradlogtarget!, :gradloglikelihood!, :gradlogprior!, :gradlogtarget, :gradloglikelihood, :gradlogprior, gradlogpdf)
  )
    setfield!(
      parameter,
      ptfield,
      if isa(args[i], Function)
        _state::BasicContUnvParameterState -> args[i](_state, parameter.states)
      else
        if isa(args[i-2], Function) && isa(getfield(parameter, ppfield), Function)
          function (_state::BasicContUnvParameterState)
            getfield(parameter, plfield)(_state)
            getfield(parameter, ppfield)(_state)
            setfield!(_state, stfield, getfield(_state, slfield)+getfield(_state, spfield))
          end
        elseif (
            isa(parameter.pdf, ContinuousUnivariateDistribution) &&
            method_exists(f, (typeof(parameter.pdf), eltype(parameter.pdf)))
          ) ||
          isa(args[1], Function)
          _state::BasicContUnvParameterState -> setfield!(_state, stfield, f(getfield(parameter, :pdf), _state.value))
        else
          nothing
        end
      end
    )
  end

  # Define tensorloglikelihood! (i = 9) and dtensorloglikelihood! (i = 12)
  # plfield stands for parameter likelihood-related field respectively
  for (i, plfield) in ((9, :tensorloglikelihood!), (12, :dtensorloglikelihood!))
    setfield!(
      parameter,
      plfield,
      if isa(args[i], Function)
        _state::BasicContUnvParameterState -> args[i](_state, parameter.states)
      else
        nothing
      end
    )
  end

  # Define tensorlogprior! (i = 10) and dtensorlogprior! (i = 13)
  # ppfield stands for parameter prior-related field respectively
  for (i, ppfield) in ((10, :tensorlogprior!), (13, :dtensorlogprior!))
    setfield!(
      parameter,
      ppfield,
      if isa(args[i], Function)
        _state::BasicContUnvParameterState -> args[i](_state, parameter.states)
      else
        nothing
      end
    )
  end

  # Define tensorlogtarget! (i = 11) and dtensorlogtarget! (i = 14)
  for (i , ptfield, plfield, ppfield, stfield, slfield, spfield) in (
    (
      11,
      :tensorlogtarget!, :tensorloglikelihood!, :tensorlogprior!,
      :tensorlogtarget, :tensorloglikelihood, :tensorlogprior
    ),
    (
      14,
      :dtensorlogtarget!, :dtensorloglikelihood!, :dtensorlogprior!,
      :dtensorlogtarget, :dtensorloglikelihood, :dtensorlogprior
    )
  )
    setfield!(
      parameter,
      ptfield,
      if isa(args[i], Function)
        _state::BasicContUnvParameterState -> args[i](_state, parameter.states)
      else
        if isa(args[i-2], Function) && isa(args[i-1], Function)
          function (_state::BasicContUnvParameterState)
            getfield(parameter, plfield)(_state)
            getfield(parameter, ppfield)(_state)
            setfield!(_state, stfield, getfield(_state, slfield)+getfield(_state, spfield))
          end
        else
          nothing
        end
      end
    )
  end

  # Define uptogradlogtarget!
  setfield!(
    parameter,
    :uptogradlogtarget!,
    if isa(args[15], Function)
      _state::BasicContUnvParameterState -> args[15](_state, parameter.states)
    else
      if isa(parameter.logtarget!, Function) && isa(parameter.gradlogtarget!, Function)
        function (state::BasicContUnvParameterState)
          parameter.logtarget!(state)
          parameter.gradlogtarget!(state)
        end
      else
        nothing
      end
    end
  )

  # Define uptotensorlogtarget!
  setfield!(
    parameter,
    :uptotensorlogtarget!,
    if isa(args[16], Function)
      _state::BasicContUnvParameterState -> args[16](_state, parameter.states)
    else
      if isa(parameter.logtarget!, Function) &&
        isa(parameter.gradlogtarget!, Function) &&
        isa(parameter.tensorlogtarget!, Function)
        function (state::BasicContUnvParameterState)
          parameter.logtarget!(state)
          parameter.gradlogtarget!(state)
          parameter.tensorlogtarget!(state)
        end
      else
        nothing
      end
    end
  )

  # Define uptodtensorlogtarget!
  setfield!(
    parameter,
    :uptodtensorlogtarget!,
    if isa(args[17], Function)
      _state::BasicContUnvParameterState -> args[17](_state, parameter.states)
    else
      if isa(parameter.logtarget!, Function) &&
        isa(parameter.gradlogtarget!, Function) &&
        isa(parameter.tensorlogtarget!, Function) &&
        isa(parameter.dtensorlogtarget!, Function)
        function (state::BasicContUnvParameterState)
          parameter.logtarget!(state)
          parameter.gradlogtarget!(state)
          parameter.tensorlogtarget!(state)
          parameter.dtensorlogtarget!(state)
        end
      else
        nothing
      end
    end
  )
end

BasicContUnvParameter(key::Symbol, index::Integer=0; signature::Symbol=:high, args...) =
  BasicContUnvParameter(key, Val{signature}, index; args...)

function BasicContUnvParameter(
  key::Symbol,
  ::Type{Val{:low}},
  index::Integer=0;
  pdf::Union{ContinuousUnivariateDistribution, Void}=nothing,
  prior::Union{ContinuousUnivariateDistribution, Void}=nothing,
  setpdf::Union{Function, Void}=nothing,
  setprior::Union{Function, Void}=nothing,
  loglikelihood::Union{Function, Void}=nothing,
  logprior::Union{Function, Void}=nothing,
  logtarget::Union{Function, Void}=nothing,
  gradloglikelihood::Union{Function, Void}=nothing,
  gradlogprior::Union{Function, Void}=nothing,
  gradlogtarget::Union{Function, Void}=nothing,
  tensorloglikelihood::Union{Function, Void}=nothing,
  tensorlogprior::Union{Function, Void}=nothing,
  tensorlogtarget::Union{Function, Void}=nothing,
  dtensorloglikelihood::Union{Function, Void}=nothing,
  dtensorlogprior::Union{Function, Void}=nothing,
  dtensorlogtarget::Union{Function, Void}=nothing,
  uptogradlogtarget::Union{Function, Void}=nothing,
  uptotensorlogtarget::Union{Function, Void}=nothing,
  uptodtensorlogtarget::Union{Function, Void}=nothing,
  diffmethods::Union{DiffMethods, Void}=nothing,
  diffopts::Union{DiffOptions, Void}=nothing,
  states::VariableStateVector=VariableState[],
  state::ParameterState{Continuous, Univariate}=BasicContUnvParameterState()
)
  parameter = BasicContUnvParameter(key, index, pdf, prior, fill(nothing, 17)..., diffmethods, diffopts, states, state)

  BasicContUnvParameter!(
    parameter,
    setpdf,
    setprior,
    loglikelihood,
    logprior,
    logtarget,
    gradloglikelihood,
    gradlogprior,
    gradlogtarget,
    tensorloglikelihood,
    tensorlogprior,
    tensorlogtarget,
    dtensorloglikelihood,
    dtensorlogprior,
    dtensorlogtarget,
    uptogradlogtarget,
    uptotensorlogtarget,
    uptodtensorlogtarget
  )

  parameter
end

function BasicContUnvParameter(
  key::Symbol,
  ::Type{Val{:high}},
  index::Integer=0;
  pdf::Union{ContinuousUnivariateDistribution, Void}=nothing,
  prior::Union{ContinuousUnivariateDistribution, Void}=nothing,
  setpdf::Union{Function, Void}=nothing,
  setprior::Union{Function, Void}=nothing,
  loglikelihood::Union{Function, Expr, Void}=nothing,
  logprior::Union{Function, Expr, Void}=nothing,
  logtarget::Union{Function, Expr, Void}=nothing,
  gradloglikelihood::Union{Function, Void}=nothing,
  gradlogprior::Union{Function, Void}=nothing,
  gradlogtarget::Union{Function, Void}=nothing,
  tensorloglikelihood::Union{Function, Void}=nothing,
  tensorlogprior::Union{Function, Void}=nothing,
  tensorlogtarget::Union{Function, Void}=nothing,
  dtensorloglikelihood::Union{Function, Void}=nothing,
  dtensorlogprior::Union{Function, Void}=nothing,
  dtensorlogtarget::Union{Function, Void}=nothing,
  uptogradlogtarget::Union{Function, Void}=nothing,
  uptotensorlogtarget::Union{Function, Void}=nothing,
  uptodtensorlogtarget::Union{Function, Void}=nothing,
  nkeys::Integer=0,
  vfarg::Bool=false,
  diffopts::Union{DiffOptions, Void}=nothing,
  states::VariableStateVector=VariableState[],
  state::ParameterState{Continuous, Univariate}=BasicContUnvParameterState()
)
  inargs = (
    setpdf,
    setprior,
    loglikelihood,
    logprior,
    logtarget,
    gradloglikelihood,
    gradlogprior,
    gradlogtarget,
    tensorloglikelihood,
    tensorlogprior,
    tensorlogtarget,
    dtensorloglikelihood,
    dtensorlogprior,
    dtensorlogtarget,
    uptogradlogtarget,
    uptotensorlogtarget,
    uptodtensorlogtarget
  )

  fnames = fieldnames(BasicContUnvParameterState)[2:13]

  if vfarg
    if nkeys > 0
      if diffopts != nothing
        error("In the case of autodiff, if nkeys is not 0, then vfarg must be false")
      end
    else
      error("If vfarg=$vfarg, nkeys must be positive, got $nkeys")
    end
  else
    @assert nkeys >= 0 "If vfarg=$vfarg, nkeys must be non-negative, got $nkeys"
  end

  if diffopts != nothing
    if diffopts.mode != :forward
      error("Only :forward mode autodiff is available for univariate parameters, got $autodiff mode")
    end

    if diffopts.order != 1
      error("Derivative order must be 1, got order=$order")
    end
  end

  if diffopts != nothing
    for i in 3:5
      if isa(inargs[i], Function) && !isa(inargs[i+3], Function)
        diffopts.targets[i-2] = true
      end
    end
  end

  parameter = BasicContUnvParameter(key, index, pdf, prior, fill(nothing, 17)..., DiffMethods(), diffopts, states, state)

  outargs = Union{Function, Void}[nothing for i in 1:17]

  if vfarg
    for i in 1:2
      if isa(inargs[i], Function)
        outargs[i] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          inargs[i](Any[s.value for s in _states])
      end
    end

    for i in 3:14
      if isa(inargs[i], Function)
        outargs[i] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          setfield!(_state, fnames[i-2], inargs[i](Any[s.value for s in _states]))
      end
    end

    if isa(inargs[15], Function)
      outargs[15] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
        (_state.logtarget, _state.gradlogtarget) = inargs[15](Any[s.value for s in _states])
    end

    if isa(inargs[16], Function)
      outargs[16] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
        (_state.logtarget, _state.gradlogtarget, _state.tensorlogtarget) = inargs[16](Any[s.value for s in _states])
    end

    if isa(inargs[17], Function)
      outargs[17] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
        (_state.logtarget, _state.gradlogtarget, _state.tensorlogtarget, _state.dtensorlogtarget) =
          inargs[17](Any[s.value for s in _states])
    end
  else
    if nkeys > 0
      for i in 1:2
        if isa(inargs[i], Function)
          outargs[i] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
            inargs[i](_state.value, Any[s.value for s in _states])
        end
      end

      for i in 3:14
        if isa(inargs[i], Function)
          outargs[i] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
            setfield!(_state, fnames[i-2], inargs[i](_state.value, Any[s.value for s in _states]))
        end
      end

      if isa(inargs[15], Function)
        outargs[15] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          (_state.logtarget, _state.gradlogtarget) = inargs[15](_state.value, Any[s.value for s in _states])
      end

      if isa(inargs[16], Function)
        outargs[16] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          (_state.logtarget, _state.gradlogtarget, _state.tensorlogtarget) =
            inargs[16](_state.value, Any[s.value for s in _states])
      end

      if isa(inargs[17], Function)
        outargs[17] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          (_state.logtarget, _state.gradlogtarget, _state.tensorlogtarget, _state.dtensorlogtarget) =
            inargs[17](_state.value, Any[s.value for s in _states])
      end
    else
      for i in 1:2
        if isa(inargs[i], Function)
          outargs[i] = (_state::BasicContUnvParameterState, _states::VariableStateVector) -> inargs[i](_state.value)
        end
      end

      for i in 3:14
        if isa(inargs[i], Function)
          outargs[i] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
            setfield!(_state, fnames[i-2], inargs[i](_state.value))
        end
      end

      if isa(inargs[15], Function)
        outargs[15] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          (_state.logtarget, _state.gradlogtarget) = inargs[15](_state.value)
      end

      if isa(inargs[16], Function)
        outargs[16] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          (_state.logtarget, _state.gradlogtarget, _state.tensorlogtarget) = inargs[16](_state.value)
      end

      if isa(inargs[17], Function)
        outargs[17] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          (_state.logtarget, _state.gradlogtarget, _state.tensorlogtarget, _state.dtensorlogtarget) =
            inargs[17](_state.value)
      end
    end
  end

  for (i, j, field, distribution, f) in ((4, 2, :logprior, :prior, logpdf), (5, 1, :logtarget, :pdf, logpdf))
    if !isa(inargs[i], Function) &&
      (
        (
          isa(getfield(parameter, distribution), ContinuousUnivariateDistribution) &&
          method_exists(f, (typeof(getfield(parameter, distribution)), eltype(getfield(parameter, distribution))))
        ) ||
        isa(inargs[j], Function)
      )
      outargs[i] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
        setfield!(_state, field, f(getfield(parameter, distribution), _state.value))
    end
  end

  if diffopts != nothing
    for (i, field) in ((3, :closurell), (4, :closurelp), (5, :closurelt))
      if isa(inargs[i], Function)
        setfield!(
          parameter.diffmethods,
          field,
          nkeys == 0 ? inargs[i] : _x::Real -> inargs[i](_x, Any[s.value for s in parameter.states])
        )
      end
    end

    for (i, returnname, diffresult, diffmethod) in (
      (6, :gradloglikelihood, :resultll, :closurell),
      (7, :gradlogprior, :resultlp, :closurelp),
      (8, :gradlogtarget, :resultlt, :closurelt)
    )
      if !isa(inargs[i], Function) && isa(inargs[i-3], Function)
        outargs[i] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
          setfield!(
            _state,
            returnname,
            forward_autodiff_derivative(
              getfield(_state.diffstate, diffresult), getfield(_state.diffmethods, diffmethod), _state.value
            )
          )
      end
    end

    if !isa(inargs[15], Function) && isa(inargs[5], Function)
      outargs[15] = (_state::BasicContUnvParameterState, _states::VariableStateVector) ->
        (_state.logtarget, _state.gradlogtarget) =
          forward_autodiff_upto_derivative(_state.diffstate.resultlt, _state.diffmethods.closurelt, _state.value)
    end
  end

  BasicContUnvParameter!(parameter, outargs...)

  parameter
end

value_support(::Type{BasicContUnvParameter}) = Continuous
value_support(::BasicContUnvParameter) = Continuous

variate_form(::Type{BasicContUnvParameter}) = Univariate
variate_form(::BasicContUnvParameter) = Univariate

default_state_type(::BasicContUnvParameter) = BasicContUnvParameterState

default_state(variable::BasicContUnvParameter, value::N, outopts::Dict) where {N<:Real} =
  BasicContUnvParameterState(
    value,
    (haskey(outopts, :diagnostics) && in(:accept, outopts[:diagnostics])) ? [:accept] : Symbol[],
    variable.diffmethods,
    variable.diffopts
  )

show(io::IO, ::Type{BasicContUnvParameter}) = print(io, "BasicContUnvParameter")


# MIT license
# Copyright (c) Microsoft Corporation. All rights reserved.
# See LICENSE in the project root for full license information.

intersectionat(α::Float64) = Intersection(α, [0.7071067811865475, 0.7071067811865475, 0.0] * α, [0.0, 0.0, 0.0], 0.0, 0.0, NullInterface())

const greenwavelength = Unitful.u"550nm"
const redwavelength = Unitful.u"650nm"
const bluewavelength = Unitful.u"450nm"
const roomtemperature = 20 * Unitful.u"°C"

transmissivethingrating(period, orders) = ThinGratingInterface(SVector(0.0, 1.0, 0.0), period, OpticSim.GlassCat.Air, OpticSim.GlassCat.Air, minorder = -orders, maxorder = orders, reflectance = [0.0 for _ in (-orders):orders], transmission = [0.2 for _ in (-orders):orders])
reflectivethingrating(period, orders) = ThinGratingInterface(SVector(0.0, 1.0, 0.0), period, OpticSim.GlassCat.Air, OpticSim.GlassCat.Air, minorder = -orders, maxorder = orders, transmission = [0.0 for _ in (-orders):orders], reflectance = [0.2 for _ in (-orders):orders])

function multiHOE()
    rect = Rectangle(5.0, 5.0, SVector(0.0, 0.0, 1.0), SVector(0.0, 0.0, 0.0))
    inbeam = SVector(0.0, 0.0, -1.0), CollimatedBeam

    int1 = HologramInterface(SVector(-5.0, 0.0, -20.0), ConvergingBeam, SVector(0.0, -1.0, -1.0), CollimatedBeam, 0.55, 100.0, OpticSim.GlassCat.Air, OpticSim.Examples.Examples_N_BK7, OpticSim.GlassCat.Air, OpticSim.GlassCat.Air, OpticSim.GlassCat.Air, 0.05, false)
    int2 = HologramInterface(SVector(5.0, 0.0, -20.0), ConvergingBeam, SVector(0.0, 1.0, -1.0), CollimatedBeam, 0.55, 100.0, OpticSim.GlassCat.Air, OpticSim.Examples.Examples_N_BK7, OpticSim.GlassCat.Air, OpticSim.GlassCat.Air, OpticSim.GlassCat.Air, 0.05, false)

    mint = MultiHologramInterface(int1, int2)
    obj = MultiHologramSurface(rect, mint)
    sys = CSGOpticalSystem(LensAssembly(obj), Rectangle(50.0, 50.0, SVector(0.0, 0.0, 1.0), SVector(0.0, 0.0, -20.0), interface = opaqueinterface()))
    return sys
end


"""
The TinStandard type contains the state variables (swe), the inputs
(prec, tair) for one time step, the parameters (param) and the time step
length (tstep) for a standard temperature index model.
"""
type TinStandard <: Snow

  swe::Array{Float64,1}
  lw::Array{Float64,1}
  prec::Array{Float64,1}
  tair::Array{Float64,1}
  q_sim::Array{Float64,1}
  param::Array{Float64,1}
  frac::Array{Float64,1}
  tstep::Float64
  time::DateTime

end

"""
    TinStandard(tstep, time, frac)

Constructor for TinStandard with predefined state variables, parameters and inputs.
The time step (tstep) is given as a fraction of one day. Thus, for hourly input
data tstep should be set to 1/24. The fraction of elevation bands should sum
up to unity.
"""
function TinStandard(tstep::Float64, time::DateTime, frac::Array{Float64,1})

  nzones = length(frac)
  swe    = zeros(Float64, nzones)
  lw     = zeros(Float64, nzones)
  prec   = zeros(Float64, nzones)
  tair   = zeros(Float64, nzones)
  q_sim  = zeros(Float64, nzones)
  param  = zeros(Float64, 5)

  TinStandard(swe, lw, prec, tair, q_sim, param, frac, tstep, time)

end

"""
    TinStandard(tstep, time, param, frac)

Constructor for TinStandard with predefined state variables and inputs.
The time step (tstep) is given as a fraction of one day. Thus, for hourly input
data tstep should be set to 1/24. The fraction of elevation bands should sum
up to unity.
"""
function TinStandard(tstep::Float64, time::DateTime, param::Array{Float64,1}, frac::Array{Float64,1})

  nzones = length(frac)
  swe    = zeros(Float64, nzones)
  lw     = zeros(Float64, nzones)
  prec   = zeros(Float64, nzones)
  tair   = zeros(Float64, nzones)
  q_sim  = zeros(Float64, nzones)

  TinStandard(swe, lw, prec, tair, q_sim, param, frac, tstep, time)

end

"""
    init_states(mdata::TinStandard)

Initilize the state variables of the model.
"""
function init_states(mdata::TinStandard)

  for ireg in eachindex(mdata.swe)
    mdata.swe[ireg] = 0.
    mdata.lw[ireg] = 0.
  end

end

"""
    get_param_range(mdata::TinStandard)

Get allowed parameter ranges for the calibration of the model.
"""
function get_param_range(mdata::TinStandard)

  param_range_snow = [(-0.5, 0.5), (0.5, 3.0), (0.5, 4.0), (0.01, 0.10), (0.5, 2.0)]

end

"""
    get_states(mdata::TinStandard)

Get state variables for computing penelty during calibration.

"""
function get_states(mdata::TinStandard)

  return [mean(mdata.swe); mean(mdata.lw)]

end

"""
    assign_param(mdata::TinStandard, param::Array{Float64,1})

Assign parameter values to the TinBasic type.
"""
function assign_param(mdata::TinStandard, param::Array{Float64,1})

  for ireg in eachindex(mdata.param)
    mdata.param[ireg] = param[ireg]
  end

end

"""
    enkf_snow(mdata::Array{TinStandard}, obs_ens, q_sim)

Implementation of the ensemble Kalman filter for the standard temperature index
snow model.
"""
function enkf_snow(mdata::Array{TinStandard}, obs_ens, q_sim)

  nens = length(mdata)

  # Allocate arrays

  swe = zeros(Float64, length(mdata[1].swe), nens)
  lw = zeros(Float64, length(mdata[1].lw), nens)

  # Add states to arrays

  for iens = 1:nens

    swe[:, iens] = mdata[iens].swe
    lw[:, iens] = mdata[iens].lw

  end

  # Run ensemble kalman filter

  swe = enkf(swe, obs_ens, q_sim)
  lw = enkf(lw, obs_ens, q_sim)

  # Check limits of states

  swe[swe .< 0] = 0.

  lwmax = swe * mdata[1].param[4]

  lw[lw .< 0] = 0.
  lw[lw .> lwmax] = 0.

  # Add arrays to states

  for iens = 1:nens

    mdata[iens].swe = swe[:, iens]
    mdata[iens].lw = lw[:, iens]

  end

  nothing

end

"""
    run_timestep(mdata::TinStandard)

Propagate the model one time step and compute simulated snowpack dischage.
"""
function run_timestep(mdata::TinStandard)

    # Parameters

    tth     = mdata.param[1]
    ddf_min = mdata.param[2]
    ddf_max = mdata.param[3]
    whcap   = mdata.param[4]
    pcorr   = mdata.param[5]

    ddf = compute_ddf(mdata.time, ddf_min, ddf_max)

    for ireg in eachindex(mdata.swe)

        swe = mdata.swe[ireg]
        lw = mdata.lw[ireg]

        # Compute solid and liquid precipitation

        psolid, pliquid = split_prec(mdata.prec[ireg], mdata.tair[ireg], tth)

        # Apply snowfall correction factor

        psolid = pcorr * psolid

        # Compute frozen water in snowpack

        fw  = swe - lw

        # Constrain fw and lw

        if fw < 0.0
            fw = 0.0
        end

        if lw < 0.0
            lw = 0.0
        end

        # Compute melt rates

        melt = compute_melt(mdata.tair[ireg], fw, ddf, tth)

        # Compute refreezing rates

        refr = compute_refreeze(mdata.tair[ireg], lw, ddf, tth)

        # Massbalance for frozen water in snowpack

        dfw = psolid + refr - melt
        fw  = fw + dfw

        # Massbalance for liquid water in snowpack

        dlw = pliquid + melt - refr
        lw  = lw + dlw

        lwmax = fw * whcap / (1 - whcap)

        q_sim = lw - lwmax

        if q_sim < 0.
            q_sim = 0.
        end

        lw = lw - q_sim

        # Massbalance for snowpack

        swe = fw + lw

        # Assign final results

        mdata.swe[ireg] = swe
        mdata.lw[ireg] = lw
        mdata.q_sim[ireg] = q_sim

    end

    mdata.time += Dates.Hour(mdata.tstep)

    return nothing

end

# Compute snowmelt

function compute_melt(tair::Float64, fw::Float64, ddf::Float64, tth_melt::Float64)

    m_melt = 0.5

    t_m = (tair - tth_melt) / m_melt
    melt = ddf * m_melt * (t_m + log(1. + exp(-t_m)))

    melt = min(max(0.,fw), melt)

    return melt

end

# Compute refreeze

function compute_refreeze(tair::Float64, lw::Float64, ddf::Float64, tth_refr::Float64)

    m_refr = 0.5

    t_m = (tth_refr - tair) / m_refr
    refr = ddf * m_refr * (t_m + log(1. + exp(-t_m)))

    if refr > lw
        refr = lw
    end

    refr = min(lw,refr)

end


#
println("Ed) Photoabsorption of Ne^+: AverageSCA model.")
using JLD
#
if  false
    # Calculation of the Ne^+  2s^2 2p^5; 2s --> 2p, 3p, 4p, 5p excitation cross sections
    setDefaults("print summary: open", "Ne-plus-photoexcitation.sum")
    setDefaults("method: continuum, asymptotic Coulomb")                                                      ## "method: continuum, Galerkin"
    grid = Radial.Grid(Radial.Grid(false); rnt = 3.0e-6, h = 2.0e-2, hp = 3.0e-2, NoPoints=1110)
    name = "Photoabsorption calculations for Ne^+ for energies = [1, 4] a.u."
    wa   = Cascade.Computation(Cascade.Computation(); name=name, nuclearModel=Nuclear.Model(10.), grid=grid, approach=Cascade.AverageSCA(),
                               scheme=Cascade.PhotonExcitationScheme([PhotoExc()], [E1], 0.5, 4.0, 1, [Shell("2s"), Shell("2p")],
                                                                     [Shell("2s"), Shell("2p"), Shell("3p"), Shell("4p"), Shell("5p")]),
                               initialConfigs=[Configuration("1s^2 2s^2 2p^5")] )
    println(wa)
    wb = perform(wa; output=true)
    setDefaults("print summary: close", "")
    #
elseif false
    # Calculation of the Ne^+  2s^2 2p^5; photo-ionization cross sections
    setDefaults("print summary: open", "Ne-plus-photoionization.sum")

    name = "Photoionization of Si- "
    grid = Radial.Grid(Radial.Grid(false); rnt = 3.0e-6, h = 2.0e-2, hp = 3.0e-2, NoPoints=1110)
    wa   = Cascade.Computation(Cascade.Computation(); name=name, nuclearModel=Nuclear.Model(10.), grid=grid, approach=Cascade.AverageSCA(),
                               scheme=Cascade.PhotonIonizationScheme([Photo()], 1, [1.0, 3.0, 5.0]),
                               initialConfigs=[Configuration("1s^2 2s^2 2p^5")] )
    println(wa)
    wb = perform(wa; output=true)
    setDefaults("print summary: close", "")

elseif false
    # Simulation of the Ne^+  2s^2 2p^5; photo-ionization cross sections
    setDefaults("print summary: open", "Ne-plus-photoabsorption-simulation.sum")

    data = [JLD.load("zzz-cascade-ionizing-computations-2020-07-10T19.jld"), JLD.load("zzz-cascade-excitation-computations-2020-07-10T19.jld")]
    ##x JLD2.@load "zzz-cascade-ionizing-computations-2020-06-23T09.jld"
    ##x resIon  = results
    ##x JLD2.@load "zzz-cascade-excitation-computations-2020-06-23T09.jld"
    ##x resExc = results
    ##x data = [resExc]

    name = "Simulation of photoabsorption for Ne^+"

    wc   = Cascade.Simulation(Cascade.Simulation(), name=name, property=Cascade.PhotoAbsorption(),
                              settings=Cascade.SimulationSettings(0., 0., 1., 4., 3., [(1, 1.0)]), computationData=data )
    println(wc)
    wd = perform(wc; output=true)
    setDefaults("print summary: close", "")
#
elseif false
    # Example for Atomic cascade computation (symmetry, 2021)
    # Calculation of the Mg  1s 2s^2 2p^6 3s^2 decay cascade after 1s photoionization
    setDefaults("print summary: open", "Mg-1s-decay.sum")

    decayScheme = Cascade.StepwiseDecayScheme([Auger(), Radiative()], 3, Dict{Int64,Float64}(), 0., Shell[], Shell[])

    grid = Radial.Grid(Radial.Grid(false), rnt = 4.0e-6, h = 5.0e-2, hp = 1.0e-2, rbox = 10.0)
    name = "Computation of Mg  1s 2s^2 2p^6 3s^2 decay cascade after 1s photoionization"
    wa   = Cascade.Computation(Cascade.Computation(); name=name, nuclearModel=Nuclear.Model(12.), grid=grid,
                               approach=Cascade.AverageSCA(), scheme=decayScheme,
                               ## initialConfigs=[Configuration("1s 2s^2 2p^6 3s^2")] )
                               initialConfigs=[Configuration("1s 2s^2 2p^6 3s^2 3p")] )
    @show wa
    wb = perform(wa; output=true)
    setDefaults("print summary: close", "")
#
elseif true
    # Simulation of the Mg  1s 2s^2 2p^6 3s^2 decay cascade after 1s photoionization
    setDefaults("print summary: open", "zzz-Cascade-simulation.sum")

    ## simulationSettings = Cascade.SimulationSettings(0., 0., 0., 0., 0., [(1, 1.0)])
    ## simulationSettings = Cascade.SimulationSettings(0., 0., 0., 0., 0., [(4, 1.0)])
    simulationSettings = Cascade.SimulationSettings(0., 0., 0., 0., 0., [(1, 0.25), (2, 0.25), (3, 0.25), (4, 0.25)])

    data = [JLD.load("zzz-cascade-decay-computations-2021-03-14T19.jld")]
    name = "Simulation of Mg  1s 2s^2 2p^6 3s^2 decay cascade after 1s photoionization"

    wc   = Cascade.Simulation(Cascade.Simulation(), name=name, property=Cascade.IonDistribution(),
                              settings=simulationSettings, computationData=data )
    @show wc
    wd = perform(wc; output=true)
    setDefaults("print summary: close", "")
    #

end


# Propeller Aerodynamic Validation using UiUC Propeller data,

include("propwing.jl")

module SysOptInit_M
# export sysoptinit, getvar, printmaxviol

# ----- Define function for getting values within objective function ----- #
function getvar(key,x,optvars,optrange,optval,optscale)
    # Determine whether variable is a design variable, assign default if not
    if key in optvars
        value = x[optrange[key]]./optscale[key]'
    else
        value = optval[key][:,3]
    end
    # Make sure Float64 is returned instead of Array{Float64,1} where applicable
    if length(value) == 1
        value = value[1]
    end
    return value
end

# ----- Define function for setting values within objective function ----- #
function setvar(key,x,optvars,optrange,optval,optscale,value)
    # Determine whether variable is a design variable, assign default if not
    if key in optvars
        x[optrange[key]] = value.*optscale[key]'
    end

end

# ----- Define function for printing maximum constraint violations ----- #
function printmaxviol(c,name)
    if !isempty(find(c.>0.0))
        println("$name: ",maximum(c))
    end
end

function sysoptinit(optvars::Array{Symbol,1})
    # ----- Define lower, upper, and starting values ----- #
    optval = Dict{Symbol,Array{Float64,2}}()
    #---General Geometry Variables

    #---Power System Variables
    # Propeller Defaults are for the Epema N250

    #Propulsion
    optval[:velocity]  = [1.0 50.0 19.0]  #m/s
    optval[:proptwist] = [-15.0 80.0 54.9242;
    -15.0 75.0 49.6058;
    -15.0 60.0 44.6923;
    -15.0 45.0 40.1836;
    -15.0 45.0 36.0798;
    -15.0 45.0 32.3809;
    -15.0 45.0 29.0869;
    -15.0 45.0 26.1977;
    -15.0 45.0 23.7134]  #degrees # Propeller Defaults are for the Epema N250

    optval[:propchord] = [0.03 0.3 0.158788;
    0.03 0.3 0.15083;
    0.03 0.3 0.146158;
    0.03 0.3 0.145793;
    0.03 0.3 0.148978;
    0.03 0.3 0.153175;
    0.03 0.3 0.154064;
    0.03 0.3 0.145548;
    0.03 0.3 0.119747]  #chord/Rtip # Propeller Defaults are for the Epema N250

    optval[:RPMprop]   = [10.0 20000.0 4040.0] #RPM
    optval[:Rtip]      = [0.01 2.0 .203] # Radius meters, # Propeller Defaults are for the Epema N250

    # ----- Define scaling for each variable ----- #
    optscale = Dict()

    #Propulsion
    optscale[:velocity]  = [1e-3]
    optscale[:proptwist] = [1e-1]
    optscale[:propchord] = [1e3]
    optscale[:RPMprop]   = [1e-4] #RPM
    optscale[:Rtip]      = [1e0] # Radius meters
    optscale[:T_prop]      = [1e-2] # Radius meters

    # ----- Apply scaling ----- #
    for var in optvars
        optval[var] = optval[var].*optscale[var]'
    end

    # ----- Define Ranges ----- #
    optrange = Dict()
    cx = 0
    for var in optvars
        optrange[var] = cx+(1:size(optval[var],1))
        cx += size(optval[var],1)
    end
    vartotal = cx

    # ----- Assemble lower, upper, and starting optval ----- #
    lb = zeros(vartotal)
    ub = zeros(vartotal)
    x0 = zeros(vartotal)
    for var in optvars
        lb[optrange[var]] = optval[var][:,1]
        ub[optrange[var]] = optval[var][:,2]
        x0[optrange[var]] = optval[var][:,3]
    end

    # ----- Define Optimizer Options ----- #
    options = Dict{String, Any}()
    options["Derivative level"] = 0
    # options["Function precision"] = 3.00E-4
    options["Difference interval"] = 1e-5
    options["Central difference interval"] = 1e-6
    options["Iterations limit"] = 1e8
    options["Major iterations limit"] = 500
    options["Minor iterations limit"]= 1e8
    options["Major optimality tolerance"] = 1e-4
    options["Minor optimality  tolerance"] = 1e-4
    options["Major feasibility tolerance"] = 1e-4
    options["Minor feasibility tolerance"] = 1e-4
    options["Minor print level"] = 1
    options["Print frequency"] = 100
    options["Scale option"] = 2
    options["Scale tolerance"] = .95
    # options["Verify level"] = 3 #only if specifying gradients

    return x0,lb,ub,options,optrange,optval,optscale
end #function sysoptinit

end #module

# Do all calling relative to file's location
fileLoc,_ = splitdir(@__FILE__)

include("$(fileLoc)/../../hale-module-miscellany/params.jl")
push!(LOAD_PATH,"$(fileLoc)/../../hale-module-miscellany/")
using StandardAtmosphere

using CCBlade

using JLD
using PyPlot
close("all")

function prop_aero(x0,optvars,optrange,optval,optscale,T_req,af,rho,unitareas,pitch=0.0,optimize=false,numblades = 2)
    #Unpack the variables, sysoptinit takes care of which are being optimized
    theta = (SysOptInit_M.getvar(:proptwist,x0,optvars,optrange,optval,optscale)+pitch)*pi/180 #in degrees, convert to rad
    RPMp = SysOptInit_M.getvar(:RPMprop,x0,optvars,optrange,optval,optscale)
    Rtip = SysOptInit_M.getvar(:Rtip,x0,optvars,optrange,optval,optscale)
    chords = SysOptInit_M.getvar(:propchord,x0,optvars,optrange,optval,optscale)*Rtip
    velocity = SysOptInit_M.getvar(:velocity,x0,optvars,optrange,optval,optscale)

    r = linspace(.2,.911,length(chords)) * Rtip
    B = numblades
    Rhub = r[1]/2
    chord_prop = chords
    twist_prop = theta
    Vinf = velocity
    Omega = RPMp/60*2*pi
    xtowing = 0.5
    uwake, vwake, T, Q, reff = propwing.propanalysis(Rhub, Rtip, r, chord_prop, twist_prop, B, af, Vinf, Omega, rho, xtowing)
    # Tprop,Qprop,Uprop,Utprop,propvolume,Np,Tp,J_p,eff,CT,CQ = CCcall.cccall(RPMp,theta,chords,CCBlade,Rtip,r,rho,velocity,af,unitareas,numblades)
    Vhub = Vinf
    precone = 0.0
    turbine = false
    J_p = Vhub/(RPMp/60*2*Rtip)
    eff, CT, CQ = CCBlade.nondim(T, Q, Vhub, Omega, rho, Rtip, precone, turbine)
    return T,Q,uwake, vwake, 0.0, 0.0, 0.0,J_p,eff, CT, CQ
end

# push!(LOAD_PATH,"$(fileLoc)/../../Snopt.jl/src/")
# using Snopt
#--------- Initialization ---------#
optvars = [
# :proptwist;
:RPMprop;
:velocity
# :Rtip;
]

x0,lb,ub,options,optrange,optval,optscale = SysOptInit_M.sysoptinit(optvars)

#Initialize Airfoil Data
twists = SysOptInit_M.getvar(:proptwist,x0,optvars,optrange,optval,optscale)
Rtip = SysOptInit_M.getvar(:Rtip,x0,optvars,optrange,optval,optscale)
chords = SysOptInit_M.getvar(:propchord,x0,optvars,optrange,optval,optscale)*Rtip
velocity = SysOptInit_M.getvar(:velocity,x0,optvars,optrange,optval,optscale)
RPMprop = SysOptInit_M.getvar(:RPMprop,x0,optvars,optrange,optval,optscale)
r = collect(linspace(.2,.911,length(chords))) .* Rtip
J = velocity/(RPMprop/60*2*Rtip) #Vinf/(Omega/(2*pi)*Rtip*2)
alt = 222.0 #meters
numblades = 6
rho,mu,q,a = StandardAtmosphere.atmosphere(alt)
re = rho/mu.*chords.*sqrt.((RPMprop*pi/30.0*r).^2+velocity^2)

afgeomnames = ["$(fileLoc)/EpemaExperimental/R023.dat";"$(fileLoc)/EpemaExperimental/R023.dat";
"$(fileLoc)/EpemaExperimental/R0405.dat";"$(fileLoc)/EpemaExperimental/R0405.dat";
"$(fileLoc)/EpemaExperimental/R0405.dat";"$(fileLoc)/EpemaExperimental/R079.dat";
"$(fileLoc)/EpemaExperimental/R079.dat";"$(fileLoc)/EpemaExperimental/R079.dat";"$(fileLoc)/EpemaExperimental/R1.dat"]

N=100
M = 3
new_RPM=4040 #linspace(4000,20000,M)
pitch_array = [-5.0,0.0,5.0]
new_velocity = linspace(1,60,N)
J_p = zeros(N,M)
eta_p = zeros(N,M)
CT_p = zeros(N,M)
CQ_p = zeros(N,M)

SysOptInit_M.setvar(:RPMprop,x0,optvars,optrange,optval,optscale,new_RPM)

velocity = SysOptInit_M.getvar(:velocity,x0,optvars,optrange,optval,optscale)
RPMprop = SysOptInit_M.getvar(:RPMprop,x0,optvars,optrange,optval,optscale)

J = velocity/(RPMprop/60*2*Rtip)

re = rho/mu.*chords.*sqrt.((RPMprop*pi/30.0*r).^2+velocity^2)

# af,unitareas, Al, Au = CCBladeAirfoilTools.af_performance_from_geom(afgeomnames,chords,Rtip,r,J,re)
# JLD.save("$(fileLoc)/af_prop$(new_RPM).jld", "af", af, "unitareas",unitareas,"Al", Al, "Au", Au)
JLDin = JLD.load("$(fileLoc)/af_prop$(new_RPM).jld")
af = JLDin["af"]
unitareas = JLDin["unitareas"]

for j = 1:M

    T_req = 0.0
    pitch = pitch_array[j]+3 #degrees
    optimize=false
    numblades = 6
    args = (optvars,optrange,optval,optscale,T_req,af,rho,unitareas,pitch,optimize,numblades)

    for i = 1:N
        SysOptInit_M.setvar(:velocity,x0,optvars,optrange,optval,optscale,new_velocity[i])

        Tprop,Qprop,Uprop, Utprop, propvolume, Np, Tp,J_p[i,j],eta_p[i,j], CT_p[i,j], CQ_p[i,j] = prop_aero(x0,args...)

    end
end

using CSV
CT_N250_Epema_25deg = CSV.read("$(fileLoc)/EpemaExperimental/CT_N250_25deg.txt", delim = '\t',header = false)
CP_N250_Epema_25deg = CSV.read("$(fileLoc)/EpemaExperimental/CP_N250_25deg.txt", delim = '\t',header = false)
CQ_N250_Epema_25deg = copy(CP_N250_Epema_25deg)
CQ_N250_Epema_25deg[:,2] = CQ_N250_Epema_25deg[:,2]/(2*pi)
ETA_N250_Epema_25deg = CT_N250_Epema_25deg[:,2].*CT_N250_Epema_25deg[:,1]./CP_N250_Epema_25deg[:,2]

CT_N250_Epema_30deg = CSV.read("$(fileLoc)/EpemaExperimental/CT_N250_30deg.txt", delim = '\t',header = false)
CP_N250_Epema_30deg = CSV.read("$(fileLoc)/EpemaExperimental/CP_N250_30deg.txt", delim = '\t',header = false)
CQ_N250_Epema_30deg = copy(CP_N250_Epema_30deg)
CQ_N250_Epema_30deg[:,2] = CQ_N250_Epema_30deg[:,2]/(2*pi)
ETA_N250_Epema_30deg = CT_N250_Epema_30deg[:,2].*CT_N250_Epema_30deg[:,1]./CP_N250_Epema_30deg[:,2]

CT_N250_Epema_35deg = CSV.read("$(fileLoc)/EpemaExperimental/CT_N250_35deg.txt", delim = '\t',header = false)
CP_N250_Epema_35deg = CSV.read("$(fileLoc)/EpemaExperimental/CP_N250_35deg.txt", delim = '\t',header = false)
CQ_N250_Epema_35deg = copy(CP_N250_Epema_35deg)
CQ_N250_Epema_35deg[:,2] = CQ_N250_Epema_35deg[:,2]/(2*pi)
ETA_N250_Epema_35deg = CT_N250_Epema_35deg[:,2].*CT_N250_Epema_35deg[:,1]./CP_N250_Epema_35deg[:,2]

rc("figure", figsize=(6.5, 2.6))
rc("font", size=10.0)
rc("lines", linewidth=1.5)
rc("lines", markersize=3.0)
rc("legend", frameon=false)
rc("axes.spines", right=false, top=false)
rc("figure.subplot", left=0.15, bottom=0.18, top=0.97, right=0.72)
rc("axes", color_cycle=["348ABD", "A60628", "009E73", "7A68A6", "D55E00", "CC79A7"])

mygreen = "#009e73"
myblue = "#348abd"
myred = "#a60628"
plotColors = [myblue,myred,mygreen]

#Thrust Coefficient
PyPlot.figure()
for i = 1:M
    PyPlot.plot(J_p[:,i],CT_p[:,i], marker="o",color = plotColors[i],markeredgecolor = "k",label = "BEM $(30+pitch_array[i])deg RPM=$(new_RPM)")

end
PyPlot.plot(CT_N250_Epema_25deg[:,1],CT_N250_Epema_25deg[:,2],linestyle="-", color = plotColors[1],label = "Epema 25deg")
PyPlot.plot(CT_N250_Epema_30deg[:,1],CT_N250_Epema_30deg[:,2],linestyle="-", color = plotColors[2],label = "Epema 30deg")
PyPlot.plot(CT_N250_Epema_35deg[:,1],CT_N250_Epema_35deg[:,2],linestyle="-", color = plotColors[3],label = "Epema 35deg")
PyPlot.xlabel(L"J\, (V/(n*D_{prop})")
PyPlot.ylabel(L"CT\, (T/(\rho*n^2*{D_{prop}}^4))")
PyPlot.xlim([0,2.0])
PyPlot.ylim([0,.4])
PyPlot.legend(loc = 1)

#Torque Coefficient

PyPlot.figure()
for i = 1:M
    PyPlot.plot(J_p[:,i],CQ_p[:,i], marker="o",color = plotColors[i],markeredgecolor = "k",label = "BEM $(30+pitch_array[i])deg RPM=$(new_RPM)")

end
PyPlot.plot(CQ_N250_Epema_25deg[:,1],CQ_N250_Epema_25deg[:,2],linestyle="-", color = plotColors[1],label = "Epema 25deg")
PyPlot.plot(CQ_N250_Epema_30deg[:,1],CQ_N250_Epema_30deg[:,2],linestyle="-", color = plotColors[2],label = "Epema 30deg")
PyPlot.plot(CQ_N250_Epema_35deg[:,1],CQ_N250_Epema_35deg[:,2],linestyle="-", color = plotColors[3],label = "Epema 35deg")
PyPlot.xlabel(L"J\, (V/(n*D_{prop})")
PyPlot.ylabel(L"Cq\, (T/(\rho*n^2*{D_{prop}}^5)")
PyPlot.xlim([0,2.0])
PyPlot.ylim([0,.1])
PyPlot.legend(loc = 1)

#Efficiency
PyPlot.figure()
for i = 1:M
    PyPlot.plot(J_p[:,i],eta_p[:,i], linestyle="-",color = plotColors[i])#,markeredgecolor = "k",label = "BEM $(30+pitch_array[i])deg RPM=$(new_RPM)")

end
PyPlot.plot(CT_N250_Epema_25deg[:,1],ETA_N250_Epema_25deg,marker="o",linestyle="", color = plotColors[1])#,label = "Epema 25deg")
PyPlot.plot(CT_N250_Epema_30deg[:,1],ETA_N250_Epema_30deg,marker="o",linestyle="", color = plotColors[2])#,label = "Epema 30deg")
PyPlot.plot(CT_N250_Epema_35deg[:,1],ETA_N250_Epema_35deg,marker="o",linestyle="", color = plotColors[3])#,label = "Epema 35deg")

PyPlot.plot([],[],"ko",label = "Experimental")
PyPlot.plot([],[],"k-",label = "Blade Element Momentum")
PyPlot.xlabel("Advance Ratio")
PyPlot.ylabel("Efficiency")
# PyPlot.xlim([0,2.0])
# PyPlot.ylim([0,1.5])
PyPlot.legend(loc="center left", bbox_to_anchor=(.8, 0.8))


### road_model.jl

using Dates
using Gadfly
using LightGraphs, MetaGraphs
using JuMP, BilevelJuMP
using Gurobi
using DataFrames

struct ModelParams
    lambda
    sensor_cap_ratio
    force_sensor_bidirectional::Bool
    junctions::Bool
    capacity_classes::Dict{String, Real}

    # Sensors can be forced on, off, or left arbitrary (not in Dict)
    # fixed_sensors::Dict{Tuple{Int, Int}, Bool}
end

struct SolveParams
    time_limit
    # TODO
    indicator_mode
    opt_attributes
end

# Default parameters
ModelParams(; lambda = 1, bidir = false, junctions = false) = ModelParams(
    lambda,
    1.0,
    bidir,
    junctions,
    Dict(
        "motorway"  => 500,
        "trunk"     => 400,
        "primary"   => 300,
        "secondary" => 200,
        "tertiary"  => 100,
        "other"     => 50
    ))

SolveParams(; time_limit = 60, indicator_mode = false, opt_attributes = Dict("MIPFocus" => 3)) = SolveParams(time_limit, indicator_mode, opt_attributes)

export ModelParams, SolveParams

struct RoadModel
    m
    G
    constraints
    MP::ModelParams
    SP::SolveParams
    data::Dict{Symbol, Any}
end

include("road_utils.jl")
include("road_constraints.jl")

function RoadModel(G, MP::ModelParams, SP::SolveParams)::RoadModel
    # TODO: init errors if global const
    GRB_ENV = Gurobi.Env()
    mode = SP.indicator_mode ? BilevelJuMP.IndicatorMode() : BilevelJuMP.StrongDualityMode(1e-5)
    m = BilevelModel(() -> Gurobi.Optimizer(GRB_ENV), mode = mode)
    set_optimizer_attribute(m, "TimeLimit", SP.time_limit)
    for (k, v) in pairs(SP.opt_attributes)
        set_optimizer_attribute(m, k, v)
    end

    # clone the graph, as it will be mutated
    model_G = deepcopy(G)

    # constraints and arbitrary model data is stored here
    data = Dict()

    # Storing variables as properties in the graph is convenient
    for (u, v, p) ∈ edge_prop_triples(model_G)
        cap = get(MP.capacity_classes, p[:street_type], MP.capacity_classes["other"])

        fuv = @variable(Lower(m), base_name = "f_$(u),$(v)")
        suv = @variable(Upper(m), base_name = "s_$(u),$(v)")
        set_lower_bound(fuv, 0)
        set_upper_bound(fuv, cap)
        set_binary(suv)

        set_prop!(model_G, u, v, :flow_var, fuv)
        set_prop!(model_G, u, v, :sensor_var, suv)
        set_prop!(model_G, u, v, :capacity, cap)
    end

    RoadModel(m, model_G, [], MP, SP, data)
end

function setup!(M::RoadModel)
    edge_prop_map(prop) = edge_prop_triples(M.G) .|> ((_, _, p),) -> p[prop]

    # Various constraint passes

    # Mutually exclusive ways to prevent flow looping on 2-cycles
    if M.MP.junctions
        cs_junction!(Lower(M.m), M)
    else
        cs_conserve_flow!(Lower(M.m), M)
        cs_inflow!(Lower(M.m), M)
    end
    cs_sensor!(Lower(M.m), M)

    # Optional: Not implemented
    # cs_anticycle_local!(Lower(M.m), M)
    # cs_anticycle_nonlocal!(Lower(M.m), M)

    # Objectives

    sensor_map((u, v, p),) = let
        # Sensors on both sides of the road should count as "one" sensor in bidirectional mode
        if M.MP.force_sensor_bidirectional && has_edge(M.G, v, u)
            p[:sensor_var] * 0.5
        else
            p[:sensor_var]
        end
    end
    num_sensors = 0
    # Type inference failure...
    for s in sensor_map.(edge_prop_triples(M.G))
        num_sensors += s
    end
    M.data[:num_sensors_var] = num_sensors

    weighted_flows = sum(edge_prop_map(:length) .* edge_prop_map(:flow_var)) / 100
    # try to normalise by "size" of graph
    λ = M.MP.lambda / length(edges(M.G))

    @objective(Lower(M.m), Max, weighted_flows)
    @objective(Upper(M.m), Min, num_sensors + λ * weighted_flows)
end

"""
Outputs sensor placements and flows as edge properties in model graph `M.G`.

Will throw error if solver fails to find a solution (seems to be picky about which parameters it likes)
"""
function solve!(M::RoadModel)
    optimize!(M.m)

    for (u, v, p) in edge_prop_triples(M.G)
        set_prop!(M.G, u, v, :sensor, value(p[:sensor_var]) ≈ 1)
        f = value(p[:flow_var])
        # Numerical errors...
        @assert f > -1e-5 "Got negative flow: $(f)"
        set_prop!(M.G, u, v, :flow, max(0, f))
    end

    termination_status(M.m)
    # TODO: print summary
end

#
# Drawing utjilty functions
#

@resumable function position_edges(G)
    for (u, v, p) in edge_prop_triples(G)
        up, vp = props.(Ref(G), [u, v])
        @yield (
            up[:latitude],
            up[:longitude],
            vp[:latitude],
            vp[:longitude],
            p[:flow]
        )
    end
end

@resumable function position_sensors(G)
    for (u, v, p) in edge_prop_triples(G)
        if p[:sensor]
            @yield (
            (props(G, u)[:latitude] + props(G, v)[:latitude]) / 2,
            (props(G, u)[:longitude] + props(G, v)[:longitude]) / 2
        )
        end
    end
end

"""
Draws graph as SVG file using the outputs from `solve!`
"""
function draw!(M::RoadModel, svg_name = "road_sensors.svg")
    # Compute latitude and longitude bounding box for plot
    lats = (1:length(vertices(M.G))) .|> i -> get_prop(M.G, i, :latitude)
    longs = (1:length(vertices(M.G))) .|> i -> get_prop(M.G, i, :longitude)

    ns = (Int ∘ round ∘ value)(M.data[:num_sensors_var])
    d = collect(zipv(position_edges(M.G)))
    df = DataFrame(Dict(:x => d[1], :y => d[2], :xend => d[3], :yend => d[4], :Flow => d[5]))

    p = plot(
		# plot sensors on top layer as points instead of lines
		layer(
			Geom.point;
			zip([:x, :y], zipv(position_sensors(M.G)))...
		),
		layer(
            df,
            x = :x,
            y = :y,
            xend = :xend,
            yend = :yend,
            color = :Flow,
			Geom.segment(arrow=true, filled=true)
		),
		Scale.x_continuous(minvalue=minimum(lats), maxvalue=maximum(lats)),
		Scale.y_continuous(minvalue=minimum(longs), maxvalue=maximum(longs)),
		Theme(line_width = 0.1mm),
        Guide.xlabel("Latitude"),
        Guide.ylabel("Longitude"),
        Guide.title("λ = $(M.MP.lambda) ($(ns) sensors)"),
	)
    Gadfly.draw(SVGJS(svg_name), p)
    p
end

"""
Converts result of a successful model solve into JSON
"""
function save_as_json(M::RoadModel)
    logs = Dict()

    sensor_all = true
    sensor_none = false
    # JSON does not have tuples (hashable), so a Dict for edges won't work
    repr_edge((src, dst, prop_dict),) = let
        sensor_all &= prop_dict[:sensor]
        sensor_none |= prop_dict[:sensor]
        [(src, dst), Dict(
            "sensor" => prop_dict[:sensor],
            "flow" => prop_dict[:flow],
        )]
    end
    # Not a particularly good way to serialise model - ids depends on latitude/longitude bounding box
    logs["edges"] = collect(It.map(repr_edge, edge_prop_triples(M.G)))
    logs["objective"] = objective_value(M.m)

    logs["trivial_all"] = sensor_all
    logs["trivial_none"] = !sensor_none

    logs
end

function load(path, G)
    logs = JSON.parsefile(path)

    # other parameters ignored for now
    MP = ModelParams(
        lambda = logs["model_params"]["lambda"],
        bidir = logs["model_params"]["force_sensor_bidirectional"],
        junctions = logs["model_params"]["junctions"]
    )

    # Loading an existing solution so solve should be near-instantaneous
    SP = SolveParams(
        time_limit = 10,
    )

    M = RoadModel(G, MP, SP)
    setup!(M)

    # fix variables to loaded values
    for ((u, v), json_edge_props) in logs["edges"]
        if json_edge_props["sensor"]
            fix(get_prop(M.G, u, v, :sensor_var), 1, force=true)
        else
            fix(get_prop(M.G, u, v, :sensor_var), 0, force=true)
        end
    end
    solve!(M)
    draw!(M)
    M
end

"""
Takes a graph and parameters, runs the model and dumps output as JSON in /out
"""
function run_model(G, MP::ModelParams, SP::SolveParams, name=""; draw = true, add_data = Dict())
    file_prefix = replace(Dates.format(now(), Dates.ISODateTimeFormat), ":" => "-")
    if !isempty(name)
        name = "-$(name)"
    end

    # JSON at the end
    logs = Dict()
    logs["model_params"] = MP
    logs["solve_params"] = SP

    # Record Gurobi solver output
    old_stdout = stdout
    stdout_rd, stdout_wr = redirect_stdout()
    M = let
        M = RoadModel(G, MP, SP)
        setup!(M)
        try
            solve!(M)
            if draw
                draw!(M, "out/$(file_prefix)$(name).svg")
            end
            merge!(logs, save_as_json(M))
        catch e
            println("ERROR: $(e)")
            # save failure report to JSON as merge! in try block did not happen
            name = "-FAILED$(name)"
            logs["exception"] = "$(e)"
        end
        Base.Libc.flush_cstdio()
        M
    end
    logs["stdout"] = String(readavailable(stdout_rd))
    redirect_stdout(old_stdout)

    merge!(logs, add_data)

    # Dump Dict to file
    mkpath("out")
    open("out/$(file_prefix)$(name).json", "w") do io
        write(io, JSON.json(logs, 4))
    end

    # return solved model
    M
end

export RoadModel, setup!, solve!, draw!, save_as_json, run_model, load


function loopwinFixsize(nSNP, nReads, rawdata, snpID, bin2geno, nPer,
                        SNPSize, flagAnnotate, snpWtType, keepIdx,
                        nPerKeep, kernel, kinship, test,
                        XtNullBasis, WPreSim, nBlockAscent, nMMmax,
                        nNullSimPts, tolX, pvalueComputing, device,
                        nNullSimNewtonIter, rankQPhi, evalPhiAdj,
                        XPhitNullBasis, yShift, y, KPhiAdj,
                        weightedW, QPhi, PrePartialSumW,
                        PreTotalSumW, windowSize, nPreRank, X, ynew)

  QRes = Array(Float64, nPerKeep, rankQPhi);
  nSNPredVec = fill(0, nReads);
  snpSqrtWts = Float64[];
  tmpvec = similar(yShift);
  tmpvecQRes = Array(Float64, rankQPhi);
  yWork = similar(evalPhiAdj);
  partialSumWConst = Array(Float64, nNullSimPts);
  totalSumWConst = Array(Float64, nNullSimPts);
  subXPhitSV = Array(Float64, size(XPhitNullBasis, 2), rankQPhi);
  pSubXPhitSV = pointer(subXPhitSV);
  offset = 0;
  oneConst = ones(nPerKeep);
  geno = Array(Float64, nPer, SNPSize);
  genoOri = Array(Float64, nPer, SNPSize);
  snpIDred = similar(snpID);
  tmpMat = Array(Float64, windowSize, size(XPhitNullBasis, 2));
  VWorkSqrt = Array(Float64, length(evalPhiAdj), windowSize);
  VWorkSqrt2 = Array(Float64, length(evalPhiAdj), windowSize);
  mafOri = Array(Float64, SNPSize);
  maf = Array(Float64, SNPSize);
  sumIsnan = Array(Float64, 1, SNPSize);
  nChrObs = Array(Float64, 1, SNPSize);
  tmpvecKernel = Array(Float64, windowSize);
  if pvalueComputing == "chi2"
    nPtsChi2 = 300;
    partialSumW = Array(Float64, nPtsChi2);
    totalSumW = Array(Float64, nPtsChi2);
    lambda = Array(Float64, 1, nPtsChi2);
    W = Array(Float64, windowSize, nPtsChi2);
    tmpmat0 = Array(Float64, windowSize, nPtsChi2);
    tmpmat1 = Array(Float64, windowSize, nPtsChi2);
    tmpmat2 = Array(Float64, windowSize, nPtsChi2);
    tmpmat3 = Array(Float64, windowSize, nPtsChi2);
    tmpmat4 = Array(Float64, windowSize, nPtsChi2);
    tmpmat5 = Array(Float64, windowSize, nPtsChi2);
    denomvec = Array(Float64, nPtsChi2);
    d1f = Array(Float64, nPtsChi2);
    d2f = Array(Float64, nPtsChi2);
    if test == "eScore"
      simnull = Array(Float64, nPtsChi2);
    else
      simnull = Float64[];
    end
  else
    nPtsChi2 = 300;
    partialSumW = Array(Float64, nNullSimPts);
    totalSumW = Array(Float64, nNullSimPts);
    lambda = Array(Float64, 1, nNullSimPts);
    W = Array(Float64, windowSize, nNullSimPts);
    tmpmat0 = Array(Float64, windowSize, nNullSimPts);
    tmpmat1 = Array(Float64, windowSize, nNullSimPts);
    tmpmat2 = Array(Float64, windowSize, nNullSimPts);
    tmpmat3 = Array(Float64, windowSize, nNullSimPts);
    tmpmat4 = Array(Float64, windowSize, nNullSimPts);
    tmpmat5 = Array(Float64, windowSize, nNullSimPts);
    denomvec = Array(Float64, nNullSimPts);
    d1f = Array(Float64, nNullSimPts);
    d2f = Array(Float64, nNullSimPts);
    if test == "eScore"
      simnull = Array(Float64, nNullSimPts);
    else
      simnull = Float64[];
    end
  end

  results = Array(Any, nReads * iceil(SNPSize / windowSize), 3);
  idx = 0;

  for idxRead = 1 : nReads

    # read the binary data
    if idxRead == nReads
      curSNPSize = nSNP - (idxRead - 1) * SNPSize;
    else
      curSNPSize = SNPSize;
    end

    readgeno!(genoOri, curSNPSize, nPer, SNPSize, idxRead, bin2geno, rawdata,
              offset, flagAnnotate);

    tmpMatIsnan = isnan(genoOri);
    sum!(sumIsnan, tmpMatIsnan);
    for i = 1 : curSNPSize
      nChrObs[1, i] = 2 * (nPer - sumIsnan[1, i]);
    end

    # minor allele frequencies and get rid of mono-allelic SNPs
    counter = 0;
    offset = (idxRead - 1) * SNPSize;
    for i = 1 : curSNPSize
      mafOri[i] = 0.0;
      for j = 1 : nPer
        if !tmpMatIsnan[j, i]
          mafOri[i] += genoOri[j, i];
        end
      end
      mafOri[i] = mafOri[i] / nChrObs[1, i];
      if mafOri[i] != 0
        counter += 1;
        maf[counter] = mafOri[i];
        snpIDred[counter] = snpID[i + offset];
        pGenoOri = pointer(genoOri) + (i - 1) * nPer * sizeof(Float64);
        pGeno = pointer(geno) + (counter - 1) * nPer * sizeof(Float64);
        BLAS.blascopy!(nPer, pGenoOri, 1, pGeno, 1);
      end
    end
    nSNPredVec[idxRead] = curSNPSize - counter;

    if counter == 0
      #println("No valid SNPs at the group which starting from row ",
      #        offset + 1);
      continue;
    end

    # impute missing genotype by expected minor allele counts
    for j = 1 : counter
      for i = 1 : nPer
        if isnan(geno[i, j])
          geno[i, j] = 2 * maf[j];
        end
      end
    end

    # boundary of windows
    if idxRead == nReads
      nSNPUpdate = nSNP - (idxRead - 1) * SNPSize - nSNPredVec[idxRead];
    else
      nSNPUpdate = SNPSize - nSNPredVec[idxRead];
    end
    gLB = [1:windowSize:nSNPUpdate];
    gUB = [gLB[2:end]-1; nSNPUpdate];
    nGrps = length(gLB);
    vc0List = zeros(nGrps);
    vc1List = zeros(nGrps);
    pvalList = zeros(nGrps);

    # looping over windows
    for g = 1 : nGrps

      # get SNP weights
      grpSize = gUB[g] - gLB[g] + 1;
      if isempty(snpWtType)
        # constant weights
        if g > 1 && g < nGrps
          snpSqrtWts = fill!(snpSqrtWts, 1);
        else
          snpSqrtWts = ones(grpSize);
        end
      elseif snpWtType == "beta"
        # weights by beta density evaluated at MAF
        snpSqrtWts = sqrt(Distributions.pdf(Distributions.Beta(1, 25), maf[gLB[g] : gUB[g]]));
      elseif snpWtType == "invvar"
        # weights by inverse variance maf*(1-maf)
        snpSqrtWts = 1 ./ sqrt( maf[gLB[g] : gUB[g]] .*
                               (1 - maf[gLB[g] : gUB[g]]) );
      end

      # retrieve group genotypes
      if kernel == "GRM"
        gMAF = maf[gLB[g] : gUB[g]];
        gSNP = geno[keepIdx, gLB[g] : gUB[g]];
        # center and scale genotype matrix by MAF
        BLAS.ger!(-2.0, oneConst, gMAF, gSNP);
        if g == nGrps && grpSize != windowSize
          tmpvecKernel = tmpvecKernel[1 : grpSize];
        end
        for i = 1 : grpSize
          tmpvecKernel[i] = snpSqrtWts[i] / sqrt(2 * gMAF[i] * (1 - gMAF[i]));
        end
        scale!(gSNP, tmpvecKernel);
        #gSNP = gSNP .* (snpSqrtWts ./ sqrt(2 * gMAF .* (1 - gMAF)))';
        if g == nGrps && grpSize != windowSize
          tmpvecKernel = [tmpvecKernel; Array(Float64, windowSize - grpSize)];
        end
      elseif kernel == "IBS1"
        # Table 3 in notes
        gSNP = hcat(geno[keepIdx, gLB[g] : gUB[g]],
                    2 - geno[keepIdx, gLB[g] : gUB[g]]);
        scale!(gSNP, [snpSqrtWts; snpSqrtWts]);
      elseif kernel == "IBS2"
        # Table 2 in notes
        gSNP = hcat(geno[keepIdx, gLB[g] : gUB[g]] .>= 1,
                    geno[keepIdx, gLB[g] : gUB[g]] .<= 1);
        scale!(gSNP, [snpSqrtWts; snpSqrtWts]);
      elseif kernel == "IBS3"
        # Table 1 in notes
        gSNP = hcat(-2 * (geno[keepIdx, gLB[g] : gUB[g]] .<= 1) + 1,
                    2 * (geno[keepIdx, gLB[g] : gUB[g]] .>= 1) - 1,
                    fill(sqrt(2), nPerKeep, gUB[g] - gLB[g] + 1));
        scale!(gSNP, [snpSqrtWts; snpSqrtWts; snpSqrtWts]);
      end

      # testing

      if kinship == "none"

        if test == "eRLRT"
          (b, vc0List[g], vc1List[g], pvalList[g]) =
            vctest(ynew, [], XtNullBasis * gSNP, WPreSim = WPreSim, tests = test,
                   Vform = "half", nBlockAscent = nBlockAscent, nMMmax = nMMmax,
                   nNullSimPts = nNullSimPts, pvalueComputings = pvalueComputing,
                   nNullSimNewtonIter = nNullSimNewtonIter, tolX = tolX,
                   devices = device, PrePartialSumW = PrePartialSumW,
                   PreTotalSumW = PreTotalSumW, partialSumWConst = partialSumWConst,
                   totalSumWConst = totalSumWConst, windowSize = windowSize,
                   partialSumW = partialSumW, totalSumW = totalSumW,
                   lambda = lambda, W = W, nPreRank = nPreRank,
                   tmpmat0 = tmpmat0, tmpmat1 = tmpmat1, tmpmat2 = tmpmat2,
                   tmpmat3 = tmpmat3, tmpmat4 = tmpmat4, tmpmat5 = tmpmat5,
                   denomvec = denomvec, d1f = d1f, d2f = d2f, offset = offset,
                   nPtsChi2 = nPtsChi2, simnull = simnull);
        else
          (b, vc0List[g], vc1List[g], pvalList[g]) =
            vctest(y, X, gSNP, WPreSim = WPreSim, tests = test,
                   Vform = "half", nBlockAscent = nBlockAscent, nMMmax = nMMmax,
                   nNullSimPts = nNullSimPts, pvalueComputings = pvalueComputing,
                   nNullSimNewtonIter = nNullSimNewtonIter, tolX = tolX,
                   devices = device, PrePartialSumW = PrePartialSumW,
                   PreTotalSumW = PreTotalSumW, partialSumWConst = partialSumWConst,
                   totalSumWConst = totalSumWConst, windowSize = windowSize,
                   partialSumW = partialSumW, totalSumW = totalSumW,
                   lambda = lambda, W = W, nPreRank = nPreRank,
                   tmpmat0 = tmpmat0, tmpmat1 = tmpmat1, tmpmat2 = tmpmat2,
                   tmpmat3 = tmpmat3, tmpmat4 = tmpmat4, tmpmat5 = tmpmat5,
                   denomvec = denomvec, d1f = d1f, d2f = d2f, offset = offset,
                   nPtsChi2 = nPtsChi2, simnull = simnull);
        end

      else

        # obtain some orthonormal vectors of space R^n - [QX,QPHI,S]
        # See Golub and Van Loan (1996) Algorithm 12.4.2 on p602
        if g == nGrps && grpSize != windowSize
          tmpMat = tmpMat[1 : grpSize, :];
        end
        BLAS.gemm!('T', 'N', 1.0, gSNP, XPhitNullBasis, 0.0, tmpMat);
        (UXPhitS, svalXPhitS, VXPhitS) = svd(tmpMat, thin = false);
        if g == nGrps && grpSize != windowSize
          tmpMat = [tmpMat; Array(Float64, windowSize - grpSize,
                                  size(XPhitNullBasis, 2))];
        end
        rankXPhitS =
          countnz(svalXPhitS .>
                  max(size(XPhitNullBasis, 2),
                      size(gSNP, 2) * eps(svalXPhitS[1])));
        pXPhitSV = pointer(VXPhitS) +
          rankXPhitS * size(XPhitNullBasis, 2) * sizeof(Float64);
        BLAS.blascopy!(size(XPhitNullBasis, 2) * rankQPhi,
                       pXPhitSV, 1, pSubXPhitSV, 1);
        BLAS.gemm!('N', 'N', 1.0, XPhitNullBasis, subXPhitSV, 0.0, QRes);

        # working Y and working non-trivial variance component
        #yWork = (PhiAdjsvd[:U]' * (yShift + KPhiAdj * (QRes' * y))) ./ sqrt(evalPhiAdj);
        BLAS.blascopy!(length(yShift), yShift, 1, tmpvec, 1);
        BLAS.gemv!('T', 1.0, QRes, y, 0.0, tmpvecQRes);
        BLAS.gemv!('N', 1.0, KPhiAdj, tmpvecQRes, 1.0, tmpvec);
        #yWork = BLAS.gemv('T', 1.0, PhiAdjsvd[:U], tmpvec) ./ sqrt(evalPhiAdj);
        BLAS.gemv!('T', 1.0, weightedW, tmpvec, 0.0, yWork);
        #for i = 1 : length(yWork)
        #  yWork[i] = yWork[i] * InvSqrtevalPhiAdj[i];
        #end
        #VWorkSqrt = PhiAdjsvd[:U]' * (QPhi' * gSNP) ./ sqrt(evalPhiAdj);
        if g == nGrps && grpSize != windowSize
          VWorkSqrt = VWorkSqrt[:, 1 : grpSize];
          VWorkSqrt2 = VWorkSqrt2[:, 1 : grpSize];
        end
        BLAS.gemm!('T', 'N', 1.0, QPhi, gSNP, 0.0, VWorkSqrt2);
        BLAS.gemm!('T', 'N', 1.0, weightedW, VWorkSqrt2, 0.0, VWorkSqrt);
        #scale!(InvSqrtevalPhiAdj, VWorkSqrt);

        # obtain LRT and p-value
        (b, vc0List[g], vc1List[g], pvalList[g]) =
          vctest(yWork, [], VWorkSqrt, WPreSim = WPreSim, tests = test,
                 Vform = "half", nBlockAscent = nBlockAscent, nMMmax = nMMmax,
                 nNullSimPts = nNullSimPts, pvalueComputings = pvalueComputing,
                 nNullSimNewtonIter = nNullSimNewtonIter, tolX = tolX,
                 devices = device, PrePartialSumW = PrePartialSumW,
                 PreTotalSumW = PreTotalSumW, partialSumWConst = partialSumWConst,
                 totalSumWConst = totalSumWConst, windowSize = windowSize,
                 partialSumW = partialSumW, totalSumW = totalSumW,
                 lambda = lambda, W = W, nPreRank = nPreRank,
                 tmpmat0 = tmpmat0, tmpmat1 = tmpmat1, tmpmat2 = tmpmat2,
                 tmpmat3 = tmpmat3, tmpmat4 = tmpmat4, tmpmat5 = tmpmat5,
                 denomvec = denomvec, d1f = d1f, d2f = d2f, offset = offset,
                 nPtsChi2 = nPtsChi2, simnull = simnull);

        # recover VWorkSqrt
        if g == nGrps && grpSize != windowSize
          VWorkSqrt = hcat(VWorkSqrt,
                           Array(Float64, length(evalPhiAdj),
                                 windowSize - grpSize));
          VWorkSqrt2 = hcat(VWorkSqrt2,
                            Array(Float64, length(evalPhiAdj),
                                  windowSize - grpSize));
        end

      end

      idx += 1;
      results[idx, :] = hcat(snpIDred[gLB[g]], snpIDred[gUB[g]], pvalList[g]);

    end

  end

  return results[1:idx, :];

end


"""
Object with a *term* and an applicable *BasisFunction* and a eventfield that are later passed to the basisfunction.

$(TYPEDEF)
$(TYPEDSIGNATURES)
$(FIELDS)
# Examples
```julia-repl
julia>  b = TimeExpandedTerm(term,kernel,[:latencyTR,:durationTR])
```
"""
struct TimeExpandedTerm{T<:AbstractTerm} <: AbstractTerm
        "Term that the basis function is applied to. This is regularly called in other functions to get e.g. term-coefnames and timeexpand those"
        term::T
        "Kernel that determines what should happen to the designmatrix of the term"
        basisfunction::BasisFunction
        "Which fields of the event-table should be passed to the basisfunction.Important: The first entry has to be the event-latency in samples!"
        eventfields::Array{Symbol}
end

function TimeExpandedTerm(term::NTuple{N,Union{<:AbstractTerm,<:MixedModels.RandomEffectsTerm}},basisfunction,eventfields::Array{Symbol,1}) where {N}
        # for mixed models, apply it to each Term
        TimeExpandedTerm.(term,Ref(basisfunction),Ref(eventfields))
end
function TimeExpandedTerm(term,basisfunction,eventfields::Nothing)
        # if the eventfield is nothing, call default
        TimeExpandedTerm(term, basisfunction)
end

function TimeExpandedTerm(term,basisfunction,eventfields::Symbol)
        # call with symbol, convert to array of symbol
        TimeExpandedTerm(term, basisfunction,[eventfields])
end

function TimeExpandedTerm(term,basisfunction;eventfields=[:latency])
        # default call, use latency
        TimeExpandedTerm(term, basisfunction,eventfields)
end

function Base.show(io::IO, p::TimeExpandedTerm)
        #print(io, "timeexpand($(p.term), $(p.basisfunction.type),$(p.basisfunction.times))")
        println(io,"$(coefnames(p))")
end

"""
To be explored abstractTerm structure to automatically detect randomeffects. Not in use
"""
struct ZeroCorr2{T<:RandomEffectsTerm} <: AbstractTerm
    term::T
end

"""
Object with a *term* and an applicable *BasisFunction* and a eventfield that are later passed to the basisfunction.

$(TYPEDEF)
$(TYPEDSIGNATURES)
$(FIELDS)
# Examples
```julia-repl
julia>  b = TimeExpandedTerm(term,kernel,[:latencyTR,:durationTR])
```
"""
struct DesignMatrix
        "Array of formulas"
        formulas
        "A concatenated designmatric. In case of Mixed Models an array, where the first one is a FeMat, later ones ReMats. "
        Xs
        "Event table with all events"
        events
end

"""
$(SIGNATURES)
Combine two UnfoldDesignmatrices. This allows combination of multiple events.

This also allows to define events with different lengths.

Not supported for models without timebasis, as it is not needed there (one can simply run multiple models)
# Examples
```julia-repl
julia>  basisfunction1 = firbasis(τ=(0,1),sfreq = 10,name="basis1")
julia>  basisfunction2 = firbasis(τ=(0,0.5),sfreq = 10,name="basis2")
julia>  Xdc1          = designmatrix(UnfoldLinearModel,@formula 0~1,tbl_1,basisfunction1)
julia>  Xdc2          = designmatrix(UnfoldLinearModel,@formula 0~1,tbl_2,basisfunction2)
julia>  combineDesignmatrices(Xdc1,Xdc2)
julia>  Xdc = Xdc1+Xdc2 # equivalently
```

"""
function combineDesignmatrices(X1::DesignMatrix,X2::DesignMatrix)
        X1 = deepcopy(X1)
        X2 = deepcopy(X2)
        Xs1 = get_Xs(X1.Xs)
        Xs2 = get_Xs(X2.Xs)

        sX1 = size(Xs1,1)
        sX2 = size(Xs2,1)

        # append 0 to the shorter designmat
        if sX1 < sX2
                Xs1 = SparseMatrixCSC(sX2, Xs1.n, Xs1.colptr, Xs1.rowval, Xs1.nzval)
        elseif sX2 < sX1
                Xs2 = SparseMatrixCSC(sX1, Xs2.n, Xs2.colptr, Xs2.rowval, Xs2.nzval)
        end
        Xcomb = hcat(Xs1,Xs2)

        #if !(Float64(X1.formulas.rhs.basisfunction.times.step) ≈ Float64(X2.formulas.rhs.basisfunction.times.step))
        #        @warn("Concatenating formulas with different sampling rates. Be sure that this is what you want.")
        #end
        if typeof(X1.Xs) <: Tuple
                # we have random effects
                # combine REMats in single-eventtpe formulas ala y ~ (1|x) + (a|x)
                Xs1 = MixedModels._amalgamate([X1.Xs[2:end]...],Float64)
                Xs2 = MixedModels._amalgamate([X2.Xs[2:end]...],Float64)

                Xcomb = (Xcomb,Xs1...,Xs2...)

                # Next we make the ranefs all equal size
                equalizeReMatLengths!(Xcomb[2:end])

                # check if ranefs can be amalgamated. If this fails, then MixedModels tried to amalgamate over different eventtypes and we should throw the warning
                # if it success, we have to check if the size before and after is identical. If it is not, it tried to amalgamize over different eventtypes which were of the same length

                try
                reterms = MixedModels._amalgamate([Xcomb[2:end]...],Float64)
                if length(reterms) != length(Xcomb[2:end])
                        throw("IncompatibleRandomGroupings")
                end
                catch e
                        @error "Error, you seem to have two different eventtypes with the same random-effect grouping variable. \n
                        This is not allowed, you have to rename one. Example:\n
                        eventA: y~1+(1|item) \n
                        eventB: y~1+(1|item)  \n
                        This leads to this error. Rename the later one\n
                        eventB: y~1+(1|itemB) "
                        throw("IncompatibleRandomGroupings")
                end

        end

        if typeof(X1.formulas) <: FormulaTerm
                DesignMatrix([X1.formulas X2.formulas],Xcomb,[X1.events, X2.events])
        else
                DesignMatrix([X1.formulas... X2.formulas],Xcomb,[X1.events, X2.events])
        end
end

function changeMatSize!(m,fe,remats)
        changeReMatSize!.(remats,Ref(m))
        fe = SparseMatrixCSC(m, fe.n, fe.colptr,fe.rowval, fe.nzval)
        return (fe,remats...,)
end
function changeReMatSize!(remat::MixedModels.AbstractReMat,m ::Integer)

        n = m-length(remat.refs) # missing elements
        if n<0
                deleteat!(remat.refs,range(m+1,stop=length(remat.refs)))
                remat.adjA = remat.adjA[:,1:(m+1)]
                remat.wtz = remat.wtz[:,1:(m+1)]
                remat.z = remat.z[:,1:(m+1)]
        elseif n>0
                append!(remat.refs,repeat([remat.refs[end]],n))
                remat.adjA = [remat.adjA sparse(repeat([0],size(remat.adjA)[1],n))]
                remat.wtz = [remat.wtz zeros((size(remat.wtz)[1],n))]
                remat.z = [remat.z zeros((size(remat.z)[1],n))]
        end

        #ReMat{T,S}(remat.trm, refs, levels, cnames, z, wtz, λ, inds, adjA, scratch)
end
function equalizeReMatLengths!(remats::NTuple{A,MixedModels.AbstractReMat}) where {A}
        # find max length
        m = maximum([x[1] for x in size.(remats)])
        @debug print("combining lengths: $m")
        # for each reMat
        for k = range(1,length=length(remats))
                remat = remats[k]
                if size(remat)[1] == m
                        continue
                end
                # prolong if necessary
                changeReMatSize!(remat,m)

        end
end

Base.:+(X1::DesignMatrix, X2::DesignMatrix) = combineDesignmatrices(X1,X2)

function get_Xs(Xs::Tuple)
        return Xs[1]
end
function get_Xs(Xs::SparseMatrixCSC)
        return Xs
end

"""
$(SIGNATURES)
designmatrix(type, f, tbl; kwargs...)
Return a *DesignMatrix* used to fit the models.
# Arguments
- type::Union{UnfoldLinearMixedModel,UnfoldLinearModel}
- f::FormulaTerm: Formula to be used in this designmatrix
- tbl: Events (usually a data frame) to be modelled
- basisfunction::BasisFunction: basisfunction to be used in modeling (if specified)
- contrasts::Dict: (optional) contrast to be applied to formula
- eventfields::Array: (optional) Array of symbols which are passed to basisfunction event-wise.
First field of array always defines eventonset in samples. Default is [:latency]

# Examples
```julia-repl
julia>  designmatrix(UnfoldLinearModel,f,tbl,basisfunction1)
```

"""
function designmatrix(type,f,tbl,basisfunction;contrasts= Dict{Symbol,Any}(), kwargs...)
        @debug("generating DesignMatrix")
        form = apply_schema(f, schema(f, tbl, contrasts), MixedModels.LinearMixedModel)
        form = apply_basisfunction(form,basisfunction,get(Dict(kwargs),:eventfields,nothing))

        # Evaluate the designmatrix
        if (!isnothing(basisfunction)) & (type<:UnfoldLinearMixedModel)
                X = modelcols.(form.rhs, Ref(tbl))
        else
                X = modelcols(form.rhs, tbl)
        end
        return DesignMatrix(form,X,tbl)
end
function designmatrix(type,f,tbl;kwargs...)
        return designmatrix(type,f,tbl,nothing;kwargs...)
end

"""
$(SIGNATURES)
timeexpand the rhs-term of the formula with the basisfunction

"""

function apply_basisfunction(form,basisfunction::BasisFunction,eventfields)
        @debug("apply_basisfunction")
        return FormulaTerm(form.lhs, TimeExpandedTerm(form.rhs,basisfunction,eventfields))
end

function apply_basisfunction(form,basisfunction::Nothing,eventfields)
        # in case of no basisfunctin, do nothing
        return form
end

"""
$(SIGNATURES)
calculates the actual designmatrix for a timeexpandedterm. Multiple dispatch on StatsModels.modelcols
"""
# Timeexpand the fixed effect part
function StatsModels.modelcols(term::TimeExpandedTerm,tbl)

        X = modelcols(term.term,tbl)
        time_expand(X,term,tbl)
end

mutable struct SparseReMat{T,S} <: MixedModels.AbstractReMat{T}
        trm
        refs::Vector{Int32}
        levels
        cnames::Vector{String}
        z::SparseMatrixCSC{T}
        wtz::SparseMatrixCSC{T}
        λ::LowerTriangular{T,Matrix{T}}
        inds::Vector{Int}
        adjA::SparseMatrixCSC{T,Int32}
        scratch::Matrix{T}
    end
# This function timeexpands the random effects and generates a ReMat object
function StatsModels.modelcols(term::TimeExpandedTerm{<:RandomEffectsTerm},tbl)
#function StatsModels.modelcols(term::TimeExpandedTerm{<:Union{<:RandomEffectsTerm,<:AbstractTerm{<:RandomEffectsTerm}}},tbl)
# exchange this to get ZeroCorr to work

        # get the non-timeexpanded reMat
        reMat = modelcols(term.term,tbl)

        # Timeexpand the designmatrix
        z = transpose(time_expand(transpose(reMat.z),term,tbl))

        # First we check if there is overlap in the timeexpanded term. If so, we cannot continue. Later implementations will remedy that
        #println(dump(term,))
        if hasfield(typeof(term.term),:rhs)
                rhs = term.term.rhs

        elseif hasfield(typeof(term.term.term),:rhs)
                # we probably have something like zerocorr, which does not need to show a .rhs necessarily
                rhs = term.term.term.rhs
        else

                println("term.term: $(dump(term.term))")
                error("unknown RE structure, has no field .rhs:$(typeof(term.term))")

        end

        group = tbl[!,rhs.sym]
        time = tbl[!,term.eventfields[1]]

        # get the from-to onsets of the grouping varibales
        onsets = time_expand_getRandomGrouping(group,time,term.basisfunction)
        #print(size(reMat.z))
        refs = zeros(size(z)[2]).+1
        for (i,o) in enumerate(onsets[2:end])
                # check for overlap
                if (minimum(o) <= maximum(onsets[i+1])) & (maximum(o) <= minimum(onsets[i+1]))
                        error("overlap in random effects structure detected, not currently supported")
                end
        end

        # From now we can assume no overlap
        # We want to fnd which subject is active when
        refs = zeros(size(z)[2]).+1
        uGroup = unique(group)

        for (i,g) = enumerate(uGroup[1:end])

                ix_start = findfirst(g.==group)
                ix_end = findlast(g.==group)
                if i == 1
                        time_start = 1
                else
                        time_start = time[ix_start]
                        time_start = time_start - sum(term.basisfunction.times.<=0)
                end
                if i == length(uGroup)
                        time_stop = size(refs,1)
                else
                        time_stop = time[ix_end]
                        time_stop = time_stop + sum(term.basisfunction.times.>0)
                end
                if time_start < 0
                        time_start = 1
                end

                if time_stop > size(refs,1)
                        time_stop = size(refs,1)
                end

                #println("$g,$time_start,$time_stop")
                refs[Int64(time_start):Int64(time_stop)] .= g
        end

        # Other variables with implementaions taken from the LinerMixedModel function
        wtz = z
        trm = term

        S = size(z, 1)
        T = eltype(z)
        λ  = LowerTriangular(Matrix{T}(I, S, S))

        inds = MixedModels.sizehint!(Int[], (S * (S + 1)) >> 1)
        m = reshape(1:abs2(S), (S, S))
        inds = sizehint!(Int[], (S * (S + 1)) >> 1)
        for j in 1:S
                for i in j:S
                        # We currently restrict to diagonal entries
                        # Once mixedmodels#293 is pushed, we can relax this and use zerocorr()
                        if i == j # for diagonal
                                push!(inds, m[i, j])
                        end
                end
        end

        levels = reMat.levels
        refs =refs

        # reMat.levels doesnt change
        cnames = coefnames(term)
        #print(refs)
        adjA = MixedModels.adjA(refs, z)
        scratch = Matrix{T}(undef, (S, length(uGroup)))

        # Once MixedModels.jl supports it, can be replaced with: SparseReMat
        ReMat{T,S}(rhs,
        refs,
        levels,
        cnames,
        z,
        wtz,
        λ,
        inds,
        adjA,
        scratch)
end

"""
$(SIGNATURES)
Get the timeranges where the random grouping variable was applied
"""
function time_expand_getRandomGrouping(tblGroup,tblLatencies,basisfunction)
        ranges = time_expand_getTimeRange.(tblLatencies,Ref(basisfunction))
end

# helper function to get the ranges from where to where the basisfunction is added
function time_expand_getTimeRange(onset,basisfunction)
        npos = sum(basisfunction.times.>=0)
        nneg = sum(basisfunction.times.<0)

        basis = basisfunction.kernel(onset)

        fromRowIx = floor(onset)-nneg
        toRowIx = floor(onset)+npos

        range(fromRowIx,stop=toRowIx)
end

"""
$(SIGNATURES)
performs the actual time-expansion in a sparse way.

 - Get the non-timeexpanded designmatrix X from StatsModels.
 - evaluate the basisfunction kernel at each event
 - calculate the necessary rows, cols and values for the sparse matrix
 Returns SparseMatrixCSC
"""
function time_expand(X,term,tbl)
        ncolsBasis = size(term.basisfunction.kernel(0),2)
        X = reshape(X,size(X,1),:)

        ncolsX = size(X)[2]
        nrowsX = size(X)[1]
        ncolsXdc = ncolsBasis*ncolsX

        # this is the predefined eventfield, usually "latency"
        onsets = tbl[!,term.eventfields[1]]

        if typeof(term.eventfields) <:Array && length(term.eventfields) == 1
                bases = term.basisfunction.kernel.(tbl[!,term.eventfields[1]])
        else
                bases = term.basisfunction.kernel.(eachrow(tbl[!,term.eventfields]))
        end

        # generate rowindices
        rows =  copy(rowvals.(bases))
        # this shift is necessary as some basisfunction time-points can be negative. But a matrix is always from 1:τ. Thus we have to shift it backwards in time.
        # The onsets are onsets-1 XXX not sure why.
        for r in 1:length(rows)
                rows[r] .+= floor(onsets[r]-1)+term.basisfunction.shiftOnset
        end

        rows = vcat(rows...)
        rows = repeat(rows,ncolsX)

        # generate column indices
        cols = []
        for Xcol in 1:ncolsX
                for b in 1:length(bases)
                        for c in 1:ncolsBasis
                                push!(cols,repeat([c+(Xcol-1)*ncolsBasis],length(nzrange(bases[b],c))))
                        end
                end
        end
        cols = vcat(cols...)

        # generate values
        vals = []
        for Xcol in 1:ncolsX
                push!(vals,vcat(nonzeros.(bases).*X[:,Xcol]...))
        end

        vals = vcat(vals...)
        ix = rows.>0
        A = sparse(rows[ix],cols[ix],vals[ix])

        return A
end

"""
$(SIGNATURES)
coefnames of a TimeExpandedTerm concatenates the basis-function name with the kronecker product of the term name and the basis-function colnames. Separator is ' : '
Some examples for a firbasis:
        basis_313 : (Intercept) : 0.1
        basis_313 : (Intercept) : 0.2
        basis_313 : (Intercept) : 0.3
        ...
"""
function StatsModels.coefnames(term::TimeExpandedTerm)
        terms = coefnames(term.term)
        colnames = term.basisfunction.colnames
        name = term.basisfunction.name
        if typeof(terms) == String
                terms = [terms]
        end
        return name.*" : ".*kron(terms.*" : ",string.(colnames))
end

function StatsModels.coefnames(terms::AbstractArray{<:FormulaTerm})
        return coefnames.(Base.getproperty.(terms,:rhs))
end
function StatsModels.coefnames(term::MixedModels.ZeroCorr)
        coefnames(term.term)
end

function StatsModels.coefnames(term::RandomEffectsTerm)
        coefnames(term.lhs)
end


#=
utilities for Pardiso
only works with Float64

TODO add to docs following example:
```julia
import Hypatia

ENV["OMP_NUM_THREADS"] = length(Sys.cpu_info())
import Pardiso

syssolver = Hypatia.Solvers.NaiveElimSparseSystemSolver{Float64}(fact_cache = Hypatia.PardisoNonSymCache())
solver = Hypatia.Solvers.Solver{Float64}(syssolver)

include("test/native.jl")
nonnegative1(Float64, solver = solver)
```
=#

mutable struct PardisoNonSymCache{Float64} <: SparseNonSymCache{Float64}
    analyzed::Bool
    pardiso::Pardiso.MKLPardisoSolver
    function PardisoNonSymCache{Float64}()
        cache = new{Float64}()
        cache.analyzed = false
        cache.pardiso = Pardiso.MKLPardisoSolver()
        Pardiso.set_matrixtype!(cache.pardiso, Pardiso.REAL_NONSYM)
        return cache
    end
end

mutable struct PardisoSymCache{Float64} <: SparseSymCache{Float64}
    analyzed::Bool
    pardiso::Pardiso.MKLPardisoSolver
    function PardisoSymCache{Float64}()
        cache = new{Float64}()
        cache.analyzed = false
        cache.pardiso = Pardiso.MKLPardisoSolver()
        Pardiso.set_matrixtype!(cache.pardiso, Pardiso.REAL_SYM_INDEF)
        return cache
    end
end

PardisoSparseCache = Union{PardisoSymCache{Float64}, PardisoNonSymCache{Float64}}
int_type(::PardisoSparseCache) = Int32

function update_fact(
    cache::PardisoSparseCache{Float64},
    A::SparseMatrixCSC{Float64, Int32},
    )
    pardiso = cache.pardiso

    if !cache.analyzed
        Pardiso.pardisoinit(pardiso)
        # solve transposed problem (Pardiso accepts CSR matrices)
        Pardiso.fix_iparm!(pardiso, :N)
        Pardiso.set_phase!(pardiso, Pardiso.ANALYSIS)
        Pardiso.pardiso(pardiso, A, Float64[])
        cache.analyzed = true
    end

    Pardiso.set_phase!(pardiso, Pardiso.NUM_FACT)
    Pardiso.pardiso(pardiso, A, Float64[])

    return
end

function inv_prod(
    cache::PardisoSparseCache{Float64},
    x::Vector{Float64},
    A::SparseMatrixCSC{Float64, Int32},
    b::Vector{Float64},
    )
    pardiso = cache.pardiso

    Pardiso.set_phase!(pardiso, Pardiso.SOLVE_ITERATIVE_REFINE)
    Pardiso.pardiso(pardiso, x, A, b)

    return x
end

function free_memory(cache::PardisoSparseCache{Float64})
    Pardiso.set_phase!(cache.pardiso, Pardiso.RELEASE_ALL)
    Pardiso.pardiso(cache.pardiso)
    return
end