Inquanto logo

Tutorial: InQuanto + qnexus

Hamiltonian averaging project on H1 Emulator using InQuanto and qnexus to estimate the ground-state energy for CH2

This notebook demonstrates a computable workflow with InQuanto and Nexus. To access Nexus, the user needs to use inquanto.extensions.nexus to submit circuits and retrieve results. Today, the intention is to submit circuits to the H1-1 emulator (H1-1E) for execution. Once results are available, they can be retrieved and processed by InQuanto to determine the ground-state energy.

Quantinuum Nexus logo

1. Nexus Project Name Setup

We can use a new Nexus project for managing our chemistry jobs, or we can re-use an old one.

from qnexus.client import projects

project_name = f"InQuanto::Project"

project_ref = projects.get_or_create(
    name=project_name, description="A demo project with inquanto-nexus", properties={}
)

2. Chemical Specification

Here we will run an example calculation on methylene, CH2.

Initially, the geometry of CH2 is specified, loaded into inquanto.geometries.GeometryMolecular and rendered as a pandas DataFrame.

The geometry we use is from CCSD(T)=FULL/aug-cc-pVTZ level calculations presented on the Computational Chemistry Comparison and Benchmark DataBase. https://cccbdb.nist.gov/

from inquanto.geometries import GeometryMolecular

xyz = [
    ["C", [0.0, 0.0, 0.1051320]],
    ["H", [0.0, 0.9882630,-0.3153960]],
    ["H", [0.0, -0.9882630, -0.3153960]],
]

geometry = GeometryMolecular(xyz, "angstrom")
geometry.df
Element X Y Z Atom
id
0 C 0.0 0.000000 0.105132 C1
1 H 0.0 0.988263 -0.315396 H2
2 H 0.0 -0.988263 -0.315396 H3

This structure can be visualised using NGLview, via the InQuanto-NGLView extension.

#from inquanto.extensions.nglview import VisualizerNGL

#visualizer_nglview = VisualizerNGL(geometry)
#visualizer_nglview.visualize_molecule()

Next, we construct an InQuanto driver object, which is used to prepare the qubit operators and spaces needed to construct the circuits.

To do this, we use the InQuanto interface to PySCF.

For a basic driver we need to specify the charge and multiplicity of our system. Here we construct a more advanced driver by applying an orbital rotation (transf). This transformation is to the basis of natural orbitals obtained after performing a CASSCF calculation with an active space of 2 electrons in 3 spatial (6-spin) orbitals

from inquanto.extensions.pyscf import (
    FromActiveSpace,
    ChemistryDriverPySCFMolecularROHF,
    CASSCF,
)

charge = 0
basis = "sto-3g"
multiplicity = 1
ncas = 3
nelecas = 2
frozen = FromActiveSpace(ncas=ncas, nelecas=nelecas) 
transf = CASSCF(ncas=ncas, nelecas=nelecas)
point_group_symmetry=True

driver_parameters = {
    "geometry": geometry, # As a GeometryMolecular object
    "charge": charge,
    "basis": basis,
    "transf": transf,
    "frozen": frozen,
    "multiplicity": multiplicity,
    "point_group_symmetry": True,
}

driver = ChemistryDriverPySCFMolecularROHF(**driver_parameters)

When the driver variables are defined we can use its get_system method to obtain the fermionic hamiltonian, space, and state. We’ll carry these objects forward to build our circuits.

fermion_hamiltonian, fermion_space, fermion_state = driver.get_system()

We can also inspect various results of our calculation, such as the point group symmetry or orbital symmetries.

f"This structure has a point group of {driver.point_group}"
'This structure has a point group of C2v'

As well as obtaining the objects needed to construct circuits, we can also use some driver convenience functions to perform classical computational chemistry methods to give results for comparison.

Here we calculate Coupled Cluster Singles & Doubles (CCSD) energies classically for comparison.

ccsd_energy = driver.run_ccsd()
scf_energy = driver.run_hf()
ccsd_correlation_energy = scf_energy - ccsd_energy 
# correlation is positive: ccsd should be lower in energy than HF

