Pyqonvert User Documentation

The qonvert 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. Pyqonvert is the Python interface for the core qonvert package, which is written in Rust. quantumlibraries Figure 1: HQS Quantum Libraries

Pyqonvert is the HQS compiler package, which offers comprehensive functionality for quantum circuit and quantum program optimization. It decomposes circuits into a given set of gates, optimizes them for efficiency, and introduces noise to emulate the behavior of physical quantum computers. Additionally, pyqonvert provides routing capabilities to further enhance circuit performance.

Functionality

HQS's pyqonvert offers a range of classes designed to "convert" quantum circuits and quantum programs, each serving as a Converter with specific functionalities:

  • Gate Decomposition - enables the decomposition of single- and two-qubit gates into native gates supported by the quantum computing hardware.
  • Noise Insertion - facilitates the insertion of decoherence noise through qoqo's Pragma operations and introduces over-rotation noise via rotation gates. Note: qoqo is the open-source toolkit to represent quantum circuits by HQS Quantum Simulations.
  • Optimization - optimizes quantum circuits and quantum programs to enhance efficiency and performance.
  • Routing - provides routing capabilities to address the challenge of limited qubit connectivity in quantum computing hardware.

Using the library

Every Converter implements the convert method, offering a unified interface that takes as input

  • circuits or quantum programs
  • a qoqo.devices device
  • optionally, a noise model.

Output: The method ensures consistency and ease of use by returning the same type that was input—if a circuit is provided, a circuit is returned; if a quantum program is input, a quantum program is returned.

This straightforward design is powerful, enabling users to apply various conversions across circuits or programs with minimal code. For instance, the converters can be gathered in a list and called from within a for loop:

converters = [ ... optimizer, routers, decomposers, noise adders ... ]

for converter in converters:
    circuit = converter.convert(circuit, device, noise_models)

To see pyqonvert in action, refer to the examples.

API Documentation

HQS provides a comprehensive API documentation for all the functions availble in pyqonvert.

Gate decompositions: single- and two-qubit

Gate decomposition in quantum computing refers to the process of breaking down a gate into a set of single- and two-qubit gates. In general, the user will create a quantum circuit using a quantum algorithm, which is hardware-agnostic. While this quantum circuit can usually be simulated on many backends (e.g. qoqo-quest, qoqo-qiskit, qoqo-for-braket) without any recompilation, running it on a real quantum device requires adapting to the restricted set of natively available gates. Therefore, it is important to be able to decompose the gates present in the circuit into the gates available on the hardware. Additionally, different types of quantum computers have distinct set of native gate making decomposition even more essential.

Gate decomposition also plays a crucial role when modeling the noise of a quantum device. As noise often occurs during each gate operation, ensuring that the circuit contains the correct sequence of gates is vital for the correctness of the computation.

Pyqonvert offers the SingleQubitGateDecomposer and TwoQubitGateDecomposer converters for this purpose. They are applied to circuits and quantum programs using the convert function.

Single-qubit gate decompositions

Functionality

The SingleQubitGateDecomposer converter decomposes any single-qubit gates into a set of specified single-qubit gates. It takes as input a quantum circuit or a quantum program and the device specifications, which specifies the available gate operations. The converter then matches the circuit's single-qubit gates with the operations supported by the device. If a single-qubit gate is not supported, the converter computes various possible decompositions, each weighted by their gate counts. Finally, the decomposition with the fewest gates is selected to replace the original gate in the circuit.

For details on the available decompositions, please refer to the Python API

Initalizing the SingleQubitGateDecomposer Class.

The SingleQubitGateDecomposer class is imported from the pyqonvert.decompositions module.

from pyqonvert.decompositions import SingleQubitGateDecomposer

# initialize noise inserter
decomposer = SingleQubitGateDecomposer()

# Ready to use for noise addition.

Example

We give an example to show the single-qubit decomposer's functionality. Figure 1 contains the resulting circuit.

from pyqonvert.decompositions import SingleQubitGateDecomposer
from qoqo import devices, Circuit
from qoqo import operations as ops

decomposer = SingleQubitGateDecomposer()

# Initialize device
number_qubits = 3
device = devices.AllToAllDevice(
    number_qubits,
    [
        "RotateY",
        "RotateZ",
        "SqrtPauliX",
        "InvSqrtPauliX",
    ],
    [],
    1.0,
    )

