Skip to content

Commit

Permalink
Add in qpy the support for Discriminator and Kernel (Qiskit#10327)
Browse files Browse the repository at this point in the history
* add support for Discriminator and Kernel

* make Kernel and Discriminator hashable

* correct _assert_nested_dict_equal()

Co-authored-by: Will Shanks <wshaos@posteo.net>

* ensure b does not have extra keys

Co-authored-by: Will Shanks <wshaos@posteo.net>

* devide _read_kernel_and_discriminator and _write_kernel_and_discriminator

* update docs

* add tests testing Kernel/Discriminator equality

* add a qpy compatibility test

* add releasenote

* improve hashing in Kernel/Discriminator

* rm hashing from Kernel/Discriminator

* use write/read_sequence() to serialize lists

* add (de)serializer for dict and list

---------

Co-authored-by: Will Shanks <wshaos@posteo.net>
Co-authored-by: Will Shanks <willshanks@us.ibm.com>
  • Loading branch information
3 people authored and to24toro committed Aug 3, 2023
1 parent f75030c commit 05f72ed
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 20 deletions.
30 changes: 30 additions & 0 deletions qiskit/pulse/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,31 @@
Configurations for pulse experiments.
"""
from typing import Dict, Union, Tuple, Optional
import numpy as np

from .channels import PulseChannel, DriveChannel, MeasureChannel
from .exceptions import PulseError


def _assert_nested_dict_equal(a, b):
if len(a) != len(b):
return False
for key in a:
if key in b:
if isinstance(a[key], dict):
if not _assert_nested_dict_equal(a[key], b[key]):
return False
elif isinstance(a[key], np.ndarray):
if not np.all(a[key] == b[key]):
return False
else:
if a[key] != b[key]:
return False
else:
return False
return True


class Kernel:
"""Settings for this Kernel, which is responsible for integrating time series (raw) data
into IQ points.
Expand All @@ -41,6 +61,11 @@ def __repr__(self):
", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()),
)

def __eq__(self, other):
if isinstance(other, Kernel):
return _assert_nested_dict_equal(self.__dict__, other.__dict__)
return False


