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

Sequence building #1026

Merged
merged 10 commits into from
Sep 13, 2024
7 changes: 7 additions & 0 deletions src/qibolab/native.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ class Native(ABC, PulseSequence):
def create_sequence(self, *args, **kwargs) -> PulseSequence:
"""Create a sequence for single-qubit rotation."""

def __call__(self, *args, **kwargs) -> PulseSequence:
"""Create a sequence for single-qubit rotation.

Alias to :meth:`create_sequence`.
"""
return self.create_sequence(*args, **kwargs)


class RxyFactory(Native):
"""Factory for pulse sequences that generate single-qubit rotations around
Expand Down
56 changes: 54 additions & 2 deletions src/qibolab/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,69 @@ def pulse_channels(self, pulse_id: int) -> list[ChannelId]:
"""Find channels on which a pulse with a given id plays."""
return [channel for channel, pulse in self if pulse.id == pulse_id]

def concatenate(self, other: "PulseSequence") -> None:
def concatenate(self, other: Iterable[_Element]) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we accept any iterable here? Isn't PulseSequence sufficient? Same for __or__ and __ior__.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seq = (
q1.RX()
| p12.CZ()
| [(ch1.drive, Delay(duration=6.5))]
| q2.RX()
| q0.RX12()
| p02.CZ()
)

The explicit pulse-like here (the Delay) is contained in a list, which is perfectly homogeneous to a PulseSequence (since PulseSequence is a list subclass), but it's not a PulseSequence.

In general, it is very cheap to support, and quite user-friendly (e.g. even an iterator over _Element would work, so you can use literals such as list comprehension and generators, that otherwise should be manually wrapped in a PulseSequence).

"""Concatenate two sequences.

Appends ``other`` in-place such that the result is:
- ``self``
- necessary delays to synchronize channels
- ``other``
Guarantees that the all the channels in the concatenated
sequence will start simultaneously
"""
_synchronize(self, PulseSequence(other).channels)
alecandido marked this conversation as resolved.
Show resolved Hide resolved
self.extend(other)

def __ilshift__(self, other: Iterable[_Element]) -> "PulseSequence":
"""Juxtapose two sequences.

Alias to :meth:`concatenate`.
"""
self.concatenate(other)
return self

def __lshift__(self, other: Iterable[_Element]) -> "PulseSequence":
"""Juxtapose two sequences.

A copy is made, and no input is altered.

Other than that, it is based on :meth:`concatenate`.
"""
copy = self.copy()
copy <<= other
return copy

def juxtapose(self, other: Iterable[_Element]) -> None:
"""Juxtapose two sequences.

Appends ``other`` in-place such that the result is:
- ``self``
- necessary delays to synchronize channels
- ``other``
Guarantee simultaneous start and no overlap.
"""
_synchronize(self, other.channels)
_synchronize(self, PulseSequence(other).channels | self.channels)
self.extend(other)

def __ior__(self, other: Iterable[_Element]) -> "PulseSequence":
"""Juxtapose two sequences.

Alias to :meth:`concatenate`.
"""
self.juxtapose(other)
return self

def __or__(self, other: Iterable[_Element]) -> "PulseSequence":
"""Juxtapose two sequences.

A copy is made, and no input is altered.

