# Tutorial 2: Entanglement Forging with Quantum Serverless¶

Entanglement forging is a method which allows us to represent expectation values of a 2n-qubit wavefunction as sums of multiple expectation values of n-qubit states, embedded in a classical optimization, thus doubling the size of the system that can be exactly simulated with a fixed number of qubits.

See Tutorial 1 for a high-level breakdown of the entanglement forging algorithm, or check out the explanatory material for a more detailed explanation.

## Adapting entanglement forging for use with Quantum Serverless¶

Quantum Serverless is a platform built to enable distributed computing across a variety of classical and quantum backends.

In this demo, we will show how to configure a ground state workflow with entanglement forging and seamlessly adapt it to run entirely on the cloud using Quantum Serverless.

## Instantiate the ElectronicStructureProblem¶

For this tutorial, we will model a system in which H2O is split on a magnesium surface, as described in Fig. 1(a) from arXiv:2203.07536. In particular, we’ll compute the ground state energy of the reactant in this reaction. Entanglement forging reduces the total number of qubits needed from 4 to 2.

Here we read in the system information, populate an IntegralDriver object from Circuit Knitting Toolbox, and use that driver to instantiate an ElectronicStructureProblem from Qiskit.

[1]:

from pathlib import Path
import numpy as np
from qiskit_nature.problems.second_quantization import ElectronicStructureProblem
from circuit_knitting_toolbox.utils import IntegralDriver

num_molecular_orbitals = 2

driver = IntegralDriver(
hcore=system_data["h1"],
mo_coeff=system_data["HF_mo_coeff"],
eri=system_data["Gamma_eri"],
num_alpha=system_data["na"],
num_beta=system_data["nb"],
nuclear_repulsion_energy=system_data["h0"],
)

problem = ElectronicStructureProblem(driver)


## Configure the entanglement forging specific inputs¶

The ansatz for Entanglement Forging consists of a set of input bitstrings and a parameterized circuit. (See the explanatory material for additional background on the method). For this demo, we will use the same bitstrings and ansatz for both the U and V subsystems, and we will use the TwoLocal circuit from Qiskit, along with hop gates.

[2]:

from qiskit.circuit import QuantumCircuit, Parameter

theta = Parameter("θ")

hop_gate = QuantumCircuit(2, name="hop_gate")
hop_gate.h(0)
hop_gate.cx(1, 0)
hop_gate.cx(0, 1)
hop_gate.ry(-theta, 0)
hop_gate.ry(-theta, 1)
hop_gate.cx(0, 1)
hop_gate.h(0)

print(f"Hop gate:\n{hop_gate.draw()}")

Hop gate:
┌───┐┌───┐     ┌────────────┐     ┌───┐
q_0: ┤ H ├┤ X ├──■──┤ Ry(-1.0*θ) ├──■──┤ H ├
└───┘└─┬─┘┌─┴─┐├────────────┤┌─┴─┐└───┘
q_1: ───────■──┤ X ├┤ Ry(-1.0*θ) ├┤ X ├─────
└───┘└────────────┘└───┘

[3]:

from qiskit.circuit.library import TwoLocal
from circuit_knitting_toolbox.entanglement_forging import EntanglementForgingAnsatz

entangler_map = [[0, 1]]
bitstrings_u = [(1, 0), (0, 1)]

circuit_u = TwoLocal(num_molecular_orbitals, [], hop_gate, entangler_map, reps=1)
ansatz = EntanglementForgingAnsatz(
circuit_u=circuit_u,
bitstrings_u=bitstrings_u,
)
ansatz.circuit_u.draw("mpl")

[3]:


## Set up the Qiskit Runtime Service¶

The Qiskit Runtime Service provides access to Qiskit Runtime Primitives and quantum backends. See the Qiskit Runtime documentation for more information. Here, we specify the backend(s) to be used to evaluate the circuits. Backends could be simulator(s) and/or quantum device(s).

Alternatively, if a Qiskit Runtime Service is not passed, then a local simulator will be used with the Qiskit Primitives, and the backend_names argument will be ignored.

[4]:

from qiskit_ibm_runtime import QiskitRuntimeService, Options

# By default, use a local simulator to implement the Qiskit Runtime Primitives
service = None

# Uncomment the following line to instead use the Qiskit Runtime Service.
# service = QiskitRuntimeService(channel="ibm_quantum")

backend_names = ["ibmq_qasm_simulator"] * 2

# If a single set of options are passed, it will be applied to all backends
options = [Options(execution={"shots": 1000}), Options(execution={"shots": 2000})]


## Set up Quantum Serverless¶

We can use Quantum Serverless to send the entanglement forging routine to a remote compute resource. For this tutorial, we will use our local CPU cores as the compute cluster. See the Quantum Serverless documentation (and below) for more information about how to use other clusters.

[5]:

from quantum_serverless import QuantumServerless

serverless = QuantumServerless()
serverless.providers()

