"""Layers of Pauli gadgets."""
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from collections.abc import Iterable, Iterator
from typing import (
Any,
Literal,
Self,
Sequence,
SupportsIndex,
final,
overload,
)
import numpy as np
from .gadgets import PAULI_CHARS, Gadget, PauliArray, Phase, are_same_phase
from .circuits import Circuit, statevec_from_gadgets, unitary_from_gadgets
from .utils.numpy import (
RNG,
Complex128Array1D,
Complex128Array2D,
ComplexArray1D,
FloatArray1D,
)
if __debug__:
from typing_validation import validate
[docs]
@final
class Layer:
"""A layer of Pauli gadgets with compatible legs."""
@staticmethod
def _subset_to_indicator(qubits: Iterable[int]) -> int:
"""Converts a collection of non-negative integers to the subset indicator."""
ind = 0
for i in qubits:
ind |= 1 << i
return ind
@staticmethod
def _selected_legs_to_subset(legs: PauliArray) -> int:
"""Convert legs to a subset index."""
return Layer._subset_to_indicator(i for i, leg in enumerate(legs) if leg != 0)
@staticmethod
def _select_leg_subset(subset: int, legs: PauliArray) -> PauliArray:
"""Selects a subset of legs based on the given subset indicator."""
return np.where(
np.fromiter((subset & (1 << x) for x in range(len(legs))), dtype=np.bool_),
legs,
0,
)
[docs]
@staticmethod
def select_leg_subset(qubits: Iterable[int], legs: PauliArray) -> PauliArray:
"""Selects legs based on the given subset of qubits."""
return Layer._select_leg_subset(Layer._subset_to_indicator(qubits), legs)
[docs]
@classmethod
def from_gadgets(
cls, gadgets: Iterable[Gadget], num_qubits: int | None = None
) -> Self:
"""Constructs a layer from the given gadgets."""
gadgets = list(gadgets)
assert Layer.__validate_gadgets(gadgets, num_qubits)
if num_qubits is None:
num_qubits = gadgets[0].num_qubits
self = cls(num_qubits)
for gadget in gadgets:
success = self.add_gadget(gadget)
if not success:
raise ValueError("Given gadgets do not form a single layer.")
return self
_phases: dict[int, Phase]
_legs: PauliArray
_leg_count: np.ndarray[tuple[int], np.dtype[np.uint32]]
# FIXME: remove limit to 2**32 gadgets per layer
__slots__ = ("__weakref__", "_phases", "_legs", "_leg_count")
[docs]
def __new__(cls, num_qubits: int) -> Self:
"""
Create an empty Pauli layer with the given number of qubits.
:meta public:
"""
assert Layer._validate_new_args(num_qubits)
self = super().__new__(cls)
self._phases = {}
self._legs = np.zeros(num_qubits, dtype=np.uint8)
self._leg_count = np.zeros(num_qubits, dtype=np.uint32)
return self
@property
def num_qubits(self) -> int:
"""Number of qubits for the Pauli layer."""
return len(self._legs)
@property
def legs(self) -> PauliArray:
"""Legs of the Pauli layer."""
view = self._legs.view()
view.setflags(write=False)
return view.view()
@property
def leg_paulistr(self) -> str:
"""Paulistring representation of the layer's legs."""
return "".join(PAULI_CHARS[int(p)] for p in self.legs)
[docs]
def phase(self, legs: PauliArray) -> Phase:
"""
Get the phase of the given legs in the layer, or :obj:`None` if the legs
are incompatible with the layer.
"""
if not self.is_compatible_with(legs):
raise ValueError("Selected legs are incompatible with layer.")
return self._phases.get(Layer._selected_legs_to_subset(legs), 0)
@overload
def is_compatible_with(self, legs: PauliArray, /) -> bool: ...
@overload
def is_compatible_with(self, gadget: Gadget, /) -> bool: ...
[docs]
def is_compatible_with(self, legs: PauliArray | Gadget, /) -> bool:
"""Check if the legs are compatible with the current layer."""
if isinstance(legs, Gadget):
legs = legs.legs
assert self.__validate_legs_self(legs)
self_legs = self._legs
return bool(np.all((self_legs == legs) | (self_legs == 0) | (legs == 0)))
@overload
def commutes_with(self, legs: PauliArray, /) -> bool: ...
@overload
def commutes_with(self, gadget: Gadget, /) -> bool: ...
[docs]
def commutes_with(self, legs: PauliArray | Gadget, /) -> bool:
"""Check if the legs commute with the current layer."""
if isinstance(legs, Gadget):
legs = legs.legs
assert self.__validate_legs_self(legs)
self_legs = self._legs
for subset in self._phases:
subset_legs = Layer._select_leg_subset(subset, self_legs)
ovlp = sum((subset_legs != legs) & (legs != 0) & (subset_legs != 0))
if ovlp % 2 != 0:
return False
return True
@overload
def add_gadget(self, gadget: Gadget, /) -> bool: ...
@overload
def add_gadget(self, legs: PauliArray, phase: Phase, /) -> bool: ...
[docs]
def add_gadget(
self, gadget_or_legs: PauliArray | Gadget, phase: Phase | None = None
) -> bool:
"""Add a gadget to the layer."""
if isinstance(gadget_or_legs, Gadget):
legs = gadget_or_legs.legs
phase = gadget_or_legs.phase
else:
legs = gadget_or_legs
assert phase is not None
if not self.is_compatible_with(legs):
return False
phases = self._phases
subset = Layer._selected_legs_to_subset(legs)
if subset in phases:
if are_same_phase(curr_phase := phases[subset], -phase):
del phases[subset]
self._leg_count -= np.where(legs == 0, np.uint32(0), np.uint32(1))
self._legs = np.where(self._leg_count == 0, 0, self._legs)
else:
phases[subset] = (curr_phase + phase) % (2 * np.pi)
return True
phases[subset] = phase
self._leg_count += np.where(legs == 0, np.uint32(0), np.uint32(1))
self._legs = np.where(legs == 0, self._legs, legs)
return True
[docs]
def unitary(
self,
*,
canonical_phase: bool = True,
_use_cupy: bool = False, # currently in alpha
) -> Complex128Array2D:
"""Returns the unitary matrix associated to this Pauli gadget circuit."""
return unitary_from_gadgets(self, self.num_qubits, canonical_phase, _use_cupy)
[docs]
def statevec(
self,
input: ComplexArray1D | FloatArray1D,
canonical_phase: bool = True,
_use_cupy: bool = False, # currently in alpha
) -> Complex128Array1D:
"""
Computes the statevector resulting from the application of this gadget circuit
to the given input statevector.
"""
return statevec_from_gadgets(self, input, canonical_phase, _use_cupy)
def __iter__(self) -> Iterator[Gadget]:
"""
Iterates over the gadgets in the layer, in insertion order.
:meta public:
"""
legs, num_qubits = self._legs, self.num_qubits
for subset, phase in self._phases.items():
subset_legs = Layer._select_leg_subset(subset, legs)
yield Gadget(Gadget.assemble_data(subset_legs, phase), num_qubits)
def __len__(self) -> int:
"""The number of gadgets (with non-zero phase) in this layer."""
return len(self._phases)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Layer):
return NotImplemented
print(self._phases)
print(other._phases)
return (
self.num_qubits == other.num_qubits
and np.array_equal(self._legs, other._legs)
and self._phases == other._phases
)
def __repr__(self) -> str:
legs_str = self.leg_paulistr
if len(legs_str) > 16:
legs_str = legs_str[:8] + "..." + legs_str[-8:]
return f"<Layer: {legs_str}, {len(self)} gadgets>"
def __sizeof__(self) -> int:
return (
object.__sizeof__(self)
+ self._phases.__sizeof__()
+ sum(
key.__sizeof__() + value.__sizeof__()
for key, value in self._phases.items()
)
+ self._legs.__sizeof__()
+ self._leg_count.__sizeof__()
)
if __debug__:
@staticmethod
def _validate_new_args(num_qubits: int) -> Literal[True]:
"""Validate arguments to the :meth:`__new__` method."""
validate(num_qubits, SupportsIndex)
num_qubits = int(num_qubits)
if num_qubits < 0:
raise ValueError("Number of qubits must be non-negative.")
return True
def __validate_legs_self(self, legs: PauliArray) -> Literal[True]:
"""Validates the value of the :attr:`legs` property."""
Gadget._validate_legs(legs)
if len(legs) != self.num_qubits:
raise ValueError("Number of legs does not match number of qubits.")
return True
@staticmethod
def __validate_gadgets(
gadgets: Sequence[Gadget], num_qubits: int | None
) -> Literal[True]:
validate(gadgets, Sequence[Gadget])
if num_qubits is None:
if not gadgets:
raise ValueError(
"At least one gadget must be supplied if num_qubits is omitted."
)
num_qubits = gadgets[0].num_qubits
for gadget in gadgets:
if gadget.num_qubits != num_qubits:
raise ValueError("All gadgets must have the same number of qubits.")
return True
[docs]
@final
class LayeredCircuit:
"""A quantum circuit, represented as a sequential composition of Pauli layers."""
_num_qubits: int
_layers: list[Layer]
__slots__ = (
"__weakref__",
"_num_qubits",
"_layers",
)
def __new__(cls, num_qubits: int) -> Self:
assert LayeredCircuit.__validate_new_args(num_qubits)
self = super().__new__(cls)
self._num_qubits = num_qubits
self._layers = []
return self
@property
def num_qubits(self) -> int:
"""Number of qubits in the circuit."""
return self._num_qubits
@property
def layers(self) -> Sequence[Layer]:
"""Layers of the circuit."""
return tuple(self._layers)
@property
def num_layers(self) -> int:
"""Number of layers in the circuit."""
return len(self._layers)
[docs]
def append(self, gadget: Gadget) -> None:
"""Appends a gadget to the layered circuit."""
assert self.__validate_gadget(gadget)
m, n = self.num_layers, self._num_qubits
layers = self._layers
layer_idx = m
for i in range(m)[::-1]:
layer = layers[i]
if layer.is_compatible_with(gadget):
layer_idx = i
elif not layer.commutes_with(gadget):
break
if layer_idx < m:
layers[layer_idx].add_gadget(gadget)
return
new_layer = Layer(n)
new_layer.add_gadget(gadget)
layers.append(new_layer)
[docs]
def extend(self, gadgets: Iterable[Gadget]) -> None:
"""Appends a sequence of gadgets to the layered circuit."""
for gadget in gadgets:
self.append(gadget)
[docs]
def unitary(
self,
*,
canonical_phase: bool = True,
_use_cupy: bool = False, # currently in alpha
) -> Complex128Array2D:
"""Returns the unitary matrix associated to this Pauli gadget circuit."""
return unitary_from_gadgets(self, self.num_qubits, canonical_phase, _use_cupy)
[docs]
def statevec(
self,
input: ComplexArray1D | FloatArray1D,
canonical_phase: bool = True,
_use_cupy: bool = False, # currently in alpha
) -> Complex128Array1D:
"""
Computes the statevector resulting from the application of this gadget circuit
to the given input statevector.
"""
return statevec_from_gadgets(self, input, canonical_phase, _use_cupy)
def __iter__(self) -> Iterator[Gadget]:
for layer in self._layers:
yield from layer
def __len__(self) -> int:
return sum(map(len, self._layers))
[docs]
def circuit(self) -> Circuit:
"""
Returns a circuit constructed from the current gadget layers,
where the gadgets for each layer are listed in canonical order.
"""
return Circuit.from_gadgets(self, self.num_qubits)
[docs]
def random_circuit(self, *, rng: int | RNG | None) -> Circuit:
"""
Returns a circuit constructed from the current gadget layers,
where the gadgets for each layer are listed in random order.
"""
if not isinstance(rng, RNG):
rng = np.random.default_rng(rng)
return Circuit.from_gadgets(
(g for layer in self._layers for g in rng.permutation(list(layer))), # type: ignore[arg-type]
self.num_qubits,
)
def __repr__(self) -> str:
m, n = self.num_layers, self.num_qubits
return f"<LayeredCircuit: {m} layers, {n} qubits>"
def __sizeof__(self) -> int:
return (
object.__sizeof__(self)
+ self._num_qubits.__sizeof__()
+ self._layers.__sizeof__()
+ sum(layer.__sizeof__() for layer in self._layers)
)
if __debug__:
@staticmethod
def __validate_new_args(
num_qubits: int,
) -> Literal[True]:
"""Validate arguments to the :meth:`__new__` method."""
validate(num_qubits, SupportsIndex)
if num_qubits < 0:
raise ValueError("Number of qubits must be non-negative.")
return True
def __validate_gadget(self, gadget: Gadget) -> Literal[True]:
validate(gadget, Gadget)
if gadget.num_qubits != self.num_qubits:
raise ValueError(
f"Found {gadget.num_qubits} qubits, expected {self.num_qubits}."
)
return True