merlin.core.state_vector

StateVector with combinatorial metadata and conversions.

This module provides a lightweight StateVector wrapper that keeps the Fock-space metadata (number of modes, number of photons, basis ordering) tied to its amplitude tensor. It supports dense and sparse tensors, Fock ordering via Combinadics, and conversion to/from exqalibur.StateVector.

StateVector

class merlin.core.state_vector.StateVector(tensor, n_modes, n_photons, encoding=EncodingSpace(family='builtin', kind='fock'), _normalized=False)

Bases: object

Amplitude tensor bundled with its Fock metadata.

Keeps n_modes / n_photons and combinadics basis ordering alongside the underlying PyTorch tensor (dense or sparse).

Parameters:
  • tensor (torch.Tensor) – Dense or sparse amplitude tensor; leading dimensions (if any) are treated as batch axes.

  • n_modes (int) – Number of modes in the Fock space.

  • n_photons (int) – Total photon number represented by the state.

  • encoding (EncodingSpace) – Logical encoding used to construct the stored tensor. Default value is EncodingSpace.FOCK.

  • _normalized (bool) – Internal flag tracking whether the stored tensor is normalized. Default value is False.

Notes

This is a thin wrapper over a torch.Tensor: only shape, device, dtype, and requires_grad are delegated automatically, and tensor-like helpers to, clone, detach, and requires_grad_ are provided to mirror common tensor workflows while preserving metadata. Layout-changing operations (e.g., reshape/view) are intentionally not exposed; perform those on tensor explicitly if needed and rebuild via from_tensor.

tensor: Tensor
n_modes: int
n_photons: int
encoding: EncodingSpace = EncodingSpace(family='builtin', kind='fock')
property is_normalized: bool

Whether the stored tensor is already normalized.

Type:

bool

property basis: Combinadics

Lazy combinadics basis for (n_modes, n_photons) in Fock ordering.

property is_sparse: bool

Return True if the underlying tensor uses a sparse layout.

property basis_size: int

Return the number of basis states for (n_modes, n_photons).

to(*args, **kwargs)

Return a new state vector moved or cast via torch.Tensor.to.

Parameters:
Returns:

Converted state vector.

Return type:

StateVector

clone()

Return a cloned state vector with identical metadata and normalization flag.

Returns:

Cloned state vector.

Return type:

StateVector

detach()

Return a detached StateVector sharing data without gradients.

Returns:

Detached state vector.

Return type:

StateVector

requires_grad_(requires_grad=True)

Set requires_grad on the underlying tensor and return self.

Parameters:

requires_grad (bool) – Whether gradients should be tracked.

Returns:

The updated instance.

Return type:

StateVector

memory_bytes()

Approximate memory footprint (bytes) of the underlying tensor data.

Return type:

int

logical_to_fock_map()

Return the logical-to-Fock index map for this state vector.

The returned dictionary exposes the exact embedding order implied by self.encoding for this state vector’s n_modes and n_photons. Keys are logical basis labels and values are indices in Merlin’s canonical full-Fock basis. For EncodingSpace.FOCK, keys are full Fock occupation tuples and values are their descending-lexicographic Fock indices.

Parameters:

None – This method uses the state vector metadata stored on self.

Returns:

Mapping from logical basis labels to canonical Fock-basis indices.

Return type:

dict[tuple[int, …], int]

Raises:

ValueError – If the stored encoding cannot resolve a basis for this state vector’s n_modes and n_photons.

Examples

>>> import torch
>>> from merlin.core import EncodingSpace
>>> from merlin.core.state_vector import StateVector
>>> logical = torch.zeros(4, dtype=torch.complex64)
>>> sv = StateVector.from_tensor(logical, encoding=EncodingSpace.DUAL_RAIL)
>>> sv.logical_to_fock_map()
{(0, 0): 2, (0, 1): 3, (1, 0): 5, (1, 1): 6}
to_perceval()

