Skip to content

Commit

Permalink
Linear function usability improvements (#10053)
Browse files Browse the repository at this point in the history
* contructing linear functions from circuits with barriers and delays; adding __eq__ method

* adding linear function extend_with_identity method

* constructing linear functions from nested circuits

* reorganizing code; constructing linear functions from cliffords (when possible)

* tests for linear functions from cliffords

* linear function from permutation gate

* simplifying condition for checking whether clifford is a linear function

* linear function from another

* pass over release notes

* black

* pylint fixes

* update docstring

* more docstring fixes

* adding pretty-printing functions for linear functions

* adding pretty-printing functions to the release notes

* adding pseudo-random tests

* moving import to the top of the file

* Fix to __eq__ as suggested in review

---------

Co-authored-by: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com>
  • Loading branch information
alexanderivrii and ShellyGarion authored Jul 17, 2023
1 parent ae40b4c commit 93feba7
Show file tree
Hide file tree
Showing 3 changed files with 456 additions and 54 deletions.
194 changes: 152 additions & 42 deletions qiskit/circuit/library/generalized_gates/linear_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@

from __future__ import annotations
import numpy as np
from qiskit.circuit import QuantumCircuit, Gate
from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate
from qiskit.circuit.exceptions import CircuitError
from qiskit.synthesis.linear import check_invertible_binary_matrix
from qiskit.circuit.library.generalized_gates.permutation import PermutationGate


class LinearFunction(Gate):
Expand Down Expand Up @@ -61,42 +62,36 @@ class LinearFunction(Gate):
`Online at umich.edu. <https://web.eecs.umich.edu/~imarkov/pubs/jour/qic08-cnot.pdf>`_
"""

def __init__(
self,
linear: list[list[int]] | np.ndarray | QuantumCircuit,
validate_input: bool | None = False,
) -> None:
def __init__(self, linear, validate_input=False):
"""Create a new linear function.
Args:
linear (list[list] or ndarray[bool] or QuantumCircuit):
either an n x n matrix, describing the linear function,
or a quantum circuit composed of linear gates only
(currently supported gates are CX and SWAP).
linear (list[list] or ndarray[bool] or QuantumCircuit or LinearFunction
or PermutationGate or Clifford): data from which a linear function
can be constructed. It can be either a nxn matrix (describing the
linear transformation), a permutation (which is a special case of
a linear function), another linear function, a clifford (when it
corresponds to a linear function), or a quantum circuit composed of
linear gates (CX and SWAP) and other objects described above, including
nested subcircuits.
validate_input: if True, performs more expensive input validation checks,
such as checking that a given n x n matrix is invertible.
Raises:
CircuitError: if the input is invalid:
either a matrix is non {square, invertible},
or a quantum circuit contains non-linear gates.
either the input matrix is not square or not invertible,
or the input quantum circuit contains non-linear objects
(for example, a Hadamard gate, or a Clifford that does
not correspond to a linear function).
"""
if not isinstance(linear, (list, np.ndarray, QuantumCircuit)):
raise CircuitError(
"A linear function must be represented either by a list, "
"a numpy array, or a quantum circuit with linear gates."
)

if isinstance(linear, QuantumCircuit):
# The following function will raise a CircuitError if there are nonlinear gates.
original_circuit = linear
linear = _linear_quantum_circuit_to_mat(linear)
# pylint: disable=cyclic-import
from qiskit.quantum_info import Clifford

else:
original_circuit = None
original_circuit = None

if isinstance(linear, (list, np.ndarray)):
# Normalize to numpy array (coercing entries to 0s and 1s)
try:
linear = np.array(linear, dtype=bool, copy=True)
Expand All @@ -116,10 +111,100 @@ def __init__(
"A linear function must be represented by an invertible matrix."
)

elif isinstance(linear, QuantumCircuit):
# The following function will raise a CircuitError if there are nonlinear gates.
original_circuit = linear
linear = LinearFunction._circuit_to_mat(linear)

elif isinstance(linear, LinearFunction):
linear = linear.linear.copy()

elif isinstance(linear, PermutationGate):
linear = LinearFunction._permutation_to_mat(linear)

elif isinstance(linear, Clifford):
# The following function will raise a CircuitError if clifford does not correspond
# to a linear function.
linear = LinearFunction._clifford_to_mat(linear)

# Note: if we wanted, we could also try to construct a linear function from a
# general operator, by first attempting to convert it to clifford, and then to
# a linear function.

else:
raise CircuitError("A linear function cannot be successfully constructed.")

super().__init__(
name="linear_function", num_qubits=len(linear), params=[linear, original_circuit]
)

@staticmethod
def _circuit_to_mat(qc: QuantumCircuit):
"""This creates a nxn matrix corresponding to the given quantum circuit."""
nq = qc.num_qubits
mat = np.eye(nq, nq, dtype=bool)

for instruction in qc.data:
if instruction.operation.name in ("barrier", "delay"):
# can be ignored
continue
if instruction.operation.name == "cx":
# implemented directly
cb = qc.find_bit(instruction.qubits[0]).index
tb = qc.find_bit(instruction.qubits[1]).index
mat[tb, :] = (mat[tb, :]) ^ (mat[cb, :])
continue
if instruction.operation.name == "swap":
# implemented directly
cb = qc.find_bit(instruction.qubits[0]).index
tb = qc.find_bit(instruction.qubits[1]).index
mat[[cb, tb]] = mat[[tb, cb]]
continue

# In all other cases, we construct the linear function for the operation.
# and compose (multiply) linear matrices.

if getattr(instruction.operation, "definition", None) is not None:
other = LinearFunction(instruction.operation.definition)
else:
other = LinearFunction(instruction.operation)

positions = [qc.find_bit(q).index for q in instruction.qubits]
other = other.extend_with_identity(len(mat), positions)
mat = np.dot(other.linear.astype(int), mat.astype(int)) % 2
mat = mat.astype(bool)

return mat

@staticmethod
def _clifford_to_mat(cliff):
"""This creates a nxn matrix corresponding to the given Clifford, when Clifford
can be converted to a linear function. This is possible when the clifford has
tableau of the form [[A, B], [C, D]], with B = C = 0 and D = A^{-1}^t, and zero
phase vector. In this case, the required matrix is A^t.
Raises an error otherwise.
"""
# Note: since cliff is a valid Clifford, then the condition D = A^{-1}^t
# holds automatically once B = C = 0.
if cliff.phase.any() or cliff.destab_z.any() or cliff.stab_x.any():
raise CircuitError("The given clifford does not correspond to a linear function.")
return np.transpose(cliff.destab_x)

@staticmethod
def _permutation_to_mat(perm):
"""This creates a nxn matrix from a given permutation gate."""
nq = len(perm.pattern)
mat = np.zeros((nq, nq), dtype=bool)
for i, j in enumerate(perm.pattern):
mat[i, j] = True
return mat

def __eq__(self, other):
"""Check if two linear functions represent the same matrix."""
if not isinstance(other, LinearFunction):
return False
return (self.linear == other.linear).all()

def validate_parameter(self, parameter):
"""Parameter validation"""
return parameter
Expand All @@ -140,7 +225,7 @@ def synthesize(self):

@property
def linear(self):
"""Returns the n x n matrix representing this linear function"""
"""Returns the n x n matrix representing this linear function."""
return self.params[0]

@property
Expand Down Expand Up @@ -171,22 +256,47 @@ def permutation_pattern(self):
locs = np.where(linear == 1)
return locs[1]

def extend_with_identity(self, num_qubits: int, positions: list[int]) -> LinearFunction:
"""Extend linear function to a linear function over nq qubits,
with identities on other subsystems.
def _linear_quantum_circuit_to_mat(qc: QuantumCircuit):
"""This creates a n x n matrix corresponding to the given linear quantum circuit."""
nq = qc.num_qubits
mat = np.eye(nq, nq, dtype=bool)

for instruction in qc.data:
if instruction.operation.name == "cx":
cb = qc.find_bit(instruction.qubits[0]).index
tb = qc.find_bit(instruction.qubits[1]).index
mat[tb, :] = (mat[tb, :]) ^ (mat[cb, :])
elif instruction.operation.name == "swap":
cb = qc.find_bit(instruction.qubits[0]).index
tb = qc.find_bit(instruction.qubits[1]).index
mat[[cb, tb]] = mat[[tb, cb]]
else:
raise CircuitError("A linear quantum circuit can include only CX and SWAP gates.")
Args:
num_qubits: number of qubits of the extended function.
return mat
positions: describes the positions of original qubits in the extended
function's qubits.
Returns:
LinearFunction: extended linear function.
"""
extended_mat = np.eye(num_qubits, dtype=bool)

for i, pos in enumerate(positions):
extended_mat[positions, pos] = self.linear[:, i]

return LinearFunction(extended_mat)

def mat_str(self):
"""Return string representation of the linear function
viewed as a matrix with 0/1 entries.
"""
return str(self.linear.astype(int))

def function_str(self):
"""Return string representation of the linear function
viewed as a linear transformation.
"""
out = "("
mat = self.linear
for row in range(self.num_qubits):
first_entry = True
for col in range(self.num_qubits):
if mat[row, col]:
if not first_entry:
out += " + "
out += "x_" + str(col)
first_entry = False
if row != self.num_qubits - 1:
out += ", "
out += ")\n"
return out
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
features:
- |
Allowing to construct a :class:`.LinearFunction` object from more general quantum circuits,
that may contain:
* Barriers (of type :class:`~qiskit.circuit.Barrier`) and delays (:class:`~qiskit.circuit.Delay`),
which are simply ignored
* Permutations (of type :class:`~qiskit.circuit.library.PermutationGate`)
* Other linear functions
* Cliffords (of type :class:`.Clifford`), when the Clifford represents a linear function
(and a ``CircuitError`` exception is raised if not)
* Nested quantum circuits of this form
- |
Added :meth:`.LinearFunction.__eq__` method. Two objects of type :class:`.LinearFunction`
are considered equal when their representations as binary invertible matrices are equal.
- |
Added :meth:`.LinearFunction.extend_with_identity` method, which allows to extend
a linear function over ``k`` qubits to a linear function over ``n >= k`` qubits,
specifying the new positions of the original qubits and padding with identities on the
remaining qubits.
- |
Added two methods for pretty-printing :class:`.LinearFunction` objects:
:meth:`.LinearFunction.mat_str`, which returns the string representation of the linear
function viewed as a matrix with 0/1 entries, and
:meth:`.LinearFunction.function_str`, which returns the string representation of the
linear function viewed as a linear transformation.
Loading

0 comments on commit 93feba7

Please sign in to comment.