Skip to content
Open
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
72 changes: 63 additions & 9 deletions dwave/gate/qcdl/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,54 @@ def run_prog(idx: int, arg: Any) -> None:
return shape, table_row


def _as_modules(qubits: Any, argument: str) -> tuple[list[QCDLModule], int | None]:
"""Normalize a group of qubits into modules and a scope id.

Accepts a :class:`.Scope`, a single :class:`.QCDLModule`, or a sequence of
either, so that a caller does not have to know which of those an API wants.

Args:
qubits: The group of qubits.
argument: Name of the parameter being normalized, for error messages.

Raises:
:exception:`~dwave.gate.qcdl.exceptions.QCDLUserError`: If no qubits
could be found in ``qubits``.

Returns:
The modules, deduplicated and in order, and the ``scope_id`` if the
group carried one.
"""
if isinstance(qubits, QCDLModuleContainer):
modules = list(qubits.qcdl_modules)
if not modules:
raise QCDLUserError(f"{argument} {qubits} does not hold any qubits")
return modules, qubits.scope_id

if isinstance(qubits, str) or not isinstance(qubits, Sequence):
raise QCDLUserError(
f"{argument} must be a Scope, a QCDLModule, or a sequence of them,"
f" not {type(qubits).__name__} ({qubits!r})"
)

# deduplicate by name while preserving order
by_name: dict[str, QCDLModule] = {}
for item in qubits:
if not isinstance(item, QCDLModuleContainer):
raise QCDLUserError(
f"every item in {argument} must be a Scope or a QCDLModule, not"
f" {type(item).__name__} ({item!r})"
)
for module in item.qcdl_modules:
by_name[module.qcdl_module_name] = module

if not by_name:
raise QCDLUserError(f"{argument} does not hold any qubits")

# a bare sequence has no identity of its own, so it carries no scope_id
return list(by_name.values()), None


class QCDLModule(QCDLModuleContainer):
"""Wrapper around a :class:`.Procedure` instance.

Expand Down Expand Up @@ -1834,28 +1882,34 @@ def signal(self) -> str:
return f"{self.qcdl_module_name}.signal"

def one_to_all(
self, destinations: Scope, send: RegisterExpression, **kwargs: Any
self,
destinations: Scope | QCDLModule | Sequence[QCDLModule],
send: RegisterExpression,
**kwargs: Any,
) -> None:
"""Send a bit from one qubit to a :class:`~dwave.gate.qcdl.Scope` of
other qubits.
"""Send a bit from one qubit to other qubits.

See the :ref:`qcdl_advanced_signals` section for a description and
examples of signals.

Args:
destinations: The scope of all qubits to send the message to. The
bit is placed on each qubit's branch condition to be used in a
conditional statement.
destinations: The qubits to send the message to. The bit is placed
on each qubit's branch condition to be used in a conditional
statement. Statements are tagged with the
``scope_id`` if a :class:`~dwave.gate.qcdl.Scope` is
specified.
send: The expression to compute the bit on the sender.

Raises:
:exception:`ValueError`: If the module has more than one qubit.
:exception:`~dwave.gate.qcdl.exceptions.QCDLUserError`: If
``destinations`` does not hold any qubits.
"""
modules, scope_id = _as_modules(destinations, "destinations")
self._multi_qubit_statement(
"one_to_all",
send=send,
qubits=destinations.qcdl_modules,
scope_id=destinations.scope_id,
qubits=modules,
scope_id=scope_id,
**kwargs,
)

Expand Down
78 changes: 78 additions & 0 deletions tests/test_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,84 @@ def main(q0, q1, q2):
assert s.args[0] == reg_name


def _one_to_all_statement(destinations_from):
"""Build a one_to_all and return its statement.

``destinations_from`` turns the listener Scope into whatever form of
destination the test wants to pass.
"""

@qcdl(3)
def main(q0, q1, q2):
send_register = q0.Register(name="reg123")
sc = Scope(q1, q2)
q0.one_to_all(destinations_from(sc, q1), send_register == 1)

statements = [
QCDLStatement.model_validate(stmt)
for stmt in main().model_dump(exclude_unset=True)["program"]["statements"]
]
return next(s for s in statements if s.op == "one_to_all")


@pytest.mark.parametrize(
"destinations_from,expected",
[
(lambda sc, q1: sc, ["q1", "q2"]),
(lambda sc, q1: sc.qcdl_modules, ["q1", "q2"]),
(lambda sc, q1: tuple(sc.qcdl_modules), ["q1", "q2"]),
(lambda sc, q1: list(reversed(sc.qcdl_modules)), ["q2", "q1"]),
(lambda sc, q1: q1, ["q1"]),
(lambda sc, q1: [q1, sc], ["q1", "q2"]),
(lambda sc, q1: [q1, q1], ["q1"]),
],
ids=["scope", "list", "tuple", "reversed", "module", "mixed", "repeated"],
)
def test_one_to_all_destination_forms(destinations_from, expected):
"""The guide passes ``scope.qcdl_modules``, which used to raise."""
assert _one_to_all_statement(destinations_from).kwargs["qubits"] == expected


def test_one_to_all_scope_id_comes_from_a_scope_only():
"""A bare sequence has no identity, so it contributes no scope_id."""
from_scope = _one_to_all_statement(lambda sc, q1: sc)
from_list = _one_to_all_statement(lambda sc, q1: sc.qcdl_modules)

assert from_scope.kwargs["scope_id"] is not None
assert from_list.kwargs["scope_id"] is None


@pytest.mark.parametrize("destinations", [5, "q1", None, 3.14])
def test_one_to_all_rejects_a_non_module(destinations):
@qcdl(2)
def main(q0, q1):
send_register = q0.Register(name="reg123")
q0.one_to_all(destinations, send_register == 1)

with pytest.raises(QCDLUserError, match="must be a Scope, a QCDLModule"):
main()


def test_one_to_all_rejects_a_non_module_in_a_sequence():
@qcdl(2)
def main(q0, q1):
send_register = q0.Register(name="reg123")
q0.one_to_all([q1, 7], send_register == 1)

with pytest.raises(QCDLUserError, match="every item in destinations"):
main()


def test_one_to_all_rejects_an_empty_sequence():
@qcdl(2)
def main(q0, q1):
send_register = q0.Register(name="reg123")
q0.one_to_all([], send_register == 1)

with pytest.raises(QCDLUserError, match="does not hold any qubits"):
main()


def test_break_outside_loop():
@qcdl(1)
def main(q0):
Expand Down