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

[unitaryhack] Adding a power wrapper for Gates. #27

Merged
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
69 changes: 69 additions & 0 deletions src/orquestra/quantum/circuits/_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ def controlled(self, num_control_qubits: int) -> "Gate":
def dagger(self) -> "Gate":
raise NotImplementedError()

def power(self, exponent: float) -> "Gate":
"""Gate representing the underlying matrix raised to the power
of the given exponent.
"""
raise NotImplementedError()

def bind(self, symbols_map: Dict[sympy.Symbol, Parameter]) -> "Gate":
raise NotImplementedError()

Expand Down Expand Up @@ -208,6 +214,9 @@ def controlled(self, num_controlled_qubits: int) -> Gate:
def dagger(self) -> Union["MatrixFactoryGate", Gate]:
return self if self.is_hermitian else Dagger(self)

def power(self, exponent: float) -> "Gate":
return Power(self, exponent)

def __str__(self):
return (
f"{self.name}({', '.join(map(str,self.params))})"
Expand Down Expand Up @@ -289,6 +298,12 @@ def dagger(self) -> "ControlledGate":
num_control_qubits=self.num_control_qubits,
)

def power(self, exponent: float) -> "Gate":
return ControlledGate(
wrapped_gate=self.wrapped_gate.power(exponent),
num_control_qubits=self.num_control_qubits,
)

def bind(self, symbols_map) -> "Gate":
return self.wrapped_gate.bind(symbols_map).controlled(self.num_control_qubits)

Expand Down Expand Up @@ -334,6 +349,60 @@ def replace_params(self, new_params: Tuple[Parameter, ...]) -> "Gate":
def dagger(self) -> "Gate":
return self.wrapped_gate

def power(self, exponent: float) -> "Gate":
return Power(self, exponent)


POWER_GATE_SYMBOL = "^"


@dataclass(frozen=True)
class Power(Gate):
wrapped_gate: Gate
exponent: float

def __post_init__(self):
if len(self.wrapped_gate.free_symbols) > 0:
raise ValueError("Gates with free symbols cannot be exponentiated")

@property
def name(self) -> str:
return f"{self.wrapped_gate.name}{POWER_GATE_SYMBOL}{self.exponent}"

@property
def params(self) -> Tuple[Parameter, ...]:
return self.wrapped_gate.params

@property
def free_symbols(self) -> Iterable[sympy.Symbol]:
return get_free_symbols(self.params)

@property
def num_qubits(self) -> int:
return self.wrapped_gate.num_qubits

@property
def matrix(self) -> sympy.Matrix:
return self.wrapped_gate.matrix**self.exponent

def controlled(self, num_control_qubits: int) -> "Gate":
return self.wrapped_gate.controlled(num_control_qubits).power(self.exponent)

@property
def dagger(self) -> "Gate":
return self.wrapped_gate.dagger.power(self.exponent)

def power(self, exponent: float) -> "Gate":
return Power(self, exponent)

def bind(self, symbols_map: Dict[sympy.Symbol, Parameter]) -> "Gate":
raise NotImplementedError(
"Gates raised to a power do not possess free symbols to bind",
)

def replace_params(self, new_params: Tuple[Parameter, ...]) -> "Gate":
return self.wrapped_gate.replace_params(new_params).power(self.exponent)


def _n_qubits(matrix):
n_qubits = math.floor(math.log2(matrix.shape[0]))
Expand Down
13 changes: 13 additions & 0 deletions src/orquestra/quantum/circuits/_serde.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ def _dagger_gate_to_dict(gate: _gates.Dagger):
}


@to_dict.register
def _power_gate_to_dict(gate: _gates.Power):
return {
"name": gate.name,
"wrapped_gate": to_dict(gate.wrapped_gate),
"exponent": gate.exponent,
}
AthenaCaesura marked this conversation as resolved.
Show resolved Hide resolved


# ---------- deserialization ----------


Expand Down Expand Up @@ -232,6 +241,10 @@ def _special_gate_from_dict(dict_, custom_gate_defs) -> _gates.Gate:
wrapped_gate = _gate_from_dict(dict_["wrapped_gate"], custom_gate_defs)
return _gates.Dagger(wrapped_gate)

elif _gates.POWER_GATE_SYMBOL in dict_["name"]:
wrapped_gate = _gate_from_dict(dict_["wrapped_gate"], custom_gate_defs)
return _gates.Power(wrapped_gate, dict_["exponent"])

else:
raise KeyError()

Expand Down
79 changes: 77 additions & 2 deletions tests/orquestra/quantum/circuits/_gates_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import pytest
import sympy

from orquestra.quantum.circuits import _builtin_gates
from orquestra.quantum.circuits._gates import GateOperation, MatrixFactoryGate
from orquestra.quantum.circuits import _builtin_gates, _gates
from orquestra.quantum.circuits._gates import GateOperation, MatrixFactoryGate, Power

GATES_REPRESENTATIVES = [
_builtin_gates.X,
Expand All @@ -33,6 +33,8 @@
_builtin_gates.CPHASE(1.5),
]

