Перейти до основного вмісту
Цей проєкт у бета-версії. Знайшов(-ла) помилку? Відкрити issue на GitHub. Є запитання чи ідея? Запитати в Discussions — будемо раді твоєму відгуку!
Яка якість перекладу на цій сторінці?

Входи та виходи Samplex

Вступ​

Samplex — основний тип бібліотеки samplomatic. Samplex представляє параметричний розподіл імовірностей над параметрами деякої шаблонної схеми, з іншими полями-масивами для використання під час постобробки даних, зібраних під час виконання зв'язаної шаблонної схеми. Його центральний інтерфейс — метод sample(), який робить вибірку з цього розподілу, щоб отримати набір масивів.

Цей посібник описує, як користуватися інтерфейсом sample(), як запитувати й задавати потрібні входи та як перевіряти очікувані виходи. Масиви, що передаються в sample() і повертаються з нього, мають сувору типізацію. Тобто для екземпляра samplex їхні імена, типи та форми фіксовані й доступні для запиту до виконання вибірки. Входи та виходи samplex залежать від багатьох чинників. Наприклад, від того, чи є measurement twirling і чи потрібна ін'єкція шуму. Тому, загалом, два екземпляри samplex не повинні мати однакові входи та виходи.

Налаштування​

Спочатку побудуй схему з боксами і збери з неї пару samplex і шаблон. Вони використовуватимуться в наступних прикладах. Зауваж, що при ручному розбитті схеми на бокси цей посібник використовує імена alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3 і conclude. Крім того, схема має чотири об'єкти Parameter. Цей посібник показує, яку роль кожен із них відіграє у входах і виходах samplex. Інформацію про автоматичне розбиття схем на бокси дивись у посібнику з транспайлера.

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")

../_images/88f6576d7e2fd76bfb52d5133c6bc07e908a28941c6538f5251ac508cfb202f7.png

Далі викликай build() на схемі з боксами, щоб побудувати пару шаблон і samplex. Ти побачиш, як кожен бокс перетворюється на «сендвіч» із бар'єрів, що містить фрагменти rz-sx-rz-sx-rz для реалізації dressing.

template, samplex = build(circuit)

template.draw("mpl", fold=100)

../_images/ec8456d0d0a5928c5d056184cbd1842dcf7aefdc7cdba428f62092b27646d601.png

Побудуй графік DAG samplex і наведи курсор на вузли, щоб побачити таке:

  • Усі вузли вибірки (зірки) відповідають за генерацію рандомізацій для twirling, вибірку з моделей шуму або ін'єкцію змін базису.

  • Усі вузли збору (метелики) відповідають за формування вихідних зрізів, якими є фліпи вимірювань або кути параметрів.

  • Усі проміжні вузли (кола) відповідають за різні види маніпуляції даними.

samplex.draw()

Запит потрібних входів і очікуваних виходів​

Найпростіший спосіб побачити потрібні входи та очікувані виходи — надрукувати об'єкт Samplex. Елементи-масиви форматуються як '{name}' <{type}[{shape}...]>, а обов'язкові входи йдуть перед необов'язковими.

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() і outputs() samplex, а також докладнішу інформацію, як-от їхні імена, форми, типи й описи, можна також запитувати програмно. Тип, що повертається обома цими методами, — TensorInterface. Наприклад, наступний код виводить усі специфікації інтерфейсу входів:

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)]

Наступний приклад друкує деяку інформацію про ті специфікатори виходів, імена яких містять рядок "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

Прив'язка вхідних значень і вибірка​

Щоб виконати sample(), samplex потребує значень принаймні для тих inputs(), які позначені як обов'язкові. Ці значення спочатку прив'язуються до інтерфейсу входів, щоб він міг виконати всю необхідну перевірку типів (або в деяких випадках приведення типів) та аналіз форм. Якщо щось не так із типами чи формами або бракує обов'язкового значення, виникає докладна помилка.

Прив'язка входу​

У наступному прикладі методу sample() передаються обов'язкові вхідні значення. Потім він використовується для вибірки трьох рандомізацій із samplex. Обов'язкові прив'язки від dressings — це мапи Паулі–Ліндблада для ін'єкції шуму та масив зміни базису. Крім того, потрібні значення для чотирьох параметрів вихідної схеми з боксами, які в цьому випадку задано як np.linspace(0, 1, 4). Samplex складає ці параметри в dressings, тож вони фактично стають модифікаціями вихідних значень параметрів 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.
>)

Входи можна прив'язувати до інтерфейсу через кілька викликів bind() або прямим присвоєнням елементів. Попри вкладений формат словника, який bind() допускає для зручності, це пласкийоб'єкт відображення, що підтримує весь звичайний синтаксис відображень. Щойно знайдено всі обов'язкові входи, fully_bound стає істинним.

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

Усі об'єкти TensorInterface, зокрема підклас SamplexOutput, відображають об'єкти на дані, прив'язані до них. Це означає, що ти можеш отримати, наприклад, значення параметрів шаблону так, як показано нижче. У цьому випадку воно має форму (3, 48), оскільки запитано три рандомізації, а шаблонна схема має 48 параметрів.

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]

Задання входу як словника​

