#
# Copyright © 2022-2024 University of Strasbourg. All Rights Reserved.
# Copyright © 2032-2024 QPerfect. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from fractions import Fraction
from symengine import pi
from mimiqcircuits.printutils import print_wrapped_parens
import sympy as sp
import numpy as np
import mimiqcircuits as mc
from mimiqcircuits.operations.gates.gate import Gate
[docs]
class Power(Gate):
"""Power operation.
Represents a Power operation raised to a specified exponent.
Examples:
>>> from mimiqcircuits import *
>>> c= Circuit()
>>> c.push(Power(GateX(),1/2),1)
2-qubit circuit with 1 instructions:
└── SX @ q[1]
<BLANKLINE>
>>> c.push(Power(GateX(),5),1)
2-qubit circuit with 2 instructions:
├── SX @ q[1]
└── X**5 @ q[1]
<BLANKLINE>
>>> c.decompose()
2-qubit circuit with 6 instructions:
├── U(1.5707963267948966, -1.570796326794897, 1.5707963267948961, 0.7853981633974482) @ q[1]
├── X @ q[1]
├── X @ q[1]
├── X @ q[1]
├── X @ q[1]
└── X @ q[1]
<BLANKLINE>
"""
_name = "Power"
_num_qubits = None
_num_bits = 0
_num_cregs = 0
_op = None
_parnames = ("exponent",)
def __init__(self, operation, exponent, *args, **kwargs):
if isinstance(operation, type) and issubclass(operation, mc.Gate):
op = operation(*args, **kwargs)
elif isinstance(operation, mc.Gate):
op = operation
else:
raise ValueError("Operation must be an Gate object or type.")
if self.num_bits != 0:
raise ValueError("Power operation cannot act on classical bits.")
super().__init__()
self._exponent = exponent
self._op = op
self._num_qubits = op.num_qubits
self._num_qregs = op.num_qregs
self._qregsizes = op.qregsizes
self._parnames = op.parnames
if isinstance(op, mc.Power):
self._op = op.op
self._exponent = exponent * op.exponent
@property
def op(self):
return self._op
@op.setter
def op(self, op):
raise ValueError("Cannot set op. Read only parameter.")
@property
def exponent(self):
return self._exponent
@exponent.setter
def exponent(self, power):
raise ValueError("Cannot set exponent. Read only parameter.")
[docs]
def iswrapper(self):
return True
def _power(self, pwr):
return self.op.power(pwr * self._exponent)
[docs]
def power(self, *args):
if len(args) == 0:
return mc.power(self)
elif len(args) == 1:
exponent = args[0]
return self._power(exponent)
else:
raise ValueError("Invalid number of arguments.")
def __pow__(self, exponent):
return self.power(exponent)
[docs]
def inverse(self):
return mc.Inverse(self)
[docs]
def control(self, *args):
if len(args) == 0:
return mc.control(self)
elif len(args) == 1:
num_controls = args[0]
return mc.Control(num_controls, self)
else:
raise ValueError("Invalid number of arguments.")
[docs]
def parallel(self, *args):
if len(args) == 0:
return mc.parallel(self)
elif len(args) == 1:
num_repeats = args[0]
return mc.Parallel(num_repeats, self)
else:
raise ValueError("Invalid number of arguments.")
def _matrix(self):
matrix = sp.Matrix(self.op.matrix().tolist())
pow_matrix = matrix ** (self.exponent)
return pow_matrix
[docs]
def getparams(self):
return self.op.getparams()
def __str__(self):
if self._op == mc.GateX() and self._exponent == 1 / 2:
return mc.GateSX().name
fraction = Fraction(self.exponent).limit_denominator(100)
if float(fraction) == self.exponent and int(fraction) != self.exponent:
return f"{print_wrapped_parens(self.op)}**({fraction})"
if self.exponent == pi:
return f"{print_wrapped_parens(self.op)}**π"
if self.exponent == -pi:
return f"{print_wrapped_parens(self.op)}**(-π)"
divpi = Fraction(self.exponent / np.pi).limit_denominator(100)
if float(divpi) == self.exponent / np.pi:
if divpi == 1:
return f"{print_wrapped_parens(self.op)}**π"
if divpi == -1:
return f"{print_wrapped_parens(self.op)}**(-1)"
if divpi > 0:
return f"{print_wrapped_parens(self.op)}**({divpi} * π)"
return f"{print_wrapped_parens(self.op)}**(({divpi}) * π)"
if self.exponent < 0:
return f"{print_wrapped_parens(self.op)}**({self.exponent})"
return f"{print_wrapped_parens(self.op)}**{self.exponent}"
def __repr__(self):
return self.__str__()
[docs]
def evaluate(self, d):
exponent = self.exponent
return self.op.evaluate(d).power(exponent)
def _decompose(self, circ, qubits, bits, zvars):
if isinstance(self.exponent, int) and self.exponent >= 1:
for _ in range(self.exponent):
circ.push(self.op, *qubits)
return circ
# try to decompose,
# if there is only a gate, maybe it is ok
# if the gates are all diagonal then we can continue
# otherwise just do nothing and push the same thing
cop = self.op._decompose(mc.Circuit(), qubits, bits, zvars)
if len(cop) == 1:
circ.push(cop.instructions[0].operation._power(self.exponent), *qubits)
return circ
circ.push(self, *qubits)
return circ
[docs]
def decompose(self):
return self._decompose(
mc.Circuit(),
range(self.num_qubits),
range(self.num_bits),
range(self.num_zvars),
)
# export operation
__all__ = ["Power"]