Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QPY Qiskit 1.0 updates #1365

Closed
wants to merge 15 commits into from
1 change: 1 addition & 0 deletions qiskit_ibm_runtime/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"""

from .interface import dump, load
from .common import QPY_VERSION, QPY_COMPATIBILITY_VERSION

# For backward compatibility. Provide, Runtime, Experiment call these private functions.
from .binary_io import (
Expand Down
116 changes: 101 additions & 15 deletions qiskit_ibm_runtime/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@
from qiskit.circuit.gate import Gate
from qiskit.circuit.singleton import SingletonInstruction, SingletonGate
from qiskit.circuit.controlledgate import ControlledGate
from qiskit.circuit.annotated_operation import (
AnnotatedOperation,
Modifier,
InverseModifier,
ControlModifier,
PowerModifier,
)
from qiskit.circuit.instruction import Instruction
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.quantumregister import QuantumRegister, Qubit
from qiskit.quantum_info.operators import SparsePauliOp
from qiskit.quantum_info.operators import SparsePauliOp, Clifford
from qiskit.synthesis import evolution as evo_synth
from qiskit.transpiler.layout import Layout, TranspileLayout
from .. import common, formats, type_keys
Expand Down Expand Up @@ -130,6 +137,8 @@ def _loads_instruction_parameter( # type: ignore[no-untyped-def]
):
if type_key == type_keys.Program.CIRCUIT:
param = common.data_from_binary(data_bytes, read_circuit, version=version)
elif type_key == type_keys.Value.MODIFIER:
param = common.data_from_binary(data_bytes, _read_modifier)
elif type_key == type_keys.Container.RANGE:
data = formats.RANGE._make(struct.unpack(formats.RANGE_PACK, data_bytes))
param = range(data.start, data.stop, data.step)
Expand Down Expand Up @@ -301,6 +310,8 @@ def _read_instruction( # type: ignore[no-untyped-def]
gate_class = getattr(circuit_mod, gate_name)
elif hasattr(controlflow, gate_name):
gate_class = getattr(controlflow, gate_name)
elif gate_name == "Clifford":
gate_class = Clifford
else:
raise AttributeError("Invalid instruction type: %s" % gate_name)

Expand Down Expand Up @@ -389,6 +400,9 @@ def _parse_custom_operation( # type: ignore[no-untyped-def]
) = custom_operations[gate_name]
else:
type_str, num_qubits, num_clbits, definition = custom_operations[gate_name]
# Strip the trailing "_{uuid}" from the gate name if the version >=11
if version >= 11:
gate_name = "_".join(gate_name.split("_")[:-1])
type_key = type_keys.CircuitInstruction(type_str)

if type_key == type_keys.CircuitInstruction.INSTRUCTION:
Expand Down Expand Up @@ -427,6 +441,14 @@ def _parse_custom_operation( # type: ignore[no-untyped-def]
inst_obj.definition = definition
return inst_obj

if version >= 11 and type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION:
with io.BytesIO(base_gate_raw) as base_gate_obj:
base_gate = _read_instruction(
base_gate_obj, None, registers, custom_operations, version, vectors, use_symengine
)
inst_obj = AnnotatedOperation(base_op=base_gate, modifiers=params)
return inst_obj

if type_key == type_keys.CircuitInstruction.PAULI_EVOL_GATE:
return definition

Expand Down Expand Up @@ -473,6 +495,25 @@ def _read_pauli_evolution_gate(file_obj, version, vectors): # type: ignore[no-u
return return_gate


def _read_modifier(file_obj): # type: ignore[no-untyped-def]
modifier = formats.MODIFIER_DEF._make(
struct.unpack(
formats.MODIFIER_DEF_PACK,
file_obj.read(formats.MODIFIER_DEF_SIZE),
)
)
if modifier.type == b"i":
return InverseModifier()
elif modifier.type == b"c":
return ControlModifier(
num_ctrl_qubits=modifier.num_ctrl_qubits, ctrl_state=modifier.ctrl_state
)
elif modifier.type == b"p":
return PowerModifier(power=modifier.power)
else:
raise TypeError("Unsupported modifier.")


def _read_custom_operations(file_obj, version, vectors): # type: ignore[no-untyped-def]
custom_operations = {}
custom_definition_header = formats.CUSTOM_CIRCUIT_DEF_HEADER._make(
Expand Down Expand Up @@ -580,6 +621,9 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): # type: igno
if isinstance(param, QuantumCircuit):
type_key = type_keys.Program.CIRCUIT
data_bytes = common.data_to_binary(param, write_circuit)
elif isinstance(param, Modifier):
type_key = type_keys.Value.MODIFIER
data_bytes = common.data_to_binary(param, _write_modifier)
elif isinstance(param, range):
type_key = type_keys.Container.RANGE
data_bytes = struct.pack(formats.RANGE_PACK, param.start, param.stop, param.step)
Expand Down Expand Up @@ -612,15 +656,19 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): # type: igno

# pylint: disable=too-many-boolean-expressions
def _write_instruction( # type: ignore[no-untyped-def]
file_obj, instruction, custom_operations, index_map, use_symengine
file_obj, instruction, custom_operations, index_map, use_symengine, version
):
gate_class_name = instruction.operation.base_class.__name__
if isinstance(instruction.operation, Instruction):
gate_class_name = instruction.operation.base_class.__name__
else:
gate_class_name = instruction.operation.__class__.__name__
custom_operations_list = []
if (
(
not hasattr(library, gate_class_name)
and not hasattr(circuit_mod, gate_class_name)
and not hasattr(controlflow, gate_class_name)
and gate_class_name != "Clifford"
)
or gate_class_name == "Gate"
or gate_class_name == "Instruction"
Expand All @@ -630,18 +678,21 @@ def _write_instruction( # type: ignore[no-untyped-def]
custom_operations[instruction.operation.name] = instruction.operation
custom_operations_list.append(instruction.operation.name)
gate_class_name = instruction.operation.name

if version >= 11:
# Assign a uuid to each instance of a custom operation
gate_class_name = f"{gate_class_name}_{uuid.uuid4().hex}"
# ucr*_dg gates can have different numbers of parameters,
# the uuid is appended to avoid storing a single definition
# in circuits with multiple ucr*_dg gates.
if instruction.operation.name in ["ucrx_dg", "ucry_dg", "ucrz_dg"]:
gate_class_name += "_" + str(uuid.uuid4())
elif instruction.operation.name in {"ucrx_dg", "ucry_dg", "ucrz_dg"}:
gate_class_name = f"{gate_class_name}_{uuid.uuid4()}"

if gate_class_name not in custom_operations:
custom_operations[gate_class_name] = instruction.operation
custom_operations_list.append(gate_class_name)
custom_operations[gate_class_name] = instruction.operation
custom_operations_list.append(gate_class_name)

elif gate_class_name == "ControlledGate":
# controlled gates can have the same name but different parameter
elif gate_class_name in {"ControlledGate", "AnnotatedOperation"}:
# controlled or annotated gates can have the same name but different parameter
# values, the uuid is appended to avoid storing a single definition
# in circuits with multiple controlled gates.
gate_class_name = instruction.operation.name + "_" + str(uuid.uuid4())
Expand All @@ -665,7 +716,7 @@ def _write_instruction( # type: ignore[no-untyped-def]
condition_value = int(instruction.operation.condition[1])

gate_class_name = gate_class_name.encode(common.ENCODE)
label = getattr(instruction.operation, "label")
label = getattr(instruction.operation, "label", None)
if label:
label_raw = label.encode(common.ENCODE)
else:
Expand All @@ -678,8 +729,12 @@ def _write_instruction( # type: ignore[no-untyped-def]
instruction.operation.target,
tuple(instruction.operation.cases_specifier()),
]
elif isinstance(instruction.operation, Clifford):
instruction_params = [instruction.operation.tableau]
elif isinstance(instruction.operation, AnnotatedOperation):
instruction_params = instruction.operation.modifiers
else:
instruction_params = instruction.operation.params
instruction_params = getattr(instruction.operation, "params", [])

num_ctrl_qubits = getattr(instruction.operation, "num_ctrl_qubits", 0)
ctrl_state = getattr(instruction.operation, "ctrl_state", 0)
Expand Down Expand Up @@ -761,8 +816,33 @@ def _write_elem(buffer, op): # type: ignore[no-untyped-def]
file_obj.write(synth_data)


def _write_modifier(file_obj, modifier): # type: ignore[no-untyped-def]
if isinstance(modifier, InverseModifier):
type_key = b"i"
num_ctrl_qubits = 0
ctrl_state = 0
power = 0.0
elif isinstance(modifier, ControlModifier):
type_key = b"c"
num_ctrl_qubits = modifier.num_ctrl_qubits
ctrl_state = modifier.ctrl_state
power = 0.0
elif isinstance(modifier, PowerModifier):
type_key = b"p"
num_ctrl_qubits = 0
ctrl_state = 0
power = modifier.power
else:
raise TypeError("Unsupported modifier.")

modifier_data = struct.pack(
formats.MODIFIER_DEF_PACK, type_key, num_ctrl_qubits, ctrl_state, power
)
file_obj.write(modifier_data)


def _write_custom_operation( # type: ignore[no-untyped-def]
file_obj, name, operation, custom_operations, use_symengine
file_obj, name, operation, custom_operations, use_symengine, version
):
type_key = type_keys.CircuitInstruction.assign(operation)
has_definition = False
Expand Down Expand Up @@ -793,6 +873,9 @@ def _write_custom_operation( # type: ignore[no-untyped-def]
num_ctrl_qubits = operation.num_ctrl_qubits
ctrl_state = operation.ctrl_state
base_gate = operation.base_gate
elif type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION:
has_definition = False
base_gate = operation.base_op
elif operation.definition is not None:
has_definition = True
data = common.data_to_binary(operation.definition, write_circuit)
Expand All @@ -807,6 +890,7 @@ def _write_custom_operation( # type: ignore[no-untyped-def]
custom_operations,
{},
use_symengine,
version,
)
base_gate_raw = base_gate_buffer.getvalue()
name_raw = name.encode(common.ENCODE)
Expand Down Expand Up @@ -1045,7 +1129,7 @@ def _read_layout_v2(file_obj, circuit): # type: ignore[no-untyped-def]


def write_circuit( # type: ignore[no-untyped-def]
file_obj, circuit, metadata_serializer=None, use_symengine=False
file_obj, circuit, metadata_serializer=None, use_symengine=False, version=common.QPY_VERSION
):
"""Write a single QuantumCircuit object in the file like object.

