Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion doc/source/api-reference/qibo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1215,7 +1215,7 @@ In Qibo, it is possible to build noisy circuits based on IBMQ's reported noise m
for its quantum computer by using the :class:`qibo.noise.IBMQNoiseModel` class.
The noise model is built using a combination of the
:class:`qibo.gates.ThermalRelaxationChannel` and :class:`qibo.gates.DepolarizingChannel`
channels. . At the end of the circuit, if the qubit is measured,
channels. At the end of the circuit, if the qubit is measured,
bitflips errors are set. Moreover, the model handles idle qubits by applying a thermal
relaxation channel for the duration of the idle-time.

Expand All @@ -1228,6 +1228,72 @@ example on :ref:`Simulating quantum hardware <noise-hardware-example>`.
:member-order: bysource


GTH Noise Model
^^^^^^^^^^^^^^^

The GST-trained heuristic (GTH) noise model is a composite noise model designed to approximate hardware noise using a combination of theoretical quantum noise channels.

The model was introduced in `arXiv:2502.19872 <https://arxiv.org/abs/2502.19872>`_, where gate set tomography (GST) training data is used to train neural networks that subsequently infer effective noise parameters from GST data obtained from hardware devices.

For single-qubit operations, the GTH noise model applies a combination of:

* :class:`qibo.gates.DepolarizingChannel`
* :class:`qibo.gates.AmplitudeDampingChannel`
* :class:`qibo.gates.PhaseDampingChannel`, and
* :class:`qibo.gates.ReadoutErrorChannel`

For two-qubit operations, the model applies pair-specific two-qubit :class:`qibo.gates.DepolarizingChannel`.

Although more sophisticated composite noise models may be constructed by including additional noise channels, it was found that this combination provides a sufficiently realistic approximation of hardware noise while remaining computationally practical.

The model accepts:

* a 4-tuple for each qubit in the format

``[depolarizing, amplitude damping, dephasing, readout]``

* and pair-specific two-qubit depolarizing parameters.

Example
"""""""

Below is an example of how to use input the noise parameters inferred from the GST on the IQM Garnet's qubit 1 and 4 (dated: ``2025-09-01``).

.. code-block:: python

from qibo.noise import GTHNoiseModel

single_qb_errors = {
1: [0.00799362, 0.00260128, 0.00304553, 0.00631564],
4: [0.00541111, 0.00270103, 0.00747088, 0.00748984],
}

two_qb_errors = {(1, 4): 0.02199975}

noise_model = GTHNoiseModel.from_parameters(single_qb_errors, two_qb_errors)

These parameters correspond to:

* qubit 1:

``[0.00799362, 0.00260128, 0.00304553, 0.00631564]``

* qubit 4:

``[0.00541111, 0.00270103, 0.00747088, 0.00748984]``

* coupled qubit pair ``(1, 4)``:

``0.02199975``

Alternatively, arbitrary (fictitious) parameters may also be supplied to emulate generic or hypothetical hardware devices for simulation and testing purposes.

More information can be found at `arXiv:2502.19872 <https://arxiv.org/abs/2502.19872>`_.

.. autoclass:: qibo.noise.GTHNoiseModel
:members:
:member-order: bysource

_______________________

.. _Hamiltonians:
Expand Down
265 changes: 265 additions & 0 deletions src/qibo/noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,3 +567,268 @@ def from_dict(self, parameters: dict):
gate=gates.M,
qubits=int(qubit),
)


