รวมตัวเลือกการลดข้อผิดพลาดกับ Estimator primitive
ประมาณการใช้งาน: เจ็ดนาทีบนโปรเซสเซอร์ Heron r2 (หมายเหตุ: นี่เป็นการประมาณการเท่านั้น ระยะเวลาจริงอาจแตกต่างกัน)
พื้นหลัง
บทแนะนำนี้จะสำรวจตัวเลือกการระงับข้อผิดพลาด (error suppression) และการลดข้อผิดพลาด (error mitigation) ที่มีให้ใช้งานกับ Estimator primitive จาก Qiskit Runtime คุณจะส ร้าง Circuit และ observable แล้วส่งงานโดยใช้ Estimator primitive ด้วยการตั้งค่าการลดข้อผิดพลาดในรูปแบบต่างๆ จากนั้นจะพล็อตผลลัพธ์เพื่อสังเกตผลกระทบของการตั้งค่าแต่ละรูปแบบ ตัวอย่างส่วนใหญ่ใช้ Circuit ขนาด 10 Qubit เพื่อให้แสดงผลได้ง่ายขึ้น และในตอนท้ายคุณสามารถขยาย workflow ไปสู่ 50 Qubit ได้
ตัวเลือกการระงับและลดข้อผิดพลาดที่จะใช้มีดังนี้:
- Dynamical decoupling
- การลดข้อผิดพลาดจากการวัด (Measurement error mitigation)
- Gate twirling
- Zero-noise extrapolation (ZNE)
ข้อกำหนดเบื้องต้น
ก่อนเริ่มบทแนะนำนี้ ให้แน่ใจว่าได้ติดตั้งสิ่งต่อไปนี้แล้ว:
- Qiskit SDK v2.1 หรือใหม่กว่า พร้อมรองรับ visualization
- Qiskit Runtime v0.40 หรือใหม่กว่า (
pip install qiskit-ibm-runtime)
การตั้งค่า
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator
ขั้นตอนที่ 1: แปลง input แบบ classical เป็นปัญหาเชิงควอนตัม
บทแนะนำนี้สมมติว่าปัญหา classical ถูกแปลงเป็นควอนตัมแล้ว เริ่มต้นด้วยการสร้าง Circuit และ observable สำหรับการวัด แม้ว่าเทคนิคที่ใช้ที่นี่จะใช้ได้กับ Circuit หลายรูปแบบ แต่เพื่อความเรียบง่าย บทแนะนำนี้จะใช้ Circuit efficient_su2 ที่รวมอยู่ใน Qiskit circuit library
efficient_su2 คือ parameterized quantum Circuit ที่ออกแบบมาให้รันได้อย่างมีประสิทธิภาพบนฮาร์ดแวร์ควอนตัมที่มีการเชื่อมต่อ Qubit จำกัด ขณะเดียวกันก็มีความสามารถในการแสดงออกเพียงพอสำหรับการแก้ปัญหาในโดเมนแอปพลิเคชันอย่างการเพิ่มประสิทธิภาพและเคมี Circuit ถูกสร้างโดยสลับชั้นของ parameterized single-qubit gates กับชั้นที่มีรูปแบบคงที่ของ two-qubit gates สำหรับจำนวนการทำซ้ำที่กำหนด รูปแบบของ two-qubit gates สามารถระบุได้โดยผู้ใช้ ที่นี่คุณสามารถใช้รูปแบบ pairwise ในตัวเพราะช่วยลดความลึกของ Circuit โดยจัดวาง two-qubit gates ให้หนาแน่นที่สุดเท่าที่เป็นไปได้ รูปแบบนี้สามารถรันได้โดยใช้เพียงการเชื่อมต่อ Qubit แบบเส้นตรง
n_qubits = 10
reps = 1
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
circuit.decompose().draw("mpl", scale=0.7)