Convert to pcvl.StateVector.

Returns:

A Perceval state for 1D tensors, or a list for batched tensors, with amplitudes preserved (no extra renormalization ).

Return type:

pcvl.StateVector | list[pcvl.StateVector]

classmethod from_perceval(state_vector, *, dtype=None, device=None, sparse=None)

Build from a pcvl.StateVector.

Parameters:
  • state_vector (pcvl.StateVector) – Perceval state to wrap.

  • dtype (torch.dtype | None) – Optional target dtype.

  • device (torch.device | None) – Optional target device.

  • sparse (bool | None) – Force sparse or dense output. If None, a density heuristic is used.

Returns:

Merlin wrapper with metadata and preserved amplitudes.

Return type:

StateVector

Raises:

ValueError – If the Perceval state is empty or has inconsistent photon or mode counts.

classmethod from_basic_state(state, *, dtype=None, device=None, sparse=True)

Create a one-hot state from a Fock occupation list/BasicState.

Parameters:
  • state (Sequence[int] | pcvl.BasicState) – Occupation numbers per mode.

  • dtype (torch.dtype | None) – Optional target dtype.

  • device (torch.device | None) – Optional target device.

  • sparse (bool) – Whether to build a sparse tensor.

Returns:

One-hot state vector.

Return type:

StateVector

classmethod from_tensor(tensor, *, n_modes=None, n_photons=None, encoding=None, dtype=None, device=None)

Wrap an existing tensor with explicit metadata.

Parameters:
  • tensor (torch.Tensor) – Dense or sparse amplitude tensor.

  • n_modes (int | None) – Number of modes. Required for Fock and unbunched inputs. For structured encodings, omitted values are inferred from the encoding contract. If provided, the value must match that contract. Default value is None.

  • n_photons (int | None) – Total photons. Required for Fock and unbunched inputs. For structured encodings, omitted values are inferred from the encoding contract or, for dual rail, from the tensor’s final dimension. If provided, the value must match that contract. Default value is None.

  • encoding (EncodingSpace | None) – Logical input encoding. When omitted, the tensor is treated as canonical Fock-space amplitudes and stored unchanged. Default value is None.

  • dtype (torch.dtype | None) – Target dtype. If omitted, complex inputs keep their dtype and real inputs are promoted to torch.complex64. Default value is None.

  • device (torch.device | None) – Target device. If omitted, the input tensor’s device is preserved. Default value is None.

Returns:

Wrapped tensor with metadata.

Return type:

StateVector

Raises:

ValueError – If the tensor is scalar, if required dimensions are missing, if explicit dimensions do not match the encoding contract, or if the last dimension does not match the resolved logical basis size.

tensor_product(other, *, sparse=None)

Tensor product of two states with metadata propagation.

If any operand is dense, the result is dense. Supports one-hot fast path. The resulting state is normalized before returning.

Parameters:
  • other (StateVector | Sequence[int] | pcvl.BasicState) – Another state vector or a basic state / occupation list.

  • sparse (bool | None) – Override sparsity of the result. By default the result remains dense if any input is dense.

Returns:

Combined state with summed modes and photons (normalized).

Return type:

StateVector

Raises:

ValueError – If tensors are not one-dimensional.

index(state)

Return basis index for the given Fock state.

Parameters:

state (Sequence[int] | pcvl.BasicState) – Occupation list or basic state.

Returns:

Basis index, or None if not present.

Return type:

int | None

to_dense()

Return a dense, normalized tensor view of the amplitudes.

Return type:

Tensor

normalize()

Normalize this state in-place and return self.

Return type:

StateVector

normalized_str()

Human-friendly string of the normalized state (forces normalization for display).

Return type:

str

Notes and Examples

Constructors

from_basic_state — one-hot Fock state (sparse by default):

from merlin.core.state_vector import StateVector

sv = StateVector.from_basic_state([1, 0, 1, 0])
assert sv.is_sparse
assert sv.n_modes == 4 and sv.n_photons == 2

