Skip to content

Commit ff15ce4

Browse files
mergify[bot]jlapeyreElePTmtreinish
authored
Fix qpy serialization of substitution of type ParameterExpression (backport #13890) (#13943)
* Fix qpy serialization of substitution of type `ParameterExpression` (#13890) * Fix qpy serialization of substitution of type ParameterExpression When substitution history `ParameterExpression._qpy_replay` is serialized, there was no branch for the case that the substituted value is of type `ParameterExpression`. This commit fixes this oversight. * Fix deserializing qpy written by previous writing fix * Added a branch for reading `ParameterExpression` in _qpy_replay * Added a missing argument (version) in an existing call in code path that was previously untested * Remove a bit of useless code introduced in last commit * run black * Add tests * Revert renaming extra_symbols to extra_expressions Reverting this to minimize changes in order to make the PR safer to backport. * Revert cleaning up logic in _encode_replay_subs This was a good change. But not necessary for the bug fix, which is the main point of this PR. * Add qpy compat test * Bind parameter when checking for equality * Run black after merge in browser --------- Co-authored-by: Luciano Bello <bel@zurich.ibm.com> (cherry picked from commit a6fa6f8) # Conflicts: # test/qpy_compat/test_qpy.py * Update test_qpy.py * Update test/qpy_compat/test_qpy.py --------- Co-authored-by: John Lapeyre <jlapeyre@users.noreply.github.com> Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
1 parent 06ac96e commit ff15ce4

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

qiskit/qpy/binary_io/value.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ def _encode_replay_subs(subs, file_obj, version):
142142

143143

144144
def _write_parameter_expression_v13(file_obj, obj, version):
145+
# A symbol is `Parameter` or `ParameterVectorElement`.
146+
# `symbol_map` maps symbols to ParameterExpression (which may be a symbol).
145147
symbol_map = {}
146148
for inst in obj._qpy_replay:
147149
if isinstance(inst, _SUBS):
@@ -234,9 +236,17 @@ def _write_parameter_expression(file_obj, obj, use_symengine, *, version):
234236
# serialize key
235237
if symbol_key == type_keys.Value.PARAMETER_VECTOR:
236238
symbol_data = common.data_to_binary(symbol, _write_parameter_vec)
239+
elif symbol_key == type_keys.Value.PARAMETER_EXPRESSION:
240+
symbol_data = common.data_to_binary(
241+
symbol,
242+
_write_parameter_expression,
243+
use_symengine=use_symengine,
244+
version=version,
245+
)
237246
else:
238247
symbol_data = common.data_to_binary(symbol, _write_parameter)
239248
# serialize value
249+
240250
value_key, value_data = dumps_value(
241251
symbol, version=version, use_symengine=use_symengine
242252
)
@@ -516,10 +526,13 @@ def _read_parameter_expression_v13(file_obj, vectors, version):
516526
symbol = _read_parameter(file_obj)
517527
elif symbol_key == type_keys.Value.PARAMETER_VECTOR:
518528
symbol = _read_parameter_vec(file_obj, vectors)
529+
elif symbol_key == type_keys.Value.PARAMETER_EXPRESSION:
530+
symbol = _read_parameter_expression_v13(file_obj, vectors, version)
519531
else:
520532
raise exceptions.QpyError(f"Invalid parameter expression map type: {symbol_key}")
521533

522534
elem_key = type_keys.Value(elem_data.type)
535+
523536
binary_data = file_obj.read(elem_data.size)
524537
if elem_key == type_keys.Value.INTEGER:
525538
value = struct.unpack("!q", binary_data)
@@ -534,6 +547,7 @@ def _read_parameter_expression_v13(file_obj, vectors, version):
534547
binary_data,
535548
_read_parameter_expression_v13,
536549
vectors=vectors,
550+
version=version,
537551
)
538552
else:
539553
raise exceptions.QpyError(f"Invalid parameter expression map type: {elem_key}")
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2025.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
"""Test serializing ParameterExpressions from qpy."""
14+
15+
import io
16+
from test import QiskitTestCase # pylint: disable=wrong-import-order
17+
from qiskit.circuit import Parameter, QuantumCircuit
18+
from qiskit import qpy
19+
20+
21+
class TestQpySerializeParameterExpression(QiskitTestCase):
22+
"""QPY serializing ParameterExpression"""
23+
24+
def test_roundtrip_equal(self):
25+
"""Test serialize deserialize with ParameterExpression in _qpy_replay"""
26+
a = Parameter("a")
27+
b = Parameter("b")
28+
a1 = a * 2
29+
a2 = a1.subs({a: 3 * b})
30+
31+
qc = QuantumCircuit(1)
32+
qc.rz(a2, 0)
33+
34+
use_symengine = True
35+
version = 13
36+
with io.BytesIO() as container:
37+
qpy.dump(qc, container, version=version, use_symengine=use_symengine)
38+
qc_qpy_str = container.getvalue()
39+
40+
with io.BytesIO(qc_qpy_str) as container:
41+
qc_from_qpy = qpy.load(container)[0]
42+
43+
self.assertEqual(qc, qc_from_qpy)

test/qpy_compat/test_qpy.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,8 +820,32 @@ def generate_v12_expr():
820820
return [index, shift]
821821

822822

823+
def generate_replay_with_expression_substitutions():
824+
"""Circuits with parameters that have substituted expressions in the replay"""
825+
a = Parameter("a")
826+
b = Parameter("b")
827+
a1 = a * 2
828+
a2 = a1.subs({a: 3 * b})
829+
qc = QuantumCircuit(1)
830+
qc.rz(a2, 0)
831+
832+
return [qc]
833+
834+
835+
def generate_v13_fix_expr():
836+
"""Circuits that contain expressions and types new in QPY v13 (after fix)."""
837+
from qiskit.circuit.classical import expr, types
838+
839+
float_expr = QuantumCircuit(name="float_expr")
840+
with float_expr.if_test(expr.less(1.0, 2.0)):
841+
pass
842+
843+
return [float_expr]
844+
845+
823846
def generate_circuits(version_parts):
824847
"""Generate reference circuits."""
848+
825849
output_circuits = {
826850
"full.qpy": [generate_full_circuit()],
827851
"unitary.qpy": [generate_unitary_gate_circuit()],
@@ -871,6 +895,12 @@ def generate_circuits(version_parts):
871895
if version_parts >= (1, 1, 0):
872896
output_circuits["standalone_vars.qpy"] = generate_standalone_var()
873897
output_circuits["v12_expr.qpy"] = generate_v12_expr()
898+
if version_parts >= (1, 4, 1):
899+
output_circuits["replay_with_expressions.qpy"] = (
900+
generate_replay_with_expression_substitutions()
901+
)
902+
if version_parts >= (2, 0, 0):
903+
output_circuits["v14_expr.qpy"] = generate_v13_fix_expr()
874904
return output_circuits
875905

876906

@@ -968,6 +998,8 @@ def load_qpy(qpy_files, version_parts):
968998
bind = np.linspace(1.0, 2.0, 22)
969999
elif path == "parameter_vector_expression.qpy":
9701000
bind = np.linspace(1.0, 2.0, 15)
1001+
elif path == "replay_with_expressions.qpy":
1002+
bind = [2.0]
9711003

9721004
assert_equal(
9731005
circuit, qpy_circuits[i], i, version_parts, bind=bind, equivalent=equivalent

0 commit comments

Comments
 (0)