Reduzindo a profundidade do circuito com o addon Qiskit AQC-Tensor
Neste notebook, percorreremos as etapas de um Qiskit pattern ao mesmo tempo que utilizamos a compilação quântica aproximada com redes de tensores (AQC-Tensor) para alcançar uma profundidade de circuito menor do que normalmente seria necessária para realizar a evolução de Trotter.
Estas são as etapas que iremos seguir:
- Etapa 1: Mapear para o problema quântico
- Inicializar o Hamiltoniano e o(s) observável(is) do nosso problema
- Gerar um estado-alvo de rede de tensores para a porção inicial do circuito
- Gerar um circuito de baixa profundidade que aproxime a porção que está sendo comprimida
- Gerar um ansatz geral a partir desse circuito
- Otimizar os parâmetros para aproximar o ansatz o máximo possível do alvo
- Adicionar etapas subsequentes de Trotter ao ansatz otimizado
- Etapa 2: Otimizar para o hardware-alvo
- Transpilar o circuito para o hardware
- Etapa 3: Executar experimentos
- Usar um backend simulado por simplicidade
- Etapa 4: Reconstruir resultados
- N/A; em vez disso, apenas geramos a saída do observável medido
Etapa 1: Mapear para o circuito quântico e o operador
Configurar um Hamiltoniano modelo e um observável
Neste notebook, usamos o modelo de Ising em um círculo de 10 sítios:
onde as condições de contorno periódicas implicam que para obtemos , é a intensidade do acoplamento entre dois sítios e é o campo magnético externo.
# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-addon-aqc-tensor qiskit-addon-utils qiskit-ibm-runtime quimb scipy
from qiskit.transpiler import CouplingMap
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
# Generate some coupling map to use for this example
coupling_map = CouplingMap.from_heavy_hex(3, bidirectional=False)
# Choose a 10-qubit circle on this coupling map
reduced_coupling_map = coupling_map.reduce([0, 13, 1, 14, 10, 16, 4, 15, 3, 9])
# Get a qubit operator describing the Ising field model
hamiltonian = generate_xyz_hamiltonian(
reduced_coupling_map,
coupling_constants=(0.0, 0.0, 1.0),
ext_magnetic_field=(0.4, 0.0, 0.0),
)
O observável que mediremos é a magnetização total.
from qiskit.quantum_info import SparsePauliOp
L = reduced_coupling_map.size()
observable = SparsePauliOp.from_sparse_list([("Z", [i], 1 / L / 2) for i in range(L)], num_qubits=L)
Determinar quanto da evolução temporal simular classicamente
Nosso objetivo geral é simular a evolução temporal do Hamiltoniano modelo acima. Fazemos isso por meio da evolução de Trotter, que dividimos em duas porções:
- Uma porção inicial que pode ser simulada com estados em produto de matrizes (MPS). "Compilaremos" essa porção usando AQC, conforme apresentado em https://arxiv.org/abs/2301.08609.
- Uma porção subsequente do circuito que será executada em hardware. Vamos planejar usar o AQC-Tensor para comprimir nosso circuito de evolução temporal até o tempo , e então evoluir usando etapas comuns de Trotter até .
Gerar circuitos antes e depois da divisão
Agora que escolhemos dividir em , geraremos dois circuitos:
- Um circuito "alvo" para a porção AQC da evolução, de a . Como ele está sendo simulado por um simulador de rede de tensores, o número de camadas afeta o tempo de execução apenas por um fator constante; portanto, podemos usar um número generoso de camadas para minimizar o erro de Trotter.
from qiskit.synthesis import SuzukiTrotter
from qiskit_addon_utils.problem_generators import generate_time_evolution_circuit
aqc_evolution_time = 4.0
aqc_target_num_trotter_steps = 45
aqc_target_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),
time=aqc_evolution_time,
)
- Um circuito de evolução subsequente, que evolui de a . Como ele será executado em hardware quântico, é desejável usar o menor número possível de camadas de Trotter.
subsequent_evolution_time = 1.0
subsequent_num_trotter_steps = 5
subsequent_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),
time=subsequent_evolution_time,
)
Para fins de comparação posterior, vamos também gerar um terceiro circuito: um que evolui durante aqc_evolution_time, mas que tem o mesmo tempo de evolução por etapa de Trotter que o circuito subsequente. Esse é o circuito com o qual estaríamos trabalhando se não tivéssemos usado um número generoso de etapas de Trotter para o circuito-alvo. Iremos nos referir a ele como circuito de comparação.
aqc_comparison_num_trotter_steps = int(
subsequent_num_trotter_steps / subsequent_evolution_time * aqc_evolution_time
)
aqc_comparison_num_trotter_steps
20
comparison_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),
time=aqc_evolution_time,
)
Gerar um ansatz e parâmetros iniciais a partir de um circuito de Trotter com menos etapas
Primeiro, construímos um circuito "bom" que tem o mesmo tempo de evolução do circuito-alvo, mas com menos etapas de Trotter (e, portanto, menos camadas).
Em seguida, passamos esse circuito "bom" para a função generate_ansatz_from_circuit do AQC-Tensor. Essa função analisa a conectividade de dois qubits do circuito e retorna duas coisas:
- um circuito ansatz geral e parametrizado com a mesma conectividade de dois qubits do circuito de entrada; e,
- parâmetros que, quando inseridos no ansatz, produzem o circuito de entrada (bom).
Em breve, pegaremos esses parâmetros e os ajustaremos iterativamente para aproximar o circuito ansatz o máximo possível do MPS-alvo.
from qiskit_addon_aqc_tensor import generate_ansatz_from_circuit
aqc_ansatz_num_trotter_steps = 5
aqc_good_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),
time=aqc_evolution_time,
)
aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(
aqc_good_circuit, qubits_initially_zero=True
)
aqc_ansatz.draw("mpl", fold=-1)

