qiskit-terra: 0.19.1
qiskit-aer: 0.10.1
qiskit-ignis: 0.7.0
qiskit-ibmq-provider: 0.18.3
qiskit-aqua: None
qiskit: 0.34.0
qiskit-nature: None
qiskit-finance: None
qiskit-optimization: None
qiskit-machine-learning: None
Quantum Computing has been talked about for some time and it may well become the next iteration of computer programming. To keep my skills up to date I should try out this approach to computation. I want to try out the Qiskit and QuTiP frameworks, both of which use python code.
This post is an evaluation of the Qiskit framework (ANIS et al. 2021).
Introduction
Qiskit is the framework that was introduced to me by a colleague last year. They tried to use this to solve a simple problem and described it as “not even at the level of assembly programming”. I have low expectations for the usability of this framework.
I’ve installed it with poetry add qiskit[visualization]
and the framework version is:
Reviewing the repository quickly this looks like an active project - the last commit was hours ago and it has a lot of stars.
Logic Gates
Since this is below the level of assembly, the first examples I can find with code are for logic gates. Let’s run them here and see how it works.
The sections following here have quite a lot of code and show the circuit diagram and results for each discrete input, making it quite long.
NOT Gate
This is the fully defined example from the page.
Code
from qiskit import QuantumCircuit, Aer, execute
def NOT(inp):
"""An NOT gate.
Parameters:
inp (str): Input, encoded in qubit 0.
Returns:
QuantumCircuit: Output NOT circuit.
str: Output value measured from qubit 0.
"""
= QuantumCircuit(1, 1) # A quantum circuit with a single qubit and a single classical bit
qc 0)
qc.reset(
# We encode '0' as the qubit state |0⟩, and '1' as |1⟩
# Since the qubit is initially |0⟩, we don't need to do anything for an input of '0'
# For an input of '1', we do an x to rotate the |0⟩ to |1⟩
if inp=='1':
0)
qc.x(
# barrier between input state and gate operation
qc.barrier()
# Now we've encoded the input, we can do a NOT on it using x
0)
qc.x(
#barrier between gate operation and measurement
qc.barrier()
# Finally, we extract the |0⟩/|1⟩ output of the qubit and encode it in the bit c[0]
0,0)
qc.measure('mpl')
qc.draw(
# We'll run the program on a simulator
= Aer.get_backend('qasm_simulator')
backend # Since the output will be deterministic, we can use just a single shot to get it
= execute(qc, backend, shots=1, memory=True)
job = job.result().get_memory()[0]
output
return qc, output
We can see the quantum circuit as well as the results:
Code
for inp in ['0', '1']:
= NOT(inp)
qc, out print('NOT with input',inp,'gives output',out)
="mpl")) display(qc.draw(output
NOT with input 0 gives output 1
NOT with input 1 gives output 0
This is very pretty. I’m not really sure why the x
operation on the qubit does a rotation. Is there an associated y
and z
(yes, there is)?
I also dislike the way that everything is string encoded. The response is string encoded too.
To extend this I want to try performing a NOT operation over all three dimensions.
Code
from qiskit import QuantumCircuit, Aer, execute
def NOT_all(x_in: bool, y_in: bool, z_in: bool):
= QuantumCircuit(1, 1)
qc 0)
qc.reset(
if x_in:
0)
qc.x(if y_in:
0)
qc.y(if z_in:
0)
qc.z(
qc.barrier()0)
qc.x(0)
qc.y(0)
qc.z(
qc.barrier()
0,0)
qc.measure('mpl')
qc.draw(
= Aer.get_backend('qasm_simulator')
backend
# Since the output will be deterministic, we can use just a single shot to get it
= execute(qc, backend, shots=1, memory=True)
job = job.result().get_memory()[0]
output
return qc, output, job
Code
= (True, True, False)
inp = NOT_all(*inp)
qc, out, job print('NOT with input',inp,'gives output',out)
="mpl")) display(qc.draw(output
NOT with input (True, True, False) gives output 0
This is annoying, it isn’t returning the data in the y or z dimensions. It turns out that the qiskit framework can only ever measure the x base, so changes in y and z are invisible to measurement.
It would be nice to move away from string encoding the output - I know that numpy is used in some part of this. It looks like the output is determined based on the meas_level
(measurement level?) or the memory
flag. Since the memory
flag is set this is the greatest level of precision for measurement which then results in string output. Presumably the use of meas_level
then returns more noisy output from the underlying circuits.
Removing Boilerplate
I want to practice using this quite a bit. Currently there is quite a bit of boilerplate around setting up the circuit and running it. If I can reduce my methods to just the core operations it will be easier to iterate.
This would be an example of the template method pattern, however using OOP to define this seems like overkill. I can use a decorator to define the repeated behaviour and leave the decorated function as the concrete operation to perform.
Code
from typing import List, Tuple
from qiskit import QuantumCircuit
from qiskit.providers.aer.jobs.aerjob import AerJob
= Tuple[QuantumCircuit, List[bool], AerJob]
QuantumResult
def setup_circuit(qubits: int, cbits: int, state: List[bool]) -> QuantumCircuit:
= QuantumCircuit(qubits, cbits)
qc range(qubits))
qc.reset(
for index, is_set in enumerate(state):
if is_set:
qc.x(index)
qc.barrier()
return qc
def run_circuit(qc: QuantumCircuit) -> QuantumResult:
# execute is a super generic name, import it here to avoid conflicts
from qiskit import Aer, execute
= Aer.get_backend('qasm_simulator')
backend
# Since the output will be deterministic, we can use just a single shot to get it
= execute(qc, backend, shots=1, memory=True)
job = [
output == "1"
cbit for cbit in job.result().get_memory()
]
return qc, output, job
With these two methods I can define a decorator around any method to make it execute on a quantum circuit.
Code
from typing import Callable, List, Tuple
from qiskit import QuantumCircuit
from qiskit.providers.aer.jobs.aerjob import AerJob
# the result of the decorator application, pass in the initial state and get out the result
= Tuple[QuantumCircuit, List[bool], AerJob]
QuantumResult = Callable[[bool, ...], QuantumResult]
StateCallable
# the underlying method signature that is invoked by the decorator
= Callable[[QuantumCircuit], QuantumCircuit]
QuantumCallable
# This decorator requires configuration so it is double layered
def with_circuit(qubits: int, cbits: int) -> Callable[QuantumCallable, StateCallable]:
def decorator(function: QuantumCallable) -> StateCallable:
def wrapper(*state: bool) -> QuantumResult:
= setup_circuit(qubits=qubits, cbits=cbits, state=state)
qc = function(qc)
qc return run_circuit(qc)
return wrapper
return decorator
Now we can try it out for the NOT gate. The following code is identical to the original NOT gate defined above:
Code
from qiskit import QuantumCircuit
@with_circuit(qubits=1, cbits=1)
def NOT_decorated(qc: QuantumCircuit):
0)
qc.x(
qc.barrier()0,0)
qc.measure(
return qc
Code
for inp in [True, False]:
= NOT_decorated(inp)
qc, out, job print('NOT with input',inp,'gives output',out[0])
="mpl")) display(qc.draw(output
NOT with input True gives output False
NOT with input False gives output True
This is far more compact!
XOR Gate
The next gate to define is the XOR gate. This time they have left the implementation blank.
The quantum circuit does define AND, OR, XOR … methods directly however I think it would be better to try implementing this myself. When looking at code that they have written for various things I see things like cx
and such which take multiple qubits. Looking at that method it uses a CXGate which the documentation describes as like the classical XOR gate.
Let’s run through this. Given the documentation:
In the computational basis, this gate flips the target qubit if the control qubit is in the \(\left| 1 \right\rangle\) state
We can define the truth table given these instructions:
target qubit | control qubit | result |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 0 |
And that looks like XOR to me!
Code
from qiskit import QuantumCircuit
@with_circuit(qubits=2, cbits=1)
def XOR(qc: QuantumCircuit):
=0, target_qubit=1)
qc.cx(control_qubit
qc.barrier()1,0)
qc.measure(
return qc
Code
for x_in in [True, False]:
for y_in in [True, False]:
= XOR(x_in, y_in)
qc, out, job print(f"XOR with input {x_in}, {y_in} gives output {out[0]}")
="mpl")) display(qc.draw(output
XOR with input True, True gives output False
XOR with input True, False gives output True
XOR with input False, True gives output True
XOR with input False, False gives output False
AND Gate
The documentation for the CCXGate is basically missing, however I am inferring that it is a CXGate that has two control qubits. If both control qubits are set then it should flip the target qubit. That means that if I ensure that the target qubit starts out false I can get it to flip to true if both control qubits are set.
When writing this using my decorator there is a problem - the output qubit is not one of the two inputs. I use the partial function from functools to provide that starting qubit value. This is really a problem with the decorator.
Code
from functools import partial
from qiskit import QuantumCircuit
@with_circuit(qubits=3, cbits=1)
def AND(qc: QuantumCircuit):
=1, control_qubit2=2, target_qubit=0)
qc.ccx(control_qubit1
qc.barrier()0,0)
qc.measure(
return qc
# I use `partial` here to hide the initial state of the output qubit
= partial(AND, False) AND
Code
for x_in in [True, False]:
for y_in in [True, False]:
= AND(x_in, y_in)
qc, out, job print(f"AND with input {x_in}, {y_in} gives output {out[0]}")
="mpl")) display(qc.draw(output
AND with input True, True gives output True
AND with input True, False gives output False
AND with input False, True gives output False
AND with input False, False gives output False
This looks great, and I did it relatively quickly too! At some point I should actually read about this stuff.
NAND Gate
I can do this one by flipping the AND gate.
Code
from functools import partial
from qiskit import QuantumCircuit
@with_circuit(qubits=3, cbits=1)
def NAND(qc: QuantumCircuit):
=1, control_qubit2=2, target_qubit=0)
qc.ccx(control_qubit1=0)
qc.x(qubit
qc.barrier()0,0)
qc.measure(
return qc
# I use `partial` here to hide the initial state of the output qubit
= partial(NAND, False) NAND
Code
for x_in in [True, False]:
for y_in in [True, False]:
= NAND(x_in, y_in)
qc, out, job print(f"NAND with input {x_in}, {y_in} gives output {out[0]}")
="mpl")) display(qc.draw(output
NAND with input True, True gives output False
NAND with input True, False gives output True
NAND with input False, True gives output True
NAND with input False, False gives output True
However given that the target qubit is flipped if both control qubits are set then I can make this more compact by making the target qubit initially true.
Code
from functools import partial
from qiskit import QuantumCircuit
@with_circuit(qubits=3, cbits=1)
def NAND(qc: QuantumCircuit):
=1, control_qubit2=2, target_qubit=0)
qc.ccx(control_qubit1# qc.x(qubit=0) # CHANGED
qc.barrier()0,0)
qc.measure(
return qc
# I use `partial` here to hide the initial state of the output qubit
= partial(NAND, True) # CHANGED NAND
Code
for x_in in [True, False]:
for y_in in [True, False]:
= NAND(x_in, y_in)
qc, out, job print(f"NAND with input {x_in}, {y_in} gives output {out[0]}")
="mpl")) display(qc.draw(output
NAND with input True, True gives output False
NAND with input True, False gives output True
NAND with input False, True gives output True
NAND with input False, False gives output True
Woo!
OR Gate
Given that we have the NAND gate we can now define any logical operation in terms of NAND. I’m trying to understand the behaviour of quantum circuits though. Let’s solve it using the operators that I have encountered so far. The simplest thing I can think of is NOT ((NOT A) AND (NOT B))
so lets start with that.
Given we are dealing with flipping again we can start with the qubit set and flip it only if neither input is set.
Code
from functools import partial
from qiskit import QuantumCircuit
@with_circuit(qubits=3, cbits=1)
def OR(qc: QuantumCircuit):
=1)
qc.x(qubit=2)
qc.x(qubit=1, control_qubit2=2, target_qubit=0)
qc.ccx(control_qubit1
qc.barrier()0,0)
qc.measure(
return qc
# I use `partial` here to hide the initial state of the output qubit
= partial(OR, True) OR
Code
for x_in in [True, False]:
for y_in in [True, False]:
= OR(x_in, y_in)
qc, out, job print(f"OR with input {x_in}, {y_in} gives output {out[0]}")
="mpl")) display(qc.draw(output
OR with input True, True gives output True
OR with input True, False gives output True
OR with input False, True gives output True
OR with input False, False gives output False
That’s all the operations that the quiz asks for. It then goes on to actually running this stuff in a real (noisy) quantum computer. I’m going to stop here as this is already quite long. The investigation of QuTiP will have to come in a different post.