Control flow¶
Control flow is built from context managers. Each with block pushes a
container onto the program's block stack, and every operation appended while it
is open lands in that container. program.body is the root container, and
leaving a with block pops the stack back to the enclosing one.
Five constructs make up the control flow: sweep, which is the only loop;
average, which repeats a body and collapses the repetitions; the if_ /
elif_ / else_ chain; block, a grouping with no semantics of its own; and
Parallel, which has no method of its own and is built with | on two or more
sweep contexts. None of them says where the code runs. Validation derives that
from the operations inside, which
Real-time and host-side covers.
sweep¶
A Sweep binds a Variable to each value a SweepSource produces. What
changes between a hardware ramp, an explicit table, a log-spaced set, and a
composed pattern is the source, not the block, so there is one loop type rather
than one per shape of values.
import qprogram as qp
program = qp.QProgram()
freq = program.variable("freq", units="Hz")
with program.sweep(freq).from_range(4e9, 6e9, 1e6):
program.set_frequency("drive_q0", freq)
program.play("drive_q0", "pi_pulse")
program.measure("readout_q0", "readout", "weights")
Naming the source¶
sweep(variable, source) binds a source object directly. sweep(variable),
with the source left out, returns a source builder whose from_* methods
construct one. Both produce the same Sweep node and the same .qp line, so
the choice is about the call site.
with program.sweep(freq).from_range(4e9, 6e9, 1e6):
...
with program.sweep(freq, qp.Range(4e9, 6e9, 1e6)):
...
Reach for the builder when writing a sweep out by hand: it is the shorter
spelling and needs no source class in scope. Pass the object when the source is
computed rather than written, which covers holding it in a variable, building it
in a comprehension, reading it from a scan spec, and nesting combinators deeper
than the rotate and repeat shortcuts below reach.
An omitted source is detected with a sentinel rather than None, so
sweep(freq, None), a source that failed to be computed, is rejected instead of
quietly returning a builder: Sweep source must be a SweepSource or a 1-D
sequence of values, got None. A bare 1-D sequence in the source position is
accepted as shorthand for qp.Values.
A builder is not a context manager, because it has no values yet. Entering one
raises ValidationError listing the from_* methods and the two-argument form,
rather than sweeping nothing. Reaching for repeat or rotate on a builder
raises AttributeError for the same reason: those shape values that have
already been picked.
Every registered source has a builder, matched on the class name with case and
underscores ignored, so from_iq_table finds a source class named IQTable.
The five scalar built-ins are also written out as real methods (from_range,
from_linspace, from_logspace, from_values, from_file) so that editors
complete and type-check them; everything else resolves against the live
registry, the combinators and vendor-registered sources included. Registering a
source is the whole of what its builder needs. A misspelling lists what exists:
AttributeError: no sweep source is registered for 'from_rang'. Did you mean from_range, from_rotate, from_repeat? Available: from_concat, from_file, from_linspace, from_logspace, from_range, from_repeat, from_rotate, from_values. Add one with qp.register_sweep_source(cls) and its builder appears here too.
The built-in sources¶
| Source | Fluent builder | Points | Kind | Token |
|---|---|---|---|---|
qp.Range(start, stop, step=1) |
.from_range(start, stop, step) |
start + step * i |
linear | sweep.range |
qp.Linspace(start, stop, num) |
.from_linspace(start, stop, num) |
num points over the closed interval |
linear | sweep.linspace |
qp.Logspace(start, stop, num) |
.from_logspace(start, stop, num) |
numpy.geomspace(start, stop, num) |
arbitrary | sweep.logspace |
qp.Values(points) |
.from_values(points) |
the array as given | arbitrary | sweep.values |
qp.File(path) |
.from_file(path) |
the 1-D array a .npy file holds |
arbitrary | sweep.file |
qp.Concat(sources) |
.from_concat(sources) |
every part's points, in order | arbitrary | sweep.concat |
qp.Repeat(source, times) |
.from_repeat(source, times), .repeat(times) |
the inner points tiled times times |
arbitrary | sweep.repeat |
qp.Rotate(source, by=1) |
.from_rotate(source, by), .rotate(by) |
the inner points shifted left by by |
arbitrary | sweep.rotate |
Range takes step=1 by default. The ramp always begins at start and holds
round((stop - start) / step) + 1 points, so it lands on stop only when
step divides stop - start evenly. Otherwise the last point falls short or
overshoots: qp.Range(0, 1, 0.3) gives 0, 0.3, 0.6, 0.9, qp.Range(0, 1, 0.6)
gives 0, 0.6, 1.2, and qp.Range(0, 0.4, 1) is the single point 0.0. The
rounding is deliberate, since it absorbs the floating-point division noise in a
range like (0.0, 1.0, 0.01). Use Linspace when the last point has to land
exactly on stop. A zero step raises, and so does a step pointing away from
stop: Range step -0.1 moves away from stop (0.0 -> 1.0); flip the step sign
or swap the bounds.
Linspace includes both ends, and num=1 yields [start]. It also exposes
step(), which returns (stop - start) / (num - 1) for a compiler that wants
the ramp in start-and-step form, and 0.0 for a single point, where the spacing
is undefined.
Logspace takes the actual first and last values, not the exponents
numpy.logspace takes, so a frequency sweep reads
qp.Logspace(1e6, 1e9, num=50) rather than qp.Logspace(6, 9, num=50). Both
bounds must be strictly positive.
Values accepts anything numpy.asarray accepts and stores it as a 1-D float
array; a 2-D input raises, as does an empty one. Its parameter is named points
so that it does not collide with the values() method every source implements.
values() hands back the stored array itself rather than a copy, so treat it as
read-only: the source's equality and hash are derived from it.
File stores the path, so a .qp file records where the points came from
instead of inlining them. Nothing is cached, because a loaded array would enter
the source's structural equality and an already-loaded instance would stop
comparing equal to a fresh one. Both length() and values() therefore read
the file, which has to be readable wherever the program is validated or run: a
missing path raises OSError, and a file holding an empty or multi-dimensional
array raises ValidationError.
The three combinators wrap other sources. A bare sequence where a source is
expected is wrapped in Values, so qp.Rotate([0.0, 1.57, 3.14], by=1) needs
no inner constructor. Concat takes an iterable, so a generator expression
works directly, and a single source passed where the iterable belongs raises
with Write Concat([a, b]) or Concat(gen_expr). Repeat tiles:
qp.Repeat(qp.Values([0, 1]), times=3) sweeps 0, 1, 0, 1, 0, 1, and each
repetition is a sweep point of its own with its own result entry, which is what
separates it from average. Rotate shifts left by by, so
qp.Rotate(qp.Values([0, 1, 2, 3]), by=1) sweeps 1, 2, 3, 0; by may be
negative, shifting right, or larger than the point count, wrapping as
numpy.roll does, and the point count is unchanged either way. The
phase-cycling pattern falls out of the two together:
qp.Concat(qp.Rotate(base, by=i) for i in range(base.length())).
Nesting combinators more than two deep is a signal to write a named
SweepSource subclass instead. A registered subclass serializes from its own
public attributes, so the .qp file records one constructor call carrying the
parameters that describe the pattern rather than the stack of wrappers that
builds it.
A sweep with no points never executes its body, so a built-in rejects an empty
parameterization as soon as it can see one: at construction for the sources
that carry their values, and on first read for File, which learns the length
only when it loads the array. Wherever length() returns, it returns at least
one, and qp.sweeps.validate_source holds a subclass to the same rule.
What a source has to answer¶
Three things, all without running the program: length(), values(), and the
class-level KIND. Parallel needs the length at construction to refuse loops
that cannot advance in lockstep, and the reference executor needs it to size
every result array before the first shot. A platform reads KIND through
ValidationContext.sweep_kind_of to choose between a loop register with an
increment, a value table, and a host-side step per point. The interpreter, the
result coordinates, and qp.optimize all need the concrete values.
That contract is why a source cannot wrap a callable: a deferred function answers none of the three ahead of time. Passing one says so, and says what to do instead.
ValidationError: Sweep source must be a SweepSource, not a callable. A source describes its values statically (length, kind, and a serializable parameterization); a function can answer none of those before the program runs. Materialize it — Values(f(...)) — or declare a SweepSource subclass with the parameters it needs.
Linear or arbitrary¶
KIND is a claim about compilability, not a description of the numbers.
sweep.linear means the values are exactly start + step * i, which is what
lets a sequencer run the loop from one register plus an increment, and Range
and Linspace are the two sources that claim it. Everything else reports
sweep.arbitrary, meaning a value table or a host-side step per point.
Two sources can produce identical values and still differ here.
qp.Values([0, 1, 2]) is arbitrary even though the numbers are evenly spaced,
because nothing about the source proves that regularity to a compiler.
Combinators degrade in the same conservative direction and always report
arbitrary, Repeat of a linear source included: a tiled ramp is re-runnable as
a nested loop, but it is not itself start + step * i. Under-claiming costs a
platform one optimization; over-claiming would have it emit a single ramp for a
sweep that is not one.
Capability tokens have the same two levels. A Sweep requires block.sweep
plus the source's own token and its sweep.<kind>, and a combinator unions the
tokens of what it wraps, so qp.Rotate(qp.Logspace(1e6, 1e9, 50)) needs
block.sweep, sweep.rotate, sweep.arbitrary, and sweep.logspace. A
platform that cannot generate a Logspace therefore also refuses a rotation of
one, instead of silently materializing the points into a table.
Shaping a bound source¶
repeat and rotate are also methods on the loop context, which covers the
everyday shaping without naming a combinator class. Each wraps whatever is bound
so far, so the outermost wrapper is the last call:
base = [0.0, 1.57, 3.14]
with program.sweep(freq).from_values(base).rotate(by=1).repeat(3):
... # qp.Repeat(qp.Rotate(qp.Values(base), by=1), times=3)
with program.sweep(freq).from_values(base).repeat(3).rotate(by=1):
... # qp.Rotate(qp.Repeat(qp.Values(base), times=3), by=1)
Both are pure, the way | is: they return a fresh context and leave the
original one usable. They shape one sweep, so calling either on a |
composition raises.
ValidationError: repeat() shapes one sweep's source, but this context already composes 2 sweeps with `|`. Call repeat() on each sweep before composing them.
Writing your own source¶
Subclass qp.SweepSource, declare KIND and TOKEN, implement length() and
values(), and register the class. Its public attributes are its parameters, so
the .qp form is derived from the object rather than from a per-class callback.
import numpy as np
@qp.register_sweep_source
class Chevron(qp.SweepSource):
KIND = "arbitrary"
TOKEN = "sweep.chevron"
def __init__(self, center: float, span: float, num: int) -> None:
self.center, self.span, self.num = center, span, num
def length(self) -> int:
return self.num
def values(self) -> np.ndarray:
return np.linspace(self.center - self.span / 2, self.center + self.span / 2, self.num)
register_sweep_source returns the class, so it works as a decorator. It keys
the registry by __name__, which is the constructor name on the wire, and adds
TOKEN to the capability registry so a profile can list it without a separate
call. Re-registering the same class is a no-op; registering a different class
under a taken name raises ValueError, since it would change how every existing
file parses that constructor.
The source now round-trips through .qp as
Chevron(center=..., span=..., num=...), reports its token to the validator,
composes inside the combinators, and gets its own from_chevron builder, none
of which needs a change in the core. qp.sweeps.validate_source(source) checks
the length() and values() invariants; it materializes the values, so it
belongs in a test rather than on the hot path. For a one-off computation with no
parameters worth naming, skip the class and materialize the array:
qp.Values(my_function(...)).
average¶
average(shots) repeats its body shots times and averages the measurement
results over the repetitions. shots has to be an integer of at least one, and
a bool is rejected even though it is an int: Average shots must be an
integer >= 1, got True.
Averaging is repetition, so the block occupies a repetition level on the
sequencer and counts toward the loop-nesting limit exactly as a sweep does.
Unlike a sweep it contributes no dimension to the results. The executor
accumulates a sum and a shot count per sweep point and divides, so iq and
raw come back as means and state as the excited-state population over the
shots. qp.Repeat is the opposite choice: it turns each repetition into a sweep
point with its own result entry. The block requires block.average and nothing
else.
with program.average(shots=1000):
program.play("drive_q0", "pi_pulse")
program.measure("readout_q0", "readout", "weights")
block¶
A generic container with no semantics of its own, used to group operations or scope a comment.
with program.block():
program.set_phase("drive_q0", 0.0)
program.play("drive_q0", "pi_pulse")
program.wait("drive_q0", 50)
It requires block.block, contributes no result dimension, and occupies no
repetition level. It is not inert to validation, though: the real-time and
host-side consensus is computed per block over that block's direct operation
children, so grouping operations changes which of them are compared with each
other.
Conditionals¶
if_, elif_, and else_ build a chain of arms from sequential with blocks,
mirroring the shape of Python's own if statement.
schema = qp.BusSchema.transmon()
q = schema.q
program = qp.QProgram(schema=schema)
m = program.measure(
q[0].readout,
"readout",
"weights",
fields=(qp.MeasurementField.IQ, qp.MeasurementField.STATE),
)
with program.if_(m.state == 1):
program.play(q[0].drive, "pi_pulse")
with program.elif_(m.state == 0):
program.play(q[0].drive, "id_pulse")
with program.else_():
program.sync()
What a condition may reference¶
A condition is a single Comparison holding at least one measurement-state
reference and, apart from that, only int literals. Four spellings reach it:
m.state == 1andm.state != 0, a measurement against anintliteral1 == m.state, the reverse order, which builds the same nodem1.state == m2.state, one measurement against another, for asking whether two qubits landed in the same stateqp.eq(m.state, 1)andqp.ne(m.state, 1), the helper forms, for building a condition without relying on operator overloading
state is the only field a condition may read. A classified scalar is the only
thing there is to branch on, so iq and raw are excluded by design rather
than by omission. Only == and != exist, because those are the two operators
the proxy behind handle.state overloads: m.state < 1 raises TypeError from
Python itself. Comparing against a float or a bool also raises TypeError,
the latter with handle.state cannot be compared to a bool; use 0 or 1 to
compare against a classified state.
Anything outside that shape is refused at the if_() or elif_() call, which
names what it got instead. A bare variable comparison and a logical combination
of two conditions both fail there:
ValidationError: if_() condition must reference at least one measurement-state ref (e.g. `handle.state`); got a comparison of Variable and Constant
ValidationError: if_() expects a Comparison condition such as `handle.state == 0` or `handle.state != 1`; got LogicalBinaryOp
There is no and or or of two conditions. Nest a second if_ inside the arm
instead.
Chain rules¶
elif_ and else_ find the open chain through pending-chain state that if_
records when it appends its Conditional, and that is cleared as soon as
anything else is appended at the same level. Each therefore has to follow the
matching if_ or elif_ immediately and at the same nesting level. An
operation or a block() in between closes the chain, and the following elif_
raises elif_() must immediately follow an if_() / elif_() block at the same
nesting level; no open conditional chain. Appends inside an arm body sit deeper
on the block stack and leave the chain open.
A chain takes at most one else_, which terminates it: leaving the else_ body
clears the pending-chain state, so a following elif_ or else_ raises the
same no-open-chain error. Conditionals nest, and a chain written inside a loop
body is a chain at that level.
Requesting state classification¶
Reading handle.state requires that the producing measurement asked for
classification. measure(...) defaults to fields=(MeasurementField.IQ,), so
pass fields=(qp.MeasurementField.IQ, qp.MeasurementField.STATE), or
fields=(qp.MeasurementField.STATE,) for state alone. Without it qp.validate
emits a missing-classification error: Conditional references
q0/readout/m0.state, but the measurement does not request state classification
(add MeasurementField.STATE to fields=). In an m1.state == m2.state
comparison both measurements are checked. A condition naming a handle that no
measurement in the program produces gives unknown-measurement instead. Both
checks are profile-independent, so they run whatever the platform declares.
The Conditional node¶
Conditional does not use the inherited elements list, because each arm
carries its own body and there is no shared body to put there. arms holds
(condition, body) pairs in source order, and the terminal else body lives on
else_body, which is None when the chain has none. append raises rather
than landing a node somewhere with no meaning.
cond = program.body.elements[-1]
isinstance(cond, qp.blocks.Conditional) # True
len(cond.arms) # 2: the if_ and the elif_
[(arm[0].op, arm[0].right.value) for arm in cond.arms] # [('==', 1), ('==', 0)]
cond.else_body # the else_ body, or None
walk() yields the conditional, then each arm body in source order, then the
else body. Arm conditions are expressions rather than AST nodes, so they are
not yielded; read arms for those. variables() does include the conditions'
variables, so a branch taken on a swept threshold counts that variable as part
of the conditional even when no operation inside reads it. Branching selects a
body rather than iterating, so a conditional occupies no repetition level.
The block requires block.conditional plus the expr.* tokens of every arm
condition, which for m.state == 1 are expr.comparison,
expr.measurement_ref, and expr.constant. The else arm has no condition and
contributes none.
Active reset¶
Reset by measurement is what the construct exists for: measure the qubit, then
apply a pi-pulse only if it landed in |1⟩.
m = program.measure(
q[0].readout,
"readout",
"weights",
fields=(qp.MeasurementField.IQ, qp.MeasurementField.STATE),
)
with program.if_(m.state == 1):
program.play(q[0].drive, "pi_pulse")
Written this way the program names no vendor operation, so it runs anywhere
block.conditional and measure.fields.state are declared. Compared with
calling a vendor's packaged active_reset, the trade-off is that a platform
with a tuned reset choreography receives the general pattern rather than a
request for its own.
Parallel loops with |¶
| on two or more sweep contexts composes them into a Parallel block that
advances the loops in lockstep over one shared body. That is how a program
sweeps coupled parameters along a single axis instead of over their cross
product.
gain = program.variable("gain")
with program.sweep(freq, qp.Linspace(4e9, 6e9, 101)) | program.sweep(gain, qp.Linspace(0.0, 1.0, 101)):
program.set_frequency("drive_q0", freq)
program.set_gain("drive_q0", gain)
program.play("drive_q0", "pi_pulse")
Every composed loop must report the same number of iterations. Sources answer
that statically, so the check happens when the with block opens rather than at
run time, and the error names the counts it found. Composing
qp.Range(4e9, 6e9, 1e6) with qp.Range(0.0, 1.0, 0.01) is the easy mistake:
the two read as a matched pair but hold 2001 and 101 points.
ValidationError: parallel loops must have the same number of iterations to advance in lockstep; got Sweep('freq'): 2001, Sweep('gain'): 101
Kinds may differ. A linear ramp composes with an explicit table, as long as the table holds the same number of points:
with program.sweep(freq).from_linspace(4e9, 6e9, 41) | program.sweep(gain).from_values(measured_gains):
program.play("drive_q0", qp.waveforms.Gaussian(amplitude=gain, duration=40, sigma=8))
Extra pipes chain more than two. __or__ is pure: it returns a fresh context
carrying the concatenated list and touches the program only on entry, so a list
of sweeps can be folded programmatically.
import functools
import operator
specs = [(freq, qp.Linspace(4e9, 6e9, 41)), (gain, qp.Linspace(0.0, 1.0, 41))]
composed = functools.reduce(operator.or_, [program.sweep(v, s) for v, s in specs])
with composed:
program.play("drive_q0", "pi_pulse")
The composed headers live on loops, not among elements, which holds the
shared body. walk() yields the block, then each header with its descendants,
then the body, so a consumer meets the loops that bind the variables before the
operations that read them, and variables() unions the headers' variables back
in, since the inherited walk over the body alone would miss them.
In the result DataArray a parallel composition is one dimension, named by
joining the variable ids with | ("freq|gain"), and each variable contributes
its own coordinate array on that shared dimension.
Parallel has no context-manager method of its own. Constructing one directly,
as an analyzer or a code generator might, is qp.blocks.Parallel(loops=[...])
with at least two qp.blocks.Sweep instances: fewer raises Parallel requires
at least two loops, got 1.
Nesting¶
Blocks nest to any depth, and the innermost open one receives whatever is appended next.
delay = program.variable("delay")
freqs = qp.Linspace(4e9, 6e9, 41)
gains = qp.Linspace(0.0, 1.0, 41)
with program.average(shots=1000): # level 1
with program.sweep(freq, freqs) | program.sweep(gain, gains): # level 2
with program.block(): # no level
with program.sweep(delay, qp.Range(0, 200, 4)): # level 3
program.set_frequency("drive_q0", freq)
program.set_gain("drive_q0", gain)
program.wait("drive_q0", delay)
program.play("drive_q0", "pi_pulse")
program.measure("readout_q0", "readout", "weights")
What a platform limit counts is repetition levels, not blocks. Sweep,
Parallel, and Average each declare REPEATS = True and contribute one
level, a Parallel one in total rather than one per composed loop, because its
headers advance together instead of nesting. A Conditional and a plain block
contribute none. Validation reads that flag rather than testing concrete
classes, so a vendor block that repeats its body is counted correctly by
declaring the flag too. The deepest count wrapping any leaf is
max_loop_nesting, which is 3 in the program above: the average, the parallel
composition, and the inner sweep.
When the platform declares a max_loop_nesting limit and the program exceeds
it, validation returns a limit-exceeded error, Program nests loops 4 deep;
limit max_loop_nesting=3. max_parallel_loops is checked the same way against
the widest Parallel in the program.
Wire form¶
A sweep is a for header: for <var> in <Source>(param=value, ...):, with
every source parameter written as a keyword so that positional ordering cannot
drift. Values is the one special case, rendered as the bracket literal
[0.1, 0.2, 0.3] and never truncated, since the literal has to reload to
exactly the same sweep. A Parallel joins its headers with a pipe, repeating the
for keyword for each. average <shots>: and block: are keyword-led like any
registered block, and a Conditional writes one header per arm, if <cond>:,
elif <cond>:, else:, with the condition's outer parentheses dropped.
The program below uses every construct on this page.
schema = qp.BusSchema.transmon()
q = schema.q
program = qp.QProgram(label="control-flow-forms", schema=schema)
freq = program.variable("freq", units="Hz")
gain = program.variable("gain")
phase = program.variable("phase")
with program.average(shots=1000):
with program.sweep(freq).from_linspace(4e9, 6e9, 101) | program.sweep(gain).from_linspace(0.0, 1.0, 101):
program.set_frequency(q[0].drive, freq)
program.set_gain(q[0].drive, gain)
with program.block():
program.play(q[0].drive, "pi_pulse")
program.sync()
m = program.measure(
q[0].readout,
"readout",
"weights",
fields=(qp.MeasurementField.IQ, qp.MeasurementField.STATE),
)
with program.if_(m.state == 1):
program.play(q[0].drive, "pi_pulse")
with program.else_():
program.wait(q[0].drive, 40)
with program.sweep(phase, qp.Concat(qp.Rotate([0.0, 1.57, 3.14], by=i) for i in range(3))):
program.set_phase(q[0].drive, phase)
print(qp.dumps(program))
#!QProgram 1.0
metadata:
label: "control-flow-forms"
schema:
element q:
drive info=IQ
readout info=IQ+acquires
body:
var freq units="Hz"
var gain
var phase
average 1000:
for freq in Linspace(start=4000000000.0, stop=6000000000.0, num=101) | for gain in Linspace(start=0.0, stop=1.0, num=101):
set_frequency q[0].drive freq
set_gain q[0].drive gain
block:
play q[0].drive "pi_pulse"
sync
measure q[0].readout "readout" "weights" name="q0/readout/m0" fields=["state", "iq"]
if q0/readout/m0.state == 1:
play q[0].drive "pi_pulse"
else:
wait q[0].drive 40
for phase in Concat(sources=[Rotate(source=[0.0, 1.57, 3.14], by=0), Rotate(source=[0.0, 1.57, 3.14], by=1), Rotate(source=[0.0, 1.57, 3.14], by=2)]):
set_phase q[0].drive phase
Real-time and host-side¶
QProgram has no real-time loop keyword. The same sweep may compile to the
sequencer in one program and be stepped from Python in another, and validation
decides per node. An operation's domain is the set of slots, rt and host, of
its routed bus that declare the operation's tokens. A block takes the consensus
of its own operation children, and operation children in different domains at
one level are a mixed-domain error. Domain constraints contributed by platform
predicates subtract from the block they target, a real-time-only block may not
contain a host-side block child (host-in-rt) while the reverse is always
allowed, and a block that could have run real-time but ends up host-side carries
one forced-host warning naming the reason.
Capabilities, diagnostics, and profiles has the routing rules
in full.
The reference platform declares the bus-scoped parameter operations,
set_parameter and get_parameter, in the host half of each bus slot only,
and carries a predicate that excludes rt from the loop binding a variable fed
to set_parameter. Real platforms are wired the same way, which is what makes
plans and forced-host warnings against the reference platform mean something.
A program that sweeps a slow-control knob outside a pulse loop classifies like
this:
platform = qp.ReferencePlatform(schema=schema)
program = qp.QProgram(schema=schema)
freq = program.variable("freq")
power = program.variable("power")
with program.average(shots=1000):
with program.sweep(power).from_range(0.0, 10.0, 5.0):
program.set_parameter(q[0].drive, "attenuation", power)
with program.sweep(freq).from_linspace(4e9, 6e9, 3):
program.set_frequency(q[0].drive, freq)
program.play(q[0].drive, "pi_pulse")
program.measure(q[0].readout, "readout", "weights")
print(qp.explain(program, platform.capabilities))
plan — errors: 0 · warnings: 1 · info: 0
body
└─ average 1000: [host] ~ forced-host: contains host-side-only sub-block 'Sweep' (parameter 'attenuation' is swept via set_parameter (host-side dispatch per iteration))
└─ for power in Range(start=0.0, stop=10.0, step=5.0): [host]
├─ set_parameter q[0].drive "attenuation" power [host]
└─ for freq in Linspace(start=4000000000.0, stop=6000000000.0, num=3): [rt|host]
├─ set_frequency q[0].drive freq [rt|host]
├─ play q[0].drive "pi_pulse" [rt|host]
└─ measure q[0].readout "readout" "weights" name="q0/readout/m0" [rt|host]
The shot loop is host-side here as well, and not because of anything inside it: a host-side-only sub-block forces its parent, which is what the warning reports. The inner frequency sweep keeps both domains, so a compiler is free to run it on the sequencer.
Measurement names inside loops¶
Measurement names are allocated when measure is called, not per iteration, and
the counter is derived from the AST on each call rather than stored on the
program. A measure inside a sweep therefore has exactly one name however
many times the loop runs. Two measure calls on the same bus in one loop body
get distinct names, q0/readout/m0 and q0/readout/m1, and the result array
carries both. See Measurements and results.
Related pages¶
- Operations for the operations that go inside these blocks
- Variables and expressions for what a swept variable can feed
- The
.qpformat for the grammar behind the wire forms above