Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/qibolab/_core/instruments/qblox/sequence/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Acquisition,
Align,
Delay,
LongPulse,
Pulse,
Readout,
VirtualZ,
Expand All @@ -17,6 +18,7 @@
Move,
Play,
Register,
SetAwgOffs,
SetPhDelta,
UpdParam,
Wait,
Expand Down Expand Up @@ -59,6 +61,81 @@ def _play_duration_swept(registers: dict[ParamRole, Register]) -> list[Instructi
]


def _process_longpulse(pulse: LongPulse, params: set[Param], merged_vzs: bool):
"""Emit Q1ASM for a LongPulse.

Uses ``set_awg_offs`` to produce a continuous CW tone without storing
any per-duration waveforms. The signal chain on the RF module is:

output = (waveform * gain + offset)

Timing: ``upd_param(4)`` starts the tone (4 ns), then ``wait(dur - 4)``
holds it. The DURATION sweep register already holds ``total_duration - 4``
(set by ``_longpulse_duration``). After the wait, ``set_awg_offs(0, 0)``
is latched and applied by the next real-time instruction.
"""
uid = pulse.id
duration_sweep = {
p.role: p.reg for p in params if p.role.value[1] is Parameter.duration
}
amplitude_sweep = {
p.role: p.reg for p in params if p.role.value[1] is Parameter.amplitude
}

if merged_vzs:
assert pulse.relative_phase == 0.0
phase_pre: list[Instruction] = []
phase_post: list[Instruction] = []
else:
phase = int(convert(pulse.relative_phase, Parameter.relative_phase))
minus_phase = int(convert(-pulse.relative_phase, Parameter.relative_phase))
phase_pre = (
[
Add(
a=Registers.phase_delta.value,
b=phase,
destination=Registers.phase_delta.value,
)
]
if phase != 0
else []
) + [SetPhDelta(value=Registers.phase_delta.value)]
phase_post = [Move(source=minus_phase, destination=Registers.phase_delta.value)]

if duration_sweep:
hold: list[Instruction] = [Wait(duration=duration_sweep[ParamRole.DURATION])]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Substract the 4 from the register here.

else:
hold = [Wait(duration=int(pulse.duration) - 4)] if pulse.duration > 4 else []

if amplitude_sweep:
pseudo_pulse = [
SetAwgOffs(value_0=amplitude_sweep[ParamRole.AMPLITUDE], value_1=0),
Line(
instruction=UpdParam(duration=4),
comment=f"longpulse id: 0x{uid.hex[:5]}",
),
]
else:
pseudo_pulse = [
SetAwgOffs(
value_0=int(convert(pulse.amplitude, Parameter.amplitude)), value_1=0
),
Line(
instruction=UpdParam(duration=4),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Instead of fixing this to 4, we can pass here a register, and remove the hold (wait instruction), which will be the duration of the pulkse.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

#1495 will support the arbitrary long (but fixed) durations. Even #1465 has been extended to mention the arbitrary instruction with long duration sweepers (excluding pulse duration sweepers).

comment=f"longpulse id: 0x{uid.hex[:5]}",
),
]

return (
phase_pre
+ pseudo_pulse
+ hold
+ phase_post
+ [SetAwgOffs(value_0=0, value_1=0)]
# Line(instruction=UpdParam(duration=4))] # This may be neeed if this is the last pulse in the sequence, otherwise it will never turn off the tone.

@alecandido alecandido Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Btw, the Qblox documentation is a bit misleading about this point. But eventually clear.

Some classical instructions like set_awg_offs and set_freq change special latched registers in the FPGA. These registers hold or latch the value until a RT instruction (such as upd_param or play) applies them to the parts of the sequencer that deal with signals.
[...]

2. The wait RT instruction does not update latched parameters.

https://docs.qblox.com/en/v2026.04.0/products/architecture/sequencers/sequencer.html#latched-instructions
(emphasis mine)

So, if you want to make sure to end your pulse at the correct time, you need to append an upd_param instruction. Otherwise, it may happen to be the last instruction before reset, and your pulse will last for all the time of the reset itself (if done through relaxation).

)