print(f"Hartree Fock (RHF) energy: {scf_energy} Ha")
print(f"CCSD energy: {ccsd_energy} Ha")
print(f"CCSD energy correlation energy: {ccsd_correlation_energy} Ha") 
Hartree Fock (RHF) energy: -38.33809020351786 Ha
CCSD energy: -38.3588270738749 Ha
CCSD energy correlation energy: 0.02073687035704097 Ha

3. Review Hamiltonian

Here, we inspect the fermionic Hamiltonian we generated for CH2 with HF in the STO-3G basis set. Initially, we view the Fermionic strings. Each index in the string corresponds to action on a spin orbital. The symbol, ^, following an index denotes electron creation, otherwise it is an electron destroying action. Our Hamiltonian contains 132 Fermionic strings (discluding the identity term).

fermion_hamiltonian.df()
Coefficient Term
0 -37.107696
1 -0.968197 F0^ F0
2 -0.030656 F0^ F4
3 0.706001 F1^ F0^ F0 F1
4 0.023347 F1^ F0^ F0 F5
5 0.052673 F1^ F0^ F2 F3
6 0.023347 F1^ F0^ F4 F1
7 0.024685 F1^ F0^ F4 F5
8 -0.968197 F1^ F1
9 -0.030656 F1^ F5
10 0.595165 F2^ F0^ F0 F2
11 0.052673 F2^ F0^ F2 F0
12 -0.018602 F2^ F0^ F2 F4
13 0.027106 F2^ F0^ F4 F2
14 0.595165 F2^ F1^ F1 F2
15 0.052673 F2^ F1^ F3 F0
16 -0.018602 F2^ F1^ F3 F4
17 0.027106 F2^ F1^ F5 F2
18 -0.895007 F2^ F2
19 0.595165 F3^ F0^ F0 F3
20 0.052673 F3^ F0^ F2 F1
21 -0.018602 F3^ F0^ F2 F5
22 0.027106 F3^ F0^ F4 F3
23 0.595165 F3^ F1^ F1 F3
24 0.052673 F3^ F1^ F3 F1
25 -0.018602 F3^ F1^ F3 F5
26 0.027106 F3^ F1^ F5 F3
27 0.052673 F3^ F2^ F0 F1
28 -0.018602 F3^ F2^ F0 F5
29 0.672833 F3^ F2^ F2 F3
30 -0.018602 F3^ F2^ F4 F1
31 0.048912 F3^ F2^ F4 F5
32 -0.895007 F3^ F3
33 -0.030656 F4^ F0
34 0.451412 F4^ F0^ F0 F4
35 0.024685 F4^ F0^ F4 F0
36 0.023347 F4^ F1^ F1 F0
37 0.451412 F4^ F1^ F1 F4
38 -0.018602 F4^ F1^ F3 F2
39 0.024685 F4^ F1^ F5 F0
40 0.025232 F4^ F1^ F5 F4
41 -0.018602 F4^ F2^ F0 F2
42 0.027106 F4^ F2^ F2 F0
43 0.500599 F4^ F2^ F2 F4
44 0.048912 F4^ F2^ F4 F2
45 -0.018602 F4^ F3^ F1 F2
46 0.027106 F4^ F3^ F3 F0
47 0.500599 F4^ F3^ F3 F4
48 0.048912 F4^ F3^ F5 F2
49 -0.271034 F4^ F4
50 0.023347 F5^ F0^ F0 F1
51 0.451412 F5^ F0^ F0 F5
52 -0.018602 F5^ F0^ F2 F3
53 0.024685 F5^ F0^ F4 F1
54 0.025232 F5^ F0^ F4 F5
55 -0.030656 F5^ F1
56 0.451412 F5^ F1^ F1 F5
57 0.024685 F5^ F1^ F5 F1
58 -0.018602 F5^ F2^ F0 F3
59 0.027106 F5^ F2^ F2 F1
60 0.500599 F5^ F2^ F2 F5
61 0.048912 F5^ F2^ F4 F3
62 -0.018602 F5^ F3^ F1 F3
63 0.027106 F5^ F3^ F3 F1
64 0.500599 F5^ F3^ F3 F5
65 0.048912 F5^ F3^ F5 F3
66 0.024685 F5^ F4^ F0 F1
67 0.025232 F5^ F4^ F0 F5
68 0.048912 F5^ F4^ F2 F3
69 0.025232 F5^ F4^ F4 F1
70 0.561441 F5^ F4^ F4 F5
71 -0.271034 F5^ F5

