Vstupy a výstupy Samplex
Úvod
Samplex je základní typ knihovny samplomatic. Samplex představuje parametrické rozdělení pravděpodobnosti nad parametry nějakého obvodu-šablony, s dalšími poli s hodnotami typu pole, která se použijí při zpracování dat získaných provedením svázaného obvodu-šablony. Jeho hlavním rozhraním je metoda sample(), která z tohoto rozdělení vzorkuje a vytváří kolekci polí.
Tento průvodce popisuje, jak používat rozhraní sample(), jak zjistit a zadat požadované vstupy a jak prozkoumat očekávané výstupy. Pole předávaná do sample() a z ní vracená jsou silně typovaná. To znamená, že pro danou instanci samplexu jsou jejich názvy, typy a tvary pevné a lze je zjistit dříve, než proběhne vzorkování. Vstupy a výstupy přítomné v samplexu závisí na mnoha faktorech. Například na tom, zda je přítomen twirling měření a zda je vyžadována injekce šumu. Z tohoto důvodu se obecně neočekává, že dvě instance samplexu budou mít stejné vstupy a výstupy.
Nastavení
Nejprve sestav obvod rozdělený do boxů a vytvoř z něj dvojici samplex a šablona. Ty se použijí v následujících příkladech. Všimni si, že při ručním rozdělování obvodu do boxů tento průvodce používá názvy alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3 a conclude. Obvod má navíc čtyři objekty Parameter. Tento průvodce ukazuje, jakou roli každý z nich hraje ve vstupech a výstupech samplexu. Informace o automatickém rozdělování obvodů do boxů najdeš v průvodci transpilátorem.
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")
Dále zavolej build() na obvodu rozděleném do boxů, aby se vytvořila dvojice šablona a samplex. Uvidíš, jak se z každého boxu stane bariérový „sendvič“, který obsahuje fragmenty rz-sx-rz-sx-rz implementující dressing.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Vykresli DAG samplexu a najetím myší na uzly zobraz následující:
-
Všechny vzorkovací uzly (hvězdy) jsou zodpovědné za generování randomizací pro twirling, vzorkování z modelů šumu nebo injekci změn báze.
-
Všechny sběrné uzly (motýlky) jsou zodpovědné za vykreslování výstupních řezů, což jsou překlopení měření nebo úhly parametrů.
-
Všechny mezilehlé uzly (kruhy) jsou zodpovědné za různé druhy manipulace s daty.
samplex.draw()
Zjištění požadovaných vstupů a očekávaných výstupů
Nejjednodušší způsob, jak zjistit požadované vstupy a očekávané výstupy, je vytisknout objekt Samplex. Položky polí jsou formátovány jako '{name}' <{type}[{shape}...]> a požadované vstupy předcházejí volitelným vstupům.
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() a outputs() samplexu, stejně jako podrobnější informace, například jejich názvy, tvary, typy a popisy, lze také zjistit programově. Návratovým typem obou těchto metod je TensorInterface. Následující kód například vypíše všechny specifikace vstupního rozhraní:
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)]
Následující příklad vytiskne některé informace o těch výstupních specifikátorech, jejichž názvy obsahují řetězec "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
Svázání vstupních hodnot a vzorkování
Aby mohl sample() provést vzorkování, potřebuje samplex hodnoty alespoň pro ty inputs(), které jsou označeny jako povinné. Tyto hodnoty se nejprve svážou se vstupním rozhraním, aby mohlo provést veškerou potřebnou kontrolu typů (nebo v některých případech typovou koerci) a analýzu tvarů. Pokud je cokoli v typech nebo tvarech špatně nebo chybí povinná hodnota, vyvolá se podrobná chyba.
Svázání vstupu
V následujícím příkladu se metodě sample() předají požadované vstupní hodnoty. Poté se použije k vzorkování tří randomizací ze samplexu. Požadovanými vazbami z dressings jsou Pauli Lindbladovy mapy pro injekci šumu a pole změny báze. Dále jsou vyžadovány hodnoty čtyř parametrů v původním obvodu rozděleném do boxů, které se zde nastaví na np.linspace(0, 1, 4). Samplex tyto parametry skládá do dressings, takže se efektivně stávají úpravami hodnot výstupních parametrů samplexu.
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.
>)
Vstupy lze s rozhraním svázat vícenásobným voláním bind() nebo přímým přiřazením položek. Přestože bind() pro pohodlí umožňuje formát vnořeného slovníku, jde o plochý mapovací objekt a podporuje veškerou obvyklou syntaxi mapování. Jakmile jsou nalezeny všechny povinné vstupy, fully_bound se stane pravdivým.
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
Všechny objekty TensorInterface, včetně podtřídy SamplexOutput, mapují objekty na data, která s nimi byla svázána. To znamená, že můžeš získat například hodnoty parametrů šablony následujícím způsobem. V tomto případě má tvar (3, 48), protože byly vyžádány tři randomizace a obvod-šablona má 48 parametrů.
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]
Specifikace vstupu jako slovníku
Místo poskytnutí plně svázaného TensorInterface můžeš vstupy zadat jako standardní slovník, jak ukazuje buňka níže. V tomto případě metoda sample() interně naváže položky slovníku na vstupní rozhraní samplexu, aby ověřila, že jsou přítomny všechny povinné vstupy a že jsou kompatibilní.
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)
Typy vstupů
V současnosti existují dva typy hodnot rozhraní:
-
Hodnoty typu pole: Mají tvar a datový typ. Svázané hodnoty se, pokud je to možné, převedou na daný datový typ. Libovolná dimenze může být volná dimenze.
-
Pauli Lindbladovy mapy: Musí být typu
qiskit.quantum_info.PauliLindbladMapa musí působit na předepsaný počet qubitů. Počet členů v mapě je volná dimenze.
# 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)
Volné dimenze
Třída TensorInterface má pojem volných dimenzí, což jsou pojmenované celočíselné velikosti, jejichž hodnoty jsou neurčené, dokud se k rozhraní nesvážou data, která je omezí. Všechny volné dimenze se stejným názvem se musí při svazování dat vyřešit na konzistentní velikost, jinak se vyvolá chyba. Například v outputs() libovolného samplexu je osa pole odpovídající randomizacím volným parametrem, který má podle konvence název "num_randomizations". Když se zavolá sample(), naváže hodnoty na výstupy
print("All free dimensions:", outputs.free_dimensions)
print("Current constraints:", outputs.bound_dimensions)
All free dimensions: {'num_randomizations'}
Current constraints: {'num_randomizations': 3}
Dalším častým scénářem volné dimenze je sdílení omezení počtu členů mezi specifikacemi Pauli Lindbladových map a jejich modifikátory. qiskit.quantum_info.PauliLindbladMap je uspořádaná posloupnost členů, kde každý člen obsahuje sazbu s plovoucí řádovou čárkou a řídkou Pauliho reprezentaci. Funkce build() přidá pro tento typ specifikátor "pauli_lindblad_maps.<ref>" pro každou anotaci InjectNoise a pokud obsahuje modifier_ref, přidá se také specifikátor "local_scales.<modifier_ref>". Oba sdílejí volnou dimenzi "num_terms_<ref>", která určuje, kolik Pauli Lindbladových členů je v modelu šumu přítomno, protože druhý z nich se musí spárovat (zip) se sazbami prvního, aby je mohl škálovat.
# 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}
Konvence řazení qubitů
Samplexy sestavené z obvodů rozdělených do boxů funkcí build() mohou vyžadovat pro sample() vstupy typu pole, kde jeden index odpovídá qubitům v boxu obvodu. Tato část vysvětluje konvenci řazení, která by se měla pro takové osy používat. Ve zkratce: použij pořadí qubitů obvodu rozděleného do boxů, omezené na qubity boxu.
Předpokládejme například, že chceš zjistit, jak zadat Pauli Lindbladovu mapu pro "noise1". Pořadí qubitů v mapě šumu se pak vztahuje k těmto qubitům:
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>]
To znamená, že pokud chceš, aby <Qubit register=(4, "q0"), index=2> byl zašuměný šumem X, měl bys definovat Pauli Lindbladovu mapu jako tuto, protože se nachází na indexu 1 v 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)>
Totéž platí pro změny báze. Pokud chceš otočit do vlastních stavů +1 operátorů X, Y a Z pro qubity 0, 1 a 2 obvodu circuit, měl bys použít následující pole; pamatuj, že tato knihovna (i Qiskit) používá konvenci , , a .
samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change
[2, 3, 1]
Všimni si, že konvence řazení qubitů není pořadí definované žádným z následujících:
-
box_instruction.qubits, kdebox_instructionjeqiskit.circuit.CircuitInstruction, jejíž operací jeqiskit.circuit.BoxOp, protože pořadí tohoto seznamu qubitů může záviset na pořadí, v jakém byly instrukce přidány do kontextu boxu, což by byla matoucí konvence. Napříkladwith box(): circuit.x([0, 1])awith box(): circuit.x([1, 0])by vedly k různému pořadí. -
box_instruction.operation.body.qubits, protože se tyto qubity nemusí vůbec objevit ve vnějším obvodu. Qubity uvnitř těla instrukcíBoxOp(nebo jakékoli jiné operace řízení toku, napříkladIfElseOp) mají zaručen konzistentní význam pouze v rámci tohoto rozsahu. Byla by to špatně definovaná konvence a obecně by se špatně používala.
Lokální testování s obvodem-šablonou
Pokud je Samplex sestaven pomocí funkce build(), měly by být hodnoty výstupních parametrů přímo kompatibilní s parametry přidruženého obvodu-šablony. Můžeš například svázat hodnoty jedné z randomizací se šablonou pro lokální prohlídku.
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)
To ti umožňuje ověřit, že samplex vytváří očekávané vzorky. Následující kód například převede svázaný obvod na objekt qiskit.quantum_info.Operator a složí ho s Pauliho hradly bitového překlopení, aby se vizuálně zkontrolovalo, že jsou všechny tři randomizace logicky ekvivalentní základnímu obvodu.
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()

Podobně následující kód předá hodnoty výstupních parametrů a šablonu do sampleru, aby simuloval 10 000 shotů všech randomizací, a porovná výsledné očekávané hodnoty (pouze registru alpha) s očekávanými hodnotami simulace základního obvodu.
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 ]

