From 83b3e7f88b73e0d47ef39cdf7c97561905b4a0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 12 Sep 2023 13:55:45 +0200 Subject: [PATCH 01/19] Add use-symengine option --- circuit.qpy | Bin 0 -> 3173 bytes qiskit/qpy/binary_io/circuits.py | 22 +++-- qiskit/qpy/binary_io/value.py | 82 +++++++++++++++--- qiskit/qpy/interface.py | 16 +++- .../circuit/test_circuit_load_from_qpy.py | 25 ++++++ 5 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 circuit.qpy diff --git a/circuit.qpy b/circuit.qpy new file mode 100644 index 0000000000000000000000000000000000000000..ae72d271d09d0b2a6284a66468354ca04046f50f GIT binary patch literal 3173 zcmd^BOKTHR6uvVp(%Mp7x{Rw)$dt|`=3#@gXt5NjD5jvRFqxUefi#avQmmy~`~`|2 zxDj`5{Q-sX2k39mRl$|rxbk@)=ged#ucj2FdZG8+^SHNPzI)EOb7%eT&Gq6eQ3(vG zMdn;GLp2#qu$C~way_f-c2rGY!jRp)7IVu)qWhyG*hUsD9zrLu%|3=T#(cBXZr$ye z-cBi-(<-{HmsQKon`+w5IchnTGu5kkTT5%@obHsZ<0DOF5mbOwmRflo!+ivw931Qh+0+My0wnmtT_)Ik0Ys!%;@lPQfcGM zrSHX6^>XFJSM^tui;w;z~IhpLzLWJ14^Ozb62JkARo&k7%xpHyDI zIrKjWX$bie?FQi8U;6sv)y~sL2N36zbKvRi5@6zltlYyN4ISakQuXsU8=ruR&&DsI z`(XFEO8s#i>^@~hEXB-8S{TkA_eArTrw*GKvlE{ct;#WH#kwRN zhV6;)!eXB$Y+NY36AS-H$iQKML6`~Ya~#0Vz=4s$P6myQVbFRXa*m1@EBQOkf_>GD zZWa?%=*ix_8)mJ2x(^?`d*T|x`3N%#OmRJoB#U&-n$& 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. @@ -427,7 +483,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) diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index a22ce5f20db3..0c2984d96f16 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -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 @@ -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. @@ -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( diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 2e07157b8898..f0b244d88c30 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -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) From 4bff73be6dbd7fd0dee6897025805dd7d8ef74c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 12 Sep 2023 13:57:40 +0200 Subject: [PATCH 02/19] Fix lint --- qiskit/qpy/binary_io/value.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index da8979ed11de..285bdf442a5c 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -14,7 +14,6 @@ from __future__ import annotations import warnings -import time import collections.abc import struct @@ -238,7 +237,6 @@ def _read_parameter_expression(file_obj): payload = file_obj.read(data.expr_size) if _optional.HAS_SYMENGINE: - time0 = time.time() from symengine import sympify from symengine.lib.symengine_wrapper import load_basic @@ -250,7 +248,6 @@ def _read_parameter_expression(file_obj): expr_ = sympify(parse_expr(payload.decode(common.ENCODE))) else: warnings.warn("Symengine not found, trying with sympy...") - time0 = time.time() try: expr_ = parse_expr(payload.decode(common.ENCODE)) @@ -300,7 +297,6 @@ def _read_parameter_expression_v3(file_obj, vectors): payload = file_obj.read(data.expr_size) if _optional.HAS_SYMENGINE: - time0 = time.time() from symengine import sympify from symengine.lib.symengine_wrapper import load_basic @@ -312,7 +308,6 @@ def _read_parameter_expression_v3(file_obj, vectors): expr_ = sympify(parse_expr(payload.decode(common.ENCODE))) else: warnings.warn("Symengine not found, trying with sympy...") - time0 = time.time() try: expr_ = parse_expr(payload.decode(common.ENCODE)) From d0746de85724c11ddea25e3b67c001104a5d6b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 12 Sep 2023 14:01:57 +0200 Subject: [PATCH 03/19] Improve error message --- qiskit/qpy/binary_io/value.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 285bdf442a5c..779637655bf2 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -54,7 +54,9 @@ def _write_parameter_expression(file_obj, obj, use_symengine): if use_symengine: if not _optional.HAS_SYMENGINE: - raise ValueError("No 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 From 3526ecd6ce6a169ee7adb70de08906aec8b8cc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 12 Sep 2023 18:29:23 +0200 Subject: [PATCH 04/19] Apply feedback --- qiskit/qpy/binary_io/circuits.py | 85 +++++++++++----- qiskit/qpy/binary_io/value.py | 68 ++++++------- qiskit/qpy/common.py | 2 +- qiskit/qpy/formats.py | 18 ++++ .../circuit/test_circuit_load_from_qpy.py | 98 +++++++++++++------ 5 files changed, 179 insertions(+), 92 deletions(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 63cb2e05f5fd..f79b41a0c1df 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -41,13 +41,21 @@ from qiskit.transpiler.layout import Layout, TranspileLayout -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), +def _read_header_after_v2(file_obj, version, vectors, metadata_deserializer=None): + if version < 10: + data = formats.CIRCUIT_HEADER_V2._make( + struct.unpack( + formats.CIRCUIT_HEADER_V2_PACK, + file_obj.read(formats.CIRCUIT_HEADER_V2_SIZE), + ) + ) + else: + data = formats.CIRCUIT_HEADER_V10._make( + struct.unpack( + formats.CIRCUIT_HEADER_V10_PACK, + file_obj.read(formats.CIRCUIT_HEADER_V10_SIZE), + ) ) - ) name = file_obj.read(data.name_size).decode(common.ENCODE) global_phase = value.loads_value( data.global_phase_type, @@ -62,6 +70,9 @@ def _read_header_v2(file_obj, version, vectors, metadata_deserializer=None): "num_registers": data.num_registers, "num_instructions": data.num_instructions, } + if version >= 10: + header["use_symengine"] = data.use_symengine + metadata_raw = file_obj.read(data.metadata_size) metadata = json.loads(metadata_raw, cls=metadata_deserializer) return header, name, metadata @@ -124,7 +135,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: @@ -139,6 +152,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: @@ -151,7 +165,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 @@ -165,7 +185,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( @@ -196,7 +218,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)) @@ -232,14 +259,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: @@ -250,7 +277,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: @@ -317,7 +344,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, @@ -346,7 +375,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. @@ -507,7 +536,7 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): 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. @@ -663,7 +692,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 @@ -702,7 +731,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) @@ -947,7 +980,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=Fal num_registers = num_qregs + num_cregs # Write circuit header - header_raw = formats.CIRCUIT_HEADER_V2( + header_raw = formats.CIRCUIT_HEADER_V10( name_size=len(circuit_name), global_phase_type=global_phase_type, global_phase_size=len(global_phase_data), @@ -956,8 +989,9 @@ def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=Fal metadata_size=metadata_size, num_registers=num_registers, num_instructions=num_instructions, + use_symengine=use_symengine, ) - header = struct.pack(formats.CIRCUIT_HEADER_V2_PACK, *header_raw) + header = struct.pack(formats.CIRCUIT_HEADER_V10_PACK, *header_raw) file_obj.write(header) file_obj.write(circuit_name) file_obj.write(global_phase_data) @@ -983,7 +1017,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=Fal 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 ) ) @@ -1022,7 +1056,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None): if version < 2: header, name, metadata = _read_header(file_obj, metadata_deserializer=metadata_deserializer) else: - header, name, metadata = _read_header_v2( + header, name, metadata = _read_header_after_v2( file_obj, version, vectors, metadata_deserializer=metadata_deserializer ) @@ -1031,7 +1065,8 @@ 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 + use_symengine = header.get("use_symengine", False) + # `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 = [] @@ -1097,7 +1132,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: diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 779637655bf2..1569143c2d8f 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -83,7 +83,7 @@ def _write_parameter_expression(file_obj, obj, use_symengine): value_key = symbol_key value_data = bytes() else: - value_key, value_data = dumps_value(value) # TODO: use_symengine? + value_key, value_data = dumps_value(value, use_symengine) elem_header = struct.pack( formats.PARAM_EXPR_MAP_ELEM_V3_PACK, @@ -236,28 +236,12 @@ def _read_parameter_expression(file_obj): from sympy.parsing.sympy_parser import parse_expr - payload = file_obj.read(data.expr_size) - if _optional.HAS_SYMENGINE: from symengine import sympify - from symengine.lib.symengine_wrapper import load_basic - try: - expr_ = load_basic(payload) - - except RuntimeError: - warnings.warn("Symengine loading failed, trying with sympy...") - expr_ = sympify(parse_expr(payload.decode(common.ENCODE))) + expr_ = sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) else: - 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 + expr_ = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)) symbol_map = {} for _ in range(data.map_elements): @@ -288,7 +272,7 @@ def _read_parameter_expression(file_obj): return ParameterExpression(symbol_map, expr_) -def _read_parameter_expression_v3(file_obj, vectors): +def _read_parameter_expression_v3(file_obj, vectors, use_symengine): data = formats.PARAMETER_EXPR( *struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE)) @@ -298,27 +282,23 @@ def _read_parameter_expression_v3(file_obj, vectors): payload = file_obj.read(data.expr_size) - if _optional.HAS_SYMENGINE: - from symengine import sympify + if use_symengine: + if not _optional.HAS_SYMENGINE: + raise exceptions.QpyError( + "``use_symengine`` requires the symengine package to be installed" + ) from symengine.lib.symengine_wrapper import load_basic - try: - expr_ = load_basic(payload) + expr_ = load_basic(payload) - except RuntimeError: - warnings.warn("Symengine loading failed, trying with sympy...") - expr_ = sympify(parse_expr(payload.decode(common.ENCODE))) else: - warnings.warn("Symengine not found, trying with sympy...") + if _optional.HAS_SYMENGINE: + from symengine import sympify - try: + expr_ = sympify(parse_expr(payload.decode(common.ENCODE))) + else: 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_V3( @@ -348,7 +328,10 @@ def _read_parameter_expression_v3(file_obj, vectors): value = symbol._symbol_expr elif elem_key == type_keys.Value.PARAMETER_EXPRESSION: value = common.data_from_binary( - binary_data, _read_parameter_expression_v3, vectors=vectors + binary_data, + _read_parameter_expression_v3, + vectors=vectors, + use_symengine=use_symengine, ) else: raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) @@ -506,7 +489,9 @@ def write_value(file_obj, obj, *, index_map=None): common.write_generic_typed_data(file_obj, type_key, data) -def loads_value(type_key, binary_data, version, vectors, *, clbits=(), cregs=None): +def loads_value( + type_key, binary_data, version, vectors, *, clbits=(), cregs=None, use_symengine=False +): """Deserialize input binary data to value object. Args: @@ -551,7 +536,10 @@ def loads_value(type_key, binary_data, version, vectors, *, clbits=(), cregs=Non return common.data_from_binary(binary_data, _read_parameter_expression) else: return common.data_from_binary( - binary_data, _read_parameter_expression_v3, vectors=vectors + binary_data, + _read_parameter_expression_v3, + vectors=vectors, + use_symengine=use_symengine, ) if type_key == type_keys.Value.EXPRESSION: return common.data_from_binary(binary_data, _read_expr, clbits=clbits, cregs=cregs or {}) @@ -559,7 +547,7 @@ def loads_value(type_key, binary_data, version, vectors, *, clbits=(), cregs=Non raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") -def read_value(file_obj, version, vectors, *, clbits=(), cregs=None): +def read_value(file_obj, version, vectors, *, clbits=(), cregs=None, use_symengine=False): """Read a value from the file like object. Args: @@ -574,4 +562,6 @@ def read_value(file_obj, version, vectors, *, clbits=(), cregs=None): """ type_key, data = common.read_generic_typed_data(file_obj) - return loads_value(type_key, data, version, vectors, clbits=clbits, cregs=cregs) + return loads_value( + type_key, data, version, vectors, clbits=clbits, cregs=cregs, use_symengine=use_symengine + ) diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 89eb1c7644b5..a8d615b28604 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -20,7 +20,7 @@ from qiskit.qpy import formats -QPY_VERSION = 9 +QPY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 64e34d9754a2..be94e811c684 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -26,6 +26,24 @@ FILE_HEADER_PACK = "!6sBBBBQ" FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_PACK) +# CIRCUIT_HEADER_V10 +CIRCUIT_HEADER_V10 = namedtuple( + "HEADER", + [ + "name_size", + "global_phase_type", + "global_phase_size", + "num_qubits", + "num_clbits", + "metadata_size", + "num_registers", + "num_instructions", + "use_symengine", + ], +) +CIRCUIT_HEADER_V10_PACK = "!H1cHIIQIQ?" +CIRCUIT_HEADER_V10_SIZE = struct.calcsize(CIRCUIT_HEADER_V10_PACK) + # CIRCUIT_HEADER_V2 CIRCUIT_HEADER_V2 = namedtuple( "HEADER", diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index f0b244d88c30..ae31f5664d1c 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. -"""Test cases for the circuit qasm_file and qasm_string method.""" +"""Test cases for qpy serialization.""" import io import json @@ -45,15 +45,16 @@ from qiskit.synthesis import LieTrotter, SuzukiTrotter from qiskit.extensions import UnitaryGate from qiskit.test import QiskitTestCase -from qiskit.qpy import dump, load +from qiskit.qpy import dump, load, exceptions from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.quantum_info.random import random_unitary from qiskit.circuit.controlledgate import ControlledGate +from qiskit.utils import optionals @ddt.ddt class TestLoadFromQPY(QiskitTestCase): - """Test circuit.from_qasm_* set of methods.""" + """Test qpy set of methods.""" def assertDeprecatedBitProperties(self, original, roundtripped): """Test that deprecated bit attributes are equal if they are set in the original circuit.""" @@ -101,31 +102,6 @@ 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) @@ -1663,3 +1639,69 @@ def test_qpy_deprecation(self): with self.assertWarnsRegex(DeprecationWarning, "is deprecated"): # pylint: disable=no-name-in-module, unused-import, redefined-outer-name, reimported from qiskit.circuit.qpy_serialization import dump, load + + +class TestSymengineLoadFromQPY(QiskitTestCase): + """Test use of symengine in qpy set of methods.""" + + def setUp(self): + super().setUp() + + 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) + + self.qc = qc + + def assertDeprecatedBitProperties(self, original, roundtripped): + """Test that deprecated bit attributes are equal if they are set in the original circuit.""" + owned_qubits = [ + (a, b) for a, b in zip(original.qubits, roundtripped.qubits) if a._register is not None + ] + if owned_qubits: + original_qubits, roundtripped_qubits = zip(*owned_qubits) + self.assertEqual(original_qubits, roundtripped_qubits) + owned_clbits = [ + (a, b) for a, b in zip(original.clbits, roundtripped.clbits) if a._register is not None + ] + if owned_clbits: + original_clbits, roundtripped_clbits = zip(*owned_clbits) + self.assertEqual(original_clbits, roundtripped_clbits) + + def test_symengine_full_path(self): + """Test use_symengine option for circuit with parameter expressions.""" + qpy_file = io.BytesIO() + dump(self.qc, qpy_file, use_symengine=True) + qpy_file.seek(0) + new_circ = load(qpy_file)[0] + self.assertEqual(self.qc, new_circ) + self.assertDeprecatedBitProperties(self.qc, new_circ) + + def test_dump_no_symengine(self): + """Test dump fails if symengine is not installed and use_symengine==True.""" + qpy_file = io.BytesIO() + with optionals.HAS_SYMENGINE.disable_locally(): + with self.assertRaises(exceptions.QpyError): + dump(self.qc, qpy_file, use_symengine=True) + + def test_load_no_symengine(self): + """Test that load fails if symengine is not installed and the file was created with use_symengine==True.""" + qpy_file = io.BytesIO() + dump(self.qc, qpy_file, use_symengine=True) + qpy_file.seek(0) + with optionals.HAS_SYMENGINE.disable_locally(): + with self.assertRaises(exceptions.QpyError): + _ = load(qpy_file)[0] From 666539094ae0b37f8d2a4bf4e867a4feb142cccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 12 Sep 2023 18:38:49 +0200 Subject: [PATCH 05/19] Update docs --- qiskit/qpy/__init__.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index d7c04ab5eeef..75ac3bf1f42f 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -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: @@ -128,6 +128,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 the ``use_symengine`` flag in ``qpy.dump()``, which allows to use +symengine-native serialization and deserialization for objects of type ``ParameterExpression``. +This option is now stored as part of the circuit header: + +HEADER +------ + +The contents of HEADER are defined as a C struct are: + +.. code-block:: c + + struct { + uint16_t name_size; + char global_phase_type; + uint16_t global_phase_size; + uint32_t num_qubits; + uint32_t num_clbits; + uint64_t metadata_size; + uint32_t num_registers; + uint64_t num_instructions; + uint64_t num_custom_gates; + _Bool use_symengine; + } .. _qpy_version_9: From bcf1b976a4e33484522bb90bcb32a5c9ac4aef64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 13 Sep 2023 13:17:18 +0200 Subject: [PATCH 06/19] Move option to file header, add schedule block --- qiskit/qpy/__init__.py | 34 ++++--- qiskit/qpy/binary_io/circuits.py | 42 ++++---- qiskit/qpy/binary_io/schedules.py | 99 +++++++++++++------ qiskit/qpy/binary_io/value.py | 22 +++-- qiskit/qpy/formats.py | 17 +++- qiskit/qpy/interface.py | 49 +++++---- .../circuit/test_circuit_load_from_qpy.py | 3 +- test/python/qpy/test_block_load_from_qpy.py | 51 +++++++++- 8 files changed, 218 insertions(+), 99 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 75ac3bf1f42f..7dde479f20d6 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -116,6 +116,7 @@ uint64_t num_circuits; } + All values use network byte order [#f1]_ (big endian) for cross platform compatibility. @@ -133,30 +134,33 @@ Version 10 ========== -Version 10 adds support for the ``use_symengine`` flag in ``qpy.dump()``, which allows to use -symengine-native serialization and deserialization for objects of type ``ParameterExpression``. -This option is now stored as part of the circuit header: +Version 10 adds support for the ``use_symengine`` flag in ``qpy.dump()``, which allows the use +of symengine-native serialization and deserialization for objects of type ``ParameterExpression`` +as well as symbolic expressions in Pulse schedule blocks. 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. For this reason, the option defaults to False. -HEADER ------- +As it affects both circuit and block schedule payloads, the option is now stored as part of +the file header: -The contents of HEADER are defined as a C struct are: +FILE_HEADER +----------- + +The contents of FILE_HEADER after V10 are defined as a C struct as: .. code-block:: c struct { - uint16_t name_size; - char global_phase_type; - uint16_t global_phase_size; - uint32_t num_qubits; - uint32_t num_clbits; - uint64_t metadata_size; - uint32_t num_registers; - uint64_t num_instructions; - uint64_t num_custom_gates; + uint8_t qpy_version; + uint8_t qiskit_major_version; + uint8_t qiskit_minor_version; + uint8_t qiskit_patch_version; + uint64_t num_circuits; _Bool use_symengine; } + .. _qpy_version_9: Version 9 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index f79b41a0c1df..b529bfd288c2 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -41,21 +41,15 @@ from qiskit.transpiler.layout import Layout, TranspileLayout -def _read_header_after_v2(file_obj, version, vectors, metadata_deserializer=None): - if version < 10: - data = formats.CIRCUIT_HEADER_V2._make( - struct.unpack( - formats.CIRCUIT_HEADER_V2_PACK, - file_obj.read(formats.CIRCUIT_HEADER_V2_SIZE), - ) - ) - else: - data = formats.CIRCUIT_HEADER_V10._make( - struct.unpack( - formats.CIRCUIT_HEADER_V10_PACK, - file_obj.read(formats.CIRCUIT_HEADER_V10_SIZE), - ) +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, @@ -70,8 +64,6 @@ def _read_header_after_v2(file_obj, version, vectors, metadata_deserializer=None "num_registers": data.num_registers, "num_instructions": data.num_instructions, } - if version >= 10: - header["use_symengine"] = data.use_symengine metadata_raw = file_obj.read(data.metadata_size) metadata = json.loads(metadata_raw, cls=metadata_deserializer) @@ -960,7 +952,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=Fal 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 + 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. @@ -980,7 +972,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=Fal num_registers = num_qregs + num_cregs # Write circuit header - header_raw = formats.CIRCUIT_HEADER_V10( + header_raw = formats.CIRCUIT_HEADER_V2( name_size=len(circuit_name), global_phase_type=global_phase_type, global_phase_size=len(global_phase_data), @@ -989,9 +981,8 @@ def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=Fal metadata_size=metadata_size, num_registers=num_registers, num_instructions=num_instructions, - use_symengine=use_symengine, ) - header = struct.pack(formats.CIRCUIT_HEADER_V10_PACK, *header_raw) + header = struct.pack(formats.CIRCUIT_HEADER_V2_PACK, *header_raw) file_obj.write(header) file_obj.write(circuit_name) file_obj.write(global_phase_data) @@ -1032,7 +1023,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=Fal _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: @@ -1045,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. @@ -1056,7 +1051,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None): if version < 2: header, name, metadata = _read_header(file_obj, metadata_deserializer=metadata_deserializer) else: - header, name, metadata = _read_header_after_v2( + header, name, metadata = _read_header_v2( file_obj, version, vectors, metadata_deserializer=metadata_deserializer ) @@ -1065,7 +1060,6 @@ def read_circuit(file_obj, version, metadata_deserializer=None): num_clbits = header["num_clbits"] num_registers = header["num_registers"] num_instructions = header["num_instructions"] - use_symengine = header.get("use_symengine", False) # `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": {}} diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index e4fd7363a4d4..da0bad871c3f 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -103,19 +103,31 @@ def _read_discriminator(file_obj, version): return Discriminator(name=name, **params) -def _loads_symbolic_expr(expr_bytes): - from sympy import parse_expr +def _loads_symbolic_expr(expr_bytes, use_symengine): if expr_bytes == b"": return None - expr_txt = zlib.decompress(expr_bytes).decode(common.ENCODE) - expr = parse_expr(expr_txt) + if use_symengine: - if _optional.HAS_SYMENGINE: - from symengine import sympify + if not _optional.HAS_SYMENGINE: + raise QpyError("``use_symengine`` requires the symengine package to be installed") + + from symengine.lib.symengine_wrapper import load_basic + + expr = load_basic(zlib.decompress(expr_bytes)) + + else: + from sympy import parse_expr + + expr_txt = zlib.decompress(expr_bytes).decode(common.ENCODE) + expr = parse_expr(expr_txt) + + if _optional.HAS_SYMENGINE: + from symengine import sympify + + return sympify(expr) - return sympify(expr) return expr @@ -201,7 +213,7 @@ def _read_symbolic_pulse(file_obj, version): raise NotImplementedError(f"Unknown class '{class_name}'") -def _read_symbolic_pulse_v6(file_obj, version): +def _read_symbolic_pulse_v6(file_obj, version, use_symengine): make = formats.SYMBOLIC_PULSE_V2._make pack = formats.SYMBOLIC_PULSE_PACK_V2 size = formats.SYMBOLIC_PULSE_SIZE_V2 @@ -214,9 +226,11 @@ def _read_symbolic_pulse_v6(file_obj, version): ) class_name = file_obj.read(header.class_name_size).decode(common.ENCODE) pulse_type = file_obj.read(header.type_size).decode(common.ENCODE) - envelope = _loads_symbolic_expr(file_obj.read(header.envelope_size)) - constraints = _loads_symbolic_expr(file_obj.read(header.constraints_size)) - valid_amp_conditions = _loads_symbolic_expr(file_obj.read(header.valid_amp_conditions_size)) + envelope = _loads_symbolic_expr(file_obj.read(header.envelope_size), use_symengine) + constraints = _loads_symbolic_expr(file_obj.read(header.constraints_size), use_symengine) + valid_amp_conditions = _loads_symbolic_expr( + file_obj.read(header.valid_amp_conditions_size), use_symengine + ) parameters = common.read_mapping( file_obj, deserializer=value.loads_value, @@ -273,14 +287,16 @@ def _read_alignment_context(file_obj, version): # pylint: disable=too-many-return-statements -def _loads_operand(type_key, data_bytes, version): +def _loads_operand(type_key, data_bytes, version, use_symengine): if type_key == type_keys.ScheduleOperand.WAVEFORM: return common.data_from_binary(data_bytes, _read_waveform, version=version) if type_key == type_keys.ScheduleOperand.SYMBOLIC_PULSE: if version < 6: return common.data_from_binary(data_bytes, _read_symbolic_pulse, version=version) else: - return common.data_from_binary(data_bytes, _read_symbolic_pulse_v6, version=version) + return common.data_from_binary( + data_bytes, _read_symbolic_pulse_v6, version=version, use_symengine=use_symengine + ) if type_key == type_keys.ScheduleOperand.CHANNEL: return common.data_from_binary(data_bytes, _read_channel, version=version) if type_key == type_keys.ScheduleOperand.OPERAND_STR: @@ -301,13 +317,15 @@ def _loads_operand(type_key, data_bytes, version): return value.loads_value(type_key, data_bytes, version, {}) -def _read_element(file_obj, version, metadata_deserializer): +def _read_element(file_obj, version, metadata_deserializer, use_symengine): type_key = common.read_type_key(file_obj) if type_key == type_keys.Program.SCHEDULE_BLOCK: - return read_schedule_block(file_obj, version, metadata_deserializer) + return read_schedule_block(file_obj, version, metadata_deserializer, use_symengine) - operands = common.read_sequence(file_obj, deserializer=_loads_operand, version=version) + operands = common.read_sequence( + file_obj, deserializer=_loads_operand, version=version, use_symengine=use_symengine + ) name = value.read_value(file_obj, version, {}) instance = object.__new__(type_keys.ScheduleInstruction.retrieve(type_key)) @@ -388,22 +406,30 @@ def _write_discriminator(file_obj, data): value.write_value(file_obj, name) -def _dumps_symbolic_expr(expr): - from sympy import srepr, sympify +def _dumps_symbolic_expr(expr, use_symengine): if expr is None: return b"" - expr_bytes = srepr(sympify(expr)).encode(common.ENCODE) + if use_symengine: + if not _optional.HAS_SYMENGINE: + raise QpyError("``use_symengine`` requires the symengine package to be installed") + expr_bytes = expr.__reduce__()[1][0] + + else: + from sympy import srepr, sympify + + expr_bytes = srepr(sympify(expr)).encode(common.ENCODE) + return zlib.compress(expr_bytes) -def _write_symbolic_pulse(file_obj, data): +def _write_symbolic_pulse(file_obj, data, use_symengine): class_name_bytes = data.__class__.__name__.encode(common.ENCODE) pulse_type_bytes = data.pulse_type.encode(common.ENCODE) - envelope_bytes = _dumps_symbolic_expr(data.envelope) - constraints_bytes = _dumps_symbolic_expr(data.constraints) - valid_amp_conditions_bytes = _dumps_symbolic_expr(data.valid_amp_conditions) + envelope_bytes = _dumps_symbolic_expr(data.envelope, use_symengine) + constraints_bytes = _dumps_symbolic_expr(data.constraints, use_symengine) + valid_amp_conditions_bytes = _dumps_symbolic_expr(data.valid_amp_conditions, use_symengine) header_bytes = struct.pack( formats.SYMBOLIC_PULSE_PACK_V2, @@ -439,13 +465,15 @@ def _write_alignment_context(file_obj, context): ) -def _dumps_operand(operand): +def _dumps_operand(operand, use_symengine): if isinstance(operand, library.Waveform): type_key = type_keys.ScheduleOperand.WAVEFORM data_bytes = common.data_to_binary(operand, _write_waveform) elif isinstance(operand, library.SymbolicPulse): type_key = type_keys.ScheduleOperand.SYMBOLIC_PULSE - data_bytes = common.data_to_binary(operand, _write_symbolic_pulse) + data_bytes = common.data_to_binary( + operand, _write_symbolic_pulse, use_symengine=use_symengine + ) elif isinstance(operand, channels.Channel): type_key = type_keys.ScheduleOperand.CHANNEL data_bytes = common.data_to_binary(operand, _write_channel) @@ -464,7 +492,7 @@ def _dumps_operand(operand): return type_key, data_bytes -def _write_element(file_obj, element, metadata_serializer): +def _write_element(file_obj, element, metadata_serializer, use_symengine): if isinstance(element, ScheduleBlock): common.write_type_key(file_obj, type_keys.Program.SCHEDULE_BLOCK) write_schedule_block(file_obj, element, metadata_serializer) @@ -475,6 +503,7 @@ def _write_element(file_obj, element, metadata_serializer): file_obj, sequence=element.operands, serializer=_dumps_operand, + use_symengine=use_symengine, ) value.write_value(file_obj, element.name) @@ -493,7 +522,7 @@ def _dumps_reference_item(schedule, metadata_serializer): return type_key, data_bytes -def read_schedule_block(file_obj, version, metadata_deserializer=None): +def read_schedule_block(file_obj, version, metadata_deserializer=None, use_symengine=False): """Read a single ScheduleBlock from the file like object. Args: @@ -506,7 +535,10 @@ def read_schedule_block(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 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: ScheduleBlock: The schedule block object from the file. @@ -534,7 +566,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None): alignment_context=context, ) for _ in range(data.num_elements): - block_elm = _read_element(file_obj, version, metadata_deserializer) + block_elm = _read_element(file_obj, version, metadata_deserializer, use_symengine) block.append(block_elm, inplace=True) # Load references @@ -556,7 +588,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None): return block -def write_schedule_block(file_obj, block, metadata_serializer=None): +def write_schedule_block(file_obj, block, metadata_serializer=None, use_symengine=False): """Write a single ScheduleBlock object in the file like object. Args: @@ -566,7 +598,10 @@ def write_schedule_block(file_obj, block, metadata_serializer=None): will be passed the :attr:`.ScheduleBlock.metadata` dictionary for ``block`` 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. Raises: TypeError: If any of the instructions is invalid data format. """ @@ -590,7 +625,7 @@ def write_schedule_block(file_obj, block, metadata_serializer=None): for block_elm in block._blocks: # Do not call block.blocks. This implicitly assigns references to instruction. # This breaks original reference structure. - _write_element(file_obj, block_elm, metadata_serializer) + _write_element(file_obj, block_elm, metadata_serializer, use_symengine) # Write references flat_key_refdict = {} diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 1569143c2d8f..d3424a767e9f 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -13,7 +13,6 @@ """Binary IO for any value objects, such as numbers, string, parameters.""" from __future__ import annotations -import warnings import collections.abc import struct @@ -83,7 +82,7 @@ def _write_parameter_expression(file_obj, obj, use_symengine): value_key = symbol_key value_data = bytes() else: - value_key, value_data = dumps_value(value, use_symengine) + value_key, value_data = dumps_value(value, use_symengine=use_symengine) elem_header = struct.pack( formats.PARAM_EXPR_MAP_ELEM_V3_PACK, @@ -429,11 +428,10 @@ def dumps_value(obj, *, index_map=None, use_symengine=False): 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 + 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. @@ -475,7 +473,7 @@ def dumps_value(obj, *, index_map=None, use_symengine=False): return type_key, binary_data -def write_value(file_obj, obj, *, index_map=None): +def write_value(file_obj, obj, *, index_map=None, use_symengine=False): """Write a value to the file like object. Args: @@ -484,8 +482,12 @@ def write_value(file_obj, obj, *, index_map=None): 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 (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. """ - type_key, data = dumps_value(obj, index_map=index_map) + type_key, data = dumps_value(obj, index_map=index_map, use_symengine=use_symengine) common.write_generic_typed_data(file_obj, type_key, data) @@ -501,6 +503,10 @@ def loads_value( vectors (dict): ParameterVector in current scope. clbits (Sequence[Clbit]): Clbits in the current scope. cregs (Mapping[str, ClassicalRegister]): Classical registers in the current scope. + 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: any: Deserialized value object. @@ -556,6 +562,10 @@ def read_value(file_obj, version, vectors, *, clbits=(), cregs=None, use_symengi vectors (dict): ParameterVector in current scope. clbits (Sequence[Clbit]): Clbits in the current scope. cregs (Mapping[str, ClassicalRegister]): Classical registers in the current scope. + 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: any: Deserialized value object. diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index be94e811c684..67a8cdf8a9c0 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -18,6 +18,22 @@ from collections import namedtuple +# FILE_HEADER_V10 +FILE_HEADER_V10 = namedtuple( + "FILE_HEADER", + [ + "preface", + "qpy_version", + "major_version", + "minor_version", + "patch_version", + "num_programs", + "use_symengine", + ], +) +FILE_HEADER_V10_PACK = "!6sBBBBQ?" +FILE_HEADER_V10_SIZE = struct.calcsize(FILE_HEADER_V10_PACK) + # FILE_HEADER FILE_HEADER = namedtuple( "FILE_HEADER", @@ -38,7 +54,6 @@ "metadata_size", "num_registers", "num_instructions", - "use_symengine", ], ) CIRCUIT_HEADER_V10_PACK = "!H1cHIIQIQ?" diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 0c2984d96f16..77793afbb7c3 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -123,10 +123,11 @@ 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. + use_symengine: If True, all objects containing symbolic expressions 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. For this reason, the option defaults to False. Raises: QpyError: When multiple data format is mixed in the output. TypeError: When invalid data type is input. @@ -157,27 +158,22 @@ def dump( version_match = VERSION_PATTERN_REGEX.search(__version__) version_parts = [int(x) for x in version_match.group("release").split(".")] header = struct.pack( - formats.FILE_HEADER_PACK, + formats.FILE_HEADER_V10_PACK, b"QISKIT", common.QPY_VERSION, version_parts[0], version_parts[1], version_parts[2], len(programs), + use_symengine, ) file_obj.write(header) common.write_type_key(file_obj, type_key) for program in programs: - 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) + writer( + file_obj, program, metadata_serializer=metadata_serializer, use_symengine=use_symengine + ) def load( @@ -231,12 +227,26 @@ def load( QiskitError: if ``file_obj`` is not a valid QPY file TypeError: When invalid data type is loaded. """ - data = formats.FILE_HEADER._make( - struct.unpack( - formats.FILE_HEADER_PACK, - file_obj.read(formats.FILE_HEADER_SIZE), + + # identify file header version + version = struct.unpack("!6sB", file_obj.read(7))[1] + file_obj.seek(0) + + if version < 10: + data = formats.FILE_HEADER._make( + struct.unpack( + formats.FILE_HEADER_PACK, + file_obj.read(formats.FILE_HEADER_SIZE), + ) ) - ) + else: + data = formats.FILE_HEADER_V10._make( + struct.unpack( + formats.FILE_HEADER_V10_PACK, + file_obj.read(formats.FILE_HEADER_V10_SIZE), + ) + ) + if data.preface.decode(common.ENCODE) != "QISKIT": raise QiskitError("Input file is not a valid QPY file") version_match = VERSION_PATTERN_REGEX.search(__version__) @@ -282,6 +292,7 @@ def load( file_obj, data.qpy_version, metadata_deserializer=metadata_deserializer, + use_symengine=data.use_symengine, ) ) return programs diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index ae31f5664d1c..47cc2a8b87a0 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1698,7 +1698,8 @@ def test_dump_no_symengine(self): dump(self.qc, qpy_file, use_symengine=True) def test_load_no_symengine(self): - """Test that load fails if symengine is not installed and the file was created with use_symengine==True.""" + """Test that load fails if symengine is not installed and the + file was created with use_symengine==True.""" qpy_file = io.BytesIO() dump(self.qc, qpy_file, use_symengine=True) qpy_file.seek(0) diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index 06d8da19775d..d12caabb40d9 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -37,7 +37,7 @@ from qiskit.pulse.instructions import Play, TimeBlockade from qiskit.circuit import Parameter, QuantumCircuit, Gate from qiskit.test import QiskitTestCase -from qiskit.qpy import dump, load +from qiskit.qpy import dump, load, exceptions from qiskit.utils import optionals as _optional from qiskit.pulse.configuration import Kernel, Discriminator @@ -401,3 +401,52 @@ def test_with_acquire_instruction_with_discriminator(self): qc.add_calibration("measure", (0,), sched) self.assert_roundtrip_equal(qc) + + +class TestSymengineLoadFromQPY(QiskitTestCase): + """Test use of symengine in qpy set of methods.""" + + def setUp(self): + super().setUp() + + # pylint: disable=invalid-name + t, amp, freq = sym.symbols("t, amp, freq") + sym_envelope = 2 * amp * (freq * t - sym.floor(1 / 2 + freq * t)) + + my_pulse = SymbolicPulse( + pulse_type="Sawtooth", + duration=100, + parameters={"amp": 0.1, "freq": 0.05}, + envelope=sym_envelope, + name="pulse1", + ) + with builder.build() as test_sched: + builder.play(my_pulse, DriveChannel(0)) + + self.test_sched = test_sched + + def test_symengine_full_path(self): + """Test use_symengine option for circuit with parameter expressions.""" + qpy_file = io.BytesIO() + dump(self.test_sched, qpy_file, use_symengine=True) + qpy_file.seek(0) + new_sched = load(qpy_file)[0] + self.assertEqual(self.test_sched, new_sched) + self.assertDeprecatedBitProperties(self.test_sched, new_sched) + + def test_dump_no_symengine(self): + """Test dump fails if symengine is not installed and use_symengine==True.""" + qpy_file = io.BytesIO() + with _optional.HAS_SYMENGINE.disable_locally(): + with self.assertRaises(exceptions.QpyError): + dump(self.test_sched, qpy_file, use_symengine=True) + + def test_load_no_symengine(self): + """Test that load fails if symengine is not installed and the + file was created with use_symengine==True.""" + qpy_file = io.BytesIO() + dump(self.test_sched, qpy_file, use_symengine=True) + qpy_file.seek(0) + with _optional.HAS_SYMENGINE.disable_locally(): + with self.assertRaises(exceptions.QpyError): + _ = load(qpy_file)[0] From 11ea847bccd35e6086c5b3a5389a46e2a63b309c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 13 Sep 2023 13:21:23 +0200 Subject: [PATCH 07/19] Add example to docs --- qiskit/qpy/__init__.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 7dde479f20d6..a6689865c1af 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -136,10 +136,31 @@ Version 10 adds support for the ``use_symengine`` flag in ``qpy.dump()``, which allows the use of symengine-native serialization and deserialization for objects of type ``ParameterExpression`` -as well as symbolic expressions in Pulse schedule blocks. 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. For this reason, the option defaults to False. +as well as symbolic expressions in Pulse schedule blocks. + +For example:: + + from qiskit.circuit import QuantumCircuit, Parameter + from qiskit import qpy + + theta = Parameter("theta") + phi = Parameter("phi") + sum_param = theta + phi + + qc = QuantumCircuit(1) + qc.rz(sum_param, 0) + qc.measure_all() + + with open('bell.qpy', 'wb') as fd: + qpy.dump(qc, fd) + + with open('bell.qpy', 'rb') as fd: + new_qc = qpy.load(fd)[0] + +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. +For this reason, the option defaults to False. As it affects both circuit and block schedule payloads, the option is now stored as part of the file header: From ee80b4afdf51ca000c13f2afd9a966e8cd7004a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 13 Sep 2023 14:02:36 +0200 Subject: [PATCH 08/19] Fix lint --- qiskit/qpy/binary_io/schedules.py | 4 +++- qiskit/qpy/binary_io/value.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index da0bad871c3f..f23490b56562 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -113,7 +113,9 @@ def _loads_symbolic_expr(expr_bytes, use_symengine): if not _optional.HAS_SYMENGINE: raise QpyError("``use_symengine`` requires the symengine package to be installed") - from symengine.lib.symengine_wrapper import load_basic + from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module + load_basic, + ) expr = load_basic(zlib.decompress(expr_bytes)) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index d3424a767e9f..2f8f10be93fa 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -286,7 +286,9 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): raise exceptions.QpyError( "``use_symengine`` requires the symengine package to be installed" ) - from symengine.lib.symengine_wrapper import load_basic + from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module + load_basic, + ) expr_ = load_basic(payload) From e34b1003a905301aeb13c8d36418cd2b70e92c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 13 Sep 2023 17:34:00 +0200 Subject: [PATCH 09/19] Apply second round of feedback --- qiskit/qpy/__init__.py | 50 +++++++++------------ qiskit/qpy/binary_io/schedules.py | 10 +++-- qiskit/qpy/binary_io/value.py | 10 +++-- qiskit/qpy/formats.py | 21 +-------- qiskit/qpy/interface.py | 10 ++++- qiskit/qpy/type_keys.py | 18 ++++++++ test/python/qpy/test_block_load_from_qpy.py | 1 - 7 files changed, 61 insertions(+), 59 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index a6689865c1af..282c6d1a7fe5 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -117,6 +117,20 @@ } +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. @@ -134,36 +148,12 @@ Version 10 ========== -Version 10 adds support for the ``use_symengine`` flag in ``qpy.dump()``, which allows the use -of symengine-native serialization and deserialization for objects of type ``ParameterExpression`` -as well as symbolic expressions in Pulse schedule blocks. - -For example:: - - from qiskit.circuit import QuantumCircuit, Parameter - from qiskit import qpy - - theta = Parameter("theta") - phi = Parameter("phi") - sum_param = theta + phi - - qc = QuantumCircuit(1) - qc.rz(sum_param, 0) - qc.measure_all() - - with open('bell.qpy', 'wb') as fd: - qpy.dump(qc, fd) - - with open('bell.qpy', 'rb') as fd: - new_qc = qpy.load(fd)[0] - -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. -For this reason, the option defaults to False. +Version 10 adds support for symengine-native serialization for objects of type +:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks. -As it affects both circuit and block schedule payloads, the option is now stored as part of -the file header: +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 +encoding and ``e`` refers to symengine encoding. FILE_HEADER ----------- @@ -178,7 +168,7 @@ uint8_t qiskit_minor_version; uint8_t qiskit_patch_version; uint64_t num_circuits; - _Bool use_symengine; + char symbolic_encoding; } diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index f23490b56562..d740d47430a3 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -20,7 +20,7 @@ import numpy as np -from qiskit.exceptions import QiskitError +from qiskit.exceptions import QiskitError, MissingOptionalLibraryError from qiskit.pulse import library, channels, instructions from qiskit.pulse.schedule import ScheduleBlock from qiskit.qpy import formats, common, type_keys @@ -111,7 +111,9 @@ def _loads_symbolic_expr(expr_bytes, use_symengine): if use_symengine: if not _optional.HAS_SYMENGINE: - raise QpyError("``use_symengine`` requires the symengine package to be installed") + raise MissingOptionalLibraryError( + "This QPY file encodes its symbolic components using 'symengine', which is not installed." + ) from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module load_basic, @@ -415,7 +417,9 @@ def _dumps_symbolic_expr(expr, use_symengine): if use_symengine: if not _optional.HAS_SYMENGINE: - raise QpyError("``use_symengine`` requires the symengine package to be installed") + raise MissingOptionalLibraryError( + "The `use_symengine` option requires the symengine package, which is not installed." + ) expr_bytes = expr.__reduce__()[1][0] else: diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 2f8f10be93fa..81f109c8eb10 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -25,6 +25,7 @@ from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement +from qiskit.exceptions import MissingOptionalLibraryError from qiskit.qpy import common, formats, exceptions, type_keys from qiskit.utils import optionals as _optional @@ -53,8 +54,8 @@ 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" + raise MissingOptionalLibraryError( + "The `use_symengine` option requires the symengine package, which is not installed." ) expr_bytes = obj._symbol_expr.__reduce__()[1][0] else: @@ -283,9 +284,10 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): if use_symengine: if not _optional.HAS_SYMENGINE: - raise exceptions.QpyError( - "``use_symengine`` requires the symengine package to be installed" + raise MissingOptionalLibraryError( + "This QPY file encodes its symbolic components using 'symengine', which is not installed." ) + from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module load_basic, ) diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 67a8cdf8a9c0..d3da8c5bed7e 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -28,10 +28,10 @@ "minor_version", "patch_version", "num_programs", - "use_symengine", + "symbolic_encoding", ], ) -FILE_HEADER_V10_PACK = "!6sBBBBQ?" +FILE_HEADER_V10_PACK = "!6sBBBBQc" FILE_HEADER_V10_SIZE = struct.calcsize(FILE_HEADER_V10_PACK) # FILE_HEADER @@ -42,23 +42,6 @@ FILE_HEADER_PACK = "!6sBBBBQ" FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_PACK) -# CIRCUIT_HEADER_V10 -CIRCUIT_HEADER_V10 = namedtuple( - "HEADER", - [ - "name_size", - "global_phase_type", - "global_phase_size", - "num_qubits", - "num_clbits", - "metadata_size", - "num_registers", - "num_instructions", - ], -) -CIRCUIT_HEADER_V10_PACK = "!H1cHIIQIQ?" -CIRCUIT_HEADER_V10_SIZE = struct.calcsize(CIRCUIT_HEADER_V10_PACK) - # CIRCUIT_HEADER_V2 CIRCUIT_HEADER_V2 = namedtuple( "HEADER", diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 77793afbb7c3..2e092f14124e 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -157,6 +157,7 @@ def dump( version_match = VERSION_PATTERN_REGEX.search(__version__) version_parts = [int(x) for x in version_match.group("release").split(".")] + encoding = type_keys.Encoding.assign(use_symengine) header = struct.pack( formats.FILE_HEADER_V10_PACK, b"QISKIT", @@ -165,7 +166,7 @@ def dump( version_parts[1], version_parts[2], len(programs), - use_symengine, + encoding, ) file_obj.write(header) common.write_type_key(file_obj, type_key) @@ -285,6 +286,11 @@ def load( else: raise TypeError(f"Invalid payload format data kind '{type_key}'.") + if data.qpy_version < 10: + use_symengine = False + else: + use_symengine = data.symbolic_encoding == type_keys.Encoding.SYMENGINE + programs = [] for _ in range(data.num_programs): programs.append( @@ -292,7 +298,7 @@ def load( file_obj, data.qpy_version, metadata_deserializer=metadata_deserializer, - use_symengine=data.use_symengine, + use_symengine=use_symengine, ) ) return programs diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 1e394ec13a57..391382108e7f 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -524,3 +524,21 @@ def assign(cls, obj): @classmethod def retrieve(cls, type_key): raise NotImplementedError + + +class Encoding(TypeKeyBase): + """Type keys for the symbolic encoding field in the file header.""" + + SYMPY = b"p" + SYMENGINE = b"e" + + @classmethod + def assign(cls, obj): + if obj is True: + return cls.SYMENGINE + else: + return cls.SYMPY + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index d12caabb40d9..5190782a8157 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -432,7 +432,6 @@ def test_symengine_full_path(self): qpy_file.seek(0) new_sched = load(qpy_file)[0] self.assertEqual(self.test_sched, new_sched) - self.assertDeprecatedBitProperties(self.test_sched, new_sched) def test_dump_no_symengine(self): """Test dump fails if symengine is not installed and use_symengine==True.""" From 362e3107f04749aa2f543beb9a227bc0dfa3516c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 13 Sep 2023 17:43:47 +0200 Subject: [PATCH 10/19] Fix lint, update tests --- qiskit/qpy/binary_io/schedules.py | 3 ++- qiskit/qpy/binary_io/value.py | 3 ++- test/python/circuit/test_circuit_load_from_qpy.py | 7 ++++--- test/python/qpy/test_block_load_from_qpy.py | 7 ++++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index d740d47430a3..8c8e65e504d7 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -112,7 +112,8 @@ def _loads_symbolic_expr(expr_bytes, use_symengine): if not _optional.HAS_SYMENGINE: raise MissingOptionalLibraryError( - "This QPY file encodes its symbolic components using 'symengine', which is not installed." + "This QPY file encodes its symbolic components using 'symengine', " + "which is not installed." ) from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 81f109c8eb10..4533ee94028d 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -285,7 +285,8 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): if use_symengine: if not _optional.HAS_SYMENGINE: raise MissingOptionalLibraryError( - "This QPY file encodes its symbolic components using 'symengine', which is not installed." + "This QPY file encodes its symbolic components using 'symengine', " + "which is not installed." ) from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 47cc2a8b87a0..6253e77c41c9 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -45,11 +45,12 @@ from qiskit.synthesis import LieTrotter, SuzukiTrotter from qiskit.extensions import UnitaryGate from qiskit.test import QiskitTestCase -from qiskit.qpy import dump, load, exceptions +from qiskit.qpy import dump, load from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.quantum_info.random import random_unitary from qiskit.circuit.controlledgate import ControlledGate from qiskit.utils import optionals +from qiskit.exceptions import MissingOptionalLibraryError @ddt.ddt @@ -1694,7 +1695,7 @@ def test_dump_no_symengine(self): """Test dump fails if symengine is not installed and use_symengine==True.""" qpy_file = io.BytesIO() with optionals.HAS_SYMENGINE.disable_locally(): - with self.assertRaises(exceptions.QpyError): + with self.assertRaises(MissingOptionalLibraryError): dump(self.qc, qpy_file, use_symengine=True) def test_load_no_symengine(self): @@ -1704,5 +1705,5 @@ def test_load_no_symengine(self): dump(self.qc, qpy_file, use_symengine=True) qpy_file.seek(0) with optionals.HAS_SYMENGINE.disable_locally(): - with self.assertRaises(exceptions.QpyError): + with self.assertRaises(MissingOptionalLibraryError): _ = load(qpy_file)[0] diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index 5190782a8157..c4d4f77c9c29 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -36,8 +36,9 @@ ) from qiskit.pulse.instructions import Play, TimeBlockade from qiskit.circuit import Parameter, QuantumCircuit, Gate +from qiskit.exceptions import MissingOptionalLibraryError from qiskit.test import QiskitTestCase -from qiskit.qpy import dump, load, exceptions +from qiskit.qpy import dump, load from qiskit.utils import optionals as _optional from qiskit.pulse.configuration import Kernel, Discriminator @@ -437,7 +438,7 @@ def test_dump_no_symengine(self): """Test dump fails if symengine is not installed and use_symengine==True.""" qpy_file = io.BytesIO() with _optional.HAS_SYMENGINE.disable_locally(): - with self.assertRaises(exceptions.QpyError): + with self.assertRaises(MissingOptionalLibraryError): dump(self.test_sched, qpy_file, use_symengine=True) def test_load_no_symengine(self): @@ -447,5 +448,5 @@ def test_load_no_symengine(self): dump(self.test_sched, qpy_file, use_symengine=True) qpy_file.seek(0) with _optional.HAS_SYMENGINE.disable_locally(): - with self.assertRaises(exceptions.QpyError): + with self.assertRaises(MissingOptionalLibraryError): _ = load(qpy_file)[0] From b1aba7781dff333cd6d10e3df419cb07d8d5e392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 13 Sep 2023 17:45:07 +0200 Subject: [PATCH 11/19] Update docs --- qiskit/qpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 282c6d1a7fe5..5824d11de1a9 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -152,7 +152,7 @@ :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 +is introduced, mapped to each symbolic library as follows: ``p`` refers to sympy encoding and ``e`` refers to symengine encoding. FILE_HEADER From e08ee43738e2d9b27afba5dd276181034533634a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 13 Sep 2023 18:53:33 +0200 Subject: [PATCH 12/19] Fix use of require_now --- qiskit/qpy/binary_io/schedules.py | 24 +++--------------------- qiskit/qpy/binary_io/value.py | 20 ++------------------ 2 files changed, 5 insertions(+), 39 deletions(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 8c8e65e504d7..4c194003eb24 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -20,7 +20,7 @@ import numpy as np -from qiskit.exceptions import QiskitError, MissingOptionalLibraryError +from qiskit.exceptions import QiskitError from qiskit.pulse import library, channels, instructions from qiskit.pulse.schedule import ScheduleBlock from qiskit.qpy import formats, common, type_keys @@ -104,35 +104,24 @@ def _read_discriminator(file_obj, version): def _loads_symbolic_expr(expr_bytes, use_symengine): - if expr_bytes == b"": return None - if use_symengine: - - if not _optional.HAS_SYMENGINE: - raise MissingOptionalLibraryError( - "This QPY file encodes its symbolic components using 'symengine', " - "which is not installed." - ) - + _optional.HAS_SYMENGINE.require_now("loads_symbolic_expr") from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module load_basic, ) expr = load_basic(zlib.decompress(expr_bytes)) - else: from sympy import parse_expr expr_txt = zlib.decompress(expr_bytes).decode(common.ENCODE) expr = parse_expr(expr_txt) - if _optional.HAS_SYMENGINE: from symengine import sympify return sympify(expr) - return expr @@ -412,22 +401,15 @@ def _write_discriminator(file_obj, data): def _dumps_symbolic_expr(expr, use_symengine): - if expr is None: return b"" - if use_symengine: - if not _optional.HAS_SYMENGINE: - raise MissingOptionalLibraryError( - "The `use_symengine` option requires the symengine package, which is not installed." - ) + _optional.HAS_SYMENGINE.require_now("dumps_symbolic_expr") expr_bytes = expr.__reduce__()[1][0] - else: from sympy import srepr, sympify expr_bytes = srepr(sympify(expr)).encode(common.ENCODE) - return zlib.compress(expr_bytes) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 4533ee94028d..c2083e121e00 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -25,7 +25,6 @@ from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.qpy import common, formats, exceptions, type_keys from qiskit.utils import optionals as _optional @@ -51,12 +50,8 @@ def _write_parameter_vec(file_obj, obj): def _write_parameter_expression(file_obj, obj, use_symengine): - if use_symengine: - if not _optional.HAS_SYMENGINE: - raise MissingOptionalLibraryError( - "The `use_symengine` option requires the symengine package, which is not installed." - ) + _optional.HAS_SYMENGINE.require_now("write_parameter_expression") expr_bytes = obj._symbol_expr.__reduce__()[1][0] else: from sympy import srepr, sympify @@ -229,11 +224,9 @@ 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 if _optional.HAS_SYMENGINE: @@ -273,28 +266,19 @@ def _read_parameter_expression(file_obj): def _read_parameter_expression_v3(file_obj, vectors, use_symengine): - 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 use_symengine: - if not _optional.HAS_SYMENGINE: - raise MissingOptionalLibraryError( - "This QPY file encodes its symbolic components using 'symengine', " - "which is not installed." - ) - + _optional.HAS_SYMENGINE.require_now("read_parameter_expression_v3") from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module load_basic, ) expr_ = load_basic(payload) - else: if _optional.HAS_SYMENGINE: from symengine import sympify From b1f6793cd212f9391b1cac33224f06d9afc68849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 13 Sep 2023 20:00:19 +0200 Subject: [PATCH 13/19] Fix compatibility test --- qiskit/qpy/binary_io/schedules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 4c194003eb24..d80a138c395f 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -103,7 +103,7 @@ def _read_discriminator(file_obj, version): return Discriminator(name=name, **params) -def _loads_symbolic_expr(expr_bytes, use_symengine): +def _loads_symbolic_expr(expr_bytes, use_symengine=False): if expr_bytes == b"": return None if use_symengine: From fb2836f574b4e8493d8485d2db8c4853e7960b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 14 Sep 2023 08:47:37 +0200 Subject: [PATCH 14/19] Make tests optional --- test/python/circuit/test_circuit_load_from_qpy.py | 4 ++++ test/python/qpy/test_block_load_from_qpy.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 6253e77c41c9..e0964d523f6f 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -16,6 +16,7 @@ import io import json import random +import unittest import ddt import numpy as np @@ -1682,6 +1683,7 @@ def assertDeprecatedBitProperties(self, original, roundtripped): original_clbits, roundtripped_clbits = zip(*owned_clbits) self.assertEqual(original_clbits, roundtripped_clbits) + @unittest.skipIf(not optionals.HAS_SYMENGINE, "Install symengine to run this test.") def test_symengine_full_path(self): """Test use_symengine option for circuit with parameter expressions.""" qpy_file = io.BytesIO() @@ -1691,6 +1693,7 @@ def test_symengine_full_path(self): self.assertEqual(self.qc, new_circ) self.assertDeprecatedBitProperties(self.qc, new_circ) + @unittest.skipIf(not optionals.HAS_SYMENGINE, "Install symengine to run this test.") def test_dump_no_symengine(self): """Test dump fails if symengine is not installed and use_symengine==True.""" qpy_file = io.BytesIO() @@ -1698,6 +1701,7 @@ def test_dump_no_symengine(self): with self.assertRaises(MissingOptionalLibraryError): dump(self.qc, qpy_file, use_symengine=True) + @unittest.skipIf(not optionals.HAS_SYMENGINE, "Install symengine to run this test.") def test_load_no_symengine(self): """Test that load fails if symengine is not installed and the file was created with use_symengine==True.""" diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index c4d4f77c9c29..95930e2a7847 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -14,6 +14,7 @@ import io from ddt import ddt, data, unpack +import unittest import numpy as np @@ -426,6 +427,7 @@ def setUp(self): self.test_sched = test_sched + @unittest.skipIf(not _optional.HAS_SYMENGINE, "Install symengine to run this test.") def test_symengine_full_path(self): """Test use_symengine option for circuit with parameter expressions.""" qpy_file = io.BytesIO() @@ -434,6 +436,7 @@ def test_symengine_full_path(self): new_sched = load(qpy_file)[0] self.assertEqual(self.test_sched, new_sched) + @unittest.skipIf(not _optional.HAS_SYMENGINE, "Install symengine to run this test.") def test_dump_no_symengine(self): """Test dump fails if symengine is not installed and use_symengine==True.""" qpy_file = io.BytesIO() @@ -441,6 +444,7 @@ def test_dump_no_symengine(self): with self.assertRaises(MissingOptionalLibraryError): dump(self.test_sched, qpy_file, use_symengine=True) + @unittest.skipIf(not _optional.HAS_SYMENGINE, "Install symengine to run this test.") def test_load_no_symengine(self): """Test that load fails if symengine is not installed and the file was created with use_symengine==True.""" From b83a5b93f1d4a43b4dce339c0d031d53941a27f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 14 Sep 2023 11:06:10 +0200 Subject: [PATCH 15/19] Fix lint --- test/python/qpy/test_block_load_from_qpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index 95930e2a7847..e68ec48d4a00 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -13,8 +13,8 @@ """Test cases for the schedule block qpy loading and saving.""" import io -from ddt import ddt, data, unpack import unittest +from ddt import ddt, data, unpack import numpy as np From ec0bb10936bfc2bff20444d964f3d42ffec752da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 14 Sep 2023 17:34:13 +0200 Subject: [PATCH 16/19] Add release note --- ...py-symbolic-encoding-81d5321af38f259f.yaml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 releasenotes/notes/add-qpy-symbolic-encoding-81d5321af38f259f.yaml diff --git a/releasenotes/notes/add-qpy-symbolic-encoding-81d5321af38f259f.yaml b/releasenotes/notes/add-qpy-symbolic-encoding-81d5321af38f259f.yaml new file mode 100644 index 000000000000..6f0ccc990b07 --- /dev/null +++ b/releasenotes/notes/add-qpy-symbolic-encoding-81d5321af38f259f.yaml @@ -0,0 +1,31 @@ +--- +features: + - | + QPY supports the use of symengine-native serialization and deserialization + for objects of type ``ParameterExpression`` as well as symbolic expressions in + Pulse schedule blocks. 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. + + The feature can be enabled through the ``use_symengine`` parameter + in :meth:`.qpy.dump`: + + .. code-block:: python + + from qiskit.circuit import QuantumCircuit, Parameter + from qiskit import qpy + + theta = Parameter("theta") + phi = Parameter("phi") + sum_param = theta + phi + + qc = QuantumCircuit(1) + qc.rz(sum_param, 0) + qc.measure_all() + + with open('bell.qpy', 'wb') as fd: + qpy.dump(qc, fd, use_symengine=True) + + with open('bell.qpy', 'rb') as fd: + new_qc = qpy.load(fd)[0] From 1580b168782967eb2f9fc24eb83d344cadc03978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:18:41 +0200 Subject: [PATCH 17/19] Update exception message Co-authored-by: Matthew Treinish --- qiskit/qpy/binary_io/schedules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index d80a138c395f..4400bd49f7a6 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -107,7 +107,7 @@ def _loads_symbolic_expr(expr_bytes, use_symengine=False): if expr_bytes == b"": return None if use_symengine: - _optional.HAS_SYMENGINE.require_now("loads_symbolic_expr") + _optional.HAS_SYMENGINE.require_now("load a symengine expression") from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module load_basic, ) From c9367b524319e8c1244ef6efbf080b5de56f6696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 27 Sep 2023 14:24:49 +0200 Subject: [PATCH 18/19] Update other message --- qiskit/qpy/binary_io/schedules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 4400bd49f7a6..83a023b9527b 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -404,7 +404,7 @@ def _dumps_symbolic_expr(expr, use_symengine): if expr is None: return b"" if use_symengine: - _optional.HAS_SYMENGINE.require_now("dumps_symbolic_expr") + _optional.HAS_SYMENGINE.require_now("dump a symengine expression") expr_bytes = expr.__reduce__()[1][0] else: from sympy import srepr, sympify From b479cf061fa95de432e7d0c76e41b20832ab5222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 27 Sep 2023 16:41:51 +0200 Subject: [PATCH 19/19] Rename Encoding to SymExprEncoding --- qiskit/qpy/interface.py | 4 ++-- qiskit/qpy/type_keys.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 2e092f14124e..c50bce14c623 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -157,7 +157,7 @@ def dump( version_match = VERSION_PATTERN_REGEX.search(__version__) version_parts = [int(x) for x in version_match.group("release").split(".")] - encoding = type_keys.Encoding.assign(use_symengine) + encoding = type_keys.SymExprEncoding.assign(use_symengine) header = struct.pack( formats.FILE_HEADER_V10_PACK, b"QISKIT", @@ -289,7 +289,7 @@ def load( if data.qpy_version < 10: use_symengine = False else: - use_symengine = data.symbolic_encoding == type_keys.Encoding.SYMENGINE + use_symengine = data.symbolic_encoding == type_keys.SymExprEncoding.SYMENGINE programs = [] for _ in range(data.num_programs): diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 391382108e7f..c87672e15cce 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -526,7 +526,7 @@ def retrieve(cls, type_key): raise NotImplementedError -class Encoding(TypeKeyBase): +class SymExprEncoding(TypeKeyBase): """Type keys for the symbolic encoding field in the file header.""" SYMPY = b"p"