global const PetscErrorType = Int32
global const PETSC_ERROR_INITIAL = Int32( 0 )
global const PETSC_ERROR_REPEAT = Int32( 1 )
global const PETSC_ERROR_IN_CXX = Int32( 2 )

global const KSPConvergedReason = Cint
global const KSP_CONVERGED_RTOL_NORMAL = Cint( 1 )
global const KSP_CONVERGED_ATOL_NORMAL = Cint( 9 )
global const KSP_CONVERGED_RTOL = Cint( 2 )
global const KSP_CONVERGED_ATOL = Cint( 3 )
global const KSP_CONVERGED_ITS = Cint( 4 )
global const KSP_CONVERGED_CG_NEG_CURVE = Cint( 5 )
global const KSP_CONVERGED_CG_CONSTRAINED = Cint( 6 )
global const KSP_CONVERGED_STEP_LENGTH = Cint( 7 )
global const KSP_CONVERGED_HAPPY_BREAKDOWN = Cint( 8 )
global const KSP_DIVERGED_NULL = Cint( -2 )
global const KSP_DIVERGED_ITS = Cint( -3 )
global const KSP_DIVERGED_DTOL = Cint( -4 )
global const KSP_DIVERGED_BREAKDOWN = Cint( -5 )
global const KSP_DIVERGED_BREAKDOWN_BICG = Cint( -6 )
global const KSP_DIVERGED_NONSYMMETRIC = Cint( -7 )
global const KSP_DIVERGED_INDEFINITE_PC = Cint( -8 )
global const KSP_DIVERGED_NANORINF = Cint( -9 )
global const KSP_DIVERGED_INDEFINITE_MAT = Cint( -10 )
global const KSP_DIVERGED_PCSETUP_FAILED = Cint( -11 )
global const KSP_CONVERGED_ITERATING = Cint( 0 )

global const MatOption = Cint
global const MAT_OPTION_MIN = Cint( -3 )
global const MAT_NEW_NONZERO_LOCATION_ERR = Cint( 11 )
global const MAT_UNUSED_NONZERO_LOCATION_ERR = Cint( -2 )
global const MAT_NEW_NONZERO_ALLOCATION_ERR = Cint( 19 )
global const MAT_ROW_ORIENTED = Cint( -1 )
global const MAT_SYMMETRIC = Cint( 1 )
global const MAT_STRUCTURALLY_SYMMETRIC = Cint( 2 )
global const MAT_NEW_DIAGONALS = Cint( 3 )
global const MAT_IGNORE_OFF_PROC_ENTRIES = Cint( 4 )
global const MAT_USE_HASH_TABLE = Cint( 5 )
global const MAT_KEEP_NONZERO_PATTERN = Cint( 6 )
global const MAT_IGNORE_ZERO_ENTRIES = Cint( 7 )
global const MAT_USE_INODES = Cint( 8 )
global const MAT_HERMITIAN = Cint( 9 )
global const MAT_SYMMETRY_ETERNAL = Cint( 10 )
global const MAT_IGNORE_LOWER_TRIANGULAR = Cint( 12 )
global const MAT_ERROR_LOWER_TRIANGULAR = Cint( 13 )
global const MAT_GETROW_UPPERTRIANGULAR = Cint( 14 )
global const MAT_SPD = Cint( 15 )
global const MAT_NO_OFF_PROC_ZERO_ROWS = Cint( 16 )
global const MAT_NO_OFF_PROC_ENTRIES = Cint( 17 )
global const MAT_NEW_NONZERO_LOCATIONS = Cint( 18 )
global const MAT_OPTION_MAX = Cint( 21 )

global const MatInfoType = Int32
global const MAT_LOCAL = Int32( 1 )
global const MAT_GLOBAL_MAX = Int32( 2 )
global const MAT_GLOBAL_SUM = Int32( 3 )

global const KSPNormType = Cint
global const KSP_NORM_DEFAULT = Cint( -1 )
global const KSP_NORM_NONE = Cint( 0 )
global const KSP_NORM_PRECONDITIONED = Cint( 1 )
global const KSP_NORM_UNPRECONDITIONED = Cint( 2 )
global const KSP_NORM_NATURAL = Cint( 3 )

global const MatOperation = UInt32
global const MATOP_SET_VALUES = UInt32( 0 )
global const MATOP_GET_ROW = UInt32( 1 )
global const MATOP_RESTORE_ROW = UInt32( 2 )
global const MATOP_MULT = UInt32( 3 )
global const MATOP_MULT_ADD = UInt32( 4 )
global const MATOP_MULT_TRANSPOSE = UInt32( 5 )
global const MATOP_MULT_TRANSPOSE_ADD = UInt32( 6 )
global const MATOP_SOLVE = UInt32( 7 )
global const MATOP_SOLVE_ADD = UInt32( 8 )
global const MATOP_SOLVE_TRANSPOSE = UInt32( 9 )
global const MATOP_SOLVE_TRANSPOSE_ADD = UInt32( 10 )
global const MATOP_LUFACTOR = UInt32( 11 )
global const MATOP_CHOLESKYFACTOR = UInt32( 12 )
global const MATOP_SOR = UInt32( 13 )
global const MATOP_TRANSPOSE = UInt32( 14 )
global const MATOP_GETINFO = UInt32( 15 )
global const MATOP_EQUAL = UInt32( 16 )
global const MATOP_GET_DIAGONAL = UInt32( 17 )
global const MATOP_DIAGONAL_SCALE = UInt32( 18 )
global const MATOP_NORM = UInt32( 19 )
global const MATOP_ASSEMBLY_BEGIN = UInt32( 20 )
global const MATOP_ASSEMBLY_END = UInt32( 21 )
global const MATOP_SET_OPTION = UInt32( 22 )
global const MATOP_ZERO_ENTRIES = UInt32( 23 )
global const MATOP_ZERO_ROWS = UInt32( 24 )
global const MATOP_LUFACTOR_SYMBOLIC = UInt32( 25 )
global const MATOP_LUFACTOR_NUMERIC = UInt32( 26 )
global const MATOP_CHOLESKY_FACTOR_SYMBOLIC = UInt32( 27 )
global const MATOP_CHOLESKY_FACTOR_NUMERIC = UInt32( 28 )
global const MATOP_SETUP_PREALLOCATION = UInt32( 29 )
global const MATOP_ILUFACTOR_SYMBOLIC = UInt32( 30 )
global const MATOP_ICCFACTOR_SYMBOLIC = UInt32( 31 )
global const MATOP_DUPLICATE = UInt32( 34 )
global const MATOP_FORWARD_SOLVE = UInt32( 35 )
global const MATOP_BACKWARD_SOLVE = UInt32( 36 )
global const MATOP_ILUFACTOR = UInt32( 37 )
global const MATOP_ICCFACTOR = UInt32( 38 )
global const MATOP_AXPY = UInt32( 39 )
global const MATOP_GET_SUBMATRICES = UInt32( 40 )
global const MATOP_INCREASE_OVERLAP = UInt32( 41 )
global const MATOP_GET_VALUES = UInt32( 42 )
global const MATOP_COPY = UInt32( 43 )
global const MATOP_GET_ROW_MAX = UInt32( 44 )
global const MATOP_SCALE = UInt32( 45 )
global const MATOP_SHIFT = UInt32( 46 )
global const MATOP_DIAGONAL_SET = UInt32( 47 )
global const MATOP_ZERO_ROWS_COLUMNS = UInt32( 48 )
global const MATOP_SET_RANDOM = UInt32( 49 )
global const MATOP_GET_ROW_IJ = UInt32( 50 )
global const MATOP_RESTORE_ROW_IJ = UInt32( 51 )
global const MATOP_GET_COLUMN_IJ = UInt32( 52 )
global const MATOP_RESTORE_COLUMN_IJ = UInt32( 53 )
global const MATOP_FDCOLORING_CREATE = UInt32( 54 )
global const MATOP_COLORING_PATCH = UInt32( 55 )
global const MATOP_SET_UNFACTORED = UInt32( 56 )
global const MATOP_PERMUTE = UInt32( 57 )
global const MATOP_SET_VALUES_BLOCKED = UInt32( 58 )
global const MATOP_GET_SUBMATRIX = UInt32( 59 )
global const MATOP_DESTROY = UInt32( 60 )
global const MATOP_VIEW = UInt32( 61 )
global const MATOP_CONVERT_FROM = UInt32( 62 )
global const MATOP_MATMAT_MULT = UInt32( 63 )
global const MATOP_MATMAT_MULT_SYMBOLIC = UInt32( 64 )
global const MATOP_MATMAT_MULT_NUMERIC = UInt32( 65 )
global const MATOP_SET_LOCAL_TO_GLOBAL_MAP = UInt32( 66 )
global const MATOP_SET_VALUES_LOCAL = UInt32( 67 )
global const MATOP_ZERO_ROWS_LOCAL = UInt32( 68 )
global const MATOP_GET_ROW_MAX_ABS = UInt32( 69 )
global const MATOP_GET_ROW_MIN_ABS = UInt32( 70 )
global const MATOP_CONVERT = UInt32( 71 )
global const MATOP_SET_COLORING = UInt32( 72 )
global const MATOP_SET_VALUES_ADIFOR = UInt32( 74 )
global const MATOP_FD_COLORING_APPLY = UInt32( 75 )
global const MATOP_SET_FROM_OPTIONS = UInt32( 76 )
global const MATOP_MULT_CONSTRAINED = UInt32( 77 )
global const MATOP_MULT_TRANSPOSE_CONSTRAIN = UInt32( 78 )
global const MATOP_FIND_ZERO_DIAGONALS = UInt32( 79 )
global const MATOP_MULT_MULTIPLE = UInt32( 80 )
global const MATOP_SOLVE_MULTIPLE = UInt32( 81 )
global const MATOP_GET_INERTIA = UInt32( 82 )
global const MATOP_LOAD = UInt32( 83 )
global const MATOP_IS_SYMMETRIC = UInt32( 84 )
global const MATOP_IS_HERMITIAN = UInt32( 85 )
global const MATOP_IS_STRUCTURALLY_SYMMETRIC = UInt32( 86 )
global const MATOP_SET_VALUES_BLOCKEDLOCAL = UInt32( 87 )
global const MATOP_GET_VECS = UInt32( 88 )
global const MATOP_MAT_MULT = UInt32( 89 )
global const MATOP_MAT_MULT_SYMBOLIC = UInt32( 90 )
global const MATOP_MAT_MULT_NUMERIC = UInt32( 91 )
global const MATOP_PTAP = UInt32( 92 )
global const MATOP_PTAP_SYMBOLIC = UInt32( 93 )
global const MATOP_PTAP_NUMERIC = UInt32( 94 )
global const MATOP_MAT_TRANSPOSE_MULT = UInt32( 95 )
global const MATOP_MAT_TRANSPOSE_MULT_SYMBO = UInt32( 96 )
global const MATOP_MAT_TRANSPOSE_MULT_NUMER = UInt32( 97 )
global const MATOP_CONJUGATE = UInt32( 102 )
global const MATOP_SET_VALUES_ROW = UInt32( 104 )
global const MATOP_REAL_PART = UInt32( 105 )
global const MATOP_IMAGINARY_PART = UInt32( 106 )
global const MATOP_GET_ROW_UPPER_TRIANGULAR = UInt32( 107 )
global const MATOP_RESTORE_ROW_UPPER_TRIANG = UInt32( 108 )
global const MATOP_MAT_SOLVE = UInt32( 109 )
global const MATOP_GET_REDUNDANT_MATRIX = UInt32( 110 )
global const MATOP_GET_ROW_MIN = UInt32( 111 )
global const MATOP_GET_COLUMN_VECTOR = UInt32( 112 )
global const MATOP_MISSING_DIAGONAL = UInt32( 113 )
global const MATOP_GET_SEQ_NONZERO_STRUCTUR = UInt32( 114 )
global const MATOP_CREATE = UInt32( 115 )
global const MATOP_GET_GHOSTS = UInt32( 116 )
global const MATOP_GET_LOCAL_SUB_MATRIX = UInt32( 117 )
global const MATOP_RESTORE_LOCALSUB_MATRIX = UInt32( 118 )
global const MATOP_MULT_DIAGONAL_BLOCK = UInt32( 119 )
global const MATOP_HERMITIAN_TRANSPOSE = UInt32( 120 )
global const MATOP_MULT_HERMITIAN_TRANSPOSE = UInt32( 121 )
global const MATOP_MULT_HERMITIAN_TRANS_ADD = UInt32( 122 )
global const MATOP_GET_MULTI_PROC_BLOCK = UInt32( 123 )
global const MATOP_FIND_NONZERO_ROWS = UInt32( 124 )
global const MATOP_GET_COLUMN_NORMS = UInt32( 125 )
global const MATOP_INVERT_BLOCK_DIAGONAL = UInt32( 126 )
global const MATOP_GET_SUB_MATRICES_PARALLE = UInt32( 128 )
global const MATOP_SET_VALUES_BATCH = UInt32( 129 )
global const MATOP_TRANSPOSE_MAT_MULT = UInt32( 130 )
global const MATOP_TRANSPOSE_MAT_MULT_SYMBO = UInt32( 131 )
global const MATOP_TRANSPOSE_MAT_MULT_NUMER = UInt32( 132 )
global const MATOP_TRANSPOSE_COLORING_CREAT = UInt32( 133 )
global const MATOP_TRANS_COLORING_APPLY_SPT = UInt32( 134 )
global const MATOP_TRANS_COLORING_APPLY_DEN = UInt32( 135 )
global const MATOP_RART = UInt32( 136 )
global const MATOP_RART_SYMBOLIC = UInt32( 137 )
global const MATOP_RART_NUMERIC = UInt32( 138 )
global const MATOP_SET_BLOCK_SIZES = UInt32( 139 )
global const MATOP_AYPX = UInt32( 140 )
global const MATOP_RESIDUAL = UInt32( 141 )
global const MATOP_FDCOLORING_SETUP = UInt32( 142 )
global const MATOP_MPICONCATENATESEQ = UInt32( 144 )

