Introduction

The alqorithms package is part of the HQS Quantum Libraries, which contain the modules alqorithms, qonvert, noise-mapper and the bath-mapper, as shown in Figure 1. quantumlibraries Figure 1: HQS Quantum Libraries

alqorithms is a powerful package which implements the generation of circuits from Hamiltonians. With advanced algorithms such as the CNOT algorithm, the QSWAP algorithm, and the Mølmer-Sørensen algorithm, alqorithms provides seamless circuit generation capabilities. Whether the user has a pure system Hamiltonian or a system-bath Hamiltonian, the generated circuits can be executed on a quantum computer using our qoqo-backends.

Functionality

The current supported algorithms in the HQS alqorithms package are

All of these algorithms can take a pure system Hamiltonian or a system-bath Hamiltonian, and the generated circuit can be run on a quantum computer using the HQS open source qoqo-backends.

Furthermore, alqorithms includes tools to create quantum circuits that prepare measurements, which are essential to read out information in quantum computing algorithms. Additionally, the HQS package provide functionalities to perform quantum simulations of two-point correlators in the infinite temperature state. The user can also apply a symmetrization on a spin system to simplify the computation.

The core alqorithms package is written in Rust, and features a Python interface named py-alqorithms. Thus, it combines the performance and memory safety benefits of Rust with the user-friendly accessibility of Python. Rust ensures high-efficiency and reliable execution. The Python interface makes the package usable for a broader audience from the Python's extensive ecosystem, while leveraging the Rust-based core. This setup offers the best of both worlds—high performance at the core and easy integration and prototyping through Python.

API Documentation

The API documentation can be found at the following link:

Available Algorithms

All available algorithms share a common interface. Each algorithm can be initialized with the desired number_trotter_steps, order (i.e. the order of the trotterization), and the decomposition_blocks boolean flag. When decomposition_blocks is set to True, the circuit resulting from each term in the Hamiltonian is wrapped with PragmaStartDecompositionBlock and PragmaStopDecompositionBlock operations, which are needed for noise mapping.

Once an instance of the desired algorithm is initialized, a circuit can be constructed with the create_circuit method, by passing as inputs the Hamiltonian for the time evolution, and the time parameter. See the Examples section for more details.

Every available algoritm is designed with a specific device connectivity in mind. In order to run a circuit on a device with a particular connectivity graph, additional routing or transpilation steps might be necessary.

CNOTAlgorithm

System-Only CNOT Algorithm

The CNOTAlgorithm is used to generate quantum circuits that implement the exponentials of PauliProducts. It assumes full connectivity between the qubits, or at least that a CNOT gate is natively available on each pair of qubits. For each term in the PauliProduct, the following operations are applied:

  • The appropriate basis rotation is applied to transform it to the Pauli \(Z\) basis
  • A sequence of CNOT gates is applied between each qubits that appear in the Pauli string
  • A RotateZ is applied to the last qubit in the PauliProduct
  • The sequence of CNOTs is applied in reverse
  • The basis rotations are undone

For example, the CNOT decomposition for the term \( e^{i \frac{\theta}{2} Z_0 Z_1}\) is given by the following circuit

RZZ

and similarly for \( e^{i \frac{\theta}{2} Z_0 Z_1 Z_2}\)

RZZZ

For the term \( e^{i \frac{\theta}{2} X_0 X_1} = H_0H_1 e^{i \frac{\theta}{2} Z_0 Z_1 } H_0 H_1\) we need basis rotations with Hadamard gates

RXX

System-Bath CNOT Algorithm

The SystemBathCNOTAlgorithm generalizes the CNOTAlgorithm to the case of a spin system coupled to a spin bath.

The use_bath_as_control boolean flag is used to decide which one between the system qubit and the bath qubit involved in a two-qubit interaction term should be the control, and which should be the target, and therefore also on which of the two the single qubit rotation is applied.

All system-only or bath-only terms are implemented using the CNOTAlgorithm, while the two-qubit interaction terms between system and bath are implemented with the following logic:

  • If the bath operator is Pauli \(X\), then the system basis is rotated to the Pauli \(X\) basis, and CNOTs are added as in the CNOTAlgorithm, with the single-qubit rotation involved being RotateX (applied on the bath qubit or the system qubit depending on use_bath_as_control).
  • If the bath operator is not Pauli \( X\), the same logic as the CNOTAlgorithm is used, again with use_bath_as_control deciding on which qubit the RotateZ is applied.

