Skip to content

Commit

Permalink
Merge pull request #27 from upsideon/add-power-wrapper-for-gates
Browse files Browse the repository at this point in the history
[unitaryhack] Adding a power wrapper for Gates.
  • Loading branch information
Athena Caesura committed Jun 11, 2022
2 parents 20974a5 + 04af026 commit ca7fbe9
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 2 deletions.
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,
}


# ---------- 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),
]
),
]


Expand Down

0 comments on commit ca7fbe9

Please sign in to comment.