Repository: quantumlib/Cirq

Issue:
BooleanHamiltonian.with_qubits is broken
**Description of the issue**
`with_qubits` is meant to return a new operation with its (ordered) qubits substituted with a new list of qubits.  `BooleanHamiltonian.with_qubits` casts its argument to `NamedQubit` and otherwise fails (throws `AttributeError`).

`BooleanHamiltonian.with_qubits` also assumes the names of the named qubits correspond to the names of the boolean variables.  This won't be the case in general, especially if the user is constructing a larger circuit.

**How to reproduce the issue**
1. `with_qubits(*non_named_qubits)`:
    ```python
    q0, q1, q2 = cirq.LineQubit.range(3)
    op = cirq.BooleanHamiltonian(
            {'a': q0, 'b': q1, 'c': q2},
            ['a|b|c', 'a^b', 'b^c'],
            0.1,
        )
    op.with_qubits(*cirq.GridQubit.rect(1, 3))  # Throws Attribute Error
    ```
2. `with_qubits(mismatched_named_qubits)`:
    ```python
    q0, q1, q2 = cirq.LineQubit.range(3)
    op = cirq.BooleanHamiltonian(
            {'a': q0, 'b': q1, 'c': q2},
            ['a|b|c', 'a^b', 'b^c'],
            0.1,
        )
    
    # with_qubits doesn't fail but now the variable names in qubit_map don't match
    # the boolean formulas and raise `KeyError` on decompose.
    op2 = op.with_qubits(*cirq.NamedQubit.range(3, prefix='x'))  # Succeeds, but invalid op
    cirq.Circuit(cirq.decompose_once(op2))  # Throws KeyError
    ```

**Cirq version**
0.12.0.dev (after #4309)



Discussion:
Related to this, I would expect this identity to hold for any operation type but `BooleanHamiltonian` does not:
```python
op.with_qubits(*op.qubits) == op
```
Thanks for opening this @cduck! 
@tonybruguier can you have a look at this please?
Ack

Resolving patch:
diff --git a/cirq-core/cirq/ops/boolean_hamiltonian.py b/cirq-core/cirq/ops/boolean_hamiltonian.py
--- a/cirq-core/cirq/ops/boolean_hamiltonian.py
+++ b/cirq-core/cirq/ops/boolean_hamiltonian.py
@@ -21,7 +21,7 @@
 [3] https://github.com/rsln-s/IEEE_QW_2020/blob/master/Slides.pdf
 """
 
-from typing import cast, Any, Dict, Generator, List, Sequence, Tuple
+from typing import Any, Dict, Generator, List, Sequence, Tuple
 
 import sympy.parsing.sympy_parser as sympy_parser
 
@@ -66,8 +66,14 @@ def __init__(
         self._theta: float = theta
 
     def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'BooleanHamiltonian':
+        if len(self._qubit_map) != len(new_qubits):
+            raise ValueError('Length of replacement qubits must be the same')
+        new_qubit_map = {
+            variable_name: new_qubit
+            for variable_name, new_qubit in zip(self._qubit_map, new_qubits)
+        }
         return BooleanHamiltonian(
-            {cast(cirq.NamedQubit, q).name: q for q in new_qubits},
+            new_qubit_map,
             self._boolean_strs,
             self._theta,
         )


Test patch:
diff --git a/cirq-core/cirq/ops/boolean_hamiltonian_test.py b/cirq-core/cirq/ops/boolean_hamiltonian_test.py
--- a/cirq-core/cirq/ops/boolean_hamiltonian_test.py
+++ b/cirq-core/cirq/ops/boolean_hamiltonian_test.py
@@ -72,7 +72,6 @@ def test_circuit(boolean_str):
     hamiltonian_gate = cirq.BooleanHamiltonian(
         {q.name: q for q in qubits}, [boolean_str], 0.1 * math.pi
     )
-    assert hamiltonian_gate.with_qubits(*qubits) == hamiltonian_gate
 
     assert hamiltonian_gate.num_qubits() == n
 
@@ -83,3 +82,19 @@ def test_circuit(boolean_str):
 
     # Compare the two:
     np.testing.assert_array_equal(actual, expected)
+
+
+def test_with_custom_names():
+    q0, q1, q2, q3 = cirq.LineQubit.range(4)
+    original_op = cirq.BooleanHamiltonian(
+        {'a': q0, 'b': q1},
+        ['a'],
+        0.1,
+    )
+    assert cirq.decompose(original_op) == [cirq.Rz(rads=-0.05).on(q0)]
+
+    renamed_op = original_op.with_qubits(q2, q3)
+    assert cirq.decompose(renamed_op) == [cirq.Rz(rads=-0.05).on(q2)]
+
+    with pytest.raises(ValueError, match='Length of replacement qubits must be the same'):
+        original_op.with_qubits(q2)