Skip to content

Commit

Permalink
Merge pull request #985 from qiboteam/new_gates
Browse files Browse the repository at this point in the history
Change `.matrix` from property to method and remove `asmatrix` method
  • Loading branch information
scarrazza authored Aug 11, 2023
2 parents fff4c99 + c5e3271 commit ef3eb04
Show file tree
Hide file tree
Showing 23 changed files with 607 additions and 360 deletions.
749 changes: 487 additions & 262 deletions poetry.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions src/qibo/backends/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,17 @@ def plus_density_matrix(self, nqubits): # pragma: no cover
raise_error(NotImplementedError)

@abc.abstractmethod
def asmatrix(self, gate): # pragma: no cover
def matrix(self, gate): # pragma: no cover
"""Convert a :class:`qibo.gates.Gate` to the corresponding matrix."""
raise_error(NotImplementedError)

@abc.abstractmethod
def asmatrix_parametrized(self, gate): # pragma: no cover
"""Equivalent to :meth:`qibo.backends.abstract.Backend.asmatrix` for parametrized gates."""
def matrix_parametrized(self, gate): # pragma: no cover
"""Equivalent to :meth:`qibo.backends.abstract.Backend.matrix` for parametrized gates."""
raise_error(NotImplementedError)

@abc.abstractmethod
def asmatrix_fused(self, gate): # pragma: no cover
def matrix_fused(self, gate): # pragma: no cover
"""Fuse matrices of multiple gates."""
raise_error(NotImplementedError)

Expand Down
20 changes: 10 additions & 10 deletions src/qibo/backends/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,25 +101,25 @@ def plus_density_matrix(self, nqubits):
state /= 2**nqubits
return state

def asmatrix(self, gate):
def matrix(self, gate):
"""Convert a gate to its matrix representation in the computational basis."""
name = gate.__class__.__name__
matrix = getattr(self.matrices, name)
return matrix(2 ** len(gate.target_qubits)) if callable(matrix) else matrix
_matrix = getattr(self.matrices, name)
return _matrix(2 ** len(gate.target_qubits)) if callable(_matrix) else _matrix

def asmatrix_parametrized(self, gate):
def matrix_parametrized(self, gate):
"""Convert a parametrized gate to its matrix representation in the computational basis."""
name = gate.__class__.__name__
return getattr(self.matrices, name)(*gate.parameters)

