Input dan output Samplex
Pengantar
Samplex adalah tipe inti dari library samplomatic. Sebuah samplex merepresentasikan distribusi probabilitas parametrik atas parameter dari suatu template circuit, dengan field bernilai array lain yang dipakai saat memproses data hasil eksekusi template circuit yang sudah di-bind. Antarmuka utamanya adalah method sample() yang menarik sampel dari distribusi ini untuk menghasilkan kumpulan array.
Panduan ini menjelaskan cara memakai antarmuka sample(), cara mengkueri dan menentukan input yang diperlukan, dan cara memeriksa output yang diharapkan. Array yang diberikan ke dan dikembalikan dari sample() bertipe kuat (strongly typed). Artinya, untuk sebuah instance samplex, nama, tipe, dan shape-nya tetap dan bisa dikueri sebelum sampling dilakukan. Input dan output yang ada dalam sebuah samplex bergantung pada banyak faktor, misalnya apakah ada measurement twirling dan apakah injeksi noise diperlukan. Karena itu, secara umum dua instance samplex tidak diharapkan punya input dan output yang sama.
Penyiapan
Pertama, buat circuit yang sudah di-box lalu bangun menjadi pasangan samplex dan template. Keduanya akan dipakai di contoh-contoh berikut. Perlu diingat bahwa saat mem-box circuit secara manual, panduan ini memakai nama alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3, dan conclude. Selain itu, circuit ini punya empat objek Parameter. Panduan ini menunjukkan peran masing-masing dalam input dan output samplex. Lihat panduan transpiler untuk informasi tentang cara mem-box circuit secara otomatis.
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit import ClassicalRegister, Parameter, QuantumCircuit, QuantumRegister
from qiskit.quantum_info import Operator, Pauli, PauliLindbladMap
from samplomatic import ChangeBasis, InjectNoise, Twirl, build
# our circuit has two classical registers named alpha and beta
circuit = QuantumCircuit(
QuantumRegister(4), alpha := ClassicalRegister(3, "alpha"), beta := ClassicalRegister(1, "beta")
)
# the first box is only twirled
with circuit.box([Twirl()]):
for idx in range(4):
circuit.rx(Parameter(f"a{idx}"), idx)
circuit.cz(0, 1)
circuit.cz(1, 2)
# the second box is twirled, and has noise injected
with circuit.box([Twirl(), InjectNoise(ref="ref1", modifier_ref="mod_ref1")]):
circuit.h(1)
circuit.cz(1, 2)
# the third box is twirled, and has different noise injected
with circuit.box([Twirl(), InjectNoise(ref="ref2", modifier_ref="mod_ref2")]):
circuit.rx(0.1, range(4))
circuit.cz(0, 1)
circuit.cz(1, 2)
# the fourth box is the same as the second, but with a different modifer ref
with circuit.box([Twirl(), InjectNoise(ref="ref1", modifier_ref="mod_ref3")]):
circuit.h(1)
circuit.cz(1, 2)
circuit.barrier()
# the final two boxes twirl, and add a basis change in one case
with circuit.box([Twirl(), ChangeBasis(ref="conclude")]):
circuit.measure(range(3), alpha)
with circuit.box([Twirl()]):
circuit.measure([3], beta)
circuit.draw("mpl")
Selanjutnya, panggil build() pada circuit yang sudah di-box untuk membangun pasangan template dan samplex. Kamu akan melihat bagaimana setiap box diubah menjadi “sandwich” barrier yang menyertakan fragmen rz-sx-rz-sx-rz untuk mengimplementasikan dressing.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Plot DAG samplex dan arahkan kursor ke node untuk melihat hal berikut:
-
Semua node sampling (bintang) bertugas menghasilkan pengacakan untuk twirling, mengambil sampel dari model noise, atau menyuntikkan basis change.
-
Semua node collection (dasi kupu-kupu) bertugas merender slice output, yaitu bit flip pengukuran atau sudut parameter.
-
Semua node intermediate (lingkaran) bertugas melakukan berbagai jenis manipulasi data.
samplex.draw()
Mengkueri input yang diperlukan dan output yang diharapkan
Cara termudah untuk melihat input yang diperlukan dan output yang diharapkan adalah dengan mencetak objek Samplex. Item array diformat sebagai '{name}' <{type}[{shape}...]>, dan input yang wajib ditampilkan sebelum input opsional.
print(samplex)
Samplex(<58 nodes>)
Inputs:
- 'basis_changes.conclude' <uint8[3]>: Basis changing gates, in the symplectic ordering
I=0, Z=1, X=2, and Y=3.
- 'parameter_values' <float64[4]>: Input parameter values to use during sampling.
- 'pauli_lindblad_maps.ref1' <PauliLindbladMap>: A PauliLindblad map acting on 2 qubits,
with 'num_terms_ref1' terms.
- 'pauli_lindblad_maps.ref2' <PauliLindbladMap>: A PauliLindblad map acting on 4 qubits,
with 'num_terms_ref2' terms.
- 'local_scales.mod_ref1' <float64['num_terms_ref1']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'local_scales.mod_ref2' <float64['num_terms_ref2']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'local_scales.mod_ref3' <float64['num_terms_ref1']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'noise_scales.mod_ref1' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
- 'noise_scales.mod_ref2' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
- 'noise_scales.mod_ref3' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
Outputs:
* 'measurement_flips.alpha' <bool['num_randomizations', 1, 3]>: Bit-flip corrections
for measurement twirling.
* 'measurement_flips.beta' <bool['num_randomizations', 1, 1]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 48]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 3]>: Signs from sampled Pauli Lindblad
maps, where boolean values represent the parity of the number of non-trivial factors in the
sampled error that arise from negative rates. In other words, in order to implement basic
PEC, the sign used to correct expectation values should be ``(-1)**bool_value``. The order
matches the iteration order of boxes in the original circuit with noise injection
annotations.
inputs() dan outputs() dari samplex, serta informasi yang lebih rinci seperti nama, shape, tipe, dan deskripsinya, juga bisa dikueri secara programatik. Tipe kembalian kedua method ini adalah TensorInterface. Misalnya, kode berikut mencantumkan semua spesifikasi antarmuka input:
samplex.inputs().specs
[TensorSpecification('basis_changes.conclude', (3,), dtype('uint8'), 'Basis changing gates, in the symplectic ordering I=0, Z=1, X=2, and Y=3.'),
TensorSpecification('local_scales.mod_ref1', ('num_terms_ref1',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('local_scales.mod_ref2', ('num_terms_ref2',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('local_scales.mod_ref3', ('num_terms_ref1',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('noise_scales.mod_ref1', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('noise_scales.mod_ref2', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('noise_scales.mod_ref3', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('parameter_values', (4,), dtype('float64'), 'Input parameter values to use during sampling.'),
PauliLindbladMapSpecification('pauli_lindblad_maps.ref1', num_qubits=2, num_terms=num_terms_ref1),
PauliLindbladMapSpecification('pauli_lindblad_maps.ref2', num_qubits=4, num_terms=num_terms_ref2)]
Contoh berikutnya mencetak beberapa informasi tentang specifier output yang namanya mengandung string "flips".
for spec in samplex.outputs().get_specs("flips"):
print(spec.name, spec.shape, spec.dtype)
measurement_flips.alpha ('num_randomizations', 1, 3) bool
measurement_flips.beta ('num_randomizations', 1, 1) bool
Bind nilai input dan sampling
Untuk melakukan sample(), samplex memerlukan nilai untuk setidaknya inputs() yang ditandai wajib. Nilai-nilai ini pertama-tama di-bind ke antarmuka input agar antarmuka itu bisa melakukan semua pemeriksaan tipe (atau koersi tipe dalam beberapa kasus) dan analisis shape. Kalau ada yang salah pada tipe atau shape, atau ada nilai wajib yang hilang, error yang rinci akan dimunculkan.
Mengikat input
Pada contoh berikut, method sample() diberi nilai input yang wajib. Method ini lalu dipakai untuk menarik tiga pengacakan dari samplex. Binding wajib dari dressing adalah peta Pauli Lindblad untuk injeksi noise dan array basis change. Selain itu, nilai untuk keempat parameter pada circuit asli yang sudah di-box juga diperlukan, yang dalam kasus ini diatur ke np.linspace(0, 1, 4). Samplex menggabungkan parameter ini ke dalam dressing, sehingga secara efektif parameter itu menjadi modifikasi pada nilai parameter output samplex.
inputs = samplex.inputs().bind(
# bind() concatenates nested dicts with a '.' so that we can
# set 'pauli_lindblad_maps.noise0/1' as follows. note that the
# names 'ref1' and 'ref2' derive from the names in the InjectNoise
# annotation.
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
# likewise, we can set all basis changes as follows, where
# the name 'conclude' comes from our basis change annotation
basis_changes={"conclude": [0, 1, 2]},
# we must provide values for all parameters in the original circuit
parameter_values=np.linspace(0, 1, 4),
)
outputs = samplex.sample(inputs, num_randomizations=3)
print(outputs)
SamplexOutput(<
- Dimension constraints: num_randomizations=3
* 'measurement_flips.alpha' <bool['num_randomizations', 1, 3]>: Bit-flip corrections for
measurement twirling.
* 'measurement_flips.beta' <bool['num_randomizations', 1, 1]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 48]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 3]>: Signs from sampled Pauli Lindblad maps,
where boolean values represent the parity of the number of non-trivial factors in the sampled
error that arise from negative rates. In other words, in order to implement basic PEC, the
sign used to correct expectation values should be ``(-1)**bool_value``. The order matches the
iteration order of boxes in the original circuit with noise injection annotations.
>)
Input bisa di-bind ke antarmuka lewat beberapa kali panggilan bind() atau lewat penugasan item langsung. Meskipun bind() menerima format dictionary bersarang demi kemudahan, objeknya adalah mapping datar, dan mendukung semua sintaks mapping yang biasa. Begitu semua input wajib sudah ditemukan, fully_bound menjadi true.
inputs = (
samplex.inputs()
.bind(
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
}
)
.bind(parameter_values=np.linspace(0, 1, 4))
)
# once the final requirement is bound, fully_bound becomes True
print("Fully bound?", inputs.fully_bound)
inputs["basis_changes.conclude"] = [0, 2, 3]
print("Fully bound?", inputs.fully_bound)
# setting optional values does not affect whether the interface is fully bound
inputs["noise_scales.mod_ref1"] = 3
print("Fully bound?", inputs.fully_bound)
Fully bound? False
Fully bound? True
Fully bound? True
Semua objek TensorInterface, termasuk subclass SamplexOutput, memetakan objek terhadap data yang sudah di-bind ke dalamnya. Artinya, kamu bisa mendapatkan, misalnya, nilai parameter template seperti berikut. Dalam kasus ini, shape-nya (3, 48) karena tiga pengacakan diminta dan template circuit punya 48 parameter.
print("(num_randomizations, num_template_params) =", outputs["parameter_values"].shape)
print(outputs["parameter_values"][0, :])
(num_randomizations, num_template_params) = (3, 48)
[ 0.0000000e+00 0.0000000e+00 0.0000000e+00 1.5707964e+00
3.3333334e-01 1.5707964e+00 -1.5707964e+00 2.4749260e+00
-1.5707964e+00 1.5707964e+00 1.0000000e+00 1.5707964e+00
0.0000000e+00 1.5707964e+00 0.0000000e+00 3.1415927e+00
0.0000000e+00 0.0000000e+00 1.5707964e+00 3.0415926e+00
-1.5707964e+00 -1.5707964e+00 3.0415926e+00 -1.5707964e+00
-1.5707964e+00 1.0000000e-01 1.5707964e+00 1.5707964e+00
1.0000000e-01 1.5707964e+00 3.1415927e+00 1.5707964e+00
0.0000000e+00 0.0000000e+00 0.0000000e+00 0.0000000e+00
0.0000000e+00 3.1415927e+00 0.0000000e+00 3.1415927e+00
0.0000000e+00 0.0000000e+00 3.1415927e+00 1.5707964e+00
0.0000000e+00 4.4408921e-16 3.1415927e+00 0.0000000e+00]
Menentukan input sebagai dictionary
Alih-alih memberikan TensorInterface yang sudah di-bind sepenuhnya, kamu bisa menentukan input sebagai dictionary standar, seperti pada cell di bawah. Dalam kasus ini, method sample() secara internal mem-bind item dictionary ke antarmuka input samplex untuk memvalidasi bahwa semua input wajib ada dan kompatibel.
inputs = {
"pauli_lindblad_maps": {
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
"basis_changes": {"conclude": [0, 1, 2]},
"parameter_values": np.linspace(0, 1, 4),
}
outputs = samplex.sample(inputs, num_randomizations=3)
Tipe input
Saat ini ada dua tipe nilai antarmuka:
-
Bernilai array: Ini punya shape dan tipe data. Nilai yang di-bind dikoersi ke tipe data tersebut kalau memungkinkan. Dimensi mana pun bisa menjadi dimensi bebas.
-
Peta Pauli Lindblad: Ini harus bertipe
qiskit.quantum_info.PauliLindbladMapdan harus bekerja pada jumlah qubit yang ditentukan. Jumlah suku dalam peta adalah dimensi bebas.
# up to precision loss, these are equivalent via automatic type coersion of array inputs
samplex.inputs()["parameter_values"] = [1, 2, 8, 9]
samplex.inputs()["parameter_values"] = np.array([1, 2, 8, 9], dtype=np.float16)
Dimensi bebas
Kelas TensorInterface punya konsep dimensi bebas, yaitu ukuran bilangan bulat bernama yang nilainya belum ditentukan sampai ada data yang di-bind ke antarmuka dan membatasinya. Semua dimensi bebas dengan nama yang sama harus menghasilkan ukuran yang konsisten saat data di-bind, kalau tidak, error akan dimunculkan. Misalnya, pada outputs() dari samplex mana pun, sumbu array yang sesuai dengan pengacakan adalah parameter bebas yang secara konvensi bernama "num_randomizations". Saat sample() dipanggil, ia mem-bind nilai ke output
print("All free dimensions:", outputs.free_dimensions)
print("Current constraints:", outputs.bound_dimensions)
All free dimensions: {'num_randomizations'}
Current constraints: {'num_randomizations': 3}
Skenario dimensi bebas umum lainnya adalah berbagi batasan suku antara spesifikasi peta Pauli lindblad dan pengubahnya. qiskit.quantum_info.PauliLindbladMap adalah urutan suku yang terurut, di mana setiap suku berisi laju floating point dan representasi Pauli yang sparse. Fungsi build() menambahkan specifier "pauli_lindblad_maps.<ref>" untuk tipe ini bagi setiap anotasi InjectNoise, dan kalau anotasi itu punya modifier_ref, specifier "local_scales.<modifier_ref>" juga ditambahkan. Keduanya berbagi dimensi bebas "num_terms_<ref>" yang menentukan berapa banyak suku Pauli Lindblad yang ada dalam model noise, karena yang belakangan perlu di-zip dengan laju yang pertama untuk menskalakannya.
# both the PauliLindbladMap and the local scales imply 2 terms, so that the free dimension
# 'num_terms_noise2' is satisfied
inputs = samplex.inputs().bind(
pauli_lindblad_maps={"ref2": PauliLindbladMap.from_list([("XXYZ", 0.1), ("IIXX", 0.2)])},
local_scales={"mod_ref2": [1, 2]},
)
print("All free dimensions:", inputs.free_dimensions)
print("Current constraints:", inputs.bound_dimensions)
All free dimensions: {'num_terms_ref1', 'num_terms_ref2'}
Current constraints: {'num_terms_ref2': 2}
Konvensi urutan qubit
Samplex yang dibangun dari circuit ber-box oleh fungsi build() bisa memerlukan input bernilai array ke sample(), di mana satu indeks sesuai dengan qubit di dalam box circuit. Bagian ini menjelaskan konvensi urutan yang harus dipakai untuk sumbu semacam itu. Singkatnya: pakai urutan qubit dari circuit yang di-box, dibatasi pada qubit milik box.
Misalnya, kamu ingin tahu cara menentukan peta Pauli Lindblad untuk "noise1". Maka urutan qubit dalam peta noise mengacu pada qubit-qubit berikut:
box_of_interest = circuit[1]
qubit_ordering_convention = [qubit for qubit in circuit.qubits if qubit in box_of_interest.qubits]
qubit_ordering_convention
[<Qubit register=(4, "q0"), index=1>, <Qubit register=(4, "q0"), index=2>]
Artinya, kalau kamu ingin <Qubit register=(4, "q0"), index=2> mengalami noise X, maka kamu harus mendefinisikan peta Pauli Lindblad seperti ini karena qubit itu berada di indeks 1 dari qubit_ordering_convention.
samplex.inputs().bind(
pauli_lindblad_maps={
"ref1": (noise1 := PauliLindbladMap.from_sparse_list([("X", [1], 0.12)], num_qubits=2))
}
)
noise1
<PauliLindbladMap with 1 term on 2 qubits: (0.12)L(X_1)>
Hal yang sama berlaku untuk basis change. Kalau kamu ingin merotasi ke eigenstate +1 dari operator X, Y, dan Z untuk qubit 0, 1, dan 2 dari circuit, secara berurutan, maka kamu harus memakai array berikut, dengan mengingat bahwa library ini (dan Qiskit) memakai konvensi , , , dan .
samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change
[2, 3, 1]
Perlu diingat bahwa konvensi urutan qubit bukan urutan yang didefinisikan oleh salah satu dari berikut:
-
box_instruction.qubits, di manabox_instructionadalahqiskit.circuit.CircuitInstructionyang operasinya adalahqiskit.circuit.BoxOp, karena urutan daftar qubit ini bisa bergantung pada urutan instruksi ditambahkan ke konteks box, yang akan menjadi konvensi yang membingungkan. Misalnya,with box(): circuit.x([0, 1])danwith box(): circuit.x([1, 0])akan menghasilkan urutan yang berbeda. -
box_instruction.operation.body.qubitskarena qubit-qubit ini bahkan mungkin tidak muncul di circuit luar. Qubit di dalam body instruksiBoxOpyang sebenarnya (atau operasi control flow lain sepertiIfElseOp) hanya dijamin punya makna yang konsisten dalam scope itu. Ini akan menjadi konvensi yang tidak terdefinisi dengan baik dan sulit dipakai secara umum.
Pengujian lokal dengan template circuit
Kalau sebuah Samplex dibangun dengan fungsi build(), nilai parameter output seharusnya langsung kompatibel dengan parameter template circuit yang terkait. Misalnya, kamu bisa mem-bind nilai untuk salah satu pengacakan ke template untuk diperiksa secara lokal.
inputs = samplex.inputs().bind(
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
basis_changes={"conclude": [0, 0, 0]},
parameter_values=np.linspace(0, 1, 4),
)
outputs = samplex.sample(inputs, num_randomizations=3)
bound_template = template.assign_parameters(outputs["parameter_values"][2])
bound_template.draw("mpl", fold=1000)
Ini memungkinkan kamu memverifikasi bahwa samplex menghasilkan sampel yang diharapkan. Misalnya, kode berikut mengubah circuit yang sudah di-bind menjadi objek qiskit.quantum_info.Operator dan menggabungkannya dengan Pauli bitflip untuk memeriksa secara visual bahwa ketiga pengacakan secara logis setara dengan circuit dasar.
from samplomatic.utils import unbox
# Operator requires that we unpack all of the boxes first
unboxed = unbox(circuit).assign_parameters(inputs["parameter_values"])
# We want to plot unitary matrix amplitudes, so we need to remove the non-unitary measurements
unboxed.remove_final_measurements()
unboxed_unitary = Operator(unboxed)
# Plot magnitudes of the unitary matrix represenation of the circuit
plt.subplot(1, 4, 1)
plt.imshow(np.abs(unboxed_unitary))
plt.title("Base Circuit")
# For each randomization we sampled, plot the unitary matrix representation
for idx in range(3):
bound_template = template.assign_parameters(outputs["parameter_values"][idx])
bound_template.remove_final_measurements()
# The example does measurement twirling by compiling random bitflip gates before the
# measurements, so we need to undo these at the operator level for each randomization.
# We do this by casting them to Paulis and composing with the bound circuit's unitary
alpha_flips = Pauli(([0, 0, 0], outputs["measurement_flips.alpha"][idx, 0]))
beta_flips = Pauli(([0], outputs["measurement_flips.beta"][idx, 0]))
flips = beta_flips ^ alpha_flips
bound_unitary = Operator(bound_template) & flips
# Plot magnitudes of the unitary matrix represenation of the circuit
plt.subplot(1, 4, idx + 2)
plt.imshow(np.abs(bound_unitary))
plt.title(f"Randomization {idx}")
plt.tight_layout()

Demikian pula, kode berikut meneruskan nilai parameter output dan template ke sampler untuk mensimulasikan 10.000 shot dari semua pengacakan, lalu membandingkan nilai ekspektasi yang dihasilkan (hanya dari register alpha) dengan nilai ekspektasi dari simulasi circuit dasar.
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
from qiskit.primitives.containers import BitArray
# simulate some expectation values direction from the base circuit
estimator_job = Estimator().run([(unboxed, ["IZII", "IIZI", "IIIZ"])])
evs = estimator_job.result()[0].data["evs"]
# get the sampler data from each randomization, and do bitflip correction
sampler_job = Sampler().run([(template, outputs["parameter_values"])], shots=10_000)
alpha_data = sampler_job.result()[0].data["alpha"]
alpha_data ^= BitArray.from_bool_array(outputs["measurement_flips.alpha"], "little")
# compare the results
print(" EVs from base circuit:", evs)
print("EVs from randomizations:", alpha_data.expectation_values(["ZII", "IZI", "IIZ"]))
EVs from base circuit: [0.7819611 0.93553883 0.99500417]
EVs from randomizations: [0.789 0.9366 0.995 ]

