特定のフォーマット済み回線をAzure Quantumに送信する方法

qdk.azure Python モジュールを使用して、特定の形式の回線をAzure Quantum サービスに送信する方法について説明します。 この記事では、回線を次の形式で送信する方法について説明します。

詳細については、量子回路に関するページを参照してください。

前提条件

Visual Studio Code (VS Code) で回線を開発して実行するには、次のものが必要です。

新しい Jupyter ノートブックを作成し、Quantum ワークスペースに接続する

VS Code の Jupyter ノートブックでワークスペースに接続するには、次の手順に従います。

  1. VS Code で、[ 表示 ] メニューを開き、[ コマンド パレット] を選択します。

  2. Create: New Jupyter Notebook」と入力します。 空のJupyter Notebook ファイルが新しいタブで開きます。

  3. ノートブックの最初のセルで、次のコードを実行します。 リソース ID は、Azure ポータルのワークスペースの Overview ペインにあります。

    from qdk.azure import Workspace
    
    workspace = Workspace (resource_id="") # Add your resource ID 
    

QIR形式で回路を送信する

量子中間表現 (QIR) は、量子プログラミング言語とターゲット量子計算プラットフォーム間の共通インターフェイスとして機能する中間表現です。 詳細については、「量子中間表現」を参照してください。

QIR 形式の回線を送信するには、次の手順に従います。

  1. QIR 回線を作成します。 たとえば、新しいセルで次のコードを実行して、単純なエンタングルメント回路を作成します。

    QIR_routine = """%Result = type opaque
    %Qubit = type opaque
    
    define void @ENTRYPOINT__main() #0 {
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
      call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*))
      call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*))
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*))
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 3 to %Qubit*))
      call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 3 to %Qubit*))
      call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) #1
      call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) #1
      call void @__quantum__rt__tuple_record_output(i64 2, i8* null)
      call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
      call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null)
      ret void
    }
    
    declare void @__quantum__qis__ccx__body(%Qubit*, %Qubit*, %Qubit*)
    declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
    declare void @__quantum__qis__cy__body(%Qubit*, %Qubit*)
    declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*)
    declare void @__quantum__qis__rx__body(double, %Qubit*)
    declare void @__quantum__qis__rxx__body(double, %Qubit*, %Qubit*)
    declare void @__quantum__qis__ry__body(double, %Qubit*)
    declare void @__quantum__qis__ryy__body(double, %Qubit*, %Qubit*)
    declare void @__quantum__qis__rz__body(double, %Qubit*)
    declare void @__quantum__qis__rzz__body(double, %Qubit*, %Qubit*)
    declare void @__quantum__qis__h__body(%Qubit*)
    declare void @__quantum__qis__s__body(%Qubit*)
    declare void @__quantum__qis__s__adj(%Qubit*)
    declare void @__quantum__qis__t__body(%Qubit*)
    declare void @__quantum__qis__t__adj(%Qubit*)
    declare void @__quantum__qis__x__body(%Qubit*)
    declare void @__quantum__qis__y__body(%Qubit*)
    declare void @__quantum__qis__z__body(%Qubit*)
    declare void @__quantum__qis__swap__body(%Qubit*, %Qubit*)
    declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1
    declare void @__quantum__rt__result_record_output(%Result*, i8*)
    declare void @__quantum__rt__array_record_output(i64, i8*)
    declare void @__quantum__rt__tuple_record_output(i64, i8*)
    
    attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="4" "required_num_results"="2" }
    attributes #1 = { "irreversible" }
    
    ; module flags
    
    !llvm.module.flags = !{!0, !1, !2, !3}
    
    !0 = !{i32 1, !"qir_major_version", i32 1}
    !1 = !{i32 7, !"qir_minor_version", i32 0}
    !2 = !{i32 1, !"dynamic_qubit_management", i1 false}
    !3 = !{i32 1, !"dynamic_result_management", i1 false}
    """
    
  2. QIR 回線をsubmit_qir_jobに送信するtarget ヘルパー関数を作成します。 この例では、入力データ形式と出力データ形式がそれぞれqir.v1microsoft.quantum-results.v1です。

    # Submit the job with proper input and output data formats
    def submit_qir_job(target, input, name, count=100):
        job = target.submit(
            input_data=input, 
            input_data_format="qir.v1",
            output_data_format="microsoft.quantum-results.v1",
            name=name,
            input_params = {
                "entryPoint": "ENTRYPOINT__main",
                "arguments": [],
                "count": count
                }
        )
    
        print(f"Queued job: {job.id}")
        job.wait_until_completed()
        print(f"Job completed with state: {job.details.status}")
        #if job.details.status == "Succeeded":
        result = job.get_results()
    
        return result
    
  3. QIR 回線を特定の Azure Quantum target に送信します。 たとえば、QIR 回線を IonQ シミュレーター targetに送信するには、次のコードを実行します。

    target = workspace.get_targets(name="ionq.simulator") 
    result = submit_qir_job(target, QIR_routine, "QIR routine")
    result
    

