Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

qmc.controlによるゲートと量子カーネルの制御

タグ: tutorial

qmc.controlを使うと、Qamomileの任意のゲート(qmc.rxのようなビルトイン関数や、ユーザが書いた@qmc.qkernel)の制御版を作れます。

qmc.controlには2つのモードがあります。concrete modeは制御量子ビットの数をPythonのintで与え、symbolic modeqmc.UIntの量子カーネルパラメータ(あるいはそれを含む式)で与えてtranspile時に解決します。power=、デフォルト引数、Vector[Qubit]を取る量子カーネル、古典kwargの並び替えなど大半の機能は両モードで同じ挙動です。モードによって違うのは制御引数の渡し方と一部の追加機能だけで、以降のセクションで分けて扱います。

# Install the latest Qamomile from pip.
# !pip install qamomile
import math

import qamomile.circuit as qmc
from qamomile.circuit.transpiler.errors import UnreturnedBorrowError
from qamomile.qiskit import QiskitTranspiler

transpiler = QiskitTranspiler()

1. 最小例: controlled-RX

qmc.controlの最も簡単かつ実用的な使い方は、Qamomileで用意されている1つのゲートを制御化することです。例えば以下では、1量子ビットゲートのqmc.rx(q, angle)qmc.controlに渡して、2量子ビットのcontrolled-RXゲートを得ています。

# 制御RXゲートを定義します。
crx = qmc.control(qmc.rx)
@qmc.qkernel
def crx_control_off() -> qmc.Bit:
    c = qmc.qubit(name="c")
    t = qmc.qubit(name="t")
    # 制御は|0>のままなので、制御回転は発火しません。
    c, t = crx(c, t, angle=math.pi)
    return qmc.measure(t)


crx_control_off.draw()
<Figure size 798x200 with 1 Axes>
@qmc.qkernel
def crx_control_on() -> qmc.Bit:
    c = qmc.qubit(name="c")
    t = qmc.qubit(name="t")
    # 制御を|1>に立てるので、制御回転が発火します。
    c = qmc.x(c)
    c, t = crx(c, t, angle=math.pi)
    return qmc.measure(t)


crx_control_on.draw()
<Figure size 893x200 with 1 Axes>

制御が実際に効いていることを確かめるために、両方の量子カーネルをQiskitにtranspileしてsimulatorで実行し、targetの測定結果を確認します。angle=math.piではRX(pi)が|0>を|1>に写すので、制御が|1>のときだけtargetは全shotで|1>になり、それ以外では|0>のままになります。

off_counts = dict(
    transpiler.transpile(crx_control_off)
    .sample(transpiler.executor(), shots=256)
    .result()
    .results
)
on_counts = dict(
    transpiler.transpile(crx_control_on)
    .sample(transpiler.executor(), shots=256)
    .result()
    .results
)
print("control |0> ->", off_counts)
assert off_counts == {0: 256}
print("control |1> ->", on_counts)
assert on_counts == {1: 256}
control |0> -> {0: 256}
control |1> -> {1: 256}

ポイントとして、

  • crx = qmc.control(qmc.rx)はqkernelの中でも外でもどちらに書いてもかまいません。返ってきたものは再利用可能な値なので、変数に置いて何度でも呼び出せます。

  • crx(c, t, angle=...)を呼ぶと、まず制御量子ビットがpositional引数として並び、次にtarget、最後に古典keyword引数が続きます。順序は制御化する対象のqmc.rx(q, angle)シグネチャを踏襲しつつ、先頭に制御を加えた形です。

  • 古典パラメータのkeyword名は制御化する対象の関数の名前をそのまま使います(qmc.rxならangleqmc.pならthetaなど)。qmc.controlが改名することはありません。

2. 2つのモードの概要

qmc.controlには2つのモードがあります。どちらかはnum_controlsに渡す型だけで決まります。Pythonのintならconcrete modeqmc.UIntハンドル(あるいはn - 1のようなUInt式)ならsymbolic modeです。その他の挙動はすべてこの選択から決まります。

