何をするの?
量子コンピューティングライブラリの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))
のうち、backend_sim = BasicAer.get_backend('qasm_simulator')
以降の流れを読んでいきます。
ただし、シミュレータの中味やトランスパイルの詳細は読みません。
GitHub上のmasterブランチを読んでいきます。
現時点のコミットIDはe7be587ですが、結構頻繁に更新されているので、記事が書き終わる頃には変わる可能性もあります。ご了承ください。
BasicAer.get_backend
の行を読む
backend_sim = BasicAer.get_backend('qasm_simulator')
この行を読みます。
BasicAer
はterraのqiskit/providers/basicaer/init.pyで
BasicAer = BasicAerProvider()
と定義されています。
BasicAerProvider
を読む
qiskit/providers/basicaer/basicaerprovider.pyのBasicAerProvider
を読んでいきます。
SIMULATORS = [
QasmSimulatorPy,
StatevectorSimulatorPy,
UnitarySimulatorPy
]
class BasicAerProvider(BaseProvider):
"""Provider for Basic Aer backends."""
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
# Populate the list of Basic Aer backends.
self._backends = self._verify_backends()
# 略
def _verify_backends(self):
"""
Return the Basic Aer backends in `BACKENDS` that are
effectively available (as some of them might depend on the presence
of an optional dependency or on the existence of a binary).
Returns:
dict[str:BaseBackend]: a dict of Basic Aer backend instances for
the backends that could be instantiated, keyed by backend name.
"""
ret = OrderedDict()
for backend_cls in SIMULATORS:
try:
backend_instance = self._get_backend_instance(backend_cls)
backend_name = backend_instance.name()
ret[backend_name] = backend_instance
except QiskitError as err:
# Ignore backends that could not be initialized.
logger.info('Basic Aer backend %s is not available: %s',
backend_cls, str(err))
return ret
一応、親クラスのBaseProvider
の__init__
を読んでおきます。
class BaseProvider(ABC):
"""Base class for a Backend Provider."""
def __init__(self, *args, **kwargs):
pass
何もしていません。
BasicAerProvider._verify_backends
を読む
続いて
self._backends = self._verify_backends()
の_verify_backends
を読みます。コードは上に貼りましたが、verify
って名前で検証以外のことするの本当によくない……
それは置いといて。BasicAer
はプロバイダーで、プロバイダーとはBackend
を持っているものである、という構造が見えてきました。
SIMULATORS
に定義されている[QasmSimulatorPy, StatevectorSimulatorPy, UnitarySimulatorPy]
クラスを、それぞれ_get_backend_instance
して、(順序付き)辞書に詰め込んで返しているようです。
_get_backend_instance
を読んでみましょう。docstringは削って引用します。
def _get_backend_instance(self, backend_cls):
# Verify that the backend can be instantiated.
try:
backend_instance = backend_cls(provider=self)
except Exception as err:
raise QiskitError('Backend %s could not be instantiated: %s' %
(backend_cls, err))
return backend_instance
単にバックエンドクラスのインスタンスを作ってるだけですが、インスタンスにプロバイダー自身を渡しています。
プロバイダーとバックエンドとは相互参照の関係になっています。
QasmSimulatorPy.__init__
を読む
これらのバックエンドクラス自体は
qiskit/providers/basicaer/qasm_simulator.py、qiskit/providers/basicaer/statevector_simulator.py、qiskit/providers/basicaer/unitary_simulator.pyに定義されています。QasmSimulatorPy.__init__
だけ見てみましょう。
class QasmSimulatorPy(BaseBackend):
"""Python implementation of a qasm simulator."""
MAX_QUBITS_MEMORY = int(log2(local_hardware_info()['memory'] * (1024 ** 3) / 16))
DEFAULT_CONFIGURATION = {
'backend_name': 'qasm_simulator',
'backend_version': '2.0.0',
'n_qubits': min(24, MAX_QUBITS_MEMORY),
'url': 'https://github.com/Qiskit/qiskit-terra',
'simulator': True,
'local': True,
'conditional': True,
'open_pulse': False,
'memory': True,
'max_shots': 65536,
'coupling_map': None,
'description': 'A python simulator for qasm experiments',
'basis_gates': ['u1', 'u2', 'u3', 'cx', 'id', 'unitary'],
'gates': [
{
'name': 'u1',
'parameters': ['lambda'],
'qasm_def': 'gate u1(lambda) q { U(0,0,lambda) q; }'
},
{
'name': 'u2',
'parameters': ['phi', 'lambda'],
'qasm_def': 'gate u2(phi,lambda) q { U(pi/2,phi,lambda) q; }'
},
{
'name': 'u3',
'parameters': ['theta', 'phi', 'lambda'],
'qasm_def': 'gate u3(theta,phi,lambda) q { U(theta,phi,lambda) q; }'
},
{
'name': 'cx',
'parameters': ['c', 't'],
'qasm_def': 'gate cx c,t { CX c,t; }'
},
{
'name': 'id',
'parameters': ['a'],
'qasm_def': 'gate id a { U(0,0,0) a; }'
},
{
'name': 'unitary',
'parameters': ['matrix'],
'qasm_def': 'unitary(matrix) q1, q2,...'
}
]
}
DEFAULT_OPTIONS = {
"initial_statevector": None,
"chop_threshold": 1e-15
}
# Class level variable to return the final state at the end of simulation
# This should be set to True for the statevector simulator
SHOW_FINAL_STATE = False
def __init__(self, configuration=None, provider=None):
super().__init__(configuration=(
configuration or QasmBackendConfiguration.from_dict(self.DEFAULT_CONFIGURATION)),
provider=provider)
# Define attributes in __init__.
self._local_random = np.random.RandomState()
self._classical_memory = 0
self._classical_register = 0
self._statevector = 0
self._number_of_cmembits = 0
self._number_of_qubits = 0
self._shots = 0
self._memory = False
self._initial_statevector = self.DEFAULT_OPTIONS["initial_statevector"]
self._chop_threshold = self.DEFAULT_OPTIONS["chop_threshold"]
self._qobj_config = None
# TEMP
self._sample_measure = False
いろいろ設定入れてるなー、という感じです。やってることは単に変数の初期化などで、大したことをやっているようには見えません。
最大で24量子ビットまでしかシミュレートできないんですね。知らなかったです。(結構少なめに設定してるな、という印象です。普通のパソコンでも、もう少し頑張れるはずです)
バックエンド自体に、ショット数や、メモリなどの途中経過と思われる状態を持たせているように見えます。(Blueqatでは、そのような実装をあえて避けています)
このあたりの思想は、これからコードを読むうちにもう少し分かってくるかもしれません。
BasicAer
とは何であったか
-
BasicAer
はBasicAerProvider
のインスタンス-
BasicAerProvider
のインスタンスはバックエンドのリストを持つ
-
が分かりました。続いて、BasicAer.get_backend
を見ていきます。
BasicAer.get_backend
を読む
def get_backend(self, name=None, **kwargs):
backends = self._backends.values()
# Special handling of the `name` parameter, to support alias resolution
# and deprecated names.
if name:
try:
resolved_name = resolve_backend_name(
name, backends,
self._deprecated_backend_names(),
{}
)
name = resolved_name
except LookupError:
raise QiskitBackendNotFoundError(
"The '{}' backend is not installed in your system.".format(name))
return super().get_backend(name=name, **kwargs)
せっかくself._backends
を辞書で持ってるのに、そのまま辞書を引かずにやっているのが気になりますね。
resolve_backend_name
を読んでみましょう。
resolve_backend_name
を読む
resolve_backend_name
はqiskit/providers/providerutils.pyに定義されています。
def resolve_backend_name(name, backends, deprecated, aliased):
"""Resolve backend name from a deprecated name or an alias.
A group will be resolved in order of member priorities, depending on
availability.
Args:
name (str): name of backend to resolve
backends (list[BaseBackend]): list of available backends.
deprecated (dict[str: str]): dict of deprecated names.
aliased (dict[str: list[str]]): dict of aliased names.
Returns:
str: resolved name (name of an available backend)
Raises:
LookupError: if name cannot be resolved through regular available
names, nor deprecated, nor alias names.
"""
available = [backend.name() for backend in backends]
resolved_name = deprecated.get(name, aliased.get(name, name))
if isinstance(resolved_name, list):
resolved_name = next((b for b in resolved_name if b in available), "")
if resolved_name not in available:
raise LookupError("backend '{}' not found.".format(name))
if name in deprecated:
logger.warning("Backend '%s' is deprecated. Use '%s'.", name,
resolved_name)
return resolved_name
available
で、バックエンド名のリストを作ります。
deprecated
は、BasicAerProvider._deprecated_backend_names()
を見ると、前からQiskit使っていた人には分かるように
@staticmethod
def _deprecated_backend_names():
"""Returns deprecated backend names."""
return {
'qasm_simulator_py': 'qasm_simulator',
'statevector_simulator_py': 'statevector_simulator',
'unitary_simulator_py': 'unitary_simulator',
'local_qasm_simulator_py': 'qasm_simulator',
'local_statevector_simulator_py': 'statevector_simulator',
'local_unitary_simulator_py': 'unitary_simulator',
'local_unitary_simulator': 'unitary_simulator',
}
{'古いバックエンド名': '現役のバックエンド名'}
の辞書になっています。
探したいバックエンド名がdeprecated
に入っていたら、現役のバックエンド名に変換します。
aliased
については、qiskit-terra
とqiskit-aer
を探しましたが空でないものが渡されているのが見つかりませんでしたが、探したいバックエンド名がaliased
辞書に入っていたら、別名のリストを取り出すようです。
別名がリストで複数あるので、一番最初のavailable
なバックエンド名にして、もしどれもavailable
でなけえれば空文字列にします。
名前の変換が終わったら、変換された名前に対応するバックエンドを見て、あればその名前を返し、なければ例外を投げます。
親クラスのget_backend()
を読む
名前の変換を行って、available
な名前を得たら、
return super().get_backend(name=name, **kwargs)
としていました。なので、BaseProvider.get_backend()
を読みましょう。docstringは省略します。
def get_backend(self, name=None, **kwargs):
backends = self.backends(name, **kwargs)
if len(backends) > 1:
raise QiskitBackendNotFoundError('More than one backend matches the criteria')
if not backends:
raise QiskitBackendNotFoundError('No backend matches the criteria')
return backends[0]
BasicAerProvider.backends(name)
で返されるリストかなにかの最初の要素を返しています。
BasicAerProvider.backends
を読みます。
def backends(self, name=None, filters=None, **kwargs):
# pylint: disable=arguments-differ
backends = self._backends.values()
# Special handling of the `name` parameter, to support alias resolution
# and deprecated names.
if name:
try:
resolved_name = resolve_backend_name(
name, backends,
self._deprecated_backend_names(),
{}
)
backends = [backend for backend in backends if
backend.name() == resolved_name]
except LookupError:
return []
return filter_backends(backends, filters=filters, **kwargs)
またresolve_backend_name
を呼び出しています。deprecated
が循環しているとか、そういう変なことがなければ、何度やっても同じ結果が返ってくるはずです。今度は名前ではなくバックエンド自身を取り出しています。
filter_backends
については、さほど大したことやってないのに長いので、気になる方はqiskit/providers/providerutils.pyをご参照ください。
docstringとコメントだけ引用します。
def filter_backends(backends, filters=None, **kwargs):
"""略
Args:
backends (list[BaseBackend]): list of backends.
filters (callable): filtering conditions as a callable.
**kwargs: dict of criteria.
Returns:
list[BaseBackend]: a list of backend instances matching the
conditions.
"""
# Inspect the backends to decide which filters belong to
# backend.configuration and which ones to backend.status, as it does
# not involve querying the API.
# 1. Apply backend.configuration filtering.
# 2. Apply backend.status filtering (it involves one API call for
# each backend).
# 3. Apply acceptor filter.
kwargs
で、backend.configuration
やbackend.status
に対する条件を与えます。
これらはまぜこぜで与えると、filter_backends
側で振り分けてくれます。
さらに、関数をfilters
引数に与えると、Python組み込みのfilter
関数でフィルタした結果を返してくれます。
ちなみに、思いっきり話が逸れますが。Python組み込みのfilter
関数って、引数にNone渡してもいいんですね。知らなかった。
Return an iterator yielding those items of iterable for which function(item)
is true. If function is None, return the items that are true.
らしいです。
list(filter(None, [1, 2, 0, 3, "", "a"]))
# => [1, 2, 3, 'a']
ともかく、いろいろとたらい回しにされながらも、名前にマッチしたバックエンドを一つ得ることができました。
これで
backend_sim = BasicAer.get_backend('qasm_simulator')
の行は読めたことになります。
execute
を読む
続いて
result = execute(qc, backend_sim).result()
の行のうち、execute
を読んでいきます。
qiskit/execute.pyから、docstringを削って引用します。
def execute(experiments, backend,
basis_gates=None, coupling_map=None, # circuit transpile options
backend_properties=None, initial_layout=None,
seed_transpiler=None, optimization_level=None, pass_manager=None,
qobj_id=None, qobj_header=None, shots=1024, # common run options
memory=False, max_credits=10, seed_simulator=None,
default_qubit_los=None, default_meas_los=None, # schedule run options
schedule_los=None, meas_level=2, meas_return='avg',
memory_slots=None, memory_slot_size=100, rep_time=None, parameter_binds=None,
**run_config):
# transpiling the circuits using given transpile options
experiments = transpile(experiments,
basis_gates=basis_gates,
coupling_map=coupling_map,
backend_properties=backend_properties,
initial_layout=initial_layout,
seed_transpiler=seed_transpiler,
optimization_level=optimization_level,
backend=backend,
pass_manager=pass_manager,
)
# assembling the circuits into a qobj to be run on the backend
qobj = assemble(experiments,
qobj_id=qobj_id,
qobj_header=qobj_header,
shots=shots,
memory=memory,
max_credits=max_credits,
seed_simulator=seed_simulator,
default_qubit_los=default_qubit_los,
default_meas_los=default_meas_los,
schedule_los=schedule_los,
meas_level=meas_level,
meas_return=meas_return,
memory_slots=memory_slots,
memory_slot_size=memory_slot_size,
rep_time=rep_time,
parameter_binds=parameter_binds,
backend=backend,
**run_config
)
# executing the circuits on the backend and returning the job
return backend.run(qobj, **run_config)
transpile
を眺める
今回、トランスパイルの詳細については立ち入りません。
本当に表面だけ眺めます。
qiskit/compiler/transpile.pyにあります。
docstringがとても長いので省略しますが、興味深いので、読まれることをおすすめします。
def transpile(circuits,
backend=None,
basis_gates=None, coupling_map=None, backend_properties=None,
initial_layout=None, seed_transpiler=None,
optimization_level=None,
pass_manager=None, callback=None, output_name=None):
# transpiling schedules is not supported yet.
if isinstance(circuits, Schedule) or \
(isinstance(circuits, list) and all(isinstance(c, Schedule) for c in circuits)):
return circuits
if optimization_level is None:
config = user_config.get_config()
optimization_level = config.get('transpile_optimization_level', None)
# Get TranspileConfig(s) to configure the circuit transpilation job(s)
circuits = circuits if isinstance(circuits, list) else [circuits]
transpile_configs = _parse_transpile_args(circuits, backend, basis_gates, coupling_map,
backend_properties, initial_layout,
seed_transpiler, optimization_level,
pass_manager, callback, output_name)
# Check circuit width against number of qubits in coupling_map(s)
coupling_maps_list = list(config.coupling_map for config in transpile_configs)
for circuit, parsed_coupling_map in zip(circuits, coupling_maps_list):
# If coupling_map is not None
if isinstance(parsed_coupling_map, CouplingMap):
n_qubits = len(circuit.qubits)
max_qubits = parsed_coupling_map.size()
if n_qubits > max_qubits:
raise TranspilerError('Number of qubits ({}) '.format(n_qubits) +
'in {} '.format(circuit.name) +
'is greater than maximum ({}) '.format(max_qubits) +
'in the coupling_map')
# Transpile circuits in parallel
circuits = parallel_map(_transpile_circuit, list(zip(circuits, transpile_configs)))
if len(circuits) == 1:
return circuits[0]
return circuits
実機などでは、CNOTなどがつながっていないゲートがありますので、それらを割付ながら回路を作っていく働きをしているようです。
さらに、その計算自体がしんどいので、並列化して計算しています。
引数を省略しても、バックエンドから適宜、設定を取ってきてくれるのは、バックエンドにいっぱい設定を付け加えたおかげと言えるでしょう。
回路を変換して回路自身を返します。
assemble
を眺める
qiskit/compiler/assemble.pyにあるassemble
も、同様に眺めていきます。(docstringは省略)
def assemble(experiments,
backend=None,
qobj_id=None, qobj_header=None,
shots=1024, memory=False, max_credits=None, seed_simulator=None,
qubit_lo_freq=None, meas_lo_freq=None,
qubit_lo_range=None, meas_lo_range=None,
schedule_los=None, meas_level=2, meas_return='avg', meas_map=None,
memory_slot_size=100, rep_time=None, parameter_binds=None,
**run_config):
experiments = experiments if isinstance(experiments, list) else [experiments]
qobj_id, qobj_header, run_config_common_dict = _parse_common_args(backend, qobj_id, qobj_header,
shots, memory, max_credits,
seed_simulator, **run_config)
# assemble either circuits or schedules
if all(isinstance(exp, QuantumCircuit) for exp in experiments):
run_config = _parse_circuit_args(parameter_binds, **run_config_common_dict)
# If circuits are parameterized, bind parameters and remove from run_config
bound_experiments, run_config = _expand_parameters(circuits=experiments,
run_config=run_config)
return assemble_circuits(circuits=bound_experiments, qobj_id=qobj_id,
qobj_header=qobj_header, run_config=run_config)
elif all(isinstance(exp, ScheduleComponent) for exp in experiments):
run_config = _parse_pulse_args(backend, qubit_lo_freq, meas_lo_freq,
qubit_lo_range, meas_lo_range,
schedule_los, meas_level, meas_return,
meas_map, memory_slot_size, rep_time,
**run_config_common_dict)
return assemble_schedules(schedules=experiments, qobj_id=qobj_id,
qobj_header=qobj_header, run_config=run_config)
else:
raise QiskitError("bad input to assemble() function; "
"must be either circuits or schedules")
回路に対するアセンブルと、パルスのスケジュールに関するアセンブルができるようです。
返す値の型はQObj
で、回路の場合はQasmQObj
型が返ってきます。
QasmQobj
型がどういうデータ構造か分からないと、この後で困るので、
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')
tran = transpile(qc, backend=backend_sim)
asm = assemble(qc, backend=backend_sim)
のasm
を見ると、以下のようになっていました。
QasmQobj(
config=QasmQobjConfig(
memory=False,
memory_slots=2,
n_qubits=2,
parameter_binds=[],
shots=1024),
experiments=[
QasmQobjExperiment(
config=QasmQobjExperimentConfig(memory_slots=2, n_qubits=2),
header=QobjExperimentHeader(
clbit_labels=[['c', 0], ['c', 1]],
creg_sizes=[['c', 2]],
memory_slots=2,
n_qubits=2,
name='circuit0',
qreg_sizes=[['q', 2]],
qubit_labels=[['q', 0], ['q', 1]]),
instructions=[
QasmQobjInstruction(
name='u2', params=[0, 3.14159265358979], qubits=[0]),
QasmQobjInstruction(name='cx', qubits=[0, 1]),
QasmQobjInstruction(memory=[0], name='measure', qubits=[0]),
QasmQobjInstruction(memory=[1], name='measure', qubits=[1])
])
],
header=QobjHeader(backend_name='qasm_simulator', backend_version='2.0.0'),
qobj_id='06682d2e-bfd8-4dba-ba8e-4e46492c1609',
schema_version='1.1.0',
type='QASM')
特に問題なく読めるかと思います。ちなみにHゲートはu2(0, π)
に変換されています。これは等価な表現です。
backend.run
を見る
回路をQasmQobj
に変換したので、次は
return backend.run(qobj, **run_config)
を読んでいきます。qiskit/providers/basicaer/qasm_simulator.py
def run(self, qobj, backend_options=None):
"""Run qobj asynchronously.
Args:
qobj (Qobj): payload of the experiment
backend_options (dict): backend options
Returns:
BasicAerJob: derived from BaseJob
Additional Information:
backend_options: Is a dict of options for the backend. It may contain
* "initial_statevector": vector_like
The "initial_statevector" option specifies a custom initial
initial statevector for the simulator to be used instead of the all
zero state. This size of this vector must be correct for the number
of qubits in all experiments in the qobj.
Example::
backend_options = {
"initial_statevector": np.array([1, 0, 0, 1j]) / np.sqrt(2),
}
"""
self._set_options(qobj_config=qobj.config,
backend_options=backend_options)
job_id = str(uuid.uuid4())
job = BasicAerJob(self, job_id, self._run_job, qobj)
job.submit()
return job
オプションに初期ベクトルを入れられるのはいいですね。完全にバックエンド依存になってしまいそうですが。
chop_threshold
オプションは、状態ベクトルをシミュレートする際に、この値以下だと値を0
として扱うらしいです。
QasmSimulatorPy._set_options
を読む
_set_options
読んでいきます。
def _set_options(self, qobj_config=None, backend_options=None):
"""Set the backend options for all experiments in a qobj"""
# Reset default options
self._initial_statevector = self.DEFAULT_OPTIONS["initial_statevector"]
self._chop_threshold = self.DEFAULT_OPTIONS["chop_threshold"]
if backend_options is None:
backend_options = {}
# Check for custom initial statevector in backend_options first,
# then config second
if 'initial_statevector' in backend_options:
self._initial_statevector = np.array(backend_options['initial_statevector'],
dtype=complex)
elif hasattr(qobj_config, 'initial_statevector'):
self._initial_statevector = np.array(qobj_config.initial_statevector,
dtype=complex)
if self._initial_statevector is not None:
# Check the initial statevector is normalized
norm = np.linalg.norm(self._initial_statevector)
if round(norm, 12) != 1:
raise BasicAerError('initial statevector is not normalized: ' +
'norm {} != 1'.format(norm))
# Check for custom chop threshold
# Replace with custom options
if 'chop_threshold' in backend_options:
self._chop_threshold = backend_options['chop_threshold']
elif hasattr(qobj_config, 'chop_threshold'):
self._chop_threshold = qobj_config.chop_threshold
バックエンド自身にオプションを持たせている都合上
# Reset default options
self._initial_statevector = self.DEFAULT_OPTIONS["initial_statevector"]
self._chop_threshold = self.DEFAULT_OPTIONS["chop_threshold"]
がこんなところで出てきてしまっています。ともかく、ただ2つのオプションを解釈してqobjにセットしてるだけです。
BasicAerJob.__init__
を読む
job_id = str(uuid.uuid4())
job = BasicAerJob(self, job_id, self._run_job, qobj)
job_idとして適当なUUIDを降っています。
jobとしてBasicAerJob
を作っています。self._run_job
はメソッドですが、メソッドをそのまま渡しています。後で呼び出されたときに見ましょう。
qiskit/providers/basicaer/basicaerjob.py
class BasicAerJob(BaseJob):
"""BasicAerJob class.
Attributes:
_executor (futures.Executor): executor to handle asynchronous jobs
"""
if sys.platform in ['darwin', 'win32']:
_executor = futures.ThreadPoolExecutor()
else:
_executor = futures.ProcessPoolExecutor()
def __init__(self, backend, job_id, fn, qobj):
super().__init__(backend, job_id)
self._fn = fn
self._qobj = qobj
self._future = None
データ作っただけで、この時点では何もしていません。
Windows, Macではスレッドプールを、その他ではプロセスプールを使っていることは、若干気になりました。
PythonはマルチスレッドではCPUが複数あっても、1スレッドしか同時に動きません。
どう使われるのか気にしておきましょう。
BasicAerJob.submit
を読む
job.submit()
return job
jobをsubmitしてから、jobを返しています。BasicAerJob.submit
を読んでいきましょう。
def submit(self):
"""Submit the job to the backend for execution.
Raises:
QobjValidationError: if the JSON serialization of the Qobj passed
during construction does not validate against the Qobj schema.
JobError: if trying to re-submit the job.
"""
if self._future is not None:
raise JobError("We have already submitted the job!")
validate_qobj_against_schema(self._qobj)
self._future = self._executor.submit(self._fn, self._job_id, self._qobj)
validate_qobj_against_schema(self._qobj)
については、Qobjが、別ファイルに定義されているJSONスキーマの形式に合っているかを、外部ライブラリのjsonschema
を使って検証しているだけでした。あまり面白くないので省略します。
_executor
は、Python標準ライブラリconcurrent.futures
のThreadPoolExecutor
またはProcessPoolExecutor
でした。そのsubmit
メソッドを呼んでいます。
公式ドキュメントを読んでみましょう。
submit(fn, *args, **kwargs)
呼び出し可能オブジェクトfn
を、fn(*args **kwargs)
として実行するようにスケジュールし、呼び出し可能オブジェクトの実行を表現する Future オブジェクトを返します。
つまり、self._fn(self._job_id, self._qobj)
を実行するようにスケジュールするためのFuture
オブジェクトを作っていることになります。
このFuture
オブジェクトは非同期実行のためのもので、_executor
によって裏で動かされながら、計算が終わってなくてもとりあえず処理は進みます。
(ハンバーガー屋さんでハンバーガーを注文したら、出来上がるまでカウンターの前で待っていても構いませんが、番号札をもらえば、先に席を取るとか、別のことができて時間が有意義に過ごせます。通常の関数呼び出しだと、結果が返ってくるまで待つ必要がありますが、非同期実行では、返ってくるまで待たずに別のことができます。Future
オブジェクトは、将来、ハンバーガーを受け取るための番号札のようなものです)
QasmSimulatorPy._run_job
を読む
_executor
よって動かされるself._fn(self._job_id, self._qobj)
って何だったかを思い出すと、job = BasicAerJob(self, job_id, self._run_job, qobj)
で作ったself._run_job(job_id, qobj)
でした。
なのでQasmSimulatorPy._run_job
を読んでみましょう。
def _run_job(self, job_id, qobj):
"""Run experiments in qobj
Args:
job_id (str): unique id for the job.
qobj (Qobj): job description
Returns:
Result: Result object
"""
self._validate(qobj)
result_list = []
self._shots = qobj.config.shots
self._memory = getattr(qobj.config, 'memory', False)
self._qobj_config = qobj.config
start = time.time()
for experiment in qobj.experiments:
result_list.append(self.run_experiment(experiment))
end = time.time()
result = {'backend_name': self.name(),
'backend_version': self._configuration.backend_version,
'qobj_id': qobj.qobj_id,
'job_id': job_id,
'results': result_list,
'status': 'COMPLETED',
'success': True,
'time_taken': (end - start),
'header': qobj.header.to_dict()}
return Result.from_dict(result)
-
self._validate
(後で読みます) - バックエンド自体に必要な情報をセット
- タイマー開始
-
qobj.experiments
からexperiment
を1個ずつ取り出して-
result_list
に各experiment
をrun_experiment
(後で読みます)した結果を追加
-
-
- タイマー終了
- 結果を辞書に詰めて、
Result
型に変換して(後で読みます)返す
QasmSimulatorPy._validate
を読む
def _validate(self, qobj):
"""Semantic validations of the qobj which cannot be done via schemas."""
n_qubits = qobj.config.n_qubits
max_qubits = self.configuration().n_qubits
if n_qubits > max_qubits:
raise BasicAerError('Number of qubits {} '.format(n_qubits) +
'is greater than maximum ({}) '.format(max_qubits) +
'for "{}".'.format(self.name()))
for experiment in qobj.experiments:
name = experiment.header.name
if experiment.config.memory_slots == 0:
logger.warning('No classical registers in circuit "%s", '
'counts will be empty.', name)
elif 'measure' not in [op.name for op in experiment.instructions]:
logger.warning('No measurements in circuit "%s", '
'classical register will remain all zeros.', name)
qubit数が最大値を越えていないことの確認と、古典レジスタがあるか、測定が行われているかの確認をしています。
(古典レジスタ無し、測定無しはエラーではなく警告扱い)
QasmSimulatorPy.run_experiment
を眺める
その前に、experiments
が何でできていたか思い出しておきましょう。
experiments=[
QasmQobjExperiment(
config=QasmQobjExperimentConfig(memory_slots=2, n_qubits=2),
header=QobjExperimentHeader(
clbit_labels=[['c', 0], ['c', 1]],
creg_sizes=[['c', 2]],
memory_slots=2,
n_qubits=2,
name='circuit0',
qreg_sizes=[['q', 2]],
qubit_labels=[['q', 0], ['q', 1]]),
instructions=[
QasmQobjInstruction(
name='u2', params=[0, 3.14159265358979], qubits=[0]),
QasmQobjInstruction(name='cx', qubits=[0, 1]),
QasmQobjInstruction(memory=[0], name='measure', qubits=[0]),
QasmQobjInstruction(memory=[1], name='measure', qubits=[1])
])
],
QasmQobjExperiment
がひとつの量子回路に対応しています。Qiskitでは、量子回路をいくつも詰めて実行できるんですが、今回はひとつだけのようです。
for experiment in qobj.experiments:
のループを回していたので、各回路ごとにrun_experiment
を呼び出していることになります。回路にはinstructions
があり、これはゲートや測定に相当します。
run_experiment
は本当に長いので省略します。ざっくりとは、shots
の回数だけゲートの計算、測定の計算を行って、
return {'name': experiment.header.name,
'seed_simulator': seed_simulator,
'shots': self._shots,
'data': data,
'status': 'DONE',
'success': True,
'time_taken': (end - start),
'header': experiment.header.to_dict()}
のような結果を返します。
なお、data
は
# Add data
data = {'counts': dict(Counter(memory))}
# Optionally add memory list
if self._memory:
data['memory'] = memory
# Optionally add final statevector
if self.SHOW_FINAL_STATE:
data['statevector'] = self._get_statevector()
# Remove empty counts and memory for statevector simulator
if not data['counts']:
data.pop('counts')
if 'memory' in data and not data['memory']
のように、測定データなどをもたせています。
qiskit.result.Result
を読む
辞書型をreturn Result.from_dict(result)
によってResult
型にしています。
qiskit/result/result.py
@bind_schema(ResultSchema)
class Result(BaseModel):
"""Model for Results.
Please note that this class only describes the required fields. For the
full description of the model, please check ``ResultSchema``.
Attributes:
backend_name (str): backend name.
backend_version (str): backend version, in the form X.Y.Z.
qobj_id (str): user-generated Qobj id.
job_id (str): unique execution id from the backend.
success (bool): True if complete input qobj executed correctly. (Implies
each experiment success)
results (list[ExperimentResult]): corresponding results for array of
experiments of the input qobj
"""
def __init__(self, backend_name, backend_version, qobj_id, job_id, success,
results, **kwargs):
self.backend_name = backend_name
self.backend_version = backend_version
self.qobj_id = qobj_id
self.job_id = job_id
self.success = success
self.results = results
本来はfrom_dict
を読みたいのですが、qiskit-terraの特徴のひとつとして、JSONスキーマ周りの処理をすごくがんばっています。from_dict
は親クラスのBaseModel
で定義されていて、その中味が
@classmethod
def from_dict(cls, dict_):
"""Deserialize a dict of simple types into an instance of this class.
Note that this method requires that the model is bound with
``@bind_schema``.
"""
try:
data = cls.schema.load(dict_)
except ValidationError as ex:
raise ModelValidationError(
ex.messages, ex.field_name, ex.data, ex.valid_data, **ex.kwargs) from None
return data
と、とてもシンプルになっています。Result
クラスとは別にResultSchema
クラスを作って
class ResultSchema(BaseSchema):
"""Schema for Result."""
# Required fields.
backend_name = String(required=True)
backend_version = String(required=True,
validate=Regexp('[0-9]+.[0-9]+.[0-9]+$'))
qobj_id = String(required=True)
job_id = String(required=True)
success = Boolean(required=True)
results = Nested(ExperimentResultSchema, required=True, many=True)
# Optional fields.
date = DateTime()
status = String()
header = Nested(ObjSchema)
とする、など、かなりちゃんとやっています。(/qiskit/result/models.pyで定義)
ちゃんとは読めてないんですが、これらのデータ型になっていることを確認しながら、ExperimentResultSchema
などは再帰的にオブジェクトにしていくのではないかと思います。
いずれにせよ、データの詰め替えなので、今回は省略させてください。
手元で動かして返ってきたResult
は、次のようになっていました。
Result(
backend_name='qasm_simulator',
backend_version='2.0.0',
header=Obj(backend_name='qasm_simulator', backend_version='2.0.0'),
job_id='65b162ae-fe6b-480a-85ba-8c890d8bbf3b',
qobj_id='c75e9b2c-da7c-4dd6-96da-d132126e6dc0',
results=[
ExperimentResult(
data=ExperimentResultData(counts=Obj(0x0=493, 0x3=531)),
header=Obj(
clbit_labels=[['c', 0], ['c', 1]],
creg_sizes=[['c', 2]],
memory_slots=2,
n_qubits=2,
name='circuit0',
qreg_sizes=[['q', 2]],
qubit_labels=[['q', 0], ['q', 1]]),
meas_level=2,
name='circuit0',
seed_simulator=1480888590,
shots=1024,
status='DONE',
success=True,
time_taken=0.12355875968933105)
],
status='COMPLETED',
success=True,
time_taken=0.12369537353515625)
BasicAerJob.result()
を読む
相当、途中を飛ばしましたが、
result = execute(qc, backend_sim).result()
のexecute(qc, backend_sim)
が終わり、.result()
を見ていきます。
execute
はBasicAerJob
を返すんでしたね。
@requires_submit
def result(self, timeout=None):
# pylint: disable=arguments-differ
"""Get job result. The behavior is the same as the underlying
concurrent Future objects,
https://docs.python.org/3/library/concurrent.futures.html#future-objects
Args:
timeout (float): number of seconds to wait for results.
Returns:
qiskit.Result: Result object
Raises:
concurrent.futures.TimeoutError: if timeout occurred.
concurrent.futures.CancelledError: if job cancelled before completed.
"""
return self._future.result(timeout=timeout)
最初の@requires_submit
は、submit
をしたかどうかを見ています。
submit
したかどうかは、BasicAerJob
を作ったときにself._future = None
としていましたが、submit
するとself._future
にFuture
オブジェクトが入るので、None
になっていないかチェックをしたら分かります。
これは、普通にFuture
オブジェクトのresult
メソッドを呼び出しています。
公式ドキュメントを見ましょう。
result(timeout=None)
呼び出しによって返された値を返します。呼び出しがまだ完了していない場合、このメソッドは timeout 秒の間待機します。呼び出しが timeout 秒間の間に完了しない場合、 concurrent.futures.TimeoutError が送出されます。 timeout にはintかfloatを指定できます。timeout が指定されていないか、 None である場合、待機時間に制限はありません。
future が完了する前にキャンセルされた場合 CancelledError が送出されます。
呼び出しが例外を送出した場合、このメソッドは同じ例外を送出します。
終わっていたら結果を返して、終わってなかったら待ってから結果を返す、というメソッドです。
結果は、上で見たとおりResult
型のオブジェクトです。
Result.get_counts
を読む
とうとう最後の行です。
print(result.get_counts(qc))
を見ていきます。
def get_counts(self, experiment=None):
"""Get the histogram data of an experiment.
Args:
experiment (str or QuantumCircuit or Schedule or int or None): the index of the
experiment, as specified by ``get_data()``.
Returns:
dict[str:int]: a dictionary with the counts for each qubit, with
the keys containing a string in binary format and separated
according to the registers in circuit (e.g. ``0100 1110``).
The string is little-endian (cr[0] on the right hand side).
Raises:
QiskitError: if there are no counts for the experiment.
"""
exp = self._get_experiment(experiment)
try:
header = exp.header.to_dict()
except (AttributeError, QiskitError): # header is not available
header = None
if 'counts' in self.data(experiment).keys():
return postprocess.format_counts(self.data(experiment)['counts'],
header)
elif 'statevector' in self.data(experiment).keys():
vec = postprocess.format_statevector(self.data(experiment)['statevector'])
return state_to_counts(vec)
else:
raise QiskitError('No counts for experiment "{0}"'.format(experiment))
_get_experiment
では、回路名に対応したexperiment
を取り出しています。今回のようにexperiment
がひとつの場合は、experiment
引数を省略しても構いません。
experiment
からヘッダーが取り出せたら、古典レジスタの長さが分かるので、測定結果をいい感じにゼロ埋めできます。
format_counts
ではいい感じに文字列に変換して辞書に詰めるのをやっています。
まとめ
今回、Qiskitで、量子計算に直接関わるところを除いた処理の流れを読んでいきました。
度重なる変更や、将来を見越した拡張性などから、とてもしんどい仕様になっている部分もありましたが、かなりいろんなことができるように作られていて、また、トランスパイルやJSONへの変換など、Blueqatでは扱っていない部分がかなり重厚で、大変勉強になる、という印象を受けました。
全体の流れを読む、という目標は今回で終了ですが、今後もQiskitソースコードリーディングは続けていこうと思います。