Repository: Qiskit/qiskit

Issue:
Transpiler eliminates classical condition for optimization_level 3
### Information

- **Qiskit Terra version**:
- 0.17.0.dev0+83eb665
- **Python version**:
- 3.9.1 (default, Dec 11 2020, 06:28:49) [Clang 10.0.0 ]
- **Operating system**:
- Darwin

### What is the current behavior?
Circuits with feed-forward logic are incorrectly transpiled for optimization levels >0


### Steps to reproduce the problem
```python
from qiskit import execute, QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import transpile
from qiskit.transpiler import CouplingMap
from qiskit.converters import dag_to_circuit
circ = QuantumCircuit()
registers = [
            QuantumRegister(1,name='qr_data'),
            QuantumRegister(1,name='qr_anci'),
            ClassicalRegister(1,name='cr_data'),
            ClassicalRegister(1,name='cr_anci'),
            ]
for regs in registers:
    circ.add_register(regs)
circ.h(0)
circ.cx(0,1)
circ.measure(1,1)
circ.h(0).c_if(registers[3],1)
# make coupling maps
CMap = CouplingMap().from_ring(12)
CMap.add_physical_qubit(12)
CMap.add_edge(11,12)
CMap.add_edge(12,5)
CMap.make_symmetric()

def callback(**kwargs):
    print(kwargs['pass_'])
    print(dag_to_circuit(kwargs['dag']))

transpile_opts       =  {
                        'optimization_level': 3,
                        'callback': callback,
                        'coupling_map': CMap,
                        'basis_gates': ['cx', 'id', 'u1', 'u2', 'u3']
                        }
circ_transpiled = transpile(circ,**transpile_opts)
circ_transpiled.draw()
```


### What is the expected behavior?
The condition on the last h gate is preserved when transpiler optimization level 3 is run. Looking through the output of the callback function it is dropped by the consolidate blocks pass

### Suggested solutions
Examine consolidate blocks section of transpiler (identified by @mtreinish )


Discussion:
From a quick look through the transpiler the consolidate blocks pass is eliminating the condition on the gate in that circuit. This will only be an issue with optimization level 3 (or if you use the `synthesis` translation method) since we only run consolidate blocks by default in the optimization level 3 pass manager. (I've updated the title and issue to reflect this)
I looked into this briefly and I'm _fairly_ sure this isn't a `ConsolidateBlocks`/`Collect2qBlocks` issue (even though those are the passes where the issue becomes apparent). Looking into the `PropertySet` in between `Collect2qBlocks` and `ConsolidateBlocks`, the conditional gate (correctly) isn't being collected into a block by `Collect2qBlocks`, and then is (correctly) being re-inserted in to the DAG in place by `ConsolidateBlocks`.

However, when re-inserting `DAGNode`s that _weren't_ collected into a block, `ConsolidateBlocks` calls `dag.apply_operation_back` which looks at `node.op.condition` to know if the instruction to be inserted should be classically conditioned. At this point, for the conditional `U2Gate`, `node.condition` is `(ClassicalRegister(1, 'cr_anci'), 1)` but `node.op.condition` is `None`. Likely one of the previous passes (probably the `BasisTranslator` via `DAGCircuit.substitute_node`) incorrectly replaced the instruction without carrying over the conditional information.

Steps to remediate:
1. Confirm that updating `DAGNode.op.condition` prior to `ConsolidateBlocks` resolves this bug.
2. `DAGNode.condition` should be either removed, or made a simple `@property` fetching `op.condition`. (The deprecated `condition` kwarg on `DAGNode.__init__` can be removed as well).
3. `DAGCircuit.substitute_node` should be updated to propagate the `condition` attribute from the to-be-replaced `op` to the new `op`. This may also be a good time to examine if `substitute_node` is still needed or should be removed, given that it has proven difficult to use as compared to `substitute_node_with_dag` ( see discussion in #5002, #5225, and https://github.com/Qiskit/qiskit-terra/pull/4915#discussion_r481152756 ) and the performance benefits it brings may no longer be meaningful for retworkx.
@kdk. I did a quick dive into this and I think there is more to it. It's true that `substitute_node `should be propagating the condition to the new node, but just doing that does not solve it.

The problem is in `BasisTranslator `and if you force set the condition in there, then both of the `u2 `gates get the same condition. In the above circuit, in this code,
https://github.com/Qiskit/qiskit-terra/blob/f3e2b7a5fdd6789f7b3ce8bd15de28e5561d0378/qiskit/transpiler/passes/basis/basis_translator.py#L155-L159
the `u2 `loaded into `dag_op `is repeated with the same instance of the `u2`. Therefore you will either get both `u2`'s with conditions or neither. A `copy.copy` when assigning `dag_op` along with adding the condition to `dag_op `solves the problem.

In regards to `node.condition`, I like your idea of doing an `@property`. There are about 20 references in the code to `node.condition`, though there might be many more with a name other than `node`. So eliminating `node.condition` could require a lot of code changes.

And though there might be other issues with `substitute_node`, I don't see a reason based on this example for removal.

If you'd like me to make the changes to load the condition in `substitute_node `and add the `@property` and `copy.copy`, I can do that.

Resolving patch:
diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py
--- a/qiskit/dagcircuit/dagcircuit.py
+++ b/qiskit/dagcircuit/dagcircuit.py
@@ -1112,13 +1112,17 @@ def substitute_node(self, node, op, inplace=False):
                     op.num_qubits, op.num_clbits))
 
         if inplace:
