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

PauliZGate #254

Merged
merged 22 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions bqskit/ir/gates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
CUGate
FSIMGate
PauliGate
PauliZGate
PhasedXZGate
RSU3Gate
RXGate
Expand Down
2 changes: 2 additions & 0 deletions bqskit/ir/gates/parameterized/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from bqskit.ir.gates.parameterized.cu import CUGate
from bqskit.ir.gates.parameterized.fsim import FSIMGate
from bqskit.ir.gates.parameterized.pauli import PauliGate
from bqskit.ir.gates.parameterized.pauliz import PauliZGate
from bqskit.ir.gates.parameterized.phasedxz import PhasedXZGate
from bqskit.ir.gates.parameterized.rsu3 import RSU3Gate
from bqskit.ir.gates.parameterized.rx import RXGate
Expand Down Expand Up @@ -41,6 +42,7 @@
'CUGate',
'FSIMGate',
'PauliGate',
'PauliZGate',
'PhasedXZGate',
'RSU3Gate',
'RXGate',
Expand Down
103 changes: 103 additions & 0 deletions bqskit/ir/gates/parameterized/pauliz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""This module implements the PauliZGate."""
from __future__ import annotations

from typing import Any

import numpy as np
import numpy.typing as npt

from bqskit.ir.gates.generalgate import GeneralGate
from bqskit.ir.gates.qubitgate import QubitGate
from bqskit.qis.pauliz import PauliZMatrices
from bqskit.qis.unitary.differentiable import DifferentiableUnitary
from bqskit.qis.unitary.unitary import RealVector
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
from bqskit.utils.docs import building_docs
from bqskit.utils.math import dexpmv
from bqskit.utils.math import dot_product
from bqskit.utils.math import pauliz_expansion
from bqskit.utils.math import unitary_log_no_i


class PauliZGate(QubitGate, DifferentiableUnitary, GeneralGate):
"""
A gate representing an arbitrary diagonal rotation.

This gate is given by:

.. math::

\\exp({i(\\vec{\\alpha} \\cdot \\vec{\\sigma_Z^{\\otimes n}})})

Where :math:`\\vec{\\alpha}` are the gate's parameters,
:math:`\\vec{\\sigma}` are the PauliZ Z matrices,
and :math:`n` is the number of qubits this gate acts on.
"""

def __init__(self, num_qudits: int) -> None:
"""
Create a PauliZGate acting on `num_qudits` qubits.

Args:
num_qudits (int): The number of qudits this gate will act on.

Raises:
ValueError: If `num_qudits` is nonpositive.
"""

if num_qudits <= 0:
raise ValueError(f'Expected positive integer, got {num_qudits}')

self._name = f'PauliZGate({num_qudits})'
self._num_qudits = num_qudits
paulizs = PauliZMatrices(self.num_qudits)
self._num_params = len(paulizs)
if building_docs():
self.sigmav: npt.NDArray[Any] = np.array([])
else:
self.sigmav = (-1j / 2) * paulizs.numpy

def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
"""Return the unitary for this gate, see :class:`Unitary` for more."""
self.check_parameters(params)
H = dot_product(params, self.sigmav)
eiH = np.diag(np.exp(np.diag(H)))
return UnitaryMatrix(eiH, check_arguments=False)

def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]:
"""
Return the gradient for this gate.

See :class:`DifferentiableUnitary` for more info.

TODO: Accelerated gradient computation for diagonal matrices.
"""
self.check_parameters(params)
H = dot_product(params, self.sigmav)
_, dU = dexpmv(H, self.sigmav)
return dU

def get_unitary_and_grad(
self,
params: RealVector = [],
) -> tuple[UnitaryMatrix, npt.NDArray[np.complex128]]:
"""
Return the unitary and gradient for this gate.

See :class:`DifferentiableUnitary` for more info.
"""
self.check_parameters(params)

H = dot_product(params, self.sigmav)
U, dU = dexpmv(H, self.sigmav)
return UnitaryMatrix(U, check_arguments=False), dU

def calc_params(self, utry: UnitaryMatrix) -> list[float]:
"""Return the parameters for this gate to implement `utry`"""
return list(-2 * pauliz_expansion(unitary_log_no_i(utry.numpy)))

def __eq__(self, o: object) -> bool:
return isinstance(o, PauliZGate) and self.num_qudits == o.num_qudits

def __hash__(self) -> int:
return hash((self.__class__.__name__, self.num_qudits))
2 changes: 2 additions & 0 deletions bqskit/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
:toctree: autogen
:recursive:

DiagonalSynthesisPass
LEAPSynthesisPass
QSearchSynthesisPass
QFASTDecompositionPass
Expand Down Expand Up @@ -319,6 +320,7 @@
'ScanPartitioner',
'QuickPartitioner',
'SynthesisPass',
'DiagonalSynthesisPass',
'LEAPSynthesisPass',
'QSearchSynthesisPass',
'QFASTDecompositionPass',
Expand Down
39 changes: 39 additions & 0 deletions bqskit/passes/control/predicates/diagonal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""This module implements the DiagonalPredicate class."""
from __future__ import annotations

from typing import TYPE_CHECKING

from bqskit.passes.control.predicate import PassPredicate
from bqskit.utils.math import diagonal_distance

if TYPE_CHECKING:
from bqskit.compiler.passdata import PassData
from bqskit.ir.circuit import Circuit


