Skip to content

Commit e8c5c94

Browse files
nbronnrafal-prachtjakelishmanmergify[bot]
authored
Allow ParameterExpression values in TemplateOptimization pass (Qiskit#6899)
* added new equivalence rzx_zz4 * added another zz-ish template (looks for basis gates) * looking at adding additions to rzx templates * fixes parsing of Parameter name for sympy during template optimization * generalized fix for multiple parameters in an expression and constant parameters, removed some excess files from another branch * one more minor fix to bring inline with qiskit-terra main * trying to fix parameter parsing for template optimization * made sure floats were added correctly to (template) parameters * got template matching to work with floats, sympy still not understanding Parameters either in equations or symbols set in template_substitution.py * further modifications to accept circuit parameters for template optimization * debugging the binding of template parameters * realized I was not treating ParameterExpressions carefully and should lean on parse_expr from sympy to cast to symbols * converted all bindings to ParameterExpressions (via .assign()), set trace to fix template use with Parameters * cleaned up _attempt_bind routine * exploring overriding removing the matched scenarios with parameters * introduced a total hack for dealing with ParamterExpressions that contain floats for RXGates * (non-latex) parameters now currently working in template optimization transpilig step followed by parameter binding * cleaned up some whitespace and removed commented-out lines * cleaned up some tox/lint errors * removed unneccessary Parameter import * bypassed unit test test_unbound_parameters() and re-tox/lint * fixed one last linting issue * fixed cyclic import error * modified calibration_creator to accept a ParameterExpression containing a float * fixed an mismatch when trying to add calibrations and addressed a conversation in the PR * last tox/lint checks i'm sure ;) * now params comes from node_op argument * handling error in case gate parameters is empty in dagcircuit.py * Fix template matching for parameters with LaTeX name. * added missing docstring * removed pdb set_trace * made changes requested in PR 6899 * made changes requested in PR 6899 #2 * remembered to tighten try/except handling * finished making changes requested in PR 6899 * fixed remaining linting issue * added comment about templates working for parameterized RZXGates and RXGates * Fix test unbound parameters * Check if matrix with Parameter is unitary * Fix merge issue * removed real=True in two symbol Symbol expressions, which was messing up the solver for some reason. * generalized to iterate over parameters, and removed reference to private member * modified .get_sympy_expr() to use symengine if possible * made the negation of the RXGate() much less verbose * working thru tox/lint checks * added unit test test_unbound_parameters_in_rzx_templates to confirm template optimization handles template optimization with unbound parameters correctly * Fix unbund parameters test * fixed issue with adding calibrations without params * Add real=True to symbols * fixed linting issue * Fix for symengine * simplified the parameter handling for adding calibrations to gates * added a check for unitary on an arbitrary float in the case symengine is imported (and fixed a couple minor bugs) * Parammeter can be complex * fixed tox/lint issues * removed one more imposition of real parameters * one last linting issue * modified release notes * fixed some transpiler library imports that were out of date * added sphinx referencing to release notes and print statement for the case of testing unbound parameters when creating a unitary gate * fixed some tox/lint issues * Fix review issues * fixing last tox/lint issues * added release notes and fixed tox/lint issues * added method in template_substitution to compare the number of parameters in circuit with the that of the template that would potentially replace it * fixing up some template matching unit tests * fixed up template matching unit tests to remove calls to UnitaryGate * Update qiskit/dagcircuit/dagcircuit.py Co-authored-by: Jake Lishman <[email protected]> * Update qiskit/extensions/unitary.py Co-authored-by: Jake Lishman <[email protected]> * Update qiskit/extensions/unitary.py Co-authored-by: Jake Lishman <[email protected]> * added template test with two parameters and new logic in the case there are duplicate parameters in the circuit and template * added two-parameter unit test and a check for overlapping parameters between circuits and templates * remove ParameterTypeeException from exceptions.py * Restore lazy symengine imports Use of `_optionals.HAS_SYMENGINE` is intended to be within run-time locations; putting it at the top level of a file causes this fairly heavy library to be imported at runtime, slowing down `import qiskit` for those who won't use that functionality. * Rename to_sympify_expression to sympify * Revert now-unnecessary changes to calibration builder * Fixup release note * Add explicit tests of template matching pass This adds several tests of the exact form produced by running the template-matching transpiler pass, including those with purely numeric circuits and those with symbolic parameters. * Fix template parameter substitution This fixes issues introduced recently in the PR that caused parameters to be incorrectly bound in the result. This meant that the actual numbers in the produced circuits were incorrect. This happened mostly by tracking data structures being updated at the wrong levels within loops. In addition, this commit also updates some data structures to more robust and efficient versions: - Testing whether a parameter has a clash is best done by constructing a set of parameters used in the input circuits, then testing directly on this, rather than stringifying expressions and using subsearch matches; this avoids problems if two parameters have contained names, or if more than one match is catenated into a single string. - Using a dictionary with a missing-element constructor to build the replacement parameters allows the looping logic to be simpler; the "build missing element" logic can be separated out to happen automatically. * Fix overlooked documentation comment * Remove qasm.pi import in favour of numpy * Add tests of multi-parameter instructions * Fix template matching with multiparameter expressions * Silence overzealous pylint warning Co-authored-by: Rafał Pracht <[email protected]> Co-authored-by: Rafał Pracht <[email protected]> Co-authored-by: Jake Lishman <[email protected]> Co-authored-by: Jake Lishman <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 0f544a7 commit e8c5c94

File tree

4 files changed

+539
-89
lines changed

4 files changed

+539
-89
lines changed

qiskit/circuit/parameterexpression.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from qiskit.circuit.exceptions import CircuitError
2323
from qiskit.utils import optionals as _optionals
2424

25-
2625
# This type is redefined at the bottom to insert the full reference to "ParameterExpression", so it
2726
# can safely be used by runtime type-checkers like Sphinx. Mypy does not need this because it
2827
# handles the references by static analysis.
@@ -522,6 +521,20 @@ def is_real(self):
522521
return False
523522
return True
524523

524+
def sympify(self):
525+
"""Return symbolic expression as a raw Sympy or Symengine object.
526+
527+
Symengine is used preferentially; if both are available, the result will always be a
528+
``symengine`` object. Symengine is a separate library but has integration with Sympy.
529+
530+
.. note::
531+
532+
This is for interoperability only. Qiskit will not accept or work with raw Sympy or
533+
Symegine expressions in its parameters, because they do not contain the tracking
534+
information used in circuit-parameter binding and assignment.
535+
"""
536+
return self._symbol_expr
537+
525538

526539
# Redefine the type so external imports get an evaluated reference; Sphinx needs this to understand
527540
# the type hints.

qiskit/transpiler/passes/optimization/template_matching/template_substitution.py

Lines changed: 108 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
Template matching substitution, given a list of maximal matches it substitutes
1515
them in circuit and creates a new optimized dag version of the circuit.
1616
"""
17+
import collections
1718
import copy
19+
import itertools
1820

19-
from qiskit.circuit import ParameterExpression
21+
from qiskit.circuit import Parameter, ParameterExpression
2022
from qiskit.dagcircuit.dagcircuit import DAGCircuit
2123
from qiskit.dagcircuit.dagdependency import DAGDependency
2224
from qiskit.converters.dagdependency_to_dag import dagdependency_to_dag
@@ -175,7 +177,6 @@ def _rules(self, circuit_sublist, template_sublist, template_complement):
175177
Returns:
176178
bool: True if the match respects the given rule for replacement, False otherwise.
177179
"""
178-
179180
if self._quantum_cost(template_sublist, template_complement):
180181
for elem in circuit_sublist:
181182
for config in self.substitution_list:
@@ -269,11 +270,6 @@ def _remove_impossible(self):
269270
list_predecessors = []
270271
remove_list = []
271272

272-
# First remove any scenarios that have parameters in the template.
273-
for scenario in self.substitution_list:
274-
if scenario.has_parameters():
275-
remove_list.append(scenario)
276-
277273
# Initialize predecessors for each group of matches.
278274
for scenario in self.substitution_list:
279275
predecessors = set()
@@ -324,8 +320,7 @@ def _substitution(self):
324320

325321
# Fake bind any parameters in the template
326322
template = self._attempt_bind(template_sublist, circuit_sublist)
327-
328-
if template is None:
323+
if template is None or self._incr_num_parameters(template):
329324
continue
330325

331326
template_list = range(0, self.template_dag_dep.size())
@@ -432,7 +427,6 @@ def run_dag_opt(self):
432427
cargs = []
433428
node = group.template_dag_dep.get_node(index)
434429
inst = node.op.copy()
435-
436430
dag_dep_opt.add_op_node(inst.inverse(), qargs, cargs)
437431

438432
# Add the unmatched gates.
@@ -486,6 +480,11 @@ def _attempt_bind(self, template_sublist, circuit_sublist):
486480
solution is found then the match is valid and the parameters
487481
are assigned. If not, None is returned.
488482
483+
In order to resolve the conflict of the same parameter names in the
484+
circuit and template, each variable in the template sublist is
485+
re-assigned to a new dummy parameter with a completely separate name
486+
if it clashes with one that exists in an input circuit.
487+
489488
Args:
490489
template_sublist (list): part of the matched template.
491490
circuit_sublist (list): part of the matched circuit.
@@ -499,51 +498,127 @@ def _attempt_bind(self, template_sublist, circuit_sublist):
499498
from sympy.parsing.sympy_parser import parse_expr
500499

501500
circuit_params, template_params = [], []
501+
# Set of all parameter names that are present in the circuits to be optimised.
502+
circuit_params_set = set()
502503

503504
template_dag_dep = copy.deepcopy(self.template_dag_dep)
504505

505-
for idx, t_idx in enumerate(template_sublist):
506+
# add parameters from circuit to circuit_params
507+
for idx, _ in enumerate(template_sublist):
506508
qc_idx = circuit_sublist[idx]
507-
circuit_params += self.circuit_dag_dep.get_node(qc_idx).op.params
508-
template_params += template_dag_dep.get_node(t_idx).op.params
509+
parameters = self.circuit_dag_dep.get_node(qc_idx).op.params
510+
circuit_params += parameters
511+
for parameter in parameters:
512+
if isinstance(parameter, ParameterExpression):
513+
circuit_params_set.update(x.name for x in parameter.parameters)
514+
515+
_dummy_counter = itertools.count()
516+
517+
def dummy_parameter():
518+
# Strictly not _guaranteed_ to avoid naming clashes, but if someone's calling their
519+
# parameters this then that's their own fault.
520+
return Parameter(f"_qiskit_template_dummy_{next(_dummy_counter)}")
521+
522+
# Substitutions for parameters that have clashing names between the input circuits and the
523+
# defined templates.
524+
template_clash_substitutions = collections.defaultdict(dummy_parameter)
525+
526+
# add parameters from template to template_params, replacing parameters with names that
527+
# clash with those in the circuit.
528+
for t_idx in template_sublist:
529+
node = template_dag_dep.get_node(t_idx)
530+
sub_node_params = []
531+
for t_param_exp in node.op.params:
532+
if isinstance(t_param_exp, ParameterExpression):
533+
for t_param in t_param_exp.parameters:
534+
if t_param.name in circuit_params_set:
535+
new_param = template_clash_substitutions[t_param.name]
536+
t_param_exp = t_param_exp.assign(t_param, new_param)
537+
sub_node_params.append(t_param_exp)
538+
template_params.append(t_param_exp)
539+
node.op.params = sub_node_params
540+
541+
for node in template_dag_dep.get_nodes():
542+
sub_node_params = []
543+
for param_exp in node.op.params:
544+
if isinstance(param_exp, ParameterExpression):
545+
for param in param_exp.parameters:
546+
if param.name in template_clash_substitutions:
547+
param_exp = param_exp.assign(
548+
param, template_clash_substitutions[param.name]
549+
)
550+
sub_node_params.append(param_exp)
551+
552+
node.op.params = sub_node_params
509553

510554
# Create the fake binding dict and check
511-
equations, symbols, sol, fake_bind = [], set(), {}, {}
512-
for t_idx, params in enumerate(template_params):
513-
if isinstance(params, ParameterExpression):
514-
equations.append(sym.Eq(parse_expr(str(params)), circuit_params[t_idx]))
515-
for param in params.parameters:
516-
symbols.add(param)
517-
518-
if not symbols:
555+
equations, circ_dict, temp_symbols, sol, fake_bind = [], {}, {}, {}, {}
556+
for circuit_param, template_param in zip(circuit_params, template_params):
557+
if isinstance(template_param, ParameterExpression):
558+
if isinstance(circuit_param, ParameterExpression):
559+
circ_param_sym = circuit_param.sympify()
560+
else:
561+
circ_param_sym = parse_expr(str(circuit_param))
562+
equations.append(sym.Eq(template_param.sympify(), circ_param_sym))
563+
564+
for param in template_param.parameters:
565+
temp_symbols[param] = param.sympify()
566+
567+
if isinstance(circuit_param, ParameterExpression):
568+
for param in circuit_param.parameters:
569+
circ_dict[param] = param.sympify()
570+
elif template_param != circuit_param:
571+
# Both are numeric parameters, but aren't equal.
572+
return None
573+
574+
if not temp_symbols:
519575
return template_dag_dep
520576

521577
# Check compatibility by solving the resulting equation
522-
sym_sol = sym.solve(equations)
578+
sym_sol = sym.solve(equations, set(temp_symbols.values()))
523579
for key in sym_sol:
524580
try:
525-
sol[str(key)] = float(sym_sol[key])
581+
sol[str(key)] = ParameterExpression(circ_dict, sym_sol[key])
526582
except TypeError:
527583
return None
528584

529585
if not sol:
530586
return None
531587

532-
for param in symbols:
533-
fake_bind[param] = sol[str(param)]
588+
for key in temp_symbols:
589+
fake_bind[key] = sol[str(key)]
534590

535591
for node in template_dag_dep.get_nodes():
536592
bound_params = []
537-
538-
for param in node.op.params:
539-
if isinstance(param, ParameterExpression):
540-
try:
541-
bound_params.append(float(param.bind(fake_bind)))
542-
except KeyError:
543-
return None
593+
for param_exp in node.op.params:
594+
if isinstance(param_exp, ParameterExpression):
595+
for param in param_exp.parameters:
596+
if param in fake_bind:
597+
if fake_bind[param] not in bound_params:
598+
param_exp = param_exp.assign(param, fake_bind[param])
544599
else:
545-
bound_params.append(param)
600+
param_exp = float(param_exp)
601+
bound_params.append(param_exp)
546602

547603
node.op.params = bound_params
548604

549605
return template_dag_dep
606+
607+
def _incr_num_parameters(self, template):
608+
"""
609+
Checks if template substitution would increase the number of
610+
parameters in the circuit.
611+
"""
612+
template_params = set()
613+
for param_list in (node.op.params for node in template.get_nodes()):
614+
for param_exp in param_list:
615+
if isinstance(param_exp, ParameterExpression):
616+
template_params.update(param_exp.parameters)
617+
618+
circuit_params = set()
619+
for param_list in (node.op.params for node in self.circuit_dag_dep.get_nodes()):
620+
for param_exp in param_list:
621+
if isinstance(param_exp, ParameterExpression):
622+
circuit_params.update(param_exp.parameters)
623+
624+
return len(template_params) > len(circuit_params)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
features:
3+
- |
4+
The :class:`.ParameterExpression` class is now allowed in the
5+
template optimization transpiler pass. An illustrative example
6+
of using :class:`.Parameter`\s is the following:
7+
8+
.. code-block::
9+
10+
from qiskit import QuantumCircuit, transpile, schedule
11+
from qiskit.circuit import Parameter
12+
13+
from qiskit.transpiler import PassManager
14+
from qiskit.transpiler.passes import TemplateOptimization
15+
16+
# New contributions to the template optimization
17+
from qiskit.transpiler.passes.calibration import RZXCalibrationBuilder, rzx_templates
18+
19+
from qiskit.test.mock import FakeCasablanca
20+
backend = FakeCasablanca()
21+
22+
phi = Parameter('φ')
23+
24+
qc = QuantumCircuit(2)
25+
qc.cx(0,1)
26+
qc.p(2*phi, 1)
27+
qc.cx(0,1)
28+
print('Original circuit:')
29+
print(qc)
30+
31+
pass_ = TemplateOptimization(**rzx_templates.rzx_templates(['zz2']))
32+
qc_cz = PassManager(pass_).run(qc)
33+
print('ZX based circuit:')
34+
print(qc_cz)
35+
36+
# Add the calibrations
37+
pass_ = RZXCalibrationBuilder(backend)
38+
cal_qc = PassManager(pass_).run(qc_cz.bind_parameters({phi: 0.12}))
39+
40+
# Transpile to the backend basis gates
41+
cal_qct = transpile(cal_qc, backend)
42+
qct = transpile(qc.bind_parameters({phi: 0.12}), backend)
43+
44+
# Compare the schedule durations
45+
print('Duration of schedule with the calibration:')
46+
print(schedule(cal_qct, backend).duration)
47+
print('Duration of standard with two CNOT gates:')
48+
print(schedule(qct, backend).duration)
49+
50+
outputs
51+
52+
.. parsed-literal::
53+
54+
Original circuit:
55+
56+
q_0: ──■──────────────■──
57+
┌─┴─┐┌────────┐┌─┴─┐
58+
q_1: ┤ X ├┤ P(2*φ) ├┤ X ├
59+
└───┘└────────┘└───┘
60+
ZX based circuit:
61+
┌─────────────┐ »
62+
q_0: ────────────────────────────────────┤0 ├────────────»
63+
┌──────────┐┌──────────┐┌──────────┐│ Rzx(2.0*φ) │┌──────────┐»
64+
q_1: ┤ Rz(-π/2) ├┤ Rx(-π/2) ├┤ Rz(-π/2) ├┤1 ├┤ Rx(-2*φ) ├»
65+
└──────────┘└──────────┘└──────────┘└─────────────┘└──────────┘»
66+
«
67+
«q_0: ────────────────────────────────────────────────
68+
« ┌──────────┐┌──────────┐┌──────────┐┌──────────┐
69+
«q_1: ┤ Rz(-π/2) ├┤ Rx(-π/2) ├┤ Rz(-π/2) ├┤ P(2.0*φ) ├
70+
« └──────────┘└──────────┘└──────────┘└──────────┘
71+
Duration of schedule with the calibration:
72+
1600
73+
Duration of standard with two CNOT gates:
74+
6848

0 commit comments

Comments
 (0)