Skip to content
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

QPY serialization of ParameterExpression throws error #13879

Open
jlapeyre opened this issue Feb 19, 2025 · 4 comments · May be fixed by #13890
Open

QPY serialization of ParameterExpression throws error #13879

jlapeyre opened this issue Feb 19, 2025 · 4 comments · May be fixed by #13890

Comments

@jlapeyre
Copy link
Contributor

jlapeyre commented Feb 19, 2025

  • Qiskit version: 1.3.2
  • qiskit-ibm-runtime version: 0.36.1
  • Python version: 3.12

Certain ParameterExpressions cause QPY serialization v13 to throw. QPY v12 does not throw.

Note

After opening this issue I found the following much simpler script for triggering the bug:

from qiskit.circuit import Parameter, ParameterExpression
from qiskit import qpy

a = Parameter("a")
b = Parameter("b")
a1 = a * 2
a2 = a1.subs({a: 2 * b})

qpy.binary_io.value.dumps_value(a2, version=13);

There is no error with QPY version 12.

When running the script above, the error,

AttributeError: 'ParameterExpression' object has no attribute 'name'

is thrown here at line 40

def _write_parameter(file_obj, obj):
name_bytes = obj.name.encode(common.ENCODE)
file_obj.write(struct.pack(formats.PARAMETER_PACK, len(name_bytes), obj.uuid.bytes))
file_obj.write(name_bytes)

I tried to track down the problem, which is centered on _qpy_replay. But there are no documentation or comments describing _qpy_replay and several adjacent structures and functions.

Note

The following script is not a minimal description of the bug. But it is an example abstracted from using EstimatorQNN and NeuralNetworkClassifier from qiskit-machine-learning on a real IBM device (in this case, ibm_kyiv). It's unlikely that simple workflows involving EstimatorQNN will work when this bug is present.

Running this script will trigger the bug:

from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes
from qiskit import QuantumCircuit
from qiskit_ibm_runtime import fake_provider
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

def make_circuit():
    n_qubits = 2
    reps = 1
    feature_map = ZZFeatureMap(feature_dimension=n_qubits, reps=reps)
    ansatz = RealAmplitudes(num_qubits=n_qubits, reps=reps)
    qc = QuantumCircuit(n_qubits)
    qc.compose(feature_map, inplace=True)
    qc.compose(ansatz, inplace=True)
    backend = fake_provider.FakeKyiv()
    pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
    isa_qc = pm.run(qc)
    return isa_qc

from qiskit import qpy
isa_qc = make_circuit()
qpy.binary_io.value.dumps_value(isa_qc.global_phase, version=13);

If I replace versions=13 with version=12, I see no apparent problem.

@mtreinish
Copy link
Member

I checked and this bug is present in the public api for qpy too. Calling qiskit.qpy.dump() will trigger the bug if the parameter expression as defined in the reproducer is used on a gate. qpy.binary_io.value.dumps_value is not part of the public api so I just wanted to rule out that it's not an issue from calling the internals directly.

@jlapeyre
Copy link
Contributor Author

@mtreinish, that's important. It would have been better if I included an explicit user-level API example in the bug report. I think both things are useful. Showing that a user will encounter the bug, and a minimal example.

This came up in code from collaborators. It's a simple machine learning example. I think most of the code is from a tutorial. It works fine on a fake backend. But fails with a real backend because of the qpy serialiazation. On the application side, QNNEstimator has only been supported on real V2 backends for a couple of months. I have not seen any other mention of people running into this problem.

@nonhermitian
Copy link
Contributor

Ran into this as well.

@sanketsharma
Copy link