Mølmer-Sørensen Algorithm

The VariableMSXXAlgorithm is similar to the CNOTAlgorithm, and only differs in the implementation of the two-qubit gates. While the CNOTAlgorithm treats two-qubit and multi-qubit gates equally, the VariableMSXXAlgorithm implements two-qubit gates using the VariableMSXX gate (i.e. \(R_{XX}(\theta)\) ) and basis rotations if needed.

For example, the term \( e^{- i t Z_0 Y_1} \) is implemented as

ms

QSWAP Algorithms

System-Only QSWAP Algorithms

The QSWAPAlgorithm and QSWAPMSAlgorithm implement the swapping algorithm presented in this paper to simulate the (second order) trotterized time evolution of a system of qubits on a device with linear connectivity. They only differ in the way they implement multi-qubit gates: the QSWAPAlgorithm uses the CNOTAlgorithm, while QSWAPMSAlgorithm uses the VARIABLEMSXXAlgorithm.

In order to implement two-qubit gates between qubits that are not nearest neighbors, the algorithm uses a SWAP network to shuffle the order of the logical qubits, so that each pair of logical qubits is encoded in a pair of nearest neighboring physical qubits at least once during the swapping procedure.

The SWAP network alternates even and odd layers of SWAP gates

\[ U_{SWAP} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \]

where the even layers apply the SWAPs to the even indices, and the odd layers to the odd indices. Indicating by a dash the (logical) qubits being swapped, we have

Even layer: \[ 0 - 1 \ \ 2 - 3 \ \ 4 - 5 \ \] Odd layer: \[ 0 \ \ 1 - 2 \ \ 3 - 4 \ \ 5 \ \]

This swapping procedure shuffles the pairs that are found next to each other at every layer. After \( 2N \) layers of this external swap network, the logical qubits recover their original ordering. During the swapping, all logical qubits are found next to each other at least once, and the relevant interactions can be implemented.

System-Bath QSWAP Algorithms

Similar to the system-only algorithms, the SystemBathQSWAPAlgorithm and the SystemBathQSWAPMSAlgorithm implement the swapping procedure described above for a spin system coupled to a non-interacting spin bath, and they only differ in the multi-qubit gates implementation.

The connectivity graph between the qubits consists of three parallel linear chains, with the corresponding qubits from each chain coupled with each other. The central chain represents the system qubits, while the two external chains the bath qubits. Since the bath is non-interacting, only the system qubits are swapped.

Measurement Circuits

alqorithms includes tools to create circuits that prepare measurements. The convention in gate-based quantum computing is that projective measurements of a quantum register happen by default in the Pauli Z basis. In other words, it is only possible to directly measure operators of the form

\[ \hat O = \hat O_1 \otimes \hat O_2 \otimes \ldots \otimes \hat O_N \]

where \( \hat O_j \in \left\{ \sigma^z_j, \mathrm{I}_j \right\} \).

In order to measure arbitrary Pauli strings, we need to append a measurement circuit at the end of our circuit to rotate the basis of the quantum register so that the standard measurement in the Pauli Z basis effectively corresponds to a measurement of the desired Pauli string.

Creating a single measurement circuit

In order to measure an arbitrary Pauli string \( \sigma_1^{\alpha_1} \otimes \sigma_2^{\alpha_2} \otimes \ldots \otimes \sigma_N^{\alpha_N} \), where \( \sigma_j^{\alpha_j} \in \left\{ \sigma^x, \sigma^y, \sigma^z, \mathrm{I} \right\}\), we need to apply single-qubit rotations to the qubits in the quantum register in order to rotate them from the Z basis to the eigenbasis of the corresponding entry in the Pauli string that we want to measure.

We can even measure multiple Pauli strings with the same measurement circuit, provided that they commute with each other. Two Pauli strings commute with each other if, for each qubit in the quantum register, both PauliProducts have either the same PauliOperator (X, Y, Z) or at least one has the identity \( \mathrm I\) operator. Such Pauli strings are compatible because they can be diagonalized simultaneously, which means that we can use a single measurement circuit to rotate the quantum register into their common eigenbasis.

In py-alqorithms, a measurement circuit can be created with the single_measurement_circuit function, as follows.

