Skip to content
Open
Show file tree
Hide file tree
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 Nov 16, 2025
b5c5553
wrote a test
yaelbh Nov 16, 2025
8afc786
fixes
yaelbh Nov 16, 2025
756bee5
parameter table
yaelbh Nov 16, 2025
712ef04
gates with multiple parameters
yaelbh Nov 16, 2025
d3383a3
a test for dynamic circuits
yaelbh Nov 16, 2025
9e2a547
more accurate duplication of the instruction
yaelbh Nov 16, 2025
a422ae6
dynamic circuits
yaelbh Nov 17, 2025
5149922
fixes
yaelbh Nov 17, 2025
9b14e8c
save the binding if the parameter expression is a parameter
yaelbh Nov 17, 2025
714344e
added a test
yaelbh Nov 17, 2025
3ad7227
black
yaelbh Nov 17, 2025
93481a8
lint
yaelbh Nov 17, 2025
95ed68c
mypy
yaelbh Nov 17, 2025
f75d001
lint
yaelbh Nov 17, 2025
b333bc0
use samplomatic parameter table
yaelbh Nov 19, 2025
e23f19c
correct usage of evaluate
yaelbh Nov 19, 2025
6e4d870
no need to store the table index
yaelbh Nov 19, 2025
28b4c78
remove a print
yaelbh Nov 19, 2025
c6aba00
make some names longer
yaelbh Nov 19, 2025
4aa4a6b
more doc
yaelbh Nov 20, 2025
5efaf88
black
yaelbh Nov 20, 2025
52b71ef
black
yaelbh Nov 20, 2025
c45d701
lint
yaelbh Nov 20, 2025
8867ecc
Update qiskit_ibm_runtime/quantum_program/utils.py
yaelbh Nov 23, 2025
19532c3
revert last commit
yaelbh Nov 23, 2025
c908358
Update qiskit_ibm_runtime/quantum_program/utils.py
yaelbh Nov 27, 2025
ddb72fe
update function doc
yaelbh Nov 27, 2025
c230055
fix
yaelbh Nov 27, 2025
cc64abf
fix
yaelbh Nov 27, 2025
7305d0d
fix
yaelbh Nov 30, 2025
4234f9e
black
yaelbh Nov 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions qiskit_ibm_runtime/quantum_program/utils.py
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
11 changes: 11 additions & 0 deletions test/unit/quantum_program/__init__.py
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.
188 changes: 188 additions & 0 deletions test/unit/quantum_program/test_utils.py
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]],
]
)
Comment on lines +39 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 = np.arange(1, 25).reshape((3, 4, 2))

Generally, the params will be floats. Therefore, I would actually favour something like this:

Suggested change
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 = np.random.random((3, 4, 2))

Copy link
Collaborator Author

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?


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))
)
)

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]])))