Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog/41.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`Sweep` takes a `SweepSource` and nothing else. A sequence of points in the source position is now a `ValidationError` naming the two spellings that pick explicit values, `sweep(variable, qp.Values([...]))` and `sweep(variable).from_values([...])`, so the block always holds something that can answer its own length and kind and write itself back out to `.qp`. The combinators are unaffected, and so is the format's bracket literal, which still reads back as `Values`.
4 changes: 1 addition & 3 deletions docs/guide/control-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ 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`.
quietly returning a builder: `Sweep source must be a SweepSource, got None`.

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,
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ built the offending node.
| `qprogram.fragments` | A fragment name that is malformed or reserved, a parameter colliding with a local variable, the wrong number of call arguments, an unknown or duplicated keyword, an unsupported argument type, a call cycle, and an expansion result that is not a bus or a waveform |
| `qprogram.sweeps.builtin` | Non-numeric or non-finite bounds, `num < 1`, a zero step, a step pointing away from `stop`, non-positive `Logspace` bounds, and a `Values` or `File` array that is empty or not 1-D |
| `qprogram.sweeps.combinators` | A `Repeat` count below 1, a non-integer `Rotate` offset, `Concat` given a single source or none, and any combinator argument that is a callable rather than a `SweepSource` |
| `qprogram.blocks` | A `Sweep` source that is a callable or not a 1-D sequence, a `Parallel` with fewer than two loops or with mismatched iteration counts, `Average(shots)` below 1, and appending directly to a `Conditional` |
| `qprogram.blocks` | A `Sweep` source that is not a `SweepSource`, a `Parallel` with fewer than two loops or with mismatched iteration counts, `Average(shots)` below 1, and appending directly to a `Conditional` |
| `qprogram.operations.operation` | A `fields=` that is a bare string, not iterable, empty, or names a field no capability token registers |
| `qprogram.waveforms.iq_pair` | An `IQPair` whose I and Q channels have different concrete durations |
| `qprogram.result` | An empty `MeasurementHandle` name, and `QProgramResult.get(field=None)` |
Expand Down
33 changes: 13 additions & 20 deletions src/qprogram/blocks/sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ class Sweep(Block):

Args:
variable (Variable): The [`Variable`][qprogram.Variable] rebound on each iteration.
source (SweepSource): The [`SweepSource`][qprogram.SweepSource] describing the values. A bare
1-D sequence is accepted as a shorthand for [`Values`][qprogram.Values].
source (SweepSource): The [`SweepSource`][qprogram.SweepSource] describing the values.

Raises:
ValidationError: If ``source`` is neither a source nor a sequence of values.
ValidationError: If ``source`` is not a [`SweepSource`][qprogram.SweepSource].
"""

REPEATS: ClassVar[bool] = True
Expand All @@ -47,7 +46,7 @@ class Sweep(Block):
def __init__(self, variable: Variable, source: SweepSource) -> None:
super().__init__()
self.variable = variable
self.source = _coerce_source(source)
self.source = _require_source(source)

def num_iterations(self) -> int:
"""Return the number of sweep points, delegated to the source.
Expand Down Expand Up @@ -87,25 +86,22 @@ def required_capabilities(self) -> set[str]:
return {"block.sweep"} | self.source.tokens()


def _coerce_source(source: object) -> SweepSource:
"""Return ``source`` as a [`SweepSource`][qprogram.SweepSource], wrapping a bare sequence.
def _require_source(source: object) -> SweepSource:
"""Return ``source`` unchanged, refusing anything that is not a [`SweepSource`][qprogram.SweepSource].

A sequence is the shorthand spelling of [`Values`][qprogram.Values]. A callable is refused
outright: a deferred function can report neither its length nor its kind before the program runs,
and cannot be serialized to ``.qp``.
The block holds a description of the values, never the values themselves: a source answers its
length and its kind before the program runs, and writes itself back out to ``.qp``. A callable
gets its own message, since it answers none of that at any point.

Args:
source (object): A sweep source, or a 1-D sequence of values to wrap.
source (object): The value offered as the sweep's source.

Returns:
The source unchanged, or a [`Values`][qprogram.Values] over the given points.
``source``, once it is known to be a sweep source.

Raises:
ValidationError: If ``source`` is a callable, or is not a 1-D sequence of values.
ValidationError: If ``source`` is not a [`SweepSource`][qprogram.SweepSource].
"""
# qprogram.sweeps imports this module, so the shorthand's import stays lazy.
from qprogram.sweeps.builtin import Values # ruff: ignore[import-outside-top-level]

if isinstance(source, SweepSource):
return source
if callable(source):
Expand All @@ -116,8 +112,5 @@ def _coerce_source(source: object) -> SweepSource:
"SweepSource subclass with the parameters it needs."
)
raise ValidationError(msg)
try:
return Values(source) # ty:ignore[invalid-argument-type]
except (ValidationError, TypeError, ValueError) as e:
msg = f"Sweep source must be a SweepSource or a 1-D sequence of values, got {source!r}"
raise ValidationError(msg) from e
msg = f"Sweep source must be a SweepSource, got {source!r}"
raise ValidationError(msg)
6 changes: 2 additions & 4 deletions src/qprogram/qprogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,17 +1311,15 @@ def sweep(self, variable: Variable, source: SweepSource | _Unset = _UNSET) -> _S

Args:
variable (Variable): The [`Variable`][qprogram.Variable] rebound each iteration.
source (SweepSource, optional): A [`SweepSource`][qprogram.SweepSource]. A bare 1-D
sequence is accepted as shorthand for [`Values`][qprogram.Values]. Omit it to
source (SweepSource, optional): A [`SweepSource`][qprogram.SweepSource]. Omit it to
get a `_SweepBuilder` and pick the values with a ``from_*`` method instead.

Returns:
A context manager opening the sweep block, or — when ``source`` is omitted — the
`_SweepBuilder` that produces one.

Raises:
ValidationError: If ``source`` is given but is neither a sweep source nor a 1-D sequence
of values.
ValidationError: If ``source`` is given but is not a sweep source.
"""
if isinstance(source, _Unset):
return _SweepBuilder(self, variable)
Expand Down
13 changes: 7 additions & 6 deletions tests/test_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,11 @@ def test_sweep_repeats():
assert Sweep.REPEATS is True


def test_sweep_accepts_a_bare_sequence_as_values_shorthand():
sw = Sweep(Variable("x"), [0.1, 0.2, 0.3])
assert isinstance(sw.source, Values)
assert sw.num_iterations() == 3
def test_sweep_rejects_a_sequence_of_points():
"""The block binds a source, not the points a source would produce."""
v = Variable("x")
with pytest.raises(ValidationError, match="must be a SweepSource"):
Sweep(v, [0.1, 0.2, 0.3]) # ty:ignore[invalid-argument-type]


def test_sweep_rejects_a_callable_source():
Expand All @@ -204,9 +205,9 @@ def test_sweep_rejects_a_callable_source():
Sweep(v, lambda i: i) # ty:ignore[invalid-argument-type]


def test_sweep_rejects_a_non_source_non_sequence():
def test_sweep_rejects_a_non_source():
v = Variable("x")
with pytest.raises(ValidationError, match="SweepSource or a 1-D sequence"):
with pytest.raises(ValidationError, match="must be a SweepSource"):
Sweep(v, object()) # ty:ignore[invalid-argument-type]


Expand Down
Loading