Adding a gate implementation

We discuss how one can add another gate implementation in this document. Assume that you want to add a custom PauliX gate implementation in Lightning Qubit. In this case, you may first create a file and add a class:

  // file: MyGateImplementation.hpp
struct MyGateImplementation {
  public:
    constexpr static std::array implemented_gates = {
        GateOperation::PauliX
    }; // List of implemented gates
    constexpr static kernel_id = KernelType::MyKernel; // Will be discussed below
    constexpr static std::string_view = "MyGateImpl"; // Name of your kernel

    /* This defines the required alignment for this kernel. If there is no special requirement,
       using std::alignment_of_v is sufficient. */
    template <typename PrecisionT>
    constexpr static std::size_t required_alignment = std::alignment_of_v<PrecisionT>;

    template <class PrecisionT>
    static void applyPauliX(std::complex<PrecisionT>* data,
                            std::size_t num_qubits,
                            const std::vector<std::size_t>& wires,
                            [[maybe_unused]] bool inverse) {
        /* Write your implementation */
        ...
    }
};

Then you can add your gate implementation to Lightning Qubit. This can be done by modifying two files:

// file: gates/KernelType.hpp
namespace Pennylane {
enum class KernelType { LM, MyKernel /* This is added */, None };

/* Rest of the file */

} // namespace Pennylane

and

// file: gates/AvailableKernels.hpp
namespace Pennylane {
    using AvailableKernels = Util::TypeList<GateImplementationsLM,
                                            MyGateImplementation /* This is added*/,
                                            void>;
} // namespace Pennylane

Now you can call your kernel functions in C++.

// sv is a statevector, i.e. an instance of StateVectorRaw or StateVectorManaged

// call statically
sv.applyPauliX_<MyKernel>(/*wires=*/{0}, /*inverse=*/false);

// call using the dynamic dispatcher
sv.applyOperation(KernelType::MyKernel, "PauliX", /*wires=*/{0}, /*inverse=*/false);

Still, note that your gate implementation is not a default implementation for PauliX gate yet, i.e.,

// simulator/KernelMap.cpp

int assignDefaultKernelsForGateOp() {
    auto &instance = OperationKernelMap<GateOperation>::getInstance();

    instance.assignKernelForOp(GateOperation::PauliX, all_threading,
                               all_memory_model, all_qubit_numbers,
                               Gates::KernelType::LM);

to

int assignDefaultKernelsForGateOp() {
    auto &instance = OperationKernelMap<GateOperation>::getInstance();

    instance.assignKernelForOp(GateOperation::PauliX, all_threading,
                               all_memory_model, all_qubit_numbers,
                               Gates::KernelType::MyKernel);

    ...
}

will make your implementation as default kernel for PauliX gate (for all C++ calls as well as for the Python binding).

Gate generators can also be handled in the same way. Note that it is possible to assign the kernel only for specific memory models or threading operations. Check overloaded functions Pennylane::KernelMap::OperationKernelMap::assignKernelForOp() for details.

Test your gate implementation

To test your own kernel implementations, you can go to tests/TestKernels.hpp and add your implementation.

using TestKernels = Pennylane::Util::TypeList<Pennylane::Gates::GateImplementationsLM,
                                              MyGateImplementation /*This is added */, void>;

It will automatically test your gate implementation. Note that, in the current implementation, this will test a gate if apply + gate name is defined even when the gate is not included in implemented_gates variable.