open Lacaml.D
open Util

module Ds = DeterminantSpace
module Sd = Spindeterminant

type t =
  {
    e_shift     : float ; (* Diagonal energy shift for increasing numerical precision *)
    det_space   : Ds.t ;
    m_H         : Matrix.t lazy_t ;
    m_S2        : Matrix.t lazy_t ;
    eigensystem : (Mat.t * Vec.t) lazy_t;
    n_states    : int;
  }

let det_space    t = t.det_space

let n_states     t = t.n_states

let m_H          t = Lazy.force t.m_H

let m_S2         t = Lazy.force t.m_S2

let eigensystem  t = Lazy.force t.eigensystem

let mo_class     t = DeterminantSpace.mo_class t.det_space

let eigenvectors t =
  let (x,_) = eigensystem t in x

let eigenvalues t =
  let (_,x) = eigensystem t in x

let h_integrals mo_basis =
  let one_e_ints    = MOBasis.one_e_ints mo_basis
  and two_e_ints    = MOBasis.two_e_ints mo_basis
  in
  ( (fun i j _ -> one_e_ints.{i,j}),
    (fun i j k l s s' ->
       if s' <> s then
         ERI.get_phys two_e_ints i j k l
       else
         (ERI.get_phys two_e_ints i j k l) -.
         (ERI.get_phys two_e_ints i j l k)
    ), None )



let h_ij mo_basis ki kj =
  let integrals =
    List.map (fun f -> f mo_basis)
      [ h_integrals ]
  in
  CIMatrixElement.make integrals ki kj
  |> List.hd


let h_ij_non_zero mo_basis deg_a deg_b ki kj =
  let integrals =
    List.map (fun f -> f mo_basis)
      [ h_integrals ]
  in
  CIMatrixElement.non_zero integrals deg_a deg_b ki kj
  |> List.hd