สำหรับ observable ของเรา ให้ใช้ตัวดำเนินการ Pauli ที่กระทำบน Qubit ตัวสุดท้าย
# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
ณ จุดนี้ คุณสามารถดำเนินการรัน Circuit และวัด observable ได้เลย อย่างไรก็ตาม คุณยังต้องการเปรียบเทียบผลลัพธ์จากอุปกรณ์ควอนตัมกับคำตอบที่ถูกต้อง ซึ่งก็คือค่าทางทฤษฎีของ observable หาก Circuit ถูกรันโดยปราศจากข้อผิดพลาด สำหรับ Circuit ควอนตัมขนาดเล็ก คุณสามารถคำนวณค่านี้โดยการจำลอง Circuit บนคอมพิวเตอร์ classical ได้ แต่ทำไม่ได้กับ Circuit ขนาดใหญ่ระดับ utility-scale คุณสามารถแก้ปัญหานี้ด้วยเทคนิค "mirror circuit" (หรือที่รู้จักในชื่อ "compute-uncompute") ซึ่งมีประโยชน์สำหรับการวัดประสิทธิภาพของอุปกรณ์ควอนตัม
Mirror circuit
ในเทคนิค mirror circuit คุณเชื่อมต่อ Circuit กับ inverse circuit ของมัน ซึ่งสร้างขึ้นโดยการกลับ Gate แต่ละตัวของ Circuit ในลำดับย้อนกลับ Circuit ที่ได้จะ implement ตัวดำเนินการ identity ซึ่งสามารถจำลองได้อย่างง่ายดาย เนื่องจากโครงสร้างของ Circuit ต้นฉบับยังคงถูกเก็บรักษาไว้ใน mirror circuit การรัน mirror circuit จึงยังคงให้ข้อมูลเกี่ยวกับประสิทธิภาพของอุปกรณ์ควอนตัมบน Circuit ต้นฉบับ
โค้ดต่อไปนี้กำหนด parameters แบบสุ่มให้กับ Circuit ของคุณ แล้วสร้าง mirror circuit โดยใช้คลาส unitary_overlap ก่อนที่ จะ mirror Circuit ให้เพิ่มคำสั่ง barrier เพื่อป้องกันไม่ให้ Transpiler รวมสองส่วนของ Circuit ที่อยู่คนละฝั่งของ barrier โดยไม่มี barrier นั้น Transpiler จะรวม Circuit ต้นฉบับกับ inverse ของมัน ส่งผลให้ Circuit ที่ transpile แล้วไม่มี Gate เลย
# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)
# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
mirror_circuit.decompose().draw("mpl", scale=0.7)


ขั้นตอนที่ 2: ปรับแต่งปัญหาสำหรับการรันบนฮาร์ดแวร์ควอนตัม
คุณต้องปรับแต่ง Circuit ก่อนรันบนฮาร์ดแวร์ กระบวนการนี้ประกอบด้วยหลายขั้นตอน:
- เลือก qubit layout ที่แมป virtual qubits ของ Circuit ไปยัง physical qubits บนฮาร์ดแวร์
- แทรก swap gates ตามที่จำเป็นเพื่อกำหนดเส้นทางการโต้ตอบระหว่าง Qubit ที่ไม่ได้เชื่อมต่อกัน
- แปล Gate ใน Circuit เป็นคำสั่ง Instruction Set Architecture (ISA) ที่สามารถรันโดยตรงบนฮาร์ดแวร์
- ดำเนินการ Circuit optimizations เพื่อลดความลึกของ Circuit และจำนวน Gate
Transpiler ที่ built-in ใน Qiskit สามารถดำเนินการทุกขั้นตอนเหล่านี้ให้คุณได้ เนื่องจากตัวอย่างนี้ใช้ hardware-efficient circuit Transpiler ควรสามารถเลือก qubit layout ที่ไม่ต้องการแทรก swap gates เพื่อกำหนดเส้นทางการโต้ตอบ
คุณต้องเลือกอุปกรณ์ฮาร์ดแวร์ที่จะใช้ก่อนที่จะปรับแต่ง Circuit โค้ดต่อไปนี้ขอ Backend ที่ว่างที่สุดที่มี Qubit อย่างน้อย 127 ตัว
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
คุณสามารถ transpile Circuit สำหรับ Backend ที่เลือกได้โดยการสร้าง pass manager แล้วรัน pass manager บน Circuit วิธีที่ง่ายในการสร้าง pass manager คือการใช้ฟังก์ชัน generate_preset_pass_manager ดู Transpile with pass managers สำหรับคำอธิบายโดยละเอี ยดเกี่ยวกับการ transpile ด้วย pass managers
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)
isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)


