Task: Add argument `assume_unitary` to ``Operator.power``. (Qiskit/qiskit#13319)

org: Qiskit
repo: qiskit
pr_number: 13319
issue_title: Add argument `assume_unitary` to ``Operator.power``.
issue_body: <!--\r\n⚠️ If you do not respect this template, your pull request will be closed.\r\n⚠️ Your pull request title should be short detailed and understandable for all.\r\n⚠️ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠️ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [x] I have added the tests to cover my changes.\r\n- [x] I have updated the documentation accordingly.\r\n- [x] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\n\r\nFixes #13307 following @jakelishman's suggestion in the description.\r\n\r\n### Details and comments\r\n\r\nAfter a quick round of profiling on my laptop (Python 10, Windows 11), the old (Schur-based) way to raise an operator to a power is 3x-5x faster than calling `scipy.linalg.fractional_matrix_power`, so it makes sense to use it when the operator is known to be unitary.\r\n\r\nI have renamed the old method to be called `Operator.power_if_unitary`, this **assumes** that the given operator is unitary. The `Operator.is_power` does not make this assumption (and calls `fractional_matrix_power`). The gate class uses the `power_if_unitary` method. \r\n\r\nP.S. In addition, for the `power` method and an integer power, I don't see much runtime difference when calling `fractional_matrix_power` and `matrix_power`. For the `power_if_unitary` method calling `matrix_power` is faster than calling the Schur-based implementation.
resolved_issues: number: 13307
title: `Operator.power` does not handle non-unitary inputs correctly
body: ### Environment\n\n- **Qiskit version**: 1.2.4\n- **Python version**: 3.10\n- **Operating system**: macOS\n\n\n### What is happening?\n\nThe way `Operator.power` works, it's strongly assuming that the internal matrix is unitary, because it's assuming that it can calculate fractional matrix powers by taking the Schur decomposition taking the matrix power of the Schur form then applying the unitary transformation (ok so far), except it assumes that the matrix power of the Schur form is the power of its diagonal.  The Schur form is upper-triangular, but not necessarily diagonal (roughly, I think it's diagonal in the case that the input is a scaled unitary).  So our `Operator.power` code fails for non-unitary operators.\n\n### How can we reproduce the issue?\n\n```python\n>>> from qiskit.quantum_info import Operator\n>>> import scipy.linalg\n>>> # Not unitary.\n>>> data = [[1, 1], [0, -1]]\n>>> print(Operator(data).power(0.5).data)\n[[1.000000e+00+0.j 0.000000e+00+0.j]\n [0.000000e+00+0.j 6.123234e-17+1.j]]\n>>> print(scipy.linalg.fractional_matrix_power(data, 0.5)))\n[[1.00000000e+00+0.j  5.00000000e-01-0.5j]\n [0.00000000e+00+0.j  4.93096709e-17+1.j ]]\n```\n\n### What should happen?\n\nThe two should give (approximately) equal results (though see #13305 for branch-cut problems).\n\n### Any suggestions?\n\nThe ability to do fractional matrix powers of `Operator` is relatively recent (#11534), and it seems to have pulled the code from `Gate.power`, which _was_ empowered to make that assumption.\n\nWe probably should just use `scipy.linalg.fractional_matrix_power` for `Operator.power`, and specialise `Gate.power` to use a more explicit eigenvector/eigenvalue form, since it's allowed to assume that its input was unitary.

Reference solution:
fix_patch: diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py\nindex 2f1c09668252..122db0973124 100644\n--- a/qiskit/circuit/gate.py\n+++ b/qiskit/circuit/gate.py\n@@ -89,7 +89,9 @@ def power(self, exponent: float, annotated: bool = False):\n         from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate\n \n         if not annotated:\n-            return UnitaryGate(Operator(self).power(exponent), label=f"{self.name}^{exponent}")\n+            return UnitaryGate(\n+                Operator(self).power(exponent, assume_unitary=True), label=f"{self.name}^{exponent}"\n+            )\n         else:\n             return AnnotatedOperation(self, PowerModifier(exponent))\n \ndiff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py\nindex 3b6ec8abff89..d549a130ae3a 100644\n--- a/qiskit/quantum_info/operators/operator.py\n+++ b/qiskit/quantum_info/operators/operator.py\n@@ -541,7 +541,9 @@ def compose(self, other: Operator, qargs: list | None = None, front: bool = Fals\n         ret._op_shape = new_shape\n         return ret\n \n-    def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator:\n+    def power(\n+        self, n: float, branch_cut_rotation=cmath.pi * 1e-12, assume_unitary=False\n+    ) -> Operator:\n         """Return the matrix power of the operator.\n \n         Non-integer powers of operators with an eigenvalue whose complex phase is :math:`\\\\pi` have\n@@ -572,6 +574,8 @@ def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator:\n                 complex plane.  This shifts the branch cut away from the common point of :math:`-1`,\n                 but can cause a different root to be selected as the principal root.  The rotation\n                 is anticlockwise, following the standard convention for complex phase.\n+            assume_unitary (bool): if ``True``, the operator is assumed to be unitary. In this case,\n+                for fractional powers we employ a faster implementation based on Schur's decomposition.\n \n         Returns:\n             Operator: the resulting operator ``O ** n``.\n@@ -579,6 +583,11 @@ def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator:\n         Raises:\n             QiskitError: if the input and output dimensions of the operator\n                          are not equal.\n+\n+        .. note::\n+            It is only safe to set the argument ``assume_unitary`` to ``True`` when the operator\n+            is unitary (or, more generally, normal). Otherwise, the function will return an\n+            incorrect output.\n         """\n         if self.input_dims() != self.output_dims():\n             raise QiskitError("Can only power with input_dims = output_dims.")\n@@ -588,11 +597,23 @@ def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator:\n         else:\n             import scipy.linalg\n \n-            ret._data = cmath.rect(\n-                1, branch_cut_rotation * n\n-            ) * scipy.linalg.fractional_matrix_power(\n-                cmath.rect(1, -branch_cut_rotation) * self.data, n\n-            )\n+            if assume_unitary:\n+                # Experimentally, for fractional powers this seems to be 3x faster than\n+                # calling scipy.linalg.fractional_matrix_power(self.data, exponent)\n+                decomposition, unitary = scipy.linalg.schur(\n+                    cmath.rect(1, -branch_cut_rotation) * self.data, output="complex"\n+                )\n+                decomposition_diagonal = decomposition.diagonal()\n+                decomposition_power = [pow(element, n) for element in decomposition_diagonal]\n+                unitary_power = unitary @ np.diag(decomposition_power) @ unitary.conj().T\n+                ret._data = cmath.rect(1, branch_cut_rotation * n) * unitary_power\n+            else:\n+                ret._data = cmath.rect(\n+                    1, branch_cut_rotation * n\n+                ) * scipy.linalg.fractional_matrix_power(\n+                    cmath.rect(1, -branch_cut_rotation) * self.data, n\n+                )\n+\n         return ret\n \n     def tensor(self, other: Operator) -> Operator:\ndiff --git a/releasenotes/notes/operator-power-assume-unitary-0a2f97ea9de91b49.yaml b/releasenotes/notes/operator-power-assume-unitary-0a2f97ea9de91b49.yaml\nnew file mode 100644\nindex 000000000000..780781c212a7\n--- /dev/null\n+++ b/releasenotes/notes/operator-power-assume-unitary-0a2f97ea9de91b49.yaml\n@@ -0,0 +1,6 @@\n+---\n+features_quantum_info:\n+  - |\n+    Added a new argument ``assume_unitary`` to :meth:`qiskit.quantum_info.Operator.power`.\n+    When ``True``, we use a faster method based on Schur's decomposition to raise an\n+    ``Operator`` to a fractional power.\n