I ran in to this when trying to run a pub with Qiskit Runtime's EstimatorV2.

  warnings.warn(
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[27], line 1
----> 1 job_ids = submit_jobs(backend=ibm_torino, pubs=pubs_torino)

File ~/git/deuteron/python/utils.py:124, in submit_jobs(pubs, backend)
    121 estimator.options.resilience_level = 0
    123 # Run job with no error mitigation
--> 124 job0 = estimator.run(pubs)
    125 jobs.append(job0)
    127 # Add dynamical decoupling (DD)

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/estimator.py:149, in EstimatorV2.run(self, pubs, precision)
    147 coerced_pubs = [EstimatorPub.coerce(pub, precision) for pub in pubs]
    148 validate_estimator_pubs(coerced_pubs)
--> 149 return self._run(coerced_pubs)

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/base_primitive.py:168, in BasePrimitiveV2._run(self, pubs)
    166 # Batch or Session
    167 if self._mode:
--> 168     return self._mode._run(
    169         program_id=self._program_id(),
    170         inputs=primitive_inputs,
    171         options=runtime_options,
    172         callback=options_dict.get("environment", {}).get("callback", None),
    173         result_decoder=DEFAULT_DECODERS.get(self._program_id()),
    174     )
    176 if self._backend:
    177     runtime_options["backend"] = self._backend

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/session.py:41, in _active_session.<locals>._wrapper(self, *args, **kwargs)
     39 if not self._active:
     40     raise IBMRuntimeError("The session is closed.")
---> 41 return func(self, *args, **kwargs)

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/session.py:169, in Session._run(self, program_id, inputs, options, callback, result_decoder)
    166 options["backend"] = self._backend
    168 if isinstance(self._service, QiskitRuntimeService):
--> 169     job = self._service._run(
    170         program_id=program_id,  # type: ignore[arg-type]
    171         options=options,
    172         inputs=inputs,
    173         session_id=self._session_id,
    174         start_session=False,
    175         callback=callback,
    176         result_decoder=result_decoder,
    177     )
    179     if self._backend is None:
    180         self._backend = job.backend()

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/qiskit_runtime_service.py:839, in QiskitRuntimeService._run(self, program_id, inputs, options, callback, result_decoder, session_id, start_session)
    837 version = inputs.get("version", 1) if inputs else 1
    838 try:
--> 839     response = self._api_client.program_run(
    840         program_id=program_id,
    841         backend_name=qrt_options.get_backend_name(),
    842         params=inputs,
    843         image=qrt_options.image,
    844         hgp=hgp_name,
    845         log_level=qrt_options.log_level,
    846         session_id=session_id,
    847         job_tags=qrt_options.job_tags,
    848         max_execution_time=qrt_options.max_execution_time,
    849         start_session=start_session,
    850         session_time=qrt_options.session_time,
    851         private=qrt_options.private,
    852     )
    853     if self._channel == "ibm_quantum":
    854         messages = response.get("messages")

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/api/clients/runtime.py:88, in RuntimeClient.program_run(self, program_id, backend_name, params, image, hgp, log_level, session_id, job_tags, max_execution_time, start_session, session_time, private)
     86     hub, group, project = from_instance_format(hgp)
     87     hgp_dict = {"hub": hub, "group": group, "project": project}
---> 88 return self._api.program_run(
     89     program_id=program_id,
     90     backend_name=backend_name,
     91     params=params,
     92     image=image,
     93     log_level=log_level,
     94     session_id=session_id,
     95     job_tags=job_tags,
     96     max_execution_time=max_execution_time,
     97     start_session=start_session,
     98     session_time=session_time,
     99     private=private,
    100     **hgp_dict,
    101 )

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/api/rest/runtime.py:128, in Runtime.program_run(self, program_id, backend_name, params, image, hub, group, project, log_level, session_id, job_tags, max_execution_time, start_session, session_time, private)
    126 if private:
    127     payload["private"] = True
--> 128 data = json.dumps(payload, cls=RuntimeEncoder)
    129 return self.session.post(
    130     url, data=data, timeout=900, headers=self._HEADER_JSON_CONTENT
    131 ).json()

File ~/anaconda3/envs/qiskit-1/lib/python3.12/json/__init__.py:238, in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    232 if cls is None:
    233     cls = JSONEncoder
    234 return cls(
    235     skipkeys=skipkeys, ensure_ascii=ensure_ascii,
    236     check_circular=check_circular, allow_nan=allow_nan, indent=indent,
    237     separators=separators, default=default, sort_keys=sort_keys,
--> 238     **kw).encode(obj)

File ~/anaconda3/envs/qiskit-1/lib/python3.12/json/encoder.py:200, in JSONEncoder.encode(self, o)
    196         return encode_basestring(o)
    197 # This doesn't pass the iterator directly to ''.join() because the
    198 # exceptions aren't as detailed.  The list call should be roughly
    199 # equivalent to the PySequence_Fast that ''.join() would do.
--> 200 chunks = self.iterencode(o, _one_shot=True)
    201 if not isinstance(chunks, (list, tuple)):
    202     chunks = list(chunks)

File ~/anaconda3/envs/qiskit-1/lib/python3.12/json/encoder.py:258, in JSONEncoder.iterencode(self, o, _one_shot)
    253 else:
    254     _iterencode = _make_iterencode(
    255         markers, self.default, _encoder, self.indent, floatstr,
    256         self.key_separator, self.item_separator, self.sort_keys,
    257         self.skipkeys, _one_shot)
--> 258 return _iterencode(o, 0)

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/utils/json.py:259, in RuntimeEncoder.default(self, obj)
    255 if isinstance(obj, QuantumCircuit):
    256     kwargs: Dict[str, object] = {
    257         "version": min(SERVICE_MAX_SUPPORTED_QPY_VERSION, QISKIT_QPY_VERSION)
    258     }
--> 259     value = _serialize_and_encode(
    260         data=obj,
    261         serializer=lambda buff, data: dump(
    262             data, buff, RuntimeEncoder, **kwargs
    263         ),  # type: ignore[no-untyped-call]
    264     )
    265     return {"__type__": "QuantumCircuit", "__value__": value}
    266 if isinstance(obj, Parameter):

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/utils/json.py:116, in _serialize_and_encode(data, serializer, compress, **kwargs)
    104 """Serialize the input data and return the encoded string.
    105 
    106 Args:
   (...)
    113     String representation.
    114 """
    115 with io.BytesIO() as buff:
--> 116     serializer(buff, data, **kwargs)
    117     buff.seek(0)
    118     serialized_data = buff.read()

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit_ibm_runtime/utils/json.py:261, in RuntimeEncoder.default.<locals>.<lambda>(buff, data)
    255 if isinstance(obj, QuantumCircuit):
    256     kwargs: Dict[str, object] = {
    257         "version": min(SERVICE_MAX_SUPPORTED_QPY_VERSION, QISKIT_QPY_VERSION)
    258     }
    259     value = _serialize_and_encode(
    260         data=obj,
--> 261         serializer=lambda buff, data: dump(
    262             data, buff, RuntimeEncoder, **kwargs
    263         ),  # type: ignore[no-untyped-call]
    264     )
    265     return {"__type__": "QuantumCircuit", "__value__": value}
    266 if isinstance(obj, Parameter):

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/utils/deprecation.py:184, in deprecate_arg.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    171 @functools.wraps(func)
    172 def wrapper(*args, **kwargs):
    173     _maybe_warn_and_rename_kwarg(
    174         args,
    175         kwargs,
   (...)
    182         predicate=predicate,
    183     )
--> 184     return func(*args, **kwargs)

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/interface.py:222, in dump(programs, file_obj, metadata_serializer, use_symengine, version)
    220     if type_key == type_keys.Program.CIRCUIT and program._calibrations_prop:
    221         pulse_gates = True
--> 222     writer(
    223         file_obj,
    224         program,
    225         metadata_serializer=metadata_serializer,
    226         use_symengine=use_symengine,
    227         version=version,
    228     )
    230 if pulse_gates:
    231     warnings.warn(
    232         category=DeprecationWarning,
    233         message="Pulse gates serialization is deprecated as of Qiskit 1.3. "
    234         "It will be removed in Qiskit 2.0.",
    235     )

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/binary_io/circuits.py:1290, in write_circuit(file_obj, circuit, metadata_serializer, use_symengine, version)
   1288 index_map["c"] = {bit: index for index, bit in enumerate(circuit.clbits)}
   1289 for instruction in circuit.data:
-> 1290     _write_instruction(
   1291         instruction_buffer,
   1292         instruction,
   1293         custom_operations,
   1294         index_map,
   1295         use_symengine,
   1296         version,
   1297         standalone_var_indices=standalone_var_indices,
   1298     )
   1300 with io.BytesIO() as custom_operations_buffer:
   1301     new_custom_operations = list(custom_operations.keys())

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/binary_io/circuits.py:847, in _write_instruction(file_obj, instruction, custom_operations, index_map, use_symengine, version, standalone_var_indices)
    845 # Encode instruction params
    846 for param in instruction_params:
--> 847     type_key, data_bytes = _dumps_instruction_parameter(
    848         param,
    849         index_map,
    850         use_symengine,
    851         version=version,
    852         standalone_var_indices=standalone_var_indices,
    853     )
    854     common.write_generic_typed_data(file_obj, type_key, data_bytes)
    855 return custom_operations_list

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/binary_io/circuits.py:710, in _dumps_instruction_parameter(param, index_map, use_symengine, version, standalone_var_indices)
    708     data_bytes = _dumps_register(param, index_map)
    709 else:
--> 710     type_key, data_bytes = value.dumps_value(
    711         param,
    712         index_map=index_map,
    713         use_symengine=use_symengine,
    714         standalone_var_indices=standalone_var_indices,
    715         version=version,
    716     )
    718 return type_key, data_bytes

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/binary_io/value.py:856, in dumps_value(obj, version, index_map, use_symengine, standalone_var_indices)
    854     binary_data = common.data_to_binary(obj, _write_parameter)
    855 elif type_key == type_keys.Value.PARAMETER_EXPRESSION:
--> 856     binary_data = common.data_to_binary(
    857         obj, _write_parameter_expression, use_symengine=use_symengine, version=version
    858     )
    859 elif type_key == type_keys.Value.EXPRESSION:
    860     clbit_indices = {} if index_map is None else index_map["c"]

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/common.py:203, in data_to_binary(obj, serializer, **kwargs)
    192 """Convert object into binary data with specified serializer.
    193 
    194 Args:
   (...)
    200     bytes: Binary data.
    201 """
    202 with io.BytesIO() as container:
--> 203     serializer(container, obj, **kwargs)
    204     binary_data = container.getvalue()
    206 return binary_data

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/binary_io/value.py:238, in _write_parameter_expression(file_obj, obj, use_symengine, version)
    236     symbol_data = common.data_to_binary(symbol, _write_parameter_vec)
    237 else:
--> 238     symbol_data = common.data_to_binary(symbol, _write_parameter)
    239 # serialize value
    240 value_key, value_data = dumps_value(
    241     symbol, version=version, use_symengine=use_symengine
    242 )

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/common.py:203, in data_to_binary(obj, serializer, **kwargs)
    192 """Convert object into binary data with specified serializer.
    193 
    194 Args:
   (...)
    200     bytes: Binary data.
    201 """
    202 with io.BytesIO() as container:
--> 203     serializer(container, obj, **kwargs)
    204     binary_data = container.getvalue()
    206 return binary_data

File ~/anaconda3/envs/qiskit-1/lib/python3.12/site-packages/qiskit/qpy/binary_io/value.py:40, in _write_parameter(file_obj, obj)
     39 def _write_parameter(file_obj, obj):
---> 40     name_bytes = obj.name.encode(common.ENCODE)
     41     file_obj.write(struct.pack(formats.PARAMETER_PACK, len(name_bytes), obj.uuid.bytes))
     42     file_obj.write(name_bytes)

AttributeError: 'ParameterExpression' object has no attribute 'name'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants