Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion docs/qcdl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ These classes are of interest mostly to developers of QCDL.

.. automodule:: dwave.gate.qcdl.components
:show-inheritance:
:members: Procedure, QCDLModuleName
:members: Procedure, QCDLModuleName, RegisterAllocation

.. automodule:: dwave.gate.qcdl.qcdl_models
:show-inheritance:
Expand Down
5 changes: 3 additions & 2 deletions docs/workflow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -986,8 +986,9 @@ condition value used in the ``If`` statement here and in subsequent examples.
# all qubits have a copy of the same register:
send_register = sc.Register(name=name)

# set the register on q0 to 0 or 1
measure(q0, register=q0.Register(name=name))
# set the register on q0 to 0 or 1; alias=True reuses the memory
# send_register already allocated instead of redeclaring it
measure(q0, register=q0.Register(name=name, alias=True))

# if any of the copies of the register are equal to 1, then all
# will receive a condition of True.
Expand Down
119 changes: 117 additions & 2 deletions dwave/gate/qcdl/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import types
from collections.abc import Mapping, Sequence, Set
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Callable, Iterator
from typing import TYPE_CHECKING, Any, Callable, Iterator, NamedTuple

import numpy as np

Expand Down Expand Up @@ -76,6 +76,21 @@ def default(self, obj: Any) -> Any:
return str(obj)


class RegisterAllocation(NamedTuple):
"""One entry in the register names a circuit has allocated.
Comment thread
qci-amos marked this conversation as resolved.
Outdated

Args:
dtype: ``"int"`` or ``"float"``.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dtype: ``"int"`` or ``"float"``.
dtype: ``"int"`` for a :class:`~dwave.gate.qcdl.registers.Register` or
``"float"`` for a
:class:`~dwave.gate.qcdl.registers.FixedPointRegister`.

This is my guess, the intention is to let the user know what each of the dtypes are meant for.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also Array... I think it's ok to leave it non-specific as "register"?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

procedure: Procedure that made the allocation. The name it was made
under is reported when a later declaration clashes, and the
identity tells a re-run of that same procedure apart from a
genuine re-declaration.
Comment thread
qci-amos marked this conversation as resolved.
Outdated
"""

dtype: str
procedure: Procedure


class Procedure(IndexerMixin):
"""A QCDL procedure.

Expand Down Expand Up @@ -241,6 +256,104 @@ def register_module_used(self, module_name: str | None) -> None:
if module not in self.modules_used:
self.modules_used.append(module)

def register_memory_allocation(
self,
modules: Sequence[QCDLModule],
name: str,
dtype: str,
allow_existing: bool = False,
initial_value_specified: bool = False,
) -> None:
"""Record a register allocation, rejecting a silent re-declaration.

Comment thread
qci-amos marked this conversation as resolved.
The compiler keeps the *first* allocation of a name, so a second
declaration of the same name on the same module is a no-op: its initial
value never reaches the qubit. That is almost always a mistake, so it is
Comment thread
qci-amos marked this conversation as resolved.
Outdated
reported here instead.

Register names are global to the circuit rather than local to a
procedure, so the record lives on the
:attr:`~dwave.gate.qcdl.qcdl_circuit.QCDLCircuit.allocated_registers`
attribute of the state, and a name taken in one procedure clashes with
the same name in another.

A procedure body is re-executed on every call while the program is
being built, but is emitted once, so a declaration reached through a
later run of the *same* procedure is not a re-declaration and is not
reported.

Re-declaring the name is allowed when the caller asked for it, but only
without an initial value: opting in to the re-declaration says the
existing memory is wanted, whereas giving a value says the opposite,
and the compiler would ignore it. This applies only once the name is
allocated; a first allocation always takes its value, whatever the
caller opted in to.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it hard to follow the above three paragraphs. Would you try a rewrite from the perspective of a user and I could then take a pass?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


This method is mostly intended for use by developers of QCDL; the
:class:`~dwave.gate.qcdl.registers.Register` and
:class:`~dwave.gate.qcdl.registers.FixedPointRegister` classes call it
for you.