class Discriminator:
"""Setting for this Discriminator, which is responsible for classifying kerneled IQ points
Expand All @@ -64,6 +89,11 @@ def __repr__(self):
", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()),
)

def __eq__(self, other):
if isinstance(other, Discriminator):
return _assert_nested_dict_equal(self.__dict__, other.__dict__)
return False


class LoRange:
"""Range of LO frequency."""
Expand Down
94 changes: 94 additions & 0 deletions qiskit/qpy/binary_io/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import zlib
import warnings

from io import BytesIO

import numpy as np

from qiskit.exceptions import QiskitError
Expand All @@ -25,6 +27,7 @@
from qiskit.qpy.binary_io import value
from qiskit.qpy.exceptions import QpyError
from qiskit.utils import optionals as _optional
from qiskit.pulse.configuration import Kernel, Discriminator

if _optional.HAS_SYMENGINE:
import symengine as sym
Expand Down Expand Up @@ -60,6 +63,46 @@ def _read_waveform(file_obj, version):
)


def _loads_obj(type_key, binary_data, version, vectors):
"""Wraps `value.loads_value` to deserialize binary data to dictionary
or list objects which are not supported by `value.loads_value`.
"""
if type_key == b"D":
with BytesIO(binary_data) as container:
return common.read_mapping(
file_obj=container, deserializer=_loads_obj, version=version, vectors=vectors
)
elif type_key == b"l":
with BytesIO(binary_data) as container:
return common.read_sequence(
file_obj=container, deserializer=_loads_obj, version=version, vectors=vectors
)
else:
return value.loads_value(type_key, binary_data, version, vectors)


def _read_kernel(file_obj, version):
params = common.read_mapping(
file_obj=file_obj,
deserializer=_loads_obj,
version=version,
vectors={},
)
name = value.read_value(file_obj, version, {})
return Kernel(name=name, **params)


def _read_discriminator(file_obj, version):
params = common.read_mapping(
file_obj=file_obj,
deserializer=_loads_obj,
version=version,
vectors={},
)
name = value.read_value(file_obj, version, {})
return Discriminator(name=name, **params)


def _loads_symbolic_expr(expr_bytes):
from sympy import parse_expr

Expand Down Expand Up @@ -229,6 +272,7 @@ def _read_alignment_context(file_obj, version):
return instance


# pylint: disable=too-many-return-statements
def _loads_operand(type_key, data_bytes, version):
if type_key == type_keys.ScheduleOperand.WAVEFORM:
return common.data_from_binary(data_bytes, _read_waveform, version=version)
Expand All @@ -241,6 +285,18 @@ def _loads_operand(type_key, data_bytes, version):
return common.data_from_binary(data_bytes, _read_channel, version=version)
if type_key == type_keys.ScheduleOperand.OPERAND_STR:
return data_bytes.decode(common.ENCODE)
if type_key == type_keys.ScheduleOperand.KERNEL:
return common.data_from_binary(
data_bytes,
_read_kernel,
version=version,
)
if type_key == type_keys.ScheduleOperand.DISCRIMINATOR:
return common.data_from_binary(
data_bytes,
_read_discriminator,
version=version,
)

return value.loads_value(type_key, data_bytes, version, {})

Expand Down Expand Up @@ -300,6 +356,38 @@ def _write_waveform(file_obj, data):
value.write_value(file_obj, data.name)


def _dumps_obj(obj):
"""Wraps `value.dumps_value` to serialize dictionary and list objects
which are not supported by `value.dumps_value`.
"""
if isinstance(obj, dict):
with BytesIO() as container:
common.write_mapping(file_obj=container, mapping=obj, serializer=_dumps_obj)
binary_data = container.getvalue()
return b"D", binary_data
elif isinstance(obj, list):
with BytesIO() as container:
common.write_sequence(file_obj=container, sequence=obj, serializer=_dumps_obj)
binary_data = container.getvalue()
return b"l", binary_data
else:
return value.dumps_value(obj)


def _write_kernel(file_obj, data):
name = data.name
params = data.params
common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj)
value.write_value(file_obj, name)


def _write_discriminator(file_obj, data):
name = data.name
params = data.params
common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj)
value.write_value(file_obj, name)


def _dumps_symbolic_expr(expr):
from sympy import srepr, sympify

Expand Down Expand Up @@ -364,6 +452,12 @@ def _dumps_operand(operand):
elif isinstance(operand, str):
type_key = type_keys.ScheduleOperand.OPERAND_STR
data_bytes = operand.encode(common.ENCODE)
elif isinstance(operand, Kernel):
type_key = type_keys.ScheduleOperand.KERNEL
data_bytes = common.data_to_binary(operand, _write_kernel)
elif isinstance(operand, Discriminator):
type_key = type_keys.ScheduleOperand.DISCRIMINATOR
data_bytes = common.data_to_binary(operand, _write_discriminator)
else:
type_key, data_bytes = value.dumps_value(operand)

Expand Down
11 changes: 7 additions & 4 deletions qiskit/qpy/type_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
MemorySlot,
RegisterSlot,
)
from qiskit.pulse.configuration import Discriminator, Kernel
from qiskit.pulse.instructions import (
Acquire,
Play,
Expand Down Expand Up @@ -334,10 +335,8 @@ class ScheduleOperand(TypeKeyBase):
WAVEFORM = b"w"
SYMBOLIC_PULSE = b"s"
CHANNEL = b"c"

# Discriminator and Acquire instance are not serialzied.
# Data format of these object is somewhat opaque and not defiend well.
# It's rarely used in the Qiskit experiements. Of course these can be added later.
KERNEL = b"k"
DISCRIMINATOR = b"d"

# We need to have own string type definition for operands of schedule instruction.
# Note that string type is already defined in the Value namespace,
Expand All @@ -355,6 +354,10 @@ def assign(cls, obj):
return cls.CHANNEL
if isinstance(obj, str):
return cls.OPERAND_STR
if isinstance(obj, Kernel):
return cls.KERNEL
if isinstance(obj, Discriminator):
return cls.DISCRIMINATOR

raise exceptions.QpyError(
f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
features:
- |
QPY supports the :class:`~qiskit.pulse.configuration.Discriminator` and
:class:`~qiskit.pulse.configuration.Kernel` objects.
This feature enables users to serialize and deserialize the
:class:`~qiskit.pulse.instructions.Acquire` instructions with these objects
using QPY.
109 changes: 108 additions & 1 deletion test/python/pulse/test_experiment_configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@

"""Test cases for the experimental conditions for pulse."""
import unittest
import numpy as np

from qiskit.pulse.channels import DriveChannel, MeasureChannel, AcquireChannel
from qiskit.pulse.exceptions import PulseError
from qiskit.pulse import LoConfig, LoRange
from qiskit.pulse import LoConfig, LoRange, Kernel, Discriminator
from qiskit.test import QiskitTestCase


Expand Down Expand Up @@ -93,5 +94,111 @@ def test_get_channel_lo(self):
lo_config.channel_lo(MeasureChannel(1))


class TestKernel(QiskitTestCase):
"""Test Kernel."""

def test_eq(self):
"""Test if two kernels are equal."""
kernel_a = Kernel(
"kernel_test",
kernel={"real": np.zeros(10), "imag": np.zeros(10)},
bias=[0, 0],
)
kernel_b = Kernel(
"kernel_test",
kernel={"real": np.zeros(10), "imag": np.zeros(10)},
bias=[0, 0],
)
self.assertTrue(kernel_a == kernel_b)

def test_neq_name(self):
"""Test if two kernels with different names are not equal."""
kernel_a = Kernel(
"kernel_test",
kernel={"real": np.zeros(10), "imag": np.zeros(10)},
bias=[0, 0],
)
kernel_b = Kernel(
"kernel_test_2",
kernel={"real": np.zeros(10), "imag": np.zeros(10)},
bias=[0, 0],
)
self.assertFalse(kernel_a == kernel_b)

def test_neq_params(self):
"""Test if two kernels with different parameters are not equal."""
kernel_a = Kernel(
"kernel_test",
kernel={"real": np.zeros(10), "imag": np.zeros(10)},
bias=[0, 0],
)
kernel_b = Kernel(
"kernel_test",
kernel={"real": np.zeros(10), "imag": np.zeros(10)},
bias=[1, 0],
)
self.assertFalse(kernel_a == kernel_b)

def test_neq_nested_params(self):
"""Test if two kernels with different nested parameters are not equal."""
kernel_a = Kernel(
"kernel_test",
kernel={"real": np.zeros(10), "imag": np.zeros(10)},
bias=[0, 0],
)
kernel_b = Kernel(
"kernel_test",
kernel={"real": np.ones(10), "imag": np.zeros(10)},
bias=[0, 0],
)
self.assertFalse(kernel_a == kernel_b)


class TestDiscriminator(QiskitTestCase):
"""Test Discriminator."""

def test_eq(self):
"""Test if two discriminators are equal."""
discriminator_a = Discriminator(
"discriminator_test",
discriminator_type="linear",
params=[1, 0],
)
discriminator_b = Discriminator(
"discriminator_test",
discriminator_type="linear",
params=[1, 0],
)
self.assertTrue(discriminator_a == discriminator_b)

def test_neq_name(self):
"""Test if two discriminators with different names are not equal."""
discriminator_a = Discriminator(
"discriminator_test",
discriminator_type="linear",
params=[1, 0],
)
discriminator_b = Discriminator(
"discriminator_test_2",
discriminator_type="linear",
params=[1, 0],
)
self.assertFalse(discriminator_a == discriminator_b)

def test_neq_params(self):
"""Test if two discriminators with different parameters are not equal."""
discriminator_a = Discriminator(
"discriminator_test",
discriminator_type="linear",
params=[1, 0],
)
discriminator_b = Discriminator(
"discriminator_test",
discriminator_type="non-linear",
params=[0, 0],
)
self.assertFalse(discriminator_a == discriminator_b)


if __name__ == "__main__":
unittest.main()
15 changes: 0 additions & 15 deletions test/python/pulse/test_instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,31 +67,16 @@ def test_can_construct_valid_acquire_command(self):

def test_instructions_hash(self):
"""Test hashing for acquire instruction."""
kernel_opts = {"start_window": 0, "stop_window": 10}
kernel = configuration.Kernel(name="boxcar", **kernel_opts)

discriminator_opts = {
"neighborhoods": [{"qubits": 1, "channels": 1}],
"cal": "coloring",
"resample": False,
}
discriminator = configuration.Discriminator(
name="linear_discriminator", **discriminator_opts
)
acq_1 = instructions.Acquire(
10,
channels.AcquireChannel(0),
channels.MemorySlot(0),
kernel=kernel,
discriminator=discriminator,
name="acquire",
)
acq_2 = instructions.Acquire(
10,
channels.AcquireChannel(0),
channels.MemorySlot(0),
kernel=kernel,
discriminator=discriminator,
name="acquire",
)

Expand Down
Loading

0 comments on commit 05f72ed

Please sign in to comment.