Using the HQS Qorrelator App

In this section, we outline how to employ the HQS Qorrelator App to compute time-resolved correlators for NMR spectroscopy simulations on quantum computers. The HQS Qorrelator App works together with the HQS struqture and qoqo libraries. In this regard, we first briefly show how to create Hamiltonians and open quantum systems using struqture. Then, we discuss the available qoqo noise models that can be used in the simulation, and we show how to set up device information. Finally, we illustrate how to use the HQS Qorrelator App to create a qoqo QuantumProgram for simulating the dynamics of the correlators.

struqture

struqture is a library enabling compact representation of quantum mechanical operators, Hamiltonians, and open quantum systems. The library supports building spin, fermionic and bosonic objects, as well as combinations thereof (mixed systems). Here, we demonstrate its usage with simple spin systems. For more complicated examples, please check the user documentation of struqture.

Creating Spin Hamiltonians

Let us now create a spin Hamiltonian using the struqture library. Here, arbitrary spin operators are built from PauliProduct operators. As the name implies, these are products of single-spin Pauli operators. Each Pauli operator in the PauliProduct operates on a different qubit. Not all qubits need to be represented in a PauliProduct (such qubits contribute via identity operators).

For instance, let us create a simple spin-Hamiltonian in the rotating-wave approximation with respect to a strong external magnetic field in the Z-direction. Such a Hamiltonian is also required as the input for constructing the QuantumProgram used to calculate the NMR correlation function. All single-spin terms need to be Z-terms. All spin-interaction terms need to be at least spin-Z preserving (only XX, YY or ZZ terms, with the same coefficient for XX and YY) or completely symmetric in X, Y and Z (same coefficient for XX, YY and ZZ). The second kind of symmetry is required when setting the b_field_direction option to a different direction from the input Hamiltonian.

For instance, we can create the following Hamiltonian

\[ H= \sum_{i=0}^9 3\sigma_{i}^z + \sum_{i=0}^8 2\sigma_{i}^x\sigma_{i+1}^x \]

using:


from struqture_py import spins
from struqture_py.spins import (SpinHamiltonianSystem, PauliProduct)

number_spins = 10
chemical_shift = 3.0
spin_coupling = 2.0

hamiltonian = SpinHamiltonianSystem()
for site in range(number_spins):
    hamiltonian.add_operator_product(PauliProduct().z(site), chemical_shift)
for site in range(number_spins-1):
    hamiltonian.add_operator_product(PauliProduct().x(site).x(site+1), spin_coupling)
    hamiltonian.add_operator_product(PauliProduct().y(site).y(site+1), spin_coupling)
    hamiltonian.add_operator_product(PauliProduct().z(site).z(site+1), spin_coupling)


Information can be accessed using the following functions:


hamiltonian.keys()             # operator keys (Pauli products / strings)
hamiltonian.get("0X1X")        # front factor of Pauli product "0X1X"
hamiltonian.number_spins()     # number of spins in the system

Creating Spin Lindblad Noise Operators

Let us now create the Lindblad noise operators for a system of spins using struqture. Such noise operators are used to describe the noise part of the the Lindblad master equation

\[ \sum_{i,j}M_{i,j} \left( A_{i}\rho A_{j}^{\dagger} - \frac{1}{2} \lbrace A_j^{\dagger} A_i, \rho \rbrace \right). \]

For more detailed information, please refer to the modeling section. We use struqture DecoherenceProducts as the operator basis. The object SpinLindbladNoiseOperator is given by a HashMap or Dictionary with the tuple (DecoherenceProduct, DecoherenceProduct) as keys and the entries in the rate matrix \(M_{j,k} \) as values. Similarly to SpinOperators, SpinLindbladNoiseOperators have a system equivalent: SpinLindbladNoiseSystem, with a number of involved spins defined by the user. For more information on these, see the user documentation of struqture.

For instance, take \( A_0 = A_1 = \sigma_0^{x} \sigma_2^{z} \) with coefficient 1.0: \( 1.0 \left( A_0 \rho A_1^{\dagger} - \frac{1}{2} \lbrace A_1^{\dagger} A_0, \rho \rbrace \right) \). This is implemented in the following code snippet.


