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={}
)
Login Successful.

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)
/python3.9/site-packages/pyscf/dft/libxc.py:772: UserWarning: Since PySCF-2.3, B3LYP (and B3P86) are changed to the VWN-RPA variant, the same to the B3LYP functional in Gaussian and ORCA (issue 1480). To restore the VWN5 definition, you can put the setting "B3LYP_WITH_VWN5 = True" in pyscf_conf.py
  warnings.warn('Since PySCF-2.3, B3LYP (and B3P86) are changed to the VWN-RPA variant, '

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.338090203517844 Ha
CCSD energy: -38.35882707387484 Ha
CCSD energy correlation energy: 0.020736870356998338 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()
Coefficients Terms
0 -37.107696
1 -0.968197 F0^ F0
2 -0.030656 F0^ F4
3 0.353000 F1^ F0^ F0 F1
4 0.353000 F1^ F0^ F0 F1
... ... ...
128 0.024456 F5^ F4^ F2 F3
129 0.024456 F5^ F4^ F2 F3
130 0.280721 F5^ F4^ F4 F5
131 0.280721 F5^ F4^ F4 F5
132 -0.271034 F5^ F5

133 rows × 2 columns

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'>
... ... ... ...
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'>

62 rows × 3 columns

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 CX 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


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)
Started using project with name: InQuanto::Project
# TIMER BLOCK-0 BEGINS AT 2023-10-26 08:04:47.252666
# TIMER BLOCK-0 ENDS - DURATION (s): 197.9548447 [0:03:17.954845]
VQE Energy: -38.35882707384031
{d0: -0.3732936553004727, d1: -0.004352090240814768, s0: -2.206496678468692e-06, s1: -1.8038857692025435e-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.3732936553004727	d0
(1.0, F4^ F0  F5^ F1 ), (-1.0, F1^ F5  F0^ F4 )	-0.004352090240814768	d1
(1.0, F4^ F0 ), (-1.0, F0^ F4 )	-2.206496678468692e-06	s0
(1.0, F5^ F1 ), (-1.0, F1^ F5 )	-1.8038857692025435e-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
)
Started using project with name: InQuanto::Project

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()
/inquanto/protocols/averaging/_mitigation.py:205: UserWarning: The PMSV is still experimental.
  warnings.warn(f"The {self.__class__.__name__} is still experimental.")
