diff --git a/changelog/41.changed.md b/changelog/41.changed.md new file mode 100644 index 0000000..e1fc1b7 --- /dev/null +++ b/changelog/41.changed.md @@ -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`. diff --git a/docs/guide/control-flow.md b/docs/guide/control-flow.md index 85319ec..8341f01 100644 --- a/docs/guide/control-flow.md +++ b/docs/guide/control-flow.md @@ -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, diff --git a/docs/reference/errors.md b/docs/reference/errors.md index 5330647..af01339 100644 --- a/docs/reference/errors.md +++ b/docs/reference/errors.md @@ -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)` | diff --git a/src/qprogram/blocks/sweep.py b/src/qprogram/blocks/sweep.py index 4d5c940..390c785 100644 --- a/src/qprogram/blocks/sweep.py +++ b/src/qprogram/blocks/sweep.py @@ -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 @@ -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. @@ -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): @@ -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) diff --git a/src/qprogram/qprogram.py b/src/qprogram/qprogram.py index ff0687c..f97ae85 100644 --- a/src/qprogram/qprogram.py +++ b/src/qprogram/qprogram.py @@ -1311,8 +1311,7 @@ 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: @@ -1320,8 +1319,7 @@ def sweep(self, variable: Variable, source: SweepSource | _Unset = _UNSET) -> _S `_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) diff --git a/tests/test_blocks.py b/tests/test_blocks.py index 1f2a813..622c697 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -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(): @@ -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]