# Dense variant
sv_dense = StateVector.from_basic_state([1, 0, 1, 0], sparse=False)
assert not sv_dense.is_sparse

from_tensor — wrap a real or complex tensor with Fock metadata. Real data is auto-promoted to complex. By default, the last dimension must match the Fock basis size \(\binom{n\_modes + n\_photons - 1}{n\_photons}\). The raw amplitudes are stored as provided; StateVector normalizes lazily when a normalized dense view or layer execution needs it:

import torch
from merlin.core.state_vector import StateVector

# Single sample (1-D)
features = torch.randn(10)
sv = StateVector.from_tensor(features, n_modes=4, n_photons=2)

# Batched (2-D) — leading dimensions are batch axes
batch = torch.randn(32, 10)
sv_batch = StateVector.from_tensor(batch, n_modes=4, n_photons=2)
assert sv_batch.shape == (32, 10)

Pass encoding=... to provide compact logical amplitudes and embed them into the Fock basis immediately. Structured encodings can infer their physical metadata from the encoding contract. Explicit n_modes and n_photons are also accepted, but they validate the contract rather than stretching it:

import torch
from merlin.core import EncodingSpace
from merlin.core.state_vector import StateVector

logical = torch.zeros(4, dtype=torch.complex64)
logical[0] = 1.0
sv = StateVector.from_tensor(
    logical,
    encoding=EncodingSpace.DUAL_RAIL,
)
assert sv.n_modes == 4
assert sv.n_photons == 2
assert sv.tensor.shape[-1] == 10
assert sv.encoding is EncodingSpace.DUAL_RAIL

# QLOQ dimensions are inferred from the qubit groups.
qloq = EncodingSpace.qloq(qubit_groups=[2, 1])
sv_qloq = StateVector.from_tensor(torch.zeros(8), encoding=qloq)
assert sv_qloq.n_modes == 6
assert sv_qloq.n_photons == 2

# Add auxiliary vacuum modes after constructing the encoded state.
padded = sv @ [0]
assert padded.n_modes == 5
assert padded.n_photons == 2

from_perceval — convert from a Perceval StateVector:

import perceval as pcvl
from merlin.core.state_vector import StateVector

pv = pcvl.StateVector(pcvl.BasicState([1, 0]))
sv = StateVector.from_perceval(pv)

# Round-trip
pv_back = sv.to_perceval()

Properties and metadata

n_modes and n_photons are set at construction and immutable:

sv = StateVector.from_basic_state([1, 0, 1, 0])
sv.n_modes    # 4
sv.n_photons  # 2
sv.n_modes = 5  # raises AttributeError

shape, device, dtype, and requires_grad are delegated to the underlying tensor:

sv.shape          # torch.Size([10])  — basis_size for (4, 2)
sv.device         # device(type='cpu')
sv.dtype          # torch.complex64

basis returns the combinadics Fock ordering for (n_modes, n_photons). basis_size is equivalent to len(basis):

sv.basis_size     # 10 for (4 modes, 2 photons)
list(sv.basis)[:3]  # [(2,0,0,0), (1,1,0,0), (1,0,1,0)]

logical_to_fock_map() exposes the logical-to-Fock index assignment used by the stored encoding:

import torch
from merlin.core import EncodingSpace
from merlin.core.state_vector import StateVector

sv = StateVector.from_tensor(
    torch.zeros(4, dtype=torch.complex64),
    encoding=EncodingSpace.DUAL_RAIL,
)
sv.logical_to_fock_map()  # {(0, 0): 2, (0, 1): 3, (1, 0): 5, (1, 1): 6}

See Encoding Spaces for full worked examples that compare logical labels, Fock occupation tuples, and tensor indices.

Amplitude lookup

Use bracket syntax with an occupation list or pcvl.BasicState:

import perceval as pcvl

