[PYTHON] Qiskit Source Code Reading ~ Terra: Read from circuit creation to adding gates and measurements

What are you doing?

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 overview

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.

Added QuantumCircuit class and dynamic methods

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)

Make a Quantum Circuit

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.

A little complaint

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.

Bonus: Make sure that q and c are created if you specify them with numbers

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.

Implementation of H gate

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

What is 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 theGate 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)

argument_conversions

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.

See ʻInstruction Set`

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.

See ʻInstruction.broadcast_arguments`

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.

See 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.

Implementation of CX 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.

Implementation of measurement

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 theGate` 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]).

Summary

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

Qiskit Source Code Reading ~ Terra: Read from circuit creation to adding gates and measurements
Qiskit Source Code Reading ~ Terra: Read from circuit creation to adding gates and measurements
[Python] Read the Flask source code
Flow from source code to creating executable
Difference in writing method to read external source code between Ruby and Python
[Python] How to read data from CIFAR-10 and CIFAR-100
[Python] Django Source Code Reading View Starting from Zero ①