print(f"Comparison circuit: depth {comparison_circuit.depth()}")
print(f"Target circuit: depth {aqc_target_circuit.depth()}")
print(f"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters")
Comparison circuit: depth 120
Target circuit: depth 270
Ansatz circuit: depth 23, with 515 parameters
Escolher configurações para a simulação de rede de tensores
Aqui, usamos o simulador de rede de tensores baseado em quimb. Neste exemplo, usamos o simulador de estado em produto de matrizes (MPS) do quimb, e usamos JAX para diferenciação automática. Veja a documentação da API para mais informações sobre como usar o simulador quimb.
from functools import partial
import quimb.tensor
from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator
simulator_settings = QuimbSimulator(
partial(quimb.tensor.CircuitMPS, max_bond=100, cutoff=1e-8),
autodiff_backend="jax",
)
Construir a representação em estado em produto de matrizes do estado-alvo do AQC
Em seguida, construímos uma representação em produto de matrizes do estado a ser aproximado pelo AQC.
from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit
aqc_target_mps = tensornetwork_from_circuit(aqc_target_circuit, simulator_settings)
Observe que, como escolhemos um número generoso de etapas de Trotter para o estado-alvo, ele tem, na verdade, menos erro de Trotter do que o circuito de comparação. Podemos calcular a fidelidade () do estado preparado pelo circuito de comparação em relação ao estado-alvo:
from qiskit_addon_aqc_tensor.simulation import compute_overlap
comparison_mps = tensornetwork_from_circuit(comparison_circuit, simulator_settings)
comparison_fidelity = abs(compute_overlap(comparison_mps, aqc_target_mps)) ** 2
comparison_fidelity
0.9996761790297157
Otimizar os parâmetros do ansatz usando cálculos com MPS
Aqui, minimizamos a função de custo mais simples possível, MaximizeStateFidelity, usando o otimizador L-BFGS do scipy.
Escolhemos um ponto de parada para a fidelidade de modo que ela fique acima do que o circuito de comparação teria, sem usar AQC. Uma vez atingido esse ponto, o circuito comprimido tem menos erro de Trotter e menor profundidade do que o circuito original. Com mais tempo de processamento, podem ser realizadas etapas adicionais de otimização para elevar ainda mais a fidelidade.
from scipy.optimize import OptimizeResult, minimize
from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity
objective = MaximizeStateFidelity(aqc_target_mps, aqc_ansatz, simulator_settings)
stopping_point = 1 - comparison_fidelity
def callback(intermediate_result: OptimizeResult):
print(f"Intermediate result: Fidelity {1 - intermediate_result.fun:.8}")
if intermediate_result.fun < stopping_point:
# Good enough for now
raise StopIteration
result = minimize(
objective.loss_function,
aqc_initial_parameters,
method="L-BFGS-B",
jac=True,
options={"maxiter": 100},
callback=callback,
)
if result.status not in (
0,
1,
99,
): # 0 => success; 1 => max iterations reached; 99 => early termination via StopIteration
raise RuntimeError(f"Optimization failed: {result.message} (status={result.status})")
print(f"Done after {result.nit} iterations.")
aqc_final_parameters = result.x
Intermediate result: Fidelity 0.95080335
Intermediate result: Fidelity 0.98408927
Intermediate result: Fidelity 0.99140876
Intermediate result: Fidelity 0.9951876
Intermediate result: Fidelity 0.99563147
Intermediate result: Fidelity 0.99646297
Intermediate result: Fidelity 0.99679298
Intermediate result: Fidelity 0.99715793
Intermediate result: Fidelity 0.99756604
Intermediate result: Fidelity 0.99804283
Intermediate result: Fidelity 0.99832283
Intermediate result: Fidelity 0.99856583
Intermediate result: Fidelity 0.99868698
Intermediate result: Fidelity 0.998867
Intermediate result: Fidelity 0.99902237
Intermediate result: Fidelity 0.99912174
Intermediate result: Fidelity 0.99919705
Intermediate result: Fidelity 0.99926724
Intermediate result: Fidelity 0.99938605
Intermediate result: Fidelity 0.99951297
Intermediate result: Fidelity 0.99956172
Intermediate result: Fidelity 0.99962274
Intermediate result: Fidelity 0.99963919
Intermediate result: Fidelity 0.99967423
Intermediate result: Fidelity 0.9997101
Done after 25 iterations.
Construir o circuito final para passar ao transpilador
final_circuit = aqc_ansatz.assign_parameters(aqc_final_parameters)
final_circuit.compose(subsequent_circuit, inplace=True)
final_circuit.draw("mpl", fold=-1)

Etapa 2: Transpilar para execução no hardware-alvo
Na Etapa 2 de um Qiskit pattern, transpilamos esse circuito e qualquer observável desejado para execução em um dispositivo-alvo. Aqui, estamos usando um backend simulado fornecido pelo qiskit-ibm-runtime.
from qiskit import transpile
from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2
backend = FakeMelbourneV2()
isa_circuit = transpile(final_circuit, backend)
isa_observable = observable.apply_layout(isa_circuit.layout)
O circuito ISA resultante pode então ser enviado para execução no backend (etapa 3 de um Qiskit pattern).
Etapa 3: Executar em hardware quântico
from qiskit_ibm_runtime import EstimatorV2 as Estimator
estimator = Estimator(backend)
job = estimator.run([(isa_circuit, isa_observable)])
pub_result = job.result()[0]
Etapa 4: Reconstruir
A reconstrução não é necessária no nosso caso. Podemos simplesmente analisar o resultado.
pub_result.data.evs[()]
np.float64(0.047998046875000006)