Skip to content

Commit

Permalink
[QI2-1307] Hybrid (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mythir authored Jan 17, 2025
1 parent c2fc270 commit 3baadcf
Showing 15 changed files with 894 additions and 459 deletions.
5 changes: 1 addition & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
{
"python.analysis.extraPaths": [
"qiskit_quantuminspire"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.pytestPath": "${command:python.interpreterPath}",
"python.testing.autoTestDiscoverOnSaveEnabled": true,
"python.testing.cwd": "${workspaceFolder}",
}
}
5 changes: 1 addition & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -64,10 +64,7 @@
templates_path = ["_templates"]

# The suffix of source filenames.
# source_suffix = {
# ".rst": "restructuredtext",
# ".md": "markdown",
# }
# source_suffix = {".rst": "restructuredtext", ".md": "markdown"}

# The encoding of source files.
# source_encoding = 'utf-8-sig'
67 changes: 67 additions & 0 deletions docs/hybrid/basics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Basics

Quantum Inspire 2 supports hybrid classical-quantum algorithms, and allows execution of both the quantum and the classical part fully on the QI2 servers.

## Script format

Any script that will be executed on the QI2 platform is required to have at least the `execute()` and `finalize()` functions.

### Execute

The execute function is the function that gets called by QI2 first. As an argument it gets a `QuantumInterface`, which is what allows you to actually execute your circuit. A simple example, where a function `generate_circuit()` is called that returns cQASM as a string, is shown below. In this example, the same circuit is executed 5 times in a row as a simple illustration of the interchanging control between the hybrid and classical domains. Normally, you might want to change your `QuantumCircuit` between each execution based on your results.

```python
def execute(qi: QuantumInterface) -> None:
"""Run the classical part of the Hybrid Quantum/Classical Algorithm.
Args:
qi: A QuantumInterface instance that can be used to execute quantum circuits
The qi object has a single method called execute_circuit, its interface is described below:
qi.execute_circuit args:
circuit: a string representation of a quantum circuit
number_of_shots: how often to execute the circuit
qi.execute_circuit return value:
The results of executing the quantum circuit, this is an object with the following attributes
results: The results from iteration n-1.
shots_requested: The number of shots requested by the user for the previous iteration.
shots_done: The number of shots actually run.
"""
for i in range(1, 5):
circuit = generate_circuit()
_ = qi.execute_circuit(circuit, 1024)
```

### Finalize

The `finalize()` function allows you to aggregate your results. It should return a dictionary which will be stored as the final result. By default it takes a list of all measurements as an argument, but through the use of globals you could also export other data.

```python
def finalize(list_of_measurements: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Aggregate the results from all iterations into one final result.
Args:
list_of_measurements: List of all results from the previous iterations.
Returns:
A free-form result, with a `property`: `value` structure. Value can
be everything serializable.
"""
return {"results": list_of_measurements}
```

## Execution

A complete script with both example functions can be found [here](./hqca_circuit.py). This can be uploaded to QI2 as follows (using the CLI), where `<backend_id>` is the id number of the selected quantum backend, which can be retrieved using the Qiskit-QuantumInspire QIProvider.

```bash
qi files upload ./hqca_circuit.py <backend_id>
```

The final results can be retrieved as follows, where `<job_id>` is the job id returned by the previous command.

```bash
qi final_results get <job_id>
```
53 changes: 53 additions & 0 deletions docs/hybrid/hqca_circuit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Any, Dict, List

from qiskit.circuit import QuantumCircuit

from qiskit_quantuminspire import cqasm
from qiskit_quantuminspire.hybrid.quantum_interface import QuantumInterface


def generate_circuit() -> str:
# Create a basic Bell State circuit:
qc = QuantumCircuit(2, 2)
qc.reset(0)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

return cqasm.dumps(qc)


def execute(qi: QuantumInterface) -> None:
"""Run the classical part of the Hybrid Quantum/Classical Algorithm.
Args:
qi: A QuantumInterface instance that can be used to execute quantum circuits
The qi object has a single method called execute_circuit, its interface is described below:
qi.execute_circuit args:
circuit: a string representation of a quantum circuit
number_of_shots: how often to execute the circuit
qi.execute_circuit return value:
The results of executing the quantum circuit, this is an object with the following attributes
results: The results from iteration n-1.
shots_requested: The number of shots requested by the user for the previous iteration.
shots_done: The number of shots actually run.
"""
for i in range(1, 5):
circuit = generate_circuit()
_ = qi.execute_circuit(circuit, 1024)


def finalize(list_of_measurements: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Aggregate the results from all iterations into one final result.
Args:
list_of_measurements: List of all results from the previous iterations.
Returns:
A free-form result, with a `property`: `value` structure. Value can
be everything serializable.
"""
return {"results": list_of_measurements}
68 changes: 68 additions & 0 deletions docs/hybrid/hydrogen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# (H2) Hydrogen molecuole ground state energy determined using VQE with a UCCSD-ansatz function.
# Compared with Hartee-Fock energies and with energies calculated by NumPyMinimumEigensolver
# This script is based on the Qiskit Chemistry tutorials
import warnings
from dataclasses import dataclass
from typing import Any, Dict, List

from qiskit.primitives import BackendEstimator
from qiskit_algorithms import NumPyMinimumEigensolverResult, VQEResult
from qiskit_algorithms.minimum_eigensolvers import VQE
from qiskit_algorithms.optimizers import COBYLA
from qiskit_nature.second_q.circuit.library import UCCSD, HartreeFock
from qiskit_nature.second_q.drivers import PySCFDriver
from qiskit_nature.second_q.mappers import ParityMapper

from qiskit_quantuminspire.hybrid.hybrid_backend import QIHybridBackend
from qiskit_quantuminspire.hybrid.quantum_interface import QuantumInterface

resultstring = ""


@dataclass
class _GroundStateEnergyResults:
result: VQEResult | NumPyMinimumEigensolverResult
nuclear_repulsion_energy: float


def calculate_H0(backend: QIHybridBackend, distance: float = 0.735) -> _GroundStateEnergyResults:
mapper = ParityMapper(num_particles=(1, 1))
molecule = f"H 0.0 0.0 0.0; H 0.0 0.0 {distance}"
driver = PySCFDriver(molecule)
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=UserWarning)
es_problem = driver.run()

fermionic_op = es_problem.hamiltonian.second_q_op()
qubit_op = mapper.map(fermionic_op)
n_particles = es_problem.num_particles
n_spatial_orbitals = es_problem.num_spatial_orbitals

nuclear_repulsion_energy = es_problem.nuclear_repulsion_energy

initial_state = HartreeFock(n_spatial_orbitals, n_particles, mapper)
ansatz = UCCSD(n_spatial_orbitals, n_particles, mapper, initial_state=initial_state)

optimizer = COBYLA(maxiter=1) # 10 iterations take two minutes
estimator = BackendEstimator(backend=backend)

algo = VQE(estimator, ansatz, optimizer)
result = algo.compute_minimum_eigenvalue(qubit_op)

print(f"{distance=}: nuclear_repulsion_energy={nuclear_repulsion_energy}, eigenvalue={result.eigenvalue}")
global resultstring
resultstring += (
f"{distance=}: nuclear_repulsion_energy={nuclear_repulsion_energy}, eigenvalue={result.eigenvalue}\n"
)
return _GroundStateEnergyResults(result, nuclear_repulsion_energy)


def execute(qi: QuantumInterface) -> None:
c = calculate_H0(backend=QIHybridBackend(qi))
global resultstring
resultstring += str(c)
print(c)


def finalize(list_of_measurements: Dict[int, List[Any]]) -> Dict[str, Any]:
return {"measurements": list_of_measurements, "resultstring": resultstring}
12 changes: 12 additions & 0 deletions docs/hybrid/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
======================
Hybrid Algorithms
======================

Unlock the power of hybrid quantum-classical computing with Qiskit-QuantumInspire.

.. toctree::
:maxdepth: 2

Basics <basics>
Leveraging Qiskit <leveraging_qiskit>

3 changes: 3 additions & 0 deletions docs/hybrid/leveraging_qiskit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Leveraging Qiskit

Qiskit has a lot more to offer than execution of simple `QuantumCircuits`, and because QI2 allows for execution of both the classical and quantum parts of a hybrid algorithm you can achieve fast execution of many of the quantum algorithms the Qiskit ecosystem has to offer. Apart from ensuring your scripts use the format shown [here](./basics.md), you are required to use the special `QIHybridBackend()` as your Qiskit backend object. This backend ensures the correct quantum backend is inferred from what you selected when uploading the script. A full working example (that can be uploaded to QI2 as-is) can be found [here](./hydrogen.py).
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ Welcome to the documentation of **Qiskit-QuantumInspire**, a Qiskit plugin for t

Documentation Home <index>
Getting Started <getting_started/index>
Hybrid Algorithms <hybrid/index>
Example Notebooks <notebooks/index>
Contributing and Development <contributing>
License <license>
Loading

0 comments on commit 3baadcf

Please sign in to comment.