How to use Qiskit 1.0 with PennyLane¶
Published: July 2, 2024. Last updated: January 8, 2025.
Note
Go to the end to download the full example code.
The PennyLane-Qiskit plugin enables you to integrate your existing Qiskit code and run circuits on IBM devices with PennyLane, encompassing two real-world scenarios: (1) working in PennyLane from the start and executing your work on an IBM device and (2) converting your existing Qiskit code to PennyLane and executing that on any device, including IBM devices, Amazon Braket — you name it!
With the first stable release of Qiskit in February 2024, we subsequently shipped some excellent features and upgrades with the PennyLane-Qiskit plugin, allowing anyone familiar with Qiskit to jump into the PennyLane ecosystem and land on both feet. In this demo, we want to show you how easy these features are to use, letting you make that aforementioned jump like it’s nothing 😌.
Note
To follow along, we recommend installing the PennyLane-Qiskit plugin in a separate virtual environment.
pip install -U pennylane-qiskit
This will install PennyLane, the plugin, and the latest Qiskit 1.0 version that the plugin supports. If you use an environment that contains a pre-1.0 version of Qiskit, upgrading to Qiskit 1.0 could cause issues. In that case, we recommend following Qiskit’s upgrade instructions.
Coding in PennyLane, executing on IBM Quantum devices¶
If you want to distill how a PennyLane plugin works down to one thing, it’s all in the provided devices! In
PennyLane, you just create your circuit (a quantum function) and decorate it with
the QNode decorator @qml.qnode(dev)
, where dev
is (one of) the plugin’s device(s).
In PennyLane and its plugins,
devices are called upon by their short name, and can be loaded via the device()
function:
qml.device("shortname", *device_options)
If you’ve
seen PennyLane code before, you’ve probably seen "default.qubit"
or "lightning.qubit"
as
short names for our Python and C++ statevector simulators, respectively.
In the PennyLane-Qiskit plugin, there are many IBM devices you can use, but there are two heavy hitters for Qiskit 1.0:
-
"qiskit.basicsim"
: uses the QiskitBasicSimulator
backend from thebasic_provider
module in Qiskit 1.0. -
"qiskit.remote"
: lets you run PennyLane code on any Qiskit device, where you can choose between different backends — either simulators tailor-made to emulate the real hardware, or the real hardware itself.
If you want to use any of these devices in PennyLane, simply put those short names into
qml.device
and any quantum function decorated with @qml.qnode(dev)
will execute on the
corresponding device.
To show how easy this is, let’s say we have this simple circuit in Qiskit 1.0.
import qiskit
from qiskit import QuantumCircuit
from qiskit.providers.basic_provider import BasicSimulator
qc = QuantumCircuit(2, 1)
qc.h(0)
qc.cx(0, 1)
qc.measure(1, 0)
backend = BasicSimulator()
counts = backend.run(qc).result().get_counts()
print(counts)
{'0': 523, '1': 501}
In PennyLane, we can execute the exact same circuit on the exact same device and backend like so:
import pennylane as qml
dev = qml.device("qiskit.basicsim", wires=2, shots=1024)
@qml.qnode(dev)
def circuit():
qml.Hadamard(0)
qml.CNOT([0, 1])
return qml.counts(wires=1)
print(circuit())
{'0': tensor(474, requires_grad=True), '1': tensor(550, requires_grad=True)}
Magic! With one line of code, you can work inside PennyLane and ship the execution off to your favourite IBM device. It’s exactly like using Qiskit 1.0, but you interact with PennyLane instead.
Converting Qiskit 1.0 code to PennyLane¶
This is probably what a lot of the audience is wondering: “Can I combine my existing work in Qiskit with PennyLane?” YES. And don’t worry, you don’t need to import a ton of things or use a bunch of functions — you only need to know two functions:
-
from_qiskit()
: converts an entire QiskitQuantumCircuit
— the whole thing — into a PennyLane quantum function. It will faithfully convert Qiskit-side measurements (even mid-circuit measurements), or you can append Pennylane-side measurements directly to it. -
from_qiskit_op()
: converts aSparsePauliOp
in Qiskit 1.0 to the equivalent operator in PennyLane.
Both of these functions give you all the functionality you need to access PennyLane’s features and user interface starting from the side of Qiskit 1.0. Let’s look at an example where both of these functions are used.
Let’s say you’ve created the following Qiskit code that prepares a modified GHZ state for an
arbitrary amount of qubits and measures several expectation values of SparsePauliOp
operators.
from qiskit.quantum_info import SparsePauliOp
n = 5
def qiskit_GHZ_circuit(n):
qc = QuantumCircuit(n)
qc.h(0)
for i in range(n - 1):
qc.cx(i, i + 1)
qc.rx(0.1967, i)
return qc
qc = qiskit_GHZ_circuit(n)
operator_strings = ["I" * i + "ZZ" + "I" * (n - 2 - i) for i in range(n - 1)]
operators = [SparsePauliOp(operator_string) for operator_string in operator_strings]
print(operators)
[SparsePauliOp(['ZZIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IZZII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIZZI'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIZZ'],
coeffs=[1.+0.j])]
With the circuit and operators defined, we can create a StatevectorEstimator primitive in Qiskit to execute the circuit and calculate expectation values.
from qiskit.primitives import StatevectorEstimator
estimator = StatevectorEstimator()
job_estimator = estimator.run([(qc, operators)])
result_estimator = job_estimator.result()[0].data.evs
print(result_estimator)
[0.98071685 0.96180554 0.96180554 0.96180554]
To convert this work into PennyLane, let’s start with the Qiskit-side SparsePauliOp
operators
and converting them to PennyLane objects with qml.from_qiskit_op
.
pl_operators = [qml.from_qiskit_op(qiskit_op) for qiskit_op in operators]
print(pl_operators)
[Z(3) @ Z(4), Z(2) @ Z(3), Z(1) @ Z(2), Z(0) @ Z(1)]
Note
PennyLane wires are enumerated from left to right, while the Qiskit convention is to
enumerate from right to left. This means a SparsePauliOp
term defined by the string “XYZ” in Qiskit
applies Z
on wire 0
, Y
on wire 1
, and X
on wire 2
. For more details, see
the String representation
section of the Qiskit documentation for the Pauli
class.
Next, we show how to convert the Qiskit QuantumCircuit
, qc
, to PennyLane with qml.from_qiskit
. We
can append the measurements — expectation values (expval()
) of pl_operators
— with the
measurements
keyword argument, which accepts a list of PennyLane measurements.
measurements = [qml.expval(op) for op in pl_operators] # expectation values
qc = qiskit_GHZ_circuit(n)
pl_qfunc = qml.from_qiskit(qc, measurements=measurements)
The last thing to do is make pl_func
a QNode. We can’t decorate pl_qfunc
with
@qml.qnode
, but we can equivalently wrap it with qml.QNode
and supply the device.
pl_circuit = qml.QNode(pl_qfunc, device=qml.device("lightning.qubit", wires=n))
print(pl_circuit())
[0.9807168489852623, 0.961805537883582, 0.961805537883582, 0.961805537883582]
What’s really useful about being able to append measurements to the end of a circuit with
from_qiskit()
is being able to measure something that isn’t available in Qiskit 1.0 but is
available in PennyLane, like the classical shadow measurement protocol, for example. In PennyLane,
you can measure this with classical_shadow()
.
measurements = [qml.classical_shadow(wires=range(n))]
pl_qfunc = qml.from_qiskit(qc, measurements=measurements)
pl_circuit = qml.QNode(pl_qfunc, device=qml.device("default.qubit", wires=n))
print(pl_circuit(shots=5))
[array([[[0, 0, 0, 0, 1],
[1, 1, 1, 0, 1],
[0, 1, 0, 0, 0],
[1, 1, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 2, 2, 1],
[2, 0, 2, 0, 1],
[1, 0, 2, 0, 2],
[1, 0, 1, 1, 1],
[1, 0, 2, 0, 2]]], dtype=int8)]
And that’s it! Now you have a copy of your work in PennyLane, where you can access fully-differentiable and hardware-agnostic quantum programming — the entire quantum programming ecosystem is at your disposal 💪.
A real-world example¶
One of the things you’ll almost certainly encounter in the wild is a cost function to optimize with tunable parameters belonging to a circuit — it’s common 😉! PennyLane is a great option for these problems because of its end-to-end differentiability and long list of optimization methods you can leverage. So, maybe you have a home-cooked variational circuit written in Qiskit and you want to access PennyLane’s seamless differentiability — yep, the PennyLane-Qiskit plugin has you covered here, too 🙌.
To keep things simple, let’s use the following Qiskit circuit as our variational circuit.
from qiskit.circuit import ParameterVector, Parameter
from matplotlib import pyplot as plt
n = 3
angles1 = ParameterVector("phis", n - 1)
angle2 = Parameter("theta")
qc = QuantumCircuit(3)
qc.rx(angles1[0], [0])
qc.ry(angles1[1], [1])
qc.ry(angle2, [2])
qc.draw("mpl")
plt.show()

This circuit contains two sets of differentiable parameters: phis
(length 2) and theta
(scalar).
If we give this Qiskit circuit to qml.from_qiskit
, we get a quantum function that can
subsequently be called within a circuit — it’s as if the gates and operations contained within it
get transferred over to our new QNode.
import pennylane.numpy as np
pl_qfunc = qml.from_qiskit(qc)
dev = qml.device("lightning.qubit", wires=n)
@qml.qnode(dev)
def differentiable_circuit(phis, theta):
pl_qfunc(phis, theta)
return [qml.expval(qml.Z(i)) for i in range(n)]
phis = np.array([0.6, 0.7])
theta = np.array([0.19])
print(differentiable_circuit(phis, theta))
qml.draw_mpl(differentiable_circuit, style="pennylane")(phis, theta)
plt.show()
[0.8253356149096783, 0.7648421872844883, 0.9820042351172701]

You’ll notice, too, that pl_func
has the call signature that you would expect:
pl_qfunc(phis, theta)
, just like the name we gave them when we defined them in Qiskit
(ParameterVector("phis", n-1)
and Parameter("theta")
).
With the circuit being immediately differentiable to PennyLane, let’s define a dummy cost function
that will sum the output of differentiable_circuit
.
def cost(phis, theta):
return np.sum(differentiable_circuit(phis, theta))
Now we’re in business and can optimize! You can use any suitable optimizer in PennyLane that you
like, and by calling its step_and_cost
method you can access the updated parameters and cost
function value after each optimization step.
opt = qml.AdamOptimizer(0.1)
for i in range(100):
(phis, theta), new_loss = opt.step_and_cost(cost, phis, theta)
print(f"Optimized parameters: phis = {phis}, theta = {theta}")
print(f"Optimized cost function value: {new_loss}")
Optimized parameters: phis = [3.12829384 3.12823583], theta = [3.1310224]
Optimized cost function val: -2.999796472821245
As we expect, the minimum value that our cost function can take is $-3$ when all angles of rotation are $\pi$ (all qubits are rotated into the $\vert 1 \rangle$ state). Of course, this is just a toy example of an easy optimization problem. But, you can apply this process to a larger, more complicated circuit ansatze without worries.
Further resources¶
There’s so much more to learn about what’s possible in PennyLane, and if you’re coming from Qiskit you’re in good hands! The PennyLany-Qiskit plugin is your personal chaperone to the PennyLane ecosystem. You can dive deeper into what’s possible with the PennyLane-Qiskit plugin by visiting the plugin homepage and, in the mean time, if you have any questions about the plugin, PennyLane, or even Qiskit, drop a question on our Discussion Forum and we’ll promptly respond.
Now that you’ve used PennyLane, every road in the wonderful world of quantum programming SDKs is open with no set speed limits 🏎️. Explore our website to see the latest and greatest PennyLane features, Demos and our blog posts, and follow us on LinkedIn or X (formerly Twitter) to stay updated!
Isaac De Vlugt
My job is to help manage the PennyLane and Catalyst feature roadmap... and spam lots of emojis in the chat 🤠
Share demo