# Initialize circuit
circuit = Circuit()
circuit += ops.DefinitionBit("ro", 1, True)
circuit += ops.CNOT(0, 1)
circuit += ops.CNOT(0, 2)
circuit += ops.Hadamard(0)
circuit += ops.MeasureQubit(0, "ro", 0)

# Decomposition
circuit = decomposer.convert(circuit, device, [])

single qubit gate decomposition Figure 1: Single-qubit gate decomposition

Two-qubit gate decompositions

The TwoQubitGateDecomposer converter decomposes general two-qubit gates into native two-qubit gates and generic single-qubit gates. It takes a quantum circuit and the device specifications as input. The device specifies the available gate operations. The converter then matches the circuit's gates with the operations supported by the device. If a two-qubit gate is not supported, the converter explores various possible decompositions, each weighted by their gate counts. Finally, the decomposition with the fewest gates is selected to replace the original gate in the circuit.

For details on the available decompositions, please refer to the Python API

Initalizing the TwoQubitGateDecomposer Class.

The TwoQubitGateDecomposer class is imported from the pyqonvert.decompositions module.

from pyqonvert.decompositions import TwoQubitGateDecomposer

# initialize noise inserter
decomposer = TwoQubitGateDecomposer()

# Ready to use for noise addition.

Example

We provide an example to demonstrate the functionality of the two-qubit decomposer, as illustrated in Figure 2, which shows the resulting circuit. It is important to note that the two-qubit decomposer produces single-qubit rotation gates that may not be natively supported by the device. Therefore, in practice, it is essential to subsequently apply the single-qubit decomposition converter.

from pyqonvert.decompositions import TwoQubitGateDecomposer
from qoqo import devices, Circuit
from qoqo import operations as ops

decomposer = TwoQubitGateDecomposer()

# Initialize device
number_qubits = 3
device = devices.AllToAllDevice(
    number_qubits,
    [],
    [
        "CNOT",
        "ControlledPauliZ",
        "PhaseShiftedControlledZ",
    ],
    1.0,
    )

# Initialize circuit
circuit = Circuit()
circuit += ops.DefinitionBit("ro", 1, True)
circuit += ops.PauliX(0)
circuit += ops.SWAP(0, 1)
circuit += ops.Hadamard(0)
circuit += ops.MeasureQubit(0, "ro", 0)

# Decomposition
circuit = decomposer.convert(circuit, device, [])

two qubit gate decomposition Figure 2: two-qubit gate decomposition

Adding noise to a QuantumProgram or Circuit

Noise is an inherent part of real quantum systems, and simulating its effects is crucial for understanding the behavior of quantum algorithms in real-world conditions. The open source package qoqo provides users with several ways to define noise models precisely. The noise operations can be directly added to a quantum circuit and then be simulated by compatible backends.

Adding Decoherence

As decoherence noise does not translate into unitary quantum gates, the noise operations are defined as Pragma operations in qoqo. The strength of the noise is determined by defining a gate_time and a rate. The noise Pragma operations affect the system as a Lindblad type noise acting on the system with the rate rate for the time gate_time.

The Noise Models

Struqture lets users represent the decoherence part of the Lindblad equation using the struqture.spins.PlusMinusLindbladNoiseOperator class. The available noise models include:

  • ContinousDecoherenceModel: The noise model representing a continuous decoherence process on qubits. This noise model assumes that all qubits are constantly experiencing decoherence over time (e.g. due to coupling to the environment). The noise for each qubit can be different but only single-qubit noise is included in the model

  • DecoherenceOnGateModel: This noise model for noise that is only present on gate executions. It adds noise when specific gates (identified by the name of the gate and qubits it acts on) are executed.

  • DecoherenceOnIdleModel: The noise model which adds noise to qubits not involved in an operation, when that gate operation is executed (i.e. to the qubits which remain idle during the operation).

The NoiseInserter Converter

The NoiseInserter allows users to add noise Pragma operations to a given circuit or QuantumProgram, where the noise operations are derived from the input noise models. The noise model specifies the type of noise operations to be inserted, e.g. PragmaDamping, PragmaDephasing or PragmaDepolarizing as well as the rate of the noise. The gate time is inferred from the device being used.