global const InsertMode = Cint
global const INSERT_VALUES = Cint( 1 )
global const ADD_VALUES = Cint( 2 )

global const KSPType = String
global const KSPRICHARDSON = String( "richardson" )
global const KSPCHEBYSHEV = String( "chebyshev" )
global const KSPCG = String( "cg" )
global const KSPGROPPCG = String( "groppcg" )
global const KSPPIPECG = String( "pipecg" )
global const KSPCGNE = String( "cgne" )
global const KSPNASH = String( "nash" )
global const KSPSTCG = String( "stcg" )
global const KSPGLTR = String( "gltr" )
global const KSPFCG = String( "fcg" )
global const KSPGMRES = String( "gmres" )
global const KSPFGMRES = String( "fgmres" )
global const KSPLGMRES = String( "lgmres" )
global const KSPDGMRES = String( "dgmres" )
global const KSPPGMRES = String( "pgmres" )
global const KSPTCQMR = String( "tcqmr" )
global const KSPBCGS = String( "bcgs" )
global const KSPIBCGS = String( "ibcgs" )
global const KSPFBCGS = String( "fbcgs" )
global const KSPFBCGSR = String( "fbcgsr" )
global const KSPBCGSL = String( "bcgsl" )
global const KSPCGS = String( "cgs" )
global const KSPTFQMR = String( "tfqmr" )
global const KSPCR = String( "cr" )
global const KSPPIPECR = String( "pipecr" )
global const KSPLSQR = String( "lsqr" )
global const KSPPREONLY = String( "preonly" )
global const KSPQCG = String( "qcg" )
global const KSPBICG = String( "bicg" )
global const KSPMINRES = String( "minres" )
global const KSPSYMMLQ = String( "symmlq" )
global const KSPLCD = String( "lcd" )
global const KSPPYTHON = String( "python" )
global const KSPGCR = String( "gcr" )

global const PCJacobiType = Int32
global const PC_JACOBI_DIAGONAL = Int32( 0 )
global const PC_JACOBI_ROWMAX = Int32( 1 )
global const PC_JACOBI_ROWSUM = Int32( 2 )

global const PCType = String
global const PCNONE = String( "none" )
global const PCJACOBI = String( "jacobi" )
global const PCSOR = String( "sor" )
global const PCLU = String( "lu" )
global const PCSHELL = String( "shell" )
global const PCBJACOBI = String( "bjacobi" )
global const PCMG = String( "mg" )
global const PCEISENSTAT = String( "eisenstat" )
global const PCILU = String( "ilu" )
global const PCICC = String( "icc" )
global const PCASM = String( "asm" )
global const PCGASM = String( "gasm" )
global const PCKSP = String( "ksp" )
global const PCCOMPOSITE = String( "composite" )
global const PCREDUNDANT = String( "redundant" )
global const PCSPAI = String( "spai" )
global const PCNN = String( "nn" )
global const PCCHOLESKY = String( "cholesky" )
global const PCPBJACOBI = String( "pbjacobi" )
global const PCMAT = String( "mat" )
global const PCHYPRE = String( "hypre" )
global const PCPARMS = String( "parms" )
global const PCFIELDSPLIT = String( "fieldsplit" )
global const PCTFS = String( "tfs" )
global const PCML = String( "ml" )
global const PCGALERKIN = String( "galerkin" )
global const PCEXOTIC = String( "exotic" )
global const PCCP = String( "cp" )
global const PCBFBT = String( "bfbt" )
global const PCLSC = String( "lsc" )
global const PCPYTHON = String( "python" )
global const PCPFMG = String( "pfmg" )
global const PCSYSPFMG = String( "syspfmg" )
global const PCREDISTRIBUTE = String( "redistribute" )
global const PCSVD = String( "svd" )
global const PCGAMG = String( "gamg" )
global const PCSACUSP = String( "sacusp" )
global const PCSACUSPPOLY = String( "sacusppoly" )
global const PCBICGSTABCUSP = String( "bicgstabcusp" )
global const PCAINVCUSP = String( "ainvcusp" )
global const PCBDDC = String( "bddc" )
global const PCKACZMARZ = String( "kaczmarz" )

global const MatAssemblyType = Cint
global const MAT_FLUSH_ASSEMBLY = Cint( 1 )
global const MAT_FINAL_ASSEMBLY = Cint( 0 )

