Wire Cutting Expresso como uma Instrução `Move` de Dois Qubits
Neste tutorial, reconstruiremos os valores esperados de um circuito de sete qubits dividindo-o em dois circuitos de quatro qubits usando wire cutting.
Estas são as etapas que vamos seguir neste padrão Qiskit:
- Etapa 1: Mapear o problema para circuitos quânticos e operadores:
- Mapear o hamiltoniano em um circuito quântico.
- Etapa 2: Otimizar para o hardware alvo [Usa o cutting addon]:
- Cortar o circuito e o observável.
- Transpilar os subexperimentos para o hardware.
- Etapa 3: Executar no hardware alvo:
- Executar os subexperimentos obtidos na Etapa 2 usando uma primitiva
Sampler.
- Executar os subexperimentos obtidos na Etapa 2 usando uma primitiva
- Etapa 4: Pós-processar os resultados [Usa o cutting addon]:
- Combinar os resultados da Etapa 3 para reconstruir o valor esperado do observável em questão.
Etapa 1: Mapear
Crie um circuito para cortar
Primeiro, começamos com um circuito inspirado na Fig. 1(a) de arXiv:2302.03366v1.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit import QuantumCircuit
qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
<qiskit.circuit.instructionset.InstructionSet at 0x7f16ab191a80>
qc_0.draw("mpl")

Especifique um observável
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])
Etapa 2: Otimizar
Crie um novo circuito onde instruções Move foram colocadas nos locais de corte desejados
Dado o circuito acima, gostaríamos de colocar dois wire cuts na linha do qubit central, de modo que o circuito possa ser separado em dois circuitos de quatro qubits cada. Uma maneira de fazer isso é colocar manualmente instruções Move de dois qubits que movem o estado de um fio de qubit para outro. Uma instrução Move é conceitualmente equivalente a uma operação de reset no segundo qubit, seguida por uma porta SWAP. O efeito dessa instrução é transferir o estado do primeiro qubit (origem) para o segundo qubit (destino), enquanto descarta o estado de entrada do segundo qubit. Para que isso funcione como pretendido, é importante que o segundo qubit (destino) não compartilhe nenhum entrelaçamento com o restante do sistema; caso contrário, a operação de reset fará com que o estado do restante do sistema seja parcialmente colapsado.
Aqui, construímos um novo circuito com um qubit adicional e as operações Move em vigor. Neste exemplo, somos capazes de reutilizar um qubit: o qubit de origem do primeiro Move se torna o qubit de destino da segunda operação Move.
Nota: Como alternativa para trabalhar diretamente com instruções Move, pode-se optar por marcar wire cuts usando uma instrução CutWire de um único qubit. A função cut_wires existe para transformar CutWires em instruções Move em qubits recém-alocados. No entanto, em contraste com o método manual, esse método automático não permite a reutilização de fios de qubits. Veja o guia de instruções do CutWire para mais detalhes.
from qiskit_addon_cutting.instructions import Move
qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.draw("mpl")

Crie um observável que acompanhe o novo circuito
Este observável corresponde a observable, mas devemos contabilizar corretamente o fio de qubit extra que foi adicionado (ou seja, inserimos um "I" no índice 4). Note que no Qiskit, na representação em string o qubit-0 corresponde ao caractere Pauli mais à direita.
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
Separe o circuito e os observáveis
Como nos tutoriais anteriores, qubits que compartilham um rótulo de partição comum serão agrupados, e portas não locais que abrangem mais de uma partição serão cortadas.
from qiskit_addon_cutting import partition_problem
partitioned_problem = partition_problem(
circuit=qc_1, partition_labels="AAAABBBB", observables=observable_expanded.paulis
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
Visualize o problema decomposto
subobservables
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']),
'B': PauliList(['ZIII', 'IIII', 'IIII'])}
subcircuits["A"].draw("mpl")

subcircuits["B"].draw("mpl")

Calcule o sampling overhead para os cortes escolhidos
Aqui cortamos dois fios, resultando em um sampling overhead de .
Para mais detalhes sobre o sampling overhead incorrido pelo circuit cutting, consulte o material explicativo.
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 256.0
Gere os subexperimentos para executar no backend
generate_cutting_experiments aceita os argumentos circuits/observables como dicionários que mapeiam rótulos de partição de qubits para os respectivos subcircuit/subobservables.
Para simular o valor esperado do circuito de tamanho completo, muitos subexperimentos são gerados a partir da distribuição de quasiprobabilidade conjunta das portas decompostas e então executados em um ou mais backends. O número de amostras retiradas da distribuição é controlado por num_samples, e um coeficiente combinado é dado para cada amostra única. Para mais informações sobre como os coeficientes são calculados, consulte o material explicativo.
from qiskit_addon_cutting import generate_cutting_experiments
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)
Escolha um backend
Aqui estamos usando um backend simulado (fake), o que fará com que o Qiskit Runtime seja executado em modo local (ou seja, em um simulador local).
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
backend = FakeManilaV2()
Prepare os subexperimentos para o backend
Devemos transpilar os circuitos com nosso backend como alvo antes de submetê-los ao Qiskit Runtime.
from qiskit.transpiler import generate_preset_pass_manager
# Transpile the subexperiments to ISA circuits
pass_manager = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
Etapa 3: Executar
Execute os subexperimentos usando a primitiva Sampler do Qiskit Runtime
from qiskit_ibm_runtime import SamplerV2, Batch
# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
/home/garrison/Qiskit/qiskit-ibm-runtime/qiskit_ibm_runtime/session.py:157: UserWarning: Session is not supported in local testing mode or when using a simulator.
warnings.warn(
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
Etapa 4: Pós-processar
Reconstrua o valor esperado
Reconstrua os valores esperados para cada termo do observável e combine-os para reconstruir o valor esperado do observável original.
from qiskit_addon_cutting import reconstruct_expectation_values
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
Compare o valor esperado reconstruído com o valor esperado exato do circuito e observável originais
from qiskit_aer.primitives import EstimatorV2
estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, observable)]).result()[0].data.evs
print(f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}")
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}")
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 1.51319069
Exact expectation value: 1.59099026
Error in estimation: -0.07779957
Relative error in estimation: -0.04890009