Comment thread
qci-amos marked this conversation as resolved.
Args:
modules: Modules the register is allocated on.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
modules: Modules the register is allocated on.
modules: Modules, typically qubits, the register is allocated on.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to keep it abstract as "modules" here because it could be couplers and we want to hide these details in the abstraction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have been using the phrase with the "typically" for exactly that purpose for this first release. The idea being to help a new user understand what a module might be (we never replace "module" with just "qubit" in such places).
Up to you

name: Name of the register.
dtype: ``"int"`` or ``"float"``.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dtype: ``"int"`` or ``"float"``.
dtype: ``"int"`` for a :class:`~dwave.gate.qcdl.registers.Register` or
``"float"`` for a
:class:`~dwave.gate.qcdl.registers.FixedPointRegister`.

This is my guess, the intention is to let the user know what each of the dtypes are meant for.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also used for Array (and other contexts like arbitrary function also have a dtype). Are you comfortable just leaving it abstract with "register" here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

allow_existing: If True, an existing allocation of ``name`` is
accepted as long as no initial value was given. Set by the
``alias`` and ``ignore_reallocation`` arguments of a register.
It has no effect when ``name`` is not already allocated.
Comment on lines +299 to +302

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
allow_existing: If True, an existing allocation of ``name`` is
accepted as long as no initial value was given. Set by the
``alias`` and ``ignore_reallocation`` arguments of a register.
It has no effect when ``name`` is not already allocated.
allow_existing: If True, allow a register declaration that reuses
``name`` if no initial value is specified. You set this by specifying
the ``alias`` and ``ignore_reallocation`` arguments in a
:class:`~dwave.gate.qcdl.registers.Register` or
:class:`~dwave.gate.qcdl.registers.FixedPointRegister`
instantiation. Ignored when ``name`` is not already allocated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initial_value_specified: Whether the caller gave an initial value
for this register.

Raises:
:exception:`~dwave.gate.qcdl.exceptions.QCDLUserError`: If ``name``
is already allocated on one of ``modules`` and either
``allow_existing`` is False or an initial value was given.
"""
for module in modules:
allocated = self.state.allocated_registers.setdefault(
module.qcdl_module_name, {}
)
previous = allocated.get(name)
if previous is not None and self._is_rerun_of(previous.procedure):
previous = None
if previous is not None and not (
allow_existing and not initial_value_specified
):
if allow_existing:
raise QCDLUserError(
f"register {name!r} is already allocated on"
f" {module.qcdl_module_name} with dtype"
f" {previous.dtype} in procedure"
f" {previous.procedure.name}, and the compiler keeps"

ghost Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next few lines seem too much implementation for an error message, can we cut this short and skip to "do not specify an initial value"?

ghost Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f" the first allocation, so the initial value given"
f" here would never reach the qubit. Re-declaring the"
f" name is allowed, but giving it a value is not: drop"
f" the initial value."
)
raise QCDLUserError(
f"register {name!r} is already allocated on"
f" {module.qcdl_module_name} with dtype {previous.dtype} in"
f" procedure {previous.procedure.name}; register names are"
f" global to the circuit and the compiler keeps the first"
f" allocation, so this one would be discarded. Reuse the"
f" existing register, pick another name, or redeclare it"
f" deliberately with alias=True or ignore_reallocation=True"
f" and no initial value."
)
allocated[name] = RegisterAllocation(dtype, self)

def _is_rerun_of(self, other: Procedure) -> bool:
"""Whether ``other`` is an earlier run of the procedure ``self`` is.

ghost Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Whether ``other`` is an earlier run of the procedure ``self`` is.
"""Whether ``other`` is an earlier run of this procedure.


Calling a procedure runs its body again, so a register it declares is
seen once per call even though the procedure is emitted once. Those
Comment thread
qci-amos marked this conversation as resolved.
Outdated
runs are separate :class:`.Procedure` instances sharing a name, and the
name is what the rest of the circuit deduplicates on, so matching on it
here agrees with what ends up in the program.

ghost Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    name is what the rest of the circuit **deduplicates** on, so matching on it
    here agrees with what ends up in the program.

This sentence is unclear to me

ghost Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""
return other is not self and other.proc_name == self.proc_name

@property
def expression_queue(self) -> list | None:
"""Create an expression queue.
Expand Down Expand Up @@ -1230,7 +1343,9 @@ def all_to_all_use(q0, q1):
sc = Scope(q0, q1)
r1 = sc.Register(name="r1")
h(q0)
measure(q0, register=q0.Register(name="r1"))
# alias=True reuses the memory r1 already allocated, so the
# outcome is stored on q0 only rather than mirrored