global const VecType = String
global const VECSEQ = String( "seq" )
global const VECMPI = String( "mpi" )
global const VECSTANDARD = String( "standard" )
global const VECSHARED = String( "shared" )
global const VECSEQCUSP = String( "seqcusp" )
global const VECMPICUSP = String( "mpicusp" )
global const VECCUSP = String( "cusp" )
global const VECSEQVIENNACL = String( "seqviennacl" )
global const VECMPIVIENNACL = String( "mpiviennacl" )
global const VECVIENNACL = String( "viennacl" )
global const VECSEQCUDA = String( "seqcuda" )
global const VECMPICUDA = String( "mpicuda" )
global const VECCUDA = String( "cuda" )
global const VECNEST = String( "nest" )

global const PCSide = Cint
global const PC_SIDE_DEFAULT = Cint( -1 )
global const PC_LEFT = Cint( 0 )
global const PC_RIGHT = Cint( 1 )
global const PC_SYMMETRIC = Cint( 2 )

global const MatType = String
global const MATSAME = String( "same" )
global const MATMAIJ = String( "maij" )
global const MATSEQMAIJ = String( "seqmaij" )
global const MATMPIMAIJ = String( "mpimaij" )
global const MATIS = String( "is" )
global const MATAIJ = String( "aij" )
global const MATSEQAIJ = String( "seqaij" )
global const MATMPIAIJ = String( "mpiaij" )
global const MATAIJCRL = String( "aijcrl" )
global const MATSEQAIJCRL = String( "seqaijcrl" )
global const MATMPIAIJCRL = String( "mpiaijcrl" )
global const MATAIJCUSP = String( "aijcusp" )
global const MATSEQAIJCUSP = String( "seqaijcusp" )
global const MATMPIAIJCUSP = String( "mpiaijcusp" )
global const MATAIJCUSPARSE = String( "aijcusparse" )
global const MATSEQAIJCUSPARSE = String( "seqaijcusparse" )
global const MATMPIAIJCUSPARSE = String( "mpiaijcusparse" )
global const MATAIJVIENNACL = String( "aijviennacl" )
global const MATSEQAIJVIENNACL = String( "seqaijviennacl" )
global const MATMPIAIJVIENNACL = String( "mpiaijviennacl" )
global const MATAIJPERM = String( "aijperm" )
global const MATSEQAIJPERM = String( "seqaijperm" )
global const MATMPIAIJPERM = String( "mpiaijperm" )
global const MATSHELL = String( "shell" )
global const MATDENSE = String( "dense" )
global const MATSEQDENSE = String( "seqdense" )
global const MATMPIDENSE = String( "mpidense" )
global const MATELEMENTAL = String( "elemental" )
global const MATBAIJ = String( "baij" )
global const MATSEQBAIJ = String( "seqbaij" )
global const MATMPIBAIJ = String( "mpibaij" )
global const MATMPIADJ = String( "mpiadj" )
global const MATSBAIJ = String( "sbaij" )
global const MATSEQSBAIJ = String( "seqsbaij" )
global const MATMPISBAIJ = String( "mpisbaij" )
global const MATSEQBSTRM = String( "seqbstrm" )
global const MATMPIBSTRM = String( "mpibstrm" )
global const MATBSTRM = String( "bstrm" )
global const MATSEQSBSTRM = String( "seqsbstrm" )
global const MATMPISBSTRM = String( "mpisbstrm" )
global const MATSBSTRM = String( "sbstrm" )
global const MATDAAD = String( "daad" )
global const MATMFFD = String( "mffd" )
global const MATNORMAL = String( "normal" )
global const MATLRC = String( "lrc" )
global const MATSCATTER = String( "scatter" )
global const MATBLOCKMAT = String( "blockmat" )
global const MATCOMPOSITE = String( "composite" )
global const MATFFT = String( "fft" )
global const MATFFTW = String( "fftw" )
global const MATSEQCUFFT = String( "seqcufft" )
global const MATTRANSPOSEMAT = String( "transpose" )
global const MATSCHURCOMPLEMENT = String( "schurcomplement" )
global const MATPYTHON = String( "python" )
global const MATHYPRESTRUCT = String( "hyprestruct" )
global const MATHYPRESSTRUCT = String( "hypresstruct" )
global const MATSUBMATRIX = String( "submatrix" )
global const MATLOCALREF = String( "localref" )
global const MATNEST = String( "nest" )

global const NormType = Int32
global const NORM_1 = Int32( 0 )
global const NORM_2 = Int32( 1 )
global const NORM_FROBENIUS = Int32( 2 )
global const NORM_INFINITY = Int32( 3 )
global const NORM_MAX = Int32( 3 )

global const PetscMatStructure = Int32
global const DIFFERENT_NONZERO_PATTERN = Int32( 0 )
global const SUBSET_NONZERO_PATTERN = Int32( 1 )
global const SAME_NONZERO_PATTERN = Int32( 2 )

global const MatReuse = Int32
global const MAT_INITIAL_MATRIX = Int32( 0 )
global const MAT_REUSE_MATRIX = Int32( 1 )
global const MAT_IGNORE_MATRIX = Int32( 2 )
global const MAT_INPLACE_MATRIX = Int32( 3 )


struct ODEInterpolatingAdjointSensitivityFunction{C<:AdjointDiffCache,Alg<:InterpolatingAdjoint,
                                                  uType,SType,CPS,pType,fType<:DiffEqBase.AbstractDiffEqFunction} <: SensitivityFunction
  diffcache::C
  sensealg::Alg
  discrete::Bool
  y::uType
  sol::SType
  checkpoint_sol::CPS
  prob::pType
  f::fType
  noiseterm::Bool
end

mutable struct CheckpointSolution{S,I,T,T2}
  cpsol::S # solution in a checkpoint interval
  intervals::I # checkpoint intervals
  cursor::Int # sol.prob.tspan = intervals[cursor]
  tols::T
  tstops::T2 # for callbacks
end

function ODEInterpolatingAdjointSensitivityFunction(g,sensealg,discrete,sol,dg,f,checkpoints,tols,tstops=nothing;noiseterm=false)
  tspan = reverse(sol.prob.tspan)
  checkpointing = ischeckpointing(sensealg, sol)
  (checkpointing && checkpoints === nothing) && error("checkpoints must be passed when checkpointing is enabled.")

  checkpoint_sol = if checkpointing
    intervals = map(tuple, @view(checkpoints[1:end-1]), @view(checkpoints[2:end]))
    interval_end = intervals[end][end]
    tspan[1] > interval_end && push!(intervals, (interval_end, tspan[1]))
    cursor = lastindex(intervals)
    interval = intervals[cursor]

    if typeof(sol.prob) <: Union{SDEProblem,RODEProblem}
      # replicated noise
      _sol = deepcopy(sol)
      idx1 = searchsortedfirst(_sol.W.t, interval[1]-1000eps(interval[1]))
      if typeof(sol.W) <: DiffEqNoiseProcess.NoiseProcess
        sol.W.save_everystep = false
        _sol.W.save_everystep = false
        forwardnoise = DiffEqNoiseProcess.NoiseWrapper(_sol.W, indx=idx1)
      elseif typeof(sol.W) <: DiffEqNoiseProcess.NoiseGrid
        #idx2 = searchsortedfirst(_sol.W.t, interval[2]+1000eps(interval[1]))
        forwardnoise = DiffEqNoiseProcess.NoiseGrid(_sol.W.t[idx1:end], _sol.W.W[idx1:end])
      else
        error("NoiseProcess type not implemented.")
      end
      dt = choose_dt((_sol.W.t[idx1]-_sol.W.t[idx1+1]), _sol.W.t, interval)

      cpsol = solve(remake(sol.prob, tspan=interval, u0=sol(interval[1]), noise=forwardnoise),
         sol.alg, save_noise=false; dt=dt, tstops=_sol.t[idx1:end] ,tols...)
    else

      if tstops === nothing
        cpsol = solve(remake(sol.prob, tspan=interval, u0=sol(interval[1])),sol.alg; tols...)
      else
        if maximum(interval[1] .< tstops .< interval[2])
          # callback might have changed p
          _p = reset_p(prob.kwargs[:callback], interval)
          cpsol = solve(remake(sol.prob, tspan=interval, u0=sol(interval[1])),tstops=tstops,
                        p=_p, sol.alg; tols...)
        else
          cpsol = solve(remake(sol.prob, tspan=interval, u0=sol(interval[1])),tstops=tstops, sol.alg; tols...)
        end
      end
    end
    CheckpointSolution(cpsol, intervals, cursor, tols, tstops)

  else
    nothing
  end

  diffcache, y = adjointdiffcache(g,sensealg,discrete,sol,dg,f;quad=false,noiseterm=noiseterm)

  return ODEInterpolatingAdjointSensitivityFunction(diffcache,sensealg,
                                                    discrete,y,sol,
                                                    checkpoint_sol,sol.prob,f,noiseterm)
end

function findcursor(intervals, t)
  # equivalent with `findfirst(x->x[1] <= t <= x[2], intervals)`
  lt(x, t) = <(x[2], t)
  return searchsortedfirst(intervals, t, lt=lt)
end

function choose_dt(dt, ts, interval)
  if dt < 1000eps(interval[2])
    if length(ts) > 2
      dt = ts[end-1]-ts[end-2]
      if dt < 1000eps(interval[2])
        dt = interval[2] - interval[1]
      end
    else
      dt = interval[2] - interval[1]
    end
  end
  return dt
