I am making Blueqat, a quantum computing library, and I plan to read the source code of Qiskit, a quantum computing library. I read the part that I was usually worried about but couldn't read.
As the title suggests, this time we will read the process of creating a circuit and adding gates and measurements to the circuit. So, this time, I will read only the very front-end part, which does not show anything like the method of quantum calculation.
Qiskit is an open source quantum computing library developed by IBM.
Qiskit is divided into packages as shown below, but when installing, it is less troublesome to install them together with pip install qiskit
than to install them separately.
package | role |
---|---|
Qiskit Terra | This is the main package. It includes a class to create a circuit, a function to transpile the circuit for the actual machine, a function to hit the API and throw it to the actual machine, etc. |
Qiskit Aer | Includes a quantum circuit simulator, usually called from Qiskit Terra |
Qiskit Ignis | This is a library for those who want to fight the noise when running a quantum circuit on an actual machine. I have never used |
Qiskit Aqua | A library that makes it easy to use quantum algorithms |
This time I'm reading a part of Qiskit Terra.
https://github.com/Qiskit/qiskit-terra
Specifically, the code written in README.md
from qiskit import *
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0,1], [0,1])
backend_sim = BasicAer.get_backend('qasm_simulator')
result = execute(qc, backend_sim).result()
print(result.get_counts(qc))
Of these, read the flow up to qc.measure ([0,1], [0,1])
.
Continue reading the master branch on GitHub. The commit ID at the moment is e7be587, but it's updated quite often and may change by the end of the article. Please note.
Take a quick look at qiskit / circuit / quantumcircuit.py.
Did you all realize that there is nothing that should be there? Notice the code above.
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0,1], [0,1])
I should have QuantumCircuit.h
, QuantumCircuit.cx
, QuantumCircuit.measure
, but I can't find it anywhere.
Since there are so many of these quantum gates, I really understand the desire to define them separately. So where is it defined?
At ʻimport,
qiskit.extensions. * Is loaded and the gate is added there. The major gates are located at [qiskit / extensions / standard](https://github.com/Qiskit/qiskit-terra/tree/master/qiskit/extensions/standard). The measurement is also added to
QuantumCircuit` at qiskit / circuit / measure.py.
(I wanted you to put these things together in one place)
Now that we've solved the mystery of gates and measurement methods, let's get back to the main topic.
qc = QuantumCircuit(2, 2)
I will read.
Read QuantumCircuit.__init__
at qiskit / circuit / quantumcircuit.py.
def __init__(self, *regs, name=None):
if name is None:
name = self.cls_prefix() + str(self.cls_instances())
# pylint: disable=not-callable
# (known pylint bug: https://github.com/PyCQA/pylint/issues/1699)
if sys.platform != "win32" and isinstance(mp.current_process(), mp.context.ForkProcess):
name += '-{}'.format(mp.current_process().pid)
self._increment_instances()
if not isinstance(name, str):
raise CircuitError("The circuit name should be a string "
"(or None to auto-generate a name).")
self.name = name
# Data contains a list of instructions and their contexts,
# in the order they were applied.
self._data = []
# This is a map of registers bound to this circuit, by name.
self.qregs = []
self.cregs = []
self.add_register(*regs)
# Parameter table tracks instructions with variable parameters.
self._parameter_table = ParameterTable()
self._layout = None
Hmm, you need a name for the circuit. If you do not specify it, it will be attached without permission.
Also, it's terrible, but I was worried as a person who is exhausted every day by programming.
raise CircuitError("The circuit name should be a string "
"(or None to auto-generate a name).")
If there are a lot of characters in one line, I get angry with pylint, but I probably divide it into two lines to avoid it. I'm always wondering, "I don't mind if I get angry. Is it possible to improve readability by dividing what is originally displayed on one line into two lines? Is it supposed to search for messages with grep?" .. It's for adults.
The rest is the initialization of the contents. Let's also read ʻadd_register`.
def add_register(self, *regs):
"""Add registers."""
if not regs:
return
if any([isinstance(reg, int) for reg in regs]):
# QuantumCircuit defined without registers
if len(regs) == 1 and isinstance(regs[0], int):
# QuantumCircuit with anonymous quantum wires e.g. QuantumCircuit(2)
regs = (QuantumRegister(regs[0], 'q'),)
elif len(regs) == 2 and all([isinstance(reg, int) for reg in regs]):
# QuantumCircuit with anonymous wires e.g. QuantumCircuit(2, 3)
regs = (QuantumRegister(regs[0], 'q'), ClassicalRegister(regs[1], 'c'))
else:
raise CircuitError("QuantumCircuit parameters can be Registers or Integers."
" If Integers, up to 2 arguments. QuantumCircuit was called"
" with %s." % (regs,))
for register in regs:
if register.name in [reg.name for reg in self.qregs + self.cregs]:
raise CircuitError("register name \"%s\" already exists"
% register.name)
if isinstance(register, QuantumRegister):
self.qregs.append(register)
elif isinstance(register, ClassicalRegister):
self.cregs.append(register)
else:
raise CircuitError("expected a register")
Originally, Qiskit had to create Quantum Register
and Classical Register
, but as Qiskit upgraded, it became better to just use numbers. ~~ Blueqat plagiarism? ~~
The first code in Qiskit-Terra's README.md also shows an example of doing it numerically without creating a register. ~~ Blueqat plagiarism? ~~
However, the internal structure seems to assume that there are registers, and if not specified, a quantum register named'q'and a classical register named'c' will be created.
Regarding the part attached after that. As you can see in the comments, this is a very subtle feeling.
Not with an underscore like _add_register
, but with ʻadd_register`, there is no underscore, so this should be a function that is supposed to be called not only internally but also externally.
However, looking at the comments and exception messages in the part that passes numbers instead of registers, it seems that it is unlikely to be called from the outside. I thought it would have been better to do the part of "Create registers'q'and'c' if it is an integer" in __init__
.
...... Well, the actual problem with the current implementation is that it's not a big problem, so it's okay.
from qiskit import *
q = QuantumRegister(3, 'q')
c = QuantumRegister(3, 'c')
qc = QuantumCircuit(4, 4)
qc.add_register(q)
# => QiskitError: 'register name "q" already exists'
qc.add_register(c)
# => QiskitError: 'register name "c" already exists'
qc = QuantumCircuit(q)
qc.add_register(4)
# => QiskitError: 'register name "q" already exists'
Uhehehehe.
Next, let's look at these.
qc.h(0)
qc.cx(0, 1)
qc.measure([0,1], [0,1])
[Qiskit / extensions / standard / h.py] with QuantumCircuit.h
implemented (https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/extensions/standard/h.py) When you look at
def h(self, q): # pylint: disable=invalid-name
"""Apply H to q."""
return self.append(HGate(), [q], [])
QuantumCircuit.h = h
It's confusing, but here self
becomes Quantum Circuit
.
Let's take a look at QuantumCircuit.append
.
QuantumCircuit.append
def append(self, instruction, qargs=None, cargs=None):
"""Append one or more instructions to the end of the circuit, modifying
the circuit in place. Expands qargs and cargs.
Args:
instruction (Instruction or Operation): Instruction instance to append
qargs (list(argument)): qubits to attach instruction to
cargs (list(argument)): clbits to attach instruction to
Returns:
Instruction: a handle to the instruction that was just added
"""
# Convert input to instruction
if not isinstance(instruction, Instruction) and hasattr(instruction, 'to_instruction'):
instruction = instruction.to_instruction()
expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []]
expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []]
instructions = InstructionSet()
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
instructions.add(self._append(instruction, qarg, carg), qarg, carg)
return instructions
to_instruction ()
?Let's start from the beginning.
if not isinstance(instruction, Instruction) and hasattr(instruction, 'to_instruction'):
instruction = instruction.to_instruction()
Gates and measurements are ʻInstruction, so they are passed through. (Specifically, [
HGate class](https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/extensions/standard/h.py#L27) is [
Gateclass] It inherits from (https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/circuit/gate.py#L24) and the
Gate class is the [ʻInstruction
class](https: / Since it inherits from /github.com/Qiskit/qiskit-terra/blob/master/qiskit/circuit/instruction.py#L51), HGate
is ʻInstruction. Other gates and measurements are also parent classes. If you follow, you will reach ʻInstruction
)
If not, it seems that if you have a to_instruction
method, it will be called.
It looks like the idea is to allow you to add some sort of "extended gate".
When I hunted the to_instruction
method with grep, I found something related to pulses for hardware control, and something for making non-gates such as Pauli matrices and Claus representations into circuits.
By the way, am I the only one who thought, "If you don't have a to_instruction
method instead of ʻInstruction`, I want you to throw an exception here." (There seems to be a story that you don't have to throw it here because it will come out later)
Let's go next.
expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []]
expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []]
Look at these contents. (Delete the docstring and quote)
def qbit_argument_conversion(self, qubit_representation):
return QuantumCircuit._bit_argument_conversion(qubit_representation, self.qubits)
def cbit_argument_conversion(self, clbit_representation):
return QuantumCircuit._bit_argument_conversion(clbit_representation, self.clbits)
Both are just calling _bit_argument_conversion
, but before that, what are self.qubits
and self.clbits
? I'll take a look.
@property
def qubits(self):
"""
Returns a list of quantum bits in the order that the registers were added.
"""
return [qbit for qreg in self.qregs for qbit in qreg]
@property
def clbits(self):
"""
Returns a list of classical bits in the order that the registers were added.
"""
return [cbit for creg in self.cregs for cbit in creg]
All the contents of the register are arranged in one list.
For example, if you have two registers, [QuantumRegister (3,'q1'), QuantumRegister (2,' q2')]
, then[q1 [0], q1 [1], q1 [2], q2 [0] ], q2 [1]]
is returned.
Then read _bit_argument_conversion
.
@staticmethod
def _bit_argument_conversion(bit_representation, in_array):
ret = None
try:
if isinstance(bit_representation, Bit):
# circuit.h(qr[0]) -> circuit.h([qr[0]])
ret = [bit_representation]
elif isinstance(bit_representation, Register):
# circuit.h(qr) -> circuit.h([qr[0], qr[1]])
ret = bit_representation[:]
elif isinstance(QuantumCircuit.cast(bit_representation, int), int):
# circuit.h(0) -> circuit.h([qr[0]])
ret = [in_array[bit_representation]]
elif isinstance(bit_representation, slice):
# circuit.h(slice(0,2)) -> circuit.h([qr[0], qr[1]])
ret = in_array[bit_representation]
elif _is_bit(bit_representation):
# circuit.h((qr, 0)) -> circuit.h([qr[0]])
ret = [bit_representation[0][bit_representation[1]]]
elif isinstance(bit_representation, list) and \
all(_is_bit(bit) for bit in bit_representation):
ret = [bit[0][bit[1]] for bit in bit_representation]
elif isinstance(bit_representation, list) and \
all(isinstance(bit, Bit) for bit in bit_representation):
# circuit.h([qr[0], qr[1]]) -> circuit.h([qr[0], qr[1]])
ret = bit_representation
elif isinstance(QuantumCircuit.cast(bit_representation, list), (range, list)):
# circuit.h([0, 1]) -> circuit.h([qr[0], qr[1]])
# circuit.h(range(0,2)) -> circuit.h([qr[0], qr[1]])
# circuit.h([qr[0],1]) -> circuit.h([qr[0], qr[1]])
ret = [index if isinstance(index, Bit) else in_array[
index] for index in bit_representation]
else:
raise CircuitError('Not able to expand a %s (%s)' % (bit_representation,
type(bit_representation)))
except IndexError:
raise CircuitError('Index out of range.')
except TypeError:
raise CircuitError('Type error handling %s (%s)' % (bit_representation,
type(bit_representation)))
return ret
It's a long time, but what you're doing is as stated in the comments. It supports various calling methods, so it's okay if you understand it to a certain extent.
The end of QuantumCircuit.append
is coming to an end.
instructions = InstructionSet()
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
instructions.add(self._append(instruction, qarg, carg), qarg, carg)
return instructions
Now. Let's read ʻInstructionSet.init` in qiskit / circuit / instructionset.py.
class InstructionSet:
"""Instruction collection, and their contexts."""
def __init__(self):
"""New collection of instructions.
The context (qargs and cargs that each instruction is attached to),
is also stored separately for each instruction.
"""
self.instructions = []
self.qargs = []
self.cargs = []
It feels like you're not doing much. If you also look at ʻInstructionSet.add`
def add(self, gate, qargs, cargs):
"""Add an instruction and its context (where it's attached)."""
if not isinstance(gate, Instruction):
raise CircuitError("attempt to add non-Instruction" +
" to InstructionSet")
self.instructions.append(gate)
self.qargs.append(qargs)
self.cargs.append(cargs)
It's pretty much as expected.
A little left!
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
instructions.add(self._append(instruction, qarg, carg), qarg, carg)
return instructions
Continue reading broadcast_arguments
.
This is implemented in qiskit / circuit / instruction.py, but qiskit / circuit / Since it is overridden by gate.py, it will be called Gate.broadcast_arguments
this time. I will.
def broadcast_arguments(self, qargs, cargs):
"""Validation and handling of the arguments and its relationship.
For example:
`cx([q[0],q[1]], q[2])` means `cx(q[0], q[2]); cx(q[1], q[2])`. This method
yields the arguments in the right grouping. In the given example::
in: [[q[0],q[1]], q[2]],[]
outs: [q[0], q[2]], []
[q[1], q[2]], []
The general broadcasting rules are:
* If len(qargs) == 1::
[q[0], q[1]] -> [q[0]],[q[1]]
* If len(qargs) == 2::
[[q[0], q[1]], [r[0], r[1]]] -> [q[0], r[0]], [q[1], r[1]]
[[q[0]], [r[0], r[1]]] -> [q[0], r[0]], [q[0], r[1]]
[[q[0], q[1]], [r[0]]] -> [q[0], r[0]], [q[1], r[0]]
* If len(qargs) >= 3::
[q[0], q[1]], [r[0], r[1]], ...] -> [q[0], r[0], ...], [q[1], r[1], ...]
Args:
qargs (List): List of quantum bit arguments.
cargs (List): List of classical bit arguments.
Returns:
Tuple(List, List): A tuple with single arguments.
Raises:
CircuitError: If the input is not valid. For example, the number of
arguments does not match the gate expectation.
"""
if len(qargs) != self.num_qubits or cargs:
raise CircuitError(
'The amount of qubit/clbit arguments does not match the gate expectation.')
if any([not qarg for qarg in qargs]):
raise CircuitError('One or more of the arguments are empty')
if len(qargs) == 1:
return Gate._broadcast_single_argument(qargs[0])
elif len(qargs) == 2:
return Gate._broadcast_2_arguments(qargs[0], qargs[1])
elif len(qargs) >= 3:
return Gate._broadcast_3_or_more_args(qargs)
else:
raise CircuitError('This gate cannot handle %i arguments' % len(qargs))
What you are doing is as you can see in the comments. The processing changes according to the number of qubits specified in the gate. In the case of H gate, there is only one, but let's take a look at all of them.
@staticmethod
def _broadcast_single_argument(qarg):
"""Expands a single argument.
For example: [q[0], q[1]] -> [q[0]], [q[1]]
"""
# [q[0], q[1]] -> [q[0]]
# -> [q[1]]
for arg0 in qarg:
yield [arg0], []
@staticmethod
def _broadcast_2_arguments(qarg0, qarg1):
if len(qarg0) == len(qarg1):
# [[q[0], q[1]], [r[0], r[1]]] -> [q[0], r[0]]
# -> [q[1], r[1]]
for arg0, arg1 in zip(qarg0, qarg1):
yield [arg0, arg1], []
elif len(qarg0) == 1:
# [[q[0]], [r[0], r[1]]] -> [q[0], r[0]]
# -> [q[0], r[1]]
for arg1 in qarg1:
yield [qarg0[0], arg1], []
elif len(qarg1) == 1:
# [[q[0], q[1]], [r[0]]] -> [q[0], r[0]]
# -> [q[1], r[0]]
for arg0 in qarg0:
yield [arg0, qarg1[0]], []
else:
raise CircuitError('Not sure how to combine these two qubit arguments:\n %s\n %s' %
(qarg0, qarg1))
@staticmethod
def _broadcast_3_or_more_args(qargs):
if all(len(qarg) == len(qargs[0]) for qarg in qargs):
for arg in zip(*qargs):
yield list(arg), []
else:
raise CircuitError(
'Not sure how to combine these qubit arguments:\n %s\n' % qargs)
In the case of one, you just put them in a list one by one.
In the case of three, [[q [0], r [0]], [q [1], r [1]], [q [2], r [2]]]
is changed to [q [ It's as simple as 0], q [1], q [2]]
and[r [0], r [1], r [2]]
.
In the case of two, it seems that the abbreviation is allowed as stated in the comment.
qc = QuantumCircuit(3, 3)
qc.cx([0, 1], 2)
print(qc.draw())
'''result(Omitted where you don't need):
q_0: |0>──■───────
│
q_1: |0>──┼────■──
┌─┴─┐┌─┴─┐
q_2: |0>┤ X ├┤ X ├
└───┘└───┘
'''
qc = QuantumCircuit(3, 3)
qc.cx(0, [1, 2])
print(qc.draw())
'''result(Omitted where you don't need):
q_0: |0>──■────■──
┌─┴─┐ │
q_1: |0>┤ X ├──┼──
└───┘┌─┴─┐
q_2: |0>─────┤ X ├
└───┘
'''
As you can see, for (qarg, carg) in instruction.broadcast_arguments (expanded_qargs, expanded_cargs):
sequentially extracts the qubits to which the gate is applied.
QuantumCircuit._append
First of all
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
instructions.add(self._append(instruction, qarg, carg), qarg, carg)
As you can see from the code
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
self._append(instruction, qarg, carg)
instructions.add(instruction, qarg, carg)
It's better to do it. I understand that I want to cut one line because I am a programmer.
Then, when I think it's over, let's take a look at the unexpectedly long _append
.
def _append(self, instruction, qargs, cargs):
"""Append an instruction to the end of the circuit, modifying
the circuit in place.
Args:
instruction (Instruction or Operator): Instruction instance to append
qargs (list(tuple)): qubits to attach instruction to
cargs (list(tuple)): clbits to attach instruction to
Returns:
Instruction: a handle to the instruction that was just added
Raises:
CircuitError: if the gate is of a different shape than the wires
it is being attached to.
"""
if not isinstance(instruction, Instruction):
raise CircuitError('object is not an Instruction.')
# do some compatibility checks
self._check_dups(qargs)
self._check_qargs(qargs)
self._check_cargs(cargs)
# add the instruction onto the given wires
instruction_context = instruction, qargs, cargs
self._data.append(instruction_context)
self._update_parameter_table(instruction)
return instruction
First _check_dups
def _check_dups(self, qubits):
"""Raise exception if list of qubits contains duplicates."""
squbits = set(qubits)
if len(squbits) != len(qubits):
raise CircuitError("duplicate qubit arguments")
This is checking for duplicates in qarg
.
A single qubit gate like H can't duplicate, but it plays something like qc.cx (0, 0)
.
Then _check_qargs
and _check_cargs
def _check_qargs(self, qargs):
"""Raise exception if a qarg is not in this circuit or bad format."""
if not all(isinstance(i, Qubit) for i in qargs):
raise CircuitError("qarg is not a Qubit")
if not all(self.has_register(i.register) for i in qargs):
raise CircuitError("register not in this circuit")
def _check_cargs(self, cargs):
"""Raise exception if clbit is not in this circuit or bad format."""
if not all(isinstance(i, Clbit) for i in cargs):
raise CircuitError("carg is not a Clbit")
if not all(self.has_register(i.register) for i in cargs):
raise CircuitError("register not in this circuit")
For Qubit
and Clbit
, it is an object that is returned when you take the index of the register and do something like q [0]
.
We have confirmed that it is a quantum register included in the circuit.
This is the end of adding H gate.
qc.h(0)
qc.cx(0, 1)
qc.measure([0,1], [0,1])
Let's take a look at cx.
The cx
method is implemented in qiskit / extensions / standard / cx.py However, it is almost the same as H gate.
def cx(self, ctl, tgt): # pylint: disable=invalid-name
"""Apply CX from ctl to tgt."""
return self.append(CnotGate(), [ctl, tgt], [])
QuantumCircuit.cx = cx
QuantumCircuit.cnot = cx
The flow of calling ʻappend` when called is the same as H gate.
Let's also look at measure
. Read qiskit / circuit / measure.py.
def measure(self, qubit, cbit):
"""Measure quantum bit into classical bit (tuples).
Args:
qubit (QuantumRegister|list|tuple): quantum register
cbit (ClassicalRegister|list|tuple): classical register
Returns:
qiskit.Instruction: the attached measure instruction.
Raises:
CircuitError: if qubit is not in this circuit or bad format;
if cbit is not in this circuit or not creg.
"""
return self.append(Measure(), [qubit], [cbit])
QuantumCircuit.measure = measure
Yes, it's just append. However, note that the
broadcast_arguments is of the
Measureclass instead of the
Gate` class.
def broadcast_arguments(self, qargs, cargs):
qarg = qargs[0]
carg = cargs[0]
if len(carg) == len(qarg):
for qarg, carg in zip(qarg, carg):
yield [qarg], [carg]
elif len(qarg) == 1 and carg:
for each_carg in carg:
yield qarg, [each_carg]
else:
raise CircuitError('register size error')
In the case of qc.measure ([0,1], [0,1])
, the one above the if statement is called.
The elif part corresponds to the case where a register is passed, such as qc.measure (q, c)
.
You can now read today's goal, qc = QuantumCircuit (2, 2)
to qc.measure ([0,1], [0,1])
.
This time, I read from the creation of quantum circuits to the addition of gates and measurements. In quantum computing libraries, dynamic method addition is often performed in order to implement gate addition etc. in the form of methods. We looked at how it was added in Qiskit. In addition, quantum registers are important for implementing Qiskit quantum circuits. I got the impression that the code was complicated due to the handling around that.
I'm still curious about the implementation of Qiskit, so I'd like to read more about the continuation.
Recommended Posts