The noise is added either before, after or symmetrical around gate operations present in the circuit. This is defined by the user when initializing the NoiseInserter.

Initializing the NoiseInserter Class

The NoiseInserter class is imported from the pyqonvert.noise module. The following values are needed to initialize it.

  • noise_mode: specifies the type of noise insertion. Allowed values include
    • active_qubits_only - noise Pragmas are added to the qubits involved in gate operations in the circuit.
    • all_qubits - noise Pragmas are added on all the qubits involved in the circuit.
    • parallelization_blocks - noise Pragmas are added to the qubits involved in the a parallel block marked by PragmaStopParallelBlock operations in the circuit.

Additionally, there is the noise placement mode property of the NoiseInserter, which can be modified by the user. This setting determines whether noise is applied after, before, or symmetrically around (both before and after) a gate operation. By default, the noise placement mode is set to "After", meaning that all noise Pragma operations are sequentially added after gate operations in a circuit. You can modify this behavior using the functions provided below.

from pyqonvert.noise import NoiseInserter

noise_mode = "all_qubits"
# initialize noise inserter
noise_inserter = NoiseInserter(noise_mode)

# Ready to use for noise addition.
Noise placement modes

The following functions help in updating the noise placement mode:

  • noise_before_gate - noise Pragmas are placed before each gate operation.
noise_inserter = NoiseInserter(noise_mode)
noise_inserter.noise_before_gate()
  • noise_symmetric_around_gate - noise Pragmas are placed symmetrically around each gate operation.
noise_inserter = NoiseInserter(noise_mode)
noise_inserter.noise_symmetric_around_gate()
  • noise_after_gate - noise Pragmas are placed after each gate operation.
noise_inserter = NoiseInserter(noise_mode)
noise_inserter.noise_after_gate()

Examples for each type of noise insertion

The NoiseInserter Class provides the convert method to insert noise into quantum circuits and QuantumPrograms.

The following example shows how one can insert noise according to a DecoherenceOnGateModel into a circuit with the "all_qubits" mode.

from pyqonvert.noise import NoiseInserter
from qoqo import devices, noise_models as nm, Circuit
from qoqo import operations as ops
from struqture_py.spins import PlusMinusLindbladNoiseOperator, PlusMinusProduct

noise_inserter = NoiseInserter("all_qubits").noise_before_gate()

decoherence_rate = 0.011
number_qubits = 2
# Initialize device and noise_model
device = devices.AllToAllDevice(
    number_qubits,
    ["Hadamard"],
    ["CNOT"],
    0.5
)
noise = PlusMinusLindbladNoiseOperator()
noise_product = PlusMinusProduct().z(0).plus(0)
noise.add_operator_product((noise_product, noise_product), 2.5)
noise_model = nm.DecoherenceOnGateModel().set_single_qubit_gate_error( 
    "Hadamard",
    0,
    noise

)

# initialize circuit
circuit = Circuit()
circuit += ops.Hadamard(0)
circuit += ops.CNOT(0,1)

# Add noise
circuit_noise = noise_inserter.convert(circuit, device, [noise_model])

decoherence on gate model Figure 1: Adding decoherence noise "before" Hadamard gates on qubit 0

The convert function is applied to a QuantumProgram in a similar fashion.

from pyqonvert.noise import NoiseInserter
from qoqo import devices, noise_models as nm, Circuit, QuantumProgram
from qoqo import operations as ops
from qoqo.measurements import PauliZProduct, PauliZProductInput

noise_inserter = NoiseInserter("active_qubits_only")

decoherence_rate = 0.011
number_qubits = 2
# Initialize device and noise_model
device = devices.AllToAllDevice(
    number_qubits,
    ["Hadamard"],
    ["CNOT"],
    0.5
)
noise_model = nm.ContinuousDecoherenceModel().add_damping_rate( 
    [0, 1],
    decoherence_rate
)

# initialize program
circuit = Circuit()
circuit += ops.Hadamard(0)
circuit += ops.CNOT(0,1)

z_circuit = Circuit()
z_circuit += ops.DefinitionBit("ro_z", 1, is_output=True)
z_circuit += ops.PragmaRepeatedMeasurement("ro_z", 1000, None)

measurement_input = PauliZProductInput(1, False)
measurement = PauliZProduct(
   constant_circuit = circuit,
   circuits=[z_circuit],
   input=measurement_input,
)
qp = QuantumProgram(measurement, [])

