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 10 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.
45 changes: 44 additions & 1 deletion qiskit/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
serialization format for :class:`~qiskit.circuit.QuantumCircuit` objects in Qiskit. The basic
file format is as follows:

A QPY file (or memory object) always starts with the following 7
A QPY file (or memory object) always starts with the following 6
ElePT marked this conversation as resolved.
Show resolved Hide resolved
byte UTF8 string: ``QISKIT`` which is immediately followed by the overall
file header. The contents of the file header as defined as a C struct are:

Expand All @@ -116,6 +116,21 @@
uint64_t num_circuits;
}


From V10 on, a new field is added to the file header struct to represent the
encoding scheme used for symbolic expressions:

.. code-block:: c

struct {
uint8_t qpy_version;
uint8_t qiskit_major_version;
uint8_t qiskit_minor_version;
uint8_t qiskit_patch_version;
uint64_t num_circuits;
char symbolic_encoding;
}

All values use network byte order [#f1]_ (big endian) for cross platform
compatibility.

Expand All @@ -128,6 +143,34 @@
by ``num_circuits`` in the file header). There is no padding between the
circuits in the data.

.. _qpy_version_10:

Version 10
==========

Version 10 adds support for symengine-native serialization for objects of type
:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks.

The symbolic_encoding field is added to the file header, and a new encoding type char
is introduced, mapped to each serialization library as follows: ``p`` refers to sympy
ElePT marked this conversation as resolved.
Show resolved Hide resolved
encoding and ``e`` refers to symengine encoding.

FILE_HEADER
-----------

The contents of FILE_HEADER after V10 are defined as a C struct as:

.. code-block:: c

struct {
uint8_t qpy_version;
uint8_t qiskit_major_version;
uint8_t qiskit_minor_version;
uint8_t qiskit_patch_version;
uint64_t num_circuits;
char symbolic_encoding;
}


.. _qpy_version_9:

Expand Down
87 changes: 63 additions & 24 deletions qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@


def _read_header_v2(file_obj, version, vectors, metadata_deserializer=None):

data = formats.CIRCUIT_HEADER_V2._make(
struct.unpack(
formats.CIRCUIT_HEADER_V2_PACK,
file_obj.read(formats.CIRCUIT_HEADER_V2_SIZE),
)
)

name = file_obj.read(data.name_size).decode(common.ENCODE)
global_phase = value.loads_value(
data.global_phase_type,
Expand All @@ -62,6 +64,7 @@ def _read_header_v2(file_obj, version, vectors, metadata_deserializer=None):
"num_registers": data.num_registers,
"num_instructions": data.num_instructions,
}

metadata_raw = file_obj.read(data.metadata_size)
metadata = json.loads(metadata_raw, cls=metadata_deserializer)
return header, name, metadata
Expand Down Expand Up @@ -124,7 +127,9 @@ def _read_registers(file_obj, num_registers):
return registers