Expand All @@ -1060,6 +1144,7 @@ def write_circuit( # type: ignore[no-untyped-def]
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.
version (int): The QPY format version to use for serializing this circuit
"""
metadata_raw = json.dumps(
circuit.metadata, separators=(",", ":"), cls=metadata_serializer
Expand Down Expand Up @@ -1100,7 +1185,7 @@ def write_circuit( # type: ignore[no-untyped-def]
index_map["c"] = {bit: index for index, bit in enumerate(circuit.clbits)}
for instruction in circuit.data:
_write_instruction(
instruction_buffer, instruction, custom_operations, index_map, use_symengine
instruction_buffer, instruction, custom_operations, index_map, use_symengine, version
)

with io.BytesIO() as custom_operations_buffer:
Expand All @@ -1117,6 +1202,7 @@ def write_circuit( # type: ignore[no-untyped-def]
operation,
custom_operations,
use_symengine,
version,
)
)

Expand Down
62 changes: 29 additions & 33 deletions qiskit_ibm_runtime/qpy/binary_io/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,19 @@
from io import BytesIO
import numpy as np

import symengine as sym
from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module
load_basic,
)

from qiskit.pulse import library, channels, instructions
from qiskit.pulse.schedule import ScheduleBlock
from qiskit.utils import optionals as _optional
from qiskit.pulse.configuration import Kernel, Discriminator
from .. import formats, common, type_keys
from ..exceptions import QpyError
from . import value


if _optional.HAS_SYMENGINE:
import symengine as sym
else:
import sympy as sym


def _read_channel(file_obj, version): # type: ignore[no-untyped-def]
type_key = common.read_type_key(file_obj)
index = value.read_value(file_obj, version, {})
Expand Down Expand Up @@ -113,25 +111,17 @@ def _read_discriminator(file_obj, version): # type: ignore[no-untyped-def]
def _loads_symbolic_expr(expr_bytes, use_symengine=False): # type: ignore[no-untyped-def]
if expr_bytes == b"":
return None

expr_bytes = zlib.decompress(expr_bytes)
if use_symengine:
_optional.HAS_SYMENGINE.require_now("load a symengine expression")
from symengine.lib.symengine_wrapper import ( # pylint: disable=import-outside-toplevel, no-name-in-module
load_basic,
)

expr = load_basic(zlib.decompress(expr_bytes))
return load_basic(expr_bytes)

else:
from sympy import parse_expr # pylint: disable=import-outside-toplevel

expr_txt = zlib.decompress(expr_bytes).decode(common.ENCODE)
expr = parse_expr(expr_txt)
if _optional.HAS_SYMENGINE:
from symengine import sympify # pylint: disable=import-outside-toplevel

return sympify(expr)
return expr
return expr


def _read_symbolic_pulse(file_obj, version): # type: ignore[no-untyped-def]
Expand Down Expand Up @@ -167,21 +157,15 @@ def _read_symbolic_pulse(file_obj, version): # type: ignore[no-untyped-def]
class_name = "SymbolicPulse" # Default class name, if not in the library

if pulse_type in legacy_library_pulses:
# Once complex amp support will be deprecated we will need:
# parameters["angle"] = np.angle(parameters["amp"])
# parameters["amp"] = np.abs(parameters["amp"])

# In the meanwhile we simply add:
parameters["angle"] = 0
parameters["angle"] = np.angle(parameters["amp"])
parameters["amp"] = np.abs(parameters["amp"])
_amp, _angle = sym.symbols("amp, angle")
envelope = envelope.subs(_amp, _amp * sym.exp(sym.I * _angle))

# And warn that this will change in future releases:
warnings.warn(
"Complex amp support for symbolic library pulses will be deprecated. "
"Once deprecated, library pulses loaded from old QPY files (Terra version < 0.23),"
" will be converted automatically to float (amp,angle) representation.",
PendingDeprecationWarning,
f"Library pulses with complex amp are no longer supported. "
f"{pulse_type} with complex amp was converted to (amp,angle) representation.",
UserWarning,
)
class_name = "ScalableSymbolicPulse"

Expand Down Expand Up @@ -256,6 +240,19 @@ def _read_symbolic_pulse_v6(file_obj, version, use_symengine): # type: ignore[n
valid_amp_conditions=valid_amp_conditions,
)
elif class_name == "ScalableSymbolicPulse":
# Between Qiskit 0.40 and 0.46, the (amp, angle) representation was present,
# but complex amp was still allowed. In Qiskit 1.0 and beyond complex amp
# is no longer supported and so the amp needs to be checked and converted.
# Once QPY version is bumped, a new reader function can be introduced without
# this check.
if isinstance(parameters["amp"], complex):
parameters["angle"] = np.angle(parameters["amp"])
parameters["amp"] = np.abs(parameters["amp"])
warnings.warn(
f"ScalableSymbolicPulse with complex amp are no longer supported. "
f"{pulse_type} with complex amp was converted to (amp,angle) representation.",
UserWarning,
)
return library.ScalableSymbolicPulse(
pulse_type=pulse_type,
duration=duration,
Expand Down Expand Up @@ -424,7 +421,6 @@ def _dumps_symbolic_expr(expr, use_symengine): # type: ignore[no-untyped-def]
return b""

if use_symengine:
_optional.HAS_SYMENGINE.require_now("dump a symengine expression")
expr_bytes = expr.__reduce__()[1][0]
else:
from sympy import srepr, sympify # pylint: disable=import-outside-toplevel
Expand Down Expand Up @@ -602,8 +598,8 @@ def read_schedule_block( # type: ignore[no-untyped-def]


def write_schedule_block( # type: ignore[no-untyped-def]
file_obj, block, metadata_serializer=None, use_symengine=False
):
file_obj, block, metadata_serializer=None, use_symengine=False, version=common.QPY_VERSION
): # pylint: disable=unused-argument
"""Write a single ScheduleBlock object in the file like object.

Args:
Expand All @@ -618,7 +614,7 @@ def write_schedule_block( # type: ignore[no-untyped-def]
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.

version (int): The QPY format version to use for serializing this circuit block
Raises:
TypeError: If any of the instructions is invalid data format.
"""
Expand Down
Loading
Loading