# Add noise
program_noise = noise_inserter.convert(qp, device, [noise_model])

continous decoherence noise Figure 2: Adding continous decoherence noise on active qubits

A circuit that has been optimized using the CircuitParallelizer converter is interspersed with PragmaStopParallelBlock instances. Noise can be added to a parallelized circuit or QuantumProgram using the "parallelization_blocks" mode. More information about the CircuitParallelizer converter can be found here and in the Python API documentation.

from pyqonvert.noise import NoiseInserter
from qoqo import devices, Circuit, noise_models as nm
from qoqo import operations as ops

noise_inserter = NoiseInserter("parallelization_blocks")

decoherence_rate = 0.011
number_qubits = 2
# Initialize device
device = devices.AllToAllDevice(
    number_qubits,
    ["Hadamard"],
    [],
    0.5
)

noise_model = nm.DecoherenceOnIdleModel().add_damping_rate( 
    [0, 1],
    decoherence_rate
)

# initialize circuit
circuit = Circuit()
circuit += ops.Hadamard(0)
circuit += ops.PragmaStopParallelBlock([0], 0.5)
circuit += ops.Hadamard(1)
circuit += ops.PragmaStopParallelBlock([1], 0.5)

# Add noise
circuit_noise = noise_inserter.convert(circuit, device, [noise_model])

Adding Overrotation Noise

Overrotation noise refers to the arbitrary rotation of qubits due to background factors. Pyqonvert facilitates the addition of such noise using the following noise model:

  • SingleQubitOverrotationOnGate: The noise model which places a rotation gate with a randomly distributed rotation angle after a specified gate operation. Through the SingleQubitOverrotationDescription class, one can define the parameters of the Gaussian distribution (mean and standard deviation) from which the overrotation angle is sampled.

The SingleQubitOverrotationAdder converter

The purpose of SingleQubitOverrotationAdder is to add single-qubit rotation gates that simulate overrotation noise informed by a SingleQubitOverrotationOnGate model.

Initializing the SingleQubitOverrotationAdder Class

The SingleQubitOverrotationAdder class is imported from the pyqonvert.noise module.

  • seed: the(optional) seed for the random number generator. If a seed is not provided, a machine-generated seed is used.
from pyqonvert.noise import SingleQubitOverrotationAdder

# initialize SingleQubitOverrotationAdder without a seed
overrotation_adder = SingleQubitOverrotationAdder()

# initialize SingleQubitOverrotationAdder with a seed
overrotation_adder_seeded = SingleQubitOverrotationAdder(seed=42)

# Ready to use for noise addition.

Example

Like NoiseInserter, the SingleQubitOverrotationAdder Class has the convert function which adds overrotation noise to quantum circuits and QuantumPrograms.

from pyqonvert.noise import SingleQubitOverrotationAdder
from qoqo import devices, Circuit, noise_models as nm
from qoqo import operations as ops

# Initializing the converter
noise_inserter = SingleQubitOverrotationAdder(seed=1)

# noise model
rotation_gate = "RotateZ"
# Parameters for Gaussian distribution single-qubit gates
mean_single = 0.0
std_single = 1.0
overrotation_noise_description_single = nm.SingleQubitOverrotationDescription("RotateZ", mean_single, std_single)

# Parameters for Gaussian distribution two qubit gates
mean_two = 1.0
std_two = 2.0
overrotation_noise_description_two = nm.SingleQubitOverrotationDescription("RotateX", mean_two, std_two)

overrotation_noise = nm.SingleQubitOverrotationOnGate()
overrotation_noise = overrotation_noise.set_single_qubit_overrotation(
    "Hadamard",
    0,
    overrotation_noise_description_single
)
overrotation_noise = overrotation_noise.set_two_qubit_overrotation(
    "CNOT",
    0,
    1,
    (overrotation_noise_description_two, overrotation_noise_description_two)
)

# Initialize device and noise_model
number_qubits = 2
device = devices.AllToAllDevice(
    number_qubits,
    ["Hadamard"],
    ["CNOT"],
    0.5
)

# initialize circuit
circuit = Circuit()
circuit += ops.Hadamard(0)
circuit += ops.CNOT(0,1)

# Add noise
circuit_noise = noise_inserter.convert(circuit, device, [overrotation_noise])