The Hamiltonian can now be converted into a qubit representation. We use the Jordan-Wigner transformation to accomplish this task. Each Fermionic string is now transformed to a Pauli operator (I, X, Y, Z) acting on a qubit q[i], where:

  • q is the name of the qubit register;
  • i is the qubit index.
. We refer to each term in this new representation as ‘Pauli word’. After the transformation, the Hamiltonian in the Qubit Pauli basis contains 61 terms, not including identity (I)

from inquanto.mappings import QubitMappingJordanWigner

jw_map = QubitMappingJordanWigner()
qubit_hamiltonian_jw = fermion_hamiltonian.qubit_encode(jw_map)
qubit_hamiltonian_jw.df()
Coefficient Term Coefficient type
0 -37.272826 <class 'numpy.float64'>
1 -0.462449 Z5 <class 'numpy.float64'>
2 -0.462449 Z4 <class 'numpy.float64'>
3 0.140360 Z4 Z5 <class 'numpy.float64'>
4 -0.243190 Z3 <class 'numpy.float64'>
5 0.112922 Z3 Z5 <class 'numpy.float64'>
6 0.125150 Z3 Z4 <class 'numpy.float64'>
7 -0.012228 X2 X3 Y4 Y5 <class 'numpy.float64'>
8 0.012228 X2 Y3 Y4 X5 <class 'numpy.float64'>
9 0.012228 Y2 X3 X4 Y5 <class 'numpy.float64'>
10 -0.012228 Y2 Y3 X4 X5 <class 'numpy.float64'>
11 -0.243190 Z2 <class 'numpy.float64'>
12 0.125150 Z2 Z5 <class 'numpy.float64'>
13 0.112922 Z2 Z4 <class 'numpy.float64'>
14 0.168208 Z2 Z3 <class 'numpy.float64'>
15 -0.006776 X1 Z3 Z4 X5 <class 'numpy.float64'>
16 0.004650 X1 X2 Y3 Y4 <class 'numpy.float64'>
17 -0.004650 X1 Y2 Y3 X4 <class 'numpy.float64'>
18 -0.011427 X1 Z2 Z4 X5 <class 'numpy.float64'>
19 -0.006308 X1 Z2 Z3 X5 <class 'numpy.float64'>
20 0.015020 X1 Z2 Z3 Z4 X5 <class 'numpy.float64'>
21 -0.006776 Y1 Z3 Z4 Y5 <class 'numpy.float64'>
22 -0.004650 Y1 X2 X3 Y4 <class 'numpy.float64'>
23 0.004650 Y1 Y2 X3 X4 <class 'numpy.float64'>
24 -0.011427 Y1 Z2 Z4 Y5 <class 'numpy.float64'>
25 -0.006308 Y1 Z2 Z3 Y5 <class 'numpy.float64'>
26 0.015020 Y1 Z2 Z3 Z4 Y5 <class 'numpy.float64'>
27 -0.196350 Z1 <class 'numpy.float64'>
28 0.106682 Z1 Z5 <class 'numpy.float64'>
29 0.112853 Z1 Z4 <class 'numpy.float64'>
30 0.135623 Z1 Z3 <class 'numpy.float64'>
31 0.148791 Z1 Z2 <class 'numpy.float64'>
32 -0.005837 X0 Z2 Z3 X4 <class 'numpy.float64'>
33 -0.006171 X0 X1 Y4 Y5 <class 'numpy.float64'>
34 -0.013168 X0 X1 Y2 Y3 <class 'numpy.float64'>
35 0.006171 X0 Y1 Y4 X5 <class 'numpy.float64'>
36 0.013168 X0 Y1 Y2 X3 <class 'numpy.float64'>
37 -0.011427 X0 Z1 Z3 X4 <class 'numpy.float64'>
38 -0.004650 X0 Z1 X2 X3 Z4 X5 <class 'numpy.float64'>
39 -0.004650 X0 Z1 X2 Y3 Z4 Y5 <class 'numpy.float64'>
40 -0.006776 X0 Z1 Z2 X4 <class 'numpy.float64'>
41 0.015020 X0 Z1 Z2 Z3 X4 <class 'numpy.float64'>
42 -0.006308 X0 Z1 Z2 Z3 X4 Z5 <class 'numpy.float64'>
43 -0.005837 Y0 Z2 Z3 Y4 <class 'numpy.float64'>
44 0.006171 Y0 X1 X4 Y5 <class 'numpy.float64'>
45 0.013168 Y0 X1 X2 Y3 <class 'numpy.float64'>
46 -0.006171 Y0 Y1 X4 X5 <class 'numpy.float64'>
47 -0.013168 Y0 Y1 X2 X3 <class 'numpy.float64'>
48 -0.011427 Y0 Z1 Z3 Y4 <class 'numpy.float64'>
49 -0.004650 Y0 Z1 Y2 X3 Z4 X5 <class 'numpy.float64'>
50 -0.004650 Y0 Z1 Y2 Y3 Z4 Y5 <class 'numpy.float64'>
51 -0.006776 Y0 Z1 Z2 Y4 <class 'numpy.float64'>
52 0.015020 Y0 Z1 Z2 Z3 Y4 <class 'numpy.float64'>
53 -0.006308 Y0 Z1 Z2 Z3 Y4 Z5 <class 'numpy.float64'>
54 -0.196350 Z0 <class 'numpy.float64'>
55 0.112853 Z0 Z5 <class 'numpy.float64'>
56 0.106682 Z0 Z4 <class 'numpy.float64'>
57 0.148791 Z0 Z3 <class 'numpy.float64'>
58 0.135623 Z0 Z2 <class 'numpy.float64'>
59 -0.005837 Z0 X1 Z2 Z3 Z4 X5 <class 'numpy.float64'>
60 -0.005837 Z0 Y1 Z2 Z3 Z4 Y5 <class 'numpy.float64'>
61 0.176500 Z0 Z1 <class 'numpy.float64'>