end

# u = λ'
# add tstop on all the checkpoints
function (S::ODEInterpolatingAdjointSensitivityFunction)(du,u,p,t)
  @unpack sol,checkpoint_sol, discrete, prob, f = S

  λ,grad,y,dλ,dgrad,dy = split_states(du,u,t,S)

  if S.noiseterm
    if length(u) == length(du)
      vecjacobian!(dλ, y, λ, p, t, S, dgrad=dgrad)
    elseif length(u) != length(du) &&  StochasticDiffEq.is_diagonal_noise(prob) && !isnoisemixing(S.sensealg)
      vecjacobian!(dλ, y, λ, p, t, S)
      jacNoise!(λ, y, p, t, S, dgrad=dgrad)
    else
      jacNoise!(λ, y, p, t, S, dgrad=dgrad, dλ=dλ)
    end
  else
    vecjacobian!(dλ, y, λ, p, t, S, dgrad=dgrad)
  end

  dλ .*= -one(eltype(λ))

  discrete || accumulate_cost!(dλ, y, p, t, S, dgrad)
  return nothing
end

function (S::ODEInterpolatingAdjointSensitivityFunction)(du,u,p,t,W)
  @unpack sol,checkpoint_sol, discrete, prob, f = S

  λ,grad,y,dλ,dgrad,dy = split_states(du,u,t,S)

  vecjacobian!(dλ, y, λ, p, t, S, dgrad=dgrad, W=W)

  dλ .*= -one(eltype(λ))

  discrete || accumulate_cost!(dλ, y, p, t, S, dgrad)
  return nothing
end

function split_states(du,u,t,S::TS;update=true) where TS<:ODEInterpolatingAdjointSensitivityFunction
  @unpack sol, y, checkpoint_sol, discrete, prob, f = S
  idx = length(y)

  if update
    if checkpoint_sol === nothing
      if typeof(t) <: ForwardDiff.Dual && eltype(S.y) <: AbstractFloat
        y = sol(t, continuity=:right)
      else
        sol(y,t, continuity=:right)
      end
    else
      intervals = checkpoint_sol.intervals
      interval = intervals[checkpoint_sol.cursor]
      if !(interval[1] <= t <= interval[2])
        cursor′ = findcursor(intervals, t)
        interval = intervals[cursor′]
        cpsol_t = checkpoint_sol.cpsol.t
        if typeof(t) <: ForwardDiff.Dual && eltype(S.y) <: AbstractFloat
          y = sol(interval[1])
        else
          sol(y, interval[1])
        end
        if typeof(sol.prob) <: Union{SDEProblem,RODEProblem}
          #idx1 = searchsortedfirst(sol.t, interval[1])
          _sol = deepcopy(sol)
          idx1 = searchsortedfirst(_sol.t, interval[1]-100eps(interval[1]))
          idx2 = searchsortedfirst(_sol.t, interval[2]+100eps(interval[2]))
          idx_noise = searchsortedfirst(_sol.W.t, interval[1]-100eps(interval[1]))
          if typeof(sol.W) <: DiffEqNoiseProcess.NoiseProcess
            _sol.W.save_everystep = false
            forwardnoise = DiffEqNoiseProcess.NoiseWrapper(_sol.W, indx=idx_noise)
          elseif typeof(sol.W) <: DiffEqNoiseProcess.NoiseGrid
            forwardnoise = DiffEqNoiseProcess.NoiseGrid(_sol.W.t[idx_noise:end], _sol.W.W[idx_noise:end])
          else
            error("NoiseProcess type not implemented.")
          end
          prob′ = remake(prob, tspan=intervals[cursor′], u0=y, noise=forwardnoise)
          dt = choose_dt(abs(cpsol_t[1]-cpsol_t[2]), cpsol_t, interval)
          cpsol′ = solve(prob′, sol.alg, noise=forwardnoise, save_noise=false; dt=dt, tstops=_sol.t[idx1:idx2], checkpoint_sol.tols...)
        else
          if checkpoint_sol.tstops===nothing
            prob′ = remake(prob, tspan=intervals[cursor′], u0=y)
            cpsol′ = solve(prob′, sol.alg; dt=abs(cpsol_t[end] - cpsol_t[end-1]), checkpoint_sol.tols...)
          else
            if maximum(interval[1] .< checkpoint_sol.tstops .< interval[2])
              # callback might have changed p
              _p = reset_p(prob.kwargs[:callback], interval)
              prob′ = remake(prob, tspan=intervals[cursor′], u0=y, p=_p)
              cpsol′ = solve(prob′, sol.alg; dt=abs(cpsol_t[end] - cpsol_t[end-1]), tstops=checkpoint_sol.tstops, checkpoint_sol.tols...)
            else
              prob′ = remake(prob, tspan=intervals[cursor′], u0=y)
              cpsol′ = solve(prob′, sol.alg; dt=abs(cpsol_t[end] - cpsol_t[end-1]), tstops=checkpoint_sol.tstops, checkpoint_sol.tols...)
            end
          end
        end
        checkpoint_sol.cpsol = cpsol′
        checkpoint_sol.cursor = cursor′
      end
      checkpoint_sol.cpsol(y, t, continuity=:right)
    end
  end

  λ     = @view u[1:idx]
  grad  = @view u[idx+1:end]

  if length(u) == length(du)
    dλ    = @view du[1:idx]
    dgrad = @view du[idx+1:end]

  elseif length(u) != length(du) &&  StochasticDiffEq.is_diagonal_noise(prob) && !isnoisemixing(S.sensealg)
    idx1 = [length(u)*(i-1)+i for i in 1:idx] # for diagonal indices of [1:idx,1:idx]

    dλ    = @view du[idx1]
    dgrad = @view du[idx+1:end,1:idx]

  elseif typeof(du) <: AbstractMatrix
    # non-diagonal noise and noise mixing case
    dλ    = @view du[1:idx,1:idx]
    dgrad = @view du[idx+1:end,1:idx]
  end

  λ,grad,y,dλ,dgrad,nothing
end