Other than that, it is based on :meth:`concatenate`.
"""
copy = self.copy()
copy |= other
return copy

def align(self, channels: list[ChannelId]) -> Align:
"""Introduce align commands to the sequence."""
align = Align()
Expand Down
Empty file added tests/integration/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions tests/integration/test_sequence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from qibolab import create_platform
from qibolab.execution_parameters import ExecutionParameters
from qibolab.pulses import Delay


def test_sequence_creation():
platform = create_platform("dummy")

single = platform.natives.single_qubit
two = platform.natives.two_qubit

# How a complex sequence is supposed to be constructed
# ----------------------------------------------------

p02 = two[(0, 2)]
p12 = two[(1, 2)]
q0 = single[0]
q1 = single[1]
q2 = single[2]
ch1 = platform.qubits[1]

seq = (
q1.RX()
| p12.CZ()
| [(ch1.drive, Delay(duration=6.5))]
| q2.RX()
| q0.RX12()
| p02.CZ()
)
for q in range(3):
seq |= single[q].MZ()

# ----------------------------------------------------

nshots = 17
res = platform.execute([seq], ExecutionParameters(nshots=nshots))

for r in res.values():
assert r.shape == (nshots,)
3 changes: 3 additions & 0 deletions tests/test_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def test_fixed_sequence_factory():
assert np not in seq.channels
assert np not in fseq2.channels

# test alias
assert factory() == seq


@pytest.mark.parametrize(
"args,amplitude,phase",
Expand Down
75 changes: 75 additions & 0 deletions tests/test_sequence.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from copy import deepcopy

from pydantic import TypeAdapter

from qibolab.pulses import (
Expand Down Expand Up @@ -144,6 +146,79 @@ def test_concatenate():
assert isinstance(s1[3][1], Pulse)
assert s1[3][0] == "a"

# Check aliases
sa1 = deepcopy(s1)
sc1 = deepcopy(s1)
sa1 <<= s2
sc1.concatenate(s2)
assert sa1 == sc1
assert sc1 == s1 << s2


def test_juxtapose():
p1 = Pulse(duration=40, amplitude=0.9, envelope=Drag(rel_sigma=0.2, beta=1))
sequence1 = PulseSequence([("ch1", p1)])
p2 = Pulse(duration=60, amplitude=0.9, envelope=Drag(rel_sigma=0.2, beta=1))
sequence2 = PulseSequence([("ch2", p2)])

sequence1.juxtapose(sequence2)
assert set(sequence1.channels) == {"ch1", "ch2"}
assert len(list(sequence1.channel("ch1"))) == 1
assert len(list(sequence1.channel("ch2"))) == 2
assert sequence1.duration == 40 + 60
channel, delay = sequence1[1]
assert channel == "ch2"
assert isinstance(delay, Delay)
assert delay.duration == 40

sequence3 = PulseSequence(
[
(
"ch2",
Pulse(duration=80, amplitude=0.9, envelope=Drag(rel_sigma=0.2, beta=1)),
),
(
"ch3",
Pulse(
duration=100, amplitude=0.9, envelope=Drag(rel_sigma=0.2, beta=1)
),
),
]
)

sequence1.juxtapose(sequence3)
assert sequence1.channels == {"ch1", "ch2", "ch3"}
assert len(list(sequence1.channel("ch1"))) == 2
assert len(list(sequence1.channel("ch2"))) == 3
assert len(list(sequence1.channel("ch3"))) == 2
assert isinstance(next(iter(sequence1.channel("ch3"))), Delay)
assert sequence1.duration == 40 + 60 + 100
assert sequence1.channel_duration("ch1") == 40 + 60
assert sequence1.channel_duration("ch2") == 40 + 60 + 80
assert sequence1.channel_duration("ch3") == 40 + 60 + 100
delay = list(sequence1.channel("ch3"))[0]
assert isinstance(delay, Delay)
assert delay.duration == 100

# Check order preservation, even with various channels
vz = VirtualZ(phase=0.1)
s1 = PulseSequence([("a", p1), ("b", vz)])
s2 = PulseSequence([("a", vz), ("a", p2)])
s1.juxtapose(s2)
target_channels = ["a", "b", "b", "a", "a"]
target_pulse_types = [Pulse, VirtualZ, Delay, VirtualZ, Pulse]
for i, (channel, pulse) in enumerate(s1):
assert channel == target_channels[i]
assert isinstance(pulse, target_pulse_types[i])

# Check aliases
sa1 = deepcopy(s1)
sc1 = deepcopy(s1)
sa1 |= s2
sc1.juxtapose(s2)
assert sa1 == sc1
assert sc1 == s1 | s2


def test_copy():
sequence = PulseSequence(
Expand Down