-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Allow ParameterExpression values in TemplateOptimization pass
#6899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 114 commits
32ee4db
8cfc6d9
c063402
1d290a4
ad85838
7098e4f
3e23380
6e18a54
c7605d7
ed74ccf
e308b84
8fc1fa6
78d3339
cc1a55a
732e887
0c63ca2
6a18dd1
133171c
caee274
bd67056
38e8bde
a99726a
59009db
432bb3f
b308587
5fd736a
71aa17e
5c3a62b
9f373e9
bf590d3
0f5f0d6
4f5b69e
bd3f9ed
6c9831c
d9530e6
06b9cf1
a9e7cdf
4ea8ff5
443a6c9
04dfc37
168c3cc
0e9c39f
d7b55b3
0547f53
fa51c3c
d4044a0
19007b7
d6e1cd7
2065826
a8b9b05
72990f5
d177d84
bd6dda4
6ea9d90
592963b
cebce74
4e2291c
6ccf293
8b6bf37
fdffda6
7721f23
9130a2a
5e31398
eb8ba0d
a92b942
84d65df
17d5002
3db29f0
31b3d62
1e8b009
bdee233
6d419de
b362089
2973339
bb1c797
d531d95
30706cd
9260cf4
767ccf4
28b384f
d55e75c
f50c4e6
e32d038
c21fdf1
bc42afb
cce7b65
5506200
708bfcc
84d566b
54403b0
3c15da0
9355893
7e9f8d9
0a179b6
5f86049
0835763
680ec53
f276cd7
a295831
2f244bc
0434a70
055832d
2504c2e
0f1cf5d
c672ee3
9a94bb2
48173c1
24097b5
2f0203c
591763a
4dcf014
8d1bc2e
4f76b73
c8a08e9
ee081e3
3f740fb
69b8472
58c8ccf
e09b1e3
f01a671
78a2f47
8496a8c
82470fd
1f17aff
fb8ca5c
0ce2fa1
9f59e06
5de0384
7906036
092f92b
95dbcfb
908f87d
fc83156
a14dc37
9d07849
7034789
16bbffb
317e2a6
6ccc663
94e1884
87e5fe8
df9c3d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ | |
|
|
||
| import numpy | ||
|
|
||
| from qiskit.circuit.exceptions import CircuitError | ||
| from qiskit.circuit.exceptions import CircuitError, ParameterTypeError | ||
|
|
||
| try: | ||
| import symengine | ||
|
|
@@ -434,7 +434,7 @@ def __complex__(self): | |
| return complex(self._symbol_expr) | ||
| # TypeError is for sympy, RuntimeError for symengine | ||
| except (TypeError, RuntimeError) as exc: | ||
| raise TypeError( | ||
| raise ParameterTypeError( | ||
|
nbronn marked this conversation as resolved.
Outdated
|
||
| "ParameterExpression with unbound parameters ({}) " | ||
| "cannot be cast to a complex.".format(self.parameters) | ||
| ) from exc | ||
|
|
@@ -444,7 +444,7 @@ def __float__(self): | |
| return float(self._symbol_expr) | ||
| # TypeError is for sympy, RuntimeError for symengine | ||
| except (TypeError, RuntimeError) as exc: | ||
| raise TypeError( | ||
| raise ParameterTypeError( | ||
| "ParameterExpression with unbound parameters ({}) " | ||
| "cannot be cast to a float.".format(self.parameters) | ||
| ) from exc | ||
|
|
@@ -532,6 +532,15 @@ def is_real(self): | |
| return False | ||
| return True | ||
|
|
||
| def to_simplify_expression(self): | ||
| """Return symbolic expression from sympy/symengine""" | ||
| if HAS_SYMENGINE: | ||
| return symengine.sympify(self._symbol_expr) | ||
| else: | ||
| from sympy import sympify | ||
|
|
||
| return sympify(self._symbol_expr) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it necessary to I'm not keen on the name of the function (or its existence at all, to be honest, but looking at the existing transpiler pass, the ship has already sailed on that). Perhaps it could be Please can you expand the documentation to explain when it returns each type, and put in a note that this should not be used with general Qiskit functionality; it's for interoperability only, and most Qiskit functions will not accept raw
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed to 'to_simplify_expression', this function returns the simplified version of the expression.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What simplification is being performed here? In [1]: import sympy
...: a, b, c = sympy.symbols("a b c")
...: expr = a*(b + c) - a*b - a*c
...: expr, sympy.sympify(expr), sympy.simplify(expr)
Out[1]: (-a*b - a*c + a*(b + c), -a*b - a*c + a*(b + c), 0)If this function is meant to simplify its internals, then it should return |
||
|
|
||
|
|
||
| # Redefine the type so external imports get an evaluated reference; Sphinx needs this to understand | ||
| # the type hints. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -269,11 +269,6 @@ 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() | ||
|
|
@@ -500,39 +495,45 @@ def _attempt_bind(self, template_sublist, circuit_sublist): | |
| template_params += template_dag_dep.get_node(t_idx).op.params | ||
|
|
||
| # Create the fake binding dict and check | ||
| equations, symbols, sol, fake_bind = [], set(), {}, {} | ||
| for t_idx, params in enumerate(template_params): | ||
| if isinstance(params, ParameterExpression): | ||
| equations.append(sym.Eq(parse_expr(str(params)), circuit_params[t_idx])) | ||
| for param in params.parameters: | ||
| symbols.add(param) | ||
|
|
||
| if not symbols: | ||
| equations, circ_dict, temp_symbols, sol, fake_bind = [], {}, {}, {}, {} | ||
| for circuit_param, template_param in zip(circuit_params, template_params): | ||
| if isinstance(template_param, ParameterExpression): | ||
| if isinstance(circuit_param, ParameterExpression): | ||
| circ_param_sym = circuit_param.to_simplify_expression() | ||
| else: | ||
| circ_param_sym = parse_expr(str(circuit_param)) | ||
| equations.append(sym.Eq(template_param.to_simplify_expression(), circ_param_sym)) | ||
|
|
||
| for param in template_param.parameters: | ||
| temp_symbols[param] = param.to_simplify_expression() | ||
|
|
||
| if isinstance(circuit_param, ParameterExpression): | ||
| for param in circuit_param.parameters: | ||
| circ_dict[param] = param.to_simplify_expression() | ||
|
|
||
| if not temp_symbols: | ||
| return template_dag_dep | ||
|
|
||
| # Check compatibility by solving the resulting equation | ||
| sym_sol = sym.solve(equations) | ||
| sym_sol = sym.solve(equations, set(temp_symbols.values())) | ||
|
jakelishman marked this conversation as resolved.
|
||
| for key in sym_sol: | ||
| try: | ||
| sol[str(key)] = float(sym_sol[key]) | ||
| sol[str(key)] = ParameterExpression(circ_dict, sym_sol[key]) | ||
| except TypeError: | ||
| return None | ||
|
|
||
| if not sol: | ||
| return None | ||
|
|
||
| for param in symbols: | ||
| fake_bind[param] = sol[str(param)] | ||
| for key in temp_symbols: | ||
| fake_bind[key] = sol[str(key)] | ||
|
|
||
| for node in template_dag_dep.get_nodes(): | ||
| bound_params = [] | ||
|
|
||
| for param in node.op.params: | ||
| if isinstance(param, ParameterExpression): | ||
| try: | ||
| bound_params.append(float(param.bind(fake_bind))) | ||
| except KeyError: | ||
| return None | ||
| for key in fake_bind: | ||
| bound_params.append(param.assign(key, fake_bind[key])) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the issue here is that we cannot pass
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this comment matches up with my issue (also the point about iterating through a dictionary with The problem is that if Please can you add a test case that involves a template (arbitrary - it can just be invented for the test) that has two parameters, and test that it binds correctly to input circuits with either numeric values or parameter values? For example, here's a (nonsensical) template match that fails with a In [5]: from qiskit.circuit import Parameter, QuantumCircuit
...: from qiskit.transpiler import PassManager
...: from qiskit.transpiler.passes import TemplateOptimization, RZXCalibrationBuilder
...:
...: a = Parameter('a')
...: b = Parameter('b')
...:
...: qc = QuantumCircuit(2)
...: qc.p(0.1, 0)
...: qc.p(-0.2, 1)
...:
...: template = QuantumCircuit(2)
...: template.p(a, 0)
...: template.p(b, 1)
...: template.cx(0, 1)
...:
...: pass_ = TemplateOptimization(
...: user_cost_dict={"cx": 0, "p": 0},
...: template_list=[template],
...: )
...: PassManager(pass_).run(qc).draw()There are two issues at play: one is the issue with the length of
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it really isn't possible to have it work for multiple parameters in the template circuit, then this should at least enforce that the template circuit only has a single unbound parameter in it, and the pass should loudly fail if it's given a template with more than one parameter. |
||
| else: | ||
| bound_params.append(param) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| --- | ||
| features: | ||
| - | | ||
| The :class:`.ParameterExpression` class is now allowed in the | ||
| template optimization transpiler pass. An illustrative example | ||
| of using :class:`.Parameter`\s is the following: | ||
|
|
||
| .. code-block:: | ||
|
|
||
| from qiskit import QuantumCircuit, transpile, schedule | ||
| from qiskit.circuit import Parameter | ||
|
|
||
| from qiskit.transpiler import PassManager | ||
| from qiskit.transpiler.passes import TemplateOptimization | ||
|
|
||
| # New contributions to the template optimization | ||
| from qiskit.transpiler.passes.scheduling import RZXCalibrationBuilder, rzx_templates | ||
|
nbronn marked this conversation as resolved.
Outdated
|
||
|
|
||
| from qiskit.test.mock import FakeCasablanca | ||
| backend = FakeCasablanca() | ||
|
|
||
| phi = Parameter('φ') | ||
|
|
||
| qc = QuantumCircuit(2) | ||
| qc.cx(0,1) | ||
| qc.p(2*phi, 1) | ||
| qc.cx(0,1) | ||
| print('Original circuit:') | ||
| print(qc) | ||
|
|
||
| pass_ = TemplateOptimization(**rzx_templates.rzx_templates(['zz2'])) | ||
| qc_cz = PassManager(pass_).run(qc) | ||
| print('ZX based circuit:') | ||
| print(qc_cz) | ||
|
|
||
| # Add the calibrations | ||
| pass_ = RZXCalibrationBuilder(backend) | ||
| cal_qc = PassManager(pass_).run(qc_cz.bind_parameters({phi: 0.12})) | ||
|
|
||
| # Transpile to the backend basis gates | ||
| cal_qct = transpile(cal_qc, backend) | ||
| qct = transpile(qc.bind_parameters({phi: 0.12}), backend) | ||
|
|
||
| # Compare the schedule durations | ||
| print('Duration of schedule with the calibration:') | ||
| print(schedule(cal_qct, backend).duration) | ||
| print('Duration of standard with two CNOT gates:') | ||
| print(schedule(qct, backend).duration) | ||
|
|
||
| outputs | ||
|
|
||
| .. parsed-literal:: | ||
|
|
||
| Original circuit: | ||
|
|
||
| q_0: ──■──────────────■── | ||
| ┌─┴─┐┌────────┐┌─┴─┐ | ||
| q_1: ┤ X ├┤ P(2*φ) ├┤ X ├ | ||
| └───┘└────────┘└───┘ | ||
| ZX based circuit: | ||
| ┌─────────────┐ » | ||
| q_0: ────────────────────────────────────┤0 ├────────────» | ||
| ┌──────────┐┌──────────┐┌──────────┐│ Rzx(2.0*φ) │┌──────────┐» | ||
| q_1: ┤ Rz(-π/2) ├┤ Rx(-π/2) ├┤ Rz(-π/2) ├┤1 ├┤ Rx(-2*φ) ├» | ||
| └──────────┘└──────────┘└──────────┘└─────────────┘└──────────┘» | ||
| « | ||
| «q_0: ──────────────────────────────────────────────── | ||
| « ┌──────────┐┌──────────┐┌──────────┐┌──────────┐ | ||
| «q_1: ┤ Rz(-π/2) ├┤ Rx(-π/2) ├┤ Rz(-π/2) ├┤ P(2.0*φ) ├ | ||
| « └──────────┘└──────────┘└──────────┘└──────────┘ | ||
| Duration of schedule with the calibration: | ||
| 1600 | ||
| Duration of standard with two CNOT gates: | ||
| 6848 | ||
|
|
||
| Note that the :class:`.Parameter`\s/:class:`.ParameterExpression`\s | ||
| must be bound before the :class:`.RZXCalibrationBuilder` can define | ||
| calibrations for the :class:`.RZXGate`\ s. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| from qiskit.converters.circuit_to_dagdependency import circuit_to_dagdependency | ||
| from qiskit.transpiler import PassManager | ||
| from qiskit.transpiler.passes import TemplateOptimization | ||
| from qiskit.transpiler.passes.calibration.rzx_templates import rzx_templates | ||
| from qiskit.test import QiskitTestCase | ||
| from qiskit.transpiler.exceptions import TranspilerError | ||
|
|
||
|
|
@@ -353,9 +354,24 @@ def template(): | |
| 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) | ||
| self.assertEqual(circuit_out.count_ops().get("cx", 0), 0) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this was pre-existing, but it'd be good to make this a test of the entire structure of the circuit, not just an op-count - as I understand it, this test should now return an empty circuit, so we should test that. (As an aside: it maybe would be good to update the test slightly so it doesn't return a fully empty circuit, because a buggy feature completely wiping out all data in the circuit seems like a plausible failure mode.)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was attempting to improve this test by using Perhaps we could move this point to another Issue?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked more, and this test is vitally important, and this PR breaks it as it is. The output of the test should contain 2 CX gates. If you run this test with the changes in this PR, you get In [4]: circuit_out.draw()
Out[4]:
┌────┐
q_0: ┤0 ├─────────────
│ P │┌───────────┐
q_1: ┤1 ├┤ P(-1.0*β) ├
└────┘└───────────┘Note that it's replaced the two CXs with two gates whose parameters don't mean anything. This is a failure; the pass should not have replaced anything, even with your changes. If the test had stronger assertions (like I was suggesting in the previous comment), it would have been clearer that this was a true test failure. Fortunately, now that I understand this, we can hugely simplify the requirements on def test_optimizer_does_not_replace_unbound_partial_match(self):
beta = Parameter("β")
template = QuantumCircuit(2)
template.cx(1, 0)
template.cx(1, 0)
template.p(beta, 1)
template.cu(0, 0, 0, -beta, 0, 1)
circuit_in = QuantumCircuit(2)
circuit_in.cx(1, 0)
circuit_in.cx(1, 0)
pass_ = TemplateOptimization(
template_list=[template],
user_cost_dict={"cx": 6, "p": 0, "cu": 8},
)
circuit_out = PassManager(pass_).run(circuit_in)
# The template optimisation should not have replaced anything, because
# that would require it to leave dummy parameters in place without
# binding them.
self.assertEqual(circuit_in, circuit_out)If we change the test to that, it lets us revert all the changes to The trick, though, is that I think to achieve your goals in the pass, you're going to need to update your logic a bit. You need to keep track of which parameters are dummy parameters used in the template, and which are parameters that come from the circuit. For example, in this test, the There's more nuance than just that as well, though, because the pass needs to handle the case that a template uses a parameter with the same name as an input circuit without breaking. You can likely handle that bit by re-binding the variables in a template to auto-generated unique names if you detect issues.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a great point to eliminate the In order to guarantee template optimization does not increase the number of As for which does not match with or so it seems that this is not much of a problem currently. (Of course template optimization fails if I use the exact form of |
||
|
|
||
| def test_unbound_parameters_in_rzx_template(self): | ||
| """ | ||
| Test that rzx template ('zz2') functions correctly for a simple | ||
| circuit with an unbound ParameterExpression. | ||
| """ | ||
|
|
||
| phi = Parameter("$\\phi$") | ||
| circuit_in = QuantumCircuit(2) | ||
| circuit_in.cx(0, 1) | ||
| circuit_in.p(2 * phi, 1) | ||
| circuit_in.cx(0, 1) | ||
|
|
||
| pass_ = TemplateOptimization(**rzx_templates(["zz2"])) | ||
| circuit_out = PassManager(pass_).run(circuit_in) | ||
|
|
||
| self.assertEqual(circuit_out.count_ops().get("cx", 0), 0) | ||
|
nbronn marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.