overrotation noise The usage in the case of QuantumPrograms is similar:

program_noise = noise_inserter.convert(quantum_program, device, [overrotation_noise])

Figure 3: Adding overrotation noise

Optimizing the QuantumProgram or Circuit

The pyqonvert package consists of a number of methods to optimize quantum circuits by reducing the number of gates in the circuit. These are packaged as Converters, hence applied using the convert function.

Available optimizers

  • IdentityRemover - optimizes a quantum circuit or QuantumProgram by eliminating gates that are identified as identity below a certain specified tolerance. This optimizer takes identity_tolerance as an input.
# Initialization
optimizer = IdentityRemover(identity_tolerance=1e-9)
  • MultipleRotationGateSimplifier - optimizes a quantum circuit or QuantumProgram by adding up identical rotation gates in separate DecompositionBlocks.
# Initialization
optimizer = MultipleRotationGateSimplifier()
  • CircuitParallelizer - optimizes a quantum circuit or QuantumProgram by grouping operations into parallel execution blocks. Each parallel execution block is terminated by a PragmaStopParallelBlock operation. The execution time of the parallel block is extracted from the properties of the input device.
# Initialization
optimizer = CircuitParallelizer()
  • SleepInserter - adds a PragmaSleep operation before or after a gate, depending on the user specified placement mode. The PragmaSleep will only be added to gates that involve at least one qubit from the input of the SleepInserter and are of the gate type specified in the input.
# Initialization
optimizer = SleepInserter(
    gates = ["PauliZ"],
    involved_qubits = [0,1],
    sleep_time = 2.5,
    placement = "after"
)

placement is an optional argument set to "after" by default.

  • NumericSingleQubitMultiplier - optimizes a quantum circuit or a QuantumProgram by multiplying non-symbolic single-qubit-gates. As an argument, it takes in identity_tolerance that specifies a threshold below which gates are neglected as identity.
# Initialization
optimizer = NumericSingleQubitMultiplier(identity_tolerance=1e-9)
  • VirtualZGateReplacement - applies virtual Z gate optimization. Provided that the natural two-qubit gate is diagonal in the Z-basis, the converter replaces RotateZ gates by virtual changes (rotations) of the qubit basis in the XY-Plane. This is done by replacing gates corresponding to rotation in XY-Plane with RotateXY gate.
# Initialization
optimizer = VirtualZGateReplacement()

By default, the converter adds RotateZ gates at the end to undo the changes made to the qubit basis. This behavior can be modified by using the function apply_final_rotz

optimizer = VirtualZGateReplacement().apply_final_rotz(False)

Note that if RotateXY gate is available in the device, the user is not required to use the VirtualZGateReplacement converter. Simply running the gate decomposition routine produces the same effect as the optimizer.

Example

We give an example where we apply a series of optimizations on a quantum circuit.

from pyqonvert.optimization import (
    IdentityRemover,
    NumericSingleQubitMultiplier,
    CircuitParallelizer,
)
from qoqo import devices, Circuit
from qoqo import operations as ops

# Initialize device
number_qubits = 2
device = devices.AllToAllDevice(
    number_qubits, ["RotateZ", "RotateX", "SingleQubitGate"], ["CNOT"], 0.5
)

# Initialize circuit
circuit = Circuit()
circuit += ops.RotateZ(0, 1e-6)
circuit += ops.RotateZ(0, 0.5)
circuit += ops.RotateX(1, -1.0)
circuit += ops.RotateX(1, 2.0)
circuit += ops.CNOT(0, 1)

# List of converters
converters = [IdentityRemover(1e-5), NumericSingleQubitMultiplier(1e-5), CircuitParallelizer()]

# Optimizations
for converter in converters:
    circuit = converter.convert(circuit, device, [])

optimized circuit Figure 1: Resulting circuit after the application of optimization converters.

Routing on a device

In many quantum computing architectures, not all qubits are directly connected, meaning that operations between distant qubits must be routed through intermediate qubits. This is accomplished by inserting SWAP operations between qubits in a circuit.

The SingleOperationSwapRouter Converter

pyqonvert provides the SingleOperationSwapRouter Converter that inserts SWAP operations into a quantum circuit or QuantumProgram to enable operations on devices with limited connectivity.

