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.
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
- the CNOT algorithm, which assumes qubits with all-to-all connectivity,
- the Mølmer-Sørensen-Algorithm, which is similar to the CNOT algorithm but implements a different two-qubit gate,
- and the QSWAP algorithm, which assumes qubits with linear connectivity.
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
PauliProduct
s. 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 thePauliProduct
- 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
and similarly for \( e^{i \frac{\theta}{2} Z_0 Z_1 Z_2}\)
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
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 beingRotateX
(applied on the bath qubit or the system qubit depending onuse_bath_as_control
). - If the bath operator is not Pauli \( X\), the same logic as the
CNOTAlgorithm
is used, again withuse_bath_as_control
deciding on which qubit theRotateZ
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
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 qubitq
. 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 whereq
is 0 one whereq
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 liketime_correlation_measurement
, but a list of left operators is passed as input instead of a single operator. The outputQuantumProgram
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 outputQuantumProgram
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 aPauliX
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
)
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 tocreate_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 toapply_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
)
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)