Pular para o conteúdo principal

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:

H^Ising=i=110Ji,(i+1)ZiZ(i+1)+hiXi,\hat{\mathcal{H}}_{\text{Ising}} = \sum_{i=1}^{10} J_{i,(i+1)} Z_i Z_{(i+1)} + h_i X_i \, ,

onde as condições de contorno periódicas implicam que para i=10i=10 obtemos i+1=111i+1=11\rightarrow1, JJ é a intensidade do acoplamento entre dois sítios e hh é 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:

  1. 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.
  2. 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 t=4t=4, e então evoluir usando etapas comuns de Trotter até t=5t=5.

Gerar circuitos antes e depois da divisão

Agora que escolhemos dividir em t=4t=4, geraremos dois circuitos:

  1. Um circuito "alvo" para a porção AQC da evolução, de ti=0t_i=0 a tf=4t_f=4. 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,
)
  1. Um circuito de evolução subsequente, que evolui de ti=4t_i=4 a tf=5t_f=5. 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:

  1. um circuito ansatz geral e parametrizado com a mesma conectividade de dois qubits do circuito de entrada; e,
  2. 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)

Quantum circuit diagram

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 (ψ1ψ22| \langle \psi_1 | \psi_2 \rangle |^2) 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)

Quantum circuit diagram

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)