Source code for paulicirc.builders

"""Circuit builders"""

# 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 Iterator, Sequence
from fractions import Fraction
from math import ceil
from typing import (
    Literal,
    Self,
    SupportsIndex,
    TypeAlias,
)

import numpy as np

from .utils.numpy import Complex128Array1D, Complex128Array2D, canonicalize_phase
from .gadgets import (
    Gadget,
    Phase,
    QubitIdx,
    QubitIdxs,
    set_gadget_leg_at,
    set_gadget_legs_at,
    set_phase,
)
from .circuits import Circuit, transversal_set_gadget_leg_at, transversal_set_phase

if __debug__:
    from typing_validation import validate

PhaseLike: TypeAlias = Phase | Fraction
r"""
Type alias for values which can be used to specify a phase:

- as a floating point value in :math:`[0, 2\pi)`, see :obj:`Phase`
- as a fraction of :math:`\pi`

"""


[docs] class CircuitBuilder: """A utility class to build circuits using common gate-based syntax.""" _circuit: Circuit _num_gadgets: int _capacity_scaling: int | float __slots__ = ("__weakref__", "_circuit", "_num_gadgets", "_capacity_scaling")
[docs] def __new__( cls, num_qubits: int, *, init_capacity: int | None = None, capacity_scaling: int | float = 2, ) -> Self: """ Create an empty circuit builder with the given number of qubits. Optionally, initial capacity and/or capacity scaling can be specified for the underlying circuit. By default, the initial capacity is set to the number of qubits (with a minimum of 1), while the capacity scaling factor is set to 2 (doubling capacity whenever required). :meta public: """ if init_capacity is None: init_capacity = max(1, num_qubits) assert CircuitBuilder.__validate_new_args( num_qubits, init_capacity, capacity_scaling ) self = super().__new__(cls) self._circuit = Circuit.zero(init_capacity, num_qubits) self._num_gadgets = 0 self._capacity_scaling = capacity_scaling return self
@property def num_qubits(self) -> int: """Number of qubits for the circuit.""" return self._circuit.num_qubits @property def capacity(self) -> int: """The current capacity of the circuit builder.""" return len(self._circuit)
[docs] def circuit(self) -> Circuit: """Returns a circuit constructed from the gadgets currently in the builder.""" return self._circuit[: self._num_gadgets].clone()
[docs] def append(self, gadget: Gadget) -> None: """Appends a single gadget to the circuit being built.""" if len(self) >= self.capacity: self._scale_up_capacity(1) self._circuit[len(self)] = gadget self._num_gadgets += 1
[docs] def extend(self, gadgets: Sequence[Gadget] | Circuit) -> None: """ Appends the given gadgets to the circuit being built. Gadgets are all validated prior to any modification, so either they are all appended to the circuit or none is. """ if isinstance(gadgets, Circuit): new_circuit = gadgets else: new_circuit = Circuit.from_gadgets(gadgets, self.num_qubits) num_gadgets = len(self) num_new_gadgets = len(new_circuit) if num_gadgets + num_new_gadgets > self.capacity: self._scale_up_capacity(num_new_gadgets) self._circuit[num_gadgets : num_gadgets + num_new_gadgets] = new_circuit self._num_gadgets += num_new_gadgets
def _scale_up_capacity(self, num_new_gadgets: int) -> None: capacity = len(self._circuit) * 1.0 capacity_scaling = self._capacity_scaling target_capacity = len(self) + num_new_gadgets while capacity < target_capacity: capacity *= capacity_scaling self.set_capacity(int(ceil(capacity)))
[docs] def set_capacity(self, new_capacity: int) -> None: """Sets the circuit capacity to the given value.""" assert self.__validate_capacity(new_capacity) circuit = self._circuit capacity = len(circuit) if new_capacity == capacity: return ext_circuit = Circuit.zero(new_capacity, self.num_qubits) ext_circuit[:capacity] = circuit self._circuit = ext_circuit
[docs] def trim_capacity(self) -> None: """Sets the circuit capacity to the minimum amount possible.""" self.set_capacity(max(1, len(self)))
[docs] def unitary(self, *, _normalise_phase: bool = True) -> Complex128Array2D: """Returns the unitary matrix associated to the circuit being built.""" res = np.eye(2**self.num_qubits, dtype=np.complex128) for gadget in self: res = gadget.unitary(canonical_phase=False) @ res if _normalise_phase: canonicalize_phase(res) return res
[docs] def statevec( self, input: Complex128Array1D, _normalise_phase: bool = False ) -> Complex128Array1D: """ Computes the statevector resulting from the application of the circuit being built to the given input statevector. """ assert validate(input, Complex128Array1D) res = input for gadget in self: res = gadget.unitary(canonical_phase=False) @ res if _normalise_phase: canonicalize_phase(res) return res
def __iter__(self) -> Iterator[Gadget]: """Iterates over the gadgets currently in the circuit.""" yield from self._circuit[: self._num_gadgets] def __len__(self) -> int: """The number of gadgets currently in the circuit.""" return self._num_gadgets def __repr__(self) -> str: m, n = len(self), self.num_qubits return f"<CircuitBuilder: {m} gadgets, {n} qubits>" def __sizeof__(self) -> int: return ( object.__sizeof__(self) + self._num_gadgets.__sizeof__() + self._capacity_scaling.__sizeof__() + self._circuit.__sizeof__() ) def _gadget(self, ps: Sequence[int], qs: QubitIdxs, phase: PhaseLike) -> None: """Adds a gadget with given Paulistring and angle on given qubits.""" assert CircuitBuilder.__validate_qubit_idxs(qs) if isinstance(phase, Fraction): phase = (float(phase) * np.pi) % (2 * np.pi) if len(self) == self.capacity: self._scale_up_capacity(1) gadget_data = self._circuit._data[self._num_gadgets] set_gadget_legs_at(gadget_data, np.array(ps, np.uint8), np.array(qs, np.uint64)) set_phase(gadget_data, phase) self._num_gadgets += 1 # TODO: add support for transversal gadget application with disjoint leg qubits def _rot(self, p: int, q: QubitIdx | QubitIdxs, phase: PhaseLike) -> None: assert CircuitBuilder.__validate_qubit_idxs(q) if isinstance(phase, Fraction): phase = (float(phase) * np.pi) % (2 * np.pi) if isinstance(q, QubitIdx): if len(self) == self.capacity: self._scale_up_capacity(1) gadget_data = self._circuit._data[self._num_gadgets] set_gadget_leg_at(gadget_data, p, q) set_phase(gadget_data, phase) self._num_gadgets += 1 else: num_qs = len(q) if len(self) + num_qs > self.capacity: self._scale_up_capacity(num_qs) start = self._num_gadgets end = start + num_qs circ = self._circuit._data transversal_set_gadget_leg_at(circ, p, np.array(q, np.uint64), start, end) transversal_set_phase(circ, phase, start, end) self._num_gadgets += num_qs
[docs] def rx(self, phase: PhaseLike, q: QubitIdx | QubitIdxs) -> None: """Adds an X rotation with given angle on the given qubit(s).""" self._rot(0b01, q, phase)
[docs] def rz(self, phase: PhaseLike, q: QubitIdx | QubitIdxs) -> None: """Adds a Z rotation with given angle on the given qubit(s).""" self._rot(0b10, q, phase)
[docs] def ry(self, phase: PhaseLike, q: QubitIdx | QubitIdxs) -> None: """Adds a Y rotation with given angle on the given qubit(s).""" self._rot(0b11, q, phase)
[docs] def x(self, q: QubitIdx | QubitIdxs) -> None: """Adds a X gate on the given qubit.""" self._rot(0b01, q, Fraction(1, 1))
[docs] def z(self, q: QubitIdx | QubitIdxs) -> None: """Adds a Z gate on the given qubit.""" self._rot(0b10, q, Fraction(1, 1))
[docs] def y(self, q: QubitIdx | QubitIdxs) -> None: """Adds a Y gate on the given qubit.""" self._rot(0b11, q, Fraction(1, 1))
[docs] def sx(self, q: QubitIdx | QubitIdxs) -> None: """Adds a √X gate on the given qubit.""" self._rot(0b01, q, Fraction(1, 2))
[docs] def sxdg(self, q: QubitIdx | QubitIdxs) -> None: """Adds a √X† gate on the given qubit.""" self._rot(0b01, q, Fraction(-1, 2))
[docs] def s(self, q: QubitIdx | QubitIdxs) -> None: """Adds a S gate on the given qubit.""" self._rot(0b10, q, Fraction(1, 2))
[docs] def sdg(self, q: QubitIdx | QubitIdxs) -> None: """Adds a S† gate on the given qubit.""" self._rot(0b10, q, Fraction(-1, 2))
[docs] def t(self, q: QubitIdx | QubitIdxs) -> None: """Adds a T gate on the given qubit.""" self._rot(0b10, q, Fraction(1, 4))
[docs] def tdg(self, q: QubitIdx | QubitIdxs) -> None: """Adds a T† gate on the given qubit.""" self._rot(0b10, q, Fraction(-1, 4))
[docs] def h(self, q: QubitIdx | QubitIdxs, *, xzx: bool = False) -> None: """ Adds a H gate on the given qubit. By default, this is decomposed as ``Z(pi/2)X(pi/2)Z(pi/2)``, but setting ``xzx=True`` decomposes it as ``X(pi/2)Z(pi/2)X(pi/2)`` instead. """ b = 0b01 if xzx else 0b10 self._rot(b, q, Fraction(1, 2)) self._rot(3 - b, q, Fraction(1, 2)) # 0b10 if xzx else 0b01 self._rot(b, q, Fraction(1, 2))
[docs] def hdg(self, q: QubitIdx | QubitIdxs, *, xzx: bool = False) -> None: """ Adds a H gate on the given qubit. By default, this is decomposed as ``Z(-pi/2)X(-pi/2)Z(-pi/2)``, but setting ``xzx=True`` decomposes it as ``X(-pi/2)Z(-pi/2)X(-pi/2)`` instead. """ b = 0b01 if xzx else 0b10 self._rot(b, q, Fraction(-1, 2)) self._rot(3 - b, q, Fraction(-1, 2)) # 0b10 if xzx else 0b01 self._rot(b, q, Fraction(-1, 2))
[docs] def cz(self, c: QubitIdx, t: QubitIdx) -> None: """Adds a CZ gate to the given control and target qubits.""" self._rot(0b10, c, Fraction(-1, 2)) self._rot(0b10, t, Fraction(-1, 2)) self._gadget([2, 2], [c, t], Fraction(1, 2))
[docs] def cx(self, c: QubitIdx, t: QubitIdx) -> None: """Adds a CX gate to the given control and target qubits.""" self._rot(0b10, t, Fraction(1, 2)) self._rot(0b01, t, Fraction(1, 2)) self._rot(0b10, c, Fraction(-1, 2)) self._rot(0b10, t, Fraction(-1, 2)) self._gadget([2, 2], [c, t], Fraction(1, 2)) self._rot(0b01, t, Fraction(-1, 2)) self._rot(0b10, t, Fraction(-1, 2))
[docs] def cy(self, c: QubitIdx, t: QubitIdx) -> None: """Adds a CY gate to the given control and target qubits.""" self._rot(0b01, t, Fraction(1, 2)) self._rot(0b10, c, Fraction(-1, 2)) self._rot(0b10, t, Fraction(-1, 2)) self._gadget([2, 2], [c, t], Fraction(1, 2)) self._rot(0b01, t, Fraction(-1, 2))
[docs] def swap(self, c: QubitIdx, t: QubitIdx) -> None: """Adds a SWAP gate to the given control and target qubits.""" self.cx(c, t) self.cx(t, c) self.cx(c, t)
[docs] def ccx(self, c0: QubitIdx, c1: QubitIdx, t: QubitIdx) -> None: """Adds a CCX gate to the given control and target qubits.""" self._rot(0b10, t, Fraction(1, 2)) self._rot(0b01, t, Fraction(1, 2)) self.ccz(c0, c1, t) self._rot(0b01, t, Fraction(-1, 2)) self._rot(0b10, t, Fraction(-1, 2))
[docs] def ccz(self, c0: QubitIdx, c1: QubitIdx, t: QubitIdx) -> None: """Adds a CCZ gate to the given control and target qubits.""" self._gadget([2, 0, 0], [c0, c1, t], Fraction(1, 4)) self._gadget([0, 2, 0], [c0, c1, t], Fraction(1, 4)) self._gadget([0, 0, 2], [c0, c1, t], Fraction(1, 4)) self._gadget([0, 2, 2], [c0, c1, t], Fraction(-1, 4)) self._gadget([2, 0, 2], [c0, c1, t], Fraction(-1, 4)) self._gadget([2, 2, 0], [c0, c1, t], Fraction(-1, 4)) self._gadget([2, 2, 2], [c0, c1, t], Fraction(1, 4))
[docs] def ccy(self, c0: QubitIdx, c1: QubitIdx, t: QubitIdx) -> None: """Adds a CCY gate to the given control and target qubits.""" self._rot(0b01, t, Fraction(1, 2)) self.ccz(c0, c1, t) self._rot(0b01, t, Fraction(-1, 2))
[docs] def cswap(self, c: QubitIdx, t0: QubitIdx, t1: QubitIdx) -> None: """Adds a CSWAP gate to the given control and target qubits.""" self.cx(t1, t0) self.ccx(c, t0, t1) self.cx(t1, t0)
if __debug__: @staticmethod def __validate_new_args( num_qubits: int, init_capacity: int, capacity_scaling: int | float ) -> Literal[True]: """Validate arguments to the :meth:`__new__` method.""" validate(num_qubits, SupportsIndex) validate(init_capacity, int) validate(capacity_scaling, int | float) num_qubits = int(num_qubits) if num_qubits < 0: raise ValueError("Number of qubits must be non-negative.") if init_capacity <= 0: raise ValueError("Circuit capacity must be >= 1.") if capacity_scaling <= 1.0: raise ValueError("Circuit capacity scalling must be > 1.") return True @staticmethod def __validate_qubit_idxs(q: QubitIdx | QubitIdxs) -> Literal[True]: if isinstance(q, QubitIdx): return True if isinstance(q, np.ndarray): if not np.issubdtype(q.dtype, np.unsignedinteger): raise ValueError( "Qubit indices specified by numpy arrays must be of uint dtype." ) return True validate(q, Sequence[int]) if not all(_q >= 0 for _q in q): raise ValueError("Qubit indices must be >= 0.") if len(set(q)) != len(q): raise ValueError("Qubit indices must not be repeated.") return True def __validate_capacity(self, new_capacity: int) -> Literal[True]: if new_capacity <= 0: raise ValueError("Circuit capacity must be >= 1.") if new_capacity < self._num_gadgets: raise ValueError("Current number of gadgets exceeds desired capacity.") return True
# class CircuitBuilder(CircuitBuilderBase): # """Circuit builder where gadgets are stored in insertion order.""" # _circuit: Circuit # _num_gadgets: int # _capacity_scaling: int | float # __slots__ = ("_circuit", "_num_gadgets", "_capacity_scaling") # def __new__( # cls, # num_qubits: int, # *, # init_capacity: int = 16, # capacity_scaling: int | float = 2, # ) -> Self: # self = super().__new__(cls, num_qubits) # assert CircuitBuilder.__validate_new_args(init_capacity, capacity_scaling) # self._circuit = Circuit.zero(init_capacity, num_qubits) # self._num_gadgets = 0 # self._capacity_scaling = capacity_scaling # return self # @property # def capacity(self) -> int: # return len(self._circuit) # def append(self, gadget: Gadget) -> None: # if len(self) >= self.capacity: # self._scale_up_capacity(1) # self._circuit[len(self)] = gadget # self._num_gadgets += 1 # def extend(self, gadgets: Sequence[Gadget] | Circuit) -> None: # if isinstance(gadgets, Circuit): # new_circuit = gadgets # else: # new_circuit = Circuit.from_gadgets(gadgets, self.num_qubits) # num_gadgets = len(self) # num_new_gadgets = len(new_circuit) # if num_gadgets + num_new_gadgets > self.capacity: # self._scale_up_capacity(num_new_gadgets) # self._circuit[num_gadgets : num_gadgets + num_new_gadgets] = new_circuit # self._num_gadgets += num_new_gadgets # def _scale_up_capacity(self, num_new_gadgets: int) -> None: # capacity = len(self._circuit) * 1.0 # capacity_scaling = self._capacity_scaling # target_capacity = len(self) + num_new_gadgets # while capacity < target_capacity: # capacity *= capacity_scaling # self.set_capacity(int(ceil(capacity))) # def set_capacity(self, new_capacity: int) -> None: # """Sets the circuit capacity to the given value.""" # assert self._validate_capacity(new_capacity) # circuit = self._circuit # capacity = len(circuit) # if new_capacity == capacity: # return # ext_circuit = Circuit.zero(new_capacity, self.num_qubits) # ext_circuit[:capacity] = circuit # self._circuit = ext_circuit # def trim_capacity(self) -> None: # """Sets the circuit capacity to the minimum amount possible.""" # self.set_capacity(max(1, len(self))) # @override # def circuit(self) -> Circuit: # return self._circuit[: self._num_gadgets].clone() # def __iter__(self) -> Iterator[Gadget]: # yield from self._circuit[: self._num_gadgets] # def __len__(self) -> int: # return self._num_gadgets # def __repr__(self) -> str: # m, n = len(self), self.num_qubits # return f"<CircuitBuilder: {m} gadgets, {n} qubits>" # def __sizeof__(self) -> int: # return ( # object.__sizeof__(self) # + self._num_qubits.__sizeof__() # + self._num_gadgets.__sizeof__() # + self._circuit.__sizeof__() # ) # if __debug__: # @staticmethod # def __validate_new_args( # init_capacity: int, capacity_scaling: int | float # ) -> Literal[True]: # validate(init_capacity, int) # validate(capacity_scaling, int | float) # if init_capacity <= 0: # raise ValueError("Circuit capacity must be >= 1.") # if capacity_scaling <= 1.0: # raise ValueError("Circuit capacity scalling must be > 1.") # return True # def _validate_capacity(self, new_capacity: int) -> Literal[True]: # if new_capacity <= 0: # raise ValueError("Circuit capacity must be >= 1.") # if new_capacity < self._num_gadgets: # raise ValueError("Current number of gadgets exceeds desired capacity.") # return True # class LayeredCircuitBuilder(CircuitBuilderBase): # """ # Circuit builder where gadgets are fused into layers of # commuting gadgets with compatible legs. # """ # _layers: list[Layer] # __slots__ = ("_layers",) # def __new__(cls, num_qubits: int) -> Self: # self = super().__new__(cls, num_qubits) # self._layers = [] # return self # @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) # def append(self, gadget: Gadget) -> None: # 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) # def extend(self, gadgets: Sequence[Gadget] | Circuit) -> None: # assert self._validate_gadgets(gadgets) # for gadget in gadgets: # self.append(gadget) # def __iter__(self) -> Iterator[Gadget]: # for layer in self._layers: # yield from layer # def __len__(self) -> int: # return sum(map(len, self._layers)) # 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] # ) # def __repr__(self) -> str: # m, n = self.num_layers, self.num_qubits # return f"<LayeredCircuitBuilder: {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) # )