項目ConcreteSymbolic
num_controls=Pythonのint(デフォルト1)qmc.UIntハンドル、またはUInt
制御引数合計量子ビット数がnum_controlsに一致する1つ以上のpositional引数(QubitVectorViewVector[Qubit])1つのpositionalなVector[Qubit] / VectorViewpool(single-pool形、任意でcontrol_indices)、またはQubit / VectorView / Vector[Qubit]を混ぜた複数のpositional引数
control_indices受け付けない任意。poolのどの量子ビットがactiveかを指定
制御数が解決される時点qmc.control(...)が評価された時(module load時かtracing時)transpile時(bindingsから)

qmc.controlのほとんどの機能(power=、デフォルト値、古典kwargの並び替え、Vector[Qubit]を受け取る量子カーネル、multi-argの制御引数形など)は両モードで同じ挙動を示します。これらは3. 両モードで動作するパターンでまとめます。4. concrete専用: 単一のscalar制御はconcrete modeを必要とする唯一の形を、5. Symbolic modeのパターンはsymbolic mode固有の機能を扱います。

3. 両モードで動作するパターン

本セクションの各機能は、どちらのモードでも同じ挙動を示します。以下では原則concrete modeを使いますが、同じ機能がsymbolic modeでも利用可能です。concrete modeと違うのはnum_controlsUInt式であることと、量子ビット数の一致がtranspile時にチェックされることだけです。symbolic専用の引数形は5. Symbolic modeのパターンで扱います。

3.1 任意のcallableを制御化

qmc.controlはビルトインのゲート関数(qmc.rxqmc.hqmc.pなど)も、ユーザ定義の@qmc.qkernelも同様に受け付けます。以下の例では、qmc.hを制御化したchとユーザ定義の_h_then_rxを制御化したcgの2つの制御演算を含む量子カーネルを扱います。

@qmc.qkernel
def _h_then_rx(q: qmc.Qubit, theta: qmc.Float) -> qmc.Qubit:
    q = qmc.h(q)
    q = qmc.rx(q, theta)
    return q


ch = qmc.control(qmc.h)
cg = qmc.control(_h_then_rx)
@qmc.qkernel
def control_any_callable_demo() -> qmc.Vector[qmc.Bit]:
    # q[0]は共通の制御。q[1] / q[2]は2つのtarget。
    q = qmc.qubit_array(3, "q")
    q[0] = qmc.x(q[0])
    q[0], q[1] = ch(q[0], q[1])
    q[0], q[2] = cg(q[0], q[2], theta=math.pi / 4)
    return qmc.measure(q)


control_any_callable_demo.draw()
<Figure size 1109x256 with 1 Axes>

3.2 Vector[Qubit]を受け取る量子カーネル

制御化する対象の量子カーネルは引数としてVector[Qubit]を取ることができます。呼び出し側は長さの一致するVectorまたはVectorViewを渡します。

@qmc.qkernel
def _vec_h(qs: qmc.Vector[qmc.Qubit]) -> qmc.Vector[qmc.Qubit]:
    qs[0] = qmc.h(qs[0])
    qs[1] = qmc.h(qs[1])
    return qs


cg = qmc.control(_vec_h, num_controls=1)
@qmc.qkernel
def vec_target_demo() -> qmc.Vector[qmc.Bit]:
    qs = qmc.qubit_array(3, "qs")
    qs[0] = qmc.x(qs[0])
    qs[0], qs[1:3] = cg(qs[0], qs[1:3])
    return qmc.measure(qs)


vec_target_demo.draw()
<Figure size 771.5x256 with 1 Axes>

3.3 制御化する対象量子カーネルのシグネチャ由来のデフォルト値

制御化する対象の量子カーネルが古典パラメータにPythonのデフォルト値を宣言している場合、呼び出し側ではそのkeywordを省略することも可能です。

