From a29d73fdd83ef792f95d189cae738b86fd161ece Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 18 Dec 2023 15:00:20 +0000 Subject: [PATCH 1/2] Ensure `TemplateOptimization` returns native-symbolic objects (#11107) `TemplateOptimization` currently uses Sympy internally to get access to solvers that have no equivalent in Symengine. Previously, it then did not convert the solutions to its equations back into Symengine format (if appropriate) before passing them on to `ParameterExpression`, which could lead to different assumptions about the type of the contained objects and bugs if `ParameterExpression` attempted to use Symengine-specific forms of methods on Sympy objects. --- .../template_substitution.py | 35 +++++++++++------- ...mplate-match-symbols-c00786155f101e39.yaml | 8 ++++ .../transpiler/test_template_matching.py | 37 +++++++++++++++++++ 3 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/template-match-symbols-c00786155f101e39.yaml diff --git a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py index b648cbaca944..06c5186d284c 100644 --- 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 = [] diff --git a/releasenotes/notes/template-match-symbols-c00786155f101e39.yaml b/releasenotes/notes/template-match-symbols-c00786155f101e39.yaml new file mode 100644 index 000000000000..a005879d22a2 --- /dev/null +++ b/releasenotes/notes/template-match-symbols-c00786155f101e39.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + The :class:`.TemplateOptimization` pass will now return parametric expressions using the native + symbolic expression format of :class:`.ParameterExpression`, rather than always using Sympy. + For most supported platforms, this means that the expressions will be Symengine objects. + Previously, the pass could return mismatched objects, which could lead to later failures in + parameter-handling code. diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index 015e19e4416d..9e461305397f 100644 --- 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. From 74661de856327425c07f5ba12c64436f29e46c39 Mon Sep 17 00:00:00 2001 From: R K Rupesh <33568503+rupeshknn@users.noreply.github.com> Date: Mon, 18 Dec 2023 21:13:31 +0530 Subject: [PATCH 2/2] add release note (#11395) --- .../providers/basicaer/unitary_simulator.py | 45 +++++++++---------- ...n-UnitarySimulatorPy-436c3a7c0dcc5eb6.yaml | 20 +++++++++ 2 files changed, 41 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/remove-qobj-input-in-UnitarySimulatorPy-436c3a7c0dcc5eb6.yaml diff --git a/qiskit/providers/basicaer/unitary_simulator.py b/qiskit/providers/basicaer/unitary_simulator.py index 16b1f69f09ce..de25f6bd83b1 100644 --- a/qiskit/providers/basicaer/unitary_simulator.py +++ b/qiskit/providers/basicaer/unitary_simulator.py @@ -17,11 +17,11 @@ .. code-block:: python - UnitarySimulator().run(qobj) + UnitarySimulator().run(run_input) -Where the input is a Qobj object and the output is a BasicAerJob object, which can -later be queried for the Result object. The result will contain a 'unitary' -data field, which is a 2**n x 2**n complex numpy array representing the +Where the input is a either Qobj object (deprecated) or QuantumCircuit or a list of circuits and +the output is a BasicAerJob object, which can later be queried for the Result object. The result +will contain a 'unitary' data field, which is a 2**n x 2**n complex numpy array representing the circuit's unitary matrix. """ import logging @@ -32,7 +32,6 @@ import numpy as np -from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.utils.multiprocessing import local_hardware_info from qiskit.providers.models import QasmBackendConfiguration from qiskit.providers.backend import BackendV1 @@ -203,11 +202,11 @@ def _get_unitary(self): unitary[abs(unitary) < self._chop_threshold] = 0.0 return unitary - def run(self, qobj, **backend_options): - """Run qobj asynchronously. + def run(self, run_input, **backend_options): + """Run experiments in run_input asynchronously. Args: - qobj (Qobj): payload of the experiment + run_input (Qobj, QuantumCircuit, list): payload of the experiment backend_options (dict): backend options Returns: @@ -222,7 +221,7 @@ def run(self, qobj, **backend_options): The "initial_unitary" option specifies a custom initial unitary matrix for the simulator to be used instead of the identity matrix. This size of this matrix must be correct for the number - of qubits inall experiments in the qobj. + of qubits in all experiments in the run_input. The "chop_threshold" option specifies a truncation value for setting small values to zero in the output unitary. The default @@ -238,21 +237,19 @@ def run(self, qobj, **backend_options): "chop_threshold": 1e-15 } """ - if isinstance(qobj, (QuantumCircuit, list)): - from qiskit.compiler import assemble - - out_options = {} - for key in backend_options: - if not hasattr(self.options, key): - warnings.warn( - "Option %s is not used by this backend" % key, UserWarning, stacklevel=2 - ) - else: - out_options[key] = backend_options[key] - qobj = assemble(qobj, self, **out_options) - qobj_options = qobj.config - else: - qobj_options = None + from qiskit.compiler import assemble + + out_options = {} + for key in backend_options: + if not hasattr(self.options, key): + warnings.warn( + "Option %s is not used by this backend" % key, UserWarning, stacklevel=2 + ) + else: + out_options[key] = backend_options[key] + qobj = assemble(run_input, self, **out_options) + qobj_options = qobj.config + self._set_options(qobj_config=qobj_options, backend_options=backend_options) job_id = str(uuid.uuid4()) job = BasicAerJob(self, job_id, self._run_job(job_id, qobj)) diff --git a/releasenotes/notes/remove-qobj-input-in-UnitarySimulatorPy-436c3a7c0dcc5eb6.yaml b/releasenotes/notes/remove-qobj-input-in-UnitarySimulatorPy-436c3a7c0dcc5eb6.yaml new file mode 100644 index 000000000000..a323af6935e8 --- /dev/null +++ b/releasenotes/notes/remove-qobj-input-in-UnitarySimulatorPy-436c3a7c0dcc5eb6.yaml @@ -0,0 +1,20 @@ +--- + +upgrade: + - | + It is no longer allowed to pass a :class:`~qiskit.qobj.QasmQobj` as the first input to + The :meth:`~qiskit.providers.basicaer.UnitarySimulatorPy.run`. This functionality was + deprecated in Qiskit 0.46 and has now been removed. + + Example usage: + + from qiskit import transpile, QuantumCircuit + from qiskit.providers.basicaer import BasicAer + + backend = BasicAer.get_backend('unitary_simulator') + circuit = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + tqc = transpile(circuit, backend) + result = backend.run(tqc).result()