Source code for qrisp.alg_primitives.arithmetic.adders.cuccaro_adder

# ********************************************************************************
# * Copyright (c) 2026 the Qrisp authors
# *
# * This program and the accompanying materials are made available under the
# * terms of the Eclipse Public License 2.0 which is available at
# * http://www.eclipse.org/legal/epl-2.0.
# *
# * This Source Code may also be made available under the following Secondary
# * Licenses when the conditions for such availability set forth in the Eclipse
# * Public License, v. 2.0 are satisfied: GNU General Public License, version 2
# * with the GNU Classpath Exception which is
# * available at https://www.gnu.org/software/classpath/license.html.
# *
# * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
# ********************************************************************************

"""Implements the Cuccaro ripple-carry in-place adder for quantum and classical-quantum addition."""

from __future__ import annotations

from numbers import Integral
from typing import TYPE_CHECKING, TypeAlias

import jax.numpy as jnp

from qrisp.alg_primitives.arithmetic.adders.adder_utilities import _is_quantum_register
from qrisp.circuit import Qubit
from qrisp.core import QuantumVariable, cx, mcx, x
from qrisp.environments import conjugate, custom_control
from qrisp.jasp import DynamicQubitArray, check_for_tracing_mode, fuse_qb_array, jlen, jrange
from qrisp.misc import int_encoder
from qrisp.qtypes import QuantumBool, QuantumChar, QuantumString

# ``BigInteger`` only appears in type hints (lazy-evaluated thanks to
# ``from __future__ import annotations``) and is never needed at runtime.
# Importing it at module level can trigger a circular import.
if TYPE_CHECKING:
    from qrisp.alg_primitives.arithmetic.jasp_arithmetic.jasp_bigintiger import (
        BigInteger,
    )

# An optional control/carry qubit may be passed as a QuantumBool or a bare Qubit.
OptionalQubit: TypeAlias = QuantumBool | Qubit

# The quantum operands are QuantumVariables (or subclasses), DynamicQubitArrays
# or plain lists of Qubits (including traced qubits).
QuantumRegister: TypeAlias = QuantumVariable | DynamicQubitArray | list[Qubit]


def _reject_unsupported_quantum_types(a: object, b: object) -> None:
    """Reject quantum types that cannot be interpreted as additive registers.

    ``QuantumChar`` and ``QuantumString`` are quantum types but do not represent
    numeric registers, so silently applying the adder to their qubits would
    produce meaningless results. Both operands are checked and rejected if
    either one has an unsupported type.
    """
    for value in (a, b):
        if isinstance(value, (QuantumChar, QuantumString)):
            raise ValueError(
                f"{type(value).__name__} is not supported by cuccaro_adder. Supported quantum types are "
                "QuantumVariable, QuantumBool, QuantumFloat and QuantumModulus."
            )


def _normalize_optional_input_arg(value: OptionalQubit | None, name: str) -> Qubit | None:
    """Normalize an optional QuantumBool or Qubit argument to a bare Qubit.

    The purpose of this helper is to centralize the coercion and validation of
    the optional ``c_in``, ``c_out`` and ``ctrl`` parameters of
    :func:`cuccaro_adder`, each of which accepts a QuantumBool, a bare Qubit or
    None, while the gate logic works only with bare Qubits. Defining it once
    avoids repeating the same unwrapping and validation logic for every
    parameter.

    A QuantumBool is converted to its underlying Qubit via ``value[0]``, a bare
    Qubit or None is passed through unchanged, and any other type raises a
    TypeError in static mode. The helper neither touches the circuit nor
    allocates qubits, so it can run before any ancilla is created, letting an
    invalid argument fail cleanly instead of leaving a half-applied circuit
    behind.
    """
    if value is None:
        return None

    if isinstance(value, QuantumBool):
        return value[0]

    if not check_for_tracing_mode() and not isinstance(value, Qubit):
        raise TypeError(f"{name} must be of type QuantumBool or Qubit, not {type(value)}")

    return value


def _binary_string_to_int(value: str) -> int:
    """Parse a little-endian binary string into an integer.

    Binary strings are read little-endian, i.e. the leftmost character is the
    least significant bit, so ``"10"`` means bit 0 is 1 (the value 1). An empty
    string represents 0. Only the characters ``0`` and ``1`` are allowed.

    Parameters
    ----------
    value : str
        The little-endian binary string to parse.

    Returns
    -------
    int
        The integer encoded by ``value``.

    Raises
    ------
    ValueError
        If ``value`` contains characters other than ``0`` and ``1``.

    """
    if not set(value) <= {"0", "1"}:
        raise ValueError(f"a must be a binary string of 0s and 1s, got {value!r}")
    return int(value[::-1], 2) if value else 0


