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

Add use_symengine option to qpy.dump #10820

Merged
merged 20 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
Binary file added circuit.qpy
Binary file not shown.
22 changes: 15 additions & 7 deletions qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ def _dumps_register(register, index_map):
return b"\x00" + str(index_map["c"][register]).encode(common.ENCODE)


def _dumps_instruction_parameter(param, index_map):
def _dumps_instruction_parameter(param, index_map, use_symengine):
if isinstance(param, QuantumCircuit):
type_key = type_keys.Program.CIRCUIT
data_bytes = common.data_to_binary(param, write_circuit)
Expand All @@ -521,13 +521,15 @@ def _dumps_instruction_parameter(param, index_map):
type_key = type_keys.Value.REGISTER
data_bytes = _dumps_register(param, index_map)
else:
type_key, data_bytes = value.dumps_value(param, index_map=index_map)
type_key, data_bytes = value.dumps_value(
param, index_map=index_map, use_symengine=use_symengine
)

return type_key, data_bytes


# pylint: disable=too-many-boolean-expressions
def _write_instruction(file_obj, instruction, custom_operations, index_map):
def _write_instruction(file_obj, instruction, custom_operations, index_map, use_symengine):
gate_class_name = instruction.operation.__class__.__name__
custom_operations_list = []
if (
Expand Down Expand Up @@ -603,7 +605,7 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map):
value.write_value(file_obj, op_condition, index_map=index_map)
else:
file_obj.write(condition_register)
# Encode instruciton args
# Encode instruction args
for qbit in instruction.qubits:
instruction_arg_raw = struct.pack(
formats.CIRCUIT_INSTRUCTION_ARG_PACK, b"q", index_map["q"][qbit]
Expand All @@ -616,7 +618,7 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map):
file_obj.write(instruction_arg_raw)
# Encode instruction params
for param in instruction_params:
type_key, data_bytes = _dumps_instruction_parameter(param, index_map)
type_key, data_bytes = _dumps_instruction_parameter(param, index_map, use_symengine)
common.write_generic_typed_data(file_obj, type_key, data_bytes)
return custom_operations_list

Expand Down Expand Up @@ -915,7 +917,7 @@ def _read_layout(file_obj, circuit):
circuit._layout = TranspileLayout(initial_layout, input_qubit_mapping, final_layout)


