Program Listing for File op.cpp

Return to documentation for file (SeQuant/domain/mbpt/op.cpp)

#include <SeQuant/domain/mbpt/context.hpp>
#include <SeQuant/domain/mbpt/op.hpp>

#include <SeQuant/core/math.hpp>
#include <SeQuant/core/op.hpp>
#include <SeQuant/core/tensor.hpp>
#include <SeQuant/core/wick.hpp>

#include <stdexcept>

namespace sequant::mbpt {

std::vector<std::wstring> cardinal_tensor_labels() {
  return {L"κ", L"γ", L"Γ", L"A", L"S", L"P", L"L",  L"λ", L"λ¹",
          L"h", L"f", L"f̃", L"g", L"θ", L"t", L"t¹", L"R", L"F",
          L"X", L"μ", L"V", L"Ṽ", L"B", L"U", L"GR", L"C", overlap_label(),
          L"a", L"ã", L"b", L"b̃", L"E"};
}

std::wstring to_wstring(OpType op) {
  auto found_it = optype2label.find(op);
  if (found_it != optype2label.end())
    return found_it->second;
  else
    throw std::invalid_argument("to_wstring(OpType op): invalid op");
}

OpClass to_class(OpType op) {
  switch (op) {
    case OpType::h:
    case OpType::f:
    case OpType::f̃:
    case OpType::g:
    case OpType::RDM:
    case OpType::RDMCumulant:
    case OpType::δ:
    case OpType::A:
    case OpType::S:
    case OpType::h_1:
    case OpType::θ:
      return OpClass::gen;
    case OpType::t:
    case OpType::R:
    case OpType::R12:
    case OpType::t_1:
      return OpClass::ex;
    case OpType::λ:
    case OpType::L:
    case OpType::λ_1:
      return OpClass::deex;
    default:
      throw std::invalid_argument("to_class(OpType op): invalid op");
  }
}

// Excitation type QNs will have quasiparticle annihilators in every space which
// intersects with the active hole space. The active particle space will have
// quasiparticle creators. The presence of additional blocks depends on whether
// the corresponding active hole or active particle space is a base space.
qns_t excitation_type_qns(std::size_t K, const IndexSpace::QuantumNumbers SQN) {
  qnc_t result;
  if (get_default_context().vacuum() == Vacuum::Physical) {
    result[0] = {0ul, K};
    result[1] = {0ul, K};
  } else {
    auto isr = get_default_context().index_space_registry();
    const auto& base_spaces = isr->base_spaces();
    // are the active qp spaces base spaces?
    bool aps_base = isr->is_base(isr->particle_space(SQN));
    bool ahs_base = isr->is_base(isr->hole_space(SQN));

    for (int i = 0; i < base_spaces.size(); i++) {
      const auto& base_space = base_spaces[i];
      result[i * 2] = {0ul, 0ul};
      result[i * 2 + 1] = {0ul, 0ul};
      if (base_space.qns() != SQN) continue;

      // ex -> creators in particle space
      if (includes(isr->particle_space(SQN).type(), base_space.type())) {
        result[i * 2] = {aps_base ? K : 0ul, K};
      }
      // ex -> annihilators in hole space
      if (includes(isr->hole_space(SQN).type(), base_space.type())) {
        result[i * 2 + 1] = {ahs_base ? K : 0ul, K};
      }
    }
  }
  return result;
}

qns_t interval_excitation_type_qns(std::size_t K,
                                   const IndexSpace::QuantumNumbers SQN) {
  qnc_t result;
  if (get_default_context().vacuum() == Vacuum::Physical) {
    result[0] = {0ul, K};
    result[1] = {0ul, K};
  } else {
    auto isr = get_default_context().index_space_registry();
    const auto& base_spaces = isr->base_spaces();

    for (int i = 0; i < base_spaces.size(); i++) {
      const auto& base_space = base_spaces[i];
      result[i * 2] = {0ul, 0ul};
      result[i * 2 + 1] = {0ul, 0ul};
      if (base_space.qns() != SQN) continue;

      // ex -> creators in particle space
      if (includes(isr->particle_space(SQN).type(), base_space.type())) {
        result[i * 2] = {0ul, K};
      }
      // ex -> annihilators in hole space
      if (includes(isr->hole_space(SQN).type(), base_space.type())) {
        result[i * 2 + 1] = {0ul, K};
      }
    }
  }
  return result;
}

qns_t deexcitation_type_qns(std::size_t K,
                            const IndexSpace::QuantumNumbers SQN) {
  qnc_t result;
  if (get_default_context().vacuum() == Vacuum::Physical) {
    result[0] = {0ul, K};
    result[1] = {0ul, K};
  } else {
    auto isr = get_default_context().index_space_registry();
    const auto& base_spaces = isr->base_spaces();
    bool aps_base = isr->is_base(isr->particle_space(SQN));
    bool ahs_base = isr->is_base(isr->hole_space(SQN));
    for (int i = 0; i < base_spaces.size(); i++) {
      const auto& base_space = base_spaces[i];
      result[i * 2] = {0ul, 0ul};
      result[i * 2 + 1] = {0ul, 0ul};
      if (base_space.qns() != SQN) continue;

      // deex -> annihilators in particle space
      if (includes(isr->particle_space(SQN).type(), base_space.type())) {
        result[i * 2 + 1] = {aps_base ? K : 0ul, K};
      }
      // deex -> creators in hole space
      if (includes(isr->hole_space(SQN).type(), base_space.type())) {
        result[i * 2] = {ahs_base ? K : 0ul, K};
      }
    }
  }
  return result;
}

qns_t interval_deexcitation_type_qns(std::size_t K,
                                     const IndexSpace::QuantumNumbers SQN) {
  qnc_t result;
  if (get_default_context().vacuum() == Vacuum::Physical) {
    result[0] = {0ul, K};
    result[1] = {0ul, K};
  } else {
    auto isr = get_default_context().index_space_registry();
    const auto& base_spaces = isr->base_spaces();
    for (int i = 0; i < base_spaces.size(); i++) {
      const auto& base_space = base_spaces[i];
      result[i * 2] = {0ul, 0ul};
      result[i * 2 + 1] = {0ul, 0ul};
      if (base_space.qns() != SQN) continue;

      // deex -> annihilators in particle space
      if (includes(isr->particle_space(SQN).type(), base_space.type())) {
        result[i * 2 + 1] = {0ul, K};
      }
      // deex -> creators in hole space
      if (includes(isr->hole_space(SQN).type(), base_space.type())) {
        result[i * 2] = {0ul, K};
      }
    }
  }
  return result;
}

qns_t general_type_qns(std::size_t K) {
  qnc_t result;
  for (int i = 0; i < result.size(); i++) {
    result[i] = {0, K};
  }
  return result;
}

qns_t generic_excitation_qns(std::size_t particle_rank, std::size_t hole_rank,
                             IndexSpace particle_space, IndexSpace hole_space,
                             const IndexSpace::QuantumNumbers SQN) {
  qnc_t result;
  if (get_default_context().vacuum() == Vacuum::Physical) {
    result[0] = {0ul, hole_rank};
    result[1] = {0ul, particle_rank};
  } else {
    auto isr = get_default_context().index_space_registry();
    const auto& base_spaces = isr->base_spaces();
    bool aps_base = isr->is_base(isr->particle_space(SQN));
    bool ahs_base = isr->is_base(isr->hole_space(SQN));
    for (int i = 0; i < base_spaces.size(); i++) {
      const auto& base_space = base_spaces[i];
      result[i * 2] = {0ul, 0ul};
      result[i * 2 + 1] = {0ul, 0ul};
      if (base_space.qns() != SQN) continue;

      // ex -> creators in particle space
      if (includes(particle_space.type(), base_space.type())) {
        result[i * 2] = {aps_base ? particle_rank : 0ul,
                         particle_rank};  // creators
      }
      // ex -> annihilators in hole space
      if (includes(hole_space.type(), base_space.type())) {
        result[i * 2 + 1] = {ahs_base ? hole_rank : 0ul,
                             hole_rank};  // annihilators
      }
    }
  }
  return result;
}

qns_t generic_deexcitation_qns(std::size_t particle_rank, std::size_t hole_rank,
                               IndexSpace particle_space, IndexSpace hole_space,
                               const IndexSpace::QuantumNumbers SQN) {
  qnc_t result;
  if (get_default_context().vacuum() == Vacuum::Physical) {
    result[0] = {0ul, particle_rank};
    result[1] = {0ul, hole_rank};
  } else {
    auto isr = get_default_context().index_space_registry();
    const auto& base_spaces = isr->base_spaces();
    bool aps_base = isr->is_base(isr->particle_space(SQN));
    bool ahs_base = isr->is_base(isr->hole_space(SQN));
    for (int i = 0; i < base_spaces.size(); i++) {
      const auto& base_space = base_spaces[i];
      result[i * 2] = {0ul, 0ul};
      result[i * 2 + 1] = {0ul, 0ul};
      if (base_space.qns() != SQN) continue;

      // deex -> creators in hole space
      if (includes(hole_space.type(), base_space.type())) {
        result[i * 2] = {ahs_base ? hole_rank : 0ul, hole_rank};  // creators
      }
      // deex -> annihilators in particle space
      if (includes(particle_space.type(), base_space.type())) {
        result[i * 2 + 1] = {aps_base ? particle_rank : 0ul,
                             particle_rank};  // annihilators
      }
    }
  }
  return result;
}

// By counting the number of contractions between indices of proper type, we can
// know the quantum numbers of a combined result.
qns_t combine(qns_t a, qns_t b) {
  assert(a.size() == b.size());
  qns_t result;

  if (get_default_context().vacuum() == Vacuum::Physical) {
    qns_t result;
    const auto ncontr = qninterval_t{0, std::min(b[0].upper(), a[1].upper())};
    const auto nc = nonnegative(a[0] + b[0] - ncontr);
    const auto na = nonnegative(a[1] + b[1] - ncontr);
    result[0] = nc;
    result[1] = na;
    return result;
  } else if (get_default_context().vacuum() == Vacuum::SingleProduct) {
    auto isr = get_default_context().index_space_registry();
    const auto& base_spaces = isr->base_spaces();
    for (auto i = 0; i < base_spaces.size(); i++) {
      auto cre = i * 2;
      auto ann = (i * 2) + 1;
      auto base_is_fermi_occupied = isr->is_pure_occupied(
          base_spaces[i]);  // need to distinguish particle and hole
                            // contractions.
      auto ncontr_space =
          base_is_fermi_occupied
              ? qninterval_t{0, std::min(b[ann].upper(), a[cre].upper())}
              : qninterval_t{0, std::min(b[cre].upper(), a[ann].upper())};
      auto nc_space = nonnegative(b[cre] + a[cre] - ncontr_space);
      auto na_space = nonnegative(b[ann] + a[ann] - ncontr_space);
      result[cre] = nc_space;
      result[ann] = na_space;
    }
    return result;
  } else {
    throw "unsupported vacuum context.";
  }
}

// must be defined including op.ipp since it's used there
template <>
bool is_vacuum<qns_t>(qns_t qns) {
  return qns == qns_t{};
}

}  // namespace sequant::mbpt