from struqture_py import spins
from struqture_py.spins import SpinLindbladNoiseSystem, DecoherenceProduct
import scipy.sparse as sp

system = spins.SpinLindbladNoiseSystem(3)

dp = spins.DecoherenceProduct().x(0).z(2)

system.add_operator_product((dp, dp), 1.0)

# Accessing information:
system.current_number_spins()     # Result: 2
system.get((dp, dp))              # Result: CalculatorFloat(2)
system.keys()                     # Result: [("0Z1X", "0Z1X")]
dimension = 4**system.number_spins()
matrix = sp.coo_matrix(system.sparse_matrix_superoperator_coo(), shape=(dimension, dimension))

Creating noise models

The current version of the HQS Qorrelator App supports single-qubit physical noise in the form of damping, dephasing, and depolarization, with user-given decoherence rates. We assume that the noise is the same for all gate-types.

We can define a ContinuousDecoherenceModel noise model with the following types of noise:

  • dephasing: using the add_dephasing_rate function
  • depolarisation: using the add_depolarising_rate function
  • damping: using the add_damping_rate function
  • excitations: using the add_excitation_rate function

Each of these functions takes a list of qubits to apply the noise to, as well as a noise rate (float), and returns the modified ContinuousDecoherenceModel. An example code snippet, which creates a noise model with damping and dephasing, reads as follows.

from qoqo import noise_models

# Setting up the noise model.
damping = 1e-3
dephasing = 5e-4
noise_model = noise_models.ContinuousDecoherenceModel().add_damping_rate([0, 1, 2], damping).add_dephasing_rate([3, 4], dephasing)

Creating a device

We can define an AllToAllDevice device with the following settings:

  • number_of_qubits : The number of qubits for the device.
  • single_qubit_gates : The list of single-qubit gates available on the quantum computer.
  • two_qubit_gates : The list of two-qubit gates available on the quantum computer.
  • default_gate_time : The default starting gate time.

The option single_qubit_gates can be any list of single-qubit gates available in qoqo as long as it contains one of the following combinations:

  • RotateX and RotateZ
  • RotateY and RotateZ
  • RotateX and RotateY
  • RotateZ and SqrtPauliX and InvSqrtPauliX

The supported choices for two_qubit_gates are:

  • CNOT
  • ControlledPauliZ
  • ControlledPhaseShift
  • MolmerSorensenXX
  • VariableMSXX

An example code snippet of setting device information reads as follows.

from qoqo import devices

# Setting up the device.
number_of_qubits=5
single_qubit_gates = ["RotateX", "RotateZ", "RotateY"]
two_qubit_gates = ["CNOT"]
default_gate_time = 1.0
device = devices.AllToAllDevice(
    number_of_qubits, single_qubit_gates, two_qubit_gates, default_gate_time
)

Using the HQS Qorrelator App

The HQS Qorrelator App can be initialized without any arguments:

from hqs_qorrelator_app import NMRCorrelator

correlator = NMRCorrelator()

Additionally, there are settings that have default values and can be set with setter functions with the same name as the setting.

  • algorithm : options are ParityBased, QSWAP, QSWAPMolmerSorensen, VariableMolmerSorensen (defaults to QSWAP).
  • b_field_direction: Z (default) and X are possible options. Determines in which direction of the B-field is used for the calculation of the correlator. Automatically transforms compatible input Hamiltonians (e.g. a input Hamiltonian defined for a Z b-field if the X direction is used).
  • noise_mode : options are active_qubits_only (noise added only for qubits involved in operations), all_qubits (noise added for all qubits at each step, default value) and parallelization_blocks (noise added after each parallelization block).
  • noise_placement : before, after or symmetric- The noise is applied before the gate, after the gate based on gate duration or symmetrically (defaults to after)
  • number_measurements : the number of projective measurements used when measuring observables (defaults to 100000)
  • optimization_level : the level of optimization when performing the algorithm: either 0 where no optimizations are applied, or 1 where SingleQubitGates are combined (defaults to 1)
  • initialisation: options are SumOverAllStates (default), ActiveReset, NonCorrelatingMeasurement, GeneralMeasurement, GeneralDephasing. See also the chapter on algorithms. We recommend using the default value.
  • decoherence_loops : This only affects optional initialisation methods and has no effect on the n optional number of loops run after the Hadamards on every qubit and before the main circuit, which are there to let off-diagonal entries of the density matrix decay through decoherence.