def _loads_instruction_parameter(type_key, data_bytes, version, vectors, registers, circuit):
def _loads_instruction_parameter(
type_key, data_bytes, version, vectors, registers, circuit, use_symengine
):
if type_key == type_keys.Program.CIRCUIT:
param = common.data_from_binary(data_bytes, read_circuit, version=version)
elif type_key == type_keys.Container.RANGE:
Expand All @@ -139,6 +144,7 @@ def _loads_instruction_parameter(type_key, data_bytes, version, vectors, registe
vectors=vectors,
registers=registers,
circuit=circuit,
use_symengine=use_symengine,
)
)
elif type_key == type_keys.Value.INTEGER:
Expand All @@ -151,7 +157,13 @@ def _loads_instruction_parameter(type_key, data_bytes, version, vectors, registe
param = _loads_register_param(data_bytes.decode(common.ENCODE), circuit, registers)
else:
param = value.loads_value(
type_key, data_bytes, version, vectors, clbits=circuit.clbits, cregs=registers["c"]
type_key,
data_bytes,
version,
vectors,
clbits=circuit.clbits,
cregs=registers["c"],
use_symengine=use_symengine,
)

return param
Expand All @@ -165,7 +177,9 @@ def _loads_register_param(data_bytes, circuit, registers):
return registers["c"][data_bytes]


def _read_instruction(file_obj, circuit, registers, custom_operations, version, vectors):
def _read_instruction(
file_obj, circuit, registers, custom_operations, version, vectors, use_symengine
):
if version < 5:
instruction = formats.CIRCUIT_INSTRUCTION._make(
struct.unpack(
Expand Down Expand Up @@ -196,7 +210,12 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
)
elif version >= 5 and instruction.conditional_key == type_keys.Condition.EXPRESSION:
condition = value.read_value(
file_obj, version, vectors, clbits=circuit.clbits, cregs=registers["c"]
file_obj,
version,
vectors,
clbits=circuit.clbits,
cregs=registers["c"],
use_symengine=use_symengine,
)
if circuit is not None:
qubit_indices = dict(enumerate(circuit.qubits))
Expand Down Expand Up @@ -232,14 +251,14 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
for _param in range(instruction.num_parameters):
type_key, data_bytes = common.read_generic_typed_data(file_obj)
param = _loads_instruction_parameter(
type_key, data_bytes, version, vectors, registers, circuit
type_key, data_bytes, version, vectors, registers, circuit, use_symengine
)
params.append(param)

# Load Gate object
if gate_name in {"Gate", "Instruction", "ControlledGate"}:
inst_obj = _parse_custom_operation(
custom_operations, gate_name, params, version, vectors, registers
custom_operations, gate_name, params, version, vectors, registers, use_symengine
)
inst_obj.condition = condition
if instruction.label_size > 0:
Expand All @@ -250,7 +269,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
return None
elif gate_name in custom_operations:
inst_obj = _parse_custom_operation(
custom_operations, gate_name, params, version, vectors, registers
custom_operations, gate_name, params, version, vectors, registers, use_symengine
)
inst_obj.condition = condition
if instruction.label_size > 0:
Expand Down Expand Up @@ -317,7 +336,9 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
return None


def _parse_custom_operation(custom_operations, gate_name, params, version, vectors, registers):
def _parse_custom_operation(
custom_operations, gate_name, params, version, vectors, registers, use_symengine
):
if version >= 5:
(
type_str,
Expand Down Expand Up @@ -346,7 +367,7 @@ def _parse_custom_operation(custom_operations, gate_name, params, version, vecto
if version >= 5 and type_key == type_keys.CircuitInstruction.CONTROLLED_GATE:
with io.BytesIO(base_gate_raw) as base_gate_obj:
base_gate = _read_instruction(
base_gate_obj, None, registers, custom_operations, version, vectors
base_gate_obj, None, registers, custom_operations, version, vectors, use_symengine
)
if ctrl_state < 2**num_ctrl_qubits - 1:
# If open controls, we need to discard the control suffix when setting the name.
Expand Down Expand Up @@ -497,7 +518,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 @@ -507,7 +528,7 @@ def _dumps_instruction_parameter(param, index_map):
elif isinstance(param, tuple):
type_key = type_keys.Container.TUPLE
data_bytes = common.sequence_to_binary(
param, _dumps_instruction_parameter, index_map=index_map
param, _dumps_instruction_parameter, index_map=index_map, use_symengine=use_symengine
)
elif isinstance(param, int):
# TODO This uses little endian. This should be fixed in next QPY version.
Expand All @@ -521,13 +542,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 +626,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 +639,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 @@ -661,7 +684,7 @@ def _write_elem(buffer, op):
file_obj.write(synth_data)


def _write_custom_operation(file_obj, name, operation, custom_operations):
def _write_custom_operation(file_obj, name, operation, custom_operations, use_symengine):
type_key = type_keys.CircuitInstruction.assign(operation)
has_definition = False
size = 0
Expand Down Expand Up @@ -700,7 +723,11 @@ def _write_custom_operation(file_obj, name, operation, custom_operations):
else:
with io.BytesIO() as base_gate_buffer:
new_custom_instruction = _write_instruction(
base_gate_buffer, CircuitInstruction(base_gate, (), ()), custom_operations, {}
base_gate_buffer,
CircuitInstruction(base_gate, (), ()),
custom_operations,
{},
use_symengine,
)
base_gate_raw = base_gate_buffer.getvalue()
name_raw = name.encode(common.ENCODE)
Expand Down Expand Up @@ -915,7 +942,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 +952,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 (bool): If True, symbolic 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 +995,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 All @@ -975,7 +1008,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None):
operation = custom_operations[name]
new_custom_operations.extend(
_write_custom_operation(
custom_operations_buffer, name, operation, custom_operations
custom_operations_buffer, name, operation, custom_operations, use_symengine
)
)

Expand All @@ -990,7 +1023,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None):
_write_layout(file_obj, circuit)


def read_circuit(file_obj, version, metadata_deserializer=None):
def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=False):
"""Read a single QuantumCircuit object from the file like object.

Args:
Expand All @@ -1003,7 +1036,11 @@ def read_circuit(file_obj, version, metadata_deserializer=None):
in the file-like object. If this is not specified the circuit metadata will
be parsed as JSON with the stdlib ``json.load()`` function using
the default ``JSONDecoder`` class.

use_symengine (bool): If True, symbolic objects will be de-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:
QuantumCircuit: The circuit object from the file.

Expand All @@ -1023,7 +1060,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None):
num_clbits = header["num_clbits"]
num_registers = header["num_registers"]
num_instructions = header["num_instructions"]
# `out_registers` is two "name: registter" maps segregated by type for the rest of QPY, and
# `out_registers` is two "name: register" maps segregated by type for the rest of QPY, and
# `all_registers` is the complete ordered list used to construct the `QuantumCircuit`.
out_registers = {"q": {}, "c": {}}
all_registers = []
Expand Down Expand Up @@ -1089,7 +1126,9 @@ def read_circuit(file_obj, version, metadata_deserializer=None):
)
custom_operations = _read_custom_operations(file_obj, version, vectors)
for _instruction in range(num_instructions):
_read_instruction(file_obj, circ, out_registers, custom_operations, version, vectors)
_read_instruction(
file_obj, circ, out_registers, custom_operations, version, vectors, use_symengine
)

# Read calibrations
if version >= 5:
Expand Down
Loading
Loading