プロバイダー固有の形式の回線をAzure Quantumに送信する

各Azure Quantum プロバイダーには、量子回路を表す独自の形式があります。 Q# や Qiskit などの QIR 言語ではなく、プロバイダー固有の形式で回線をAzure Quantumに送信できます。

回線を JSON 形式で IonQ に送信する

IonQ は JSON 形式を使用して、 targetsで回線を実行します。 詳細については、 IonQ targetsIonQ API のドキュメントを参照してください

次の例では、JSON 形式の 3 つの量子ビット間に重ね合わせを作成します。

  1. 新しいセルで、JSON 形式の量子回路を作成します。

    circuit = {
        "qubits": 3,
        "circuit": [
            {
            "gate": "h",
            "target": 0
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 1
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 2
            },
        ]
    }
    
  2. 回線を IonQ targetに送信します。 次の例では、Job オブジェクトを返す IonQ シミュレーターを使用します。

    target = workspace.get_targets(name="ionq.simulator")
    job = target.submit(circuit)
    
  3. ジョブが完了したら、結果を取得します。

    results = job.get_results()
    print(results)
    

Pulser SDK 形式で Pasqal に回線を送信する

Pulser SDK を使用してパルス シーケンスを作成し、Pasqal targetsに送信できます。

Pulser SDK をインストールする

Pulser は、中性原子量子デバイスのパルス シーケンスを作成、シミュレート、実行できるフレームワークです。 Pulser は、量子実験を量子プロセッサに送信するためのパススルーとして PASQAL によって設計されています。 詳細については、 Pulser のドキュメントを参照してください

pulse シーケンスを送信するには、まず Pulser SDK パッケージをインストールします。

try:
    import pulser
    import pulser_pasqal
except ImportError:
    !pip -q install pulser pulser-pasqal --index-url https://pypi.org/simple

量子レジスタを作成する

レジスタとレイアウトの両方を定義します。 レジスタはアトムを配置する場所を指定し、レイアウトはレジスタ内の原子をキャプチャして構造化するトラップの位置を指定します。

レイアウトの詳細については、 Pulser のドキュメントを参照してください。

Pasqal 量子コンピューター devices をインポートして、targetオブジェクトFRESNEL_CAN1を作成します。

from pulser_pasqal import PasqalCloud

devices = PasqalCloud().fetch_available_devices()
QPU = devices["FRESNEL_CAN1"]
事前に調整されたレイアウト

デバイスは、事前に調整されたレイアウトの一覧を定義します。 これらのレイアウトの 1 つからレジスタをビルドできます。

QPU のパフォーマンスが向上するため、可能な場合は事前に調整されたレイアウトを使用します。

次の例では、デバイス上の事前に調整された最初のレイアウトを使用します。

# Use the first layout available on the device
layout = QPU.pre_calibrated_layouts[0]

# Select traps 1, 3 and 5 of the layout to define the register
traps = [1,v3,v5]
reg = layout.define_register(*traps)

# Draw the register to verify that it matches your expectations
reg.draw()
任意のレイアウト

事前に調整されたレイアウトが実験の要件を満たしていない場合は、カスタム レイアウトを使用します。

特定の任意のレジスタの場合、ニュートラルアトム QPU はレイアウトに従ってトラップを配置し、調整する必要があります。 各調整には時間がかかるため、可能な場合は、既存の校正済みレイアウトを再利用することをお勧めします。