namespace sequant {

mbpt::qns_t adjoint(mbpt::qns_t qns) {
  mbpt::qns_t new_qnst;
  new_qnst.resize(qns.size());
  auto is_even = [](int i) { return i % 2 == 0; };
  for (int i = 0; i < qns.size(); i++) {
    if (is_even(i)) {
      new_qnst[i + 1] = qns[i];
    } else {
      new_qnst[i - 1] = qns[i];
    }
  }
  return new_qnst;
}

template <Statistics S>
std::wstring to_latex(const mbpt::Operator<mbpt::qns_t, S>& op) {
  using namespace sequant::mbpt;

  auto result = L"{\\hat{" + utf_to_latex(op.label()) + L"}";

  // check if operator has adjoint label, remove if present for base label
  auto base_lbl = sequant::to_wstring(op.label());
  bool is_adjoint = false;
  if (base_lbl.back() == adjoint_label) {
    is_adjoint = true;
    base_lbl.pop_back();
  }

  auto it = label2optype.find(base_lbl);
  OpType optype = OpType::invalid;
  if (it != label2optype.end()) {  // handle special cases
    optype = it->second;
    if (to_class(optype) == OpClass::gen) {
      if (optype == OpType::θ) {  // special case for θ
        result += L"_{" + std::to_wstring(op()[0].upper()) + L"}";
      }
      result += L"}";
      return result;
    }
  }
  std::wstring baseline_char = is_adjoint ? L"^" : L"_";
  if (get_default_context().vacuum() == Vacuum::Physical) {
    if (op()[0] == op()[1]) {  // particle conserving
      result += L"_{" + std::to_wstring(op()[0].lower()) + L"}";
    } else {  // non-particle conserving
      result += L"_{" + std::to_wstring(op()[1].lower()) + L"}^{" +
                std::to_wstring(op()[0].lower()) + L"}";
    }
  } else {  // single product vacuum
    auto nann_p = is_adjoint ? op().ncre_particles() : op().nann_particles();
    auto ncre_h = is_adjoint ? op().nann_holes() : op().ncre_holes();
    auto ncre_p = is_adjoint ? op().nann_particles() : op().ncre_particles();
    auto nann_h = is_adjoint ? op().ncre_holes() : op().nann_holes();

    if (!is_definite(nann_p) || !is_definite(ncre_h) || !is_definite(ncre_p) ||
        !is_definite(nann_h)) {
      throw std::invalid_argument(
          "to_latex(const Operator<qns_t, S>& op): "
          "can only handle generic operators with definite cre/ann numbers");
    }

    // pure quasiparticle creator/annihilator?
    const auto qprank_cre = ncre_p.lower() + nann_h.lower();
    const auto qprank_ann = nann_p.lower() + ncre_h.lower();
    const auto qppure = qprank_cre == 0 || qprank_ann == 0;
    if (qppure) {
      if (qprank_cre) {
        if (ncre_p.lower() == nann_h.lower()) {  // q-particle conserving
          result +=
              baseline_char + L"{" + std::to_wstring(nann_h.lower()) + L"}";
        } else {  // particle non-conserving
          result += baseline_char + L"{" + std::to_wstring(nann_h.lower()) +
                    L"," + std::to_wstring(ncre_p.lower()) + L"}";
        }
      } else {
        if (ncre_h.lower() == nann_p.lower()) {  // q-particle conserving
          result +=
              baseline_char + L"{" + std::to_wstring(ncre_h.lower()) + L"}";
        } else {  // q-particle non-conserving
          result += baseline_char + L"{" + std::to_wstring(ncre_h.lower()) +
                    L"," + std::to_wstring(nann_p.lower()) + L"}";
        }
      }
    } else {  // not pure qp creator/annihilator
      result += L"_{" + std::to_wstring(nann_h.lower()) + L"," +
                std::to_wstring(ncre_p.lower()) + L"}^{" +
                std::to_wstring(ncre_h.lower()) + L"," +
                std::to_wstring(nann_p.lower()) + L"}";
    }
  }
  result += L"}";
  return result;
}

}  // namespace sequant