def _normalize_list_operand(value):
    """Convert a ``list[Qubit]`` operand to a ``DynamicQubitArray`` while tracing.

    Python lists require concrete indices, so they cannot be sliced or indexed
    with the traced values used by :func:`jrange`. Lists are left unchanged in
    static mode, and other operand types (including existing
    ``DynamicQubitArray`` inputs) are returned as they are.
    """
    if not isinstance(value, list) or not check_for_tracing_mode() or not value:
        return value

    if len(value) == 1:
        # Fuse the qubit with itself to obtain an array, then slice back to a
        # single element. Wrapping a bare qubit directly would not produce a
        # qubit-array value at runtime.
        return DynamicQubitArray(fuse_qb_array(value[0], value[0]))[:1]

    result = value[0]
    for qb in value[1:]:
        result = result + qb
    return result


def _apply_maj_gates(
    a: QuantumRegister,
    b: QuantumRegister,
    ancilla: QuantumVariable,
    dim_a: int,
) -> None:
    """Apply the majority (maj) gate chain of the Cuccaro adder.

    The maj gates compute the carry bits into the ``ancilla`` (reusing the
    ``a`` qubits as scratch space) while leaving the sum bits of ``b``
    untouched. The first few gates set up the carry at the least significant
    bit, and the loop then propagates it upward across the remaining ``dim_a``
    bits.
    """
    cx(a[0], b[0])
    cx(a[0], ancilla[0])
    mcx([ancilla[0], b[0]], a[0])

    for i in jrange(1, dim_a):
        cx(a[i], b[i])
        cx(a[i], a[i - 1])
        mcx([a[i - 1], b[i]], a[i])


def _apply_uma_gates(
    a: QuantumRegister,
    b: QuantumRegister,
    ancilla: QuantumVariable,
    ctrl: Qubit | None,
    dim_a: int,
) -> None:
    """Apply the unmajority (uma) gate chain of the Cuccaro adder.

    The uma gates run in reverse over the qubits and use the carries stored in
    the previous phase to write the result of the addition back into ``b``.
    There are two variants: the uncontrolled version toggles qubits with ``x``
    to save an ancilla, while the controlled version (``ctrl`` given) replaces
    those X gates with Toffolis controlled on ``ctrl`` so the addition only
    happens when the control is |1>.
    """
    if ctrl is None:
        for j in jrange(dim_a - 1):
            i = dim_a - j - 1

            x(b[i])
            cx(a[i - 1], b[i])
            mcx([a[i - 1], b[i]], a[i])
            x(b[i])
            cx(a[i], a[i - 1])
            cx(a[i], b[i])

        x(b[0])
        cx(ancilla[0], b[0])
        mcx([ancilla[0], b[0]], a[0])
        x(b[0])
        cx(a[0], ancilla[0])
        cx(a[0], b[0])

    else:
        for j in jrange(dim_a - 1):
            i = dim_a - j - 1

            mcx([a[i - 1], b[i]], a[i])
            mcx([ctrl, a[i - 1]], b[i])
            cx(a[i], a[i - 1])
            cx(a[i], b[i])

        mcx([ancilla[0], b[0]], a[0])
        mcx([ctrl, ancilla[0]], b[0])
        cx(a[0], ancilla[0])
        cx(a[0], b[0])


def _apply_c_out(c_out: Qubit | None, a: QuantumRegister, ctrl: Qubit | None) -> None:
    """Copy the final carry into the carry-out qubit.

    After the maj phase the most significant carry still sits in the top ``a``
    qubit. If a carry-out qubit was requested, this helper copies it over
    before the uma phase uncomputes the carries. For a controlled addition,
    the copy is also controlled so the carry-out remains unchanged when the
    control is disabled.
    """
    if c_out is not None:
        if ctrl is None:
            cx(a[-1], c_out)
        else:
            mcx([ctrl, a[-1]], c_out)


def _apply_c_in_cx(c_in: Qubit | None, ancilla: QuantumVariable) -> None:
    """Toggle the carry ancilla with the carry-in value.

    ``_normalize_optional_input_arg`` resolves the carry-in without touching the circuit.
    This helper applies the CNOT that seeds the carry ancilla with the carry-in
    value before the maj phase, and is applied a second time after the uma phase
    to clear the ancilla again so it can be safely deleted.
    """
    if c_in is not None:
        cx(c_in, ancilla[0])