POWER_GATE_EXPONENTS = [-2.0, 0, 0.5, 1.0]


def example_one_qubit_matrix_factory(a, b):
return sympy.Matrix([[a, b], [b, a]])
Expand Down Expand Up @@ -122,6 +124,10 @@ def test_dagger_of_hermitian_gate_is_the_same_gate(self):
)
assert gate.dagger is gate

def test_power_of_dagger_is_dagger_wrapped_by_power(self):
gate = MatrixFactoryGate("V", example_one_qubit_matrix_factory, (1, 0), 1)
assert gate.dagger.power(0.5) == Power(gate.dagger, 0.5)

def test_binding_gates_in_dagger_is_propagated_to_wrapped_gate(self):
theta = sympy.Symbol("theta")
gate = MatrixFactoryGate("V", example_one_qubit_matrix_factory, (theta, 0), 1)
Expand Down Expand Up @@ -193,6 +199,11 @@ def test_dagger_of_controlled_gate_is_controlled_gate_wrapping_dagger(self, gate

assert controlled_gate.dagger == gate.dagger.controlled(4)

def test_power_of_controlled_gate_is_controlled_gate_wrapping_power(self, gate):
if len(gate.free_symbols) == 0:
controlled_gate = gate.controlled(2)
assert controlled_gate.power(0.5) == gate.power(0.5).controlled(2)

def test_binding_parameters_in_control_gate_is_propagated_to_wrapped_gate(
self, gate
):
Expand All @@ -213,6 +224,70 @@ def test_constructing_controlled_gate_with_zero_control_raises_error(self, gate)
gate.controlled(0)


@pytest.mark.parametrize("gate", GATES_REPRESENTATIVES)
@pytest.mark.parametrize("exponent", POWER_GATE_EXPONENTS)
class TestPowerGate:
def test_constructing_a_power_gate_with_free_symbols_raises_error(
self, gate, exponent
):
if len(gate.free_symbols) > 0:
with pytest.raises(ValueError):
gate.power(exponent)

def test_power_gate_naming_scheme(self, gate, exponent):
if len(gate.free_symbols) == 0:
power_gate = gate.power(exponent)
assert power_gate.name == f"{gate.name}{_gates.POWER_GATE_SYMBOL}{exponent}"

def test_has_same_parameters_as_wrapped_gate(self, gate, exponent):
if len(gate.free_symbols) == 0:
assert gate.power(exponent).params == gate.params

def test_has_same_free_symbols_as_wrapped_gate(self, gate, exponent):
if len(gate.free_symbols) == 0:
assert gate.power(exponent).free_symbols == gate.free_symbols

def test_has_same_number_of_qubits_as_wrapped_gate(self, gate, exponent):
if len(gate.free_symbols) == 0:
assert gate.power(exponent).num_qubits == gate.num_qubits

def test_has_matrix_equal_to_wrapped_gate_matrix_exponentiated(
self, gate, exponent
):
if len(gate.free_symbols) == 0:
wrapped_gate_matrix_exponentiated = gate.matrix**exponent
assert gate.power(exponent).matrix == wrapped_gate_matrix_exponentiated

def test_dagger_of_power_gate_power_gate_of_dagger(self, gate, exponent):
if len(gate.free_symbols) == 0:
power_gate = gate.power(exponent)
assert power_gate.dagger == gate.dagger.power(exponent)

def test_creating_power_gate_from_power_gate(self, gate, exponent):
if len(gate.free_symbols) == 0:
power_gate = gate.power(exponent)
powered_power_gate = power_gate.power(exponent)
assert powered_power_gate.matrix == power_gate.matrix**exponent

def test_parameter_binding_not_implemented_for_power_gates(self, gate, exponent):
if len(gate.free_symbols) == 0:
power_gate = gate.power(exponent)
symbols_map = {sympy.Symbol("theta"): 0.5, sympy.Symbol("x"): 3}
with pytest.raises(NotImplementedError):
power_gate.bind(symbols_map)

def test_constructing_power_gate_and_replacing_parameters_commute(
self, gate, exponent
):
if len(gate.free_symbols) == 0:
power_gate = gate.power(exponent)
new_params = tuple(3 * param for param in power_gate.params)

assert power_gate.replace_params(new_params) == gate.replace_params(
new_params
).power(exponent)


@pytest.mark.parametrize("gate", GATES_REPRESENTATIVES)
class TestGateOperation:
def test_bound_symbols_are_not_present_in_gate_parameters(self, gate):
Expand Down
6 changes: 6 additions & 0 deletions tests/orquestra/quantum/circuits/_serde_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@
_builtin_gates.RY(PARAMETER_VECTOR[0] * PARAMETER_VECTOR[1])(1),
]
),
_circuit.Circuit(
[
_builtin_gates.Z.power(0.25)(0),
_builtin_gates.SWAP.power(0.5)(0, 1),
]
AthenaCaesura marked this conversation as resolved.
Show resolved Hide resolved
),
]


Expand Down