-
Notifications
You must be signed in to change notification settings - Fork 192
[executor] A function to remove parameter expressions #2478
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
Open
yaelbh
wants to merge
32
commits into
Qiskit:executor_preview
Choose a base branch
from
yaelbh:removeparamexp
base: executor_preview
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
349d07e
A function to remove parameter expressions
yaelbh b5c5553
wrote a test
yaelbh 8afc786
fixes
yaelbh 756bee5
parameter table
yaelbh 712ef04
gates with multiple parameters
yaelbh d3383a3
a test for dynamic circuits
yaelbh 9e2a547
more accurate duplication of the instruction
yaelbh a422ae6
dynamic circuits
yaelbh 5149922
fixes
yaelbh 9b14e8c
save the binding if the parameter expression is a parameter
yaelbh 714344e
added a test
yaelbh 3ad7227
black
yaelbh 93481a8
lint
yaelbh 95ed68c
mypy
yaelbh f75d001
lint
yaelbh b333bc0
use samplomatic parameter table
yaelbh e23f19c
correct usage of evaluate
yaelbh 6e4d870
no need to store the table index
yaelbh 28b4c78
remove a print
yaelbh c6aba00
make some names longer
yaelbh 4aa4a6b
more doc
yaelbh 5efaf88
black
yaelbh 52b71ef
black
yaelbh c45d701
lint
yaelbh 8867ecc
Update qiskit_ibm_runtime/quantum_program/utils.py
yaelbh 19532c3
revert last commit
yaelbh c908358
Update qiskit_ibm_runtime/quantum_program/utils.py
yaelbh ddb72fe
update function doc
yaelbh c230055
fix
yaelbh cc64abf
fix
yaelbh 7305d0d
fix
yaelbh 4234f9e
black
yaelbh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| # This code is part of Qiskit. | ||
| # | ||
| # (C) Copyright IBM 2025. | ||
| # | ||
| # This code is licensed under the Apache License, Version 2.0. You may | ||
| # obtain a copy of this license in the LICENSE.txt file in the root directory | ||
| # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||
| # | ||
| # Any modifications or derivative works of this code must retain this | ||
| # copyright notice, and modified files need to carry a notice indicating | ||
| # that they have been altered from the originals. | ||
|
|
||
| """Util functions for the quantum program.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import numpy as np | ||
|
|
||
| from qiskit.circuit import Parameter, QuantumCircuit, ParameterExpression, CircuitInstruction | ||
|
|
||
| from samplomatic.samplex import ParameterExpressionTable | ||
|
|
||
|
|
||
| def _replace_parameter_expressions( | ||
| circuit: QuantumCircuit, | ||
| parameter_table: ParameterExpressionTable, | ||
| parameter_expressions_to_new_parameters_map: dict[ParameterExpression, Parameter], | ||
| ) -> QuantumCircuit: | ||
| new_circuit = circuit.copy_empty_like() | ||
| new_data = [] | ||
|
|
||
| for instruction in circuit.data: | ||
| if instruction.is_control_flow(): | ||
| new_blocks = [ | ||
| _replace_parameter_expressions( | ||
| block, parameter_table, parameter_expressions_to_new_parameters_map | ||
| ) | ||
| for block in instruction.operation.blocks | ||
| ] | ||
| new_gate = instruction.operation.replace_blocks(new_blocks) | ||
| new_data.append(instruction.replace(params=new_gate.params, operation=new_gate)) | ||
| continue | ||
|
|
||
| param_exps = [ | ||
| op_param | ||
| for op_param in instruction.operation.params | ||
| if isinstance(op_param, ParameterExpression) | ||
| ] | ||
| if len(param_exps) == 0: | ||
| new_data.append(instruction) | ||
| continue | ||
|
|
||
| new_op_params = [] | ||
| for param_exp in param_exps: | ||
| if param_exp in parameter_expressions_to_new_parameters_map: | ||
| new_param = parameter_expressions_to_new_parameters_map[param_exp] | ||
| else: | ||
| if isinstance(param_exp, Parameter): | ||
| new_param = param_exp | ||
| else: | ||
| new_param = Parameter(str(param_exp)) | ||
| parameter_table.append(param_exp) | ||
| parameter_expressions_to_new_parameters_map[param_exp] = new_param | ||
| new_op_params.append(new_param) | ||
|
|
||
| new_op = type(instruction.operation)(*new_op_params) | ||
| new_data.append( | ||
| CircuitInstruction( | ||
| operation=new_op, qubits=instruction.qubits, clbits=instruction.clbits | ||
| ) | ||
| ) | ||
|
|
||
| new_circuit.data = new_data | ||
| return new_circuit | ||
|
|
||
|
|
||
| def replace_parameter_expressions( | ||
| circuit: QuantumCircuit, parameter_values: np.ndarray | ||
| ) -> tuple[QuantumCircuit, np.ndarray]: | ||
| """ | ||
| A helper to replace a circuit's parameter expressions with parameters. | ||
|
|
||
| The function tranverses the circuit and collects all the parameters and parameter expressions. | ||
| A new parameter is created for every parameter expression that is not a parameter. | ||
| The function builds a new circuit, where each parameter expression is replaced by the | ||
| corresponding new parameter. | ||
| In addition, the function creates a new array of parameter values, which matches the parameters | ||
| of the new circuit. Values for new parameters are obtained by evaluating the original | ||
| expressions over the original parameter values. | ||
|
|
||
| Example: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import numpy as np | ||
| from qiskit.circuit import QuantumCircuit, Parameter | ||
| from qiskit_ibm_runtime.quantum_program.utils import replace_parameter_expressions | ||
|
|
||
| circuit = QuantumCircuit(1) | ||
| circuit.rx(a := Parameter("a"), 0) | ||
| circuit.rx(b := Parameter("b"), 0) | ||
| circuit.rx(a + b, 0) | ||
|
|
||
| values = np.array([[1, 2], [3, 4]]) | ||
|
|
||
| # ``new_circuit`` will incorporate a new parameter, which replaces the parameter | ||
| # expression ``a + b`` | ||
| # ``new_values`` will be ``np.array([[1, 2, 3], [3, 4, 7]])`` | ||
| new_circuit, new_values = replace_parameter_expressions(circuit, values) | ||
|
|
||
| .. note: | ||
|
|
||
| The instructions of the new circuit are the same as in the original circuit, in terms of | ||
| operation types, qubits, and classical bits. Other instruction attributes, such as | ||
| ``label``, are not copied. Instruction operations are assumed to be one of | ||
| ``global_phase``, ``p``, ``r``, ``rx``, ``rxx``, ``ry``, ``ryy``, ``rz``, ``rzx``, ``rzz``, | ||
| ``u``, ``u1``, ``u2``, ``u3``. Other operations may yield unexpected behavior. | ||
| """ | ||
| parameter_table = ParameterExpressionTable() | ||
| parameter_expressions_to_new_parameters_map: dict[ParameterExpression, Parameter] = {} | ||
|
|
||
| new_circuit = _replace_parameter_expressions( | ||
| circuit, parameter_table, parameter_expressions_to_new_parameters_map | ||
| ) | ||
|
|
||
| new_values = np.zeros(parameter_values.shape[:-1] + (len(new_circuit.parameters),)) | ||
| for idx in np.ndindex(parameter_values.shape[:-1]): | ||
| new_values[idx] = parameter_table.evaluate(parameter_values[idx + (slice(None),)]) | ||
|
|
||
| return new_circuit, new_values |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # This code is part of Qiskit. | ||
| # | ||
| # (C) Copyright IBM 2025. | ||
| # | ||
| # This code is licensed under the Apache License, Version 2.0. You may | ||
| # obtain a copy of this license in the LICENSE.txt file in the root directory | ||
| # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||
| # | ||
| # Any modifications or derivative works of this code must retain this | ||
| # copyright notice, and modified files need to carry a notice indicating | ||
| # that they have been altered from the originals. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| # This code is part of Qiskit. | ||
| # | ||
| # (C) Copyright IBM 2025. | ||
| # | ||
| # This code is licensed under the Apache License, Version 2.0. You may | ||
| # obtain a copy of this license in the LICENSE.txt file in the root directory | ||
| # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||
| # | ||
| # Any modifications or derivative works of this code must retain this | ||
| # copyright notice, and modified files need to carry a notice indicating | ||
| # that they have been altered from the originals. | ||
|
|
||
| """Test utility functions of the quantum program.""" | ||
|
|
||
| import numpy as np | ||
|
|
||
| from qiskit.circuit import QuantumCircuit, Parameter | ||
| from qiskit.circuit.library import U2Gate | ||
| from qiskit.quantum_info import Operator | ||
|
|
||
| from qiskit_ibm_runtime.quantum_program.utils import replace_parameter_expressions | ||
|
|
||
| from ...ibm_test_case import IBMTestCase | ||
|
|
||
|
|
||
| # pylint: disable=not-context-manager | ||
|
|
||
|
|
||
| class TestRemoveParameterExpressions(IBMTestCase): | ||
| """Test the function :func:`~remove_parameter_expressions`.""" | ||
|
|
||
| def test_remove_parameter_expressions_static_circuit(self): | ||
| """ | ||
| Test the function :func:`~remove_parameter_expressions` for static circuits. | ||
| The static property allows a rigorous check using operator equivalence. | ||
| """ | ||
| p1 = Parameter("p1") | ||
| p2 = Parameter("p2") | ||
| param_values = np.array( | ||
| [ | ||
| [[1, 2], [3, 4], [5, 6], [7, 8]], | ||
| [[9, 10], [11, 12], [13, 14], [15, 16]], | ||
| [[17, 18], [19, 20], [21, 22], [23, 24]], | ||
| ] | ||
| ) | ||
|
|
||
| circ = QuantumCircuit(2) | ||
| circ.h(0) | ||
| circ.rz(p1, 0) | ||
| circ.rx(p1 + p2, 1) | ||
| circ.rx(p1 + p2, 0) | ||
| circ.append(U2Gate(p1 - p2, p1 + p2), [1]) | ||
|
|
||
| new_circ, new_values = replace_parameter_expressions(circ, param_values) | ||
|
|
||
| self.assertEqual(len(new_circ.parameters), 3) | ||
| self.assertEqual(new_circ.parameters[0], p1) | ||
|
|
||
| self.assertEqual(param_values.shape[:-1], new_values.shape[:-1]) | ||
| param_values_flat = param_values.reshape(-1, param_values.shape[-1]) | ||
| new_values_flat = new_values.reshape(-1, new_values.shape[-1]) | ||
| for param_set_1, param_set_2 in zip(param_values_flat, new_values_flat): | ||
| self.assertTrue( | ||
| Operator.from_circuit(circ.assign_parameters(param_set_1)).equiv( | ||
| Operator.from_circuit(new_circ.assign_parameters(param_set_2)) | ||
| ) | ||
yaelbh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
| def test_remove_parameter_expressions_dynamic_circuit(self): | ||
| """ | ||
| Test the function :func:`~remove_parameter_expressions` for dynamic circuits. | ||
| """ | ||
| p1 = Parameter("p1") | ||
| p2 = Parameter("p2") | ||
| param_values = np.array( | ||
| [ | ||
| [[1, 2], [3, 4], [5, 6], [7, 8]], | ||
| [[9, 10], [11, 12], [13, 14], [15, 16]], | ||
| [[17, 18], [19, 20], [21, 22], [23, 24]], | ||
| ] | ||
| ) | ||
| param_values_flat = param_values.reshape(-1, param_values.shape[-1]) | ||
|
|
||
| circ = QuantumCircuit(2, 1) | ||
| circ.h(0) | ||
| circ.rz(p1, 0) | ||
| with circ.box(): | ||
| circ.rx(p1 + p2, 1) | ||
| circ.append(U2Gate(p1 - p2, p1 + p2), [1]) | ||
| circ.rz(p1, 1) | ||
| circ.rx(p1 + p2, 0) | ||
| circ.measure(0, 0) | ||
| with circ.if_test((0, 1)): | ||
| with circ.if_test((0, 0)) as else_: | ||
| circ.h(0) | ||
| with else_: | ||
| circ.rz(p1 + 3, 0) | ||
| circ.rx(p1 * p2, 1) | ||
|
|
||
| new_circ, new_values = replace_parameter_expressions(circ, param_values) | ||
|
|
||
| # parameter names: 3 + p1, p1, p1 + p2, p1 - p2, p1*p2 | ||
| self.assertEqual(len(new_circ.parameters), 5) | ||
| self.assertEqual(param_values.shape[:-1], new_values.shape[:-1]) | ||
|
|
||
| outer_circ_1 = QuantumCircuit(2) | ||
| outer_circ_1.data = [circ.data[i] for i in (0, 1, 3, 6)] | ||
| outer_circ_2 = QuantumCircuit(2) | ||
| outer_circ_2.data = [new_circ.data[i] for i in (0, 1, 3, 6)] | ||
|
|
||
| outer_params_2 = new_values[..., [0, 1, 4]] | ||
| outer_2_flat = outer_params_2.reshape(-1, outer_params_2.shape[-1]) | ||
| for param_set_1, param_set_2 in zip(param_values_flat, outer_2_flat): | ||
| self.assertTrue( | ||
| Operator.from_circuit(outer_circ_1.assign_parameters(param_set_1)).equiv( | ||
| Operator.from_circuit(outer_circ_2.assign_parameters(param_set_2)) | ||
| ) | ||
| ) | ||
|
|
||
| self.assertEqual(new_circ.data[2].operation.name, "box") | ||
| box_circ_1 = QuantumCircuit(2) | ||
| box_circ_1.data = circ.data[2].operation.blocks[0] | ||
| box_circ_2 = QuantumCircuit(2) | ||
| box_circ_2.data = new_circ.data[2].operation.blocks[0] | ||
|
|
||
| box_params_2 = new_values[..., [0, 1, 2]] | ||
| box_2_flat = box_params_2.reshape(-1, box_params_2.shape[-1]) | ||
| for param_set_1, param_set_2 in zip(param_values_flat, box_2_flat): | ||
| self.assertTrue( | ||
| Operator.from_circuit(box_circ_1.assign_parameters(param_set_1)).equiv( | ||
| Operator.from_circuit(box_circ_2.assign_parameters(param_set_2)) | ||
| ) | ||
| ) | ||
|
|
||
| self.assertEqual(new_circ.data[5].operation.name, "if_else") | ||
| self.assertEqual(new_circ.data[5].operation.blocks[0].data[0].operation.name, "if_else") | ||
| if_circ_1 = circ.data[5].operation.blocks[0].data[0].operation.blocks[0] | ||
| if_circ_2 = new_circ.data[5].operation.blocks[0].data[0].operation.blocks[0] | ||
|
|
||
| self.assertTrue(Operator.from_circuit(if_circ_1).equiv(Operator.from_circuit(if_circ_2))) | ||
|
|
||
| else_circ_1 = circ.data[5].operation.blocks[0].data[0].operation.blocks[1] | ||
| else_circ_2 = new_circ.data[5].operation.blocks[0].data[0].operation.blocks[1] | ||
|
|
||
| else_1_flat = param_values_flat[:, 0] | ||
| else_2_flat = new_values[..., 3].ravel() | ||
| for param_set_1, param_set_2 in zip(else_1_flat, else_2_flat): | ||
| self.assertTrue( | ||
| Operator.from_circuit(else_circ_1.assign_parameters([param_set_1])).equiv( | ||
| Operator.from_circuit(else_circ_2.assign_parameters([param_set_2])) | ||
| ) | ||
| ) | ||
|
|
||
| def test_remove_parameter_expressions_one_parameter(self): | ||
| """ | ||
| Test the function :func:`~remove_parameter_expressions` in the edge | ||
| case where the circuit contains one parameter. | ||
| """ | ||
| p = Parameter("p") | ||
|
|
||
| circ = QuantumCircuit(1) | ||
| circ.h(0) | ||
| circ.rz(p + 1, 0) | ||
|
|
||
| param_values = np.array([5]) | ||
| _, new_values = replace_parameter_expressions(circ, param_values) | ||
| self.assertTrue(np.array_equal(new_values, np.array([6]))) | ||
|
|
||
| circ = QuantumCircuit(1) | ||
| circ.h(0) | ||
| circ.rz(p, 0) | ||
|
|
||
| param_values = np.array([5]) | ||
| _, new_values = replace_parameter_expressions(circ, param_values) | ||
| self.assertTrue(np.array_equal(new_values, np.array([5]))) | ||
|
|
||
| circ = QuantumCircuit(1) | ||
| circ.h(0) | ||
| circ.rz(p + 1, 0) | ||
| circ.rz(p, 0) | ||
|
|
||
| param_values = np.array([5]) | ||
| _, new_values = replace_parameter_expressions(circ, param_values) | ||
| self.assertTrue(np.array_equal(new_values, np.array([6, 5]))) | ||
|
|
||
| param_values = np.array([[5], [10]]) | ||
| _, new_values = replace_parameter_expressions(circ, param_values) | ||
| self.assertTrue(np.array_equal(new_values, np.array([[6, 5], [11, 10]]))) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, the params will be floats. Therefore, I would actually favour something like this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it more readable if the array is written explicitly. How about keeping the original array, and only adding
dtype=float?