def asmatrix_fused(self, fgate):
def matrix_fused(self, fgate):
rank = len(fgate.target_qubits)
matrix = np.eye(2**rank, dtype=self.dtype)
for gate in fgate.gates:
# transfer gate matrix to numpy as it is more efficient for
# small tensor calculations
# explicit to_numpy see https://github.com/qiboteam/qibo/issues/928
gmatrix = self.to_numpy(gate.asmatrix(self))
gmatrix = self.to_numpy(gate.matrix(self))
# Kronecker product with identity is needed to make the
# original matrix have shape (2**rank x 2**rank)
eye = np.eye(2 ** (rank - len(gate.qubits)), dtype=self.dtype)
Expand Down Expand Up @@ -147,7 +147,7 @@ def control_matrix(self, gate):
"unitary for more than two "
"control qubits.",
)
matrix = gate.asmatrix(self)
matrix = gate.matrix(self)
shape = matrix.shape
if shape != (2, 2):
raise_error(
Expand All @@ -163,7 +163,7 @@ def control_matrix(self, gate):
def apply_gate(self, gate, state, nqubits):
state = self.cast(state)
state = self.np.reshape(state, nqubits * (2,))
matrix = gate.asmatrix(self)
matrix = gate.matrix(self)
if gate.is_controlled_by:
matrix = self.np.reshape(matrix, 2 * len(gate.target_qubits) * (2,))
ncontrol = len(gate.control_qubits)
Expand All @@ -190,7 +190,7 @@ def apply_gate(self, gate, state, nqubits):
def apply_gate_density_matrix(self, gate, state, nqubits):
state = self.cast(state)
state = self.np.reshape(state, 2 * nqubits * (2,))
matrix = gate.asmatrix(self)
matrix = gate.matrix(self)
if gate.is_controlled_by:
matrix = self.np.reshape(matrix, 2 * len(gate.target_qubits) * (2,))
matrixc = self.np.conj(matrix)
Expand Down Expand Up @@ -239,7 +239,7 @@ def apply_gate_density_matrix(self, gate, state, nqubits):
def apply_gate_half_density_matrix(self, gate, state, nqubits):
state = self.cast(state)
state = np.reshape(state, 2 * nqubits * (2,))
matrix = gate.asmatrix(self)
matrix = gate.matrix(self)
if gate.is_controlled_by: # pragma: no cover
raise_error(
NotImplementedError,
Expand Down
12 changes: 6 additions & 6 deletions src/qibo/backends/tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,16 @@ def zero_density_matrix(self, nqubits):
state = self.tf.tensor_scatter_nd_update(state, idx, update)
return state

def asmatrix(self, gate):
npmatrix = super().asmatrix(gate)
def matrix(self, gate):
npmatrix = super().matrix(gate)
return self.tf.cast(npmatrix, dtype=self.dtype)

def asmatrix_parametrized(self, gate):
npmatrix = super().asmatrix_parametrized(gate)
def matrix_parametrized(self, gate):
npmatrix = super().matrix_parametrized(gate)
return self.tf.cast(npmatrix, dtype=self.dtype)

def asmatrix_fused(self, gate):
npmatrix = super().asmatrix_fused(gate)
def matrix_fused(self, gate):
npmatrix = super().matrix_fused(gate)
return self.tf.cast(npmatrix, dtype=self.dtype)

def execute_circuit(
Expand Down
39 changes: 27 additions & 12 deletions src/qibo/gates/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import sympy

from qibo.backends import GlobalBackend
from qibo.config import raise_error


Expand Down Expand Up @@ -269,8 +270,26 @@ def decompose(self, *free) -> List["Gate"]:
# of the same gate.
return [self.__class__(*self.init_args, **self.init_kwargs)]

def asmatrix(self, backend):
return backend.asmatrix(self)
def matrix(self, backend=None):
"""Returns the matrix representation of the gate.
Args:
backend (:class:`qibo.backends.abstract.Backend`, optional): backend
to be used in the execution. If ``None``, it uses
:class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
Returns:
ndarray: Matrix representation of gate.
.. note::
``Gate.matrix`` was an defined as an atribute in ``qibo`` versions prior to ``0.2.0``.
From ``0.2.0`` on, it has been converted into a method and has replaced the ``asmatrix`` method.
"""
if backend is None: # pragma: no cover
backend = GlobalBackend()

return backend.matrix(self)

def generator_eigenvalue(self):
"""
Expand All @@ -292,13 +311,6 @@ def basis_rotation(self):
f"Basis rotation is not implemented for {self.__class__.__name__}",
)

@property
def matrix(self):
from qibo.backends import GlobalBackend

backend = GlobalBackend()
return self.asmatrix(backend)

def apply(self, backend, state, nqubits):
return backend.apply_gate(self, state, nqubits)

Expand All @@ -315,7 +327,7 @@ def commutes(self, gate):
def on_qubits(self, qubit_map):
raise_error(NotImplementedError, "Cannot use special gates on subroutines.")

def asmatrix(self, backend): # pragma: no cover
def matrix(self, backend=None): # pragma: no cover
raise_error(
NotImplementedError, "Special gates do not have matrix representation."
)
Expand Down Expand Up @@ -391,5 +403,8 @@ def substitute_symbols(self):
params[i] = float(param)
self.parameters = tuple(params)

def asmatrix(self, backend):
return backend.asmatrix_parametrized(self)
def matrix(self, backend=None):
if backend is None: # pragma: no cover
backend = GlobalBackend()

return backend.matrix_parametrized(self)
2 changes: 1 addition & 1 deletion src/qibo/gates/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def to_choi(self, nqubits: Optional[int] = None, order: str = "row", backend=Non
for coeff, gate in zip(self.coefficients, self.gates):
kraus_op = FusedGate(*range(nqubits))
kraus_op.append(gate)
kraus_op = kraus_op.asmatrix(backend)
kraus_op = kraus_op.matrix(backend)
kraus_op = vectorization(kraus_op, order=order, backend=backend)
super_op += coeff * np.outer(kraus_op, np.conj(kraus_op))
del kraus_op
Expand Down
2 changes: 1 addition & 1 deletion src/qibo/gates/measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def controlled_by(self, *q):
""""""
raise_error(NotImplementedError, "Measurement gates cannot be controlled.")

def asmatrix(self, backend):
def matrix(self, backend=None):
""""""
raise_error(
NotImplementedError, "Measurement gates do not have matrix representation."
Expand Down
19 changes: 16 additions & 3 deletions src/qibo/gates/special.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from qibo.backends import GlobalBackend
from qibo.gates.abstract import SpecialGate
from qibo.gates.measurements import M

Expand Down Expand Up @@ -31,7 +32,6 @@ def apply_density_matrix(self, backend, state, nqubits):

class FusedGate(SpecialGate):
"""Collection of gates that will be fused and applied as single gate during simulation.
This gate is constructed automatically by :meth:`qibo.models.circuit.Circuit.fuse`
and should not be used by user.
"""
Expand Down Expand Up @@ -95,8 +95,21 @@ def can_fuse(self, gate, max_qubits):
return False
return True

def asmatrix(self, backend):
return backend.asmatrix_fused(self)
def matrix(self, backend=None):
"""Returns matrix representation of special gate.
Args:
backend (:class:`qibo.backends.abstract.Backend`, optional): backend
to be used in the execution. If ``None``, it uses
:class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
Returns:
ndarray: Matrix representation of special gate.
"""
if backend is None: # pragma: no cover
backend = GlobalBackend()

return backend.matrix_fused(self)

def fuse(self, gate):
"""Fuses two gates."""
Expand Down
2 changes: 1 addition & 1 deletion src/qibo/models/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,7 @@ def unitary(self, backend=None):
for gate in self.queue:
if not isinstance(gate, (gates.SpecialGate, gates.M)):
fgate.append(gate)
return fgate.asmatrix(backend)
return fgate.matrix(backend)

@property
def final_state(self):
Expand Down
27 changes: 11 additions & 16 deletions src/qibo/models/error_mitigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from scipy.optimize import curve_fit

from qibo import gates
from qibo.backends import GlobalBackend
from qibo.config import raise_error


Expand Down Expand Up @@ -132,9 +133,7 @@ def ZNE(
numpy.ndarray: Estimate of the expected value of ``observable`` in the noise free condition.
"""

if backend == None: # pragma: no cover
from qibo.backends import GlobalBackend

if backend is None: # pragma: no cover
backend = GlobalBackend()

expected_val = []
Expand Down Expand Up @@ -168,6 +167,7 @@ def sample_training_circuit(
circuit,
replacement_gates: list = None,
sigma: float = 0.5,
backend=None,
):
"""Samples a training circuit for CDR by susbtituting some of the non-Clifford gates.
Expand All @@ -179,10 +179,14 @@ def sample_training_circuit(
form (``gates.XYZ``, ``kwargs``). For example, phase gates are used by default:
``list((RZ, {'theta':0}), (RZ, {'theta':pi/2}), (RZ, {'theta':pi}), (RZ, {'theta':3*pi/2}))``.
sigma (float, optional): standard devation of the Gaussian distribution used for sampling.
backend (:class:`qibo.backends.abstract.Backend`, optional): Calculation engine.
Returns:
:class:`qibo.models.Circuit`: The sampled circuit.
"""
if backend is None: # pragma: no cover
backend = GlobalBackend()

if replacement_gates is None:
replacement_gates = [(gates.RZ, {"theta": n * np.pi / 2}) for n in range(4)]

Expand All @@ -207,7 +211,8 @@ def sample_training_circuit(
replacement.append(rep_gates)
distance.append(
np.linalg.norm(
gate.matrix - [rep_gate.matrix for rep_gate in rep_gates],
gate.matrix(backend)
- [rep_gate.matrix(backend) for rep_gate in rep_gates],
ord="fro",
axis=(1, 2),
)
Expand Down Expand Up @@ -283,9 +288,7 @@ def CDR(
"""

# Set backend
if backend == None: # pragma: no cover
from qibo.backends import GlobalBackend

if backend is None: # pragma: no cover
backend = GlobalBackend()
# Sample the training set
training_circuits = [
Expand Down Expand Up @@ -386,9 +389,7 @@ def vnCDR(
"""

# Set backend
if backend == None: # pragma: no cover
from qibo.backends import GlobalBackend

if backend is None: # pragma: no cover
backend = GlobalBackend()

# Sample the training circuits
Expand Down Expand Up @@ -479,10 +480,6 @@ def calibration_matrix(nqubits, noise_model=None, nshots: int = 1000, backend=No
from qibo import Circuit # pylint: disable=import-outside-toplevel

if backend is None: # pragma: no cover
from qibo.backends import ( # pylint: disable=import-outside-toplevel
GlobalBackend,
)

backend = GlobalBackend()

matrix = np.zeros((2**nqubits, 2**nqubits))
Expand Down Expand Up @@ -559,8 +556,6 @@ def apply_randomized_readout_mitigation(
)

if backend is None: # pragma: no cover
from qibo.backends import GlobalBackend

backend = GlobalBackend()

qubits = circuit.queue[-1].qubits
Expand Down
25 changes: 10 additions & 15 deletions src/qibo/quantum_info/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def entanglement_of_formation(


def entropy(state, base: float = 2, check_hermitian: bool = False, backend=None):
"""The von-Neumann entropy :math:`S(\\rho)` of a quantum state :math:`\\rho`, which
"""The von-Neumann entropy :math:`S(\\rho)` of a quantum ``state`` :math:`\\rho`, which
is given by
.. math::
Expand Down Expand Up @@ -251,7 +251,7 @@ def entanglement_entropy(
backend=None,
):
"""Calculates the entanglement entropy :math:`S` of bipartition :math:`A`
of``state`` :math:`\\rho`. This is given by
of ``state`` :math:`\\rho`. This is given by
.. math::
S(\\rho_{A}) = -\\text{tr}(\\rho_{A} \\, \\log(\\rho_{A})) \\, ,
Expand Down Expand Up @@ -615,20 +615,14 @@ def bures_distance(state, target, check_hermitian: bool = False, backend=None):
def entanglement_fidelity(
channel, nqubits: int, state=None, check_hermitian: bool = False, backend=None
):
"""Entanglement fidelity of a ``channel`` :math:`\\mathcal{E}` on ``state``
:math:`\\rho`, which is given by
"""Entanglement fidelity :math:`F_{\\mathcal{E}}` of a ``channel`` :math:`\\mathcal{E}`
on ``state`` :math:`\\rho` is given by
.. math::
\\begin{align}
F_{\\mathcal{E}} &= \\text{fidelity}(\\rho_{f}, \\rho) \\nonumber \\\\
&= \\text{tr}(\\rho_{f} \\, \\rho) \\nonumber
\\end{align}
where
.. math::
\\rho_{f} = \\mathcal{E}_{A} \\otimes I_{B}(\\rho)
F_{\\mathcal{E}}(\\rho) = F(\\rho_{f}, \\rho)
where :math:`F` is the :func:`qibo.quantum_info.fidelity` function for states,
and :math:`\\rho_{f} = \\mathcal{E}_{A} \\otimes I_{B}(\\rho)`
is the state after the channel :math:`\\mathcal{E}` was applied to
partition :math:`A`.
Expand Down Expand Up @@ -863,10 +857,11 @@ def diamond_norm(channel, target=None, **kwargs):
``channel`` :math:`\\mathcal{E}`, which is given by
.. math::
\\|\\mathcal{E}\\|_{\\diamond} = \\max_{\\rho} \\, \\| \\left(\\mathcal{E} \\bigotimes I_{d^{2}}\\right)(\\rho) \\| \\, ,
\\|\\mathcal{E}\\|_{\\diamond} = \\max_{\\rho} \\, \\| \\left(\\mathcal{E} \\otimes I_{d^{2}}\\right)(\\rho) \\|_{1} \\, ,
where :math:`I_{d^{2}}` is the :math:`d^{2} \\times d^{2}` Identity operator,
:math:`d = 2^{n}`, and :math:`n` is the number of qubits.
:math:`d = 2^{n}`, :math:`n` is the number of qubits,
and :math:`\\|\\cdot\\|_{1}` denotes the trace norm.
If a ``target`` channel :math:`\\Lambda` is specified,
then it calculates :math:`\\| \\mathcal{E} - \\Lambda\\|_{\\diamond}`.
Expand Down
Loading

0 comments on commit ef3eb04

Please sign in to comment.