Skip to content

Commit 11e32f8

Browse files
committed
Merge branch 'main' into emulator-cudaq-latest
2 parents cd2178c + e618b6d commit 11e32f8

File tree

18 files changed

+571
-437
lines changed

18 files changed

+571
-437
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ repos:
1010
- id: check-toml
1111
- id: debug-statements
1212
- repo: https://github.com/astral-sh/ruff-pre-commit
13-
rev: v0.14.13
13+
rev: v0.15.2
1414
hooks:
1515
# Run the linter.
1616
- id: ruff

doc/source/main-documentation/experiment.rst

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ Qibolab offers a range of pre-defined pulse shapes which can be found in :py:mod
2323
- Exponential (:class:`qibolab.Exponential`)
2424
- Gaussian (:class:`qibolab.Gaussian`)
2525
- Drag (:class:`qibolab.Drag`)
26-
- IIR (:class:`qibolab.Iir`)
2726
- SNZ (:class:`qibolab.Snz`)
28-
- eCap (:class:`qibolab.ECap`)
2927
- Custom (:class:`qibolab.Custom`)
3028

3129
To illustrate, here is an examples of how to instantiate a pulse using the Qibolab API:
@@ -69,7 +67,6 @@ To organize pulses into sequences, Qibolab provides the :class:`qibolab.PulseSeq
6967

7068
from qibolab import Pulse, PulseSequence, Rectangular
7169

72-
7370
pulse1 = Pulse(
7471
duration=40, # timing, in all qibolab, is expressed in ns
7572
amplitude=0.5, # this amplitude is relative to the range of the instrument

doc/source/tutorials/lab.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ Here is the ``create()`` method that loads the parameters from the JSON:
276276
)
277277
from qibolab.instruments import DummyInstrument
278278

279-
280279
FOLDER = Path.cwd()
281280

282281

@@ -365,7 +364,6 @@ the instrument dictionary when instantiating the :class:`qibolab.Platform`, in t
365364
)
366365
from qibolab.instruments import DummyInstrument
367366

368-
369367
FOLDER = Path.cwd()
370368

371369

poetry.lock

Lines changed: 146 additions & 152 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/qibolab/_core/execution_parameters.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,14 @@ def estimate_duration(
111111
self,
112112
sequences: list[PulseSequence],
113113
sweepers: list[ParallelSweepers],
114+
time_of_flight: float = 0.0,
114115
) -> float:
115116
"""Estimate experiment duration."""
116117
duration = sum(seq.duration for seq in sequences)
117118
relaxation = default(self.relaxation_time, 0)
118119
nshots = default(self.nshots, 0)
119120
return (
120-
(duration + len(sequences) * relaxation)
121+
(duration + len(sequences) * relaxation + time_of_flight)
121122
* nshots
122123
* nano
123124
* prod(iteration_length(s) for s in sweepers)

src/qibolab/_core/instruments/qblox/cluster.py

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from qibolab._core.instruments.abstract import Controller
1616
from qibolab._core.pulses.pulse import PulseId
1717
from qibolab._core.sequence import PulseSequence
18+
from qibolab._core.serialize import Model
1819
from qibolab._core.sweeper import ParallelSweepers, normalize_sweepers
1920

2021
from . import config
@@ -31,6 +32,40 @@
3132
SAMPLING_RATE = 1
3233

3334

35+
def _compute_duration(
36+
ps: PulseSequence,
37+
sweepers: list[ParallelSweepers],
38+
options: ExecutionParameters,
39+
configs: Configs,
40+
) -> float:
41+
"""Compute the total program duration including time of flight and
42+
synchronization waiting time.
43+
"""
44+
45+
# TODO: include the time of flight calculation at the level of
46+
# Platform.execute rather than in the qblox driver. This will require
47+
# propagating the changes also to qibocal.
48+
time_of_flight = max(
49+
[time_of_flights(configs)[ch[0]] for ch in ps if hasattr(ch[1], "acquisition")],
50+
default=0.0,
51+
)
52+
53+
# TODO: wait_sync duration is determined as explained in this comment
54+
# https://github.com/qiboteam/qibolab/pull/1389#issuecomment-3884129213.
55+
# It should be checked with Qblox if the sync time can indeed be of the
56+
# order of 1000 ns.
57+
wait_sync_duration = 1000
58+
duration = options.estimate_duration(
59+
[ps], sweepers, time_of_flight + wait_sync_duration
60+
)
61+
return duration
62+
63+
64+
class ClusterConfigs(Model):
65+
modules: dict[int, config.ModuleConfig]
66+
sequencers: dict[int, dict[int, config.SequencerConfig]]
67+
68+
3469
class Cluster(Controller):
3570
"""Controller object for Qblox cluster."""
3671

@@ -134,13 +169,13 @@ def play(
134169

135170
# then configure modules and sequencers
136171
# (including sequences upload)
137-
sequencers = self._configure(
138-
sequences_, configs, options_.acquisition_type
172+
sequencers, _ = self.configure(
173+
configs, options_.acquisition_type, sequences=sequences_
139174
)
140175
log.status(self.cluster, sequencers)
141176

142177
# finally execute the experiment, and fetch results
143-
duration = options_.estimate_duration([ps], sweepers_)
178+
duration = _compute_duration(ps, sweepers_, options_, configs)
144179
data = self._execute(
145180
sequencers, sequences_, duration, options_.acquisition_type
146181
)
@@ -161,18 +196,33 @@ def play(
161196
results |= concat_shots(psres, options)
162197
return results
163198

164-
def _configure(
199+
def configure(
165200
self,
166-
sequences: dict[ChannelId, Q1Sequence],
167201
configs: Configs,
168-
acquisition: AcquisitionType,
169-
) -> SequencerMap:
202+
acquisition: AcquisitionType = AcquisitionType.INTEGRATION,
203+
sequences: Optional[dict[ChannelId, Q1Sequence]] = None,
204+
) -> tuple[SequencerMap, ClusterConfigs]:
170205
"""Configure modules and sequencers.
171206
172207
The return value consists of the association map from channels
173208
to sequencers, for each module.
209+
210+
For configuration testing purpose, it is possible to also configure modules and
211+
sequencers with no sequence provided. In which case, it will attempt to assign
212+
sequencers to all available channels (as opposed to just those involved in the
213+
experiment, and thus in the sequences).
214+
For the sake of simplifiying the usage of this function, a default acquisition
215+
type is provided (:attr:`AcquisitionType.INTEGRATION`). The only true
216+
alternative to this value is :attr:`AcquisitionType.RAW`, since further
217+
configurations are required to operate in scope mode.
174218
"""
175219
sequencers = defaultdict(dict)
220+
exec_mode = sequences is not None
221+
sequences_ = defaultdict(lambda: None, sequences if exec_mode else {})
222+
223+
modcfgs = {}
224+
seqcfgs = {}
225+
176226
for slot, chs in self._channels_by_module.items():
177227
module = self._modules[slot]
178228

@@ -185,24 +235,42 @@ def _configure(
185235
los = config.module.los(self._los, configs, ids)
186236
mixers = config.module.mixers(self._mixers, configs, ids)
187237
# compute module configurations, and apply them
188-
config.ModuleConfig.build(
189-
channels, configs, los, mixers, module.is_qrm_type
190-
).apply(module)
238+
modcfg = modcfgs[slot] = config.ModuleConfig.build(
239+
channels, configs, los, mixers
240+
)
241+
modcfg.apply(module)
242+
seqcfgs[slot] = {}
191243

192-
# configure all sequencers, and store active ones' association to channels
244+
# configure all sequencers, and store association to channels
193245
rf = module.is_rf_type
194246
for idx, ((ch, address), sequencer) in enumerate(
195247
zip(chs, module.sequencers)
196248
):
197-
seq = sequences.get(ch, Q1Sequence.empty())
198-
config.SequencerConfig.build(
199-
address, seq, ch, self.channels, configs, acquisition, idx, rf
200-
).apply(sequencer)
201-
# only collect active sequencers
202-
if not seq.is_empty:
203-
sequencers[slot][ch] = idx
249+
# only configure and register sequencer for active channels
250+
# for passive channels the sequencer operations are not relevant, e.g. a
251+
# flux channel with no registered pulse will still set an offset, but
252+
# this will happen at port level, and it is consumed in the
253+
# `ModuleConfig` above
254+
# if not in execution mode, cnfigure all channels, to test the
255+
# configuration itself
256+
if exec_mode and ch not in sequences:
257+
continue
258+
259+
seqcfg = seqcfgs[slot][idx] = config.SequencerConfig.build(
260+
address,
261+
ch,
262+
self.channels,
263+
configs,
264+
acquisition,
265+
idx,
266+
rf,
267+
sequence=sequences_[ch],
268+
)
269+
seqcfg.apply(sequencer)
270+
# populate channel-to-sequencer mapping
271+
sequencers[slot][ch] = idx
204272

205-
return sequencers
273+
return sequencers, ClusterConfigs(modules=modcfgs, sequencers=seqcfgs)
206274

207275
def _execute(
208276
self,
@@ -219,8 +287,8 @@ def _execute(
219287
module.arm_sequencer(seq)
220288
module.start_sequencer()
221289

222-
# approximately wait for experiment completion
223-
time.sleep(duration + 1)
290+
# wait for experiment completion
291+
time.sleep(duration)
224292

225293
# fetch acquired results
226294
acquisitions = {}

0 commit comments

Comments
 (0)