4. Chemically Aware Unitary Coupled Cluster State-Preparation

We now build the Unitary Coupled Cluster Singles & Doubles (UCCSD) state-preparation circuit. This captures a similar level of electronic correlation as classical CCSD.

Inside the state and space from our driver is symmetry information and orbital occupancies, which are now inputted into the FermionSpaceAnsatzChemicallyAwareUCCSD object to generate the UCCSD state-preparation circuit. We can inspect some of this data as such:

fermion_space.print_state(fermion_state)
 0 0a         :  1    
 1 0b         :  1    
 2 1a         :  0    
 3 1b         :  0    
 4 2a         :  0    
 5 2b         :  0

Then, we use these quantities to build the ansatz state-preparation circuit:

from inquanto.ansatzes import FermionSpaceAnsatzChemicallyAwareUCCSD
from pytket.circuit import OpType

ansatz = FermionSpaceAnsatzChemicallyAwareUCCSD(fermion_space, fermion_state)

As a first, we print some basic circuit statistics:

  • # of qubits;
  • # of symbols;
  • # of CX gates.


print(f"# of qubits: {ansatz.n_qubits}")
print(f"# of symbols: {ansatz.n_symbols}")
print(f"# of 2=qubit gates: {ansatz.circuit_resources()['gates_2q']}")
# of qubits: 6
# of symbols: 4
# of 2=qubit gates: 23

We can also render the State-Preparation circuit in jupyter for inspection

from pytket.circuit.display import render_circuit_jupyter

render_circuit_jupyter(ansatz.state_circuit)

5. Perform a classical VQE project to obtain parameters that characterise the ground-state wavefunction

In the cell below we perform a variational quantum eigensolver using inquanto’s express module. This is a statevector backed only convenience function. Firstly we instantiate a statevector NexusConfig into our NexusBackend. Then we provide the run_vqe algorithm class with the qubit hamiltonian and the ansatz circuit. A loop then beings where, at each step, the statevector job is submitted to our Nexus project and executed. These jobs can be inspected as https://nexus.quantinuum.com/ . When the VQE cycle has converged with respect to energy the cycle stops and we can inspect the total energy obtained and the symbol coefficients of our parameterized circuit

from inquanto.express import run_vqe
from qnexus import AerStateConfig
import warnings

warnings.filterwarnings("ignore")