class GTHNoiseModel(NoiseModel):
"""Class for implementing the GST-trained heuristic (GTH) noise model to mimic hardware noise.

This noise model applies a combination of :class:`qibo.gates.DepolarizingChannel`,
:class:`qibo.gates.AmplitudeDampingChannel`, :class:`qibo.gates.PhaseDampingChannel`,
and :class:`qibo.gates.ReadoutErrorChannel` after each single-qubit gate.

It also applies a two-qubit :class:`qibo.gates.DepolarizingChannel` after each
two-qubit gate.

The single-qubit :class:`qibo.gates.ReadoutErrorChannel` is applied before every
measurement gate.

The noise parameters are expected to be obtained using the GST-trained
heuristic (GTH) workflow, where gate set tomography (GST) data is used
to infer effective noise parameters for each qubit and qubit pair.
This procedure is described in detail in https://arxiv.org/abs/2502.19872.

Alternatively, arbitrary (fictitious) parameters may be supplied to
emulate generic or hypothetical hardware noise for simulation and testing
purposes.

Example for 2-qubit noise (based on IQM Garnet device's qubits 1 and 4, dated 2025-09-01):

.. testcode::

from qibo import Circuit, gates
from qibo.noise import GTHNoiseModel
from qibo.backends import NumpyBackend
import numpy as np

# Define a dictionary containing 4-tuples representing:
# [depolarizing_error, amplitude_damping_error, dephasing_error, readout_error]:
single_qb_errors = {
1: [0.00799362, 0.00260128, 0.00304553, 0.00631564],
4: [0.00541111, 0.00270103, 0.00747088, 0.00748984],
}

# Define a dictionary containing two-qubit depolarizing error.
two_qb_errors = {(1, 4): 0.02199975}

# Construct composite noise model
noise_model = GTHNoiseModel.from_parameters(single_qb_errors, two_qb_errors)

# Apply to circuit
c = Circuit(5, density_matrix=True)
c.add(gates.PRX(1, np.pi/3, np.pi/4))
c.add(gates.PRX(4, np.pi/3, np.pi/2))
c.add(gates.CZ(1, 4))
c.add(gates.M(1))
c.add(gates.M(4))

c_noisy = noise_model.apply(c)

Example for 4-qubit noise (ficitious parameters):

.. testcode::

from qibo import Circuit, gates
from qibo.noise import GTHNoiseModel
from qibo.backends import NumpyBackend
import numpy as np

# Define a dictionary containing 4-tuples representing:
# [depolarizing_error, amplitude_damping_error, dephasing_error, readout_error]:
single_qb_errors = {
1: [0.3, 0.4, 0.2, 0.1],
2: [0.3, 0.4, 0.2, 0.3],
4: [0.5, 0.3, 0.4, 0.6],
5: [0.5, 0.2, 0.4, 0.1],
}

# Define a dictionary containing two-qubit depolarizing error.
two_qb_errors = {
(1, 4): 0.25,
(4, 5): 0.1,
(2, 5): 0.21,
(1, 2): 0.14,
}

# Construct composite noise model
noise_model = GTHNoiseModel.from_parameters(single_qb_errors, two_qb_errors)

# Apply to circuit
c = Circuit(6, density_matrix=True)
c.add(gates.PRX(1, np.pi/3, np.pi/4))
c.add(gates.PRX(2, np.pi/4, np.pi/5))
c.add(gates.PRX(4, np.pi/3, np.pi/2))
c.add(gates.PRX(5, np.pi/2, np.pi/6))
c.add(gates.CZ(1, 4))
c.add(gates.CZ(1, 2))
c.add(gates.CZ(4, 5))
c.add(gates.CZ(2, 5))
c.add(gates.M(1))
c.add(gates.M(2))
c.add(gates.M(4))
c.add(gates.M(5))

c_noisy = noise_model.apply(c)
"""

@classmethod
def from_parameters(cls, single_params: dict, pair_params: dict):
"""Class method to construct the GTH noise model from structured parameters.

Args:
single_params (dict): Dictionary mapping qubit indices to 4-tuples or lists
``[p_dep, p_amp, p_phase, p_readout]`` representing, respectively,
the single-qubit depolarizing, amplitude damping, phase damping,
and readout error probabilities.
pair_params (dict): Dictionary mapping qubit index pairs ``(q1, q2)``
to two-qubit depolarizing error probabilities.

Returns:
GTHNoiseModel: Noise model instance initialised with the provided parameters.
"""
trial_parameters = {
"depolarizing_one_qubit": {},
"amplitude_damping": {},
"phase_damping": {},
"readout_one_qubit": {},
"depolarizing_two_qubit": {},
}

for q, p in single_params.items():
trial_parameters["depolarizing_one_qubit"][str(q)] = p[0]
trial_parameters["amplitude_damping"][str(q)] = p[1]
trial_parameters["phase_damping"][str(q)] = p[2]
trial_parameters["readout_one_qubit"][str(q)] = (p[3], p[3])

for (i, j), pij in pair_params.items():
trial_parameters["depolarizing_two_qubit"][f"{i}-{j}"] = pij

obj = cls()
obj.from_dict(trial_parameters)
return obj

