Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linear function usability improvements #10053

Merged
merged 20 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bb9ad01
contructing linear functions from circuits with barriers and delays; …
alexanderivrii Apr 30, 2023
3387d69
adding linear function extend_with_identity method
alexanderivrii Apr 30, 2023
551d763
constructing linear functions from nested circuits
alexanderivrii May 1, 2023
5f55883
reorganizing code; constructing linear functions from cliffords (when…
alexanderivrii May 1, 2023
8f3c904
tests for linear functions from cliffords
alexanderivrii May 1, 2023
52dfa8e
linear function from permutation gate
alexanderivrii May 1, 2023
688b1bc
simplifying condition for checking whether clifford is a linear function
alexanderivrii May 1, 2023
d18719d
linear function from another
alexanderivrii May 1, 2023
be79093
pass over release notes
alexanderivrii May 1, 2023
0a2a7ff
black
alexanderivrii May 1, 2023
23478d4
pylint fixes
alexanderivrii May 2, 2023
a405627
update docstring
alexanderivrii May 2, 2023
752cd9b
more docstring fixes
alexanderivrii May 2, 2023
6288cd8
adding pretty-printing functions for linear functions
alexanderivrii May 16, 2023
60b61de
Merge branch 'main' into linear-function-constructor
alexanderivrii Jun 27, 2023
836944c
Merge branch 'main' into linear-function-constructor
ShellyGarion Jul 10, 2023
68c2aa9
adding pretty-printing functions to the release notes
alexanderivrii Jul 11, 2023
6190778
adding pseudo-random tests
alexanderivrii Jul 11, 2023
5a1cf9f
moving import to the top of the file
alexanderivrii Jul 15, 2023
ad347ed
Fix to __eq__ as suggested in review
alexanderivrii Jul 15, 2023
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
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()
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

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
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
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