Repository: Qiskit/qiskit

Issue:
Template substitution with parameters can result in unbound templates
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: on commit 287dc80e17ad302be1fb9d79150e35cba131d083
- **Python version**: 3.7.9
- **Operating system**: Ubuntu

### What is the current behavior?
Template substitution where templates have parameters sometimes causes errors. When parameters are present the template substitution first performs a soft match and then solves a system of equations in `_attempt_bind` to determine if the match is valid. When the whole template is matched this results in a temporary template where all parameters have been bound. For incomplete matches occurring before `_attempt_bind` the temporary template may still have parameters in it. This causes the template substitution to crash as we cannot invert circuits with parameters.

### Steps to reproduce the problem
Perform template matching with a template with parameters that does not result in a complete match.

### What is the expected behavior?
The code should not crash and furthermore should not use temporary templates after `_attempt_bind` that still have parameters.

### Suggested solutions
Augment the already existing function `_remove_impossible` to exclude the temporary templates produced by `_attempt_bind` that have parameters.



Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py
--- a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py
+++ b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py
@@ -38,10 +38,19 @@ def __init__(self, circuit_config, template_config, pred_block,
         self.clbit_config = clbit_config if clbit_config is not None else []
         self.pred_block = pred_block
 
+    def has_parameters(self):
+        """Ensure that the template does not have parameters."""
+        for node in self.template_dag_dep.get_nodes():
+            for param in node.op.params:
+                if isinstance(param, ParameterExpression):
+                    return True
+
+        return False
+
 
 class TemplateSubstitution:
     """
-    Class to run the subsitution algorithm from the list of maximal matches.
+    Class to run the substitution algorithm from the list of maximal matches.
     """
 
     def __init__(self, max_matches, circuit_dag_dep, template_dag_dep):
@@ -213,6 +222,11 @@ def _remove_impossible(self):
         list_predecessors = []
         remove_list = []
 
+        # First remove any scenarios that have parameters in the template.
+        for scenario in self.substitution_list:
+            if scenario.has_parameters():
+                remove_list.append(scenario)
+
         # Initialize predecessors for each group of matches.
         for scenario in self.substitution_list:
             predecessors = set()


Test patch:
diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py
--- a/test/python/transpiler/test_template_matching.py
+++ b/test/python/transpiler/test_template_matching.py
@@ -299,6 +299,46 @@ def count_cx(qc):
         np.testing.assert_almost_equal(Operator(circuit_in).data,
                                        Operator(circuit_out).data)
 
+    def test_unbound_parameters(self):
+        """
+        Test that partial matches with parameters will not raise errors.
+        This tests that if parameters are still in the temporary template after
+        _attempt_bind then they will not be used.
+        """
+        class PhaseSwap(Gate):
+            """CZ gates used for the test."""
+
+            def __init__(self, num_qubits, params):
+                super().__init__('p', num_qubits, params)
+
+            def inverse(self):
+                inverse = UnitaryGate(
+                    np.diag(
+                        [1.0, 1.0, np.exp(-1.0j * self.params[0]), np.exp(-1.0j * self.params[0])]))
+                inverse.name = 'p'
+                return inverse
+
+        def template():
+            beta = Parameter('β')
+            qc = QuantumCircuit(2)
+            qc.cx(1, 0)
+            qc.cx(1, 0)
+            qc.p(beta, 1)
+            qc.append(PhaseSwap(2, [beta]), [0, 1])
+
+            return qc
+
+        circuit_in = QuantumCircuit(2)
+        circuit_in.cx(1, 0)
+        circuit_in.cx(1, 0)
+
+        pass_ = TemplateOptimization(template_list=[template()])
+        circuit_out = PassManager(pass_).run(circuit_in)
+
+        # This template will not fully match as long as gates with parameters do not
+        # commute with any other gates in the DAG dependency.
+        self.assertEqual(circuit_out.count_ops().get('cx', 0), 2)
+
 
 if __name__ == '__main__':
     unittest.main()