#include <SeQuant/domain/mbpt/op.ipp>

namespace sequant::mbpt {

template <Statistics S>
OpMaker<S>::OpMaker(OpType op) : op_(op) {}

template <Statistics S>
OpMaker<S>::OpMaker(OpType op, ncre nc, nann na) {
  op_ = op;
  assert(nc > 0 || na > 0);
  switch (to_class(op)) {
    case OpClass::ex:
      cre_spaces_ = decltype(cre_spaces_)(nc, get_particle_space(Spin::any));
      ann_spaces_ = decltype(ann_spaces_)(na, get_hole_space(Spin::any));
      break;
    case OpClass::deex:
      cre_spaces_ = decltype(cre_spaces_)(nc, get_hole_space(Spin::any));
      ann_spaces_ = decltype(ann_spaces_)(na, get_particle_space(Spin::any));
      break;
    case OpClass::gen:
      cre_spaces_ = decltype(cre_spaces_)(nc, get_complete_space(Spin::any));
      ann_spaces_ = decltype(ann_spaces_)(na, get_complete_space(Spin::any));
      break;
  }
}

template <Statistics S>
OpMaker<S>::OpMaker(OpType op, std::size_t rank)
    : OpMaker(op, ncre(rank), nann(rank)) {}

template <Statistics S>
OpMaker<S>::OpMaker(OpType op, ncre nc, nann na,
                    const cre<IndexSpace>& cre_space,
                    const ann<IndexSpace>& ann_space) {
  op_ = op;
  assert(nc > 0 || na > 0);
  cre_spaces_ = decltype(cre_spaces_)(nc, cre_space);
  ann_spaces_ = decltype(ann_spaces_)(na, ann_space);
}

template <Statistics S>
ExprPtr OpMaker<S>::operator()(std::optional<UseDepIdx> dep,
                               std::optional<Symmetry> opsymm_opt) const {
  auto isr = get_default_context(Statistics::FermiDirac).index_space_registry();
  // if not given dep, use mbpt::Context::CSV to determine whether to use
  // dependent indices for pure (de)excitation ops
  if (!dep && get_default_mbpt_context().csv() == mbpt::CSV::Yes) {
    if (to_class(op_) == OpClass::ex) {
      for (auto&& s : cre_spaces_) {
        assert(isr->contains_unoccupied(s));
      }
      dep = UseDepIdx::Bra;
    } else if (to_class(op_) == OpClass::deex) {
      for (auto&& s : ann_spaces_) {
        assert(isr->contains_unoccupied(s));
      }
      dep = UseDepIdx::Ket;
    } else {
      dep = UseDepIdx::None;
    }
  }

  return make(
      cre_spaces_, ann_spaces_,
      [this, opsymm_opt](const auto& creidxs, const auto& annidxs,
                         Symmetry opsymm) {
        return ex<Tensor>(to_wstring(op_), bra(creidxs), ket(annidxs),
                          opsymm_opt ? *opsymm_opt : opsymm);
      },
      dep ? *dep : UseDepIdx::None);
}

template class OpMaker<Statistics::FermiDirac>;
template class OpMaker<Statistics::BoseEinstein>;

template class Operator<qns_t, Statistics::FermiDirac>;
template class Operator<qns_t, Statistics::BoseEinstein>;

inline namespace op {

namespace tensor {
ExprPtr H_(std::size_t k) {
  assert(k > 0 && k <= 2);
  switch (k) {
    case 1:
      switch (get_default_context().vacuum()) {
        case Vacuum::Physical:
          return OpMaker<Statistics::FermiDirac>(OpType::h, 1)();
        case Vacuum::SingleProduct:
          return OpMaker<Statistics::FermiDirac>(OpType::f, 1)();
        case Vacuum::MultiProduct:
          return OpMaker<Statistics::FermiDirac>(OpType::f, 1)();
        default:
          abort();
      }

    case 2:
      return OpMaker<Statistics::FermiDirac>(OpType::g, 2)();

    default:
      abort();
  }
}

ExprPtr H(std::size_t k) {
  assert(k > 0 && k <= 2);
  return k == 1 ? tensor::H_(1) : tensor::H_(1) + tensor::H_(2);
}

ExprPtr F(bool use_tensor, IndexSpace reference_occupied) {
  if (use_tensor) {
    return OpMaker<Statistics::FermiDirac>(OpType::f, 1)();
  } else {                       // explicit density matrix construction
    assert(reference_occupied);  // cannot explicitly instantiate fock operator
                                 // without providing an occupied indexspace
    // add \bar{g}^{\kappa x}_{\lambda y} \gamma^y_x with x,y in occ_space_type
    auto make_g_contribution = [](const auto occ_space) {
      auto isr = get_default_context().index_space_registry();
      return mbpt::OpMaker<Statistics::FermiDirac>::make(
          {isr->complete_space(Spin::any)}, {isr->complete_space(Spin::any)},
          [=](auto braidxs, auto ketidxs, Symmetry opsymm) {
            auto m1 = Index::make_tmp_index(occ_space);
            auto m2 = Index::make_tmp_index(occ_space);
            assert(opsymm == Symmetry::antisymm || opsymm == Symmetry::nonsymm);
            if (opsymm == Symmetry::antisymm) {
              braidxs.push_back(m1);
              ketidxs.push_back(m2);
              return ex<Tensor>(to_wstring(mbpt::OpType::g),
                                bra(std::move(braidxs)),
                                ket(std::move(ketidxs)), Symmetry::antisymm) *
                     ex<Tensor>(to_wstring(mbpt::OpType::δ), bra{m2}, ket{m1},
                                Symmetry::nonsymm);
            } else {  // opsymm == Symmetry::nonsymm
              auto braidx_J = braidxs;
              braidx_J.push_back(m1);
              auto ketidxs_J = ketidxs;
              ketidxs_J.push_back(m2);
              auto braidx_K = braidxs;
              braidx_K.push_back(m1);
              auto ketidxs_K = ketidxs;
              using std::begin;
              ketidxs_K.emplace(begin(ketidxs_K), m2);
              return (ex<Tensor>(to_wstring(mbpt::OpType::g),
                                 bra(std::move(braidx_J)),
                                 ket(std::move(ketidxs_J)), Symmetry::nonsymm) -
                      ex<Tensor>(
                          to_wstring(mbpt::OpType::g), bra(std::move(braidx_K)),
                          ket(std::move(ketidxs_K)), Symmetry::nonsymm)) *
                     ex<Tensor>(to_wstring(mbpt::OpType::δ), bra{m2}, ket{m1},
                                Symmetry::nonsymm);
            }
          });
    };
    auto isr = get_default_context().index_space_registry();
    return OpMaker<Statistics::FermiDirac>(OpType::h, 1)() +
           make_g_contribution(reference_occupied);
  }
}

ExprPtr θ(std::size_t K) {
  return OpMaker<Statistics::FermiDirac>(OpType::θ, K)();
}

ExprPtr T_(std::size_t K) {
  return OpMaker<Statistics::FermiDirac>(OpType::t, K)();
}

ExprPtr T(std::size_t K, bool skip1) {
  assert(K > (skip1 ? 1 : 0));
  ExprPtr result;
  for (auto k = skip1 ? 2ul : 1ul; k <= K; ++k) {
    result += tensor::T_(k);
  }
  return result;
}

ExprPtr Λ_(std::size_t K) {
  return OpMaker<Statistics::FermiDirac>(OpType::λ, K)();
}

ExprPtr Λ(std::size_t K) {
  assert(K > 0);

  ExprPtr result;
  for (auto k = 1ul; k <= K; ++k) {
    result = k > 1 ? result + tensor::Λ_(k) : tensor::Λ_(k);
  }
  return result;
}

ExprPtr R_(nann na, ncre nc, const cre<IndexSpace>& cre_space,
           const ann<IndexSpace>& ann_space) {
  return OpMaker<Statistics::FermiDirac>(OpType::R, nc, na, cre_space,
                                         ann_space)();
}
ExprPtr R_(nₚ np, nₕ nh) {
  assert(np >= 0 && nh >= 0);
  return OpMaker<Statistics::FermiDirac>(OpType::R, ncre(np.value()),
                                         nann(nh.value()))();
}

ExprPtr L_(nann na, ncre nc, const cre<IndexSpace>& cre_space,
           const ann<IndexSpace>& ann_space) {
  return OpMaker<Statistics::FermiDirac>(OpType::L, nc, na, cre_space,
                                         ann_space)();
}

ExprPtr L_(nₚ np, nₕ nh) {
  assert(np >= 0 && nh >= 0);
  return OpMaker<Statistics::FermiDirac>(OpType::L, ncre(nh.value()),
                                         nann(np.value()))();
}

ExprPtr P(nₚ np, nₕ nh) {
  if (np != nh)
    assert(
        get_default_context().spbasis() != SPBasis::spinfree &&
        "Spinfree basis does not support non-particle conserving projectors");
  return get_default_context().spbasis() == SPBasis::spinfree
             ? tensor::S(-nh /* nh == np */)
             : tensor::A(-np, -nh);
}

ExprPtr A(nₚ np, nₕ nh) {
  assert(!(np == 0 && nh == 0));
  // if one of them is not zero, nh and np should have the same sign
  if (np != 0 && nh != 0) {
    assert((np > 0 && nh > 0) || (np < 0 && nh < 0));
  }

  container::svector<IndexSpace> creators;
  container::svector<IndexSpace> annihilators;
  if (nh > 0)  // ex
    for ([[maybe_unused]] auto i : ranges::views::iota(0, nh))
      annihilators.emplace_back(get_hole_space(Spin::any));
  else if (nh < 0)  // deex
    for ([[maybe_unused]] auto i : ranges::views::iota(0, -nh))
      creators.emplace_back(get_hole_space(Spin::any));
  if (np > 0)  // ex
    for ([[maybe_unused]] auto i : ranges::views::iota(0, np))
      creators.emplace_back(get_particle_space(Spin::any));
  else if (np < 0)  // deex
    for ([[maybe_unused]] auto i : ranges::views::iota(0, -np))
      annihilators.emplace_back(get_particle_space(Spin::any));
  // don't populate if rank is zero

  std::optional<OpMaker<Statistics::FermiDirac>::UseDepIdx> dep;
  if (get_default_mbpt_context().csv() == mbpt::CSV::Yes)
    dep = (np > 0 || nh > 0) ? OpMaker<Statistics::FermiDirac>::UseDepIdx::Bra
                             : OpMaker<Statistics::FermiDirac>::UseDepIdx::Ket;
  return OpMaker<Statistics::FermiDirac>(
      OpType::A, cre(creators), ann(annihilators))(dep, {Symmetry::antisymm});
}

ExprPtr S(std::int64_t K) {
  assert(K != 0);
  container::svector<IndexSpace> creators;
  container::svector<IndexSpace> annihilators;
  if (K > 0)  // ex
    for ([[maybe_unused]] auto i : ranges::views::iota(0, K))
      annihilators.emplace_back(get_hole_space(Spin::any));
  else  // deex
    for ([[maybe_unused]] auto i : ranges::views::iota(0, -K))
      creators.emplace_back(get_hole_space(Spin::any));
  if (K > 0)  // ex
    for ([[maybe_unused]] auto i : ranges::views::iota(0, K))
      creators.emplace_back(get_particle_space(Spin::any));
  else  // deex
    for ([[maybe_unused]] auto i : ranges::views::iota(0, -K))
      annihilators.emplace_back(get_particle_space(Spin::any));

  std::optional<OpMaker<Statistics::FermiDirac>::UseDepIdx> dep;
  if (get_default_mbpt_context().csv() == mbpt::CSV::Yes)
    dep = K > 0 ? OpMaker<Statistics::FermiDirac>::UseDepIdx::Bra
                : OpMaker<Statistics::FermiDirac>::UseDepIdx::Ket;
  return OpMaker<Statistics::FermiDirac>(
      OpType::S, cre(creators), ann(annihilators))(dep, {Symmetry::nonsymm});
}

ExprPtr H_pt(std::size_t order, std::size_t R) {
  assert(order == 1 &&
         "sequant::sr::H_pt(): only supports first order perturbation");
  assert(R > 0);
  return OpMaker<Statistics::FermiDirac>(OpType::h_1, R)();
}

ExprPtr T_pt_(std::size_t order, std::size_t K) {
  assert(order == 1 &&
         "sequant::sr::T_pt_(): only supports first order perturbation");
  return OpMaker<Statistics::FermiDirac>(OpType::t_1, K)();
}

ExprPtr T_pt(std::size_t order, std::size_t K, bool skip1) {
  assert(K > (skip1 ? 1 : 0));
  ExprPtr result;
  for (auto k = (skip1 ? 2ul : 1ul); k <= K; ++k) {
    result = k > 1 ? result + tensor::T_pt_(order, k) : tensor::T_pt_(order, k);
  }
  return result;
}

ExprPtr Λ_pt_(std::size_t order, std::size_t K) {
  assert(order == 1 &&
         "sequant::sr::Λ_pt_(): only supports first order perturbation");
  return OpMaker<Statistics::FermiDirac>(OpType::λ_1, K)();
}

ExprPtr Λ_pt(std::size_t order, std::size_t K, bool skip1) {
  assert(K > (skip1 ? 1 : 0));
  ExprPtr result;
  for (auto k = (skip1 ? 2ul : 1ul); k <= K; ++k) {
    result = k > 1 ? result + tensor::Λ_pt_(order, k) : tensor::Λ_pt_(order, k);
  }
  return result;
}

}  // namespace tensor

ExprPtr H_(std::size_t k) {
  assert(k > 0 && k <= 2);
  switch (k) {
    case 1:
      return ex<op_t>(
          [vacuum = get_default_context().vacuum()]() -> std::wstring_view {
            switch (vacuum) {
              case Vacuum::Physical:
                return L"h";
              case Vacuum::SingleProduct:
                return L"f";
              case Vacuum::MultiProduct:
                return L"f";
              default:
                abort();
            }
          },
          [=]() -> ExprPtr { return tensor::H_(1); },
          [=](qnc_t& qns) {
            qnc_t op_qnc_t = general_type_qns(1);
            qns = combine(op_qnc_t, qns);
          });

    case 2:
      return ex<op_t>([]() -> std::wstring_view { return L"g"; },
                      [=]() -> ExprPtr { return tensor::H_(2); },
                      [=](qnc_t& qns) {
                        qnc_t op_qnc_t = general_type_qns(2);
                        qns = combine(op_qnc_t, qns);
                      });

    default:
      abort();
  }
}

ExprPtr H(std::size_t k) {
  assert(k > 0 && k <= 2);
  return k == 1 ? H_(1) : H_(1) + H_(2);
}

ExprPtr θ(std::size_t K) {
  assert(K > 0);
  return ex<op_t>([]() -> std::wstring_view { return L"θ"; },
                  [=]() -> ExprPtr { return tensor::θ(K); },
                  [=](qnc_t& qns) {
                    qnc_t op_qnc_t = general_type_qns(K);
                    qns = combine(op_qnc_t, qns);
                  });
}

ExprPtr T_(std::size_t K) {
  assert(K > 0);
  return ex<op_t>([]() -> std::wstring_view { return L"t"; },
                  [=]() -> ExprPtr { return tensor::T_(K); },
                  [=](qnc_t& qns) {
                    qnc_t op_qnc_t = excitation_type_qns(K);
                    qns = combine(op_qnc_t, qns);
                  });
}

ExprPtr T(std::size_t K, bool skip1) {
  assert(K > (skip1 ? 1 : 0));
  ExprPtr result;
  for (auto k = skip1 ? 2ul : 1ul; k <= K; ++k) {
    result += T_(k);
  }
  return result;
}

ExprPtr Λ_(std::size_t K) {
  assert(K > 0);
  return ex<op_t>([]() -> std::wstring_view { return L"λ"; },
                  [=]() -> ExprPtr { return tensor::Λ_(K); },
                  [=](qnc_t& qns) {
                    qnc_t op_qnc_t = deexcitation_type_qns(K);
                    qns = combine(op_qnc_t, qns);
                  });
}

ExprPtr Λ(std::size_t K) {
  assert(K > 0);
  ExprPtr result;
  for (auto k = 1ul; k <= K; ++k) {
    result = k > 1 ? result + Λ_(k) : Λ_(k);
  }
  return result;
}

ExprPtr F(bool use_f_tensor, IndexSpace occupied_density) {
  if (use_f_tensor) {
    return ex<op_t>(
        []() -> std::wstring_view { return L"f"; },
        [=]() -> ExprPtr { return tensor::F(true, occupied_density); },
        [=](qnc_t& qns) {
          qnc_t op_qnc_t = general_type_qns(1);
          qns = combine(op_qnc_t, qns);
        });
  } else {
    throw "non-tensor use at operator level not yet supported";
  }
}

ExprPtr A(nₚ np, nₕ nh) {
  assert(!(nh == 0 && np == 0));
  // if one of them is not zero, nh and np should have the same sign
  if (nh != 0 && np != 0) {
    assert((nh > 0 && np > 0) || (nh < 0 && np < 0));
  }
  // if np or nh is negative, it's a deexcitation operator
  const auto deexcitation = (np < 0 || nh < 0);

  auto particle_space = get_particle_space(Spin::any);
  auto hole_space = get_hole_space(Spin::any);
  return ex<op_t>([]() -> std::wstring_view { return L"A"; },
                  [=]() -> ExprPtr { return tensor::A(np, nh); },
                  [=](qnc_t& qns) {
                    const std::size_t abs_nh = std::abs(nh);
                    const std::size_t abs_np = std::abs(np);
                    if (deexcitation) {
                      qnc_t op_qnc_t = generic_deexcitation_qns(
                          abs_np, abs_nh, particle_space, hole_space);
                      qns = combine(op_qnc_t, qns);
                    } else {
                      qnc_t op_qnc_t = generic_excitation_qns(
                          abs_np, abs_nh, particle_space, hole_space);
                      qns = combine(op_qnc_t, qns);
                    }
                  });
}

ExprPtr S(std::int64_t K) {
  assert(K != 0);
  return ex<op_t>([]() -> std::wstring_view { return L"S"; },
                  [=]() -> ExprPtr { return tensor::S(K); },
                  [=](qnc_t& qns) {
                    const std::size_t abs_K = std::abs(K);
                    if (K < 0) {
                      qnc_t op_qnc_t = deexcitation_type_qns(abs_K);
                      qns = combine(op_qnc_t, qns);
                    } else {
                      qnc_t op_qnc_t = excitation_type_qns(abs_K);
                      qns = combine(op_qnc_t, qns);
                    }
                  });
}

ExprPtr P(nₚ np, nₕ nh) {
  if (get_default_context().spbasis() == SPBasis::spinfree) {
    assert(nh == np &&
           "Only particle number conserving cases are supported with spinfree "
           "basis for now");
    const auto K = np;  // K = np = nh
    return S(-K);
  } else {
    assert(get_default_context().spbasis() == SPBasis::spinorbital);
    return A(-np, -nh);
  }
}

ExprPtr H_pt(std::size_t order, std::size_t R) {
  assert(R > 0);
  assert(order == 1 && "only first order perturbation is supported now");
  return ex<op_t>(
      []() -> std::wstring_view { return optype2label.at(OpType::h_1); },
      [=]() -> ExprPtr { return tensor::H_pt(order, R); },
      [=](qnc_t& qns) { qns = combine(general_type_qns(R), qns); });
}

ExprPtr T_pt_(std::size_t order, std::size_t K) {
  assert(K > 0);
  assert(order == 1 && "only first order perturbation is supported now");
  return ex<op_t>(
      []() -> std::wstring_view { return optype2label.at(OpType::t_1); },
      [=]() -> ExprPtr { return tensor::T_pt_(order, K); },
      [=](qnc_t& qns) { qns = combine(excitation_type_qns(K), qns); });
}

ExprPtr T_pt(std::size_t order, std::size_t K, bool skip1) {
  assert(K > (skip1 ? 1 : 0));
  ExprPtr result;
  for (auto k = (skip1 ? 2ul : 1ul); k <= K; ++k) {
    result = k > 1 ? result + T_pt_(order, k) : T_pt_(order, k);
  }
  return result;
}

ExprPtr Λ_pt_(std::size_t order, std::size_t K) {
  assert(K > 0);
  assert(order == 1 && "only first order perturbation is supported now");
  return ex<op_t>(
      []() -> std::wstring_view { return optype2label.at(OpType::λ_1); },
      [=]() -> ExprPtr { return tensor::Λ_pt_(order, K); },
      [=](qnc_t& qns) { qns = combine(deexcitation_type_qns(K), qns); });
}

ExprPtr Λ_pt(std::size_t order, std::size_t K, bool skip1) {
  assert(K > (skip1 ? 1 : 0));
  ExprPtr result;
  for (auto k = (skip1 ? 2ul : 1ul); k <= K; ++k) {
    result = k > 1 ? result + Λ_pt_(order, k) : Λ_pt_(order, k);
  }
  return result;
}

ExprPtr R_(nann na, ncre nc, const cre<IndexSpace>& cre_space,
           const ann<IndexSpace>& ann_space) {
  return ex<op_t>(
      []() -> std::wstring_view { return optype2label.at(OpType::R); },
      [=]() -> ExprPtr { return tensor::R_(na, nc, cre_space, ann_space); },
      [=](qnc_t& qns) {
        // ex -> creators in particle_space, annihilators in hole_space
        qns = combine(
            generic_excitation_qns(/*particle_rank*/ nc, /*hole_rank*/ na,
                                   cre_space, ann_space),
            qns);
      });
}

ExprPtr R_(nₚ np, nₕ nh) { return R_(nann(nh), ncre(np)); }

ExprPtr L_(nann na, ncre nc, const cre<IndexSpace>& cre_space,
           const ann<IndexSpace>& ann_space) {
  return ex<op_t>(
      []() -> std::wstring_view { return optype2label.at(OpType::L); },
      [=]() -> ExprPtr { return tensor::L_(na, nc, cre_space, ann_space); },
      [=](qnc_t& qns) {
        // deex -> creators in hole_space, annihilators in particle_space
        qns = combine(
            generic_deexcitation_qns(
                /*particle_rank*/ na, /*hole_rank*/ nc, ann_space, cre_space),
            qns);
      });
}

ExprPtr L_(nₚ np, nₕ nh) { return L_(nann(np), ncre(nh)); }

ExprPtr R(nann na, ncre nc, const cre<IndexSpace>& cre_space,
          const ann<IndexSpace>& ann_space) {
  assert(na > 0 || nc > 0);
  ExprPtr result;

  for (std::int64_t ra = na, rc = nc; ra > 0 || rc > 0; --ra, --rc) {
    result += R_(nann(ra), ncre(rc), cre_space, ann_space);
  }
  return result;
}

ExprPtr R(nₚ np, nₕ nh) {
  assert(np >= 0 && nh >= 0);
  return R(nann(nh), ncre(np));
}

ExprPtr L(nann na, ncre nc, const cre<IndexSpace>& cre_space,
          const ann<IndexSpace>& ann_space) {
  assert(na > 0 || nc > 0);
  ExprPtr result;

  for (std::int64_t ra = na, rc = nc; ra > 0 || rc > 0; --ra, --rc) {
    result += L_(nann(ra), ncre(rc), cre_space, ann_space);
  }
  return result;
}

ExprPtr L(nₚ np, nₕ nh) { return L(nann(np), ncre(nh)); }

bool can_change_qns(const ExprPtr& op_or_op_product, const qns_t target_qns,
                    const qns_t source_qns = {}) {
  qns_t qns = source_qns;
  if (op_or_op_product.is<Product>()) {
    const auto& op_product = op_or_op_product.as<Product>();
    for (auto& op_ptr : ranges::views::reverse(op_product.factors())) {
      assert(op_ptr->template is<op_t>());
      const auto& op = op_ptr->template as<op_t>();
      qns = op(qns);
    }
    return qns.overlaps_with(target_qns);
  } else if (op_or_op_product.is<op_t>()) {
    const auto& op = op_or_op_product.as<op_t>();
    qns = op();
    return qns.overlaps_with(target_qns);
  } else
    throw std::invalid_argument(
        "sequant::mbpt::sr::contains_rank(op_or_op_product): op_or_op_product "
        "must be mbpt::sr::op_t or Product thereof");
}

bool raises_vacuum_up_to_rank(const ExprPtr& op_or_op_product,
                              const unsigned long k) {
  assert(op_or_op_product.is<op_t>() || op_or_op_product.is<Product>());

  return can_change_qns(op_or_op_product, interval_excitation_type_qns(k));
}

bool lowers_rank_or_lower_to_vacuum(const ExprPtr& op_or_op_product,
                                    const unsigned long k) {
  assert(op_or_op_product.is<op_t>() || op_or_op_product.is<Product>());
  return can_change_qns(op_or_op_product, qns_t{},
                        interval_excitation_type_qns(k));
}

bool raises_vacuum_to_rank(const ExprPtr& op_or_op_product,
                           const unsigned long k) {
  assert(op_or_op_product.is<op_t>() || op_or_op_product.is<Product>());
  return can_change_qns(op_or_op_product, excitation_type_qns(k));
}

bool lowers_rank_to_vacuum(const ExprPtr& op_or_op_product,
                           const unsigned long k) {
  assert(op_or_op_product.is<op_t>() || op_or_op_product.is<Product>());
  return can_change_qns(op_or_op_product, qns_t{}, excitation_type_qns(k));
}

#include <SeQuant/domain/mbpt/vac_av.ipp>

namespace tensor {

ExprPtr vac_av(ExprPtr expr, std::vector<std::pair<int, int>> nop_connections,
               bool use_top) {
  simplify(expr);
  auto isr = get_default_context().index_space_registry();
  const auto spinorbital =
      get_default_context().spbasis() == SPBasis::spinorbital;
  // convention is to use different label for spin-orbital and spin-free RDM
  const auto rdm_label = spinorbital ? optype2label.at(OpType::RDM) : L"Γ";

  // only need full contractions if don't have any density outside of
  // the orbitals occupied in the vacuum
  bool full_contractions =
      (isr->reference_occupied_space() == isr->vacuum_occupied_space()) ? true
                                                                        : false;
  // N.B. reference < vacuum is not yet supported
  if (isr->reference_occupied_space().intersection(
          isr->vacuum_occupied_space()) != isr->vacuum_occupied_space()) {
    throw std::invalid_argument(
        "mbpt::tensor::vac_av: vacuum occupied orbitals must be same as or "
        "subset of the reference orbital set.");
  }

  FWickTheorem wick{expr};
  wick.use_topology(use_top).set_nop_connections(nop_connections);
  wick.full_contractions(full_contractions);
  auto result = wick.compute();
  simplify(result);

  if (Logger::instance().wick_stats) {
    std::wcout << "WickTheorem stats: # of contractions attempted = "
               << wick.stats().num_attempted_contractions
               << " # of useful contractions = "
               << wick.stats().num_useful_contractions << std::endl;
  }
  // only need to handle the special case where the dense(at least partially
  // occupied) states, contain additional functions to the vacuum_occupied.
  // including a density occupied partition using a "single-reference" method
  // will replace FNOPs with RDMs. i.e. "multi-reference" RDM replacement rules
  // work in the limit of one reference.
  if (isr->reference_occupied_space() == IndexSpace::Type{} ||
      isr->reference_occupied_space(Spin::any) ==
          isr->vacuum_occupied_space(Spin::any)) {
    return result;
  } else {
    const auto target_rdm_space_type =
        get_default_context().vacuum() == Vacuum::SingleProduct
            ? isr->intersection(isr->particle_space(Spin::any),
                                isr->hole_space(Spin::any))
            : isr->reference_occupied_space(Spin::any);

    // STEP1. replace NOPs by RDM
    auto replace_nop_with_rdm = [&rdm_label, spinorbital](ExprPtr& exptr) {
      auto replace = [&rdm_label, spinorbital](const auto& nop) -> ExprPtr {
        using index_container = container::svector<Index>;
        auto braidxs = nop.annihilators() |
                       ranges::views::transform(
                           [](const auto& op) { return op.index(); }) |
                       ranges::to<index_container>();
        auto ketidxs = nop.creators() |
                       ranges::views::transform(
                           [](const auto& op) { return op.index(); }) |
                       ranges::to<index_container>();
        assert(braidxs.size() ==
               ketidxs.size());  // need to handle particle # violating case?
        const auto rank = braidxs.size();
        return ex<Tensor>(
            rdm_label, bra(std::move(braidxs)), ket(std::move(ketidxs)),
            rank > 1 && spinorbital ? Symmetry::antisymm : Symmetry::nonsymm);
      };

      if (exptr.template is<FNOperator>()) {
        exptr = replace(exptr.template as<FNOperator>());
      } else if (exptr.template is<BNOperator>()) {
        exptr = replace(exptr.template as<BNOperator>());
      }
    };
    result->visit(replace_nop_with_rdm, true);

    // STEP 2: project RDM indices onto the target RDM subspace
    // since RDM indices only make sense within a single TN expand + flatten
    // first, then do the projection individually for each TN
    expand(result);
    // flatten(result);  // TODO where is flatten?
    auto project_rdm_indices_to_target = [&](ExprPtr& exptr) {
      auto impl_for_single_tn = [&](ProductPtr& product_ptr) {
        // enlist all indices and count their instances
        auto for_each_index_in_tn = [](const auto& product_ptr,
                                       const auto& op) {
          ranges::for_each(product_ptr->factors(), [&](auto& factor) {
            auto tensor_ptr = std::dynamic_pointer_cast<AbstractTensor>(factor);
            if (tensor_ptr) {
              ranges::for_each(tensor_ptr->_braket(),
                               [&](auto& idx) { op(idx, *tensor_ptr); });
            }
          });
        };

        // compute external indices
        container::map<Index, std::size_t> indices_w_counts;
        auto retrieve_indices_with_counts = [&indices_w_counts](const auto& idx,
                                                                auto&) {
          auto found_it = indices_w_counts.find(idx);
          if (found_it != indices_w_counts.end()) {
            found_it->second++;
          } else {
            indices_w_counts.emplace(idx, 1);
          }
        };
        for_each_index_in_tn(product_ptr, retrieve_indices_with_counts);

        container::set<Index> external_indices =
            indices_w_counts | ranges::views::filter([](auto& idx_cnt) {
              auto& [idx, cnt] = idx_cnt;
              return cnt == 1;
            }) |
            ranges::views::keys | ranges::to<container::set<Index>>;

        // extract RDM-only and all indices
        container::set<Index> rdm_indices;
        std::set<Index, Index::LabelCompare> all_indices;
        auto retrieve_rdm_and_all_indices = [&rdm_indices, &all_indices,
                                             &rdm_label](const auto& idx,
                                                         const auto& tensor) {
          all_indices.insert(idx);
          if (tensor._label() == rdm_label) {
            rdm_indices.insert(idx);
          }
        };
        for_each_index_in_tn(product_ptr, retrieve_rdm_and_all_indices);

        // compute RDM->target replacement rules
        container::map<Index, Index> replacement_rules;
        ranges::for_each(rdm_indices, [&](const Index& idx) {
          const auto target_type =
              isr->intersection(idx.space(), target_rdm_space_type);
          if (target_type) {
            Index target = Index::make_tmp_index(target_type);
            replacement_rules.emplace(idx, target);
          }
        });

        if (false) {
          std::wcout << "expr = " << product_ptr->to_latex()
                     << "\n  external_indices = ";
          ranges::for_each(external_indices, [](auto& index) {
            std::wcout << index.label() << " ";
          });
          std::wcout << "\n  replrules = ";
          ranges::for_each(replacement_rules, [](auto& index) {
            std::wcout << to_latex(index.first) << "\\to"
                       << to_latex(index.second) << "\\,";
          });
          std::wcout.flush();
        }

        if (!replacement_rules.empty()) {
          sequant::detail::apply_index_replacement_rules(
              product_ptr, replacement_rules, external_indices, all_indices,
              isr);
        }
      };

      if (exptr.template is<Product>()) {
        auto product_ptr = exptr.template as_shared_ptr<Product>();
        impl_for_single_tn(product_ptr);
        exptr = product_ptr;
      } else {
        assert(exptr.template is<Sum>());
        auto result = std::make_shared<Sum>();
        for (auto& summand : exptr.template as<Sum>().summands()) {
          assert(summand.template is<Product>());
          auto result_summand = summand.template as<Product>().clone();
          auto product_ptr = result_summand.template as_shared_ptr<Product>();
          impl_for_single_tn(product_ptr);
          result->append(product_ptr);
        }
        exptr = result;
      }
    };
    project_rdm_indices_to_target(result);

    // rename dummy indices that might have been generated by
    // project_rdm_indices_to_target
    // + may combine terms

    // TensorCanonicalizer is given a custom comparer that moves active
    // indices to the front external-vs-internal trait still takes precedence
    auto current_index_comparer =
        TensorCanonicalizer::instance()->index_comparer();
    TensorCanonicalizer::instance()->index_comparer(
        [&](const Index& idx1, const Index& idx2) -> bool {
          auto active_space = isr->intersection(isr->particle_space(Spin::any),
                                                isr->hole_space(Spin::any));
          const auto idx1_active = idx1.space().type() == active_space.type();
          const auto idx2_active = idx2.space().type() == active_space.type();
          if (idx1_active) {
            if (idx2_active)
              return current_index_comparer(idx1, idx2);
            else
              return true;
          } else {
            if (idx2_active)
              return false;
            else
              return current_index_comparer(idx1, idx2);
          }
        });
    simplify(result);
    TensorCanonicalizer::instance()->index_comparer(
        std::move(current_index_comparer));

    if (Logger::instance().wick_stats) {
      std::wcout << "WickTheorem stats: # of contractions attempted = "
                 << wick.stats().num_attempted_contractions
                 << " # of useful contractions = "
                 << wick.stats().num_useful_contractions << std::endl;
    }
    return result;
  }
}

}  // namespace tensor
}  // namespace op

bool can_change_qns(const ExprPtr& op_or_op_product, const qns_t target_qns,
                    const qns_t source_qns = {}) {
  qns_t qns = source_qns;
  if (op_or_op_product.is<Product>()) {
    const auto& op_product = op_or_op_product.as<Product>();
    for (auto& op_ptr : ranges::views::reverse(op_product.factors())) {
      assert(op_ptr->template is<op_t>());
      const auto& op = op_ptr->template as<op_t>();
      qns = op(qns);
    }
    return qns.overlaps_with(target_qns);
  } else if (op_or_op_product.is<op_t>()) {
    const auto& op = op_or_op_product.as<op_t>();
    qns = op();
    return qns.overlaps_with(target_qns);
  } else
    throw std::invalid_argument(
        "sequant::mbpt::sr::contains_rank(op_or_op_product): op_or_op_product "
        "must be mbpt::sr::op_t or Product thereof");
}

}  // namespace sequant::mbpt