from struqture_py import PauliProduct
from py_alqorithms import single_measurement_circuit

# These PauliProducts are compatible
pp1 = PauliProduct.from_string("0X2Y3Z")
pp2 = PauliProduct.from_string("0X1Z")
pp3 = PauliProduct.from_string("2Y3Z")

meas_circuit = single_measurement_circuit([pp1, pp2, pp3], "ro", False, 4)

Creating a measurement for an observable

If we want to measure an arbitrary observable, represented in struqture as a linear combination of PauliProducts, in general, the Pauli products will not all be compatible with each other. We will then need multiple measurement circuits that we'll use to perform separate measurements for each set of compatible PauliProducts. For this, we can use the function measure_spin_operator (or measure_spin_operator_map if we have a list of operators to measure), returning a qoqo PauliZProduct, an object that collects all the information needed to perform the separated measurements and post-process the measurement results into the expectation value of the observable. It can be used as follows

from qoqo import Circuit
from qoqo import Circuit
from qoqo.operations import RotateY
from struqture_py import PauliProduct, SpinSystem
from py_alqorithms import measure_spin_operator

number_measurements = 10
number_qubits = 5
# whether to undo the basis rotation after the measurement circuit
undo_basis_rotation = False

# Prepare the base circuit after which to measure the observable
constant_circuit = Circuit()
for qubit in range(number_qubits):
    constant_circuit += RotateY(qubit, 0.5)

pauli_products = [
  # First set of compatible PauliProducts
  "0X2Y3Z",
  "0X1Z",
  "2Y3Z",
  # Second set of compatible PauliProducts
  "0Z2Z3X",
  "0Z3X",
  "1Y2Z",
]

op = SpinSystem()
for pp in pauli_products: 
    op.set(pp, 1.0)
    
measurement = measure_spin_operator(
    op,
    constant_circuit,
    "my_op",
    undo_basis_rotation,
    number_measurements
)

This measurement can then be used in a QuantumProgram as follows

from qoqo_quest import Backend

backend = Backend(number_qubits)
qp = QuantumProgram(measurement, [])
qp.run(backend, None)

Similarly we can create cheated measurements (CheatedPauliZProduct) using the function measure_spin_operator_map_cheated.

Correlators

alqorithms provides the class InfiniteTemperatureCorrelator to create qoqo QuantumPrograms to perform quantum simulations of two-point correlators in the infinite temperature state.

Infinite Temperature State Preparation

We want to measure correlators of the form \(\langle A(t)B \rangle\) with respect to the infinite-temperature state, represented by the maximally-mixed density matrix

\[ \hat \rho_0 = \frac{1}{2^N} \hat 1 . \]

There are different ways to achieve this. The default initialization is all_initial_states, which measures the infinite temperature operator \( A \) by running the correlation measurement over all possible time-evolved eigenstates of the \( B \) operator. With this option, the number of circuits and measurements scales exponentially with the number of qubits. It has, however, the least requirements for the quantum computer. Other ways to prepare the infinite-temperature state can require active-reset or mid-circuit measurement operations.

The other available initializations are:

  • active_reset: Uses an initialisation that prepares the infinite-temperature state by dephasing after applying Hadamard gates. Assumes the \( B \) operator only acts on one qubit q. Actively resets the qubit the \( B \) operator is acting on and adds two measurement circuits:

    • One where the qubit is set to 0 and the output of a virtual qubit is set to zero
    • One where the qubit is set to 1 and the output of a virtual qubit is set to zero

    This avoids measuring inside the circuit which is more efficient. This can only be applied when the \( B \) operator is a sum of local operators and the number of measurements is comparable to the dimension of the Hilbert space of the system. The sequence is: Hadamard on all qubits -> ActiveReset on q -> Two Circuits: One where q is 0 one where q is 1 -> Rotate back from \( B \) basis.

  • local_operator_measurement: Uses an initialisation that prepares the infinite-temperature state by applying Hadamard gates and using the measurement that is normally necessary to extract the correlator with the \( B \) correlation operator. This can only be applied when the \( B \) operator is a sum of local operators and the number of measurements is comparable to the dimension of the Hilbert space of the system. The sequence is: Hadamard on all qubits -> Measure -> Rotate back from \( B \) basis. It is recommended to only use this on an experimental basis.

  • measurement: Uses an initialization that prepares the infinite-temperature state by measuring after a Hadamard gate. This can only be applied when the number of measurements is comparable to the dimension of the Hilbert space of the system. The sequence is: Hadamard on all qubits -> Measure -> Rotate into \( B \) basis -> Measure -> Rotate back from \( B \) basis. It is recommended to only use this on an experimental basis.

  • dephasing: Uses an initialization that prepares the infinite-temperature state by dephasing after applying Hadamard gates. This can only be applied when the number of measurements is comparable to \(2^N \) where \(N\) is the number of qubits the \( B \) operator acts on. The sequence is: Hadamard on all qubits -> Let system dephase -> Rotate into \( B \) basis -> Measure -> Rotate back from \( B \) basis. It is recommended to only use this on an experimental basis.