def from_dict(self, parameters: dict):
"""Method used to initialise the GTH noise model from a dictionary.

Args:
parameters (dict): Contains parameters required to initialise
:class:`qibo.noise.DepolarizingError`,
:class:`qibo.noise.AmplitudeDampingError`,
:class:`qibo.noise.PhaseDampingError`, and
:class:`qibo.noise.ReadoutError`.

The keys and values of the dictionary are defined below:

- ``"depolarizing_one_qubit"`` (*int* or *float* or *dict*):
If ``int`` or ``float``, all qubits share the same depolarizing parameter.
If ``dict``, expects qubit indices as keys and depolarizing parameters as values.

- ``"amplitude_damping"`` (*int* or *float* or *dict*):
If ``int`` or ``float``, all qubits share the same amplitude damping parameter.
If ``dict``, expects qubit indices as keys and damping parameters as values.

- ``"phase_damping"`` (*int* or *float* or *dict*):
If ``int`` or ``float``, all qubits share the same phase damping parameter.
If ``dict``, expects qubit indices as keys and damping parameters as values.

- ``"readout_one_qubit"`` (*int* or *float* or *dict*):
If ``int`` or ``float``, assumes symmetric readout error
:math:`p(0|1) = p(1|0)` for all qubits.
If ``dict``, expects qubit indices as keys and values as
``float`` or ``tuple/list`` of the form ``(p(0|1), p(1|0))``.

- ``"depolarizing_two_qubit"`` (*int* or *float* or *dict*):
If ``int`` or ``float``, all two-qubit gates share the same depolarizing parameter.
If ``dict``, expects qubit pairs as string keys in the format ``"q1-q2"``
and corresponding depolarizing parameters as values.

Notes:
Noise channels are applied conditionally using :class:`_Conditions`
depending on whether gates are single- or two-qubit operations.
"""
phase_damping = parameters["phase_damping"]
amplitude_damping = parameters["amplitude_damping"]
readout_one_qubit = parameters["readout_one_qubit"]
depolarizing_one_qubit = parameters["depolarizing_one_qubit"]
depolarizing_two_qubit = parameters["depolarizing_two_qubit"]

if isinstance(phase_damping, (float, int)):
self.add(
PhaseDampingError(phase_damping),
conditions=_Conditions().condition_gate_single,
)

if isinstance(phase_damping, dict):
for qubit_key, lamb in phase_damping.items():
self.add(
PhaseDampingError(lamb),
qubits=int(qubit_key),
conditions=_Conditions().condition_gate_single,
)

if isinstance(amplitude_damping, (float, int)):
self.add(
AmplitudeDampingError(amplitude_damping),
conditions=_Conditions().condition_gate_single,
)

if isinstance(amplitude_damping, dict):
for qubit_key, lamb in amplitude_damping.items():
self.add(
AmplitudeDampingError(lamb),
qubits=int(qubit_key),
conditions=_Conditions().condition_gate_single,
)

if isinstance(readout_one_qubit, (int, float)):
probabilities = [
[1 - readout_one_qubit, readout_one_qubit],
[readout_one_qubit, 1 - readout_one_qubit],
]
self.add(ReadoutError(probabilities), gate=gates.M)

if isinstance(readout_one_qubit, dict):
for qubit, probs in readout_one_qubit.items():
if isinstance(probs, (int, float)):
probs = (probs, probs)
elif isinstance(probs, (tuple, list)) and len(probs) == 1:
probs *= 2

probabilities = [[1 - probs[0], probs[0]], [probs[1], 1 - probs[1]]]
self.add(
ReadoutError(probabilities),
gate=gates.M,
qubits=int(qubit),
)

if isinstance(depolarizing_one_qubit, (float, int)):
self.add(
DepolarizingError(depolarizing_one_qubit),
conditions=_Conditions().condition_gate_single,
)

if isinstance(depolarizing_one_qubit, dict):
for qubit_key, lamb in depolarizing_one_qubit.items():
self.add(
DepolarizingError(lamb),
qubits=int(qubit_key),
conditions=_Conditions().condition_gate_single,
)

if isinstance(depolarizing_two_qubit, (float, int)):
self.add(
DepolarizingError(depolarizing_two_qubit),
conditions=_Conditions().condition_gate_two,
)

if isinstance(depolarizing_two_qubit, dict):
for key, lamb in depolarizing_two_qubit.items():
qubits = key.replace(" ", "").split("-")
qubits = tuple(map(int, qubits))
self.add(
DepolarizingError(lamb),
qubits=qubits,
conditions=[
_Conditions().condition_gate_two,
_Conditions(qubits).condition_qubits,
],
)
Loading
Loading