[5]:

[<Provider: local>]


## Create a wrapper function to send to the remote cluster¶

Use Quantum Serverless to send the solve_remote method to a remote cluster.

Here we create a wrapper function for EntanglementForgingGroundStateSolver and its solve method, and we annotate it with the @run_qiskit_remote() decorator from Quantum Serverless. This allows us to call this function from a serverless context and have it sent for remote execution on the specified cluster.

[6]:

from typing import Optional, Sequence, Tuple, Dict, Union
from qiskit.algorithms.optimizers import Optimizer
from qiskit.result import Result
from quantum_serverless import run_qiskit_remote
from circuit_knitting_toolbox.entanglement_forging import (
EntanglementForgingGroundStateSolver,
)

@run_qiskit_remote()
def solve_remote(
problem: ElectronicStructureProblem,
ansatz: EntanglementForgingAnsatz,
optimizer: Optimizer,
service_args: Optional[Dict] = None,
backend_names: Optional[Union[str, Sequence[str]]] = None,
options_dicts: Optional[Union[Dict, Sequence[Dict]]] = None,
initial_point: Optional[Sequence[float]] = None,
orbitals_to_reduce: Optional[Sequence[int]] = None,
) -> Result:
"""
Wrapper function for running entanglement forging VQE on a remote resource.

Args:
- problem: A class encoding the problem to be solved
- ansatz: The ansatz
- optimizer: Optimizer to use to optimize the ansatz circuit parameters
- service_args: The arguments for instantiating a QiskitRuntimeService
- backend_names: List of backend names to use during parallel computation
- options_dicts: Options to use with backends
- initial_point: Initial values for ansatz parameters
- orbitals_to_reduce: List of orbital indices to remove from the problem before
decomposition.
Returns:
- An interpreted EigenstateResult
"""
service = QiskitRuntimeService(**service_args) if service_args else None

# Convert options dictionaries into Options objects
options = None
if options_dicts:
if isinstance(options_dicts, Dict):
options_dicts = [options_dicts]
options = [Options(**o) for o in options_dicts]

solver = EntanglementForgingGroundStateSolver(
ansatz=ansatz,
service=service,
optimizer=optimizer,
backend_names=backend_names,
options=options,
initial_point=initial_point,
orbitals_to_reduce=orbitals_to_reduce,
)
result = solver.solve(problem)

return result


## Run entanglement forging on a remote cluster¶

Once a user has set up their ElectronicStructureProblem, EntanglementForgingAnsatz, and other program options, the only thing remaining is call EntanglementForgingGroundStateSolver.solve, which we have wrapped in the solve_remote function.

We will call the solve_remote function within a QuantumServerless context, which means it will be run on the specified cluster. Remember, the default cluster for this demo will use the cores on our local CPU. To specify a new cluster, the QuantumServerless.set_provider method should be used.

When the remote function is called, it will return a “future” object, and Python will continue interpreting the next line of code. The get function is a blocking command which should be used to retrieve the results of the remote function via the “future” object. The program will not continue past the get call until the results of the remote function are returned.

[7]:

%%capture

from dataclasses import asdict
from qiskit.algorithms.optimizers import COBYLA
from quantum_serverless import get

optimizer = COBYLA()

# QiskitRuntimeService is not serializable, so we must convert it to a dictionary before passing to remote function
service_args = None if service is None else service.active_account()

# The Options class is not serializable, so we must convert it to a dictionary before passing to remote function
options_dicts = [asdict(o) for o in options]

with serverless:
forging_result_future = solve_remote(
problem,
ansatz,
optimizer,
service_args=service_args,
backend_names=backend_names,
options_dicts=options_dicts,
)
results = get(forging_result_future)


## Visualize the results¶

Visualize the convergence of the estimated ground state energy and the Schmidt coefficients as the ansatz parameters are optimized.

[8]:

import matplotlib.pyplot as plt

print("Energy:")
plt.plot([evaluation.eigenvalue for evaluation in results.history])
plt.xlabel("Iterations")
plt.show()

print("Schmidt Coefficients:")
plt.plot([abs(evaluation.eigenstate) for evaluation in results.history])
plt.xlabel("Iterations")
plt.yscale("log")
plt.show()

print("Parameters:")
plt.plot([evaluation.parameters for evaluation in results.history])
plt.xlabel("Iterations")
plt.show()

Energy:

Schmidt Coefficients:

Parameters:

[9]:

import qiskit.tools.jupyter

%qiskit_version_table


### Version Information

Qiskit SoftwareVersion
qiskit-terra0.22.1
qiskit-aer0.11.1
qiskit-ibmq-provider0.19.2
qiskit0.39.1
qiskit-nature0.4.5
System information
Python version3.9.13
Python compilerClang 12.0.0
Python buildmain, Aug 25 2022 18:29:29
OSDarwin
CPUs8
Memory (Gb)32.0
Thu Nov 03 18:12:06 2022 CDT