The HQS Qorrelator App can create QuantumPrograms to time-propagate a spin state. The QuantumProgram will initialize a spin state on a quantum computer, time-propagate the spin state with a quantum algorithm, measure the values of spin observables and compute the NMR correlator as described in the Quantum Algorithm section. The following example shows how to obtain the QuantumProgram for a previously defined hamiltonian (hamiltonian) and device (device).

from hqs_qorrelator_app import NMRCorrelator

number_qubits = hamiltonian.number_qubits()
trotter_timestep = 0.005
gyromagnetic_factors = [1.0]*number_qubits

correlator = NMRCorrelator()
program = correlator.spectrum_program_fixed_step(
    hamiltonian,
    trotter_timestep,
    gyromagnetic_factors,
    device
)

The QuantumProgram can then be simulated using the backend of the user's choosing. For Linux users, the qoqo-quest Backend is recommended for simulations. The following example shows how to run the obtained QuantumProgram on the backend and get the real and imaginary parts of the NMR correlator from the resulting dictionary.

import numpy as np
from qoqo_quest import Backend

number_trottersteps = 100
backend = Backend(number_qubits)

correlator_re = np.zeros(number_trottersteps, dtype=float)
correlator_im = np.zeros(number_trottersteps, dtype=float)

for i in range(0, number_trottersteps):
    result = backend.run_program(program, [i])
    correlator_re[i] = result["correlator_total_re"]
    correlator_im[i] = result["correlator_total_im"]
    
correlator = correlator_re + 1.0j * correlator_im

Considering the effects of noise

Estimating the overall decoherence rate

Physical noise on the quantum computer used to run the QuantumProgram will lead to a decay in the calculated correlation functions. This decay will lead to a broadening in the NMR spectrum (the Fourier transform of the correlation function).

Based on the choice of algorithm, device specification, and noise models, the mean decoherence rate that affects the spin system during the time evolution can be estimated. This estimated decoherence rate gives an expected broadening of the peaks in the NMR spectrum.

Please note that this is just an estimate, and therefore does not require a computationally costly full simulation of the time evolution.

from hqs_qorrelator_app import NMRCorrelator
from struqture_py import spins
from qoqo import devices, noise_models

# define hamiltonian
gyromagnetic = 1.0
number_spins = 2
time = np.pi/4
coupling = 1.0
shift = 1.0
gyromagnetic_factors = [gyromagnetic for _ in range(number_qubits)]
hamiltonian = spins.SpinHamiltonianSystem(number_spins)
for site in range(number_spins):
    hamiltonian.set("{}Z".format(site), -shift * site)

# Setting up the device.
single_qubit_gates = ["RotateX", "RotateZ", "RotateY"]
two_qubit_gates = ["CNOT"]
gate_times = 1.0
damping = 1e-3
device = devices.AllToAllDevice(
    number_spins, single_qubit_gates, two_qubit_gates, gate_times
)
# While the noise model is not needed to generate the QuantumProgram, it will be required
# when simulating the QuantumProgram.
noise_model = noise_models.ContinuousDecoherenceModel().add_damping_rate([0, 1, 2], damping)

trotter_timestep = 0.01

# Create circuit.
qorrelator_app_sc = NMRCorrelator()
qorrelator_app_sc.algorithm = "QSWAP"
estimated_decoherence_rate = qorrelator_app_sc.estimate_decoherence_rate(hamiltonian, trotter_timestep, device, [noise_model])

print("estimated decoherence rate:", estimated_decoherence_rate) 