@qmc.qkernel
def _phase(
    q: qmc.Qubit,
    # runtime: ``_create_bound_input`` は ``qmc.Float`` 引数のデフォルトに
    # 生の ``float`` を受けて定数 ``Float`` handle に wrap する。静的シグネチャは
    # ハンドル型のままにしておくため、デフォルトリテラルが ``Float`` インスタンス
    # でないことを ``# type: ignore`` で許容する。
    theta: qmc.Float = math.pi / 2,  # type: ignore[assignment]
) -> qmc.Qubit:
    return qmc.rx(q, theta)


cg = qmc.control(_phase)
@qmc.qkernel
def default_arg_demo() -> qmc.Bit:
    c = qmc.qubit(name="c")
    t = qmc.qubit(name="t")
    c = qmc.x(c)
    c, t = cg(c, t)  # thetaはデフォルトのmath.pi / 2が入る
    return qmc.measure(t)


default_arg_demo.draw()
<Figure size 886x200 with 1 Axes>
# 同じ`_phase`量子カーネルを、今度はsymbolicな`num_controls=n - 1`で制御化
# します。呼び出し側が`theta`を名指ししなくても`theta=math.pi / 2`の
# デフォルトはそのまま適用されます。別の角度を使いたいがkwargには
# 切り替えたくない場合は、省略した`theta`をcallsiteのpositional上書き
# (`cg(q[0 : n - 1], q[n - 1], math.pi / 4)`)に置き換えます。
@qmc.qkernel
def default_arg_demo_symbolic(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(n, "q")
    q[0 : n - 1] = qmc.x(q[0 : n - 1])  # 制御量子ビットを全て|1>にする
    cg = qmc.control(_phase, num_controls=n - 1)  # 制御量子ビット数をsymbolicに指定
    q[0 : n - 1], q[n - 1] = cg(q[0 : n - 1], q[n - 1])
    return qmc.measure(q)


default_arg_demo_symbolic.draw(n=3, fold_loops=False)
<Figure size 886x256 with 1 Axes>

3.4 power=U^kを制御

power=kを渡すと、Uそのものではなくk乗U^kが制御されます。powerはPythonのint(コンパイル時に解決)もqmc.UIntハンドル(bindingsからtranspile時に解決)も受け取り、num_controlsがconcreteかsymbolicかに関係なく動作します。

cg = qmc.control(qmc.rx)  # num_controls = 1 (concrete)
@qmc.qkernel
def power_demo_concrete() -> qmc.Bit:
    c = qmc.qubit(name="c")
    t = qmc.qubit(name="t")
    c = qmc.x(c)
    c, t = cg(c, t, angle=math.pi / 4, power=3)  # powerはPythonのint
    return qmc.measure(t)


power_demo_concrete.draw()
<Figure size 925.5x200 with 1 Axes>

3.5 制御引数を別々のpositionalで渡す(CCXスタイル)

num_controls=2にすると、呼び出し側では各制御量子ビットをそれぞれ独立したpositional引数としてtargetの前に並べます。以下は典型的なCCX(Toffoli)で、2つの制御c0c1と1つのtargettを渡しています。同じパターンはnum_controls=3(CCCX)やnum_controls=4にも拡張でき、渡したいQubitnum_controlsで指定した数だけあれば成立します。

ccx = qmc.control(qmc.x, num_controls=2)
@qmc.qkernel
def toffoli_demo() -> qmc.Bit:
    c0 = qmc.qubit(name="c0")
    c1 = qmc.qubit(name="c1")
    t = qmc.qubit(name="t")
    c0 = qmc.x(c0)
    c1 = qmc.x(c1)
    c0, c1, t = ccx(c0, c1, t)
    return qmc.measure(t)


toffoli_demo.draw()
<Figure size 690.5x256 with 1 Axes>

3.6 scalar QubitとVectorViewの制御を混ぜる

positional引数で渡す制御量子ビットは、合計量子ビット数がnum_controlsと一致する限り、scalarなQubitVectorViewVector[Qubit]を自由に混ぜられます。以下ではnum_controls=3のcontrolled-Hに対し、3つの制御をqs[0](scalar Qubit、1量子ビット)とqs[1:3](VectorView、2量子ビット)で渡しています。

cg = qmc.control(qmc.h, num_controls=3)
@qmc.qkernel
def mixed_controls_demo() -> qmc.Vector[qmc.Bit]:
    qs = qmc.qubit_array(5, "qs")
    qs[0] = qmc.x(qs[0])
    qs[1] = qmc.x(qs[1])
    qs[2] = qmc.x(qs[2])
    qs[0], qs[1:3], qs[3] = cg(qs[0], qs[1:3], qs[3])
    return qmc.measure(qs)


mixed_controls_demo.draw()
<Figure size 702.5x416 with 1 Axes>

4. concrete専用: 単一のscalar制御

制御引数の形はほぼすべて両モードで動き(3. 両モードで動作するパターン)、symbolic modeはさらに固有の機能を持ちます(5. Symbolic modeのパターン)。concrete modeを必要とする唯一の形は、単一のscalar Qubitを制御にするケースです。symbolic modeでは単一の制御引数はpoolの形と解釈されVector / VectorViewが要求されます。そもそも制御数が1に固定されている制御をsymbolicにする理由がないためです。これは1. 最小例: controlled-RXの最小controlled-RXとまったく同じ形で、以下のcontrolled-X(CNOT)も同じ単一scalar制御の形です。

cx = qmc.control(qmc.x)  # num_controlsはデフォルトの1(concrete)
@qmc.qkernel
def cnot_demo() -> qmc.Bit:
    c = qmc.qubit(name="c")
    t = qmc.qubit(name="t")
    c = qmc.x(c)  # 制御を|1>に立ててXを発火させる
    c, t = cx(c, t)
    return qmc.measure(t)


cnot_demo.draw()
<Figure size 690.5x200 with 1 Axes>

5. Symbolic modeのパターン

本セクションはnum_controlsqmc.UIntハンドル(またはn - 1のようなUInt式)のときのパターン、symbolic modeについてみていきます。制御量子ビットの数はqmc.control(..., num_controls=...)の評価時ではなく、bindingsからtranspile時に決まります。また、5.5 Multi-argの制御prefixでは3.5 制御引数を別々のpositionalで渡す(CCXスタイル) / 3.6 scalar QubitとVectorViewの制御を混ぜるのsymbolic版を確認します。

制御量子ビットの渡し方としては以下の2種類があります。

control_indiceskeywordはsymbolic mode専用で、single-poolの引数のどの量子ビットがactiveな制御として実際に配線されるかを指定します(残りはそのまま素通りします)。control_indicesはsingle-poolの形でのみ有効で、multi-argの形と組み合わせるとrejectされます。

5.1 num_controls = nでpool全体を制御に

最もシンプルなsymbolicの形としてnum_controls=nとしてpool(長さn)全体を制御として使います。パラメータnbindingsからtranspile時に具体化されます。

@qmc.qkernel
def symbolic_pool(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    ctrls = qmc.qubit_array(n, "ctrls")
    tgt = qmc.qubit(name="tgt")
    ctrls = qmc.x(ctrls)  # 制御を全て|1>にする
    cg = qmc.control(qmc.x, num_controls=n)  # シンボリックなnを制御数に指定
    ctrls, tgt = cg(ctrls, tgt)
    return qmc.measure(ctrls)


symbolic_pool.draw(n=3, fold_loops=False)
<Figure size 738.5x336 with 1 Axes>

5.2 n - 1の典型的なmulti-controlled形

multi-controlled-X設計で頻出する形で、レジスタの最初のn - 1量子ビットを制御に、最後の1量子ビットをtargetにします。num_controlsの値はsymbolic式のn - 1で、制御引数はスライスqs[0:n - 1]です。

@qmc.qkernel
def mcx_demo(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    qs = qmc.qubit_array(n, "qs")
    qs[0 : n - 1] = qmc.x(qs[0 : n - 1])  # 制御部分を全て|1>にする
    mcx = qmc.control(qmc.x, num_controls=n - 1)
    qs[0 : n - 1], qs[n - 1] = mcx(qs[0 : n - 1], qs[n - 1])
    return qmc.measure(qs)


mcx_demo.draw(n=4, fold_loops=False)
<Figure size 702.5x336 with 1 Axes>

5.3 control_indicesでsubsetを選ぶ

制御poolがactiveな制御数より広い場合、control_indiceskeyword(symbolic mode専用)でpoolのどの量子ビットを制御として使うかを指定します。残りの量子ビットには触りません。indexは連続である必要はありません。

@qmc.qkernel
def subset_pool(n: qmc.UInt, k_ctrls: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    pool = qmc.qubit_array(n, "pool")
    tgt = qmc.qubit(name="tgt")
    pool[0] = qmc.x(pool[0])
    pool[1] = qmc.x(pool[1])
    pool[3] = qmc.x(pool[3])  # pool[2]は|0>のまま。これがinactiveな量子ビット。
    cg = qmc.control(qmc.x, num_controls=k_ctrls)
    pool, tgt = cg(pool, tgt, control_indices=[0, 1, 3])
    return qmc.measure(pool)


subset_pool.draw(n=4, k_ctrls=3)
<Figure size 726.5x416 with 1 Axes>

5.4 control_indicesUInt式を含める

control_indicesの各エントリはPythonのintリテラル、qmc.UIntでもUInt値による算術式のいずれでも構いません。リテラルintエントリに対する軽い構造チェック(bool、負値、リテラルint同士の重複の拒否)はcompose時に行われますが、それ以外、すなわちnum_controlsとの長さ整合、pool sizeに対する範囲、UIntの値解決を必要とするチェックはtranspile時、bindingsからパラメータが具体化されてからに先送りされます。

@qmc.qkernel
def subset_pool_with_uint(n: qmc.UInt, k_ctrls: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    pool = qmc.qubit_array(n, "pool")
    tgt = qmc.qubit(name="tgt")
    pool[0] = qmc.x(pool[0])
    pool[1] = qmc.x(pool[1])
    pool[3] = qmc.x(pool[3])
    cg = qmc.control(qmc.x, num_controls=k_ctrls)
    pool, tgt = cg(pool, tgt, control_indices=[0, 1, n - 1])
    return qmc.measure(pool)


subset_pool_with_uint.draw(n=4, k_ctrls=3)
<Figure size 726.5x416 with 1 Axes>

5.5 Multi-argの制御prefix

制御を複数のpositional引数に分けたい場合、(典型的には「同じVectorのいくつかの量子ビットをactiveな制御に、別の量子ビットをtargetにしたい」場合)symbolic modeでもconcrete modeと同じmulti-argの形(3.5 制御引数を別々のpositionalで渡す(CCXスタイル) / 3.6 scalar QubitとVectorViewの制御を混ぜる)が使えます。同じVector[Qubit]から複数の量子ビットを取り出しても、互いにdisjoint(重ならない)な量子ビットであれば制御prefixに並べられます。制御prefixの各引数の量子ビット数の合計が、transpile時にnum_controlsと照合されます。

なお、control_indicesはmulti-argの形では使えません(6. rejectされるパターンとedge caseのreject caseを参照)。subset選択が必要ならsingle-poolの形(5.3 control_indicesでsubsetを選ぶ / 5.4 control_indicesUInt式を含める)、multi-argの自由度が必要ならprefix全体をactiveとして使うかのどちらかを選んでください。

@qmc.qkernel
def controlled_increment_demo(
    n: qmc.UInt, control_index: qmc.UInt
) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(n, "q")
    q[control_index] = qmc.x(q[control_index])
    # ``q.shape[0]`` は ``int | UInt`` 型なので、`n` パラメータ (狭い
    # ``qmc.UInt``) に再代入すると型が広がる。ローカル名で受ける。
    n_shape = q.shape[0]
    for k in qmc.range(n_shape - 1):
        target_idx = n - 2 - k
        ctrl_main = q[control_index]
        prefix = q[0:target_idx]
        tgt = q[target_idx]
        cg = qmc.control(qmc.x, num_controls=target_idx + 1)
        ctrl_main, prefix, tgt = cg(ctrl_main, prefix, tgt)
        q[control_index] = ctrl_main
        q[0:target_idx] = prefix
        q[target_idx] = tgt
    return qmc.measure(q)


controlled_increment_demo.draw(n=4, control_index=3, fold_loops=False)
<Figure size 880.5x336 with 1 Axes>

6. rejectされるパターンとedge case

本章ではrejectされる呼び出し形を1つずつ見ていきます。最後に、nested blockをgateへ変換できるbackend(Qiskitなど)でsupportedな、sliced-QFTのconcrete caseもregression例として確認します。

ケースモード例外
6.1 制御量子ビット数が引数境界をまたぐconcreteValueError
6.2 concrete modeでcontrol_indicesconcreteValueError
6.3 concrete modeでsymbolic長のVectorViewconcreteNotImplementedError
6.4 同じpool量子ビットをtargetに再利用symbolicUnreturnedBorrowError
6.5 multi-arg制御prefix + control_indicessymbolicValueError
6.6 symbolic modeで単一scalar制御symbolicValueError
6.7 sub-kernel内のUInt sliceに対するcontrolled QFTconcreteblock-to-gate変換が成功する場合はsupported
def expect_error(label: str, exc_type: type[BaseException], body) -> None:
    """``body``が``exc_type``の例外を投げることをassertします。

    ヘルパは*想定*の例外クラスだけをcatchします。それ以外の
    例外はそのまま伝播するので、別の例外型に変わってしまうような
    regressionはnotebook上で通常のtracebackとして見えます。
    例外が一度も発生しなかった場合は``AssertionError``を投げます。
    """
    try:
        body()
    except exc_type as exc:
        print(f"[{type(exc).__name__}] {label}: {exc}")
        return
    raise AssertionError(
        f"{label}: expected {exc_type.__name__}, but no exception was raised"
    )

6.1 制御量子ビット数が引数境界をまたぐ (concrete)

concrete modeはpositional引数を順に確認して、各引数を制御リストに畳み込むということを累計がnum_controlsに達するまで続けます。VectorViewVectorが与えられ、そこまでの累計の量子ビット数がnum_controlsを超えるような場合にはエラーが起きます。

def case_count_mismatch() -> None:
    @qmc.qkernel
    def kernel() -> qmc.Bit:
        qs = qmc.qubit_array(6, "qs")
        cg = qmc.control(qmc.x, num_controls=3)
        view, t = cg(qs[0:5], qs[5])  # 5量子ビット渡しているが3expected
        qs[0:5] = view
        return qmc.measure(qs[5])

    _ = kernel.block


expect_error("control count mismatch", ValueError, case_count_mismatch)
[ValueError] control count mismatch: concrete num_controls=3: positional argument #0 would push the control qubit count from 0 to 5, crossing the control / sub-kernel boundary mid-argument.  Split the argument so the boundary falls between args.

6.2 concrete modeでcontrol_indices (concrete)

control_indicesは選択元となる制御poolがある時にだけ意味を持つsymbolic modeの専用の引数です。concreteなnum_controlsと一緒に渡すと、compose時にValueErrorになります。

def case_control_indices_in_concrete() -> None:
    @qmc.qkernel
    def kernel() -> qmc.Bit:
        c = qmc.qubit(name="c")
        t = qmc.qubit(name="t")
        cg = qmc.control(qmc.x)  # num_controlsはデフォルトの1 (concrete)
        c, t = cg(c, t, control_indices=[0])
        return qmc.measure(t)

    _ = kernel.block


expect_error(
    "control_indices in concrete mode",
    ValueError,
    case_control_indices_in_concrete,
)
[ValueError] control_indices in concrete mode: control_indices is only valid in symbolic mode (num_controls=UInt).  Got concrete num_controls; concrete-mode controls are positional and have no selection step (see design §1.1).

6.3 concrete modeでsymbolic長のVectorView (concrete)

concrete modeは各制御引数の量子ビット数をコンパイル時に決定する必要があります。長さがUIntに依存するスライスはconcrete modeでは未対応で、NotImplementedErrorになります。

def case_symbolic_view_in_concrete() -> None:
    @qmc.qkernel
    def kernel(m: qmc.UInt) -> qmc.Bit:
        qs = qmc.qubit_array(m, "qs")
        cg = qmc.control(qmc.x, num_controls=3)
        view, q_out = cg(qs[0:m], qs[m - 1])
        qs[0:m] = view
        qs[m - 1] = q_out
        return qmc.measure(qs[m - 1])

    _ = kernel.block


expect_error(
    "symbolic-length VectorView in concrete mode",
    NotImplementedError,
    case_symbolic_view_in_concrete,
)
[NotImplementedError] symbolic-length VectorView in concrete mode: concrete num_controls with a symbolic-length Vector / VectorView control is not yet implemented in the frontend (tracked under Step 2.b of the controlled-API redesign).

6.4 同じpool量子ビットをtargetに再利用 — single-poolの形 (symbolic)

single-poolの形(cg(pool, ...)control_indicesを組み合わせる場合)で、pool内のinactiveな量子ビットを取り出してtargetとして渡したくなることがあります。例えばcg(pool, pool[2], control_indices=[0, 1, 3])としてpool[2]をcontrolled-Uのtargetにする、といった形です。この呼び出しはlinear typeのborrow trackerによってrejectされます。poolが1引数として消費されている最中にpool[2]が別引数として借り出されるため、compose時にUnreturnedBorrowErrorとして表面化します。

Workaround(推奨順):

  1. Multi-arg symbolicの形(5.5 Multi-argの制御prefix)。 各量子ビットまたはsub-viewを別々のpositional引数として渡します。cg(pool[0], pool[1], pool[3], pool[2])(またはcontrolled-incrementの例のようにscalar / sliceを混ぜる形)。各引数はpoolからの別borrowで、borrow trackerがdisjointnessをcheckし、num_controlsはtranspile時に量子ビット数の合計と照合されます。

  2. Concrete mode(3.6 scalar QubitとVectorViewの制御を混ぜる)。 num_controlsがPythonのintなら、同じmulti-argの形がsymbolicの仕組みなしでそのまま動きます。

def case_pool_slot_as_target() -> None:
    @qmc.qkernel
    def kernel(n: qmc.UInt, k_ctrls: qmc.UInt) -> qmc.Vector[qmc.Bit]:
        pool = qmc.qubit_array(n, "pool")
        cg = qmc.control(qmc.x, num_controls=k_ctrls)
        pool, q = cg(pool, pool[2], control_indices=[0, 1, 3])
        pool[2] = q
        return qmc.measure(pool)

    _ = kernel.block


expect_error(
    "same-pool slot reused as target",
    UnreturnedBorrowError,
    case_pool_slot_as_target,
)
[UnreturnedBorrowError] same-pool slot reused as target: Array 'pool' has unreturned borrowed elements.
Borrowed elements: pool[2]

Fix: Write back all borrowed elements before using the array:
  q = pool[i]
  q = qm.h(q)
  pool[i] = q  # Return the element

6.5 Multi-arg制御prefix + control_indices (symbolic)

symbolic modeの2つの機能は相互排他です。control_indicesは単一の制御pool(Vector引数1つ)に対してのみ意味を持ち、複数のpositional制御引数と組み合わせるとcompose時にValueErrorがraiseされます。

def case_multi_arg_with_control_indices() -> None:
    @qmc.qkernel
    def kernel(n: qmc.UInt, k: qmc.UInt) -> qmc.Vector[qmc.Bit]:
        q = qmc.qubit_array(n, "q")
        ctrl_main = q[0]
        prefix = q[1:k]
        tgt = q[k]
        cg = qmc.control(qmc.x, num_controls=k + 1)
        ctrl_main, prefix, tgt = cg(ctrl_main, prefix, tgt, control_indices=[0, 1, 2])
        q[0] = ctrl_main
        q[1:k] = prefix
        q[k] = tgt
        return qmc.measure(q)

    _ = kernel.block


expect_error(
    "multi-arg + control_indices",
    ValueError,
    case_multi_arg_with_control_indices,
)
[ValueError] multi-arg + control_indices: control_indices is only supported with a single Vector[Qubit] / VectorView[Qubit] control argument (the pool form).  Combining control_indices with multiple positional control args is not supported.

6.6 symbolic modeで単一scalar制御 (symbolic)

単一のscalar Qubit制御は、concrete modeが必要になる唯一の形です。symbolic modeでは単一の制御引数はsingle-poolの形と解釈され、Vector / VectorViewが要求されます。

def case_single_scalar_control_symbolic() -> None:
    @qmc.qkernel
    def kernel(n: qmc.UInt) -> qmc.Bit:
        c = qmc.qubit(name="c")
        t = qmc.qubit(name="t")
        cg = qmc.control(qmc.rx, num_controls=n)
        c, t = cg(c, t, angle=math.pi)
        return qmc.measure(t)

    _ = kernel.block


expect_error(
    "single scalar control in symbolic mode",
    ValueError,
    case_single_scalar_control_symbolic,
)
[ValueError] single scalar control in symbolic mode: When num_controls is symbolic (UInt), a single control argument must be a Vector[Qubit] / VectorView pool, not a scalar Qubit.  A single fixed scalar control has no symbolic meaning (the count is one), so use concrete mode instead -- e.g. qmc.control(gate) with the default num_controls=1.  To keep a scalar control in symbolic mode, pass it together with at least one more control argument (the multi-arg form).

6.7 sub-kernel内のUInt sliceに対するcontrolled QFT (concrete)

制御対象のsub-kernelが、呼び出し側でサイズの分かっているVector[Qubit]引数全体にqmc.qft / qmc.iqftを適用する形は使えます。例えばapply_qft(q)q全体にQFTを適用するなら、controlled_qft = qmc.control(apply_qft)という形は動作します。

下のようにsub-kernelが古典UInt引数を受け取り、その値でq[:m]を作ってからQFTを呼ぶ、より狭い形もQiskit-backedな経路では動作します。Qamomileはborrow checkの後、nested controlled block内のslice markerを取り除くため、block-to-gate変換が成功するcontrolled-U emitterはsliced composite blockをlowerできます。一方で、block-to-gate変換を持たないbackendでは、このmulti-target fallbackをまだrejectすることがあります。

def case_controlled_qft_over_uint_slice() -> None:
    @qmc.qkernel
    def qft_prefix(q: qmc.Vector[qmc.Qubit], m: qmc.UInt) -> qmc.Vector[qmc.Qubit]:
        prefix = q[:m]
        prefix = qmc.qft(prefix)
        q[:m] = prefix
        return q

    @qmc.qkernel
    def kernel(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
        q = qmc.qubit_array(n + 1, "q")
        q[0] = qmc.x(q[0])
        controlled_qft = qmc.control(qft_prefix)
        q[0], targets = controlled_qft(q[0], q[1 : n + 1], n)
        q[1 : n + 1] = targets
        return qmc.measure(q)

    transpiler.transpile(kernel, bindings={"n": 2})


case_controlled_qft_over_uint_slice()

7. まとめ

qmc.control(fn, num_controls=...)を使うことでQamomileのビルトインゲートやユーザ定義の量子カーネルを制御化することができます。qmc.controlには二つのモードがあり、そのモードはnum_controlsの型で決まります。Pythonのintであればconcrete modeqmc.UInt(またはn - 1のようなUInt式)ならsymbolic modeです。