Repository: Qiskit/qiskit

Issue:
`TemplateOptimization` creates `ParameterExpression`s with Sympy types when Symengine is available
### Environment

- **Qiskit Terra version**: 0.45.0rc1
- **Python version**: 3.10
- **Operating system**: macOS


### What is happening?

The `TemplateOptimization` pass can return circuits that contain Sympy-backed `ParameterExpression`s even when Symengine is installed and available.  This doesn't _immediately_ seem to pose any troubles, mostly because Symengine can work with Sympy objects and converts them internally, Symengine is supposed to be used if available and `ParameterExpression` assumes that its types are all Symengine types if it is available.  This can lead to problems when it wants to call Symengine functions that do not have the same arguments / names as their Sympy equivalents.

### How can we reproduce the issue?

```python
# Not used, but this example doesn't make sense if you don't have symengine installed.
import symengine

from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.transpiler.passes import TemplateOptimization

a, b = Parameter("a"), Parameter("b")

template = QuantumCircuit(1)
template.p(a, 0)
template.p(-a, 0)
template.rz(a, 0)
template.rz(-a, 0)

circuit = QuantumCircuit(1)
circuit.p(-b, 0)
circuit.p(b, 0)

pass_ = TemplateOptimization(template_list=[template], user_cost_dict={"p": 100, "rz": 1})
out = pass_(circuit)
type(out.data[1].operation.params[0]._symbol_expr)
```

The output type is some Sympy type, but it should be a Symengine one.

### What should happen?

Template optimisation uses sympy internally in order to use `sympy.solve`, but does not necessarily convert its objects back to the native form before creating `ParameterExpression` objects.  It should arrange for these objects to be cast back to Symengine ones, if it's playing with `ParameterExpression` at a low level.

### Any suggestions?

Tbh, the template optimisation code probably doesn't actually need sympy here at all - if we instead restricted the templates to say that parameters can only be used in linearly (templates are just simple), we'd be able to roll our own basic linear simultaneous-equation solver and skip the heavy sympy import and use.

This code isn't used much though, and it could do with a fairly serious refactor, so that's a very low priority.


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
@@ -22,6 +22,7 @@
 from qiskit.dagcircuit.dagcircuit import DAGCircuit
 from qiskit.dagcircuit.dagdependency import DAGDependency
 from qiskit.converters.dagdependency_to_dag import dagdependency_to_dag
+from qiskit.utils import optionals as _optionals
 
 
 class SubstitutionConfig:
@@ -496,6 +497,15 @@ def _attempt_bind(self, template_sublist, circuit_sublist):
         import sympy as sym
         from sympy.parsing.sympy_parser import parse_expr
 
+        if _optionals.HAS_SYMENGINE:
+            import symengine
+
+            # Converts Sympy expressions to Symengine ones.
+            to_native_symbolic = symengine.sympify
+        else:
+            # Our native form is sympy, so we don't need to do anything.
+            to_native_symbolic = lambda x: x
+
         circuit_params, template_params = [], []
         # Set of all parameter names that are present in the circuits to be optimised.
         circuit_params_set = set()
@@ -555,7 +565,7 @@ def dummy_parameter():
             node.op.params = sub_node_params
 
         # Create the fake binding dict and check
-        equations, circ_dict, temp_symbols, sol, fake_bind = [], {}, {}, {}, {}
+        equations, circ_dict, temp_symbols = [], {}, {}
         for circuit_param, template_param in zip(circuit_params, template_params):
             if isinstance(template_param, ParameterExpression):
                 if isinstance(circuit_param, ParameterExpression):
@@ -577,19 +587,18 @@ def dummy_parameter():
         if not temp_symbols:
             return template_dag_dep
 
-        # Check compatibility by solving the resulting equation
-        sym_sol = sym.solve(equations, set(temp_symbols.values()))
-        for key in sym_sol:
-            try:
-                sol[str(key)] = ParameterExpression(circ_dict, sym_sol[key])
-            except TypeError:
-                return None
-
-        if not sol:
+        # Check compatibility by solving the resulting equation.  `dict=True` (surprisingly) forces
+        # the output to always be a list, even if there's exactly one solution.
+        sym_sol = sym.solve(equations, set(temp_symbols.values()), dict=True)
+        if not sym_sol:
+            # No solutions.
             return None
-
-        for key in temp_symbols:
-            fake_bind[key] = sol[str(key)]
+        # If there's multiple solutions, arbitrarily pick the first one.
+        sol = {
+            param.name: ParameterExpression(circ_dict, to_native_symbolic(expr))
+            for param, expr in sym_sol[0].items()
+        }
+        fake_bind = {key: sol[key.name] for key in temp_symbols}
 
         for node in template_dag_dep.get_nodes():
             bound_params = []


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
@@ -344,6 +344,43 @@ def count_cx(qc):
         # however these are equivalent if the operators are the same
         self.assertTrue(Operator(circuit_in).equiv(circuit_out))
 
+    def test_output_symbolic_library_equal(self):
+        """Test that the template matcher returns parameter expressions that use the same symbolic
+        library as the default; it should not coerce everything to Sympy when playing with the
+        `ParameterExpression` internals."""
+
+        a, b = Parameter("a"), Parameter("b")
+
+        template = QuantumCircuit(1)
+        template.p(a, 0)
+        template.p(-a, 0)
+        template.rz(a, 0)
+        template.rz(-a, 0)
+
+        circuit = QuantumCircuit(1)
+        circuit.p(-b, 0)
+        circuit.p(b, 0)
+
+        pass_ = TemplateOptimization(template_list=[template], user_cost_dict={"p": 100, "rz": 1})
+        out = pass_(circuit)
+
+        expected = QuantumCircuit(1)
+        expected.rz(-b, 0)
+        expected.rz(b, 0)
+        self.assertEqual(out, expected)
+
+        def symbolic_library(expr):
+            """Get the symbolic library of the expression - 'sympy' or 'symengine'."""
+            return type(expr._symbol_expr).__module__.split(".")[0]
+
+        out_exprs = [expr for instruction in out.data for expr in instruction.operation.params]
+        self.assertEqual(
+            [symbolic_library(b)] * len(out_exprs), [symbolic_library(expr) for expr in out_exprs]
+        )
+
+        # Assert that the result still works with parametric assignment.
+        self.assertEqual(out.assign_parameters({b: 1.5}), expected.assign_parameters({b: 1.5}))
+
     def test_optimizer_does_not_replace_unbound_partial_match(self):
         """
         Test that partial matches with parameters will not raise errors.