任意のレイアウトを作成するには、次のいずれかのオプションを選択します。

  • 指定したレジスタに基づいてレイアウトを自動的に生成します。 大規模なレジスタの場合、このプロセスは最適ではないソリューションを生成する可能性があります。 次に例を示します。

    from pulser import Register
    qubits = {
        "q0": (0, 0),
        "q1": (0, 10),
        "q2": (8, 2),
        "q3": (1, 15),
        "q4": (-10, -3),
        "q5": (-8, 5),
    }
    
    reg = Register(qubits).with_automatic_layout(device) 
    
  • レジスタを作成するレイアウトを手動で定義します。 たとえば、2D 平面にランダムに配置される 20 個のトラップを含む任意のレイアウトを作成します。

    import numpy as np
    from pulser.register.register_layout import RegisterLayout
    
    # Generate random coordinates
    np.random.seed(301122)  # Keeps results consistent between runs
    traps = np.random.randint(0, 30, size=(20, 2))
    traps = traps - np.mean(traps, axis=0)
    
    # Create the layout
    layout = RegisterLayout(traps, slug="random_20")
    
    # Define your register with specific trap IDs
    trap_ids = [4, 8, 19, 0]
    reg = layout.define_register(*trap_ids, qubit_ids=["a", "b", "c", "d"])
    reg.draw()
    

パルス シーケンスを書き込む

中性原子はレーザーパルスで制御されます。 Pulser SDK を使用すると、量子レジスタに適用するパルス シーケンスを作成できます。

  1. 原子を制御するチャネルを宣言して、パルス シーケンス属性を定義します。 Sequenceを作成するには、シーケンスが実行されるデバイスと共にRegister インスタンスを指定します。 たとえば、次のコードは、1 つのチャネルを宣言します: ch0

    from pulser import Sequence
    
    seq = Sequence(reg, QPU)
    
    # Print the available channels for your sequence
    print(seq.available_channels)
    
    # Declare a channel. For example, `rydberg_global`
    seq.declare_channel("ch0", "rydberg_global")
    

    注記

    QPU = devices["FRESNEL_CAN1"] デバイスを使用するか、Pulser から仮想デバイスをインポートして柔軟性を高めることができます。 VirtualDeviceを使用すると、デバイスの仕様によって制約が少ないシーケンス作成が可能になり、エミュレーターで実行できます。 詳細については、 Pulser のドキュメントを参照してください。

  2. シーケンスにパルスを追加します。 これを行うには、宣言したチャネルにパルスを作成して追加します。 たとえば、次のコードはパルスを作成し、チャネル ch0に追加します。

    from pulser import Pulse
    from pulser.waveforms import RampWaveform, BlackmanWaveform
    import numpy as np
    
    amp_wf = BlackmanWaveform(1000, np.pi)
    det_wf = RampWaveform(1000, -5, 5)
    pulse = Pulse(amp_wf, det_wf, 0)
    seq.add(pulse, "ch0")
    
    seq.draw()
    

    次の図は、パルス シーケンスを示しています。

    パルス シーケンス

シーケンスを JSON 文字列に変換する

パルス シーケンスを送信するには、Pulser オブジェクトを入力データとして使用できる JSON 文字列に変換します。

import json

# Convert the sequence to a JSON string
def prepare_input_data(seq):
    input_data = {}
    input_data["sequence_builder"] = json.loads(seq.to_abstract_repr())
    to_send = json.dumps(input_data)
    return to_send

パルス シーケンスを Pasqal に送信する target

  1. 適切な入力データ形式と出力データ形式を設定します。 たとえば、次のコードは、入力データ形式を pasqal.pulser.v1 に設定し、出力データ形式を pasqal.pulser-results.v1に設定します。

    # Submit the job with proper input and output data formats
    def submit_job(target, seq, shots):
        job = target.submit(
            input_data=prepare_input_data(seq), # Take the JSON string previously defined as input data
            input_data_format="pasqal.pulser.v1",
            output_data_format="pasqal.pulser-results.v1",
            name="Pasqal sequence",
            shots=shots # Number of shots
        )
    
        print(f"Queued job: {job.id}")
        return job
    

    注記

    QPU でジョブを実行するために必要な時間は、現在のキュー時刻によって異なります。 ワークスペースの [target] ウィンドウで、の平均キュー時間を表示できます。

  2. プログラムを Pasqal に送信します。 実際の量子ハードウェアにコードを送信する前に、エミュレーター pasqal.sim.emu-mpstargetでコードをテストすることをお勧めします。

    target = workspace.get_targets(name="pasqal.sim.emu-mps") # Change to "pasqal.qpu.fresnel-can1" to use FRESNEL_CAN1 QPU
    job = submit_job(target, seq, 10)
    
    job.wait_until_completed()
    print(f"Job completed with state: {job.details.status}")
    result = job.get_results()
    print(result)
    
    {
        "1000000": 3, 
        "0010000": 1, 
        "0010101": 1
    }
    