vqe = run_vqe(ansatz, qubit_hamiltonian_jw, AerStateConfig(), with_gradient=False, project_ref=project_ref)
print(f"VQE Energy: {vqe.final_value}")
gs_parameters = vqe.final_parameters
print(vqe.final_parameters)
# TIMER BLOCK-0 BEGINS AT 2025-06-23 15:14:18.750726
# TIMER BLOCK-0 ENDS - DURATION (s): 1310.9996753 [0:21:50.999675]
VQE Energy: -38.35882707383371
{d0: np.float64(-0.37329427840313417), d1: np.float64(-0.004353051918026713), s0: np.float64(-2.9756111818475934e-06), s1: np.float64(-1.974384093816498e-06)}

We can relate these coefficients to the fermionic exponents:

print("Fermion Excitations and Amplitudes")
print("==================================")
print("Exponent\t\t\t\t\tAmplitude\t\tSymbol")
for fermion_operator, symbol in ansatz._fermion_operator_exponents:
    parameter = vqe.final_parameters[symbol]
    print(f"{fermion_operator}\t{parameter}\t{symbol}")
Fermion Excitations and Amplitudes
==================================
Exponent					Amplitude		Symbol
(1.0, F2^ F0  F3^ F1 ), (-1.0, F1^ F3  F0^ F2 )	-0.37329427840313417	d0
(1.0, F4^ F0  F5^ F1 ), (-1.0, F1^ F5  F0^ F4 )	-0.004353051918026713	d1
(1.0, F4^ F0 ), (-1.0, F0^ F4 )	-2.9756111818475934e-06	s0
(1.0, F5^ F1 ), (-1.0, F1^ F5 )	-1.974384093816498e-06	s1

6. Hamiltonian Averaging using InQuanto Computables

In general, InQuanto uses ‘Computables’ to calculate the chemical quantity of relevance. A protocol is then the blueprint which defines how we calculate the chemical quantity on the quantum computer. The simplest computables, such as expectation values of an operator on a state, can be evaluated directly using a single protocol. More details on these structures can be found at https://inquanto.quantinuum.com/

6. a. Prepare Protocol

We will re-use our ansatz and operator from above.

The protocol we use today is called PauliAveraging, and is one way to perform Hamiltonian Averaging. This protocol generates measurement circuits from the Hamiltonian and state-preparation circuit. These circuits will be submitted to the H1 emulator (H1-1E) via Nexus. Once the results are retrieved, the protocol can then estimate the expectation value of the Pauli words. These expectations are multiplied by the corresponding coefficient in the Hamiltonian, and summed to obtain the ground-state energy.

from inquanto.protocols import PauliAveraging
from qnexus import QuantinuumConfig
from inquanto.core._tket import PauliPartitionStrat

configuration = QuantinuumConfig(device_name="H1-1E", user_group="Default - UK")
protocol = PauliAveraging(
    configuration,
    shots_per_circuit=5000,
    pauli_partition_strategy=PauliPartitionStrat.CommutingSets,
    project_ref=project_ref
)

6. b. Partition Measurement Symmetry Verification

We recognise that our system possesses C2v point group symmetry. This can be exploited in the Hamiltonian Averaging procedure since we know the parity of certain Pauli words and therefore their expectation values.

These Pauli words corresponding to symmetry operations - mirror planes and pi radian rotations.

One can use InQuanto to calculate the Pauli words (within Jordan Wigner encoding)

pauli_symmetries = jw_map.operator_map(fermion_space.symmetry_operators_z2_in_sector(fermion_state))

for op in pauli_symmetries:
    print(op)
(1.0, Z2 Z3)
(1.0, Z0 Z1 Z2 Z3 Z4 Z5)
(-1.0, Z0 Z2 Z4)

The protocol can be modified to include Symmetry Verification step. The flavour we use in InQuanto is called Partition Measurement Symmetry Verification (PMSV). The steps are as follows:

  1. Find largest Abelian point group of molecule. Transformations of this point group are Pauli-symmetries.
  2. Build symmetry verifiable circuits using the measurement reduction facility in tket. The operators that we need to measure on the quantum device to solve out problem consist of Pauli-Is, Pauli-Xs, Pauli-Ys and Pauli-Zs across the qubit register. These operations can be partitioned into commuting sets. For element in a commuting set, if the Pauli-symmetries commute element-wise, it can be added to the commuting set. This way, each commuting set is symmetry verifiable.
  3. Post-select on measurement result before counting bitstrings to compute expectation value of problem Hamiltonian. The quantity to post-select on is the XOR sum over the bit-strings needed to compute expectation value of Pauli-symmetry.
