Greetings there,
Hope all are well. So, I am creating a library where I wrap existing circuit frameworks. The UI I use is very similar to Qiskit:
circuit = Circuit(2)
circuit.H(0)
circuit.CX(0, 1)
So, here a Circuit
instance is first created, and all gates are added to this circuit instance. Now, the natural difficulty I have with wrapping Pennylane is that Pennylane represents circuit instances using decorated functions, like so
@qml.qnode(dev)
def circuit():
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliZ(wires=[0, 1]))
So, a few issues from my standpoint:
- This UI requires knowing all operations in advance to create the circuit, and if one attempts to store the ops, and then “update” the circuit instance, they’d have to recreate the circuit from scratch every time using a new decorated function with
qml.apply(op)
. - This UI does not allow for creating just the circuit. It also requires some return type as specified in the documentation for QNodes.
This is my current understanding. I could be wrong, so please correct me if so.
So, I want a way to have a UI similar to Qiskit (or what I described above, which is also how Cirq and TKET do the circuit creation). Please let me know if any part of what I explained is unclear/confusing and I will do my best to remedy it.
Here’s my current attempt (which works, but admittedly I hate it given it need to recreate the circuit every time for use).
""" Wrapper class for using Xanadu's PennyLane in Qickit SDK.
"""
from __future__ import annotations
__all__ = ["PennylaneCircuit"]
from collections.abc import Sequence
import numpy as np
from numpy.typing import NDArray
from typing import Callable, TYPE_CHECKING
import pennylane as qml # type: ignore
if TYPE_CHECKING:
from qickit.backend import Backend
from qickit.circuit import Circuit
from qickit.circuit.circuit import GATES
class PennylaneCircuit(Circuit):
""" `qickit.circuit.PennylaneCircuit` is the wrapper for using Xanadu's PennyLane in Qickit SDK.
Notes
-----
Xanadu's PennyLane is a cross-platform Python library for quantum computing,
quantum machine learning, and quantum chemistry.
For more information on PennyLane:
- Documentation:
https://docs.pennylane.ai/en/stable/code/qml.html
- Source Code:
https://github.com/PennyLaneAI/pennylane
- Publication:
https://arxiv.org/pdf/1811.04968
Parameters
----------
`num_qubits` : int
Number of qubits in the circuit.
Attributes
----------
`num_qubits` : int
Number of qubits in the circuit.
`circuit` : list[qml.Operation]
The circuit.
`gate_mapping` : dict[str, Callable]
The mapping of the gates in the input quantum computing
framework to the gates in qickit.
`device` : qml.Device
The PennyLane device to use.
`measured_qubits` : set[int]
The set of measured qubits indices.
`circuit_log` : list[dict]
The circuit log.
`global_phase` : float
The global phase of the circuit.
`process_gate_params_flag` : bool
The flag to process the gate parameters.
Raises
------
TypeError
- Number of qubits bits must be integers.
ValueError
- Number of qubits bits must be greater than 0.
Usage
-----
>>> circuit = PennylaneCircuit(num_qubits=2)
"""
def __init__(
self,
num_qubits: int
) -> None:
super().__init__(num_qubits=num_qubits)
self.device = qml.device("default.qubit", wires=self.num_qubits)
self.circuit: list[qml.Operation] = []
@staticmethod
def _define_gate_mapping() -> dict[str, Callable]:
# Define lambda factory for non-parameterized gates
def const(x):
return lambda _angles: x
gate_mapping = {
"I": const(qml.Identity(0).matrix()),
"X": const(qml.PauliX(0).matrix()),
"Y": const(qml.PauliY(0).matrix()),
"Z": const(qml.PauliZ(0).matrix()),
"H": const(qml.Hadamard(wires=0).matrix()),
"S": const(qml.S(wires=0).matrix()),
"Sdg": const(qml.adjoint(qml.S(0)).matrix()), # type: ignore
"T": const(qml.T(wires=0).matrix()),
"Tdg": const(qml.adjoint(qml.T(0)).matrix()), # type: ignore
"RX": lambda angles: qml.RX(phi=angles[0], wires=0).matrix(), # type: ignore
"RY": lambda angles: qml.RY(phi=angles[0], wires=0).matrix(), # type: ignore
"RZ": lambda angles: qml.RZ(phi=angles[0], wires=0).matrix(), # type: ignore
"Phase": lambda angles: qml.PhaseShift(phi=angles[0], wires=0).matrix(), # type: ignore
"U3": lambda angles: qml.U3(theta=angles[0], phi=angles[1], delta=angles[2], wires=0).matrix() # type: ignore
}
return gate_mapping
def _gate_mapping(
self,
gate: GATES,
target_indices: int | Sequence[int],
control_indices: int | Sequence[int] = [],
angles: Sequence[float] = [0, 0, 0]
) -> None:
target_indices = [target_indices] if isinstance(target_indices, int) else target_indices
control_indices = [control_indices] if isinstance(control_indices, int) else control_indices
# Lazily extract the value of the gate from the mapping to avoid
# creating all the gates at once, and to maintain the abstraction
# Apply the gate operation to the specified qubits
if "MC" in gate:
for target_index in target_indices:
self.circuit.append(
qml.ControlledQubitUnitary(
self.gate_mapping[gate.removeprefix("MC")](angles),
control_wires=control_indices,
wires=target_index
)
)
return
for target_index in target_indices:
self.circuit.append(
qml.QubitUnitary(self.gate_mapping[gate](angles), wires=target_index)
)
def GlobalPhase(
self,
angle: float
) -> None:
self.process_gate_params(gate=self.GlobalPhase.__name__, params=locals())
# Create a Global Phase gate
global_phase = qml.GlobalPhase
self.circuit.append(global_phase(-angle))
self.global_phase += angle
def measure(
self,
qubit_indices: int | Sequence[int]
) -> None:
self.process_gate_params(gate=self.measure.__name__, params=locals())
# In PennyLane, we apply measurements in '.get_statevector', and '.get_counts'
# methods
# This is due to the need for PennyLane quantum functions to return measurement results
# Therefore, we do not need to do anything here
if isinstance(qubit_indices, int):
qubit_indices = [qubit_indices]
# Set the measurement as applied
for qubit_index in qubit_indices:
self.measured_qubits.add(qubit_index)
self.circuit.append((qml.measure(qubit_index), False)) # type: ignore
def get_statevector(
self,
backend: Backend | None = None,
) -> NDArray[np.complex128]:
# Copy the circuit as the operations are applied inplace
circuit: PennylaneCircuit = self.copy() # type: ignore
# PennyLane uses MSB convention for qubits, so we need to reverse the qubit indices
circuit.vertical_reverse()
def compile_circuit() -> qml.StateMP:
""" Compile the circuit.
Parameters
----------
circuit : Collection[qml.Op]
The list of operations representing the circuit.
Returns
-------
qml.StateMP
The state vector of the circuit.
"""
# Apply the operations in the circuit
for op in circuit.circuit:
if isinstance(op, tuple):
qml.measure(op[0].wires[0], reset=op[1]) # type: ignore
continue
qml.apply(op)
return qml.state()
if backend is None:
state_vector = qml.QNode(compile_circuit, circuit.device)()
else:
state_vector = backend.get_statevector(circuit)
return np.array(state_vector)
def get_counts(
self,
num_shots: int,
backend: Backend | None = None
) -> dict[str, int]:
np.random.seed(0)
if len(self.measured_qubits) == 0:
raise ValueError("At least one qubit must be measured.")
# Copy the circuit as the operations are applied inplace
circuit: PennylaneCircuit = self.copy() # type: ignore
# PennyLane uses MSB convention for qubits, so we need to reverse the qubit indices
circuit.vertical_reverse()
def compile_circuit() -> qml.CountsMp:
""" Compile the circuit.
Parameters
----------
circuit : Collection[qml.Op]
The list of operations representing the circuit.
Returns
-------
Collection[qml.ProbabilityMP]
The list of probability measurements.
"""
# Apply the operations in the circuit
for op in circuit.circuit:
if isinstance(op, tuple):
qml.measure(op[0].wires[0], reset=op[1]) # type: ignore
continue
qml.apply(op)
return qml.counts(wires=circuit.measured_qubits, all_outcomes=True)
if backend is None:
device = qml.device(circuit.device.name, wires=circuit.num_qubits, shots=num_shots)
result = qml.QNode(compile_circuit, device)()
counts = {list(result.keys())[i]: int(list(result.values())[i]) for i in range(len(result))}
else:
result = backend.get_counts(self, num_shots=num_shots)
return counts
def get_depth(self) -> int:
circuit = self.convert(QiskitCircuit)
return circuit.get_depth()
def get_unitary(self) -> NDArray[np.complex128]:
# Copy the circuit as the operations are applied inplace
circuit: PennylaneCircuit = self.copy() # type: ignore
# PennyLane uses MSB convention for qubits, so we need to reverse the qubit indices
circuit.vertical_reverse()
def compile_circuit() -> None:
""" Compile the circuit.
Parameters
----------
`circuit` : Collection[qml.Op]
The list of operations representing the circuit.
"""
if circuit.circuit == [] or (
isinstance(circuit.circuit[0], qml.GlobalPhase) and len(circuit.circuit) == 1
):
for i in range(circuit.num_qubits):
circuit.circuit.append(qml.Identity(wires=i))
# Apply the operations in the circuit
for op in circuit.circuit:
if isinstance(op, tuple):
qml.measure(op[0].wires[0], reset=op[1]) # type: ignore
continue
qml.apply(op)
# Run the circuit and define the unitary matrix
unitary = np.array(qml.matrix(compile_circuit, wire_order=range(self.num_qubits))(), dtype=complex) # type: ignore
return unitary
def reset_qubit(
self,
qubit_indices: int | Sequence[int]
) -> None:
self.process_gate_params(gate=self.reset_qubit.__name__, params=locals())
if isinstance(qubit_indices, int):
qubit_indices = [qubit_indices]
for qubit_index in qubit_indices:
self.circuit.append((qml.measure(qubit_index), True)) # type: ignore