let create_matrix_arbitrary f det_space =
  lazy (
    let ndet = Ds.size det_space in
    let data =
      match Ds.determinants det_space with
      | Ds.Arbitrary a -> a
      | _ -> assert false
    in
    let det_alfa    = data.Ds.det_alfa
    and det_beta    = data.Ds.det_beta
    and det         = data.Ds.det
    and index_start = data.Ds.index_start
    in


    (** Array of (list of singles, list of doubles) in the beta spin *)
    let degree_bb =
      Array.map (fun det_i ->
          let deg = Spindeterminant.degree det_i in
          let doubles =
            Array.mapi (fun i det_j ->
                let d = deg det_j in
                if d < 3 then
                  Some (i,d,det_j)
                else
                  None
              ) det_beta
            |> Array.to_list
            |> Util.list_some
          in
          let singles =
            List.filter (fun (i,d,det_j) -> d < 2) doubles
            |> List.rev_map (fun (i,_,det_j) -> (i,det_j))
            |> List.rev
          in
          let doubles =
            List.rev_map (fun (i,_,det_j) -> (i,det_j)) doubles
            |> List.rev
          in
          (singles, doubles)
        ) det_beta
    in

    let task (i,i_dets) =
      let shift = index_start.(i) in

      let result =
        Array.init (index_start.(i+1) - shift)
          (fun _ -> [])
      in

      (** Update function when ki and kj are connected *)
      let update i j deg_a deg_b ki kj =
        let x = f deg_a deg_b ki kj in
        if abs_float x > Constants.epsilon then
          result.(i - shift) <- (j, x) :: result.(i - shift) ;
      in

      let i_alfa = det_alfa.(i) in
      let deg_a = Spindeterminant.degree i_alfa in

      Array.iteri (fun j j_dets ->
          let j_alfa = det_alfa.(j) in
          let degree_a = deg_a j_alfa in

          begin
            match degree_a with
            | 2 ->
              Array.iteri (fun i' i_b ->
                  try
                    Array.iteri (fun j' j_b ->
                        if j_b >= i_b then
                          ( if j_b = i_b then
                              ( let i_beta = det_beta.(i_b) in
                                let ki = Determinant.of_spindeterminants i_alfa i_beta in
                                let kj = Determinant.of_spindeterminants j_alfa i_beta in
                                update (index_start.(i) + i')
                                  (index_start.(j) + j' + 1) 2 0 ki kj);
                            raise Exit)
                      ) j_dets
                  with Exit -> ()
                ) i_dets
            | 1 ->
              Array.iteri (fun i' i_b ->
                  let i_beta = det_beta.(i_b) in
                  let ki = Determinant.of_spindeterminants i_alfa i_beta in
                  let singles, _ = degree_bb.(i_b) in
                  let rec aux singles j' =
                    match singles with
                    | [] -> ()
                    | (js, j_beta)::r_singles ->
                      begin
                        match compare js j_dets.(j') with
                        | -1 -> (aux [@tailcall]) r_singles j'
                        |  0 ->
                          let kj =
                            Determinant.of_spindeterminants j_alfa j_beta
                          in (update
                                (index_start.(i) + i') (index_start.(j) + j' + 1)
                                1 (Determinant.degree_beta ki kj) ki kj;
                              (aux [@tailcall]) r_singles (j'+1);)
                        |  1 -> if (j' < Array.length j_dets) then (aux [@tailcall]) singles (j'+1)
                        |  _ -> assert false
                      end
                  in aux singles 0
                ) i_dets
            | 0 ->
              Array.iteri (fun i' i_b ->
                  let i_beta = det_beta.(i_b) in
                  let ki = Determinant.of_spindeterminants i_alfa i_beta in
                  let _, doubles = degree_bb.(i_b) in
                  let rec aux doubles j' =
                    match doubles with
                    | [] -> ()
                    | (js, j_beta)::r_doubles ->
                      begin
                        match compare js j_dets.(j') with
                        | -1 -> (aux [@tailcall]) r_doubles j'
                        |  0 ->
                          let kj =
                            Determinant.of_spindeterminants j_alfa j_beta
                          in (update
                                (index_start.(i) + i') (index_start.(j) + j' + 1)
                                0 (Determinant.degree_beta ki kj) ki kj;
                              (aux [@tailcall]) r_doubles (j'+1);)
                        |  1 -> if (j' < Array.length j_dets) then (aux [@tailcall]) doubles (j'+1)
                        |  _ -> assert false
                      end
                  in aux doubles 0
                ) i_dets
            | _ -> ();
          end
        ) det;
      let r =
        Array.map (fun l ->
            List.rev l
            |> Vector.sparse_of_assoc_list ndet
          ) result
      in (i,r)
    in


    let result =
      if Parallel.master then
        Array.init ndet (fun _ -> Vector.sparse_of_assoc_list ndet [])
      else
        Array.init ndet (fun _ -> Vector.sparse_of_assoc_list ndet [])
    in

    let n_det_alfa =
      Array.length det_alfa
    in
    Array.mapi (fun i i_dets -> i, i_dets) det
    |> Array.to_list
    |> Stream.of_list
    |> Farm.run ~ordered:false ~f:task
    |> Stream.iter (fun (k, r) ->
        let shift = index_start.(k) in
        let det_k = det.(k) in
        Array.iteri (fun j r_j -> result.(shift+det_k.(j)) <- r_j) r;
        Printf.eprintf "%8d / %8d\r%!" (k+1) n_det_alfa;
      ) ;
    Matrix.sparse_of_vector_array result
  )


(* Create a matrix using the fact that the determinant space is made of
   the outer product of spindeterminants.  *)
let create_matrix_spin ?(nmax=2) f det_space =
  lazy (
    let ndet = Ds.size det_space in
    let a, b =
      match Ds.determinants det_space with
      | Ds.Spin (a,b) -> (a,b)
      | _ -> assert false
    in
    let n_beta = Array.length b in


    (** Array of (list of singles, list of doubles, list of triples) in the beta spin *)
    let degree_bb =
      match nmax with
      | 2 -> begin
                Array.map (fun det_i ->
                    let deg = Spindeterminant.degree det_i in
                    let triples = [] in
                    let doubles =
                      Array.mapi (fun i det_j ->
                          let d = deg det_j in
                          if d < 3 then
                            Some (i,d,det_j)
                          else
                            None
                        ) b
                      |> Array.to_list
                      |> Util.list_some
                    in
                    let singles =
                      List.filter (fun (i,d,det_j) -> d < 2) doubles
                      |> List.rev_map (fun (i,_,det_j) -> (i,det_j))
                      |> List.rev
                    in
                    let doubles =
                      List.rev_map (fun (i,_,det_j) -> (i,det_j)) doubles
                      |> List.rev
                    in
                    (singles, doubles, triples)
                  ) b
             end
      | 3 -> begin
                Array.map (fun det_i ->
                    let deg = Spindeterminant.degree det_i in
                    let triples =
                      Array.mapi (fun i det_j ->
                          let d = deg det_j in
                          if d < 4 then
                            Some (i,d,det_j)
                          else
                            None
                        ) b
                      |> Array.to_list
                      |> Util.list_some
                    in
                    let doubles =
                      List.filter (fun (i,d,det_j) -> d < 3) triples
                    in
                    let singles =
                      List.filter (fun (i,d,det_j) -> d < 2) doubles
                    in

                    let triples =
                      List.rev_map (fun (i,_,det_j) -> (i,det_j)) triples
                      |> List.rev
                    in
                    let doubles =
                      List.rev_map (fun (i,_,det_j) -> (i,det_j)) doubles
                      |> List.rev
                    in
                    let singles =
                      List.rev_map (fun (i,_,det_j) -> (i,det_j)) singles
                      |> List.rev
                    in
                    (singles, doubles, triples)
                  ) b
             end
      | _ -> assert false
    in
    let a = Array.to_list a
    and b = Array.to_list b
    in

    let task =
      let result =
        Array.init n_beta (fun _ -> [])
      in

      (** Update function when ki and kj are connected *)
      let update i j deg_a deg_b ki kj =
        let x = f deg_a deg_b ki kj in
        if abs_float x > Constants.epsilon then
          result.(i) <- (j, x) :: result.(i) ;
      in

      fun (i,i_alfa) ->
        begin match nmax with
        | 2 -> begin
                  let j = ref 1 in

                  let deg_a = Spindeterminant.degree i_alfa in
                  List.iter (fun j_alfa ->
                      let degree_a = deg_a j_alfa in
                      begin
                        match degree_a with
                        | 2 ->
                          let i' = ref 0 in
                          List.iteri (fun ib i_beta ->
                              let ki = Determinant.of_spindeterminants i_alfa i_beta in
                              let kj = Determinant.of_spindeterminants j_alfa i_beta in
                              update !i' (ib + !j) 2 0 ki kj;
                              incr i';
                            ) b;
                        | 1 ->
                          let i' = ref 0 in
                          List.iteri (fun ib i_beta ->
                              let ki = Determinant.of_spindeterminants i_alfa i_beta in
                              let singles, _doubles, _triples = degree_bb.(ib) in
                              List.iter (fun (j', j_beta) ->
                                  let kj = Determinant.of_spindeterminants j_alfa j_beta in
                                  update !i' (j' + !j) 1 (Determinant.degree_beta ki kj) ki kj
                                ) singles;
                              incr i';
                            ) b;
                        | 0 ->
                          let i' = ref 0 in
                          List.iteri (fun ib i_beta ->
                              let ki = Determinant.of_spindeterminants i_alfa i_beta in
                              let _singles, doubles, _triples = degree_bb.(ib) in
                              List.iter (fun (j', j_beta) ->
                                  let kj = Determinant.of_spindeterminants j_alfa j_beta in
                                  update !i' (j' + !j) 0 (Determinant.degree_beta ki kj) ki kj
                                ) doubles;
                              incr i';
                            ) b;
                        | _ -> ();
                      end;
                      j := !j + n_beta
                    ) a
                  end
        | 3 -> begin
                  let j = ref 1 in

                  let deg_a = Spindeterminant.degree i_alfa in
                  List.iter (fun j_alfa ->
                      let degree_a = deg_a j_alfa in
                      begin
                        match degree_a with
                        | 3 ->
                          let i' = ref 0 in
                          List.iteri (fun ib i_beta ->
                              let ki = Determinant.of_spindeterminants i_alfa i_beta in
                              let kj = Determinant.of_spindeterminants j_alfa i_beta in
                              update !i' (ib + !j) 3 0 ki kj;
                              incr i';
                            ) b;
                        | 2 ->
                          let i' = ref 0 in
                          List.iteri (fun ib i_beta ->
                              let ki = Determinant.of_spindeterminants i_alfa i_beta in
                              let singles, _doubles, _triples = degree_bb.(ib) in
                              List.iter (fun (j', j_beta) ->
                                  let kj = Determinant.of_spindeterminants j_alfa j_beta in
                                  update !i' (j' + !j) 2 (Determinant.degree_beta ki kj) ki kj
                                ) singles;
                              incr i';
                            ) b;
                        | 1 ->
                          let i' = ref 0 in
                          List.iteri (fun ib i_beta ->
                              let ki = Determinant.of_spindeterminants i_alfa i_beta in
                              let _singles, doubles, _triples = degree_bb.(ib) in
                              List.iter (fun (j', j_beta) ->
                                  let kj = Determinant.of_spindeterminants j_alfa j_beta in
                                  update !i' (j' + !j) 1 (Determinant.degree_beta ki kj) ki kj
                                ) doubles;
                              incr i';
                            ) b;
                        | 0 ->
                          let i' = ref 0 in
                          List.iteri (fun ib i_beta ->
                              let ki = Determinant.of_spindeterminants i_alfa i_beta in
                              let _singles, _doubles, triples = degree_bb.(ib) in
                              List.iter (fun (j', j_beta) ->
                                  let kj = Determinant.of_spindeterminants j_alfa j_beta in
                                  update !i' (j' + !j) 0 (Determinant.degree_beta ki kj) ki kj
                                ) triples;
                              incr i';
                            ) b;
                        | _ -> ();
                      end;
                      j := !j + n_beta
                    ) a
                  end
          | _ -> assert false
          end;
          let r =
            Array.map (fun l ->
                List.rev l
                |> Vector.sparse_of_assoc_list ndet
              ) result
          in (i,r)
    in


    let result =
      Array.init ndet (fun _ -> Vector.sparse_of_assoc_list ndet [])
    in


    List.mapi (fun i i_alfa -> i*n_beta, i_alfa) a
    |> Stream.of_list
    |> Farm.run ~ordered:false ~f:task
    |> Stream.iter (fun (k, r) ->
        Array.iteri (fun j r_j -> result.(k+j) <- r_j) r;
        Printf.eprintf "%8d / %8d\r%!" (k+Array.length r) ndet;
      ) ;
    Matrix.sparse_of_vector_array result
  )



(* Create a matrix using the fact that the determinant space is made of
   the outer product of spindeterminants.  *)
let create_matrix_spin_computed ?(nmax=2) f det_space =
  lazy (
    let ndet = Ds.size det_space in
    let a, b =
      match Ds.determinants det_space with
      | Ds.Spin (a,b) -> (a,b)
      | _ -> assert false
    in
    let n_beta = Array.length b in

    let h2 i_alfa j_alfa =
        let deg_a = Spindeterminant.degree a.(i_alfa) a.(j_alfa) in
        match deg_a with
        | 2 ->
          let ai, aj = a.(i_alfa), a.(j_alfa) in
          (fun i_beta j_beta ->
              if i_beta <> j_beta then 0. else
                let deg_b = 0 in
                let ki = Determinant.of_spindeterminants ai b.(i_beta) in
                let kj = Determinant.of_spindeterminants aj b.(j_beta) in
                f deg_a deg_b ki kj
          )
        | 1 ->
          let ai, aj = a.(i_alfa), a.(j_alfa) in
          (fun i_beta j_beta ->
              let deg_b = Spindeterminant.degree b.(i_beta) b.(j_beta) in
              match deg_b with
              | 0 | 1 ->
                let ki = Determinant.of_spindeterminants ai b.(i_beta) in
                let kj = Determinant.of_spindeterminants aj b.(j_beta) in
                  f deg_a deg_b ki kj
              | _ -> 0.
          )
        | 0 ->
          let ai, aj = a.(i_alfa), a.(j_alfa) in
          (fun i_beta j_beta ->
              let deg_b = Spindeterminant.degree b.(i_beta) b.(j_beta) in
              match deg_b with
              | 0 | 1 | 2 ->
                let deg_b = Spindeterminant.degree b.(i_beta) b.(j_beta) in
                let ki = Determinant.of_spindeterminants ai b.(i_beta) in
                let kj = Determinant.of_spindeterminants aj b.(j_beta) in
                f deg_a deg_b ki kj
              | _ -> 0.
          )
        | _ -> (fun _ _ -> 0.)
    in

    let h3 i_alfa j_alfa =
        let deg_a = Spindeterminant.degree a.(i_alfa) a.(j_alfa) in
        match deg_a with
        | 3 ->
          let ai, aj = a.(i_alfa), a.(j_alfa) in
          (fun i_beta j_beta ->
              if i_beta <> j_beta then 0. else
                let deg_b = 0 in
                let ki = Determinant.of_spindeterminants ai b.(i_beta) in
                let kj = Determinant.of_spindeterminants aj b.(j_beta) in
                f deg_a deg_b ki kj
          )
        | 2 ->
          let ai, aj = a.(i_alfa), a.(j_alfa) in
          (fun i_beta j_beta ->
              let deg_b = Spindeterminant.degree b.(i_beta) b.(j_beta) in
              match deg_b with
              | 0 | 1 ->
                let ki = Determinant.of_spindeterminants ai b.(i_beta) in
                let kj = Determinant.of_spindeterminants aj b.(j_beta) in
                  f deg_a deg_b ki kj
              | _ -> 0.
          )
        | 1 ->
          let ai, aj = a.(i_alfa), a.(j_alfa) in
          (fun i_beta j_beta ->
              let deg_b = Spindeterminant.degree b.(i_beta) b.(j_beta) in
              match deg_b with
              | 0 | 1 | 2 ->
                let deg_b = Spindeterminant.degree b.(i_beta) b.(j_beta) in
                let ki = Determinant.of_spindeterminants ai b.(i_beta) in
                let kj = Determinant.of_spindeterminants aj b.(j_beta) in
                f deg_a deg_b ki kj
              | _ -> 0.
          )
        | 0 ->
          let ai, aj = a.(i_alfa), a.(j_alfa) in
          (fun i_beta j_beta ->
              let deg_b = Spindeterminant.degree b.(i_beta) b.(j_beta) in
              match deg_b with
              | 0 | 1 | 2 | 3 ->
                let deg_b = Spindeterminant.degree b.(i_beta) b.(j_beta) in
                let ki = Determinant.of_spindeterminants ai b.(i_beta) in
                let kj = Determinant.of_spindeterminants aj b.(j_beta) in
                f deg_a deg_b ki kj
              | _ -> 0.
          )
        | _ -> (fun _ _ -> 0.)
    in

    let i_prev = ref (-10)
    and result = ref (fun _ -> 0.)
    in
    let h123 = ref (fun _ -> 0.)  in

    let h = if nmax = 2 then h2 else h3 in

    let g i =
      (*
        i      : index of the i-th determinant. 1 <= i <= ndet
        i_prev : index of the i-th determinant in the previous function call.
                  1 <= i_prev <= ndet
        i_a    : index of the i_a-th alpha determinant. 0 <= i_a < n_alfa
        i_b    : index of the i_b-th beta determinant. 0 <= i_b < n_beta
        j0     : index - 1 of the first determinant with the same alfa component
                  0 <= j0 < n_beta*(n_alfa-1)
        j1     : index - 1 of the next determinant with the 1st beta determinant
                  n_beta <= j1 <= ndet
        j_a    : index of the j_a-th alpha determinant. 0 <= j_a < n_alfa
        j_b    : index of the j_b-th beta determinant. 0 <= j_b < n_beta
      *)
      if i <> !i_prev then
        begin
          i_prev := i;
          let i_a = (i-1)/n_beta in
          let h1 = h i_a in
          let i_b = i - i_a*n_beta - 1  in
          let j0 = ref (2*ndet) in
          let j1 = ref (2*ndet) in
          let j_a = ref 0 in
          result := fun j ->
              if  (!j0 < j) && (j <= !j1) then
                  ()
              else
                begin
                  if (!j1 < j) && (j <= !j1 + n_beta) then
                    begin
                      incr j_a;
                      j0 := !j1;
                    end
                  else
                    begin
                      j_a := (j-1)/n_beta;
                      j0 := !j_a * n_beta;
                    end;
                  j1 := !j0 + n_beta;
                  h123 := h1 !j_a i_b;
                end;
                let j_b = j - !j0 - 1 in
                !h123 j_b
        end;
      !result
    in

    Matrix.of_fun ndet ndet g
  )



let make ?(n_states=1) ?(algo=`Direct) det_space =

  let mo_basis = Ds.mo_basis det_space in

  (* While in a sequential region, initiate the parallel
     4-idx transformation to avoid nested parallel jobs
  *)
  ignore @@ MOBasis.two_e_ints mo_basis;

  let e_shift =
    let d0 =
      Ds.determinant_stream det_space
      |> Stream.next
    in
    h_ij_non_zero mo_basis 0 0 d0 d0
  in

  let m_H =

    let f =
      match Ds.determinants det_space with
      | Ds.Arbitrary _ -> create_matrix_arbitrary
      | Ds.Spin      _ ->
        if algo = `Direct then
          create_matrix_spin_computed ~nmax:2
        else
          create_matrix_spin  ~nmax:2
    in
    f (fun deg_a deg_b ki kj ->
         if deg_a + deg_b > 0 then
            h_ij_non_zero mo_basis deg_a deg_b ki kj
         else
            h_ij_non_zero mo_basis 0 0 ki ki -. e_shift
      ) det_space
  in

  let m_S2 =
    let f =
      match Ds.determinants det_space with
      | Ds.Arbitrary _ -> create_matrix_arbitrary
      | Ds.Spin      _ -> create_matrix_spin  ~nmax:2
    in
    f (fun _deg_a _deg_b ki kj -> CIMatrixElement.make_s2 ki kj) det_space
    (*TODO*)
  in

  let eigensystem = lazy (

    let eigensystem_incore () =
      let m_H =
        Lazy.force m_H
      in
      let diagonal =
        Parallel.broadcast (lazy (
          Vec.init (Matrix.dim1 m_H) (fun i -> Matrix.get m_H i i)
        ))
      in
      let matrix_prod psi =
        let result =
          Matrix.mm ~transa:`T psi m_H
          |> Matrix.transpose
        in
        Parallel.broadcast (lazy result)
      in
      let eigenvectors, eigenvalues =
        let result =
          Davidson.make ~threshold:1.e-6  ~n_states diagonal matrix_prod
        in
        Parallel.broadcast (lazy result)
      in
      let eigenvalues = Vec.map (fun x -> x +. e_shift) eigenvalues in
      (Conventions.rephase @@ Util.remove_epsilons eigenvectors), eigenvalues
    in

    let eigensystem_direct () =
      let m_H =
        Lazy.force m_H
      in
      let diagonal =
        Parallel.broadcast (lazy (
          Vec.init (Matrix.dim1 m_H) (fun i -> Matrix.get m_H i i)
        ))
      in
      let matrix_prod psi =
        let result =
          Matrix.parallel_mm ~transa:`T ~transb:`N psi m_H
          |> Matrix.transpose
        in
        Parallel.broadcast (lazy result)
      in
      let eigenvectors, eigenvalues =
        let result =
          Davidson.make ~threshold:1.e-6  ~n_states diagonal matrix_prod
        in
        Parallel.broadcast (lazy result)
      in
      let eigenvalues = Vec.map (fun x -> x +. e_shift) eigenvalues in
      (Conventions.rephase @@ Util.remove_epsilons eigenvectors), eigenvalues
    in

    match algo with
    | `Direct -> eigensystem_direct ()
    | `InCore -> eigensystem_incore ()
  )
  in
  { det_space ; e_shift ; m_H ; m_S2 ; eigensystem ; n_states }




let second_order_sum { det_space ; m_H ; m_S2 ; eigensystem ; n_states }
    list_holes  list_particles ?(unique=true) is_internal
    i_o1_alfa  alfa_o2_i  w_alfa  psi0 =

  let list_holes     = Array.of_list list_holes in
  let list_particles = Array.of_list list_particles in

  let psi0 =
    let stream =
      Ds.determinant_stream det_space
    in
    Array.init (Ds.size det_space) (fun i ->
        (Stream.next stream), (Mat.copy_row psi0 (i+1)) )
  in



  let symmetric = i_o1_alfa == alfa_o2_i in


  let det_contribution i =

    let already_generated =
      if unique then
        (fun alfa ->
          if is_internal alfa then
            true
          else
            let rec aux = function
              | -1 -> false
              | j  -> Determinant.degree (fst psi0.(j)) alfa <= 2
                      || aux (j-1)
            in
            aux (i-1)
        )
      else
        is_internal
    in

    let psi_filtered_idx =
      let rec aux accu =  function
        | j when j < i -> List.rev accu
        | j ->
          if Determinant.degree (fst psi0.(i)) (fst psi0.(j)) < 4
          then
            aux (j::accu) (j-1)
          else
            aux accu (j-1)
      in aux [] (Array.length psi0 - 1)
    in

    let psi_filtered =
      List.rev_map (fun i -> psi0.(i)) psi_filtered_idx
      |> List.rev
    in

    let psi_h_alfa alfa =
      List.fold_left (fun accu (det, coef) ->
(* Single state here *)
          accu +. coef.{1} *. (i_o1_alfa det alfa)) 0. psi_filtered
    in

    let alfa_h_psi alfa =
      List.fold_left (fun accu (det, coef) ->
(* Single state here *)
              accu +. coef.{1} *. (alfa_o2_i alfa det)) 0. psi_filtered
    in

    let psi_h_alfa_alfa_h_psi alfa =
      if symmetric then
        let x = psi_h_alfa alfa in x *. x
      else
        (psi_h_alfa alfa) *. (alfa_h_psi alfa)
    in

    let det_i = fst psi0.(i) in
    let w_alfa = w_alfa det_i in

    let same_spin =
      List.fold_left (fun accu spin ->
          accu +.
          Array.fold_left (fun accu particle ->
              accu +.
              Array.fold_left (fun accu hole ->
                  if hole = particle then accu else
                    let alfa =
                      Determinant.single_excitation spin hole particle det_i
                    in
                    if Determinant.is_none alfa then accu else

                      let single =
                        if already_generated alfa then 0. else
                          w_alfa alfa *. psi_h_alfa_alfa_h_psi alfa
                      in

                      let double =
                        Array.fold_left (fun accu particle' ->
                            if particle' >= particle || particle' = hole then
                              accu
                            else
                              accu +.
                              Array.fold_left (fun accu hole' ->
                                  if hole' = particle' || hole' = particle || hole' <= hole then
                                    accu
                                  else
                                      let alfa =
                                        Determinant.single_excitation
                                          spin  hole' particle' alfa
                                      in
                                      if Determinant.is_none alfa ||
                                         already_generated alfa then
                                        accu
                                      else
                                        accu +. w_alfa alfa *. psi_h_alfa_alfa_h_psi alfa
                                ) 0. list_holes
                          ) 0. list_particles
                      in
                      accu +. single +. double
                ) 0. list_holes
            ) 0. list_particles
        ) 0. [ Spin.Alfa ; Spin.Beta ]
    in

    let opposite_spin =
      Array.fold_left (fun accu particle ->
          accu +.
          Array.fold_left (fun accu hole ->
              if hole = particle then accu else
                let alfa =
                  Determinant.single_excitation Spin.Alfa hole particle det_i
                in
                if Determinant.is_none alfa then accu else

                  let double_ab =
                    Array.fold_left (fun accu particle' ->
                          accu +.
                          Array.fold_left (fun accu hole' ->
                              if hole' = particle' then accu else
                                let alfa =
                                  Determinant.double_excitation
                                    Spin.Alfa hole  particle
                                    Spin.Beta hole' particle' det_i
                                in
                                if Determinant.is_none alfa ||
                                  already_generated alfa then
                                  accu
                                else
                                  accu +. w_alfa alfa *. psi_h_alfa_alfa_h_psi alfa
                          ) 0. list_holes
                      ) 0. list_particles
                  in

                  accu +. double_ab
            ) 0. list_holes
        ) 0. list_particles
    in
    same_spin +. opposite_spin
  in

  Util.stream_range 0 (Array.length psi0 - 1)
  |> Farm.run ~ordered:true ~f:det_contribution
  |> Util.stream_to_list



let second_order_sum2 { det_space ; m_H ; m_S2 ; eigensystem ; n_states }
    list_holes  list_particles i_o1_alfa e0 psi0 =

  let psi0 =
    let stream =
      Ds.determinant_stream det_space
    in
    Array.init (Ds.size det_space) (fun i ->
        (Stream.next stream), (Mat.copy_row psi0 (i+1)) )
  in

  let determinants =

    Ds.determinants_array det_space
    |> Array.to_list
    |> List.concat_map (fun det_i ->
        [ Spin.Alfa ; Spin.Beta ]
        |>  List.concat_map (fun spin ->
            List.concat_map (fun particle ->
              List.map (fun hole ->
                  [ [ Determinant.single_excitation spin hole particle det_i ] ;
                    List.concat_map (fun particle' ->
                        List.map (fun hole' ->
                          Determinant.double_excitation
                            spin  hole particle
                            spin  hole' particle' det_i
                        ) list_holes
                    ) list_particles
                    ;
                    List.concat_map (fun particle' ->
                        List.map (fun hole' ->
                          Determinant.double_excitation
                            spin  hole particle
                            (Spin.other spin)  hole' particle' det_i
                        ) list_holes
                    ) list_particles
                  ]
                  |> List.concat
                ) list_holes
            ) list_particles
          )
        )
        |> List.concat
    |> List.filter (fun alfa -> not (Determinant.is_none alfa))
    |> List.sort_uniq compare
  in

  List.fold_left (fun accu alfa ->
    let alfa_o2 = i_o1_alfa alfa in
    let a_h_psi =
      Array.fold_left (fun accu (det,ci) -> ci.{1} *. (alfa_o2 det)) 0. psi0
    in
    accu +. (a_h_psi *. a_h_psi) /. (e0 -. (alfa_o2 alfa))
    ) 0. determinants




let is_internal det_space =
    let mo_class      = DeterminantSpace.mo_class det_space in
    let numbits = Array.length @@ MOClass.mo_class_array mo_class in
    let m l =
      List.fold_left (fun accu i ->
          let j = i-1 in
          Bitstring.logor accu (Bitstring.shift_left_one numbits j)
        ) (Bitstring.zero numbits) l
    in
    let active_mask   = m (MOClass.active_mos   mo_class) in
    let occ_mask      = m (MOClass.core_mos     mo_class) in
    let inactive_mask = m (MOClass.inactive_mos mo_class) in
    let occ_mask = Bitstring.logor occ_mask inactive_mask in
    let neg_active_mask = Bitstring.lognot active_mask in
    fun a ->
      let alfa =
        Determinant.alfa a
        |> Spindeterminant.bitstring
      in
      if Bitstring.logand neg_active_mask alfa <> occ_mask then
        false
      else
        let beta =
          Determinant.beta a
          |> Spindeterminant.bitstring
        in
        Bitstring.logand neg_active_mask beta = occ_mask


let is_external det_space =
    let mo_class      = DeterminantSpace.mo_class det_space in
    let numbits = Array.length @@ MOClass.mo_class_array mo_class in
    let m l =
      List.fold_left (fun accu i ->
          let j = i-1 in
          Bitstring.logor accu (Bitstring.shift_left_one numbits j)
        ) (Bitstring.zero numbits) l
    in
    let inactive_mask = m (MOClass.inactive_mos mo_class) in
    fun a ->
      let alfa =
        Determinant.alfa a
        |> Spindeterminant.bitstring
      in
      let n_a =
	Bitstring.(popcount @@ logand inactive_mask alfa)
      in
      match n_a with
      | 0 | 1 | 2 ->
	let beta =
	  Determinant.beta a
	  |> Spindeterminant.bitstring
	in
	n_a + Bitstring.(popcount @@ logand inactive_mask beta) < 3
      | _ ->   false


let pt2_en ci =

  let mo_basis  = Ds.mo_basis ci.det_space in
  let psi0, e0 = Parallel.broadcast ci.eigensystem in

  let i_o1_alfa = h_ij mo_basis in

  let w_alfa det_i =
    let one_e, two_e, _ = h_integrals mo_basis in
    let fock_diag_alfa, fock_diag_beta =
      Ds.fock_diag ci.det_space det_i
    in
    let h_aa alfa =
      match Excitation.of_det det_i alfa with
      | Excitation.Double (_,
            {hole = h ; particle = p ; spin = s },
            {hole = h'; particle = p'; spin = s'}) ->
              let fock_diag1 =
                if s = Spin.Alfa then
                  fock_diag_alfa
                else
                  fock_diag_beta
              in
              let fock_diag2 =
                if s' = Spin.Alfa then
                  fock_diag_alfa
                else
                  fock_diag_beta
              in
              fock_diag1.(0) -. fock_diag1.(h)
              +. (fock_diag1.(p ) -. two_e h p h p s s)
              -. (fock_diag2.(h') -. two_e h h' h h' s s' +. two_e p h' p h' s s')
              +. (fock_diag2.(p') -. two_e h p' h p' s s'
                  +. two_e p p' p p' s s' -. two_e h' p' h' p' s' s')
      | Excitation.Single (_,
            {hole = h ; particle = p ; spin = s }) ->
              let fock_diag =
                if s = Spin.Alfa then
                  fock_diag_alfa
                else
                  fock_diag_beta
              in
              fock_diag.(0) -. fock_diag.(h)
              +. (fock_diag.(p) -. two_e h p h p s s)
              |> ignore;
              h_ij mo_basis alfa alfa
      | _ -> e0.{1} -. 1.0
    in
    let e0 = e0.{1} in
    fun alfa ->
      1. /. (e0 -. h_aa alfa)
  in

  let mo_class = mo_class ci in
  let list_holes = List.concat
    [ MOClass.inactive_mos mo_class ; MOClass.active_mos mo_class ]
  and list_particles = List.concat
    [ MOClass.active_mos mo_class ; MOClass.virtual_mos mo_class ]
  in

  second_order_sum  ci  list_holes  list_particles
    (is_internal ci.det_space) i_o1_alfa  i_o1_alfa  w_alfa  psi0
  |> List.fold_left (+.) 0.


let _pt2_en ci =

  let mo_basis  = Ds.mo_basis ci.det_space in
  let psi0, e0 = Parallel.broadcast ci.eigensystem in

  let i_o1_alfa = h_ij mo_basis in

  let mo_class = mo_class ci in
  let list_holes = List.concat
    [ MOClass.inactive_mos mo_class ; MOClass.active_mos mo_class ]
  and list_particles = List.concat
    [ MOClass.active_mos mo_class ; MOClass.virtual_mos mo_class ]
  in
  second_order_sum2 ci list_holes  list_particles i_o1_alfa e0.{1} psi0



let pt2_mp ci =

  let mo_basis  = Ds.mo_basis ci.det_space in

  let i_o1_alfa = h_ij mo_basis in

  let eps = MOBasis.mo_energies mo_basis in
  let w_alfa det_i alfa=
    match Excitation.of_det  det_i  alfa with
    | Excitation.Single (_, { hole ; particle ; spin })->
        1./.(eps.{hole} -. eps.{particle})
    | Excitation.Double (_, { hole=h ; particle=p ; spin=s },
                            { hole=h'; particle=p'; spin=s'})->
        1./.(eps.{h} +. eps.{h'} -. eps.{p} -. eps.{p'})
    | _ -> assert false
  in

  let mo_class = mo_class ci in
  let list_holes = List.concat
    [ MOClass.inactive_mos mo_class ; MOClass.active_mos mo_class ]
  and list_particles = List.concat
    [ MOClass.active_mos mo_class ; MOClass.virtual_mos mo_class ]
  in

  let psi0, _ = Parallel.broadcast ci.eigensystem in
  second_order_sum  ci  list_holes  list_particles
    (is_internal ci.det_space) i_o1_alfa  i_o1_alfa  w_alfa  psi0
  |> List.fold_left (+.) 0.


let variance ci =

  let mo_basis  = Ds.mo_basis ci.det_space in
  let psi0, _ = Parallel.broadcast ci.eigensystem in

  let i_o1_alfa = h_ij mo_basis in

  let w_alfa _ _ = 1. in

  let mo_class = mo_class ci in
  let list_holes = List.concat
    [ MOClass.inactive_mos mo_class ; MOClass.active_mos mo_class ]
  and list_particles = List.concat
    [ MOClass.active_mos mo_class ; MOClass.virtual_mos mo_class ]
  in

  second_order_sum  ci  list_holes  list_particles
    (is_internal ci.det_space) i_o1_alfa  i_o1_alfa  w_alfa  psi0
  |> List.fold_left (+.) 0.




let pt2_en_reference ci =

  let mo_basis  = Ds.mo_basis ci.det_space in
  let psi0, e0 = Parallel.broadcast ci.eigensystem in

  let aux_basis = mo_basis in
  let ds =
    DeterminantSpace.fci_of_mo_basis ~frozen_core:false aux_basis
  in

  let det_stream =
    let e =
      let f = is_external ci.det_space  in
      function
      | None -> false
      | Some d -> f d
    in
    let stream =
      DeterminantSpace.determinant_stream ds
    in
    let rec next i =
      let det =
        try
          Some (Stream.next stream)
        with Stream.Failure -> None
      in
      if det = None then
        None
      else
        if e det then
          det
        else
          (next [@tailcall]) i
    in
    Stream.from next
  in

  let out_dets =
    det_stream
    |> stream_to_list
    |> Array.of_list
  in

  let in_dets =
    DeterminantSpace.determinants_array ci.det_space
  in

  let m_H_aux =
    let h_aa =
      Array.map (fun ki -> h_ij aux_basis ki ki) out_dets
    in
    Array.map (fun ki ->
      Array.mapi (fun j kj ->
        (h_ij aux_basis ki kj) /. (e0.{1} -. h_aa.(j))
      ) out_dets
    ) in_dets
    |> Mat.of_array
  in

  let m_F_aux =
    Array.map (fun ki ->
      Array.map (fun kj ->
        h_ij aux_basis ki kj
      ) out_dets
    ) in_dets
    |> Mat.of_array
  in

  let m_HF =
    gemm m_H_aux m_F_aux ~transb:`T
  in
  (gemm ~transa:`T psi0 @@ gemm m_HF psi0).{1,1}