Initalizing the SingleOperationSwapRouter Class.

The SingleOperationSwapRouter class is imported from the pyqonvert.routing module.

Parameters
  • swap_back - if set to True, additional SWAP operations are inserted to retrieve the original layout.
  • only_before_decomposition - if set to True, SWAPS are only inserted outside of decomposition blocks. This option comes in handy during noise mapping which extensively makes use of decomposition blocks. Naively routing together with noise mapping requires a large number of SWAPs contributing to additional complexity. To minimize the number of SWAPs and allow for remapping without disrupting noise mapping, SWAPs are strategically inserted only before decomposition blocks that involve one or two qubits. To learn more about noise mapping, refer to this research article.

Note that when using setting only_before_decomposition=True, not all operations in the circuit (especially decomposition blocks involving more qubits in the first place) are routed. For full routing, a second pass with swap_back=True is necessary.

from pyqonvert.routing import SingleOperationSwapRouter

# initialize converter
noise_inserter = SingleOperationSwapRouter(
    swap_back=True,
    only_before_decomposition=False,
)


# Ready to use for routing.

Examples

Here is a simple example showing the functionality of the routing converter.

from pyqonvert.routing import SingleOperationSwapRouter
from qoqo import devices, Circuit
from qoqo import operations as ops

router = SingleOperationSwapRouter(True, False)

# Initialize device
number_qubits = 3
device = devices.GenericDevice(number_qubits)
device.set_two_qubit_gate_time("CNOT", 0, 1, 1.0)
device.set_two_qubit_gate_time("CNOT", 1, 0, 1.0)
device.set_two_qubit_gate_time("CNOT", 1, 2, 1.0)
device.set_two_qubit_gate_time("CNOT", 2, 1, 1.0)

# initialize circuit
circuit = Circuit()
circuit += ops.CNOT(0,2)
circuit += ops.PragmaRepeatedMeasurement("ro", 10)

# Routing
circuit = router.convert(circuit, device, [])

routing decomposition blocks Figure 1: SWAP gates insertions in a circuit for routing over a device of linear topology.

The following example shows how only_before_decomposition flag functions.

from pyqonvert.routing import SingleOperationSwapRouter
from qoqo import devices, Circuit
from qoqo import operations as ops

router = SingleOperationSwapRouter(False, True)

# Initialize device
number_qubits = 3
device = devices.GenericDevice(number_qubits)
device.set_two_qubit_gate_time("CNOT", 0, 1, 1.0)
device.set_two_qubit_gate_time("CNOT", 1, 0, 1.0)
device.set_two_qubit_gate_time("CNOT", 1, 2, 1.0)
device.set_two_qubit_gate_time("CNOT", 2, 1, 1.0)

# Initialize circuit
circuit = Circuit()
map = {0: 2, 2:0}
circuit += ops.PragmaStartDecompositionBlock([0, 2], map)
circuit += ops.CNOT(0, 2)
circuit += ops.CNOT(2, 0)
circuit += ops.PragmaStopDecompositionBlock([0, 2])
circuit += ops.PragmaStartDecompositionBlock([0, 1, 2], {})
circuit += ops.CNOT(1, 2)
circuit += ops.PragmaStopDecompositionBlock([0, 1, 2])

# Routing
circuit = router.convert(circuit, device, [])

routing decomposition blocks Figure 2: SWAP gates are added outside of the original decomposition blocks when only_before_decomposition = True.

Examples

Ising Model Hamiltonian

We will consider the example of a 3-spin transverse Ising model Hamiltonian that we want to simulate on a noisy quantum device. The device is assumed to have a linear topology, with direct connectivity between qubits 0 and 1, and between qubits 1 and 2. The noise will be modeled using classes from the qoqo.noise_models module. By utilizing pyqonvert, we will generate a finalized circuit that can be run on the device.

The Hamiltonian takes the form \[ H = \sum_{\langle i,j \rangle} J_{ij} \sigma^z_i \sigma^z_j + \sum_i h_i \sigma^x. \]

To begin, we import all the necessary packages

from pyqonvert.optimization import IdentityRemover, NumericSingleQubitMultiplier
from pyqonvert.decompositions import SingleQubitGateDecomposer, TwoQubitGateDecomposer
from pyqonvert.routing import SingleOperationSwapRouter
from pyqonvert.noise import NoiseInserter
from qoqo import devices, Circuit, noise_models as nm
from qoqo import operations as ops
from struqture_py.spins import PlusMinusProduct, PlusMinusLindbladNoiseOperator