# g is either g(t,u,p) or discrete g(t,u,i)
@noinline function ODEAdjointProblem(sol,sensealg::InterpolatingAdjoint,
                                     g::G,t=nothing,dg::DG=nothing;
                                     checkpoints=sol.t,
                                     callback=CallbackSet(),
                                     reltol=nothing, abstol=nothing,
                                     kwargs...) where {G,DG}
  @unpack f, p, u0, tspan = sol.prob
  tspan = reverse(tspan)
  discrete = t != nothing

  # remove duplicates from checkpoints
  if ischeckpointing(sensealg) &&  (length(unique(checkpoints)) != length(checkpoints))
    _checkpoints, duplicate_iterator_times = separate_nonunique(checkpoints)
    tstops = duplicate_iterator_times[1]
    checkpoints = filter(x->x ∉ tstops, _checkpoints)
  else
    tstops = nothing
  end

  numstates = length(u0)
  numparams = p === nothing || p === DiffEqBase.NullParameters() ? 0 : length(p)

  len = numstates+numparams

  λ = p === nothing || p === DiffEqBase.NullParameters() ? similar(u0) : one(eltype(u0)) .* similar(p, len)
  λ .= false

  sense = ODEInterpolatingAdjointSensitivityFunction(g,sensealg,discrete,sol,dg,f,
                                                     checkpoints,
                                                     (reltol=reltol,abstol=abstol),
                                                     tstops)

  init_cb = t !== nothing && tspan[1] == t[end]
  cb, duplicate_iterator_times = generate_callbacks(sense, g, λ, t, callback, init_cb)
  z0 = vec(zero(λ))
  original_mm = sol.prob.f.mass_matrix
  if original_mm === I || original_mm === (I,I)
    mm = I
  else
    adjmm = copy(sol.prob.f.mass_matrix')
    zzz = similar(adjmm, numstates, numparams)
    fill!(zzz, zero(eltype(zzz)))
    # using concrate I is slightly more efficient
    II = Diagonal(I, numparams)
    mm = [adjmm       zzz
          copy(zzz')   II]
  end

  jac_prototype = sol.prob.f.jac_prototype
  if !sense.discrete || jac_prototype === nothing
    adjoint_jac_prototype = nothing
  else
    _adjoint_jac_prototype = copy(jac_prototype')
    zzz = similar(_adjoint_jac_prototype, numstates, numparams)
    fill!(zzz, zero(eltype(zzz)))
    II = Diagonal(I, numparams)
    adjoint_jac_prototype = [_adjoint_jac_prototype zzz
                             copy(zzz')             II]
  end

  odefun = ODEFunction(sense, mass_matrix=mm, jac_prototype=adjoint_jac_prototype)
  return ODEProblem(odefun,z0,tspan,p,callback=cb)
end

@noinline function SDEAdjointProblem(sol,sensealg::InterpolatingAdjoint,
                                     g,t=nothing,dg=nothing;
                                     checkpoints=sol.t,
                                     callback=CallbackSet(),
                                     reltol=nothing, abstol=nothing,
                                     diffusion_jac=nothing, diffusion_paramjac=nothing,
                                     kwargs...)
  @unpack f, p, u0, tspan = sol.prob
  tspan = reverse(tspan)
  discrete = t != nothing

  # remove duplicates from checkpoints
  if ischeckpointing(sensealg) && (length(unique(checkpoints)) != length(checkpoints))
    _checkpoints, duplicate_iterator_times = separate_nonunique(checkpoints)
    tstops = duplicate_iterator_times[1]
    checkpoints = filter(x->x ∉ tstops, _checkpoints)
  else
    tstops = nothing
  end

  numstates = length(u0)
  numparams = p === nothing || p === DiffEqBase.NullParameters() ? 0 : length(p)

  len = numstates+numparams

  λ = one(eltype(u0)) .* similar(p, len)
  λ .= false

  sense_drift = ODEInterpolatingAdjointSensitivityFunction(g,sensealg,discrete,sol,dg,sol.prob.f,
                                                     checkpoints,(reltol=reltol,abstol=abstol))

  diffusion_function = ODEFunction(sol.prob.g, jac=diffusion_jac, paramjac=diffusion_paramjac)
  sense_diffusion = ODEInterpolatingAdjointSensitivityFunction(g,sensealg,discrete,sol,dg,diffusion_function,
                                                     checkpoints,(reltol=reltol,abstol=abstol);noiseterm=true)

  init_cb = t !== nothing && tspan[1] == t[end]
  cb, duplicate_iterator_times = generate_callbacks(sense_drift, g, λ, t, callback, init_cb)
  z0 = vec(zero(λ))
  original_mm = sol.prob.f.mass_matrix
  if original_mm === I || original_mm === (I,I)
    mm = I
  else
    adjmm = copy(sol.prob.f.mass_matrix')
    zzz = similar(adjmm, numstates, numparams)
    fill!(zzz, zero(eltype(zzz)))
    # using concrate I is slightly more efficient
    II = Diagonal(I, numparams)
    mm = [adjmm       zzz
          copy(zzz')   II]
  end

  jac_prototype = sol.prob.f.jac_prototype
  if !sense_drift.discrete || jac_prototype === nothing
    adjoint_jac_prototype = nothing
  else
    _adjoint_jac_prototype = copy(jac_prototype')
    zzz = similar(_adjoint_jac_prototype, numstates, numparams)
    fill!(zzz, zero(eltype(zzz)))
    II = Diagonal(I, numparams)
    adjoint_jac_prototype = [_adjoint_jac_prototype zzz
                             copy(zzz')             II]
  end

  sdefun = SDEFunction(sense_drift,sense_diffusion,mass_matrix=mm,jac_prototype=adjoint_jac_prototype)

  # replicated noise
  _sol = deepcopy(sol)
  backwardnoise = reverse(_sol.W)

  if StochasticDiffEq.is_diagonal_noise(sol.prob) && typeof(sol.W[end])<:Number
    # scalar noise case
    noise_matrix = nothing
  else
    noise_matrix = similar(z0,length(z0),numstates)
    noise_matrix .= false
  end

  return SDEProblem(sdefun,sense_diffusion,z0,tspan,p,
    callback=cb,
    noise=backwardnoise,
    noise_rate_prototype = noise_matrix
    )
end

@noinline function RODEAdjointProblem(sol,sensealg::InterpolatingAdjoint,
                                     g,t=nothing,dg=nothing;
                                     checkpoints=sol.t,
                                     callback=CallbackSet(),
                                     reltol=nothing, abstol=nothing,
                                     kwargs...)
  @unpack f, p, u0, tspan = sol.prob
  tspan = reverse(tspan)
  discrete = t != nothing

  # remove duplicates from checkpoints
  if ischeckpointing(sensealg) && (length(unique(checkpoints)) != length(checkpoints))
    _checkpoints, duplicate_iterator_times = separate_nonunique(checkpoints)
    tstops =  duplicate_iterator_times[1]
    checkpoints = filter(x->x ∉ tstops, _checkpoints)
  else
    tstops = nothing
  end

  numstates = length(u0)
  numparams = p === nothing || p === DiffEqBase.NullParameters() ? 0 : length(p)

  len = numstates+numparams

  λ = p === nothing || p === DiffEqBase.NullParameters() ? similar(u0) : one(eltype(u0)) .* similar(p, len)
  λ .= false

  sense = ODEInterpolatingAdjointSensitivityFunction(g,sensealg,discrete,sol,dg,f,
                                                     checkpoints,
                                                     (reltol=reltol,abstol=abstol),
                                                     tstops)

  init_cb = t !== nothing && tspan[1] == t[end]
  cb, duplicate_iterator_times = generate_callbacks(sense, g, λ, t, callback, init_cb)
  z0 = vec(zero(λ))
  original_mm = sol.prob.f.mass_matrix
  if original_mm === I || original_mm === (I,I)
    mm = I
  else
    adjmm = copy(sol.prob.f.mass_matrix')
    zzz = similar(adjmm, numstates, numparams)
    fill!(zzz, zero(eltype(zzz)))
    # using concrate I is slightly more efficient
    II = Diagonal(I, numparams)
    mm = [adjmm       zzz
          copy(zzz')   II]
  end

  jac_prototype = sol.prob.f.jac_prototype
  if !sense.discrete || jac_prototype === nothing
    adjoint_jac_prototype = nothing
  else
    _adjoint_jac_prototype = copy(jac_prototype')
    zzz = similar(_adjoint_jac_prototype, numstates, numparams)
    fill!(zzz, zero(eltype(zzz)))
    II = Diagonal(I, numparams)
    adjoint_jac_prototype = [_adjoint_jac_prototype zzz
                             copy(zzz')             II]
  end

  rodefun = RODEFunction(sense, mass_matrix=mm, jac_prototype=adjoint_jac_prototype)

  # replicated noise
  _sol = deepcopy(sol)
  backwardnoise = reverse(_sol.W)
  # make sure noise grid starts at correct time values, e.g., if sol.W.t is longer than sol.t
  tspan[1]!=backwardnoise.t[1] && reinit!(backwardnoise,backwardnoise.t[2]-backwardnoise.t[1],t0=tspan[1])

  if StochasticDiffEq.is_diagonal_noise(sol.prob) && typeof(sol.W[end])<:Number
    # scalar noise case
    noise_matrix = nothing
  else
    noise_matrix = similar(z0,length(z0),numstates)
    noise_matrix .= false
  end

  return RODEProblem(rodefun,z0,tspan,p,callback=cb,
                    noise=backwardnoise,
                    noise_rate_prototype = noise_matrix)
end

function reset_p(CBS, interval)
  # TODO: treat continuous callbacks
  # check which events are close to tspan[1]
  ts = map(CBS.discrete_callbacks) do cb
    indx = searchsortedfirst(cb.affect!.event_times, interval[1])
    (indx, cb.affect!.event_times[indx])
  end
  perm = minimum(sortperm([t for t in getindex.(ts,2)]))
  p = deepcopy(CBS.discrete_callbacks[perm].affect!.pleft[getindex.(ts,1)[perm]])
  return p
end


__precompile__(false)
module PDESolver

# defs.jl
export AssembleElementData, AbstractShockSensor

# from registration.jl
export register_physics, retrieve_physics, registerIC, registerBC

# from interface.jl
export evalResidual, evalJacobian, evalHomotopy, evalHomotopyJacobian,
       evalJacobianStrong, createFunctional,
       evalFunctional, evalFunctionalDeriv_q, evalFunctionalDeriv_m,
       updateMetricDependents,
       solvePDE, evalResidual_revm, evalResidual_revq, evaldRdqProduct,
       _getSparsityPattern, getShockSensor, setShockSensor, setShockSensorAlpha,
       createShockSensor, copyParameters, openLoggingFiles, closeLoggingFiles,
       setFluxFunction

# from interface2.jl
export createObjects, createLinearSolver, getSparsityPattern,
       createSBPOperator, createEnrichedObjects

# from startup_func
export run_solver

# from interative.jl
export printICNames, printBCNames

# load paths for all the components of PDESolver
push!(LOAD_PATH, joinpath(Pkg.dir("PumiInterface"), "src"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/solver"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/NonlinearSolvers"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/linearsolvers"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/jacobian"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/Utils"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/Debugging"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/input"))

# add physics modules to load path (but don't load them, because that would
# create a circular dependency)
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/solver/advection"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/solver/euler"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/solver/elliptic"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/solver/simpleODE"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/solver/elliptic"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/solver/navier_stokes"))
push!(LOAD_PATH, joinpath(Pkg.dir("PDESolver"), "src/optimization"))

# load the modules
using MPI
using ODLCommonTools
import ODLCommonTools: evalFunctional, _evalFunctional,
                       evalFunctionalDeriv_m, _evalFunctionalDeriv_m,
                       evalFunctionalDeriv_q, _evalFunctionalDeriv_q

function finalizeMPI()
  if MPI.Initialized()
    MPI.Finalize()
  end
end

if !MPI.Initialized()
  MPI.Init()
  atexit(finalizeMPI)
end

using PETSc2

function finalizePetsc()
  if PetscInitialized()
    PetscFinalize()
  end
end

if !PetscInitialized()
  PetscInitialize()
  atexit(finalizePetsc)
end

using Input
using ODLCommonTools
using PdePumiInterface  # common mesh interface - pumi
using SummationByParts  # SBP operators
#using LinearSolvers
#using NonlinearSolvers   # non-linear solvers
using ArrayViews
import ArrayViews.view
using Utils
import ODLCommonTools.sview
#using Input

include("defs.jl")  # common definitions
include("registration.jl")  # registering physics modules
include("interface.jl")  # functions all physics modules need to implement
include("interface2.jl") # functions physics modules don't have to implement
include("startup_func.jl")  # unified solver invokation
include("interactive.jl")
# package code goes here

end # module


"""
An [`AbstractConstraintSet`](@ref) that stores the constraint values as well as Lagrange
multiplier and penalty terms for each constraint.

The cost associated with constraint terms in the augmented Lagrangian can be evaluated for

	cost!(J::Vector, ::ALConstraintSet)

which adds the cost at each time step to the vector `J` of length `N`.

The cost expansion for these terms is evaluated along the trajectory `Z` using

	cost_expansion!(E::Objective, conSet::ALConstraintSet, Z)

which also adds the expansion terms to the terms in `E`.

The penalty and multiplier terms can be updated using

	penalty_update!(::ALConstraintSet)
	dual_update!(::ALConstraintSet)

The current set of active constraint (with tolerance `tol`) can be re-calculated using

	update_active_set!(::ALConstraintSet, ::Val{tol})

The maximum penalty can be queried using `max_penalty(::ALConstraintSet)`, and the
penalties and/or multipliers can be reset using

	reset!(::ALConstraintSet)
	reset_penalties!(::ALConstraintSet)
	reset_duals!(::ALConstraintSet)

# Constructor
	ALConstraintSet(::ConstraintList, ::AbstractModel)
	ALConstraintSet(::Problem)
"""
struct ALConstraintSet{T} <: AbstractConstraintSet
    convals::Vector{ConVal}
    errvals::Vector{ConVal}
    λ::Vector{<:Vector}
    μ::Vector{<:Vector}
    active::Vector{<:Vector}
    c_max::Vector{T}
    μ_max::Vector{T}
    μ_maxes::Vector{Vector{T}}
	params::Vector{ConstraintParams{T}}
	p::Vector{Int}
end

function ALConstraintSet(cons::ConstraintList, model::AbstractModel)
    n,m = cons.n, cons.m
    n̄ = RobotDynamics.state_diff_size(model)
    ncon = length(cons)
    useG = model isa LieGroupModel
    errvals = map(1:ncon) do i
        C,c = gen_convals(n̄, m, cons[i], cons.inds[i])
        ConVal(n̄, m, cons[i], cons.inds[i], C, c, useG)
    end
    convals = map(errvals) do errval
        ConVal(n, m, errval)
    end
	errvals = convert(Vector{ConVal}, errvals)
	convals = convert(Vector{ConVal}, convals)
    λ = map(1:ncon) do i
        p = length(cons[i])
        [@SVector zeros(p) for i in cons.inds[i]]
    end
    μ = map(1:ncon) do i
        p = length(cons[i])
        [@SVector ones(p) for i in cons.inds[i]]
    end
    a = map(1:ncon) do i
        p = length(cons[i])
        [@SVector ones(Bool,p) for i in cons.inds[i]]
    end
    c_max = zeros(ncon)
    μ_max = zeros(ncon)
    μ_maxes = [zeros(length(ind)) for ind in cons.inds]
	params = [ConstraintParams() for con in cons.constraints]
    ALConstraintSet(convals, errvals, λ, μ, a, c_max, μ_max, μ_maxes, params, copy(cons.p))
end

@inline ALConstraintSet(prob::Problem) = ALConstraintSet(prob.constraints, prob.model)

# Iteration
Base.iterate(conSet::AbstractConstraintSet) =
	isempty(get_convals(conSet)) ? nothing : (get_convals(conSet)[1].con,1)
Base.iterate(conSet::AbstractConstraintSet, state::Int) =
	state >= length(conSet) ? nothing : (get_convals(conSet)[state+1].con, state+1)
@inline Base.length(conSet) = length(get_convals(conSet))
Base.IteratorSize(::AbstractConstraintSet) = Base.HasLength()
Base.IteratorEltype(::AbstractConstraintSet) = Base.HasEltype()
Base.eltype(::AbstractConstraintSet) = AbstractConstraint

"""
	link_constraints!(set1, set2)

Link any common constraints between `set1` and `set2` by setting elements in `set1` to point
to elements in `set2`
"""
function link_constraints!(set1::ALConstraintSet, set2::ALConstraintSet)
	# Find common constraints
	links = Tuple{Int,Int}[]
	for (i,con1) in enumerate(set1)
		for (j,con2) in enumerate(set2)
			if con1 === con2
				push!(links, (i,j))
			end
		end
	end

	# Link values
	for (i,j) in links
		set1.convals[i] = set2.convals[j]
		set1.errvals[i] = set2.errvals[j]
		set1.active[i] = set2.active[j]
		set1.λ[i] = set2.λ[j]
		set1.μ[i] = set2.μ[j]
	end
	return links
end

@inline get_convals(conSet::ALConstraintSet) = conSet.convals
@inline get_errvals(conSet::ALConstraintSet) = conSet.errvals

# Augmented Lagrangian Updates
function dual_update!(conSet::ALConstraintSet)
    for i in eachindex(conSet.λ)
        dual_update!(conSet.convals[i], conSet.λ[i], conSet.μ[i], conSet.params[i])
    end
end

function dual_update!(conval::ConVal, λ::Vector{<:SVector}, μ::Vector{<:SVector}, params::ConstraintParams)
    c = conval.vals
	λ_max = params.λ_max
	λ_min = sense(conval.con) == Equality() ? -λ_max : zero(λ_max)
	for i in eachindex(conval.inds)
		λ[i] = clamp.(λ[i] + μ[i] .* c[i], λ_min, λ_max)
	end
end

function penalty_update!(conSet::ALConstraintSet)
	for i in eachindex(conSet.μ)
		penalty_update!(conSet.μ[i], conSet.params[i])
	end
end

function penalty_update!(μ::Vector{<:SVector}, params::ConstraintParams)
	ϕ = params.ϕ
	μ_max = params.μ_max
	for i in eachindex(μ)
		μ[i] = clamp.(ϕ * μ[i], 0.0, μ_max)
	end
end

# Active Set
function update_active_set!(conSet::ALConstraintSet, val::Val{tol}=Val(0.0)) where tol
	for i in eachindex(conSet.active)
		update_active_set!(conSet.active[i], conSet.λ[i], conSet.convals[i], val)
	end
end

function update_active_set!(a::Vector{<:StaticVector}, λ::Vector{<:StaticVector},
		conval::ConVal, ::Val{tol}) where tol
	if sense(conval.con) == Inequality()
		for i in eachindex(a)
			a[i] = @. (conval.vals[i] >= -tol) | (λ[i] > zero(tol))
		end
	end
end

# Cost
function cost!(J::Vector{<:Real}, conSet::ALConstraintSet)
	for i in eachindex(conSet.convals)
		cost!(J, conSet.convals[i], conSet.λ[i], conSet.μ[i], conSet.active[i])
	end
end

function cost!(J::Vector{<:Real}, conval::ConVal, λ::Vector{<:StaticVector},
		μ::Vector{<:StaticVector}, a::Vector{<:StaticVector})
	for (i,k) in enumerate(conval.inds)
		c = SVector(conval.vals[i])
		Iμ = Diagonal(SVector(μ[i] .* a[i]))
		J[k] += λ[i]'c .+ 0.5*c'Iμ*c
	end
end

function cost_expansion!(E::Objective, conSet::ALConstraintSet, Z::AbstractTrajectory,
		init::Bool=false, rezero::Bool=false)
	for i in eachindex(conSet.errvals)
		cost_expansion!(E, conSet.convals[i], conSet.λ[i], conSet.μ[i], conSet.active[i])
	end
end

@generated function cost_expansion!(E::QuadraticObjective{n,m}, conval::ConVal{C}, λ, μ, a) where {n,m,C}
	if C <: StateConstraint
		expansion = quote
			cx = ∇c
			E[k].Q .+= cx'Iμ*cx
			E[k].q .+= cx'g
		end
	elseif C <: ControlConstraint
		expansion = quote
			cu = ∇c
			E[k].R .+= cu'Iμ*cu
			E[k].r .+= cu'g
		end
	elseif C<: StageConstraint
		ix = SVector{n}(1:n)
		iu = SVector{m}(n .+ (1:m))
		expansion = quote
			cx = ∇c[:,$ix]
			cu = ∇c[:,$iu]
			E[k].Q .+= cx'Iμ*cx
			E[k].q .+= cx'g
			E[k].H .+= cu'Iμ*cx
			E[k].R .+= cu'Iμ*cu
			E[k].r .+= cu'g
		end
	else
		throw(ArgumentError("cost expansion not supported for CoupledConstraints"))
	end
	quote
		for (i,k) in enumerate(conval.inds)
			∇c = SMatrix(conval.jac[i])
			c = conval.vals[i]
			Iμ = Diagonal(a[i] .* μ[i])
			g = Iμ*c .+ λ[i]

			$expansion
		end
	end
end

"""
	max_penalty(conSet::ALConstraintSet)

Calculate the maximum constrained penalty across all constraints.
"""
function max_penalty(conSet::ALConstraintSet)
	max_penalty!(conSet)
	maximum(conSet.μ_max)
end

function max_penalty!(conSet::ALConstraintSet{T}) where T
    conSet.c_max .*= 0
    for i in eachindex(conSet.μ)
        maxes = conSet.μ_maxes[i]::Vector{T}
        max_penalty!(maxes, conSet.μ[i])
        conSet.μ_max[i] = maximum(maxes)
    end
end

function max_penalty!(μ_max::Vector{<:Real}, μ::Vector{<:StaticVector})
    for i in eachindex(μ)
        μ_max[i] = maximum(μ[i])
    end
    return nothing
end

# Reset
function reset!(conSet::ALConstraintSet)
    reset_duals!(conSet)
    reset_penalties!(conSet)
end

function reset_duals!(conSet::ALConstraintSet)
	function _reset!(V::Vector{<:SVector})
	    for i in eachindex(V)
	        V[i] = zero(V[i])
	    end
	end
    for i in eachindex(conSet.λ)
        _reset!(conSet.λ[i])
    end
end

function reset_penalties!(conSet::ALConstraintSet)
	function _reset!(V::Vector{<:SVector}, params::ConstraintParams)
	    for i in eachindex(V)
	        V[i] = zero(V[i]) .+ params.μ0
	    end
	end
    for i in eachindex(conSet.μ)
        # reset!(conSet.μ[i], conSet.params[i].μ0)
		# μ0 = conSet.params[i].μ0
        _reset!(conSet.μ[i], conSet.params[i])
    end
end


using Gtk, GtkUtilities

 function randbw(ctx, w, h)
    pic = zeros(Int64, w, h)
    for i in 1:length(pic)
        pic[i] = rand([1, 0])
    end
    copy!(ctx, pic)
end

const can = @GtkCanvas()
const win = GtkWindow(can, "Image Noise", 320, 240)

@guarded draw(can) do widget
    ctx = getgc(can)
    h = height(can)
    w = width(can)
    randbw(ctx, w, h)
end

show(can)
const cond = Condition()
endit(w) = notify(cond)
signal_connect(endit, win, :destroy)

while true
    frames = 0
    t = time()
    for _ in 1:100
        draw(can)
        show(can)
        sleep(0.0001)
        frames += 1
    end
    fps = round(frames / (time() - t), digits=1)
    set_gtk_property!(win, :title, "Image Noise: $fps fps")
end

wait(cond)


function plot_net(df::DataFrame)

    # drop the helper columns
    select!(df, 1:findfirst(contains("Column"), names(df))-1)

    # figure
    plot()
    totals = ["GBP", "USD", "EUR", "total (GBP)"]
    individuals = filter!(c -> !(c in totals || c == "date"), names(df))
    for ind in individuals
        plot!(df.date, df[!, ind]*0.001; label=ind, ls=:dash, lw=2.0)
    end
    for tot in totals
        plot!(df.date, df[!, tot]*0.001; label=tot, lw=3)
    end

    # compute days to FI
    Δ_wealth = df[end, "total (GBP)"] - df[1, "total (GBP)"]
    Δ_days = df[end, "date"] - df[1, "date"]
    rate_of_change = Δ_wealth / Δ_days.value
    missing_wealth = 500000 - df[end, "total (GBP)"]
    days2fi = Day(round(missing_wealth / rate_of_change))
    fi_date = today() + days2fi
    bday = Date(1993, 6, 30)
    age_fi = "$(year(fi_date) - year(bday))y, $(month(fi_date) - month(bday))m"

    # cosmetics
    ylabel!("Net worth (k)")
    ylims!((0, ylims()[2]))
    title!("$days2fi to FI (£500k on $fi_date), aged $age_fi")
    plot!(legend=:topleft)

    # save
    savefig("figures/networth.pdf")
end

function plot_categories(expenses::DataFrame)

    # sort the categories by total spent
    categories = Exchequer.aggregate(expenses, "category")
    sort!(categories, :amount_sum, rev=true)

    nmonths = months(expenses)
    currency = expenses.currency[1]

    # figure
    bar(categories.category, categories.amount_sum / nmonths,
        xrotation=20, fillcolor=COLORS, legend=nothing, width=0.5)

    # cosmetics
    ylabel!("Monthly spending ($currency)")
    ylims!((0, ylims()[2]))

    # save
    savefig("figures/categories.pdf")

end

function plot_yearly(expenses::DataFrame, incomes::DataFrame)

    # sort the categories by total spent
    categories = Exchequer.aggregate(expenses, "category")
    sort!(categories, :amount_sum, rev=true)

    # group expenses by year and month and category
    exp = Exchequer.aggregate(expenses, "year", "category")

    # and fill in zeros to categories
    fill_zeros!(exp)

    # group incomes by year and month
    inc = Exchequer.aggregate(incomes, "year")

    # compute number of months in each year (first and last != 12)
    months_in_year = Dict(y=>length(unique(expenses[expenses.year.==y, "month"]))
                          for y in unique(expenses.year))
end

function plot_monthly(expenses::DataFrame, incomes::DataFrame)
    # sort the categories by total spent
    categories = Exchequer.aggregate(expenses, "category")
    sort!(categories, :amount_sum, rev=true)

    # group expenses by year and month and category
    exp_groups = Exchequer.aggregate(expenses, "year", "month", "category")
    fill_zeros!(exp_groups)

    # group incomes by year and month
    inc_groups = Exchequer.aggregate(incomes, "year", "month")
    fill_zeros!(inc_groups)

    # make sure they have the same starting and ending point in time
    fill_zeros!(exp_groups, inc_groups)

    # make figures
    currency = expenses.currency[1]
    plot_unstacked(categories, exp_groups, currency)
    plot_stacked(categories, exp_groups, inc_groups, currency)

    return inc_groups
end

function plot_unstacked(categories::DataFrame, groups::DataFrame, currency::String)
    # clear the figure
    plot()

    # and show every category separately
    spending = groups[groups.category .== "sport", :]
    xpos = [i for i in 1:1:nrow(spending)]
    for (cat, color) in zip(categories.category, COLORS)
        # select the spending and construct dates
        spending = groups[groups.category .== cat, :]
        plot!(xpos, spending.amount_sum, label=cat, color=color, linewidth=2)
    end

    # xticks cosmetics
    yearmonths = zip(spending.year, spending.month)
    labels = [m == 1 ? "$(MONTHS[m])\n$y" : "$(MONTHS[m])" for (y,m) in yearmonths]
    xticks!(xpos, labels)

    # other
    ylabel!("Monthly spending ($currency)")
    ylims!(0, ylims()[2])
    xlims!(1, xlims()[2])
    plot!(legend=:topright)

    # save
    savefig("figures/monthly_unstacked.pdf")
end

function plot_stacked(categories::DataFrame,
                      exp_groups::DataFrame,
                      inc_groups::DataFrame,
                      currency::String)
    # clear the figure
    plot()

    # get the upper and lower limits for each category
    ncat = nrow(categories)
    nmonth = nrow(exp_groups) ÷ ncat
    ys = zeros(ncat+1, nmonth)
    for (i, c) in enumerate(categories.category)
        ys[i+1, :] = exp_groups[exp_groups.category .== c, "amount_sum"]
    end
    cs = cumsum(ys, dims=1)
    lowers = cs[1:end-1, :]
    uppers = cs[2:end, :]

    total_expense = uppers[end,:]
    total_income = inc_groups.amount_sum

    # plot mains
    xpos = [i for i in 1:1:nmonth]
    for (i, c) in enumerate(categories.category)
        plot!(xpos, lowers[i, :], fillrange=uppers[i, :],
              label=c, color=COLORS[i], alpha=0.8, width=0.01)
    end

    # plot income
    plot!(xpos, inc_groups.amount_sum,
          linewidth=2, linecolor="black", linestyle=:dot, label="income")

    # plot net numbers
    net = [trunc(Int, n) for n in total_income - total_expense]
    ypos = [max(a, b)+50 for (a, b) in zip(total_income, total_expense)]
    texts = [(x, y, text(n, 7, :left, n > 0 ? :green : :red; rotation=60,))
             for (x, y, n) in zip(xpos, ypos, net)]
    annotate!(texts)

    # xticks cosmetics
    spending = exp_groups[exp_groups.category .== "sport", :]
    yearmonths = zip(spending.year, spending.month)
    labels = [m == 1 ? "$(MONTHS[m])\n$y" : "$(MONTHS[m])" for (y,m) in yearmonths]
    xticks!(xpos, labels)

    # other cosmetics
    ylabel!("Monthly spending ($currency)")
    ylims!(0, ylims()[2]*1.05)
    xlims!(1, xlims()[2])
    plot!(legend=:topright)

    savefig("figures/monthly_stacked.pdf")
end

function plot_weekly(expenses::DataFrame)

    # sort the categories by total spent
    categories = Exchequer.aggregate(expenses, "category")
    sort!(categories, :amount_sum, rev=true)

    # day of week, normalised to per day spend
    day_exp = Exchequer.aggregate(expenses, "category", "dayofweek")
    ndays = Dates.value(maximum(expenses.date) - minimum(expenses.date))
    day_exp.amount_sum /= (ndays/7)

    # prepare data for plotting
    data = zeros(7, nrow(categories))
    for (i, c) in enumerate(categories.category)
        for d in 1:1:7
            cut = (day_exp.category.==c) .& (day_exp.dayofweek.==d)
            if nrow(day_exp[cut, :]) != 0
                data[d, i] = day_exp[cut, "amount_sum"][1]
            end
        end
    end

    # plot histograms
    bins = [i for i in 0.5:1:7.5]
    lowers = bins[1:end-1]
    highers = bins[2:end]
    centres = 0.5*(lowers+highers)

    ncat = nrow(categories)
    width = 0.8 / ncat

    plot()
    for (i, c) in enumerate(categories.category)
        shift = (-(ncat/2)+(i-0.5))*width
        histogram!(centres.+shift, bins=bins.+shift, weights=data[:, i],
                   bar_width=width, label=c, color=COLORS[i], width=0.5)
    end

    # total spend
    plot!(centres, sum(data, dims=2), color="black", label="total")

    # cosmetics
    currency = expenses.currency[1]
    ylabel!("Daily spending ($currency)")
    days = Dict(1=>"Mon", 2=>"Tue", 3=>"Wed", 4=>"Thu", 5=>"Fri", 6=>"Sat", 7=>"Sun")
    xticks!(centres, [days[d] for d in centres])
    savefig("figures/weekly.pdf")

end


function get_field(::Type{GeoMagneticFieldFactory}, arg0::GeoMagneticFieldFactory_FieldModel, arg1::jdouble)
    return jcall(GeoMagneticFieldFactory, "getField", GeoMagneticField, (GeoMagneticFieldFactory_FieldModel, jdouble), arg0, arg1)
end

function get_geo_magnetic_fields(::Type{GeoMagneticFieldFactory})
    return jcall(GeoMagneticFieldFactory, "getGeoMagneticFields", LazyLoadedGeoMagneticFields, ())
end

function get_igrf(::Type{GeoMagneticFieldFactory}, arg0::jdouble)
    return jcall(GeoMagneticFieldFactory, "getIGRF", GeoMagneticField, (jdouble,), arg0)
end

function get_wmm(::Type{GeoMagneticFieldFactory}, arg0::jdouble)
    return jcall(GeoMagneticFieldFactory, "getWMM", GeoMagneticField, (jdouble,), arg0)
end


##############################################################################################
module TestFilterExps
##############################################################################################
##############################################################################################
# imports and exports
using DataAssimilationBenchmarks.FilterExps
using JLD2, Statistics

##############################################################################################
##############################################################################################
# run and analyze the ETKF for state estimation with the Lorenz-96 model

function run_filter_state_L96()
    try
        path = joinpath(@__DIR__, "../src/data/time_series/")
        L96_test_ts = "L96_time_series_seed_0000_dim_40_diff_0.000_F_08.0" *
                      "_tanl_0.05_nanl_05000_spin_1500_h_0.050.jld2"

        args = (path * L96_test_ts, "etkf", 0, 3500, 1.0, 40, 1.00, 21, 1.02)
        filter_state(args)
        true
    catch
        false
    end
end

function analyze_filter_state_L96()
    try
        # test if the filter RMSE for standard simulation falls below adequate threshold
        path = joinpath(@__DIR__, "../src/data/etkf/")
        rmse = load(path * "etkf_L96_state_seed_0000_diff_0.000_sysD_40_obsD_40" *
                    "_obsU_1.00_gamma_001.0_nanl_03500_tanl_0.05_h_0.05_nens_021" *
                    "_stateInfl_1.02.jld2")
        rmse = rmse["filt_rmse"]

        # note, we use a small burn-in to reach more regular cycles
        if mean(rmse[501:end]) < 0.2
            true
        else
            false
        end
    catch
        false
    end
end

##############################################################################################
# run and analyze the ETKF for joint state-parameter estimation with the Lorenz-96 model

function run_filter_param_L96()
    try
        path = joinpath(@__DIR__, "../src/data/time_series/")
        L96_test_ts = "L96_time_series_seed_0000_dim_40_diff_0.000_F_08.0" *
                      "_tanl_0.05_nanl_05000_spin_1500_h_0.050.jld2"

        args = (path * L96_test_ts, "etkf", 0, 3500, 1.0, 40, 1.0, 0.10, 0.0010, 21, 1.02, 1.0)
        filter_param(args)
        true
    catch
        false
    end
end

function analyze_filter_param_L96()
    try
        # test if the filter RMSE for standard simulation falls below adequate threshold
        path = joinpath(@__DIR__, "../src/data/etkf/")
        rmse = load(path * "etkf_L96_param_seed_0000_diff_0.000_sysD_41_stateD_40_obsD_40_" *
                    "obsU_1.00_gamma_001.0_paramE_0.10_paramW_0.0010_nanl_03500_tanl_0.05_" *
                    "h_0.05_nens_021_stateInfl_1.02_paramInfl_1.00.jld2")
        filt_rmse = rmse["filt_rmse"]
        para_rmse = rmse["param_rmse"]

        # note, we use a small burn-in to reach more regular cycles
        if (mean(filt_rmse[501:end]) < 0.2) && (mean(para_rmse[501:end]) < 0.01)
            true
        else
            false
        end
    catch
        false
    end
end

##############################################################################################
# run and analyzed the ETKF for state estimateion with the IEEE39bus model

# static version for test cases
function run_filter_state_IEEE39bus()
    try
        path = joinpath(@__DIR__, "../src/data/time_series/")
        IEEE39bus_test_ts = "IEEE39bus_time_series_seed_0000_diff_0.000_tanl" *
                            "_0.01_nanl_05000_spin_1500_h_0.010.jld2"
        args = (path * IEEE39bus_test_ts, "etkf", 0, 3500, 0.1, 20, 1.00, 21, 1.02)
        filter_state(args)
        true
    catch
        false
    end
end

function analyze_filter_state_IEEE39bus()
    try
        # test if the filter RMSE for standard simulation falls below adequate threshold
        path = joinpath(@__DIR__, "../src/data/etkf/")
        rmse = load(path * "etkf_IEEE39bus_state_seed_0000_diff_0.000_sysD_20_obsD_20_" *
                    "obsU_0.10_gamma_001.0_nanl_03500_tanl_0.01_h_0.01_nens_021_" *
                    "stateInfl_1.02.jld2")
        rmse = rmse["filt_rmse"]

        # note, we use a small burn-in to reach more regular cycles
        if mean(rmse[501:end]) < 0.02
            true
        else
            false
        end
    catch
        false
    end
end

##############################################################################################
# end module

end


using Test
using PlayingCards
using PlayingCards: rank_string

@testset "Ranks" begin
    for v in ranks()
        v==1 && continue
        @test high_value(v*♣) == low_value(v*♣) == v
    end
    @test low_value(1*♣) == 1
end

@testset "Color" begin
    @test color(J♣) == :black
    @test color(A♠) == :black
    @test color(♣) == :black
    @test color(♠) == :black
    @test color(J♢) == :red
    @test color(A♡) == :red
    @test color(♢) == :red
    @test color(♡) == :red
end

@testset "Card" begin
    @test rank(J♣) == 11
    @test suit(J♣) == ♣
    @test suit(J♡) == ♡
    @test suit(J♢) == ♢
    @test suit(J♠) == ♠
    @test suit(A♣) == ♣
    @test suit(A♡) == ♡
    @test suit(A♢) == ♢
    @test suit(A♠) == ♠
    @test_throws ArgumentError 14*♣
    @test_throws ArgumentError 0*♣
end

@testset "strings" begin
    @test sprint(show, 2♣) == "2♣"
    @test sprint(show, J♣) == "J♣"
    @test string(2♣) == "2♣"
    @test string(T♣) == "T♣"
    @test string(J♣) == "J♣"
    @test string(Q♣) == "Q♣"
    @test string(K♣) == "K♣"
    @test string(A♣) == "A♣"
    @test string(♣) == "♣"
    @test string(♠) == "♠"
    @test string(♡) == "♡"
    @test string(♢) == "♢"
    @test_throws AssertionError rank_string(Int8(-1))
end

@testset "Deck" begin
    deck = ordered_deck()
    @test length(deck) == 52
    @test iterate(deck) == iterate(deck.cards)
    shuffle!(deck)
    cards = pop!(deck, 2)
    @test length(cards)==2
    @test length(deck)==50
    @test length(full_deck())==52

    s="A♣ 2♣ 3♣ 4♣ 5♣ 6♣ 7♣ 8♣ 9♣ T♣ J♣ Q♣ K♣
A♠ 2♠ 3♠ 4♠ 5♠ 6♠ 7♠ 8♠ 9♠ T♠ J♠ Q♠ K♠
A♡ 2♡ 3♡ 4♡ 5♡ 6♡ 7♡ 8♡ 9♡ T♡ J♡ Q♡ K♡
A♢ 2♢ 3♢ 4♢ 5♢ 6♢ 7♢ 8♢ 9♢ T♢ J♢ Q♢ K♢
"
    @test sprint(show, ordered_deck()) == s

    deck = ordered_deck()
    five_spades = pop!(deck, 5♠)
    @test five_spades === 5♠
    @test length(deck) == 51
    @test findfirst(x->x==5♠, deck.cards) == nothing
    @test_throws ErrorException pop!(deck, 5♠)
end

@testset "Allocations" begin
    alloc = @allocated ordered_deck()
    @test alloc == 304
end