"""This module implements the UnitaryBuilder class."""
from __future__ import annotations
import logging
from collections.abc import Sequence
from typing import cast
from typing import TYPE_CHECKING
import numpy as np
import numpy.typing as npt
from bqskit.qis.unitary.unitary import RealVector
from bqskit.qis.unitary.unitary import Unitary
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
from bqskit.utils.typing import is_integer
from bqskit.utils.typing import is_valid_radixes
if TYPE_CHECKING:
from bqskit.ir.location import CircuitLocationLike
logger = logging.getLogger(__name__)
[docs]
class UnitaryBuilder(Unitary):
"""
An object for fast unitary accumulation using tensor networks.
A UnitaryBuilder is similar to a StringBuilder in the sense that it is an
efficient way to string together or accumulate :class:`Unitary` objects.
This class uses concepts from tensor networks to efficiently multiply
unitary matrices.
"""
[docs]
def __init__(self, num_qudits: int, radixes: Sequence[int] = []) -> None:
"""
UnitaryBuilder constructor.
Args:
num_qudits (int): The number of qudits to build a Unitary for.
radixes (Sequence[int]): A sequence with its length equal
to `num_qudits`. Each element specifies the base of a
qudit. Defaults to qubits.
Raises:
ValueError: If `num_qudits` is nonpositive.
ValueError: If the length of `radixes` is not equal to
`num_qudits`.
Examples:
>>> builder = UnitaryBuilder(4) # Creates a 4-qubit builder.
"""
if not is_integer(num_qudits):
raise TypeError(
'Expected int for num_qudits, got %s.' %
type(num_qudits),
)
if num_qudits <= 0:
raise ValueError(
'Expected positive number for num_qudits, got %d.' %
num_qudits,
)
self._num_qudits = num_qudits
self._radixes = tuple(radixes if len(radixes) > 0 else [2] * num_qudits)
if not is_valid_radixes(self.radixes):
raise TypeError('Invalid qudit radixes.')
if len(self.radixes) != self.num_qudits:
raise ValueError(
'Expected length of radixes to be equal to num_qudits:'
' %d != %d' % (len(self.radixes), self.num_qudits),
)
self._num_params = 0
self._dim = int(np.prod(self.radixes))
self.tensor = np.identity(self.dim, dtype=np.complex128)
self.tensor = self.tensor.reshape(self.radixes * 2)
[docs]
def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
"""Build the unitary, see :func:`Unitary.get_unitary` for more."""
utry = self.tensor.reshape((self.dim, self.dim))
return UnitaryMatrix(utry, self.radixes, False)
[docs]
def apply_right(
self,
utry: UnitaryMatrix,
location: CircuitLocationLike,
inverse: bool = False,
check_arguments: bool = True,
) -> None:
"""
Apply the specified unitary on the right of this UnitaryBuilder.
..
.-----. .------.
0 -| |---| |-
1 -| |---| utry |-
. . '------'
. .
. .
n-1 -| |------------
'-----'
Args:
utry (UnitaryMatrix): The unitary to apply.
location (CircuitLocationLike): The qudits to apply the unitary on.
inverse (bool): If true, apply the inverse of the unitary.
check_arguments (bool): If true, check the inputs for type and
value errors.
Raises:
ValueError: If `utry`'s size does not match the given location.
ValueError: if `utry`'s radixes does not match the given location.
Notes:
- Applying the unitary on the right is equivalent to multiplying
the unitary on the left of the tensor. The notation comes
from the quantum circuit perspective.
- This operation is performed using tensor contraction.
"""
from bqskit.ir.location import CircuitLocation
if check_arguments:
if not isinstance(utry, UnitaryMatrix):
raise TypeError('Expected UnitaryMatrix, got %s', type(utry))
if not CircuitLocation.is_location(location, self.num_qudits):
raise TypeError('Invalid location.')
location = CircuitLocation(location)
if len(location) != utry.num_qudits:
raise ValueError('Unitary and location size mismatch.')
for utry_radix, bldr_radix_idx in zip(utry.radixes, location):
if utry_radix != self.radixes[bldr_radix_idx]:
raise ValueError('Unitary and location radix mismatch.')
left_perm = list(cast(CircuitLocation, location))
mid_perm = [x for x in range(self.num_qudits) if x not in left_perm]
right_perm = [x + self.num_qudits for x in range(self.num_qudits)]
left_dim = int(np.prod([self.radixes[x] for x in left_perm]))
utry = utry.dagger if inverse else utry
perm = left_perm + mid_perm + right_perm
self.tensor = self.tensor.transpose(perm)
self.tensor = self.tensor.reshape((left_dim, -1))
self.tensor = utry @ self.tensor
shape = list(self.radixes) * 2
shape = [shape[p] for p in perm]
self.tensor = self.tensor.reshape(shape)
inv_perm = list(np.argsort(perm))
self.tensor = self.tensor.transpose(inv_perm)
[docs]
def apply_left(
self,
utry: UnitaryMatrix,
location: CircuitLocationLike,
inverse: bool = False,
check_arguments: bool = True,
) -> None:
"""
Apply the specified unitary on the left of this UnitaryBuilder.
..
.------. .-----.
0 -| |---| |-
1 -| gate |---| |-
'------' . .
. .
. .
n-1 ------------| |-
'-----'
Args:
utry (UnitaryMatrix): The unitary to apply.
location (CircuitLocationLike): The qudits to apply the unitary on.
inverse (bool): If true, apply the inverse of the unitary.
check_arguments (bool): If true, check the inputs for type and
value errors.
Raises:
ValueError: If `utry`'s size does not match the given location.
ValueError: if `utry`'s radixes does not match the given location.
Notes:
- Applying the unitary on the left is equivalent to multiplying
the unitary on the right of the tensor. The notation comes
from the quantum circuit perspective.
- This operation is performed using tensor contraction.
"""
from bqskit.ir.location import CircuitLocation
if check_arguments:
if not isinstance(utry, UnitaryMatrix):
raise TypeError('Expected UnitaryMatrix, got %s', type(utry))
if not CircuitLocation.is_location(location, self.num_qudits):
raise TypeError('Invalid location.')
location = CircuitLocation(location)
if len(location) != utry.num_qudits:
raise ValueError('Unitary and location size mismatch.')
for utry_radix, bldr_radix_idx in zip(utry.radixes, location):
if utry_radix != self.radixes[bldr_radix_idx]:
raise ValueError('Unitary and location radix mismatch.')
location = cast(CircuitLocation, location)
left_perm = list(range(self.num_qudits))
mid_perm = [
x + self.num_qudits
for x in left_perm
if x not in location
]
right_perm = [x + self.num_qudits for x in location]
right_dim = int(
np.prod([
self.radixes[x - self.num_qudits]
for x in right_perm
]),
)
utry = utry.dagger if inverse else utry
perm = left_perm + mid_perm + right_perm
self.tensor = self.tensor.transpose(perm)
self.tensor = self.tensor.reshape((-1, right_dim))
self.tensor = self.tensor @ utry
shape = list(self.radixes) * 2
shape = [shape[p] for p in perm]
self.tensor = self.tensor.reshape(shape)
inv_perm = list(np.argsort(perm))
self.tensor = self.tensor.transpose(inv_perm)
[docs]
def eval_apply_right(
self,
M: npt.NDArray[np.complex128],
location: CircuitLocationLike,
) -> npt.NDArray[np.complex128]:
"""
Evaluate the application of `M` on the right of this UnitaryBuilder.
See :func:`apply_right` for more info.
"""
from bqskit.ir.location import CircuitLocation
left_perm = list(cast(CircuitLocation, location))
mid_perm = [x for x in range(self.num_qudits) if x not in left_perm]
right_perm = [x + self.num_qudits for x in range(self.num_qudits)]
left_dim = int(np.prod([self.radixes[x] for x in left_perm]))
perm = left_perm + mid_perm + right_perm
tensor_copy = self.tensor.copy()
tensor_copy = tensor_copy.transpose(perm)
tensor_copy = tensor_copy.reshape((left_dim, -1))
tensor_copy = M @ tensor_copy # TODO: Require out matrix to avoid copy
shape = list(self.radixes) * 2
shape = [shape[p] for p in perm]
tensor_copy = tensor_copy.reshape(shape)
inv_perm = list(np.argsort(perm))
tensor_copy = tensor_copy.transpose(inv_perm)
out_M = tensor_copy.reshape((self.dim, self.dim))
return out_M
[docs]
def eval_apply_left(
self,
M: npt.NDArray[np.complex128],
location: CircuitLocationLike,
) -> npt.NDArray[np.complex128]:
"""
Evaluate the application of `M` on the left of this UnitaryBuilder.
See :func:`apply_left` for more info.
"""
from bqskit.ir.location import CircuitLocation
location = cast(CircuitLocation, location)
left_perm = list(range(self.num_qudits))
mid_perm = [
x + self.num_qudits
for x in left_perm
if x not in location
]
right_perm = [x + self.num_qudits for x in location]
right_dim = int(
np.prod([
self.radixes[x - self.num_qudits]
for x in right_perm
]),
)
perm = left_perm + mid_perm + right_perm
tensor_copy = self.tensor.copy()
tensor_copy = tensor_copy.transpose(perm)
tensor_copy = tensor_copy.reshape((-1, right_dim))
tensor_copy = tensor_copy @ M
shape = list(self.radixes) * 2
shape = [shape[p] for p in perm]
tensor_copy = tensor_copy.reshape(shape)
inv_perm = list(np.argsort(perm))
tensor_copy = tensor_copy.transpose(inv_perm)
out_M = tensor_copy.reshape((self.dim, self.dim))
return out_M
[docs]
def calc_env_matrix(
self, location: Sequence[int],
) -> npt.NDArray[np.complex128]:
"""
Calculates the environment matrix w.r.t.
the specified location.
Args:
location (Sequence[int]): Calculate the environment matrix
with respect to the qudit indices in location.
Returns:
np.ndarray: The environmental matrix.
"""
left_perm = list(range(self.num_qudits))
left_perm = [x for x in left_perm if x not in location]
left_perm = left_perm + [x + self.num_qudits for x in left_perm]
right_perm = list(location) + [x + self.num_qudits for x in location]
perm = left_perm + right_perm
a = np.transpose(self.tensor, perm)
a = np.reshape(
a, (
2 ** (self.num_qudits - len(location)),
2 ** (self.num_qudits - len(location)),
2 ** len(location),
2 ** len(location),
),
)
return np.trace(a)