+            save_condition = node.op.condition
             node.op = op
             node.name = op.name
+            node.op.condition = save_condition
             return node
 
         new_node = copy.copy(node)
+        save_condition = new_node.op.condition
         new_node.op = op
         new_node.name = op.name
+        new_node.op.condition = save_condition
         self._multi_graph[node._node_id] = new_node
         return new_node
 
diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py
--- a/qiskit/dagcircuit/dagnode.py
+++ b/qiskit/dagcircuit/dagnode.py
@@ -26,23 +26,16 @@ class DAGNode:
     be supplied to functions that take a node.
     """
 
-    __slots__ = ['type', '_op', 'name', '_qargs', 'cargs', 'condition', '_wire',
-                 'sort_key', '_node_id']
+    __slots__ = ['type', '_op', 'name', '_qargs', 'cargs', '_wire', 'sort_key', '_node_id']
 
     def __init__(self, type=None, op=None, name=None, qargs=None, cargs=None,
-                 condition=None, wire=None, nid=-1):
+                 wire=None, nid=-1):
         """Create a node """
         self.type = type
         self._op = op
         self.name = name
         self._qargs = qargs if qargs is not None else []
         self.cargs = cargs if cargs is not None else []
-        if condition:
-            warnings.warn("Use of condition arg is deprecated, set condition in instruction.",
-                          DeprecationWarning)
-        if self._op:
-            self._op.condition = condition if self._op.condition is None else self._op.condition
-        self.condition = self._op.condition if self._op is not None else None
         self._wire = wire
         self._node_id = nid
         self.sort_key = str(self._qargs)
@@ -58,6 +51,20 @@ def op(self):
     def op(self, data):
         self._op = data
 
+    @property
+    def condition(self):
+        """Returns the condition of the node.op"""
+        if not self.type or self.type != 'op':
+            raise QiskitError("The node %s is not an op node" % (str(self)))
+        return self._op.condition
+
+    @condition.setter
+    def condition(self, new_condition):
+        """Sets the node.condition which sets the node.op.condition."""
+        if not self.type or self.type != 'op':
+            raise QiskitError("The node %s is not an op node" % (str(self)))
+        self._op.condition = new_condition
+
     @property
     def qargs(self):
         """
@@ -133,16 +140,17 @@ def semantic_eq(node1, node2, bit_indices1=None, bit_indices2=None):
         if 'barrier' == node1.name == node2.name:
             return set(node1_qargs) == set(node2_qargs)
 
-        result = False
         if node1.type == node2.type:
             if node1._op == node2._op:
                 if node1.name == node2.name:
                     if node1_qargs == node2_qargs:
                         if node1_cargs == node2_cargs:
-                            if node1.condition == node2.condition:
-                                if (
-                                        bit_indices1.get(node1._wire, None)
-                                        == bit_indices2.get(node2._wire, None)
-                                ):
-                                    result = True
-        return result
+                            if node1.type == 'op':
+                                if node1._op.condition != node2._op.condition:
+                                    return False
+                            if (
+                                bit_indices1.get(node1._wire, None)
+                                == bit_indices2.get(node2._wire, None)
+                            ):
+                                return True
+        return False
diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py
--- a/qiskit/transpiler/passes/basis/basis_translator.py
+++ b/qiskit/transpiler/passes/basis/basis_translator.py
@@ -155,6 +155,10 @@ def run(self, dag):
                 if (len(bound_target_dag.op_nodes()) == 1
                         and len(bound_target_dag.op_nodes()[0].qargs) == len(node.qargs)):
                     dag_op = bound_target_dag.op_nodes()[0].op
+                    # dag_op may be the same instance as other ops in the dag,
+                    # so if there is a condition, need to copy
+                    if node.op.condition:
+                        dag_op = dag_op.copy()
                     dag.substitute_node(node, dag_op, inplace=True)
 
                     if bound_target_dag.global_phase:


Test patch:
diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py
--- a/test/python/transpiler/test_basis_translator.py
+++ b/test/python/transpiler/test_basis_translator.py
@@ -17,6 +17,7 @@
 from numpy import pi
 
 from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
+from qiskit import transpile
 from qiskit.test import QiskitTestCase
 from qiskit.circuit import Gate, Parameter, EquivalenceLibrary
 from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, CU1Gate, CU3Gate
@@ -777,3 +778,23 @@ def test_global_phase(self):
         self.assertEqual(out_dag, expected_dag)
         self.assertEqual(float(out_dag.global_phase), float(expected_dag.global_phase))
         self.assertEqual(Operator(dag_to_circuit(out_dag)), Operator(expected))
+
+    def test_condition_set_substitute_node(self):
+        """Verify condition is set in BasisTranslator on substitute_node"""
+        qr = QuantumRegister(2, 'q')
+        cr = ClassicalRegister(2, 'c')
+        circ = QuantumCircuit(qr, cr)
+        circ.h(0)
+        circ.cx(0, 1)
+        circ.measure(1, 1)
+        circ.h(0).c_if(cr, 1)
+        circ_transpiled = transpile(circ, optimization_level=3,
+                                    basis_gates=['cx', 'id', 'u1', 'u2', 'u3'])
+        qr = QuantumRegister(2, 'q')
+        cr = ClassicalRegister(2, 'c')
+        expected = QuantumCircuit(qr, cr)
+        expected.u2(0, pi, 0)
+        expected.cx(0, 1)
+        expected.measure(1, 1)
+        expected.u2(0, pi, 0).c_if(cr, 1)
+        self.assertEqual(circ_transpiled, expected)