[docs] @custom_control def cuccaro_adder( a: int | str | BigInteger | QuantumRegister, b: QuantumRegister, c_in: OptionalQubit | None = None, c_out: OptionalQubit | None = None, ctrl: OptionalQubit | None = None, ) -> None: """In-place adder as introduced in https://arxiv.org/abs/quant-ph/0410184 This function works in both static and dynamic modes. The two inputs may both be quantum, or one may be quantum and the other classical. All supported :ref:`quantum types <QuantumTypes>` (e.g. QuantumFloat, QuantumBool, QuantumModulus, ...) can be used as quantum inputs, along with lists of Qubits and :class:`~qrisp.jasp.DynamicQubitArray` objects. - A ``DynamicQubitArray`` is simply an array of qubits that is created automatically in Jasp mode; you never need to construct one yourself. - A quantum input can also be a list of bare Qubits. Bare Qubits are obtained by indexing a quantum variable, e.g. ``qv[0]`` yields a single Qubit and ``qv[0:2]`` yields a list of Qubits; a bare Qubit may likewise be passed directly as the carry or control argument. The table below lists the accepted input forms and the behaviour in the special cases. .. list-table:: :widths: 30 70 :header-rows: 1 * - Input - Behaviour * - Quantum first input ``a`` - Any quantum type, ``list[Qubit]`` or ``DynamicQubitArray``. It is adjusted to the size of ``b`` before the addition. * - Quantum second input ``b`` - Must be a quantum register (``QuantumVariable``, ``list[Qubit]`` or ``DynamicQubitArray``); it holds the result in place and its size sets the modulus of the addition. * - Classical first input - An ``int``, a little-endian binary ``str``, or a ``BigInteger`` (dynamic mode only). It is encoded into a temporary quantum variable of the same size as ``b`` and uncomputed afterwards, so no ancilla is left over. The second input cannot be classical, since the addition is performed in place on it. * - Classical addend larger than ``b`` - Truncated modulo ``2**len(b)`` before it is encoded. * - Quantum ``a`` larger than ``b`` - ``a`` is truncated by slicing off its high-order qubits, so the addition is performed modulo ``2**len(b)``. No qubits are added or removed and ``a`` keeps its value and size. * - Quantum ``a`` smaller than ``b`` - ``a`` is padded with ancilla qubits in the ``|0>`` state so it can be processed at the size of ``b``. The ancillas are deleted once the addition is done, and ``a`` itself is not modified. * - ``QuantumBool`` - A single-qubit register, so the addition wraps around modulo 2. * - ``QuantumModulus`` - Works as long as the sum stays below the modulus. * - ``c_in`` and ``c_out`` - Optional carry-in and carry-out; each accepts a ``QuantumBool`` or a bare ``Qubit``. ``c_in`` adds an extra 1 to the sum, and ``c_out`` records the overflow. * - ``ctrl`` - Optional control qubit; accepts a ``QuantumBool`` or a bare ``Qubit``. The addition is only applied when the control is in the ``|1>`` state. Equivalently, the call can be wrapped in a :func:`~qrisp.control` environment. * - Dynamic mode - Inside a :func:`~qrisp.jasp.jaspify` function the quantum inputs may be ``DynamicQubitArray`` objects such as ``a.reg`` or a full slice ``a[:]``. The result is read out with ``measure``. The custom control implementation is based on Theorem 2.12 of https://arxiv.org/abs/2407.20167 .. note:: If the first input is quantum and the second classical, the function cannot work as addition is performed "in-place" on the second input. Parameters ---------- a : int, str, BigInteger, QuantumVariable, list[Qubit] or DynamicQubitArray The value that should be added. A ``str`` is interpreted as a little-endian binary string (``"10"`` means bit 0 is 1, i.e. the value 1). A ``BigInteger`` is only supported in dynamic (tracing) mode. b : QuantumVariable or list[Qubit] or DynamicQubitArray The value that should be modified in the in-place addition. c_in : QuantumBool or Qubit, optional An optional carry in value. The default is None. c_out : QuantumBool or Qubit, optional An optional carry out value. The default is None. ctrl : QuantumBool or Qubit, optional An optional control qubit. If provided, the addition is only applied when the control qubit is in the ``|1>`` state. The default is None. Raises ------ AttributeError If ``ctrl`` is not a QuantumBool or Qubit. The ``custom_control`` decorator inspects the control qubit before the function body runs. TypeError If carry in or carry out is not of type QuantumBool or Qubit in static mode. TypeError If the first argument is classical but not an integer, binary string or BigInteger in static mode. ValueError If the first argument is a ``str`` that is not a valid binary string. ValueError If the second argument is not a quantum register, i.e. if ``b`` is not a QuantumVariable, DynamicQubitArray or a non-empty ``list[Qubit]``. ValueError If the first argument is a ``list`` that does not contain only Qubits. ValueError If ``a`` or ``b`` is a ``QuantumChar`` or ``QuantumString``, which are not supported numeric register types. Returns ------- None The function modifies the second input in place. Examples -------- The examples below use ``QuantumFloat`` inputs; any other supported quantum type works the same way. In static mode, ``print`` already simulates and shows the outcome: >>> from qrisp import QuantumFloat, cuccaro_adder >>> a = QuantumFloat(4) >>> b = QuantumFloat(4) >>> a[:] = 4 >>> b[:] = 5 >>> cuccaro_adder(a, b) >>> print(b) {9: 1.0} In dynamic mode, the call is wrapped in a :func:`~qrisp.jasp.jaspify` function and the result is collapsed with ``measure`` before it is returned: >>> from qrisp import QuantumFloat, cuccaro_adder, measure >>> from qrisp.jasp import jaspify >>> @jaspify ... def main(): ... a = QuantumFloat(4) ... b = QuantumFloat(4) ... a[:] = 4 ... b[:] = 5 ... cuccaro_adder(a, b) ... return measure(b) >>> main() Array(9., dtype=float64) """ # QuantumChar and QuantumString are not numeric registers and therefore not # valid operands, even though QuantumChar is a QuantumVariable subclass. _reject_unsupported_quantum_types(a, b) # The second argument is required to be a (non-empty) quantum register if not _is_quantum_register(b) or (isinstance(b, list) and len(b) == 0): raise ValueError( "The second argument must be of type QuantumVariable, DynamicQubitArray or a non-empty list[Qubit]." ) # A list that does not contain only Qubits is neither a valid quantum register # nor a valid classical input. if isinstance(a, list) and not _is_quantum_register(a): raise ValueError("If the first argument is a list, it must contain only Qubits.") # Convert plain lists to DynamicQubitArray so traced slicing and indexing # work in the size adjustment and in the maj/uma loops. a = _normalize_list_operand(a) b = _normalize_list_operand(b) # Normalize and validate the optional arguments before allocating any # ancillas or applying gates, so that an invalid argument raises cleanly # without leaving qubits or a half-applied circuit behind. c_in = _normalize_optional_input_arg(c_in, "c_in") c_out = _normalize_optional_input_arg(c_out, "c_out") ctrl = _normalize_optional_input_arg(ctrl, "ctrl") # convert the classical input to a quantum input if not _is_quantum_register(a): if isinstance(a, str): # This runs in both static and tracing mode, so malformed binary # strings are rejected during tracing before any circuit is built. a = _binary_string_to_int(a) # int_encoder reads only the target's bits while tracing. In static mode, # validate the integer and truncate here because int_encoder rejects # values wider than the register. if not check_for_tracing_mode(): if not isinstance(a, Integral): raise TypeError( "a must be of type int, str, BigInteger, QuantumVariable, " f"DynamicQubitArray or list[Qubit], not {type(a)}" ) a = a % (1 << jlen(b)) # create a quantum variable of the same size as the other quantum input q_a = QuantumVariable(jlen(b)) with conjugate(int_encoder)(q_a, a): cuccaro_adder(q_a, b, c_in=c_in, c_out=c_out, ctrl=ctrl) # outside the conjugation, q_a is back in the state |0> and the addition has been performed on b # delete the temporary quantum variable created for the classical input q_a.delete() return # when the quantum inputs are of unequal length # pad the size of the input with the smaller size dim_a = jlen(a) dim_b = jlen(b) # reduce the size of a to the size of b if a is larger than b effective_size_a = jnp.minimum(dim_a, dim_b) a = a[:effective_size_a] # create an extension ancilla to change the size of a when it is smaller than b extension_size = jnp.maximum(0, dim_b - dim_a) extension_anc_a = QuantumVariable(extension_size) extended_a = a[:] + extension_anc_a[:] a = extended_a # redefine the dimensions of a and b after the size adjustments dim_a = jlen(a) # a single carry ancilla is sufficient for the Cuccaro adder ancilla = QuantumVariable(1) # seed the carry ancilla with the carry-in value _apply_c_in_cx(c_in, ancilla) # first maj gate application + iterator maj gate application _apply_maj_gates(a, b, ancilla, dim_a) # copy carry-out _apply_c_out(c_out, a, ctrl) # iterator + last uma gate application _apply_uma_gates(a, b, ancilla, ctrl, dim_a) # clear the carry-in seeding so the ancilla can be deleted _apply_c_in_cx(c_in, ancilla) # delete the ancilla used for carry bits ancilla.delete() # delete the extension ancillas when the inputs are of unequal length extension_anc_a.delete()