Skip to content

Commit

Permalink
Add Expr support to QPY for conditions and targets (#10392)
Browse files Browse the repository at this point in the history
* Add `Expr` support to QPY for conditions and targets

This adds support to QPY for the current `Expr` nodes, for *all*
instruction parameters and conditions.  The `Expr` tree is written out
to the file in a sort of forwards Polish notation; each node has a type
code and header, followed by a type-code-specific number of `Expr`
children.

While only `IfElseOp.condition`, `WhileLoopOp.condition` and
`SwitchCaseOp.target` are allowed to have these nodes in Terra's data
model right now, the QPY serialisation does not need to have this
arbitrary restriction, and it's much easier just to write the general
case.

The backwards-compatibility guarantees of QPY are now brought to bear on
the `Unary.Op` and `Binary.Op` enumeration values. They were already
marked in the source code as their values needing to be part of the
stable public interface, and their use in things like QPY is the reason
why.

* Improve documentation of `EXPRESSION` payload

* Factor out magic numbers from discriminator sizes
  • Loading branch information
jakelishman authored Jul 19, 2023
1 parent 9a46c5a commit 02886d0
Show file tree
Hide file tree
Showing 8 changed files with 826 additions and 35 deletions.
147 changes: 147 additions & 0 deletions qiskit/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,151 @@
by ``num_circuits`` in the file header). There is no padding between the
circuits in the data.
.. _qpy_version_9:
Version 9
=========
Version 9 addds support for classical :class:`~.expr.Expr` nodes and their associated
:class:`~.types.Type`\\ s.
EXPRESSION
----------
An :class:`~.expr.Expr` node is represented by a stream of variable-width data. A node itself is
represented by (in order in the byte stream):
#. a one-byte type code discriminator;
#. an EXPR_TYPE object;
#. a type-code-specific additional payload;
#. a type-code-specific number of child EXPRESSION payloads (the number of these is implied by the
type code and not explicitly stored).
Each of these are described in the following table:
====================== ========= ======================================================= ========
Qiskit class Type code Payload Children
====================== ========= ======================================================= ========
:class:`~.expr.Var` ``x`` One EXPR_VAR. 0
:class:`~.expr.Value` ``v`` One EXPR_VALUE. 0
:class:`~.expr.Cast` ``c`` One ``_Bool`` that corresponds to the value of 1
``implicit``.
:class:`~.expr.Unary` ``u`` One ``uint8_t`` with the same numeric value as the 1
:class:`.Unary.Op`.
:class:`~.expr.Binary` ``b`` One ``uint8_t`` with the same numeric value as the 2
:class:`.Binary.Op`.
====================== ========= ======================================================= ========
EXPR_TYPE
---------
A :class:`~.types.Type` is encoded by a single-byte ASCII ``char`` that encodes the kind of type,
followed by a payload that varies depending on the type. The defined codes are:
====================== ========= =================================================================
Qiskit class Type code Payload
====================== ========= =================================================================
:class:`~.types.Bool` ``b`` None.
:class:`~.types.Uint` ``u`` One ``uint32_t width``.
====================== ========= =================================================================
EXPR_VAR
--------
This represents a runtime variable of a :class:`~.expr.Var` node. These are a type code, followed
by a type-code-specific payload:
=========================== ========= ============================================================
Python class Type code Payload
=========================== ========= ============================================================
:class:`.Clbit` ``C`` One ``uint32_t index`` that is the index of the
:class:`.Clbit` in the containing circuit.
:class:`.ClassicalRegister` ``R`` One ``uint16_t reg_name_size``, followed by that many bytes
of UTF-8 string data of the register name.
=========================== ========= ============================================================
EXPR_VALUE
----------
This represents a literal object in the classical type system, such as an integer. Currently there
are very few such literals. These are encoded as a type code, followed by a type-code-specific
payload.
=========== ========= ============================================================================
Python type Type code Payload
=========== ========= ============================================================================
``bool`` ``b`` One ``_Bool value``.
``int`` ``i`` One ``uint8_t num_bytes``, followed by the integer encoded into that many
many bytes (network order) in a two's complement representation.
=========== ========= ============================================================================
Changes to INSTRUCTION
----------------------
To support the use of :class:`~.expr.Expr` nodes in the fields :attr:`.IfElseOp.condition`,
:attr:`.WhileLoopOp.condition` and :attr:`.SwitchCaseOp.target`, the INSTRUCTION struct is changed
in an ABI compatible-manner to :ref:`its previous definition <qpy_instruction_v5>`. The new struct
is the C struct:
.. code-block:: c
struct {
uint16_t name_size;
uint16_t label_size;
uint16_t num_parameters;
uint32_t num_qargs;
uint32_t num_cargs;
uint8_t conditional_key;
uint16_t conditional_reg_name_size;
int64_t conditional_value;
uint32_t num_ctrl_qubits;
uint32_t ctrl_state;
}
where the only change is that a ``uint8_t conditional_key`` entry has replaced ``_Bool
has_conditional``. This new ``conditional_key`` takes the following numeric values, with these
effects:
===== =============================================================================================
Value Effects
===== =============================================================================================
0 The instruction has its ``.condition`` field set to ``None``. The
``conditional_reg_name_size`` and ``conditional_value`` fields should be ignored.
1 The instruction has its ``.condition`` field set to a 2-tuple of either a :class:`.Clbit`
or a :class:`.ClassicalRegister`, and a integer of value ``conditional_value``. The
INSTRUCTION payload, including its trailing data is parsed exactly as it would be in QPY
versions less than 8.
2 The instruction has its ``.condition`` field set to a :class:`~.expr.Expr` node. The
``conditional_reg_name_size`` and ``conditional_value`` fields should be ignored. The data
following the struct is followed (as in QPY versions less than 8) by ``name_size`` bytes of
UTF-8 string data for the class name and ``label_size`` bytes of UTF-8 string data for the
label (if any). Then, there is one INSTRUCTION_PARAM, which will contain an EXPRESSION. After
that, parsing continues with the INSTRUCTION_ARG structs, as in previous versions of QPY.
===== =============================================================================================
Changes to INSTRUCTION_PARAM
----------------------------
A new type code ``x`` is added that defines an EXPRESSION parameter.
.. _qpy_version_8:
Version 8
Expand Down Expand Up @@ -525,6 +670,8 @@
only :class:`~.ScheduleBlock` payload is supported.
Finally, :ref:`qpy_schedule_block` payload is packed for each CALIBRATION_DEF entry.
.. _qpy_instruction_v5:
INSTRUCTION
-----------
Expand Down
49 changes: 32 additions & 17 deletions qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from qiskit import circuit as circuit_mod
from qiskit import extensions
from qiskit.circuit import library, controlflow, CircuitInstruction
from qiskit.circuit.classical import expr
from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
from qiskit.circuit.gate import Gate
from qiskit.circuit.controlledgate import ControlledGate
Expand Down Expand Up @@ -149,7 +150,9 @@ def _loads_instruction_parameter(type_key, data_bytes, version, vectors, registe
elif type_key == type_keys.Value.REGISTER:
param = _loads_register_param(data_bytes.decode(common.ENCODE), circuit, registers)
else:
param = value.loads_value(type_key, data_bytes, version, vectors)
param = value.loads_value(
type_key, data_bytes, version, vectors, clbits=circuit.clbits, cregs=registers["c"]
)

return param

Expand Down Expand Up @@ -183,12 +186,18 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
qargs = []
cargs = []
params = []
condition_tuple = None
if instruction.has_condition:
condition_tuple = (
condition = None
if (version < 5 and instruction.has_condition) or (
version >= 5 and instruction.conditional_key == type_keys.Condition.TWO_TUPLE
):
condition = (
_loads_register_param(condition_register, circuit, registers),
instruction.condition_value,
)
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"]
)
if circuit is not None:
qubit_indices = dict(enumerate(circuit.qubits))
clbit_indices = dict(enumerate(circuit.clbits))
Expand Down Expand Up @@ -232,7 +241,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
inst_obj = _parse_custom_operation(
custom_operations, gate_name, params, version, vectors, registers
)
inst_obj.condition = condition_tuple
inst_obj.condition = condition
if instruction.label_size > 0:
inst_obj.label = label
if circuit is None:
Expand All @@ -243,7 +252,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
inst_obj = _parse_custom_operation(
custom_operations, gate_name, params, version, vectors, registers
)
inst_obj.condition = condition_tuple
inst_obj.condition = condition
if instruction.label_size > 0:
inst_obj.label = label
if circuit is None:
Expand All @@ -264,7 +273,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
raise AttributeError("Invalid instruction type: %s" % gate_name)

if gate_name in {"IfElseOp", "WhileLoopOp"}:
gate = gate_class(condition_tuple, *params)
gate = gate_class(condition, *params)
elif version >= 5 and issubclass(gate_class, ControlledGate):
if gate_name in {
"MCPhaseGate",
Expand All @@ -279,7 +288,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
gate = gate_class(*params)
gate.num_ctrl_qubits = instruction.num_ctrl_qubits
gate.ctrl_state = instruction.ctrl_state
gate.condition = condition_tuple
gate.condition = condition
else:
if gate_name in {
"Initialize",
Expand All @@ -296,7 +305,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version,
elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}:
params = [len(qargs), len(cargs)]
gate = gate_class(*params)
gate.condition = condition_tuple
gate.condition = condition
if instruction.label_size > 0:
gate.label = label
if circuit is None:
Expand Down Expand Up @@ -512,7 +521,7 @@ 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)
type_key, data_bytes = value.dumps_value(param, index_map=index_map)

return type_key, data_bytes

Expand Down Expand Up @@ -544,13 +553,16 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map):
custom_operations[gate_class_name] = instruction.operation
custom_operations_list.append(gate_class_name)