class DiagonalPredicate(PassPredicate):
"""
The DiagonalPredicate class.

The DiagonalPredicate class returns True if the circuit's unitary can be
approximately inverted by a diagonal unitary. A unitary is approximately
inverted when the Hilbert-Schmidt distance to the identity is less than some
threshold.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to put a "See Also" section in the documentation.

"""

def __init__(self, threshold: float) -> None:
"""
Construct a DiagonalPredicate.

Args:
threshold (float): If a circuit can be approximately inverted
by a diagonal unitary (meaning the Hilbert-Schmidt distance
to the identity is less than or equal to this number after
multiplying by the diagonal unitary), True is returned.
"""
self.threshold = threshold

def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
"""Call this predicate, see :class:`PassPredicate` for more info."""
dist = diagonal_distance(circuit.get_unitary().numpy)
return dist <= self.threshold
2 changes: 2 additions & 0 deletions bqskit/passes/synthesis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""This package implements synthesis passes and synthesis related classes."""
from __future__ import annotations

from bqskit.passes.synthesis.diagonal import WalshDiagonalSynthesisPass
from bqskit.passes.synthesis.leap import LEAPSynthesisPass
from bqskit.passes.synthesis.pas import PermutationAwareSynthesisPass
from bqskit.passes.synthesis.qfast import QFASTDecompositionPass
Expand All @@ -10,6 +11,7 @@
from bqskit.passes.synthesis.target import SetTargetPass

__all__ = [
'WalshDiagonalSynthesisPass',
'LEAPSynthesisPass',
'QFASTDecompositionPass',
'QPredictDecompositionPass',
Expand Down
112 changes: 112 additions & 0 deletions bqskit/passes/synthesis/diagonal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""This module implements the WalshDiagonalSynthesisPass."""
from __future__ import annotations

import logging

from numpy import where

from bqskit.compiler.passdata import PassData
from bqskit.ir.circuit import Circuit
from bqskit.ir.gates import CNOTGate
from bqskit.ir.gates import RZGate
from bqskit.passes.synthesis.synthesis import SynthesisPass
from bqskit.qis.state.state import StateVector
from bqskit.qis.state.system import StateSystem
from bqskit.qis.unitary import UnitaryMatrix
from bqskit.utils.math import pauliz_expansion
from bqskit.utils.math import unitary_log_no_i


_logger = logging.getLogger(__name__)


class WalshDiagonalSynthesisPass(SynthesisPass):
"""
A pass that synthesizes diagonal unitaries into Walsh functions.

Based on: https://arxiv.org/abs/1306.3991
"""

def __init__(
self,
parameter_precision: float = 1e-8,
) -> None:
"""
Constructor for WalshDiagonalSynthesisPass.

Args:
parameter_precision (float): Pauli strings with parameter values
less than this are rounded to zero. (Default: 1e-8)

TODO:
- Cancel adjacent CNOTs
- See how QFAST can be used to generalize to qudits
"""
self.parameter_precision = parameter_precision

def gray_code(self, number: int) -> int:
"""Convert a number to its Gray code representation."""
gray = number ^ (number >> 1)
return gray

def pauli_to_subcircuit(
self,
string_id: int,
angle: float,
num_qubits: int,
) -> Circuit:
string = bin(string_id)[2:].zfill(num_qubits)
circuit = Circuit(num_qubits)
locations = [i for i in range(num_qubits) if string[i] == '1']
if len(locations) == 1:
circuit.append_gate(RZGate(), locations[0], [angle])
elif len(locations) > 1:
pairs = [
(locations[i], locations[i + 1])
for i in range(len(locations) - 1)
]
for pair in pairs:
circuit.append_gate(CNOTGate(), pair)
circuit.append_gate(RZGate(), locations[-1], [angle])
for pair in reversed(pairs):
circuit.append_gate(CNOTGate(), pair)
return circuit

async def synthesize(
self,
utry: UnitaryMatrix | StateVector | StateSystem,
data: PassData,
) -> Circuit:
"""Synthesize `utry`, see :class:`SynthesisPass` for more."""
if not isinstance(utry, UnitaryMatrix):
m = 'WalshDiagonalSynthesisPass can only synthesize diagonal, '
m += f'`UnitaryMatrix`s, got {type(utry)}.'
raise TypeError(m)

if not utry.is_qubit_only():
m = 'WalshDiagonalSynthesisPass can only synthesize diagonal '
m += '`UnitaryMatrix`s with qubits, got higher radix than 2.'
raise ValueError(m)

num_qubits = utry.num_qudits
circuit = Circuit(num_qubits)

# Find parameters of each I/Z Pauli string
H_matrix = unitary_log_no_i(utry.numpy)
params = pauliz_expansion(H_matrix) * 2
# Remove low weight terms - these are likely numerical errors
params = where(abs(params) < self.parameter_precision, 0, params)

# Order the Pauli strings by their Gray code representation
pauli_params = sorted(
[(i, -p) for i, p in enumerate(params)],
key=lambda x: self.gray_code(x[0]),
)
subcircuits = [
self.pauli_to_subcircuit(i, p, num_qubits) for i, p in pauli_params
]

for subcircuit in subcircuits:
circuit.append_circuit(subcircuit, [_ for _ in range(num_qubits)])

return circuit
Loading
Loading