def write_circuit(file_obj, circuit, metadata_serializer=None):
def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=False):
"""Write a single QuantumCircuit object in the file like object.

Args:
Expand All @@ -925,6 +927,10 @@ def write_circuit(file_obj, circuit, metadata_serializer=None):
will be passed the :attr:`.QuantumCircuit.metadata` dictionary for
``circuit`` and will be used as the ``cls`` kwarg
on the ``json.dump()`` call to JSON serialize that dictionary.
use_symengine: If True, ``ParameterExpression`` objects will be serialized using symengine's
native mechanism. This is a faster serialization alternative, but not supported in all
platforms. Please check that your target platform is supported by the symengine library
before setting this option, as it will be required by qpy to deserialize the payload.
"""
metadata_raw = json.dumps(
circuit.metadata, separators=(",", ":"), cls=metadata_serializer
Expand Down Expand Up @@ -964,7 +970,9 @@ def write_circuit(file_obj, circuit, metadata_serializer=None):
index_map["q"] = {bit: index for index, bit in enumerate(circuit.qubits)}
index_map["c"] = {bit: index for index, bit in enumerate(circuit.clbits)}
for instruction in circuit.data:
_write_instruction(instruction_buffer, instruction, custom_operations, index_map)
_write_instruction(
instruction_buffer, instruction, custom_operations, index_map, use_symengine
)

with io.BytesIO() as custom_operations_buffer:
new_custom_operations = list(custom_operations.keys())
Expand Down
79 changes: 67 additions & 12 deletions qiskit/qpy/binary_io/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""Binary IO for any value objects, such as numbers, string, parameters."""

from __future__ import annotations
import warnings

import collections.abc
import struct
Expand Down Expand Up @@ -49,10 +50,19 @@ def _write_parameter_vec(file_obj, obj):
file_obj.write(name_bytes)


def _write_parameter_expression(file_obj, obj):
from sympy import srepr, sympify
def _write_parameter_expression(file_obj, obj, use_symengine):

if use_symengine:
if not _optional.HAS_SYMENGINE:
raise exceptions.QpyError(
"``use_symengine`` requires the symengine package to be installed"
)
expr_bytes = obj._symbol_expr.__reduce__()[1][0]
else:
from sympy import srepr, sympify

expr_bytes = srepr(sympify(obj._symbol_expr)).encode(common.ENCODE)

expr_bytes = srepr(sympify(obj._symbol_expr)).encode(common.ENCODE)
param_expr_header_raw = struct.pack(
formats.PARAMETER_EXPR_PACK, len(obj._parameter_symbols), len(expr_bytes)
)
Expand All @@ -73,7 +83,7 @@ def _write_parameter_expression(file_obj, obj):
value_key = symbol_key
value_data = bytes()
else:
value_key, value_data = dumps_value(value)
value_key, value_data = dumps_value(value) # TODO: use_symengine?

elem_header = struct.pack(
formats.PARAM_EXPR_MAP_ELEM_V3_PACK,
Expand Down Expand Up @@ -219,17 +229,36 @@ def _read_parameter_vec(file_obj, vectors):


def _read_parameter_expression(file_obj):

data = formats.PARAMETER_EXPR(
*struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE))
)

from sympy.parsing.sympy_parser import parse_expr

payload = file_obj.read(data.expr_size)

if _optional.HAS_SYMENGINE:
import symengine
from symengine import sympify
from symengine.lib.symengine_wrapper import load_basic

try:
expr_ = load_basic(payload)

expr_ = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)))
except RuntimeError:
warnings.warn("Symengine loading failed, trying with sympy...")
expr_ = sympify(parse_expr(payload.decode(common.ENCODE)))
else:
expr_ = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))
warnings.warn("Symengine not found, trying with sympy...")

try:
expr_ = parse_expr(payload.decode(common.ENCODE))

except UnicodeDecodeError as exc:
raise exceptions.QpyError(
"Decoding failed. Did you serialize using symengine?"
) from exc

symbol_map = {}
for _ in range(data.map_elements):
elem_data = formats.PARAM_EXPR_MAP_ELEM(
Expand Down Expand Up @@ -260,17 +289,36 @@ def _read_parameter_expression(file_obj):


def _read_parameter_expression_v3(file_obj, vectors):

data = formats.PARAMETER_EXPR(
*struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE))
)

from sympy.parsing.sympy_parser import parse_expr

payload = file_obj.read(data.expr_size)

if _optional.HAS_SYMENGINE:
import symengine
from symengine import sympify
from symengine.lib.symengine_wrapper import load_basic

try:
expr_ = load_basic(payload)

expr_ = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)))
except RuntimeError:
warnings.warn("Symengine loading failed, trying with sympy...")
expr_ = sympify(parse_expr(payload.decode(common.ENCODE)))
else:
expr_ = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))
warnings.warn("Symengine not found, trying with sympy...")
ElePT marked this conversation as resolved.
Show resolved Hide resolved

try:
expr_ = parse_expr(payload.decode(common.ENCODE))

except UnicodeDecodeError as exc:
ElePT marked this conversation as resolved.
Show resolved Hide resolved
raise exceptions.QpyError(
"Decoding failed. Did you serialize using symengine?"
) from exc

symbol_map = {}
for _ in range(data.map_elements):
elem_data = formats.PARAM_EXPR_MAP_ELEM_V3(
Expand Down Expand Up @@ -393,14 +441,19 @@ def _read_expr_type(file_obj) -> types.Type:
raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'")


def dumps_value(obj, *, index_map=None):
def dumps_value(obj, *, index_map=None, use_symengine=False):
"""Serialize input value object.

Args:
obj (any): Arbitrary value object to serialize.
use_symengine (bool): Use symengine native serialization
index_map (dict): Dictionary with two keys, "q" and "c". Each key has a value that is a
dictionary mapping :class:`.Qubit` or :class:`.Clbit` instances (respectively) to their
integer indices.
use_symengine: If True, ``ParameterExpression`` objects will be serialized using symengine's
native mechanism. This is a faster serialization alternative, but not supported in all
platforms. Please check that your target platform is supported by the symengine library
before setting this option, as it will be required by qpy to deserialize the payload.

Returns:
tuple: TypeKey and binary data.
Expand All @@ -427,7 +480,9 @@ def dumps_value(obj, *, index_map=None):
elif type_key == type_keys.Value.PARAMETER:
binary_data = common.data_to_binary(obj, _write_parameter)
elif type_key == type_keys.Value.PARAMETER_EXPRESSION:
binary_data = common.data_to_binary(obj, _write_parameter_expression)
binary_data = common.data_to_binary(
obj, _write_parameter_expression, use_symengine=use_symengine
)
elif type_key == type_keys.Value.EXPRESSION:
clbit_indices = {} if index_map is None else index_map["c"]
binary_data = common.data_to_binary(obj, _write_expr, clbit_indices=clbit_indices)
Expand Down
16 changes: 14 additions & 2 deletions qiskit/qpy/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def dump(
programs: Union[List[QPY_SUPPORTED_TYPES], QPY_SUPPORTED_TYPES],
file_obj: BinaryIO,
metadata_serializer: Optional[Type[JSONEncoder]] = None,
use_symengine: bool = False,
):
"""Write QPY binary data to a file

Expand Down Expand Up @@ -122,7 +123,10 @@ def dump(
metadata_serializer: An optional JSONEncoder class that
will be passed the ``.metadata`` attribute for each program in ``programs`` and will be
used as the ``cls`` kwarg on the `json.dump()`` call to JSON serialize that dictionary.

use_symengine: If True, ``ParameterExpression`` objects will be serialized using symengine's
native mechanism. This is a faster serialization alternative, but not supported in all
platforms. Please check that your target platform is supported by the symengine library
before setting this option, as it will be required by qpy to deserialize the payload.
Raises:
QpyError: When multiple data format is mixed in the output.
TypeError: When invalid data type is input.
Expand Down Expand Up @@ -165,7 +169,15 @@ def dump(
common.write_type_key(file_obj, type_key)

for program in programs:
writer(file_obj, program, metadata_serializer=metadata_serializer)
if issubclass(program_type, QuantumCircuit):
writer(
file_obj,
program,
metadata_serializer=metadata_serializer,
use_symengine=use_symengine,
)
else:
writer(file_obj, program, metadata_serializer=metadata_serializer)


def load(
Expand Down
25 changes: 25 additions & 0 deletions test/python/circuit/test_circuit_load_from_qpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,31 @@ def test_qpy_full_path(self):
self.assertEqual(q_circuit.name, new_circ.name)
self.assertDeprecatedBitProperties(q_circuit, new_circ)

def test_qpy_symengine(self):
"""Test use_symengine option for circuit with parameter expressions."""
theta = Parameter("theta")
phi = Parameter("phi")
sum_param = theta + phi
qc = QuantumCircuit(5, 1)
qc.h(0)
for i in range(4):
qc.cx(i, i + 1)
qc.barrier()
qc.rz(sum_param, range(3))
qc.rz(phi, 3)
qc.rz(theta, 4)
qc.barrier()
for i in reversed(range(4)):
qc.cx(i, i + 1)
qc.h(0)
qc.measure(0, 0)
qpy_file = io.BytesIO()
dump(qc, qpy_file, use_symengine=True)
qpy_file.seek(0)
new_circ = load(qpy_file)[0]
self.assertEqual(qc, new_circ)
self.assertDeprecatedBitProperties(qc, new_circ)

def test_circuit_with_conditional(self):
"""Test that instructions with conditions are correctly serialized."""
qc = QuantumCircuit(1, 1)
Expand Down
Loading