Program Listing for File ObservablesTNCuda.hpp

Return to documentation for file (pennylane_lightning/core/simulators/lightning_tensor/tncuda/observables/ObservablesTNCuda.hpp)

// Copyright 2024 Xanadu Quantum Technologies Inc. and contributors.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <tuple>
#include <unordered_set>
#include <vector>

#include "Constant.hpp"
#include "ConstantUtil.hpp" // lookup
#include "Error.hpp"
#include "Util.hpp"

#include "cuda_helpers.hpp"

namespace {
using namespace Pennylane::Util;
using namespace Pennylane::LightningGPU::Util;

template <class T> using vector1D = std::vector<T>;
template <class T> using vector2D = std::vector<vector1D<T>>;
template <class T> using vector3D = std::vector<vector2D<T>>;
} // namespace

namespace Pennylane::LightningTensor::TNCuda::Observables {
template <class TensorNetT> class ObservableTNCuda {
  public:
    using PrecisionT = typename TensorNetT::PrecisionT;
    using ComplexT = typename TensorNetT::ComplexT;
    using MetaDataT = std::tuple<std::string, std::vector<PrecisionT>,
                                 std::vector<ComplexT>>; // name, params, matrix

  protected:
    vector1D<PrecisionT> coeffs_;      // coefficients of each term
    vector1D<std::size_t> numTensors_; // number of tensors in each term
    vector2D<std::size_t>
        numStateModes_; // number of state modes of each tensor in each term
    vector3D<std::size_t>
        stateModes_;               // state modes of each tensor in each term
    vector2D<MetaDataT> metaData_; // meta data of each tensor in each term

  protected:
    ObservableTNCuda() = default;
    ObservableTNCuda(const ObservableTNCuda &) = default;
    ObservableTNCuda(ObservableTNCuda &&) noexcept = default;
    ObservableTNCuda &operator=(const ObservableTNCuda &) = default;
    ObservableTNCuda &operator=(ObservableTNCuda &&) noexcept = default;

  private:
    [[nodiscard]] virtual bool
    isEqual(const ObservableTNCuda<TensorNetT> &other) const = 0;

  public:
    virtual ~ObservableTNCuda() = default;

    [[nodiscard]] virtual auto getObsName() const -> std::string = 0;

    [[nodiscard]] virtual auto getWires() const -> std::vector<std::size_t> = 0;

    [[nodiscard]] auto getNumTensors() const -> const vector1D<std::size_t> & {
        return numTensors_;
    }

    [[nodiscard]] auto getNumStateModes() const
        -> const vector2D<std::size_t> & {
        return numStateModes_;
    }

    [[nodiscard]] auto getStateModes() const -> const vector3D<std::size_t> & {
        return stateModes_;
    }

    [[nodiscard]] auto getMetaData() const -> const vector2D<MetaDataT> & {
        return metaData_;
    }

    [[nodiscard]] auto getCoeffs() const -> const vector1D<PrecisionT> & {
        return coeffs_;
    };

    [[nodiscard]] auto
    operator==(const ObservableTNCuda<TensorNetT> &other) const -> bool {
        return typeid(*this) == typeid(other) && isEqual(other);
    }

    [[nodiscard]] auto
    operator!=(const ObservableTNCuda<TensorNetT> &other) const -> bool {
        return !(*this == other);
    }
};

template <class TensorNetT>
class NamedObsTNCuda : public ObservableTNCuda<TensorNetT> {
  public:
    using BaseType = ObservableTNCuda<TensorNetT>;
    using PrecisionT = typename TensorNetT::PrecisionT;
    using ComplexT = typename TensorNetT::ComplexT;

  private:
    std::string obs_name_;
    std::vector<std::size_t> wires_;
    std::vector<PrecisionT> params_;

    [[nodiscard]] auto isEqual(const ObservableTNCuda<TensorNetT> &other) const
        -> bool override {
        const auto &other_cast =
            static_cast<const NamedObsTNCuda<TensorNetT> &>(other);

        return (obs_name_ == other_cast.obs_name_) &&
               (wires_ == other_cast.wires_) && (params_ == other_cast.params_);
    }