ghost Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to suggest that setting alias=False would mirror the outcome to other qubit registers.

ghost Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ghost Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

measure(q0, register=q0.Register(name="r1", alias=True))
sc.all_to_all(send=r1==1, reduce_op="&")
with sc.If(None):
x(q1)
Expand Down
20 changes: 19 additions & 1 deletion dwave/gate/qcdl/qcdl_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import numpy as np

from .base import IndexerMixin
from .components import Procedure, QCDLModule
from .components import Procedure, QCDLModule, RegisterAllocation
from .exceptions import QCDLInternalError, QCDLUserError
from .qcdl_models import QCDLProgram, QCDLModuleName, QCDLProcedureDef
from .transformer import print_qcdl
Expand Down Expand Up @@ -120,6 +120,11 @@ def __init__(
self._main: Procedure | None = None
self._program: QCDLProcedureDef | None = None
self._procedures: dict[str, QCDLProcedureDef] = {}

# register names are global to a circuit rather than local to a
# procedure, so they are tracked here rather than on Procedure
self._allocated_registers: dict[str, dict[str, RegisterAllocation]] = {}

self._validate_non_deterministic_qubits_mid = (
validate_non_deterministic_qubits_mid
)
Expand Down Expand Up @@ -485,6 +490,19 @@ def set_or_check_nondeterministic_modules(
def procedures(self) -> dict[str, QCDLProcedureDef]:
return self._procedures

@property
def allocated_registers(self) -> dict[str, dict[str, RegisterAllocation]]:
"""Register names allocated so far, by module name then register name.

Register names are global to a circuit: a name allocated in one
procedure is the same memory as that name in another, and only the
first allocation of it takes effect. The
:meth:`~dwave.gate.qcdl.components.Procedure.register_memory_allocation`
method maintains this, and reports a second allocation rather than
letting it be discarded.

ghost Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
letting it be discarded.
discarding it.

Can we say something about how that report is used? It seems to me from the code below, that what is actually meant by "reports a second allocation" is in fact "raises an error", yes?

ghost Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""
return self._allocated_registers

def get_procedure(self, procedure_name: str) -> QCDLProcedureDef | None:
return self.procedures.get(procedure_name)

Expand Down
61 changes: 51 additions & 10 deletions dwave/gate/qcdl/registers.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ def _register_initialization(
if isinstance(initial_value, np.ndarray):
initial_value = initial_value.tolist()

# None means the caller gave no initial value. That distinction matters
# because a value that would never reach the qubit has to be reported
# rather than silently dropped.
initial_value_specified = initial_value is not None
if not initial_value_specified:
initial_value = 0.0 if str(dtype) == "float" else 0

if length is None:
length = len(initial_value) if isinstance(initial_value, Sequence) else 1

Expand All @@ -257,6 +264,24 @@ def _register_initialization(
name, initial_value=initial_value, length=length, dtype=dtype, signed=signed
)

if alias is True and initial_value_specified:
raise QCDLUserError(
f"register {name!r} is an alias, so no memory is allocated for"
f" it and the initial value given here would never reach the"
f" qubit; drop the initial value, or drop alias=True to"
f" allocate new memory"

ghost Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more typical error message just says something like "f"register {name!r} is an alias, so you cannot specify an initial value"

)

# An alias deliberately names memory that already exists, and
# ignore_reallocation is the documented opt out.
modules[0].procedure.register_memory_allocation(
modules,
name,
dtype,
allow_existing=alias is True or ignore_reallocation,
initial_value_specified=initial_value_specified,
)

if alias is not True:
# use alias=True if some other code called allocate_memory for this
# register
Expand Down Expand Up @@ -655,16 +680,24 @@ class Register(IntegerOpsMixin, AssignmentOpsMixin, RegisterInitializerMixin, Ta
one or more qubits. Typically, you create a register from a
:class:`~dwave.gate.qcdl.Scope` object, which handles this parameter
for you.
initial_value: Initial value. Defaults to 0.
initial_value: Initial value. Defaults to 0. Only the first allocation
of a name takes effect, so a value given for a name that is already
Comment thread
qci-amos marked this conversation as resolved.
Outdated
allocated is rejected rather than silently discarded; see
``ignore_reallocation``. An ``alias`` never allocates, so it never
takes a value at all.
name: Name for this register; useful for troubleshooting. If None, a
name is generated. See the ``alias`` parameter for type punning.
master_kwargs: Propagate this to the master instruction. This parameter
is intended for use by developers of QCDL.
alias: Set to True if you are aliasing an existing register, and for
type punning reuse that register's name in the ``name`` parameter.
Aliased registers are not reinitialized.
ignore_reallocation: If True, the compiler does not reallocate if
already allocated (and does not raise an exception).
Aliased registers are not reinitialized, so you may not give an
``initial_value``.
ignore_reallocation: If True, and the name is already allocated, the
Comment thread
qci-amos marked this conversation as resolved.
Outdated
compiler does not reallocate it (and does not raise an exception).
In that case you may not give an ``initial_value``, since it would
never reach the qubit. A name that is not yet allocated is
allocated as usual and may carry a value.
Comment thread
qci-amos marked this conversation as resolved.
Outdated
scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register
is derived from. The
:meth:`~dwave.gate.qcdl.QCDLModuleContainer.Register` method sets
Expand Down Expand Up @@ -727,7 +760,7 @@ def direct(q0, q1):
def __init__(
self,
modules: Sequence[QCDLModule],
initial_value: int = 0,
initial_value: int | None = None,
name: str | None = None,
master_kwargs: dict[str, Any] | None = None,
alias: bool | str = False,
Expand Down Expand Up @@ -772,16 +805,24 @@ class FixedPointRegister(
one or more qubits. Typically, you create a register from a
:class:`~dwave.gate.qcdl.Scope` object, which handles this parameter
for you.
initial_value: Initial value. Defaults to 0.0.
initial_value: Initial value. Defaults to 0.0. Only the first
allocation of a name takes effect, so a value given for a name that
Comment thread
qci-amos marked this conversation as resolved.
Outdated
is already allocated is rejected rather than silently discarded;
see ``ignore_reallocation``. An ``alias`` never allocates, so it
never takes a value at all.
Comment thread
qci-amos marked this conversation as resolved.
Outdated
name: Name for this register; useful for troubleshooting. If None, a
name is generated. See the ``alias`` parameter for type punning.
master_kwargs: Propagate this to the master instruction. This parameter
is intended for use by developers of QCDL.
alias: Set to True if you are aliasing an existing register, and for
type punning reuse that register's name in the ``name`` parameter.
Aliased registers are not reinitialized.
ignore_reallocation: If True, the compiler does not reallocate if
already allocated (and does not raise an exception).
Aliased registers are not reinitialized, so you may not give an
``initial_value``.
ignore_reallocation: If True, and the name is already allocated, the
compiler does not reallocate it (and does not raise an exception).
In that case you may not give an ``initial_value``, since it would
never reach the qubit. A name that is not yet allocated is
allocated as usual and may carry a value.
Comment thread
qci-amos marked this conversation as resolved.
Outdated
scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register
is derived from. The
:meth:`~dwave.gate.qcdl.QCDLModuleContainer.FixedPointRegister`
Expand Down Expand Up @@ -825,7 +866,7 @@ def create_fixed_reg(q0, q1):
def __init__(
self,
modules: Sequence[QCDLModule],
initial_value: float = 0.0,
initial_value: float | None = None,
name: str | None = None,
master_kwargs: dict[str, Any] | None = None,
alias: bool | str = False,
Expand Down
Loading