Skip to content

Commit

Permalink
Add use_symengine option to qpy.dump (Qiskit#10820)
Browse files Browse the repository at this point in the history
* Add use-symengine option

* Fix lint

* Improve error message

* Apply feedback

* Update docs

* Move option to file header, add schedule block

* Add example to docs

* Fix lint

* Apply second round of feedback

* Fix lint, update tests

* Update docs

* Fix use of require_now

* Fix compatibility test

* Make tests optional

* Fix lint

* Add release note

* Update exception message

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* Update other message

* Rename Encoding to SymExprEncoding

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
  • Loading branch information
2 people authored and rupeshknn committed Oct 9, 2023
1 parent 4426ad7 commit dfd2572
Show file tree
Hide file tree
Showing 12 changed files with 460 additions and 90 deletions.
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
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 symbolic library as follows: ``p`` refers to sympy
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 @@ -43,12 +43,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 @@ -63,6 +65,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 @@ -125,7 +128,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 @@ -140,6 +145,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 @@ -152,7 +158,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 @@ -166,7 +178,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 @@ -197,7 +211,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 @@ -233,14 +252,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 @@ -251,7 +270,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 @@ -329,7 +348,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 @@ -358,7 +379,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 @@ -509,7 +530,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 @@ -519,7 +540,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 @@ -533,13 +554,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 @@ -619,7 +642,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 @@ -632,7 +655,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 @@ -677,7 +700,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 @@ -716,7 +739,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 @@ -931,7 +958,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 @@ -941,6 +968,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 @@ -980,7 +1011,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 @@ -991,7 +1024,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 @@ -1006,7 +1039,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 @@ -1019,7 +1052,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 @@ -1039,7 +1076,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 @@ -1105,7 +1142,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

0 comments on commit dfd2572

Please sign in to comment.