Extracting the Noisy algorithm model

The noisy algorithm model represents the effective Lindbladian that is being simulated in the presence of noise. Please refer to this paper and to the mapping section for details.

With the HQS Qorrelator App, the noisy algorithm model of a Hamiltonian can be obtained using the function NMRCorrelator.noisy_algorithm_model, with input arguments:

  • hamiltonian: The Hamiltonian for which the noise algorithm model is created.
  • trotter_timestep: The simulation time the circuit propagates the simulated system.
  • device: The device determining the topology.
  • noise_models: Noise models determining noise properties.

An example is:


from hqs_qorrelator_app import NMRCorrelator
from struqture_py import spins
from struqture_py.spins import (SpinHamiltonianSystem, PauliProduct)
from qoqo import devices, noise_models

# define hamiltonian 
number_spins = 4
hamiltonian = spins.SpinHamiltonianSystem(number_spins)
hamiltonian.add_operator_product(PauliProduct().z(0).z(2).z(3), 4.0)

# Setting up the device
single_qubit_gates = ["RotateX", "RotateZ"]
two_qubit_gates = ["CNOT"]
gate_times = 1.0
damping = 1e-3
device = devices.AllToAllDevice(
    number_spins, single_qubit_gates, two_qubit_gates, gate_times
)

# While the noise model is not needed to generate the QuantumProgram, it will be required
# when simulating the QuantumProgram.
noise_model = noise_models.ContinuousDecoherenceModel().add_damping_rate([0, 1, 2, 3], damping)

# create inputs
trotter_timestep=0.01
qorrelator_app = NMRCorrelator()

# obtain noisy algorithm model
noisy_model = qorrelator_app.noisy_algorithm_model(hamiltonian, trotter_timestep, device, [noise_model])
print(noisy_model)

noisy_model is an object of class struqture.spins.SpinLindbladNoiseSystem.

Backends

The HQS Qorrelator App does not provide an internal simulator, but there are multiple interfaces (such as qoqo-quest, qoqo-for-braket and qoqo-qiskit) which can be used to either simulate the QuantumProgram or run it on quantum hardware.

Should the user wish to run a simulation emulating the device noise, the insert_noise function in the HQS Qorrelator App can be used to add in the chosen noise to the QuantumProgram.


import numpy as np
from struqture_py import spins
from struqture_py.spins import (SpinHamiltonianSystem, PauliProduct)
from hqs_qorrelator_app import NMRCorrelator
from qoqo import devices, noise_models
from qoqo_quest import Backend

number_spins = 2
time = np.pi/4
coupling = 1.0
shift = 1.0
gyromagnetic = 1.0
gyromagnetic_factors = [gyromagnetic for _ in range(number_qubits)]
number_trotter_steps = 20
trotter_timestep = 0.005

hamiltonian = spins.SpinHamiltonianSystem(number_spins)
for site in range(number_spins):
    hamiltonian.set("{}Z".format(site), -shift * site)

# Setting up the device.
single_qubit_gates = ["RotateX", "RotateZ"]
two_qubit_gates = ["CNOT"]
gate_times = 1.0
device = devices.AllToAllDevice(
    number_spins, single_qubit_gates, two_qubit_gates, gate_times
)

# While the noise model is not needed to generate the QuantumProgram, it will be requried
# when simulating the QuantumProgram.
damping = 0.0001
noise_model = noise_models.ContinuousDecoherenceModel().add_damping_rate([0, 1, 2, 3, 4], damping)

qorrelator_app = NMRCorrelator()

quantum_program = qorrelator_app.spectrum_program_fixed_step(hamiltonian, trotter_timestep, gyromagnetic_factors, device)
quantum_program_with_noise = qorrelator_app.insert_noise(quantum_program, device, [noise_model])

backend = Backend(number_spins)

# This is an alternative way of running the QuantumProgram, which allows to substitute one's
# backend of choice in the arguments. It is equivalent to backend.run_program(program, [number_trottersteps])
result = quantum_program_with_noise.run(backend, [number_trottersteps])