from inquanto.protocols import PMSV
mitms_pmsv = PMSV(pauli_symmetries)
protocol.clear()

6. c. Build Protocol

We now build and compile the measurement circuits for the shot based protocol, including instructions for symmetry verification noise mitigation with Hamiltonian Averaging. Note that compilation is also performed using Nexus. The uncompiled circuits are sent and compiled circuits are retrieved. When all compiled circuits are obtained we can launch the execution job.

protocol.build(vqe.final_parameters, ansatz, qubit_hamiltonian_jw, noise_mitigation=mitms_pmsv).compile_circuits()
protocol.dataframe_circuit_shot()
Qubits Depth Count Depth2q Count2q DepthCX CountCX Shots
0 6 47 84 24 28 0 0 5000
1 6 46 80 24 27 0 0 5000
2 6 50 87 26 29 0 0 5000
3 6 46 85 24 28 0 0 5000
4 6 46 80 24 27 0 0 5000
5 6 40 74 20 24 0 0 5000
Sum - - - - - - - 30000
circuits=protocol.get_circuits()

We can examine the circuits object here, which is a list of NexusCircuits (as opposed to tket Circuits like usual). Not only does this contain a tket circuit, but it also contains information about the job and its submission

circuits[0]
[PhasedX(2.5, 0) q[0]; PhasedX(2.5, 0.5) q[1]; PhasedX(0.5, 0) q[2]; PhasedX(0.5, 0.5) q[3]; PhasedX(0.5, 0) q[4]; PhasedX(0.5, 0.5) q[5]; ZZPhase(0.5) q[2], q[0]; PhasedX(0.118823, 1.5) q[0]; PhasedX(2.11882, 1.5) q[2]; ZZPhase(0.5) q[2], q[0]; ZZPhase(0.5) q[4], q[0]; PhasedX(0.5, 0) q[2]; PhasedX(2.00139, 0.5) q[0]; ZZPhase(0.5) q[2], q[3]; PhasedX(2.00139, 1.5) q[4]; ZZPhase(0.5) q[4], q[0]; PhasedX(2.5, 1) q[2]; PhasedX(0.5, 0) q[3]; PhasedX(0.5, 0) q[0]; ZZPhase(0.5) q[3], q[2]; PhasedX(2.5, 0) q[4]; ZZPhase(0.5) q[0], q[1]; PhasedX(0.5, 0.5) q[2]; ZZPhase(0.5) q[4], q[5]; PhasedX(0.5, 0.5) q[0]; ZZPhase(0.5) q[2], q[1]; PhasedX(1, 0.5) q[4]; PhasedX(1.5, 0) q[5]; PhasedX(0.5, 0.5) q[1]; ZZPhase(0.5) q[1], q[4]; PhasedX(0.5, 0) q[4]; ZZPhase(0.5) q[4], q[0]; PhasedX(2, 0) q[0]; PhasedX(2, 0.5) q[4]; ZZPhase(0.5) q[4], q[0]; PhasedX(2.5, 0.5) q[0]; PhasedX(2.5, 0) q[4]; ZZPhase(0.5) q[1], q[4]; PhasedX(2.5, 0.5) q[1]; PhasedX(1, 1.5) q[4]; ZZPhase(0.5) q[2], q[1]; PhasedX(1.5, 0.5) q[1]; PhasedX(1.5, 1.5) q[2]; ZZPhase(0.5) q[3], q[2]; PhasedX(0.5, 0) q[3]; ZZPhase(0.5) q[4], q[3]; PhasedX(0.5, 1.5) q[3]; ZZPhase(0.5) q[3], q[2]; PhasedX(0.5, 0.5) q[2]; ZZPhase(0.5) q[2], q[5]; PhasedX(3.5, 0) q[5]; ZZPhase(0.5) q[5], q[1]; PhasedX(2, 1.5) q[1]; PhasedX(2, 1.5) q[5]; ZZPhase(0.5) q[5], q[1]; PhasedX(3.5, 0) q[1]; PhasedX(2.5, 1) q[5]; ZZPhase(0.5) q[2], q[5]; PhasedX(1.5, 1.5) q[2]; PhasedX(3.5, 1) q[5]; ZZPhase(0.5) q[0], q[5]; ZZPhase(0.5) q[3], q[2]; PhasedX(2.5, 0) q[0]; PhasedX(1.5, 0.5) q[3]; PhasedX(0.5, 0.5) q[5]; Measure q[5] --> c[5]; ZZPhase(0.5) q[4], q[3]; PhasedX(0.5, 0) q[4]; ZZPhase(0.5) q[1], q[4]; PhasedX(1.5, 0) q[1]; PhasedX(0.5, 1.5) q[4]; Measure q[4] --> c[4]; ZZPhase(0.5) q[0], q[1]; PhasedX(1.5, 0) q[0]; PhasedX(0.5, 1) q[1]; Measure q[1] --> c[1]; ZZPhase(0.5) q[0], q[2]; ZZPhase(0.5) q[0], q[3]; PhasedX(0.5, 0.5) q[2]; Measure q[2] --> c[2]; PhasedX(0.5, 1.5) q[0]; PhasedX(0.5, 1.5) q[3]; Measure q[0] --> c[0]; Measure q[3] --> c[3]; ]

