# ********************************************************************************
# * 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
# ********************************************************************************
"""Prepares Dicke states on a QuantumVariable via deterministic or divide-and-conquer circuits."""
from collections.abc import Sequence
from typing import Literal, TypeAlias, assert_never, get_args
import jax.numpy as jnp
from jax import Array, lax
from jax.scipy.special import gammaln
from qrisp.circuit import Qubit
from qrisp.core import QuantumVariable, cx, ry, x
from qrisp.environments import control
from qrisp.jasp import jlen, jrange, q_cond
#: The state preparation methods that ``dicke_state`` knows about.
DickeStateMethod: TypeAlias = Literal["deterministic", "divide-and-conquer"]
# The same methods as a runtime tuple, derived from ``DickeStateMethod`` so that the two cannot drift apart.
_METHODS: tuple[DickeStateMethod, ...] = get_args(DickeStateMethod)
[docs]
def dicke_state(
qv: QuantumVariable | Sequence[Qubit],
k: int | Array,
*,
method: DickeStateMethod = "deterministic",
) -> None:
r"""Prepare a Dicke state :math:`|D^n_k\rangle` on a QuantumVariable.
A Dicke state is the equal superposition of all basis states of Hamming weight :math:`k` on :math:`n` qubits, where
:math:`n` is the number of qubits of ``qv``.
``qv`` has to be initialized to the basis state :math:`|0\rangle^{\otimes n-l}|1\rangle^{\otimes l}` beforehand, as
in the example below. ``"divide-and-conquer"`` requires :math:`l = k`; ``"deterministic"`` accepts any
:math:`l \leq k` (see ``method``).
Parameters
----------
qv : QuantumVariable or Sequence[Qubit]
The qubits to prepare, initialized as described above.
k : int
The Hamming weight (i.e. the number of "ones") of the desired Dicke state.
method : {"deterministic", "divide-and-conquer"}, optional
Either ``"deterministic"`` (`arXiv:1904.07358 <https://arxiv.org/abs/1904.07358>`_, the default) or
``"divide-and-conquer"`` (`arXiv:2112.12435 <https://arxiv.org/abs/2112.12435>`_). The latter prepares the two
halves of ``qv`` on disjoint qubits and therefore has roughly half the circuit depth. The former is the full
Dicke state unitary :math:`U_{n,k}`: it also maps an input of Hamming weight :math:`l \leq k` to
:math:`|D^n_l\rangle`, and superpositions of such inputs to the corresponding superposition of Dicke states.
Raises
------
ValueError
If ``method`` is unknown, or if ``k`` and :math:`n` are plain Python integers (i.e. outside of tracing) and
violate :math:`0 \leq k \leq n`.
Examples
--------
We initialize a QuantumVariable in the "0011" state and from this create the Dicke state with
Hamming weight 2.
::
from qrisp import QuantumVariable, x, dicke_state
qv = QuantumVariable(4)
x(qv[2])
x(qv[3])
dicke_state(qv, 2)
print(qv)
Under Jasp, the Hamming weight may be a traced value. Here we prepare the same state with the
shallower divide-and-conquer circuit.
::
from qrisp import QuantumVariable, x, dicke_state
from qrisp.jasp import jrange, terminal_sampling
@terminal_sampling
def main(k):
qv = QuantumVariable(4)
for i in jrange(4 - k, 4):
x(qv[i])
dicke_state(qv, k, method="divide-and-conquer")
return qv
print(main(2))
"""
if method not in _METHODS:
raise ValueError(f"Unknown `method`: {method!r}. Possible methods are: {', '.join(map(repr, _METHODS))}.")
n = jlen(qv)
if isinstance(k, int) and isinstance(n, int) and not 0 <= k <= n:
raise ValueError(f"`k` must satisfy 0 <= k <= n, got k={k} for n={n}.")
if method == "deterministic":
_apply_dicke_unitary(qv, n, k)
elif method == "divide-and-conquer":
# The divide-and-conquer method cannot handle k > n/2.
# Instead we prepare D(n, n-k) and at the end apply the X gate to all qubits, which changes it to D(n, k).
large_k = k > n // 2
# Reduce the input |0^(n-k) 1^k> to |0^k 1^(n-k)>. Empty range unless k > n/2.
for i in jrange(n - k, jnp.maximum(k, n - k)):
x(qv[i])
k = jnp.minimum(k, n - k) # Equivalent to: `k = n - k if large_k else k`.
n1 = n // 2 # floor(n/2)
n2 = (n + 1) // 2 # ceil(n/2)
_divide(qv, n1, n2, k) # k <= n1 now, which `_divide` needs.
# Disjoint qubits, so the compiler runs these in parallel — this is the depth advantage.
_apply_dicke_unitary(qv[:n1], n1, k)
_apply_dicke_unitary(qv[n1:], n2, k)
q_cond(large_k, x, lambda qv: qv, qv) # Equivalent to: `if large_k: x(qv)`
else:
assert_never(method)
def _log_binom(n: int | Array, k: int | Array) -> Array:
r"""Compute :math:`\log \binom{n}{k}` in a Jasp/Jax-traceable way.
Staying in log space keeps the intermediate values finite: :math:`\binom{n}{k}` itself overflows a 64 bit float
at around :math:`n = 1030`, while its logarithm does not.
Parameters
----------
n : int
The size of the set to choose from. May be a traced value.
k : int
The number of elements to choose. May be a traced value.
Returns
-------
Array
A float64 scalar holding :math:`\log \binom{n}{k}`, or ``-jnp.inf`` outside of ``0 <= k <= n``, i.e. wherever
the binomial coefficient vanishes.
"""
n_f = jnp.asarray(n, dtype=jnp.float64)
k_f = jnp.asarray(k, dtype=jnp.float64)
return gammaln(n_f + 1.0) - gammaln(k_f + 1.0) - gammaln(n_f - k_f + 1.0)
def _divide(qv: QuantumVariable | Sequence[Qubit], n1: int | Array, n2: int | Array, k: int | Array) -> None:
r"""Execute the "divide" step of the divide-and-conquer method of https://arxiv.org/abs/2112.12435.
This takes a computational basis state consisting of ``n1 + n2 - k`` zeros followed by
``k`` ones and changes it into a superposition
.. math::
\frac{1}{\sqrt{\binom{n_1+n_2}{k}}}
\sum_{k_1 = 0}^k
\sqrt{\binom{n_1}{k_1} \binom{n_2}{k-k_1}}
|0\rangle^{\otimes n_1-k_1}
|1\rangle^{\otimes k_1}
|0\rangle^{\otimes n_2-k+k_1}
|1\rangle^{\otimes k-k_1}
that splits the target Hamming weight ``k`` over the two halves of the register with the correct
binomial weights. Each half is then completed independently by ``_apply_dicke_unitary``, which is
the "conquer" step. Note that each half is left in exactly the form that ``_apply_dicke_unitary``
expects: zeros followed by ones.
The notation follows the paper: :math:`n_1, n_2` are the sizes of the two halves and :math:`x_i`,
:math:`s_i` are the quantities on page 8, defined in the nested helpers below.
Parameters
----------
qv : QuantumVariable or Sequence[Qubit]
The quantum variable to be divided. Has to be in the state :math:`|0\rangle^{\otimes n-k}|1\rangle^{\otimes k}`.
n1 : int
The size of the first half of the quantum variable.
n2 : int
The size of the second half of the quantum variable.
k : int
The Hamming weight (i.e. number of "ones") of the Dicke state to be constructed.
"""
def log_x(i: int | Array) -> Array:
r"""Compute :math:`\log x_i`, following :math:`x_i` from page 8 of https://arxiv.org/abs/2112.12435.
That is, :math:`x_i = \binom{n_1}{i} \binom{n_2}{k-i}`, equal to ``-jnp.inf`` in log space whenever one of
the two binomial coefficients vanishes.
"""
return _log_binom(n1, i) + _log_binom(n2, k - i)
def ratio_x_s(i: int | Array) -> Array:
r"""Compute :math:`x_i / s_i`, where :math:`s_i = \sum_{j \geq i} x_j`, without forming either quantity.
Both :math:`x_i` and :math:`s_i` may easily overflow. Rather than computing the two and dividing, we rearrange
into a form in which only the ratio ever appears:
.. math::
\frac{x_i}{s_i} = \frac{x_i}{\sum_{j \geq i} x_j}
= \left( \sum_{j \geq i} \frac{x_j}{x_i} \right)^{-1}
= \left( \sum_{j \geq i} e^{\log x_j - \log x_i} \right)^{-1}
The sum is recomputed from scratch for every ``i`` rather than carried between iterations, so that each
iteration of the quantum loop depends only on ``i`` and the loop stays invertible. This costs
:math:`\mathcal{O}(k)` per step, but it is classical arithmetic with no quantum operations in it.
"""
log_x_i = log_x(i)
total = lax.fori_loop(
i,
k + 1,
lambda j, acc: acc + jnp.exp(log_x(j) - log_x_i),
jnp.asarray(0.0, dtype=jnp.float64),
)
return 1.0 / total
def angle(i: int | Array) -> Array:
r"""Compute the rotation angle :math:`2 \arccos \sqrt{x_i / s_i}` for the (controlled) RY gate at step ``i``."""
return 2 * jnp.arccos(jnp.sqrt(ratio_x_s(i)))
# i = 0 is the only iteration without a control qubit.
for _ in jrange(jnp.where(k > 0, 1, 0)):
ry(angle(0), qv[n1 - 1])
for i in jrange(1, jnp.maximum(k, 1)):
with control(qv[n1 - i]):
ry(angle(i), qv[n1 - 1 - i])
# Reduce the Hamming weight of the 2nd half controlled by the state (Hamming weight) of the 1st half.
for i in jrange(k):
cx(qv[n1 - 1 - i], qv[n1 + n2 - k + i])
def _apply_dicke_unitary(qv: QuantumVariable | Sequence[Qubit], n: int | Array, k: int | Array) -> None:
r"""Apply the Dicke state unitary :math:`U_{n,k}` from Lemma 2 of https://arxiv.org/abs/1904.07358.
:math:`U_{n,k}` is built as a ladder of *Split & Cyclic Shift* blocks :math:`SCS_{n,k}`. It is
defined by :math:`U_{n,k} |0\rangle^{\otimes n-l} |1\rangle^{\otimes l} = |D^n_l\rangle` for *every*
:math:`l \leq k`, not just for :math:`l = k` (arXiv:2112.12435, Eq. 2), and extends linearly to
superpositions of such inputs.
That is why no :math:`k \to n-k` reduction may be applied here, even though it would shorten the
circuit: it is only valid for an input of Hamming weight exactly :math:`k` and would silently
destroy the :math:`l < k` branches. ``_divide`` does assume weight exactly :math:`k`, so the
reduction lives in ``dicke_state`` on the divide-and-conquer path only.
Parameters
----------
qv : QuantumVariable or Sequence[Qubit]
Initial quantum variable to be prepared. Has to be in the basis state
:math:`|0\rangle^{\otimes n-l} |1\rangle^{\otimes l}` for some :math:`l \leq k`, or in a superposition of such
states.
n : int
The size of the quantum variable.
k : int
The Hamming weight (i.e. number of "ones") of the Dicke state to be constructed.
"""
for offset in jrange(jnp.where(k > 0, n - k, 0)): # If `k == 0`, we don't execute anything. D(n, 0) = |00 ... 0>
index2 = n - offset
_split_cycle_shift(qv, index2, k)
for offset in jrange(jnp.maximum(k - 1, 0)):
index = k - offset
_split_cycle_shift(qv, index, index - 1)
def _split_cycle_shift(qv: QuantumVariable | Sequence[Qubit], n: int | Array, k: int | Array) -> None:
"""Apply the *Split & Cyclic Shift* unitary :math:`SCS_{n, k}` defined in https://arxiv.org/abs/1904.07358.
Helper function for Dicke State initialization of a QuantumVariable. The construction follows section 2.2. of the
above-linked paper. The unitary is applied to ``qv`` in place.
Parameters
----------
qv : QuantumVariable or Sequence[Qubit]
Initial quantum variable to be prepared. Has to be in target subspace.
n : int
Index ``n`` for indication of preparation steps, as seen in original algorithm.
k : int
Index ``k`` for indication of preparation steps, as seen in original algorithm.
"""
# Qubit labels are off by one, since Qrisp labels qubits starting from 0 whereas the paper starts from 1.
# l = 1
param = 2 * jnp.arccos(jnp.sqrt(1 / n))
cx(qv[n - 2], qv[n - 1])
with control(qv[n - 1]):
ry(param, qv[n - 2])
cx(qv[n - 2], qv[n - 1])
# 2 <= l <= k
for l in jrange(2, k + 1): # noqa: E741
param = 2 * jnp.arccos(jnp.sqrt(l / n))
cx(qv[n - l - 1], qv[n - 1])
with control([qv[n - 1], qv[n - l]]):
ry(param, qv[n - l - 1])
cx(qv[n - l - 1], qv[n - 1])