LoginSignup
3
0

More than 3 years have passed since last update.

Qiskitソースコードリーディング 〜 Terra: バックエンドの取得、呼び出し、結果の取得を読む

Last updated at Posted at 2019-12-15

何をするの?

量子コンピューティングライブラリの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.pyBasicAerProviderを読んでいきます。

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.pyqiskit/providers/basicaer/statevector_simulator.pyqiskit/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とは何であったか

  • BasicAerBasicAerProviderのインスタンス
    • 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_nameqiskit/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-terraqiskit-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.configurationbackend.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.futuresThreadPoolExecutorまたは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に各experimentrun_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()を見ていきます。
executeBasicAerJobを返すんでしたね。

    @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._futureFutureオブジェクトが入るので、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ソースコードリーディングは続けていこうと思います。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0