With the circuits returned, we can launch them for execution using protocol.launch(). This will tell Nexus to run the list of circuits in the protocol on the backend, returning a ResultsHandle for each circuit so that we may later retrieve them when our hardware/emulation is complete.

results_handles=protocol.launch()
#wait 

The result_handles is a list of ResultHandle objects. These are made up of tuples of the job id and an enumerator.

print(results_handles)

6. e. Retrieve results from Nexus

Results can be retrieved from Nexus using the computables “retrieve_distributions” function when they have completed.

results = protocol.retrieve(results_handles)

Now our protocol is complete and we can examine and evaluate it! Firstly we take a peak at the results for the individual Pauli strings we measured for our hamiltonian and PMSV. Note that sample size is below 5000 has symmetry violating shots are discarded. Some Pauli strings have been measured by multiple circuits and so have more than 5000 samples.

protocol.dataframe_measurements()
Pauli string Mean Standard error Sample size
0 Z1 Z5 -0.736200 0.009732 4837
1 Y1 Z3 Z4 Y5 -0.042382 0.014367 4837
2 Z2 0.736200 0.009732 4837
3 Y2 Y3 X4 X5 -0.012949 0.014452 4788
4 Z4 0.998760 0.000716 4837
5 Y0 Z1 Z3 Y4 0.015038 0.014452 4788
6 X2 X3 Y4 Y5 -0.016618 0.014412 4814
7 X0 X1 Y2 Y3 0.667221 0.010737 4814
8 Y0 X1 X4 Y5 0.004779 0.014416 4813
9 Z3 Z5 0.735589 0.009791 4788
10 X1 Z2 Z3 X5 -0.042795 0.014367 4837
11 X0 Z1 Z2 Z3 X4 Z5 -0.027789 0.014397 4822
12 X0 Z1 X2 X3 Z4 X5 0.005194 0.014416 4813
13 Y0 Z1 Z2 Z3 Y4 Z5 0.001253 0.014453 4788
14 Y0 Z1 Z2 Y4 0.015038 0.014452 4788
15 X0 X1 Y4 Y5 0.025343 0.014410 4814
16 Z0 Z1 0.997092 0.001099 4814
17 Z0 Z3 -0.998760 0.000716 4837
18 X1 Z2 Z4 X5 -0.034112 0.014372 4837
19 X0 Y1 Y2 X3 -0.643050 0.011040 4813
20 X2 Y3 Y4 X5 -0.021816 0.014412 4813
21 X1 Z3 Z4 X5 -0.034112 0.014372 4837
22 Z5 0.997097 0.001097 4822
23 Y1 Z2 Z4 Y5 -0.042382 0.014367 4837
24 X0 Z2 Z3 X4 -0.001253 0.014453 4788
25 Z4 Z5 0.997092 0.001099 4814
26 Y1 Z2 Z3 Y5 -0.034526 0.014371 4837
27 Y0 Z1 Y2 X3 Z4 X5 -0.012114 0.014452 4788
28 X0 Z1 Z3 X4 -0.001039 0.014416 4813
29 Y1 Z2 Z3 Z4 Y5 -0.034112 0.014372 4837
30 Y0 X1 X2 Y3 -0.664455 0.010763 4822
31 Z0 -0.734960 0.009751 4837
32 Y1 Y2 X3 X4 -0.012547 0.014461 4782
33 Z3 0.736200 0.009732 4837
34 Y0 Z2 Z3 Y4 0.027789 0.014397 4822
35 Y2 X3 X4 Y5 -0.007519 0.014453 4788
36 Z2 Z5 0.735589 0.009791 4788
37 X0 Z1 Z2 Z3 X4 0.015038 0.014452 4788
38 Y0 Z1 Z2 Z3 Y4 -0.001039 0.014416 4813
39 Z0 Z2 -0.998760 0.000716 4837
40 Z1 Z2 -0.997097 0.001097 4822
41 X0 Y1 Y4 X5 0.004779 0.014416 4813
42 Z0 Y1 Z2 Z3 Z4 Y5 0.042795 0.014367 4837
43 Z1 Z3 -0.997097 0.001097 4822
44 Y1 X2 X3 Y4 0.017835 0.014400 4822
45 Z2 Z4 0.734960 0.009751 4837
46 X1 X2 Y3 Y4 -0.017420 0.014400 4822
47 Y0 Y1 X4 X5 0.025343 0.014410 4814
48 X0 Z1 Z2 X4 -0.001039 0.014416 4813
49 Y0 Y1 X2 X3 0.665143 0.010763 4814
50 Z0 Z5 -0.732330 0.009848 4782
51 Z0 Z4 -0.736200 0.009732 4837
52 X1 Y2 Y3 X4 -0.005194 0.014416 4813
53 X0 Z1 X2 Y3 Z4 Y5 -0.009201 0.014462 4782
54 Y0 Z1 Y2 Y3 Z4 Y5 0.024227 0.014449 4788
55 Z2 Z3 1.000000 0.000000 28856
56 X1 Z2 Z3 Z4 X5 -0.042382 0.014367 4837
57 Z1 -0.735589 0.009791 4788
58 Z1 Z4 -0.732330 0.009848 4782
59 Z0 X1 Z2 Z3 Z4 X5 0.034526 0.014371 4837
60 Z3 Z4 0.734960 0.009751 4837