<inquanto.protocols.averaging._pauli_averaging.PauliAveraging at 0x7f15ab798400>

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()
<inquanto.protocols.averaging._pauli_averaging.PauliAveraging at 0x7f15ab798400>
protocol.dataframe_circuit_shot()
Qubits Depth Depth2q DepthCX Shots
0 6 46 24 0 5000
1 6 40 20 0 5000
2 6 46 24 0 5000
3 6 46 24 0 5000
4 6 50 26 0 5000
5 6 47 24 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]
NexusCircuit(id=fde92d6f-5c54-45cd-85dd-8f13a1d131a6, submitted_time=2023-10-26 08:08:16.504778+00:00, project_name=InQuanto::Project, project_id=f225abb6-e9b9-472a-92b6-32a6f066b796)[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(0.5, 0) q[0]; PhasedX(0.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(1.5, 0) q[1]; PhasedX(0.5, 1) q[5]; Measure q[1] --> c[1]; ZZPhase(0.5) q[2], q[5]; PhasedX(1.5, 1.5) q[2]; PhasedX(3.5, 1) q[5]; ZZPhase(0.5) q[3], q[2]; PhasedX(1.5, 0.5) q[3]; ZZPhase(0.5) q[4], q[3]; PhasedX(1, 1) q[4]; ZZPhase(0.5) q[0], q[4]; PhasedX(0.5, 0) q[0]; PhasedX(0.5, 0.5) q[4]; Measure q[4] --> c[4]; 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]; ZZPhase(0.5) q[0], q[5]; PhasedX(0.5, 1.5) q[3]; Measure q[3] --> c[3]; PhasedX(0.5, 1.5) q[0]; PhasedX(0.5, 0.5) q[5]; Measure q[0] --> c[0]; Measure q[5] --> c[5]; ]

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)
[ResultHandle('723e324f-dfbc-458f-a459-5a26afa86ba3', 1199879), ResultHandle('723e324f-dfbc-458f-a459-5a26afa86ba3', 1199880), ResultHandle('723e324f-dfbc-458f-a459-5a26afa86ba3', 1199881), ResultHandle('723e324f-dfbc-458f-a459-5a26afa86ba3', 1199882), ResultHandle('723e324f-dfbc-458f-a459-5a26afa86ba3', 1199883), ResultHandle('723e324f-dfbc-458f-a459-5a26afa86ba3', 1199884)]

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 stderr umean sample_size
0 Z1 Z5 -0.728972 0.009978 -0.729+/-0.010 4708
1 Y1 Z3 Z4 Y5 0.012744 0.014574 0.013+/-0.015 4708
2 Z2 0.728972 0.009978 0.729+/-0.010 4708
3 Y2 Y3 X4 X5 0.005788 0.014642 0.006+/-0.015 4665
4 Z4 0.995327 0.001407 0.9953+/-0.0014 4708
5 Y0 Z1 Z3 Y4 -0.003215 0.014643 -0.003+/-0.015 4665
6 X2 X3 Y4 Y5 -0.008321 0.014608 -0.008+/-0.015 4687
7 X0 X1 Y2 Y3 0.635161 0.011283 0.635+/-0.011 4687
8 Y0 X1 X4 Y5 0.005731 0.014571 0.006+/-0.015 4711
9 Z3 Z5 0.726045 0.010069 0.726+/-0.010 4665
10 X1 Z2 Z3 X5 0.013169 0.014574 0.013+/-0.015 4708
11 X0 Z1 Z2 Z3 X4 Z5 -0.024055 0.014653 -0.024+/-0.015 4656
12 X0 Z1 X2 X3 Z4 X5 0.006580 0.014571 0.007+/-0.015 4711
13 Y0 Z1 Z2 Z3 Y4 Z5 -0.013934 0.014641 -0.014+/-0.015 4665
14 Y0 Z1 Z2 Y4 -0.003215 0.014643 -0.003+/-0.015 4665
15 X0 X1 Y4 Y5 0.022402 0.014605 0.022+/-0.015 4687
16 X1 Z2 Z4 X5 0.027188 0.014570 0.027+/-0.015 4708
17 Z0 Z1 0.996160 0.001279 0.9962+/-0.0013 4687
18 Z0 Z3 -0.995327 0.001407 -0.9953+/-0.0014 4708
19 X0 Y1 Y2 X3 -0.656549 0.010991 -0.657+/-0.011 4711
20 X1 Z3 Z4 X5 0.027188 0.014570 0.027+/-0.015 4708
21 X2 Y3 Y4 X5 0.007429 0.014571 0.007+/-0.015 4711
22 Z5 0.996134 0.001288 0.9961+/-0.0013 4656
23 Y1 Z2 Z4 Y5 0.012744 0.014574 0.013+/-0.015 4708
24 X0 Z2 Z3 X4 0.013934 0.014641 0.014+/-0.015 4665
25 Z4 Z5 0.996160 0.001279 0.9962+/-0.0013 4687
26 Y1 Z2 Z3 Y5 0.027613 0.014570 0.028+/-0.015 4708
27 Y0 Z1 Y2 X3 Z4 X5 0.001072 0.014643 0.001+/-0.015 4665
28 X0 Z1 Z3 X4 -0.023986 0.014567 -0.024+/-0.015 4711
29 Y1 Z2 Z3 Z4 Y5 0.027188 0.014570 0.027+/-0.015 4708
30 Y0 X1 X2 Y3 -0.642182 0.011235 -0.642+/-0.011 4656
31 Z0 -0.725998 0.010024 -0.726+/-0.010 4708
32 Z3 0.728972 0.009978 0.729+/-0.010 4708
33 Y1 Y2 X3 X4 -0.011381 0.014654 -0.011+/-0.015 4657
34 Y0 Z2 Z3 Y4 0.024055 0.014653 0.024+/-0.015 4656
35 Y2 X3 X4 Y5 0.007074 0.014642 0.007+/-0.015 4665
36 Z2 Z5 0.726045 0.010069 0.726+/-0.010 4665
37 X0 Z1 Z2 Z3 X4 -0.003215 0.014643 -0.003+/-0.015 4665
38 Y0 Z1 Z2 Z3 Y4 -0.023986 0.014567 -0.024+/-0.015 4711
39 Z0 Z2 -0.995327 0.001407 -0.9953+/-0.0014 4708
40 Z1 Z2 -0.996134 0.001288 -0.9961+/-0.0013 4656
41 Z0 X1 Z2 Z3 Z4 X5 -0.027613 0.014570 -0.028+/-0.015 4708
42 X0 Y1 Y4 X5 0.005731 0.014571 0.006+/-0.015 4711
43 Z0 Y1 Z2 Z3 Z4 Y5 -0.013169 0.014574 -0.013+/-0.015 4708
44 Z1 Z3 -0.996134 0.001288 -0.9961+/-0.0013 4656
45 Y1 X2 X3 Y4 0.013746 0.014655 0.014+/-0.015 4656
46 Z2 Z4 0.725998 0.010024 0.726+/-0.010 4708
47 X1 X2 Y3 Y4 -0.014175 0.014655 -0.014+/-0.015 4656
48 Y0 Y1 X4 X5 0.022402 0.014605 0.022+/-0.015 4687
49 X0 Z1 Z2 X4 -0.023986 0.014567 -0.024+/-0.015 4711
50 Y0 Y1 X2 X3 0.634734 0.011288 0.635+/-0.011 4687
51 Z0 Z5 -0.729440 0.010025 -0.729+/-0.010 4657
52 Z0 Z4 -0.728972 0.009978 -0.729+/-0.010 4708
53 Z2 Z3 1.000000 0.000000 1.0+/-0 28084
54 Y0 Z1 Y2 Y3 Z4 Y5 -0.007503 0.014642 -0.008+/-0.015 4665
55 X0 Z1 X2 Y3 Z4 Y5 -0.019540 0.014652 -0.020+/-0.015 4657
56 X1 Z2 Z3 Z4 X5 0.012744 0.014574 0.013+/-0.015 4708
57 Z1 -0.726045 0.010069 -0.726+/-0.010 4665
58 Z1 Z4 -0.729440 0.010025 -0.729+/-0.010 4657
59 X1 Y2 Y3 X4 0.018467 0.014569 0.018+/-0.015 4711
60 Z3 Z4 0.725998 0.010024 0.726+/-0.010 4708

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.35429920140337 Ha
Energy [Benchmark]: -38.35882707387484 Ha

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

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

Absolute Error [Hamiltonian Averaging] on H1-1E via nexus: 0.004527872436938196 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