Usage

Once initialized, the class InfiniteTemperatureCorrelator provides three methods to compute the desired correlator:

  • time_correlation_measurement: Run the measurement for a single correlator.
  • time_correlation_measurement_multiple_left: Run the measurement for a list of correlators that have different left operators \( A \). It works like time_correlation_measurement, but a list of left operators is passed as input instead of a single operator. The output QuantumProgram has a symbolic parameter for the evolution time.
  • time_correlation_measurement_multiple_left_fixed_timestep: Similar to the method above, but allows to pass a fixed trotter timestep. The output QuantumProgram has a symbolic parameter for the number of trotter steps.

The following example in Python shows how to initialize the InfiniteTemperatureCorrelator class and use it to compute the correlator \( \langle Y_0(t) Z_1 \rangle \).

from py_alqorithms import InfiniteTemperatureCorrelator
from struqture_py import SpinSystem, SpinHamiltonianSystem

number_spins = 3

# Set up Hamiltonian for time evolution
hamiltonian = SpinHamiltonianSystem()
for i in range(number_spins-1):
    hamiltonian.add_operator_product(f"X{i}X{i + 1}", 1.0)
for i in range(number_spins):
    hamiltonian.add_operator_product(f"Z{i}", 1.0)
print(f"Hamiltonian: \n{hamiltonian}\n")


# Define right and left operators to correlate
left_op = SpinSystem()
left_op.add_operator_product("Y0", 1.0)
print(f"Left operator: \n{left_op}\n")

right_op = SpinSystem()
right_op.add_operator_product("Z1", 1.0)
print(f"Right operator: \n{right_op}\n")

# Initialize correlator class
correlator = InfiniteTemperatureCorrelator(number_trottersteps=100)
correlator.algorithm = "ParityBased"
correlator.number_measurements = 10000
correlator.initialisation = "all_initial_states"
print("Correlator: ", correlator)

# Construct quantum program
measurement = correlator.time_correlation_measurement(left_op, hamiltonian, right_op)
program = QuantumProgram(measurement, [])

Symmetrization

The py_alqorithms.symmetrization module provides functions to symmetrize a spin system by conjugating the Hamiltonian with Pauli \( \sigma^x \) operators.

Pure Spin System

For pure systems, the following two functions are provided:

  • create_symmetrized_spin_circuit: Creates a quantum circuit implementing the trotterized time evolution with the Hamiltonian

    \[ \tilde H = \left( \bigotimes_i \sigma_i^x \right) H \left( \bigotimes_i \sigma_i^x \right) \]

    where \( H \) is the input spin Hamiltonian.

  • apply_symmetrization_spins: Similar to the function above, but additionally applies a PauliX gate to every qubit to switch the definition of \( | 0 \rangle \) and \( | 1 \rangle \), both before and after the circuit implementing the trotterized time evolution with the symmetrized Hamiltonian.

Example

A minimal example describing the use of apply_symmetrization_spins function. The hamiltonian is given by

\[ H = \frac{1}{2} \sigma_0^z + \sigma_1^z. \]

Figure 1 illustrates the time evolution circuit of \(H\) with two trotter steps.

import py_alqorithms
from struqture_py.spins import SpinHamiltonianSystem, PauliProduct

# Parameters
algorithm = "ParityBased"
trotterization_order = 1
number_trottersteps = 2
time = 5.0

# Initizaling spin hamiltonian
hamiltonian = SpinHamiltonianSystem(2)
hamiltonian.add_operator_product(PauliProduct().z(0), 0.5)
hamiltonian.add_operator_product(PauliProduct().z(1), 1.0)