Замість того щоб надавати повністю прив'язаний TensorInterface, можна задати входи як стандартний словник, як показано в комірці нижче. У цьому випадку метод sample() внутрішньо прив'язує елементи словника до інтерфейсу входів samplex, щоб перевірити, що всі обов'язкові входи присутні та сумісні.

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)

Типи входів​

Наразі існує два типи значень інтерфейсу:

  • Значення-масиви: вони мають форму та тип даних. Прив'язані значення за можливості приводяться до типу даних. Будь-який вимір може бути вільним.

  • Мапи Паулі–Ліндблада: вони мають бути типу qiskit.quantum_info.PauliLindbladMap і діяти на задану кількість кубітів. Кількість доданків у мапі є вільним виміром.

# 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)

Вільні виміри​

Клас TensorInterface має поняття вільних вимірів — іменованих цілих розмірів, значення яких невизначені, доки до інтерфейсу не прив'язано дані, що їх обмежують. Усі вільні виміри з однаковим іменем мають розв'язуватися в узгоджений розмір під час прив'язки даних, інакше виникне помилка. Наприклад, в outputs() будь-якого samplex вісь масиву, що відповідає рандомізаціям, є вільним параметром з іменем "num_randomizations" за домовленістю. Коли викликається sample(), він прив'язує значення до виходів

print("All free dimensions:", outputs.free_dimensions)
print("Current constraints:", outputs.bound_dimensions)
All free dimensions: {'num_randomizations'}
Current constraints: {'num_randomizations': 3}

Ще один поширений сценарій вільного виміру — спільні обмеження на кількість доданків між специфікаціями мап Паулі–Ліндблада та їхніми модифікаторами. qiskit.quantum_info.PauliLindbladMap — це впорядкована послідовність доданків, кожен з яких містить швидкість із плаваючою комою та розріджене представлення Паулі. Функція build() додає специфікатор "pauli_lindblad_maps.<ref>" для цього типу для кожної анотації InjectNoise, а якщо вона містить modifier_ref, також додається специфікатор "local_scales.<modifier_ref>". Обидва вони мають спільний вільний вимір "num_terms_<ref>", що задає, скільки доданків Паулі–Ліндблада присутні в моделі шуму, бо другий має зшиватися зі швидкостями першого, щоб масштабувати їх.

# 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}

Порядок кубітів​

Samplex, побудовані із схем з боксами функцією build(), можуть вимагати входів-масивів для sample(), де один індекс відповідає кубітам у боксі схеми. Цей розділ пояснює порядок, який слід використовувати для таких осей. Коротко: використовуй порядок кубітів схеми з боксами, обмежений кубітами бокса.

Наприклад, припустімо, ти хочеш з'ясувати, як задати мапу Паулі–Ліндблада для "noise1". Тоді порядок кубітів у мапі шуму визначається щодо цих кубітів:

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>]

Тобто, якщо ти хочеш, щоб <Qubit register=(4, "q0"), index=2> мав шум X, то слід визначити мапу Паулі–Ліндблада на кшталт цієї, бо він має індекс 1 у 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)>

Те саме стосується змін базису. Якщо ти хочеш повернути до власних станів +1 операторів X, Y і Z для кубітів 0, 1 і 2 circuit відповідно, використовуй наступний масив, пам'ятаючи, що в цій бібліотеці (та в Qiskit) використовується домовленість I=0I=0, Z=1Z=1, X=2X=2 і Y=3Y=3.

samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change
[2, 3, 1]

Зауваж, що порядок кубітів не є порядком, визначеним жодним із наступного:

  • box_instruction.qubits, де box_instruction — це qiskit.circuit.CircuitInstruction, операція якої є qiskit.circuit.BoxOp, оскільки порядок цього списку кубітів може залежати від порядку, у якому інструкції додавалися до контексту бокса, що було б заплутаною домовленістю. Наприклад, with box(): circuit.x([0, 1]) і with box(): circuit.x([1, 0]) дали б різні порядки.

  • box_instruction.operation.body.qubits, бо ці кубіти можуть навіть не з'являтися у зовнішній схемі. Кубіти всередині власне тіла інструкцій BoxOp (або будь-якої іншої операції керування потоком, як-от IfElseOp) мають узгоджений зміст лише в межах цієї області. Це була б погано визначена домовленість, яку складно використовувати загалом.

Локальне тестування за допомогою шаблонної схеми​

Якщо Samplex побудовано за допомогою функції build(), вихідні значення параметрів мають бути безпосередньо сумісні з параметрами пов'язаної шаблонної схеми. Наприклад, ти можеш прив'язати до шаблону значення для однієї з рандомізацій для локальної перевірки.

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)

../_images/1d82c46209f8648a2df572cfef8ca256f47952566d34386761ab942aa6e50afd.png

Це дозволяє перевірити, що samplex видає очікувані вибірки. Наприклад, наступний код перетворює зв'язану схему на об'єкт qiskit.quantum_info.Operator і складає його з Паулі бітових фліпів, щоб візуально переконатися, що всі три рандомізації логічно еквівалентні базовій схемі.

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()

../_images/af1aa74f8a0e8abed673d4cd43ff8b810c44297d2f3a42fcfb49ade811e51c58.png

Так само наступний код передає вихідні значення параметрів і шаблон у sampler, щоб симулювати 10 000 shots для всіх рандомізацій, і порівнює отримані очікувані значення (лише регістра alpha) з очікуваними значеннями симуляції базової схеми.

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 ]