Then, we create the quantum circuit representation of the time evolution given by the 3-spin Hamiltonian

# hamiltonian parameters
J = -0.5
h = 0.3
# Trotter timestep
t = 0.001
# Initialising the circuit for the hamiltonian simulation
circuit = Circuit()
circuit += ops.CNOT(0,1)
circuit += ops.RotateZ(1, J*t)
circuit += ops.CNOT(0,1)
circuit += ops.CNOT(1,2)
circuit += ops.RotateZ(2, J*t)
circuit += ops.CNOT(1,2)
circuit += ops.CNOT(0,2)
circuit += ops.RotateZ(2, J*t)
circuit += ops.CNOT(0,2)
circuit += ops.RotateX(0, h*t)
circuit += ops.RotateX(1, h*t)
circuit += ops.RotateX(2, h*t)

ising circuit Figure 1: Circuit representing the time evolution of 3-spin Ising model Hamiltonian We define the device specifications. We assume the device is connected linearly and that the available gates are RotateZ, RotateX, RotateY and CNOT.

# Device layout
#  0 --- 1 --- 2
# Initialising a device
number_qubits = 3
device = devices.GenericDevice(number_qubits)

# add single-qubit gates
for q in range(number_qubits):
    device.set_single_qubit_gate_time("RotateZ", q, 1.0)
    device.set_single_qubit_gate_time("RotateX", q, 1.0)

# add two-qubit gates
for q in range(number_qubits-1):
        device.set_two_qubit_gate_time("CNOT", q, q+1, 1.0)
        device.set_two_qubit_gate_time("CNOT", q+1, q, 1.0)

We also specify the noise model using struqture.spins.PlusMinusLindbladNoiseOperator and qoqo.noise_models. We assume there is decoherence on the CNOT gates and general continuous decoherence on all the qubits in the circuit, represented by a DecoherenceOnGateModel and a ContinuousDecoherenceModel ,respectively.

# Initialising noise models
noise_product_1 = PlusMinusProduct().z(1).plus(1)
noise_1 = PlusMinusLindbladNoiseOperator()
noise_1.add_operator_product((noise_product_1, noise_product_1), 0.05)

noise_product_2 = PlusMinusProduct().z(2).plus(2)
noise_2 = PlusMinusLindbladNoiseOperator()
noise_2.add_operator_product((noise_product_2, noise_product_2), 0.025)

on_gate_noise = nm.DecoherenceOnGateModel()
on_gate_noise = on_gate_noise.set_two_qubit_gate_error(
    "CNOT", 0, 1, noise_1
)
on_gate_noise = on_gate_noise.set_two_qubit_gate_error(
    "CNOT", 1, 2, noise_2
)

decoherence_rate = 0.005
continuous_noise = nm.ContinuousDecoherenceModel().add_damping_rate([0, 1, 2], decoherence_rate)
noise_models = [on_gate_noise, continuous_noise]

To run the circuit on a quantum device, we need to apply a series of converters. First, we initialize the SingleOperationSwapRouter, which handles routing to overcome the device's limited connectivity. Next, the TwoQubitGateDecomposer decomposes two-qubit gates into the device's native gates—in this case, CNOT gates—along with generic single-qubit rotations. The NumericSingleQubitMultiplier optimizes the circuit by combining single-qubit gates where possible, and the SingleQubitGateDecomposer further breaks down these gates into the device's allowable operations. The IdentityRemover then cleans up the circuit by removing any redundant single-qubit gates that simplify to identity operations after these conversions. Finally, the circuit is ready for noise addition, which is handled by the NoiseInserter, adding noise Pragmas to the circuit. We store the converters sequentially in a list.

# Initialising the converters
converters = [
    SingleOperationSwapRouter(True, False),
    TwoQubitGateDecomposer(),
    NumericSingleQubitMultiplier(1e-9),
    SingleQubitGateDecomposer(),
    IdentityRemover(1e-9),
    NoiseInserter("active_qubits_only"),
]

The converters are then effortlessy applied by iterating over them in a for loop,

for converter in converters:
    circuit = converter.convert(circuit, device, noise_models)