Then we can evaluate our expectation value using the coefficients in our Hamiltonian.

energy_nexus = protocol.evaluate_expectation_value(ansatz, qubit_hamiltonian_jw)

6. f. Post-process classically to evaluate ground-state energy

Below we perform a little analysis of our results.

import numpy
rel_error = (
    100
    * numpy.absolute(vqe.final_value - energy_nexus)
    / numpy.absolute(vqe.final_value)
)
abs_error = numpy.absolute(vqe.final_value - energy_nexus)
correlation_energy_nexus = scf_energy - energy_nexus

print(f"Energy [Hamiltonian Averaging] on H1-1E via nexus: {energy_nexus} Ha")
print(f"Energy [Benchmark]: {ccsd_energy} Ha")
print(
    f"\nCorrelation Energy [Hamiltonian Averaging] on H1-1E via nexus: {correlation_energy_nexus} Ha"
)
print(f"Correlation Energy [Benchmark]: {ccsd_correlation_energy} Ha")
print(f"\nRelative Error [Hamiltonian Averaging] on H1-1E via nexus: {rel_error} %")
print(f"\nAbsolute Error [Hamiltonian Averaging] on H1-1E via nexus: {abs_error} Ha")
Energy [Hamiltonian Averaging] on H1-1E via nexus: -38.35591669827696 Ha
Energy [Benchmark]: -38.3588270738749 Ha

Correlation Energy [Hamiltonian Averaging] on H1-1E via nexus: 0.01782649475909892 Ha
Correlation Energy [Benchmark]: 0.02073687035704097 Ha

Relative Error [Hamiltonian Averaging] on H1-1E via nexus: 0.007587238137260944 %

Absolute Error [Hamiltonian Averaging] on H1-1E via nexus: 0.0029103755567518874 Ha

For this small system we’ve got pretty good results. Our noisy backend averaging is 0.004 Ha from the statevector (noiseless) result when using 5000 shots and PMSV. This represents a relative error of about 0.01 %

../_images/quantinuumlogo.png