Repository: quantumlib/Cirq

Issue:
MatrixGate: the name is not taken over during exponentiation
**Description of the issue**
When you create a matrix gate with a name and exponentiate it, the initial name is lost as the name is not handed over to the power gate: 
def __pow__(self, exponent: Any) -> 'MatrixGate':
        if not isinstance(exponent, (int, float)):
            return NotImplemented
        new_mat = linalg.map_eigenvalues(self._matrix, lambda b: b**exponent)
        return MatrixGate(new_mat, qid_shape=self._qid_shape)

**How to reproduce the issue**

Create a matrix gate with name and print a circuit containing a power of the gate

```
u = cirq.MatrixGate(np.identity(2), name = "U")
print(cirq.Circuit(u(cirq.LineQubit(1)),u(cirq.LineQubit(1))**2))

```



Discussion:
What would you expect the name to be? It probably shouldn't have the same name after exponentiating because it's not the same matrix. Could maybe construct a name like `"U**2"` in the example you gave, but that could lead to some strange edge cases, for example if you square again the name would end up as something like `"U**2**2"` which is ambiguous.

Another possibility is that you exponentiate the matrix manually and then construct separate gates with the names you want, e.g.

```python
Um = np.array(...)
U = cirq.MatrixGate(Um, name="U")
U2 = cirq.MatrixGate(Um @ Um, name="U^2")
```
Then use U and U2 as desired. One advantage of this is the matrix products are computed once when U2 is defined, rather than every time U is exponentiated, which could be many times in a large circuit.
To be honest, I would have expected something like "U^2". For me, it feels wrong that the name is lost. But I understand your point that potentizing several times can result in very long names.
We could perhaps add a method `MatrixGate.with_name` to create a new gate with the same matrix but a different name. Then the example I wrote above would be:
```python
U = cirq.MatrixGate(np.array(...), name="U")
U2 = (U ** 2).with_name("U^2")
```
If we could add that, that would be great and would meet my requirements.

Resolving patch:
diff --git a/cirq-core/cirq/ops/matrix_gates.py b/cirq-core/cirq/ops/matrix_gates.py
--- a/cirq-core/cirq/ops/matrix_gates.py
+++ b/cirq-core/cirq/ops/matrix_gates.py
@@ -55,6 +55,7 @@ def __init__(
         *,
         name: str = None,
         qid_shape: Optional[Iterable[int]] = None,
+        unitary_check: bool = True,
         unitary_check_rtol: float = 1e-5,
         unitary_check_atol: float = 1e-8,
     ) -> None:
@@ -66,6 +67,10 @@ def __init__(
             qid_shape: The shape of state tensor that the matrix applies to.
                 If not specified, this value is inferred by assuming that the
                 matrix is supposed to apply to qubits.
+            unitary_check: If True, check that the supplied matrix is unitary up to the
+                given tolerances. This should only be disabled if the matrix has already been
+                checked for unitarity, in which case we get a slight performance improvement by
+                not checking again.
             unitary_check_rtol: The relative tolerance for checking whether the supplied matrix
                 is unitary. See `cirq.is_unitary`.
             unitary_check_atol: The absolute tolerance for checking whether the supplied matrix
@@ -99,8 +104,14 @@ def __init__(
                 f'qid_shape: {self._qid_shape}\n'
             )
 
-        if not linalg.is_unitary(matrix, rtol=unitary_check_rtol, atol=unitary_check_atol):
-            raise ValueError(f'Not a unitary matrix: {self._matrix}')
+        if unitary_check and not linalg.is_unitary(
+            matrix, rtol=unitary_check_rtol, atol=unitary_check_atol
+        ):
+            raise ValueError(f'Not a unitary matrix: {matrix}')
+
+    def with_name(self, name: str) -> 'MatrixGate':
+        """Creates a new MatrixGate with the same matrix and a new name."""
+        return MatrixGate(self._matrix, name=name, qid_shape=self._qid_shape, unitary_check=False)
 
     def _json_dict_(self) -> Dict[str, Any]:
         return {'matrix': self._matrix.tolist(), 'qid_shape': self._qid_shape}


Test patch:
diff --git a/cirq-core/cirq/ops/matrix_gates_test.py b/cirq-core/cirq/ops/matrix_gates_test.py
--- a/cirq-core/cirq/ops/matrix_gates_test.py
+++ b/cirq-core/cirq/ops/matrix_gates_test.py
@@ -271,6 +271,16 @@ def test_named_two_qubit_diagram():
     assert expected_vertical == c.to_text_diagram(transpose=True).strip()
 
 
+def test_with_name():
+    gate = cirq.MatrixGate(cirq.unitary(cirq.Z**0.25))
+    T = gate.with_name('T')
+    S = (T**2).with_name('S')
+    assert T._name == 'T'
+    np.testing.assert_allclose(cirq.unitary(T), cirq.unitary(gate))
+    assert S._name == 'S'
+    np.testing.assert_allclose(cirq.unitary(S), cirq.unitary(T**2))
+
+
 def test_str_executes():
     assert '1' in str(cirq.MatrixGate(np.eye(2)))
     assert '0' in str(cirq.MatrixGate(np.eye(4)))