has_condition = False
condition_type = type_keys.Condition.NONE
condition_register = b""
condition_value = 0
if getattr(instruction.operation, "condition", None):
has_condition = True
condition_register = _dumps_register(instruction.operation.condition[0], index_map)
condition_value = int(instruction.operation.condition[1])
if (op_condition := getattr(instruction.operation, "condition", None)) is not None:
if isinstance(op_condition, expr.Expr):
condition_type = type_keys.Condition.EXPRESSION
else:
condition_type = type_keys.Condition.TWO_TUPLE
condition_register = _dumps_register(instruction.operation.condition[0], index_map)
condition_value = int(instruction.operation.condition[1])

gate_class_name = gate_class_name.encode(common.ENCODE)
label = getattr(instruction.operation, "label")
Expand Down Expand Up @@ -578,7 +590,7 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map):
len(instruction_params),
instruction.operation.num_qubits,
instruction.operation.num_clbits,
has_condition,
condition_type.value,
len(condition_register),
condition_value,
num_ctrl_qubits,
Expand All @@ -587,7 +599,10 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map):
file_obj.write(instruction_raw)
file_obj.write(gate_class_name)
file_obj.write(label_raw)
file_obj.write(condition_register)
if condition_type is type_keys.Condition.EXPRESSION:
value.write_value(file_obj, op_condition, index_map=index_map)
else:
file_obj.write(condition_register)
# Encode instruciton args
for qbit in instruction.qubits:
instruction_arg_raw = struct.pack(
Expand Down
Loading

0 comments on commit 02886d0

Please sign in to comment.