# Symmetrizing spin circuit
circuit = py_alqorithms.apply_symmetrization_spins(
    algorithm,
    hamiltonian,
    trotterization_order,
    number_trottersteps,
    time
)

symmetrization spins Figure 1: Trotterized time evolution of spin hamiltonian

System-Bath

For system-bath algorithms, the following functions are provided:

  • create_symmetrized_system_bath_circuit: Similar to create_symmetrized_spin_circuit, only applying the transformation to the system qubits. Together with the resulting circuit created by the selected algorithm, it also returns the list of qubits that constitute the system.
  • apply_symmetrization_system_bath - Similar to apply_symmetrization_spins, only applying the transformation to system qubits.

Example

A minimal example describing the symmetrization of a system-bath circuit. The hamiltonian is given by

\[ H = \frac{1}{2} \sigma_{0,s}^z + \frac{1}{2} \sigma_{1,s}^x \sigma_{0,b}^x + \sigma_{1, b}^z. \]

Figure 2 illustrates a single trotter step time evolution of the system-bath hamiltonian.

import py_alqorithms
from struqture_py.spins import PauliProduct
from struqture_py.mixed_systems import MixedHamiltonianSystem, HermitianMixedProduct

# Parameters
algorithm = "ParityBased"
number_trottersteps = 1
time = 5.0
remapping = ([0,1], [2,3,4,5], None)
use_bath_as_control = False

# Initializing the system bath hamiltonian
hamiltonian = MixedHamiltonianSystem([2,2], [], [])
hamiltonian.add_operator_product(
    HermitianMixedProduct(
        [PauliProduct().z(0), PauliProduct()],
        [],
        []
    ),
    0.5
)

hamiltonian.add_operator_product(
    HermitianMixedProduct(
        [PauliProduct().x(1), PauliProduct().x(0)],
        [],
        []
    ),
    0.5
)
hamiltonian.add_operator_product(
    HermitianMixedProduct(
        [PauliProduct(), PauliProduct().z(1)],
        [],
        []
    ),
    1.0
)

# Symmetrizing system-bath circuit
circuit = py_alqorithms.apply_symmetrization_system_bath(
    algorithm,
    hamiltonian,
    number_trottersteps,
    time,
    use_bath_as_control,
    remapping
)

symmetrization system bath Figure 2: Trotterized time evolution of system-bath hamiltonian

Examples

CNOTAlgorithm

from struqture_py.spins import PauliProduct, SpinHamiltonianSystem
from py_alqorithms import CNOTAlgorithm

# create Hamiltonian
hamiltonian = SpinHamiltonianSystem(5)
hamiltonian.set("X0Y2", 0.5)
hamiltonian.set("Z1X3", 1.5)
hamiltonian.set("Z0", 1.0)
hamiltonian.set("X2", 1.0)

# create algorithm (here with second order trotterization)
alg = CNOTAlgorithm(number_trotter_steps=10).decomposition_blocks(True).order(2)
time = 3.0

# create circuit
circuit = alg.create_circuit(hamiltonian, time)

VariableMSXXAlgorithm

from struqture_py.spins import PauliProduct, SpinHamiltonianSystem
from py_alqorithms import VariableMSXXAlgorithm

# create Hamiltonian
hamiltonian = SpinHamiltonianSystem(5)
hamiltonian.set("X0Y2", 0.5)
hamiltonian.set("Z1X3", 1.5)
hamiltonian.set("Z0", 1.0)
hamiltonian.set("X2", 1.0)

# create algorithm
alg = VariableMSXXAlgorithm(number_trotter_steps=10).decomposition_blocks(True)
time = 3.0

# create circuit
circuit = alg.create_circuit(hamiltonian, time)

QSWAPAlgorithm

from struqture_py.spins import PauliProduct, SpinHamiltonianSystem
from py_alqorithms import QSWAPAlgorithm

# create Hamiltonian
hamiltonian = SpinHamiltonianSystem()
hamiltonian.set("X0Y3", 0.5)
hamiltonian.set("Z0X2", 1.5)
hamiltonian.set("Z0Y1", 1.0)
hamiltonian.set("X2", 1.0)
hamiltonian.set("Z1", 1.0)

# create algorithm
alg = QSWAPAlgorithm(number_trotter_steps=10).decomposition_blocks(True)
time = 3.0

# create circuit
circuit = alg.create_circuit(hamiltonian, time)