  public:
    NamedObsTNCuda(std::string obs_name, std::vector<std::size_t> wires,
                   std::vector<PrecisionT> params = {})
        : obs_name_{obs_name}, wires_{wires}, params_{params} {
        BaseType::coeffs_.emplace_back(PrecisionT{1.0});
        BaseType::numTensors_.emplace_back(std::size_t{1});
        BaseType::numStateModes_.emplace_back(
            vector1D<std::size_t>{wires_.size()});
        BaseType::stateModes_.emplace_back(vector2D<std::size_t>{wires_});

        BaseType::metaData_.push_back(
            {std::make_tuple(obs_name, params_, std::vector<ComplexT>{})});
    }

    [[nodiscard]] auto getObsName() const -> std::string override {
        using Pennylane::Util::operator<<;
        std::ostringstream obs_stream;
        obs_stream << obs_name_ << wires_;
        return obs_stream.str();
    }

    [[nodiscard]] auto getWires() const -> std::vector<std::size_t> override {
        return wires_;
    }
};

template <class TensorNetT>
class HermitianObsTNCuda : public ObservableTNCuda<TensorNetT> {
  public:
    using BaseType = ObservableTNCuda<TensorNetT>;
    using PrecisionT = typename TensorNetT::PrecisionT;
    using ComplexT = typename TensorNetT::ComplexT;
    using MatrixT = std::vector<ComplexT>;

  private:
    inline static const MatrixHasher mh;
    MatrixT matrix_;
    std::vector<std::size_t> wires_;

    [[nodiscard]] auto isEqual(const ObservableTNCuda<TensorNetT> &other) const
        -> bool override {
        const auto &other_cast =
            static_cast<const HermitianObsTNCuda<TensorNetT> &>(other);

        return (matrix_ == other_cast.matrix_) && (wires_ == other_cast.wires_);
    }

  public:
    HermitianObsTNCuda(MatrixT matrix, std::vector<std::size_t> wires)
        : matrix_{std::move(matrix)}, wires_{std::move(wires)} {
        PL_ABORT_IF(wires_.size() != 1, "The number of Hermitian target wires "
                                        "must be 1 for Lightning-Tensor.");
        PL_ASSERT(matrix_.size() == Pennylane::Util::exp2(2 * wires_.size()));
        BaseType::coeffs_.emplace_back(PrecisionT{1.0});
        BaseType::numTensors_.emplace_back(std::size_t{1});
        BaseType::numStateModes_.emplace_back(
            vector1D<std::size_t>{wires_.size()});
        BaseType::stateModes_.emplace_back(vector2D<std::size_t>{wires_});

        BaseType::metaData_.push_back(
            {std::make_tuple("Hermitian", std::vector<PrecisionT>{}, matrix_)});
    }

    [[nodiscard]] auto getObsName() const -> std::string override {
        // To avoid collisions on cached GPU data, use matrix elements to
        // uniquely identify Hermitian
        // TODO: Replace with a performant hash function
        std::ostringstream obs_stream;
        obs_stream << "Hermitian" << mh(matrix_);
        return obs_stream.str();
    }

    [[nodiscard]] auto getWires() const -> std::vector<std::size_t> override {
        return wires_;
    }

    [[nodiscard]] auto getMatrix() const -> const MatrixT & { return matrix_; }
};

template <class TensorNetT>
class TensorProdObsTNCuda : public ObservableTNCuda<TensorNetT> {
  public:
    using BaseType = ObservableTNCuda<TensorNetT>;
    using PrecisionT = typename TensorNetT::PrecisionT;
    using MetaDataT = BaseType::MetaDataT;

  private:
    std::vector<std::shared_ptr<ObservableTNCuda<TensorNetT>>> obs_;
    std::vector<std::size_t> all_wires_;

    [[nodiscard]] auto isEqual(const ObservableTNCuda<TensorNetT> &other) const
        -> bool override {
        const auto &other_cast =
            static_cast<const TensorProdObsTNCuda<TensorNetT> &>(other);

        if (obs_.size() != other_cast.obs_.size()) {
            return false;
        }

        for (std::size_t i = 0; i < obs_.size(); i++) {
            if (*obs_[i] != *other_cast.obs_[i]) {
                return false;
            }
        }
        return true;
    }

  public:
    template <typename... Ts>
    explicit TensorProdObsTNCuda(Ts &&...arg) : obs_{std::forward<Ts>(arg)...} {
        if (obs_.size() == 1 &&
            obs_[0]->getObsName().find('@') != std::string::npos) {
            // This would prevent the misuse of this constructor for creating
            // TensorProdObs(TensorProdObs).
            PL_ABORT("A new TensorProdObs observable cannot be created "
                     "from a single TensorProdObs.");
        }

        for (const auto &ob : obs_) {
            PL_ABORT_IF(ob->getObsName().find("Hamiltonian") !=
                            std::string::npos,
                        "A TensorProdObs observable cannot be created from a "
                        "Hamiltonian.");
        }

        BaseType::coeffs_.push_back(PrecisionT{1.0});
        BaseType::numTensors_.push_back(obs_.size());

        vector1D<std::size_t> numStateModesLocal;
        vector2D<std::size_t> stateModesLocal;
        vector1D<MetaDataT> dataLocal;

        for (const auto &ob : obs_) {
            numStateModesLocal.insert(numStateModesLocal.end(),
                                      ob->getNumStateModes().front().begin(),
                                      ob->getNumStateModes().front().end());

            stateModesLocal.insert(stateModesLocal.end(),
                                   ob->getStateModes().front().begin(),
                                   ob->getStateModes().front().end());

            dataLocal.insert(dataLocal.end(), ob->getMetaData().front().begin(),
                             ob->getMetaData().front().end());
        }

        BaseType::numStateModes_.emplace_back(numStateModesLocal);
        BaseType::stateModes_.emplace_back(stateModesLocal);
        BaseType::metaData_.emplace_back(dataLocal);

        std::unordered_set<std::size_t> wires;
        for (const auto &ob : obs_) {
            const auto ob_wires = ob->getWires();
            for (const auto wire : ob_wires) {
                PL_ABORT_IF(wires.contains(wire),
                            "All wires in observables must be disjoint.");
                wires.insert(wire);
            }
        }
        all_wires_ = std::vector<std::size_t>(wires.begin(), wires.end());
        std::sort(all_wires_.begin(), all_wires_.end());
    }

    static auto
    create(std::initializer_list<std::shared_ptr<ObservableTNCuda<TensorNetT>>>
               obs) -> std::shared_ptr<TensorProdObsTNCuda<TensorNetT>> {
        return std::shared_ptr<TensorProdObsTNCuda<TensorNetT>>{
            new TensorProdObsTNCuda(std::move(obs))};
    }

    static auto
    create(std::vector<std::shared_ptr<ObservableTNCuda<TensorNetT>>> obs)
        -> std::shared_ptr<TensorProdObsTNCuda<TensorNetT>> {
        return std::shared_ptr<TensorProdObsTNCuda<TensorNetT>>{
            new TensorProdObsTNCuda(std::move(obs))};
    }

    [[nodiscard]] auto getSize() const -> std::size_t { return obs_.size(); }

    [[nodiscard]] auto getWires() const -> std::vector<std::size_t> override {
        return all_wires_;
    }

    [[nodiscard]] auto getObsName() const -> std::string override {
        using Pennylane::Util::operator<<;
        std::ostringstream obs_stream;
        const auto obs_size = obs_.size();
        for (std::size_t idx = 0; idx < obs_size; idx++) {
            obs_stream << obs_[idx]->getObsName();
            if (idx != obs_size - 1) {
                obs_stream << " @ ";
            }
        }
        return obs_stream.str();
    }

    [[nodiscard]] auto getObs() const
        -> std::vector<std::shared_ptr<ObservableTNCuda<TensorNetT>>> {
        return obs_;
    };
};

template <class TensorNetT>
class HamiltonianTNCuda : public ObservableTNCuda<TensorNetT> {
  public:
    using BaseType = ObservableTNCuda<TensorNetT>;
    using PrecisionT = typename TensorNetT::PrecisionT;

  private:
    std::vector<PrecisionT> coeffs_ham_;
    std::vector<std::shared_ptr<ObservableTNCuda<TensorNetT>>> obs_;

    [[nodiscard]] bool
    isEqual(const ObservableTNCuda<TensorNetT> &other) const override {
        const auto &other_cast =
            static_cast<const HamiltonianTNCuda<TensorNetT> &>(other);

        if (coeffs_ham_ != other_cast.coeffs_ham_) {
            return false;
        }

        for (std::size_t i = 0; i < obs_.size(); i++) {
            if (*obs_[i] != *other_cast.obs_[i]) {
                return false;
            }
        }
        return true;
    }

  public:
    template <typename T1, typename T2>
    HamiltonianTNCuda(T1 &&coeffs, T2 &&obs)
        : coeffs_ham_{std::forward<T1>(coeffs)}, obs_{std::forward<T2>(obs)} {
        BaseType::coeffs_ = coeffs_ham_;
        PL_ASSERT(BaseType::coeffs_.size() == obs_.size());

        for (std::size_t term_idx = 0; term_idx < BaseType::coeffs_.size();
             term_idx++) {
            auto ob = obs_[term_idx];
            // This is aligned with statevector backends
            PL_ABORT_IF(ob->getObsName().find("Hamiltonian") !=
                            std::string::npos,
                        "A Hamiltonian observable cannot be created from "
                        "another Hamiltonian.");
            BaseType::numTensors_.emplace_back(ob->getNumTensors().front());
            BaseType::numStateModes_.emplace_back(
                ob->getNumStateModes().front());
            BaseType::stateModes_.emplace_back(ob->getStateModes().front());
            BaseType::metaData_.emplace_back(ob->getMetaData().front());
        }
    }

    static auto
    create(std::initializer_list<PrecisionT> coeffs,
           std::initializer_list<std::shared_ptr<ObservableTNCuda<TensorNetT>>>
               obs) -> std::shared_ptr<HamiltonianTNCuda<TensorNetT>> {
        return std::shared_ptr<HamiltonianTNCuda<TensorNetT>>(
            new HamiltonianTNCuda<TensorNetT>{std::move(coeffs),
                                              std::move(obs)});
    }

    [[nodiscard]] auto getWires() const -> std::vector<std::size_t> override {
        std::unordered_set<std::size_t> wires;

        for (const auto &ob : obs_) {
            const auto ob_wires = ob->getWires();
            wires.insert(ob_wires.begin(), ob_wires.end());
        }
        auto all_wires = std::vector<std::size_t>(wires.begin(), wires.end());
        std::sort(all_wires.begin(), all_wires.end());
        return all_wires;
    }

    [[nodiscard]] auto getObsName() const -> std::string override {
        using Pennylane::Util::operator<<;
        std::ostringstream ss;
        ss << "Hamiltonian: { 'coeffs' : " << BaseType::coeffs_
           << ", 'observables' : [";
        const auto term_size = BaseType::coeffs_.size();
        for (std::size_t t = 0; t < term_size; t++) {
            ss << obs_[t]->getObsName();
            if (t != term_size - 1) {
                ss << ", ";
            }
        }
        ss << "]}";
        return ss.str();
    }
    [[nodiscard]] auto getObs() const
        -> std::vector<std::shared_ptr<ObservableTNCuda<TensorNetT>>> {
        return obs_;
    };

    [[nodiscard]] auto getCoeffs() const -> std::vector<PrecisionT> {
        return BaseType::getCoeffs();
    };
};
} // namespace Pennylane::LightningTensor::TNCuda::Observables