def _process_pulse(
pulse: Pulse, params: set[Param], waveforms: WaveformIndices, merged_vzs: bool
):
Expand Down Expand Up @@ -175,6 +252,8 @@ def play(
"""Process the individual pulse in experiment."""
pulse = parpulse[0]
params = parpulse[1]
if isinstance(pulse, LongPulse):
return _process_longpulse(pulse, params, merged_vzs)
if isinstance(pulse, Pulse):
return _process_pulse(pulse, params, waveforms, merged_vzs)
if isinstance(pulse, Delay):
Expand Down
3 changes: 2 additions & 1 deletion src/qibolab/_core/instruments/qblox/sequence/sweepers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from qibolab._core.instruments.qblox.sequence.asm import Registers
from qibolab._core.pulses.pulse import (
LongPulse,
Pulse,
PulseId,
PulseLike,
Expand Down Expand Up @@ -59,7 +60,7 @@ def from_sweeper(cls, sweep: Sweeper) -> "ParamRole":
def unique(cls, sweep: Sweeper) -> bool:
return sweep.parameter is not Parameter.duration or (
sweep.pulses is not None
and not any(isinstance(p, Pulse) for p in sweep.pulses)
and not any(isinstance(p, (Pulse, LongPulse)) for p in sweep.pulses)
)

@property
Expand Down
12 changes: 9 additions & 3 deletions src/qibolab/_core/instruments/qblox/sequence/waveforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,18 @@ def waveforms(

pulses_not_swept: list[Pulse] = []
pulses_swept: list[tuple[Pulse, Sweeper]] = []
_seen_swept: set = set()
for p in sequence:
if isinstance(p, (Pulse, Readout)):
if p.id in duration_swept:
pulses_swept.append(
(_pulse(p, p.id in amplitude_swept), duration_swept[p.id])
)
# Deduplicate by UUID: the same pulse object may appear N times
# in the sequence (pulse-train approach) but should only
# contribute one set of waveforms to avoid N-fold memory usage.
if p.id not in _seen_swept:
_seen_swept.add(p.id)
pulses_swept.append(
(_pulse(p, p.id in amplitude_swept), duration_swept[p.id])
)
else:
pulses_not_swept.append(_pulse(p, p.id in amplitude_swept))

Expand Down
23 changes: 22 additions & 1 deletion src/qibolab/_core/pulses/pulse.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Acquisition",
"Align",
"Delay",
"LongPulse",
"Pulse",
"PulseId",
"PulseLike",
Expand Down Expand Up @@ -98,6 +99,26 @@ def envelopes(self, sampling_rate: float) -> IqWaveform:
return np.array([self.i(sampling_rate), self.q(sampling_rate)])


class LongPulse(_PulseLike):
"""Long rectangular pulse for hardware with limited waveform memory.

On Qblox the backend stores a minimal 4-sample waveform and extends the
output with a Q1ASM ``wait``, avoiding the per-duration waveform storage
that exhausts AWG memory for swept long pulses.
"""

kind: Literal["longpulse"] = "longpulse"

duration: float
"""Total pulse duration [ns]."""

amplitude: float
"""Pulse digital amplitude (unitless), normalised between -1 and 1."""

relative_phase: float = 0.0
"""Relative phase of the pulse, in radians."""


class Delay(_PulseLike):
"""Wait instruction.

Expand Down Expand Up @@ -198,6 +219,6 @@ class Align(_PulseLike):


PulseLike = Annotated[
Union[Align, Pulse, Delay, VirtualZ, Acquisition, Readout],
Union[Align, LongPulse, Pulse, Delay, VirtualZ, Acquisition, Readout],
Field(discriminator="kind"),
]
5 changes: 3 additions & 2 deletions src/qibolab/_core/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from qibolab._core.pulses.pulse import PulseId, VirtualZ

from .identifier import ChannelId
from .pulses import Acquisition, Align, Delay, Pulse, PulseLike, Readout
from .pulses import Acquisition, Align, Delay, LongPulse, Pulse, PulseLike, Readout

__all__ = ["PulseSequence"]

Expand Down Expand Up @@ -284,7 +284,8 @@ def to_vzs(self) -> "PulseSequence":
el
for els in (
[(ch, ev)]
if not isinstance(ev, Pulse) or np.isclose(ev.relative_phase, 0)
if not isinstance(ev, (Pulse, LongPulse))
or np.isclose(ev.relative_phase, 0)
else [
(ch, VirtualZ(phase=ev.relative_phase)),
(ch, ev.model_copy(update={"relative_phase": 0})),
Expand Down