sv = StateVector.from_basic_state([1, 0, 1, 0], sparse=False)
amp = sv[[1, 0, 1, 0]]                    # complex scalar
amp = sv[pcvl.BasicState([1, 0, 1, 0])]   # equivalent

For batched states, the returned tensor matches the batch shape:

import torch

batch = torch.randn(8, 10, dtype=torch.complex64)
sv = StateVector.from_tensor(batch, n_modes=4, n_photons=2)
amps = sv[[1, 0, 1, 0]]   # shape: (8,)

index(state) returns the integer basis index (or None if the state is absent in a sparse tensor):

sv.index([1, 0, 1, 0])  # e.g. 2

Superpositions and arithmetic

Addition and subtraction require matching n_modes and n_photons. Results are not automatically normalized — call .normalize() explicitly:

a = StateVector.from_basic_state([1, 0], sparse=False)
b = StateVector.from_basic_state([0, 1], sparse=False)

superposed = (a + b).normalize()       # (|1,0⟩ + |0,1⟩) / √2
diff       = (a - b).normalize()       # (|1,0⟩ - |0,1⟩) / √2
scaled     = 0.5 * a                   # unnormalized until .normalize()

normalize() acts in-place and returns self.

Tensor product (tensor_product or @) combines two sub-systems:

left  = StateVector.from_basic_state([1, 0], sparse=False)
right = StateVector.from_basic_state([0, 1], sparse=True)
combined = left.tensor_product(right)  # or: left @ right
assert combined.n_modes == 4 and combined.n_photons == 2

Dense tensor access

to_dense() returns a normalized, dense torch.Tensor. Sparse states are materialized; already-dense states are returned directly:

sv = StateVector.from_basic_state([1, 0, 1, 0])
dense = sv.to_dense()   # shape: (10,), complex, sum of |amplitudes|^2 == 1

PyTorch-like helpers

to, clone, detach, and requires_grad_ mirror the standard torch.Tensor API while preserving Fock metadata:

sv = StateVector.from_basic_state([1, 0], sparse=False)

sv_cuda = sv.to("cuda")               # moves tensor, preserves n_modes/n_photons
sv_copy = sv.clone()                   # independent copy with same metadata
sv_det  = sv.detach()                  # shares data, no gradient graph
sv.requires_grad_(True)               # enable gradients in-place; returns self

QuantumLayer integration

As input_state — sets the initial photon configuration:

import merlin as ML
from merlin.core.state_vector import StateVector

layer = ML.QuantumLayer(
    builder=builder,
    input_state=StateVector.from_basic_state([1, 0, 1, 0]),
    n_photons=2,
    measurement_strategy=ML.MeasurementStrategy.probs(ML.ComputationSpace.FOCK),
)

As input to forward() — activates amplitude encoding with classical data:

import torch
from merlin.core.state_vector import StateVector

features = torch.randn(32, len(layer.output_keys))
sv = StateVector.from_tensor(features, n_modes=4, n_photons=2)
output = layer(sv)   # shape: (32, output_size)

As output from forward() — with MeasurementStrategy.amplitudes(ComputationSpace.FOCK) and return_object=True:

layer = ML.QuantumLayer(
    builder=builder,
    n_photons=2,
    measurement_strategy=ML.MeasurementStrategy.amplitudes(ML.ComputationSpace.FOCK),
    return_object=True,
)
sv_out = layer(sv)               # StateVector
sv_out[[1, 0, 1, 0]]            # amplitude lookup on the output

Perceval interoperability

Round-trip between Merlin and Perceval representations:

import perceval as pcvl
from merlin.core.state_vector import StateVector

# Perceval → Merlin
pcvl_sv = (
    pcvl.StateVector(pcvl.BasicState([1, 0, 1, 0]))
    + pcvl.StateVector(pcvl.BasicState([0, 1, 0, 1]))
)
sv = StateVector.from_perceval(pcvl_sv)

# Merlin → Perceval
pv_back = sv.to_perceval()