Quantinuum に OpenQASM 回線を送信する

  1. OpenQASM 表記で量子回路を作成します。 たとえば、次のコードは Teleportation 回線を作成します。

    circuit = """OPENQASM 2.0;
    include "qelib1.inc";
    qreg q[3];
    creg c0[3];
    h q[0];
    cx q[0], q[1];
    cx q[1], q[2];
    measure q[0] -> c0[0];
    measure q[1] -> c0[1];
    measure q[2] -> c0[2];
    """
    

    または、OpenQASM ファイルから回線を読み込みます。

    with open("my_teleport.qasm", "r") as f:
        circuit = f.read()
    
  2. Quantinuum targetに回線を送信します。 次の例では、Quantinuum シミュレーター targetsのいずれかにジョブを送信します。

    target = workspace.get_targets(name="quantinuum.sim.h2-1sc")
    job = target.submit(circuit, shots=500)
    
  3. ジョブが完了するのを待ってから、結果を取得してください。

    results = job.get_results()
    print(results)
    

注記

これらの結果は、ランダムではないすべてのショットに対して 000 を返します。 これは、API バリデーターは、コードが Quantinuum ハードウェア上で実行できるかどうかをチェックするだけで、量子測定ごとに 0 を返すからです。 真の乱数ジェネレーターにするには、量子ハードウェアで回路を実行する必要があります。

リゲッティにキル回路を提出する

Quil ジョブを Rigetti target に送信するには、qdk.azurePython モジュールを使用します。

  1. 必要なインポートを読み込みます。

    from azure.quantum import Workspace
    from azure.quantum.target.rigetti import Result, Rigetti, RigettiTarget, InputParams
    
  2. target オブジェクトを作成し、ジョブを送信する Rigetti targetの名前を渡します。 たとえば、次のコードは QVMtargetを選択します。

    target = Rigetti(workspace=workspace, name=RigettiTarget.QVM)
    
  3. Quil プログラムを作成します。 プログラムを受け入れるには、読み取り値を "ro" に設定する必要があります。

    readout = "ro"
    bell_state_quil = f"""
    DECLARE {readout} BIT[2]
    
    H 0
    CNOT 0 1
    
    MEASURE 0 {readout}[0]
    MEASURE 1 {readout}[1]
    """
    
    num_shots = 5
    job = target.submit(
        input_data=bell_state_quil, 
        name="bell state", 
        shots=100, 
        input_params=InputParams(skip_quilc=False)
    )
    
    print(f"Job completed with state: {job.details.status}")
    result = Result(job)  # This throws an exception if the job failed
    
  4. 読み取りアウトの名前を使用して Result にインデックスを付けることができます。 次のコードでは、 data_per_shot は長 num_shotsのリストであり、リスト内の各項目は、そのショットのレジスタのデータを含む別のリストです。

    data_per_shot = result[readout]
    
    ro_data_first_shot = data_per_shot[0]
    

    この場合、レジスタの型は BIT であるため、型は整数であり、値は 0 または 1 です。

    assert isinstance(ro_data_first_shot[0], int)
    assert ro_data_first_shot[0] == 1 or ro_data_first_shot[0] == 0
    
  5. すべてのデータを出力します。

    print(f"Data from '{readout}' register:")
    for i, shot in enumerate(data_per_shot):
        print(f"Shot {i}: {shot}")
    

重要

1 つのジョブで複数の回線を送信することはできません。 回避策として、 backend.run メソッドを呼び出して各回線を非同期に送信し、各ジョブの結果をフェッチできます。 次に例を示します。

jobs = []
for circuit in circuits:
    jobs.append(backend.run(circuit, shots=N))

results = []
for job in jobs:
    results.append(job.result())