Circuit ที่ transpile แล้วตอนนี้มีเฉพาะคำสั่ง ISA เท่านั้น Single-qubit gates ถูก decompose เป็น gates และการหมุน และ CX gates ถูก decompose เป็น ECR gates และการหมุน single-qubit
กระบวนการ transpilation ได้แมป virtual qubits ของ Circuit ไปยัง physical qubits บนฮาร์ดแวร์ ข้อมูลเกี่ยวกับ qubit layout ถูกจัดเก็บในแอตทริบิวต์ layout ของ Circuit ที่ transpile แล้ว observable ยังถูกกำหนดในเชิง virtual qubits ดังนั้นคุณต้องใช้ layout นี้กับ observable ด้วย ซึ่งทำได้ด้วยเมธอด apply_layout ของ SparsePauliOp
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])
Observable with layout applied:
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
ขั้นตอนที่ 3: รันโดยใช้ Qiskit primitives
ตอนนี้คุณพร้อมที่จะรัน Circuit โดยใช้ Estimator primitive แล้ว
ที่นี่คุณจะส่งงานห้าชิ้นแยกกัน เริ่มต้นโดยไม่มีการระงับหรือลดข้อผิดพลาด และค่อยๆ เปิดใช้งานตัวเลือกการระงับและลดข้อผิดพลาดต่างๆ ที่มีใน Qiskit Runtime สำหรับข้อมูลเกี่ยวกับตัวเลือกเหล่านี้ ดูหน้าต่อไปนี้:
- ภาพรวมของตัวเลือกทั้งหมด
- Dynamical decoupling
- Resilience รวมถึงการลดข้อผิดพลาดจากการวัดและ zero-noise extrapolation (ZNE)
- Twirling
เนื่องจากงานเหล่านี้สามารถรันได้อิสระจากกัน คุณสามารถใช้ batch mode เพื่อให้ Qiskit Runtime ปรับแต่งเวลาการรันให้เหมาะสม
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
ขั้นตอนที่ 4: ประมวลผลหลังการรันและคืนผลในรูปแบบ classical ที่ต้องการ
สุดท้าย คุณสามารถวิเคราะห์ข้อมูลได้ ที่นี่คุณจะดึงผลลัพธ์ของงาน ดึงค่า expectation values ที่วัดได้จากผลลัพธ์ และพล็อตค่าเหล่านั้น รวมถึง error bars ที่ส่วนเบี่ยงเบนมาตรฐานหนึ่งหน่วย
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
ในขนาดเล็กนี้ เป็นเรื่องยากที่จะสังเกตผลกระทบของเทคนิคการลดข้อผิดพลาดส่วนใหญ่ แต่ zero-noise extrapolation ให้การปรับปรุงที่เห็นได้ชัดเจน อย่างไรก็ตาม โปรดสังเกตว่าการปรับปรุงนี้ไม่ได้ฟรี เพราะผลลัพธ์ ZNE ยังมี error bar ที่ใหญ่กว่าด้วย
ขยายขนาดการทดลอง
เมื่อพัฒนาการทดลอง การเริ่มด้วย Circuit ขนาดเล็กจะเป็นประโยชน์เพื่อให้แสดงผลและจำลองได้ง่ายขึ้น ตอนนี้ที่คุณพัฒนาและทดสอบ workflow บน Circuit ขนาด 10 Qubit แล้ว คุณสามารถขยายไปสู่ 50 Qubit ได้ โค้ดต่อไปนี้ทำซ้ำทุกขั้นตอนในบทแนะนำนี้ แต่ตอนนี้ใช้กับ Circuit ขนาด 50 Qubit
n_qubits = 50
reps = 1
# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
# Run jobs
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
เมื่อเปรียบเทียบผลลัพธ์ 50 Qubit กับผลลัพธ์ 10 Qubit ก่อนหน้า คุณอาจสังเกตสิ่งต่อไปนี้ (ผลลัพธ์ของคุณอาจแตกต่างกันในแต่ละรอบการรัน):
- ผลลัพธ์โดยไม่มีการลดข้อผิดพลาดแย่ลง การรัน Circuit ขนาดใหญ่ขึ้นต้องใช้ Gate มากขึ้น ดังนั้นจึงมีโอกาสให้ข้อผิดพลาดสะสมมากขึ้น
- การเพิ่ม dynamical decoupling อาจทำให้ประสิทธิภาพแย่ลง ซึ่งไม่น่าแปลกใจ เพราะ Circuit มีความหนาแน่นมาก Dynamical decoupling มีประโยชน์หลักเมื่อมีช่องว่างขนาดใหญ่ใน Circuit ในช่วงที่ Qubit รอโดยไม่มี Gate ถูกใช้กับพวกมัน เมื่อไม่มีช่องว่างเหล่านี้ dynamical decoupling ก็ไม่มีประสิทธิภาพ และอาจทำให้ประสิทธิภาพแย่ลงจริงๆ เนื่องจากข้อผิดพลาดใน dynamical decoupling pulses เอง Circuit 10 Qubit อาจเล็กเกินไปที่จะสังเกตผลกระทบนี้
- ด้วย zero-noise extrapolation ผลลัพธ์ดีพอๆ กันหรือเกือบดีเท่าผลลัพธ์ 10 Qubit แม้ว่า error bar จะใหญ่กว่ามาก นี่แสดงให้เห็ นพลังของเทคนิค ZNE!
สรุป
ในบทแนะนำนี้ คุณได้ตรวจสอบตัวเลือกการลดข้อผิดพลาดต่างๆ ที่มีให้กับ Qiskit Runtime Estimator primitive คุณพัฒนา workflow โดยใช้ Circuit ขนาด 10 Qubit แล้วขยายไปสู่ 50 Qubit คุณอาจสังเกตว่าการเปิดใช้งานตัวเลือกการระงับและลดข้อผิดพลาดมากขึ้นไม่ได้ปรับปรุงประสิทธิภาพเสมอไป (โดยเฉพาะการเปิดใช้งาน dynamical decoupling ในกรณีนี้) ตัวเลือกส่วนใหญ่รองรับการกำหนดค่าเพิ่มเติม ซึ่งคุณสามารถทดลองใช้ในงานของคุณเอง!