何をするの?
量子コンピューティングライブラリのBlueqatを作っている私が、量子コンピューティングライブラリのQiskitのソースコードを読んでみる、という企画です。
普段気になっていながらも読めていなかった部分を、せっかくなので読んでみました。
タイトルの通り、今回は回路を作成して、ゲートや測定を回路に付け加えるまでの処理を読んでいきます。
なので、今回は量子計算のやり方のようなものは一切出てこない、ごくごくフロントエンドの部分だけを読みます。
Qiskit概要
QiskitはIBMが開発しているオープンソースの量子コンピューティングライブラリです。
Qiskitは以下のようにパッケージが分かれていますが、インストールする際はバラバラにやるよりもpip install qiskit
でまとめてインストールした方がトラブルが少ないです。
パッケージ | 役割 |
---|---|
Qiskit Terra | メインとなるパッケージです。回路を作るクラスや、回路を実機向けにトランスパイルする機能、APIを叩いて実機に投げる機能などが含まれています |
Qiskit Aer | 量子回路のシミュレータが含まれており、通常はQiskit Terraから呼び出します |
Qiskit Ignis | 量子回路を実機で動かした際のノイズと戦いたい人のためのライブラリです。私は使ったことがありません |
Qiskit Aqua | 量子アルゴリズムを簡単に使えるようにしたライブラリです |
今回読むのはQiskit Terraの一部です。
具体的には、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))
のうち、qc.measure([0,1], [0,1])
までの流れを読んでいきます。
GitHub上のmasterブランチを読んでいきます。
現時点のコミットIDはe7be587ですが、結構頻繁に更新されているので、記事が書き終わる頃には変わる可能性もあります。ご了承ください。
QuantumCircuitクラスと動的なメソッド追加
qiskit/circuit/quantumcircuit.pyをざっと眺めましょう。
皆さん、あるはずのものがないことに、お気づきになられましたでしょうか?
先ほどのコードに注目ください。
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0,1], [0,1])
QuantumCircuit.h
, QuantumCircuit.cx
, QuantumCircuit.measure
があるはずなのに、どこにも見つかりません。
これらの量子ゲートは数が多いので別で定義したい、という気持ちはすごくよく分かります。
では、一体どこで定義されているのでしょうか。
import
時に、qiskit.extensions.*
が読み込まれて、ゲートはそこで追加されています。
メジャーなゲートはqiskit/extensions/standardにあります。
また、測定はqiskit/circuit/measure.pyでQuantumCircuit
に追加されます。
(こういうのは、1箇所にまとめといて欲しいなぁ、と思いました)
QuantumCircuitを作る
さて、ゲートや測定のメソッドの謎を解いたところで、本題に戻りましょう。
qc = QuantumCircuit(2, 2)
を読んでみます。
qiskit/circuit/quantumcircuit.pyにあるQuantumCircuit.__init__
を読みます。
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
へー、回路に名前が必要なんですね。指定しなかったら勝手に付けてくれるみたいです。
あと、すげーくだらないんですが、プログラミングで日々消耗する者として気になったのが。
raise CircuitError("The circuit name should be a string "
"(or None to auto-generate a name).")
1行の文字数が多いとpylintとかに怒られるんですが、恐らくは、それを避けるために2行に分けています。
こういうの、いつも「それ怒られてもなぁ。本来は1行で表示されるものを2行に分けることで可読性上がるの? メッセージをgrepで検索とか想定してる?」って思っちゃうんですが。大人の対応ですね。
あとは、中味の初期化とかですね。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")
元々は、Qiskitでは、QuantumRegister
とClassicalRegister
を作らなければならなかったんですが、Qiskitがバージョンアップしていくうちに、単なる数字だけでもよくなりました。Blueqatのパクリかな?
Qiskit-TerraのREADME.mdで一番最初に出るコードも、わざわざレジスタを作らずに数字でやる、という例が示されています。Blueqatのパクリかな?
ですが、内部構造としては、レジスタがあることを前提としているようで、指定しなければ'q'という名前の量子レジスタと'c'という名前の古典レジスタが作られるようです。
ちょっとした文句
その後付け部分についてですが。コメントを見たら分かるように、これ、とても微妙な感じになっています。
_add_register
のようなアンダースコア付きではなく、add_register
と、アンダースコアなしなので、これは内部だけではなく外部からも呼び出せることを想定した関数のはずです。
けれど、レジスタではなく数字を渡す部分のコメントや例外メッセージを見ると、外部からの呼び出しを想定してなさそうな雰囲気になっています。「整数だったらレジスタ'q'と'c'を作る」の部分については、__init__
の中でやった方がよかったんじゃないかなぁ、と思いました。
……まぁ、今の実装で実際にすごく困るかというと、大して困らないので、いいんですが。
おまけ: 数字で指定したらq, cが作られる、ということを確認する
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'
うへへへへ。
Hゲートの実装
続いて、これらを見ていきます。
qc.h(0)
qc.cx(0, 1)
qc.measure([0,1], [0,1])
QuantumCircuit.h
が実装されているqiskit/extensions/standard/h.pyを見ると
def h(self, q): # pylint: disable=invalid-name
"""Apply H to q."""
return self.append(HGate(), [q], [])
QuantumCircuit.h = h
ややこしいのですが、ここでのself
はQuantumCircuit
になります。
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()
とは
初めから見ていきましょう。
if not isinstance(instruction, Instruction) and hasattr(instruction, 'to_instruction'):
instruction = instruction.to_instruction()
ゲートや測定はInstruction
なのでスルーされるのですが。
(具体的には、HGate
クラスがGate
クラスを継承していて、Gate
クラスがInstruction
クラスを継承しているので、HGate
はInstruction
です。他のゲートや測定も、親クラスを追っていくとInstruction
に辿り着きます)
そうじゃない場合、もしto_instruction
メソッドを持っていれば、それが呼び出されるようです。
ある種の「ゲートを拡張したようなもの」を追加できるようにする、という考えに見えます。
grepでto_instruction
メソッドを漁ってみたところ、ハードウェア制御用のパルスに関するもの、パウリ行列やクラウス表現などのゲートではないものを回路にするためのもの、が見つかりました。
ところで、「もしInstruction
じゃなく、to_instruction
メソッドも持ってなければ、ここで例外投げてほしいなー」と思ったのは、私だけでしょうか。(ここで投げなくても、後段で出るからいい、という話もありそうですが)
argument_conversionたち
次にいきましょう。
expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []]
expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []]
これらの中味を見ます。(docstringは削除して引用します)
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)
どっちも_bit_argument_conversion
を呼び出しているだけですが、その前にself.qubits
, self.clbits
ってなんだろ。見てみます。
@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]
レジスタの中味を全部、ひとつのリストに並べたものですね。
例えば[QuantumRegister(3, 'q1'), QuantumRegister(2, 'q2')]
の2つのレジスタがあれば、[q1[0], q1[1], q1[2], q2[0], q2[1]]
が返ってきます。
続いて_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
長いですが、何をやってるかはコメントに書いてあるとおりです。
いろんな呼び出し方をサポートしてるんだな、程度に捉えておけば大丈夫です。
InstructionSet
を見る
QuantumCircuit.append
も終わりが見えてきました。
instructions = InstructionSet()
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
instructions.add(self._append(instruction, qarg, carg), qarg, carg)
return instructions
さて。qiskit/circuit/instructionset.pyの、InstructionSet.__init__
を読んでいきましょう。
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 = []
大したことをしてない感じですね。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)
だいぶ想定通りですね。
Instruction.broadcast_arguments
を見る
残り少し!
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
instructions.add(self._append(instruction, qarg, carg), qarg, carg)
return instructions
broadcast_arguments
を読んでいきます。
これはqiskit/circuit/instruction.pyで実装されていますが、qiskit/circuit/gate.pyでオーバーライドされているため、今回呼び出されるのはGate.broadcast_arguments
になります。
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))
やってること自体は、コメントにあるとおりです。
ゲートに指定する量子ビット数に応じて処理が変わっています。Hゲートの場合1個ですが、ついでなので全部見ていきましょう。
@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)
1個の場合は、単にひとつずつリストに詰めて出すだけですね。
3個の場合も、[[q[0], r[0]], [q[1], r[1]], [q[2], r[2]]]
を[q[0], q[1], q[2]]
と[r[0], r[1], r[2]]
に分けるだけ、とシンプルです。
2個の場合、コメントにあるように、省略記法を許しているようです。
qc = QuantumCircuit(3, 3)
qc.cx([0, 1], 2)
print(qc.draw())
''' 結果(いらないところは略):
q_0: |0>──■───────
│
q_1: |0>──┼────■──
┌─┴─┐┌─┴─┐
q_2: |0>┤ X ├┤ X ├
└───┘└───┘
'''
qc = QuantumCircuit(3, 3)
qc.cx(0, [1, 2])
print(qc.draw())
'''結果(いらないところは略):
q_0: |0>──■────■──
┌─┴─┐ │
q_1: |0>┤ X ├──┼──
└───┘┌─┴─┐
q_2: |0>─────┤ X ├
└───┘
'''
このようにfor (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
で、ゲートを適用する量子ビットを順次取り出していることが分かります。
QuantumCircuit._append
を見る
最初に言っとくと
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
instructions.add(self._append(instruction, qarg, carg), qarg, carg)
の処理ですが、コードをこれから見ると分かるように
for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
self._append(instruction, qarg, carg)
instructions.add(instruction, qarg, carg)
とやった方が行儀がいいですね。1行削りたくなる気持ちはプログラマなので分かりますが。
では、終わったと思ったら意外と長かった_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
まず_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")
これは、qarg
に重複がないか確認しています。
Hのような単一量子ビットゲートでは、重複は起こりえませんが、qc.cx(0, 0)
のようなものを弾いてくれます。
続いて、_check_qargs
と_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")
Qubit
, Clbit
については、レジスタのインデックスを取ってq[0]
のようにすると返ってくるオブジェクトです。
ちゃんと回路に含まれている量子レジスタであることを確認しています。
以上でHゲートの追加は終わりです。
CXゲートの実装
qc.h(0)
qc.cx(0, 1)
qc.measure([0,1], [0,1])
の、cxについて見てみます。
cx
メソッドはqiskit/extensions/standard/cx.pyで実装されていますが、Hゲートとほぼ同じです。
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
呼び出されるとappend
が呼ばれる流れもHゲートと同じですね。
測定の実装
measure
についても見ていきましょう。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
はい、append
してるだけですね。ですが、broadcast_arguments
にGate
クラスのものではなくMeasure
クラスのものが使われることに注意してください。
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')
qc.measure([0,1], [0,1])
の場合だと、if文の上のやつが呼び出されます。
elifの部分はqc.measure(q, c)
のように、レジスタ渡しした場合に相当します。
これで、本日の目標の、qc = QuantumCircuit(2, 2)
からqc.measure([0,1], [0,1])
を読むことができました。
まとめ
今回、量子回路の作成からゲート、測定の追加までを読みました。
量子コンピューティングライブラリでは、ゲート追加などをメソッドの形で実装するために、動的なメソッド追加を行うことが多いです。Qiskitではどのように追加されているのか見ていきました。
また、Qiskitの量子回路の実装には、量子レジスタが重要になってきます。そのあたりのハンドリングでコードが煩雑になっている印象を受けました。
Qiskitの実装はやはり気になるので、続きに関してもまた読んでいきたいと思います。