Skip to content

PyTorch Quantum Layer & Continuous Resonance

quanta.torch.layer

quanta.torch.layer — Variational Quantum Circuit Layer for PyTorch.

Provides QuantumLayer(nn.Module), an autograd-differentiable quantum circuit layer supporting arbitrary variational ansatzes, flexible Pauli observable readouts, and exact analytical gradients via the Parameter-Shift Rule.

ObservableParser

Compiles diverse user observable inputs into canonical ParsedObservables.

Source code in quanta/torch/layer.py
class ObservableParser:
    """Compiles diverse user observable inputs into canonical ParsedObservables."""

    VALID_PAULIS = frozenset({"I", "X", "Y", "Z"})

    @classmethod
    def parse_all(
        cls,
        observables: Sequence[Any] | None,
        num_qubits: int,
    ) -> list[ParsedObservable]:
        """Parse observable specifications into canonical form.

        Args:
            observables: None, list of strings, list of tuples, or nested lists.
            num_qubits: Number of qubits in the circuit.

        Returns:
            List of ParsedObservable instances.
        """
        if observables is None:
            # Default: Single-qubit Z on all qubits: [Z0, Z1, ..., Z_{N-1}]
            return [
                ParsedObservable(
                    terms=(
                        PauliTerm(
                            pauli_string="I" * q + "Z" + "I" * (num_qubits - q - 1),
                            coeff=1.0,
                        ),
                    ),
                    name=f"Z{q}",
                )
                for q in range(num_qubits)
            ]

        if not observables:
            raise ValueError("Observables list cannot be empty.")

        # Check if user passed a single composite Hamiltonian: list of (str, float) tuples
        first = observables[0]
        if (
            isinstance(first, tuple)
            and len(first) == 2
            and isinstance(first[0], str)
            and isinstance(first[1], (int, float))
        ):
            # Single composite observable
            composite_terms = tuple(
                cls._parse_term(term_str, float(coeff), num_qubits)
                for term_str, coeff in observables
            )
            return [ParsedObservable(terms=composite_terms, name="H_0")]

        parsed_list: list[ParsedObservable] = []
        for idx, obs in enumerate(observables):
            if isinstance(obs, str):
                term = cls._parse_term(obs, 1.0, num_qubits)
                parsed_list.append(ParsedObservable(terms=(term,), name=obs))
            elif (
                isinstance(obs, tuple)
                and len(obs) == 2
                and isinstance(obs[0], str)
                and isinstance(obs[1], (int, float))
            ):
                term = cls._parse_term(obs[0], float(obs[1]), num_qubits)
                parsed_list.append(
                    ParsedObservable(terms=(term,), name=f"{obs[0]}*{obs[1]}")
                )
            elif isinstance(obs, (list, tuple)):
                # Composite multi-term observable
                if not obs:
                    raise ValueError("Composite observable cannot be empty.")
                terms = tuple(
                    cls._parse_term(t_str, float(c), num_qubits)
                    for t_str, c in obs
                )
                parsed_list.append(
                    ParsedObservable(terms=terms, name=f"Obs_{idx}")
                )
            else:
                raise TypeError(
                    f"Unsupported observable type at index {idx}: {type(obs)}. "
                    f"Expected str, tuple[str, float], or list of tuples."
                )

        return parsed_list

    @classmethod
    def _parse_term(cls, raw: str, coeff: float, num_qubits: int) -> PauliTerm:
        """Parses a Pauli string term (full string or indexed shorthand)."""
        raw = raw.strip().upper()
        if not raw:
            raise ValueError("Observable descriptor string cannot be empty or whitespace.")

        # Case 1: Full canonical Pauli string of exact length num_qubits
        if len(raw) == num_qubits and all(ch in cls.VALID_PAULIS for ch in raw):
            return PauliTerm(pauli_string=raw, coeff=coeff)

        # Case 2: Indexed notation, e.g. "Z0", "X1", "Z0 Z1", "X0 Y1"
        chars = list("I" * num_qubits)
        tokens = raw.split()
        pattern = re.compile(r"^([XYZI])(\d+)$")

        for token in tokens:
            match = pattern.match(token)
            if not match:
                raise ValueError(
                    f"Invalid Pauli descriptor '{token}'. Must be full string "
                    f"of length {num_qubits} (e.g. 'ZIZ') or indexed (e.g. 'Z0', 'X1')."
                )
            pauli_ch, q_str = match.groups()
            qubit_idx = int(q_str)
            if qubit_idx < 0 or qubit_idx >= num_qubits:
                raise ValueError(
                    f"Qubit index {qubit_idx} out of range [0, {num_qubits - 1}] "
                    f"in observable descriptor '{token}'."
                )
            chars[qubit_idx] = pauli_ch

        return PauliTerm(pauli_string="".join(chars), coeff=coeff)
parse_all classmethod
parse_all(
    observables: Sequence[Any] | None, num_qubits: int
) -> list[ParsedObservable]

Parse observable specifications into canonical form.

Parameters:

Name Type Description Default
observables Sequence[Any] | None

None, list of strings, list of tuples, or nested lists.

required
num_qubits int

Number of qubits in the circuit.

required

Returns:

Type Description
list[ParsedObservable]

List of ParsedObservable instances.

Source code in quanta/torch/layer.py
@classmethod
def parse_all(
    cls,
    observables: Sequence[Any] | None,
    num_qubits: int,
) -> list[ParsedObservable]:
    """Parse observable specifications into canonical form.

    Args:
        observables: None, list of strings, list of tuples, or nested lists.
        num_qubits: Number of qubits in the circuit.

    Returns:
        List of ParsedObservable instances.
    """
    if observables is None:
        # Default: Single-qubit Z on all qubits: [Z0, Z1, ..., Z_{N-1}]
        return [
            ParsedObservable(
                terms=(
                    PauliTerm(
                        pauli_string="I" * q + "Z" + "I" * (num_qubits - q - 1),
                        coeff=1.0,
                    ),
                ),
                name=f"Z{q}",
            )
            for q in range(num_qubits)
        ]

    if not observables:
        raise ValueError("Observables list cannot be empty.")

    # Check if user passed a single composite Hamiltonian: list of (str, float) tuples
    first = observables[0]
    if (
        isinstance(first, tuple)
        and len(first) == 2
        and isinstance(first[0], str)
        and isinstance(first[1], (int, float))
    ):
        # Single composite observable
        composite_terms = tuple(
            cls._parse_term(term_str, float(coeff), num_qubits)
            for term_str, coeff in observables
        )
        return [ParsedObservable(terms=composite_terms, name="H_0")]

    parsed_list: list[ParsedObservable] = []
    for idx, obs in enumerate(observables):
        if isinstance(obs, str):
            term = cls._parse_term(obs, 1.0, num_qubits)
            parsed_list.append(ParsedObservable(terms=(term,), name=obs))
        elif (
            isinstance(obs, tuple)
            and len(obs) == 2
            and isinstance(obs[0], str)
            and isinstance(obs[1], (int, float))
        ):
            term = cls._parse_term(obs[0], float(obs[1]), num_qubits)
            parsed_list.append(
                ParsedObservable(terms=(term,), name=f"{obs[0]}*{obs[1]}")
            )
        elif isinstance(obs, (list, tuple)):
            # Composite multi-term observable
            if not obs:
                raise ValueError("Composite observable cannot be empty.")
            terms = tuple(
                cls._parse_term(t_str, float(c), num_qubits)
                for t_str, c in obs
            )
            parsed_list.append(
                ParsedObservable(terms=terms, name=f"Obs_{idx}")
            )
        else:
            raise TypeError(
                f"Unsupported observable type at index {idx}: {type(obs)}. "
                f"Expected str, tuple[str, float], or list of tuples."
            )

    return parsed_list

ParsedObservable dataclass

A quantum observable composed of one or more Pauli terms.

Attributes:

Name Type Description
terms tuple[PauliTerm, ...]

Tuple of Pauli terms summing to the observable H = Σ c_i P_i.

name str

Human-readable display label.

Source code in quanta/torch/layer.py
@dataclass(frozen=True)
class ParsedObservable:
    """A quantum observable composed of one or more Pauli terms.

    Attributes:
        terms: Tuple of Pauli terms summing to the observable H = Σ c_i P_i.
        name: Human-readable display label.
    """

    terms: tuple[PauliTerm, ...]
    name: str

PauliTerm dataclass

A single Pauli tensor product with a scalar coefficient.

Attributes:

Name Type Description
pauli_string str

String of length num_qubits, e.g. "ZII", "IXY".

coeff float

Real coefficient.

Source code in quanta/torch/layer.py
@dataclass(frozen=True)
class PauliTerm:
    """A single Pauli tensor product with a scalar coefficient.

    Attributes:
        pauli_string: String of length num_qubits, e.g. "ZII", "IXY".
        coeff: Real coefficient.
    """

    pauli_string: str
    coeff: float = 1.0

QuantumLayer

Bases: Module

Variational Quantum Circuit Layer for PyTorch.

Accepts classical input tensors and evaluates expectation values of specified quantum observables across parameterized quantum states. Differentiable via the analytical Parameter-Shift Rule for both variational parameters and inputs.

Parameters:

Name Type Description Default
num_qubits int

Number of simulated qubits (>= 1).

required
circuit_fn Callable[..., Any] | Any | str

Ansatz preset name ('hardware_efficient', 'strongly_entangling', 'reuploading', 'real_amplitudes'), or a custom Callable.

'hardware_efficient'
num_layers int

Depth of the variational circuit (>= 1).

1
observables Sequence[Any] | None

Observables to measure. Defaults to [Z0, Z1, ..., Z_{N-1}].

None
diff_method str

Differentiation method ('parameter-shift' or 'finite-diff').

'parameter-shift'
device str | device | None

Torch device for execution and parameters.

None
dtype dtype

Floating-point precision (torch.float32 or torch.float64).

float32
init_method str

Weight initialization scheme ('uniform', 'normal', 'zeros').

'uniform'
encoding str

Feature encoding method ('angle').

'angle'
num_params int | None

Optional explicit parameter count override for custom callables.

None

Examples:

>>> import torch
>>> import torch.nn as nn
>>> from quanta.torch import QuantumLayer
>>>
>>> layer = QuantumLayer(num_qubits=4, num_layers=2)
>>> x = torch.randn(8, 4)
>>> y = layer(x)
>>> y.shape
torch.Size([8, 4])
>>>
>>> # Sequential model integration
>>> model = nn.Sequential(
...     nn.Linear(2, 4),
...     QuantumLayer(num_qubits=4, num_layers=1),
...     nn.Linear(4, 1),
... )
Source code in quanta/torch/layer.py
class QuantumLayer(nn.Module):
    """Variational Quantum Circuit Layer for PyTorch.

    Accepts classical input tensors and evaluates expectation values of specified
    quantum observables across parameterized quantum states. Differentiable via
    the analytical Parameter-Shift Rule for both variational parameters and inputs.

    Args:
        num_qubits: Number of simulated qubits (>= 1).
        circuit_fn: Ansatz preset name ('hardware_efficient', 'strongly_entangling',
            'reuploading', 'real_amplitudes'), or a custom Callable.
        num_layers: Depth of the variational circuit (>= 1).
        observables: Observables to measure. Defaults to [Z0, Z1, ..., Z_{N-1}].
        diff_method: Differentiation method ('parameter-shift' or 'finite-diff').
        device: Torch device for execution and parameters.
        dtype: Floating-point precision (torch.float32 or torch.float64).
        init_method: Weight initialization scheme ('uniform', 'normal', 'zeros').
        encoding: Feature encoding method ('angle').
        num_params: Optional explicit parameter count override for custom callables.

    Examples:
        >>> import torch
        >>> import torch.nn as nn
        >>> from quanta.torch import QuantumLayer
        >>>
        >>> layer = QuantumLayer(num_qubits=4, num_layers=2)
        >>> x = torch.randn(8, 4)
        >>> y = layer(x)
        >>> y.shape
        torch.Size([8, 4])
        >>>
        >>> # Sequential model integration
        >>> model = nn.Sequential(
        ...     nn.Linear(2, 4),
        ...     QuantumLayer(num_qubits=4, num_layers=1),
        ...     nn.Linear(4, 1),
        ... )
    """

    def __init__(
        self,
        num_qubits: int,
        circuit_fn: Callable[..., Any] | Any | str = "hardware_efficient",
        num_layers: int = 1,
        observables: Sequence[Any] | None = None,
        diff_method: str = "parameter-shift",
        device: str | torch.device | None = None,
        dtype: torch.dtype = torch.float32,
        init_method: str = "uniform",
        encoding: str = "angle",
        num_params: int | None = None,
    ) -> None:
        super().__init__()

        if num_qubits < 1:
            raise ValueError(f"num_qubits must be >= 1, got {num_qubits}")
        if num_layers < 1:
            raise ValueError(f"num_layers must be >= 1, got {num_layers}")
        if diff_method not in {"parameter-shift", "finite-diff"}:
            raise ValueError(
                f"Unsupported diff_method '{diff_method}'. "
                "Supported: 'parameter-shift', 'finite-diff'"
            )

        self.num_qubits: int = num_qubits
        self.num_layers: int = num_layers
        self.circuit_fn: Callable[..., Any] | str = circuit_fn
        self.diff_method: str = diff_method
        self.encoding: str = encoding
        self.init_method: str = init_method

        # Dimensions
        self.in_features: int = num_qubits
        self._parsed_observables: list[ParsedObservable] = ObservableParser.parse_all(
            observables, num_qubits
        )
        self.out_features: int = len(self._parsed_observables)

        # Calculate parameter count
        if num_params is not None:
            self.num_params: int = num_params
        elif isinstance(circuit_fn, str):
            self.num_params = get_ansatz_param_count(circuit_fn, num_qubits, num_layers)
        elif hasattr(circuit_fn, "param_count"):
            self.num_params = int(circuit_fn.param_count(num_qubits, num_layers))
        elif hasattr(circuit_fn, "num_params"):
            self.num_params = int(circuit_fn.num_params)
        else:
            raise ValueError(
                "Could not infer parameter count for custom circuit_fn. "
                "Please specify num_params explicitly."
            )

        target_dev = ops.resolve_device(device) if device is not None else None
        self.weights = nn.Parameter(
            torch.empty(self.num_params, dtype=dtype, device=target_dev)
        )
        self.reset_parameters()

    def reset_parameters(self) -> None:
        """Initialize variational parameters."""
        with torch.no_grad():
            if self.init_method == "uniform":
                nn.init.uniform_(self.weights, a=-math.pi, b=math.pi)
            elif self.init_method == "uniform_positive":
                nn.init.uniform_(self.weights, a=0.0, b=2.0 * math.pi)
            elif self.init_method == "normal":
                nn.init.normal_(self.weights, mean=0.0, std=0.1)
            elif self.init_method == "zeros":
                nn.init.zeros_(self.weights)
            else:
                raise ValueError(f"Unknown init_method: '{self.init_method}'")

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """Executes forward simulation and returns observable expectations.

        Args:
            x: Input feature tensor of shape (B, in_features), (in_features,),
                or (*batch, in_features).

        Returns:
            Expectation tensor of shape (B, out_features), (out_features,),
            or (*batch, out_features).
        """
        if x.ndim == 0:
            raise ValueError("Input tensor must have at least 1 dimension, got 0D scalar.")

        last_dim = x.shape[-1]
        if last_dim != self.in_features:
            raise ValueError(
                f"Feature dimension mismatch: expected in_features={self.in_features} "
                f"(matching {self.num_qubits} qubits), but got x.shape[-1]={last_dim}."
            )

        orig_shape = x.shape
        if x.ndim == 1:
            x_2d = x.unsqueeze(0)
        elif x.ndim > 2:
            x_2d = x.reshape(-1, last_dim)
        else:
            x_2d = x

        # Ensure device and dtype match weights (convert dtype first, then device)
        if x_2d.dtype != self.weights.dtype:
            x_2d = x_2d.to(dtype=self.weights.dtype)
        if x_2d.device != self.weights.device:
            x_2d = x_2d.to(device=self.weights.device)

        out_tensor = cast(
            torch.Tensor,
            _QuantumLayerFunction.apply(
                x_2d,
                self.weights,
                self.num_qubits,
                self.num_layers,
                self.circuit_fn,
                self._parsed_observables,
                self.diff_method,
                self.encoding,
            ),
        )

        if x.ndim == 1:
            return out_tensor.squeeze(0)
        elif x.ndim > 2:
            return out_tensor.reshape(*orig_shape[:-1], self.out_features)
        return out_tensor

    def extra_repr(self) -> str:
        circuit_name = (
            self.circuit_fn
            if isinstance(self.circuit_fn, str)
            else getattr(self.circuit_fn, "__name__", str(self.circuit_fn))
        )
        return (
            f"in_features={self.in_features}, out_features={self.out_features}, "
            f"num_qubits={self.num_qubits}, num_layers={self.num_layers}, "
            f"ansatz='{circuit_name}', diff_method='{self.diff_method}', "
            f"num_params={self.num_params}"
        )
forward
forward(x: Tensor) -> torch.Tensor

Executes forward simulation and returns observable expectations.

Parameters:

Name Type Description Default
x Tensor

Input feature tensor of shape (B, in_features), (in_features,), or (*batch, in_features).

required

Returns:

Type Description
Tensor

Expectation tensor of shape (B, out_features), (out_features,),

Tensor

or (*batch, out_features).

Source code in quanta/torch/layer.py
def forward(self, x: torch.Tensor) -> torch.Tensor:
    """Executes forward simulation and returns observable expectations.

    Args:
        x: Input feature tensor of shape (B, in_features), (in_features,),
            or (*batch, in_features).

    Returns:
        Expectation tensor of shape (B, out_features), (out_features,),
        or (*batch, out_features).
    """
    if x.ndim == 0:
        raise ValueError("Input tensor must have at least 1 dimension, got 0D scalar.")

    last_dim = x.shape[-1]
    if last_dim != self.in_features:
        raise ValueError(
            f"Feature dimension mismatch: expected in_features={self.in_features} "
            f"(matching {self.num_qubits} qubits), but got x.shape[-1]={last_dim}."
        )

    orig_shape = x.shape
    if x.ndim == 1:
        x_2d = x.unsqueeze(0)
    elif x.ndim > 2:
        x_2d = x.reshape(-1, last_dim)
    else:
        x_2d = x

    # Ensure device and dtype match weights (convert dtype first, then device)
    if x_2d.dtype != self.weights.dtype:
        x_2d = x_2d.to(dtype=self.weights.dtype)
    if x_2d.device != self.weights.device:
        x_2d = x_2d.to(device=self.weights.device)

    out_tensor = cast(
        torch.Tensor,
        _QuantumLayerFunction.apply(
            x_2d,
            self.weights,
            self.num_qubits,
            self.num_layers,
            self.circuit_fn,
            self._parsed_observables,
            self.diff_method,
            self.encoding,
        ),
    )

    if x.ndim == 1:
        return out_tensor.squeeze(0)
    elif x.ndim > 2:
        return out_tensor.reshape(*orig_shape[:-1], self.out_features)
    return out_tensor
reset_parameters
reset_parameters() -> None

Initialize variational parameters.

Source code in quanta/torch/layer.py
def reset_parameters(self) -> None:
    """Initialize variational parameters."""
    with torch.no_grad():
        if self.init_method == "uniform":
            nn.init.uniform_(self.weights, a=-math.pi, b=math.pi)
        elif self.init_method == "uniform_positive":
            nn.init.uniform_(self.weights, a=0.0, b=2.0 * math.pi)
        elif self.init_method == "normal":
            nn.init.normal_(self.weights, mean=0.0, std=0.1)
        elif self.init_method == "zeros":
            nn.init.zeros_(self.weights)
        else:
            raise ValueError(f"Unknown init_method: '{self.init_method}'")

get_ansatz_param_count

get_ansatz_param_count(
    ansatz: str, num_qubits: int, num_layers: int
) -> int

Compute total parameter count for built-in ansatz presets.

Parameters:

Name Type Description Default
ansatz str

Name of ansatz preset.

required
num_qubits int

Number of qubits.

required
num_layers int

Number of variational layers.

required

Returns:

Type Description
int

Integer parameter count.

Source code in quanta/torch/layer.py
def get_ansatz_param_count(ansatz: str, num_qubits: int, num_layers: int) -> int:
    """Compute total parameter count for built-in ansatz presets.

    Args:
        ansatz: Name of ansatz preset.
        num_qubits: Number of qubits.
        num_layers: Number of variational layers.

    Returns:
        Integer parameter count.
    """
    preset = ansatz.lower()
    if preset == "hardware_efficient":
        return 2 * num_qubits * num_layers
    elif preset == "strongly_entangling":
        return 3 * num_qubits * num_layers
    elif preset == "reuploading":
        return 1 * num_qubits * num_layers
    elif preset == "real_amplitudes":
        return num_qubits * (num_layers + 1)
    else:
        raise ValueError(
            f"Unknown ansatz preset '{ansatz}'. Supported presets: "
            f"'hardware_efficient', 'strongly_entangling', 'reuploading', 'real_amplitudes'."
        )

quanta.torch.continuous

quanta.torch.continuous — Continuous Resonant Quantum Neural Network Layer.

Pillar 2 of Quanta SDK (Milestone 3): Brain-inspired continuous-time quantum resonance, non-local quantum coherence, and simultaneous non-sequential state evolution grounded in foundational physics (Einstein-Podolsky-Rosen non-locality, continuous-time quantum walks, many-body spin networks).

Evolves quantum statevectors continuously under a parameterized network Hamiltonian

H(x, θ) = H_XY(J) + H_Z(x, h, W) + H_X(ω) |ψ(t)⟩ = exp(-i H(x, θ) t) |ψ0⟩

with simultaneous multi-observable expectation readout across all nodes in O(2^N) time. Features exact analytical autograd gradients via Ehrenfest's theorem (for duration t) and Daleckii-Krein matrix spectral Fréchet derivatives (for J, h, W, ω, x).

ContinuousResonantLayer

Bases: Module

Continuous-Time Resonant Quantum Neural Network Layer.

Evolves quantum statevectors under an interacting many-body graph Hamiltonian

H(x, θ) = H_XY(J) + H_Z(x, h, W) + H_X(ω) |ψ(t)⟩ = exp(-i H(x, θ) t) |ψ0⟩

with simultaneous multi-observable expectation readout across all nodes.

Parameters:

Name Type Description Default
num_nodes int

Number of qubits/nodes in the network (>= 1).

required
in_features int

Dimension of input feature vector x (>= 1).

required
coupling_graph Tensor | Sequence[tuple[int, int]] | str

Interaction graph topology: - Preset string: 'complete', 'ring', 'line', 'star', 'none'. - Adjacency matrix: torch.Tensor of shape (num_nodes, num_nodes). - Custom edge list: Sequence of (u, v) tuples.

'complete'
observable_types tuple[str, ...]

Tuple of observables to measure across all nodes. Supported: ('Z', 'X'), ('Z',), ('Z', 'X', 'Y'). Output feature dimension is num_nodes * len(observable_types).

('Z', 'X')
initial_state str | Tensor

Reference initial quantum state: - Preset mode: 'zero' (|0...0>), 'plus' (|+...+>), 'ghz'. - Custom tensor: Normalized statevector of shape (2^num_nodes,).

'zero'
learnable_time bool

Whether evolution duration t is a trainable parameter. If True, registered as nn.Parameter; if False, registered as buffer.

True
initial_time float

Initial evolution duration t0 (> 0.0). Defaults to 1.0.

1.0
device str | device | None

Target execution device ('cpu', 'mps', or torch.device). Defaults to MPS if available, otherwise CPU.

None
dtype dtype

Real scalar floating-point precision (torch.float32 or torch.float64). Note: Apple Silicon MPS only supports torch.float32.

float32
init_method str

Parameter initialization strategy ('default', 'uniform', 'normal', 'zeros').

'default'
diff_method str

Differentiation strategy ('exact', 'spectral', 'autograd', 'finite-diff').

'exact'

Examples:

>>> import torch
>>> import torch.nn as nn
>>> from quanta.torch import ContinuousResonantLayer
>>>
>>> layer = ContinuousResonantLayer(num_nodes=4, in_features=3, coupling_graph="ring")
>>> x = torch.randn(8, 3)
>>> y = layer(x)
>>> y.shape
torch.Size([8, 8])
>>>
>>> # Seamless Sequential integration
>>> model = nn.Sequential(
...     nn.Linear(2, 4),
...     ContinuousResonantLayer(num_nodes=4, in_features=4, coupling_graph="complete"),
...     nn.Linear(8, 1),
... )
Source code in quanta/torch/continuous.py
class ContinuousResonantLayer(nn.Module):
    """Continuous-Time Resonant Quantum Neural Network Layer.

    Evolves quantum statevectors under an interacting many-body graph Hamiltonian:
        H(x, θ) = H_XY(J) + H_Z(x, h, W) + H_X(ω)
        |ψ(t)⟩ = exp(-i H(x, θ) t) |ψ0⟩
    with simultaneous multi-observable expectation readout across all nodes.

    Args:
        num_nodes: Number of qubits/nodes in the network (>= 1).
        in_features: Dimension of input feature vector x (>= 1).
        coupling_graph: Interaction graph topology:
            - Preset string: 'complete', 'ring', 'line', 'star', 'none'.
            - Adjacency matrix: torch.Tensor of shape (num_nodes, num_nodes).
            - Custom edge list: Sequence of (u, v) tuples.
        observable_types: Tuple of observables to measure across all nodes.
            Supported: ('Z', 'X'), ('Z',), ('Z', 'X', 'Y').
            Output feature dimension is num_nodes * len(observable_types).
        initial_state: Reference initial quantum state:
            - Preset mode: 'zero' (|0...0>), 'plus' (|+...+>), 'ghz'.
            - Custom tensor: Normalized statevector of shape (2^num_nodes,).
        learnable_time: Whether evolution duration t is a trainable parameter.
            If True, registered as nn.Parameter; if False, registered as buffer.
        initial_time: Initial evolution duration t0 (> 0.0). Defaults to 1.0.
        device: Target execution device ('cpu', 'mps', or torch.device).
            Defaults to MPS if available, otherwise CPU.
        dtype: Real scalar floating-point precision (torch.float32 or torch.float64).
            Note: Apple Silicon MPS only supports torch.float32.
        init_method: Parameter initialization strategy ('default', 'uniform', 'normal', 'zeros').
        diff_method: Differentiation strategy ('exact', 'spectral', 'autograd', 'finite-diff').

    Examples:
        >>> import torch
        >>> import torch.nn as nn
        >>> from quanta.torch import ContinuousResonantLayer
        >>>
        >>> layer = ContinuousResonantLayer(num_nodes=4, in_features=3, coupling_graph="ring")
        >>> x = torch.randn(8, 3)
        >>> y = layer(x)
        >>> y.shape
        torch.Size([8, 8])
        >>>
        >>> # Seamless Sequential integration
        >>> model = nn.Sequential(
        ...     nn.Linear(2, 4),
        ...     ContinuousResonantLayer(num_nodes=4, in_features=4, coupling_graph="complete"),
        ...     nn.Linear(8, 1),
        ... )
    """

    initial_state: torch.Tensor
    t: torch.Tensor

    def __init__(
        self,
        num_nodes: int,
        in_features: int,
        coupling_graph: torch.Tensor | Sequence[tuple[int, int]] | str = "complete",
        observable_types: tuple[str, ...] = ("Z", "X"),
        initial_state: str | torch.Tensor = "zero",
        learnable_time: bool = True,
        initial_time: float = 1.0,
        device: str | torch.device | None = None,
        dtype: torch.dtype = torch.float32,
        init_method: str = "default",
        diff_method: str = "exact",
    ) -> None:
        super().__init__()

        if num_nodes < 1:
            raise ValueError(f"num_nodes must be >= 1, got {num_nodes}")
        if in_features < 1:
            raise ValueError(f"in_features must be >= 1, got {in_features}")
        if initial_time <= 0.0:
            raise ValueError(f"initial_time must be > 0.0, got {initial_time}")

        target_dev = ops.resolve_device(device) if device is not None else None
        dev = target_dev if target_dev is not None else torch.device("cpu")
        if (
            target_dev is not None
            and target_dev.type == "mps"
            and dtype in (torch.float64, torch.complex128)
        ):
            raise ops.UnsupportedDtypeError(
                f"Apple Silicon MPS does not support 64-bit precision ({dtype}). "
                f"Use torch.float32 on MPS or switch execution to device='cpu'."
            )

        self.num_nodes: int = num_nodes
        self.in_features: int = in_features
        self.coupling_graph_spec = coupling_graph
        self.edges: list[tuple[int, int]] = GraphTopologyParser.parse(coupling_graph, num_nodes)
        self.num_edges: int = len(self.edges)

        canonical_obs = tuple(obs.upper() for obs in observable_types)
        for obs in canonical_obs:
            if obs not in ("Z", "X", "Y"):
                raise ValueError(
                    f"Unsupported observable type '{obs}'. Supported types: 'Z', 'X', 'Y'."
                )
        self.observable_types: tuple[str, ...] = canonical_obs
        self.out_features: int = num_nodes * len(self.observable_types)

        self.learnable_time: bool = learnable_time
        self.initial_time: float = float(initial_time)
        self.init_method: str = init_method
        self.diff_method: str = diff_method
        self._cached_basis: ops.ResonantInteractionBasis | None = None

        self.complex_dtype = ops.resolve_complex_dtype(dtype, dev)

        # Variational parameters
        self.W = nn.Parameter(torch.empty(num_nodes, in_features, dtype=dtype, device=target_dev))
        self.h = nn.Parameter(torch.empty(num_nodes, dtype=dtype, device=target_dev))
        self.omega = nn.Parameter(torch.empty(num_nodes, dtype=dtype, device=target_dev))
        self.J = nn.Parameter(torch.empty(self.num_edges, dtype=dtype, device=target_dev))

        if self.learnable_time:
            self.t = nn.Parameter(
                torch.tensor([self.initial_time], dtype=dtype, device=target_dev)
            )
        else:
            self.register_buffer(
                "t", torch.tensor([self.initial_time], dtype=dtype, device=target_dev)
            )

        # Initial reference quantum state
        self.initial_state_spec = initial_state
        if isinstance(initial_state, torch.Tensor) and initial_state.numel() != 2 ** num_nodes:
            raise ValueError(
                f"Custom initial_state must have length {2 ** num_nodes}, "
                f"got {initial_state.numel()}."
            )

        init_mode = "plus" if initial_state == "superposition" else initial_state
        resolved_dev = target_dev if target_dev is not None else torch.device("cpu")
        psi0 = ops.create_initial_state(
            init_mode, num_qubits=num_nodes, device=resolved_dev, dtype=self.complex_dtype
        )
        self.register_buffer("initial_state", psi0)

        self.reset_parameters()

    def reset_parameters(self) -> None:
        """Initializes variational parameters based on physical resonance criteria."""
        with torch.no_grad():
            if self.init_method == "default":
                stdv = 1.0 / math.sqrt(self.in_features) if self.in_features > 0 else 1.0
                nn.init.uniform_(self.W, -stdv, stdv)
                nn.init.uniform_(self.h, -0.1, 0.1)
                nn.init.uniform_(self.omega, 0.5, 1.5)
                if self.num_edges > 0:
                    nn.init.uniform_(self.J, -0.5, 0.5)
                if self.learnable_time and hasattr(self, "t") and isinstance(self.t, nn.Parameter):
                    self.t.data.fill_(self.initial_time)
            elif self.init_method == "uniform":
                nn.init.uniform_(self.W, -0.1, 0.1)
                nn.init.uniform_(self.h, -0.1, 0.1)
                nn.init.uniform_(self.omega, 0.5, 1.5)
                if self.num_edges > 0:
                    nn.init.uniform_(self.J, -0.1, 0.1)
                if self.learnable_time and hasattr(self, "t") and isinstance(self.t, nn.Parameter):
                    self.t.data.fill_(self.initial_time)
            elif self.init_method == "normal":
                nn.init.normal_(self.W, mean=0.0, std=0.1)
                nn.init.normal_(self.h, mean=0.0, std=0.1)
                nn.init.normal_(self.omega, mean=1.0, std=0.1)
                if self.num_edges > 0:
                    nn.init.normal_(self.J, mean=0.0, std=0.2)
                if self.learnable_time and hasattr(self, "t") and isinstance(self.t, nn.Parameter):
                    self.t.data.fill_(self.initial_time)
            elif self.init_method == "zeros":
                nn.init.zeros_(self.W)
                nn.init.zeros_(self.h)
                nn.init.zeros_(self.omega)
                if self.num_edges > 0:
                    nn.init.zeros_(self.J)
                if self.learnable_time and hasattr(self, "t") and isinstance(self.t, nn.Parameter):
                    self.t.data.fill_(self.initial_time)
            else:
                raise ValueError(
                    f"Unknown init_method: '{self.init_method}'. "
                    f"Supported: 'default', 'uniform', 'normal', 'zeros'."
                )

    def _get_basis(
        self, device: torch.device, complex_dtype: torch.dtype
    ) -> ops.ResonantInteractionBasis:
        """Retrieves or lazily constructs device-resident interaction basis operators."""
        if (
            self._cached_basis is None
            or self._cached_basis.device != device
            or self._cached_basis.dtype != complex_dtype
        ):
            self._cached_basis = ops.ResonantInteractionBasis(
                num_nodes=self.num_nodes,
                edges=self.edges,
                device=device,
                dtype=complex_dtype,
            )
        return self._cached_basis

    def _apply(self, fn: Any, recurse: bool = True) -> ContinuousResonantLayer:
        init_st = self._buffers.pop("initial_state", None)
        super()._apply(fn, recurse=recurse)
        self._cached_basis = None
        target_dev = self.W.device
        target_c_dtype = ops.resolve_complex_dtype(self.W.dtype, target_dev)
        self.complex_dtype = target_c_dtype
        if init_st is not None:
            if isinstance(self.initial_state_spec, str):
                init_mode = (
                    "plus"
                    if self.initial_state_spec == "superposition"
                    else self.initial_state_spec
                )
                self.register_buffer(
                    "initial_state",
                    ops.create_initial_state(
                        init_mode,
                        num_qubits=self.num_nodes,
                        device=target_dev,
                        dtype=target_c_dtype,
                    ),
                )
            else:
                self.register_buffer(
                    "initial_state",
                    init_st.to(device=target_dev, dtype=target_c_dtype),
                )
        return self

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """Executes continuous Hamiltonian evolution and returns expectation readouts.

        Args:
            x: Input feature tensor of shape (B, in_features), (in_features,),
                or (*batch, in_features).

        Returns:
            Expectation tensor of shape (B, out_features), (out_features,),
            or (*batch, out_features).
        """
        if x.ndim == 0:
            raise ValueError("Input tensor must have at least 1 dimension, got 0D scalar.")

        last_dim = x.shape[-1]
        if last_dim != self.in_features:
            raise ValueError(
                f"Feature dimension mismatch: expected in_features={self.in_features}, "
                f"but got x.shape[-1]={last_dim}."
            )

        orig_shape = x.shape
        if x.ndim == 1:
            x_2d = x.unsqueeze(0)
        elif x.ndim > 2:
            x_2d = x.reshape(-1, last_dim)
        else:
            x_2d = x

        # Ensure dtype matches first, then device (critical for MPS 64-bit safety)
        if x_2d.dtype != self.W.dtype:
            x_2d = x_2d.to(dtype=self.W.dtype)
        if x_2d.device != self.W.device:
            x_2d = x_2d.to(device=self.W.device)

        basis = self._get_basis(self.W.device, self.complex_dtype)

        # Check for empty batch
        if x_2d.shape[0] == 0:
            if self.diff_method == "autograd":
                out_tensor = torch.empty(
                    (0, self.out_features), dtype=self.W.dtype, device=self.W.device
                )
            else:
                out_tensor = cast(
                    torch.Tensor,
                    _ContinuousResonantFunction.apply(
                        x_2d,
                        self.J,
                        self.h,
                        self.W,
                        self.omega,
                        self.t,
                        self.num_nodes,
                        self.edges,
                        self.observable_types,
                        self.initial_state,
                        basis,
                        self.diff_method,
                    ),
                )
        elif self.diff_method == "autograd":
            # Native PyTorch autograd through matrix_exp
            H_batch = ops.build_batch_resonant_hamiltonian(
                x=x_2d,
                J=self.J,
                edges=self.edges,
                h=self.h,
                W=self.W,
                omega=self.omega,
                num_nodes=self.num_nodes,
                basis=basis,
                device=self.W.device,
                dtype=self.complex_dtype,
            )
            psi_t = ops.unitary_evolution(H_batch, self.t, self.initial_state)
            out_tensor = ops.simultaneous_readout(psi_t, self.num_nodes, self.observable_types)
        else:
            out_tensor = cast(
                torch.Tensor,
                _ContinuousResonantFunction.apply(
                    x_2d,
                    self.J,
                    self.h,
                    self.W,
                    self.omega,
                    self.t,
                    self.num_nodes,
                    self.edges,
                    self.observable_types,
                    self.initial_state,
                    basis,
                    self.diff_method,
                ),
            )

        if x.ndim == 1:
            return out_tensor.squeeze(0)
        if x.ndim > 2:
            return out_tensor.reshape(*orig_shape[:-1], self.out_features)
        return out_tensor

    def extra_repr(self) -> str:
        graph_desc = (
            self.coupling_graph_spec
            if isinstance(self.coupling_graph_spec, str)
            else f"{self.num_edges} edges"
        )
        return (
            f"num_nodes={self.num_nodes}, in_features={self.in_features}, "
            f"out_features={self.out_features}, graph='{graph_desc}', "
            f"observables={self.observable_types}, learnable_time={self.learnable_time}, "
            f"initial_time={self.initial_time}, diff_method='{self.diff_method}'"
        )
forward
forward(x: Tensor) -> torch.Tensor

Executes continuous Hamiltonian evolution and returns expectation readouts.

Parameters:

Name Type Description Default
x Tensor

Input feature tensor of shape (B, in_features), (in_features,), or (*batch, in_features).

required

Returns:

Type Description
Tensor

Expectation tensor of shape (B, out_features), (out_features,),

Tensor

or (*batch, out_features).

Source code in quanta/torch/continuous.py
def forward(self, x: torch.Tensor) -> torch.Tensor:
    """Executes continuous Hamiltonian evolution and returns expectation readouts.

    Args:
        x: Input feature tensor of shape (B, in_features), (in_features,),
            or (*batch, in_features).

    Returns:
        Expectation tensor of shape (B, out_features), (out_features,),
        or (*batch, out_features).
    """
    if x.ndim == 0:
        raise ValueError("Input tensor must have at least 1 dimension, got 0D scalar.")

    last_dim = x.shape[-1]
    if last_dim != self.in_features:
        raise ValueError(
            f"Feature dimension mismatch: expected in_features={self.in_features}, "
            f"but got x.shape[-1]={last_dim}."
        )

    orig_shape = x.shape
    if x.ndim == 1:
        x_2d = x.unsqueeze(0)
    elif x.ndim > 2:
        x_2d = x.reshape(-1, last_dim)
    else:
        x_2d = x

    # Ensure dtype matches first, then device (critical for MPS 64-bit safety)
    if x_2d.dtype != self.W.dtype:
        x_2d = x_2d.to(dtype=self.W.dtype)
    if x_2d.device != self.W.device:
        x_2d = x_2d.to(device=self.W.device)

    basis = self._get_basis(self.W.device, self.complex_dtype)

    # Check for empty batch
    if x_2d.shape[0] == 0:
        if self.diff_method == "autograd":
            out_tensor = torch.empty(
                (0, self.out_features), dtype=self.W.dtype, device=self.W.device
            )
        else:
            out_tensor = cast(
                torch.Tensor,
                _ContinuousResonantFunction.apply(
                    x_2d,
                    self.J,
                    self.h,
                    self.W,
                    self.omega,
                    self.t,
                    self.num_nodes,
                    self.edges,
                    self.observable_types,
                    self.initial_state,
                    basis,
                    self.diff_method,
                ),
            )
    elif self.diff_method == "autograd":
        # Native PyTorch autograd through matrix_exp
        H_batch = ops.build_batch_resonant_hamiltonian(
            x=x_2d,
            J=self.J,
            edges=self.edges,
            h=self.h,
            W=self.W,
            omega=self.omega,
            num_nodes=self.num_nodes,
            basis=basis,
            device=self.W.device,
            dtype=self.complex_dtype,
        )
        psi_t = ops.unitary_evolution(H_batch, self.t, self.initial_state)
        out_tensor = ops.simultaneous_readout(psi_t, self.num_nodes, self.observable_types)
    else:
        out_tensor = cast(
            torch.Tensor,
            _ContinuousResonantFunction.apply(
                x_2d,
                self.J,
                self.h,
                self.W,
                self.omega,
                self.t,
                self.num_nodes,
                self.edges,
                self.observable_types,
                self.initial_state,
                basis,
                self.diff_method,
            ),
        )

    if x.ndim == 1:
        return out_tensor.squeeze(0)
    if x.ndim > 2:
        return out_tensor.reshape(*orig_shape[:-1], self.out_features)
    return out_tensor
reset_parameters
reset_parameters() -> None

Initializes variational parameters based on physical resonance criteria.

Source code in quanta/torch/continuous.py
def reset_parameters(self) -> None:
    """Initializes variational parameters based on physical resonance criteria."""
    with torch.no_grad():
        if self.init_method == "default":
            stdv = 1.0 / math.sqrt(self.in_features) if self.in_features > 0 else 1.0
            nn.init.uniform_(self.W, -stdv, stdv)
            nn.init.uniform_(self.h, -0.1, 0.1)
            nn.init.uniform_(self.omega, 0.5, 1.5)
            if self.num_edges > 0:
                nn.init.uniform_(self.J, -0.5, 0.5)
            if self.learnable_time and hasattr(self, "t") and isinstance(self.t, nn.Parameter):
                self.t.data.fill_(self.initial_time)
        elif self.init_method == "uniform":
            nn.init.uniform_(self.W, -0.1, 0.1)
            nn.init.uniform_(self.h, -0.1, 0.1)
            nn.init.uniform_(self.omega, 0.5, 1.5)
            if self.num_edges > 0:
                nn.init.uniform_(self.J, -0.1, 0.1)
            if self.learnable_time and hasattr(self, "t") and isinstance(self.t, nn.Parameter):
                self.t.data.fill_(self.initial_time)
        elif self.init_method == "normal":
            nn.init.normal_(self.W, mean=0.0, std=0.1)
            nn.init.normal_(self.h, mean=0.0, std=0.1)
            nn.init.normal_(self.omega, mean=1.0, std=0.1)
            if self.num_edges > 0:
                nn.init.normal_(self.J, mean=0.0, std=0.2)
            if self.learnable_time and hasattr(self, "t") and isinstance(self.t, nn.Parameter):
                self.t.data.fill_(self.initial_time)
        elif self.init_method == "zeros":
            nn.init.zeros_(self.W)
            nn.init.zeros_(self.h)
            nn.init.zeros_(self.omega)
            if self.num_edges > 0:
                nn.init.zeros_(self.J)
            if self.learnable_time and hasattr(self, "t") and isinstance(self.t, nn.Parameter):
                self.t.data.fill_(self.initial_time)
        else:
            raise ValueError(
                f"Unknown init_method: '{self.init_method}'. "
                f"Supported: 'default', 'uniform', 'normal', 'zeros'."
            )

GraphTopologyParser

Parses diverse user graph inputs into canonical sorted edge lists.

Supports presets ('complete', 'ring', 'line', 'star', 'none'), adjacency matrices, and explicit edge sequences with strict validation.

Source code in quanta/torch/continuous.py
class GraphTopologyParser:
    """Parses diverse user graph inputs into canonical sorted edge lists.

    Supports presets ('complete', 'ring', 'line', 'star', 'none'),
    adjacency matrices, and explicit edge sequences with strict validation.
    """

    @classmethod
    def parse(
        cls,
        graph: torch.Tensor | Sequence[tuple[int, int]] | str,
        num_nodes: int,
    ) -> list[tuple[int, int]]:
        """Parses and validates graph topologies into canonical sorted edge lists.

        Args:
            graph: Graph topology preset name, adjacency matrix, or edge sequence.
            num_nodes: Total number of nodes/qubits (>= 1).

        Returns:
            Canonical list of sorted (u, v) edge tuples with 0 <= u < v < num_nodes.

        Raises:
            ValueError: On invalid node count, out-of-bounds index, self-loop,
                or non-symmetric adjacency matrix.
            TypeError: On unsupported input type.
        """
        if num_nodes < 1:
            raise ValueError(f"num_nodes must be >= 1, got {num_nodes}")

        # 1. String Presets
        if isinstance(graph, str):
            preset = graph.lower().strip()
            if preset in ("complete", "all_to_all", "clique"):
                return [(j, k) for j in range(num_nodes) for k in range(j + 1, num_nodes)]
            if preset in ("ring", "cycle"):
                if num_nodes <= 1:
                    return []
                if num_nodes == 2:
                    return [(0, 1)]
                edges = [(j, j + 1) for j in range(num_nodes - 1)] + [(0, num_nodes - 1)]
                return sorted(edges)
            if preset in ("line", "chain", "path"):
                return [(j, j + 1) for j in range(num_nodes - 1)]
            if preset == "star":
                return [(0, j) for j in range(1, num_nodes)]
            if preset in ("none", "empty", "disconnected"):
                return []
            raise ValueError(
                f"Unknown coupling_graph preset: '{graph}'. Supported presets: "
                f"'complete', 'ring', 'line', 'star', 'none'."
            )

        # 2. Adjacency Matrix Tensor
        if isinstance(graph, torch.Tensor):
            if graph.ndim != 2 or graph.shape[0] != num_nodes or graph.shape[1] != num_nodes:
                raise ValueError(
                    f"Adjacency matrix must have shape ({num_nodes}, {num_nodes}), "
                    f"got {tuple(graph.shape)}."
                )
            if not torch.allclose(graph, graph.T):
                raise ValueError("Adjacency matrix must be symmetric (A == A.T).")

            edges_set: set[tuple[int, int]] = set()
            for j in range(num_nodes):
                for k in range(j + 1, num_nodes):
                    if graph[j, k] != 0 or graph[k, j] != 0:
                        edges_set.add((j, k))
            return sorted(list(edges_set))

        # 3. Explicit Sequence of Edge Tuples
        if isinstance(graph, (list, tuple)):
            raw_edges: set[tuple[int, int]] = set()
            for idx, edge in enumerate(graph):
                if not isinstance(edge, (list, tuple)) or len(edge) != 2:
                    raise ValueError(
                        f"Invalid edge at index {idx}: expected 2-tuple (u, v), got {edge}."
                    )
                u, v = int(edge[0]), int(edge[1])
                if u < 0 or u >= num_nodes or v < 0 or v >= num_nodes:
                    raise ValueError(
                        f"Edge ({u}, {v}) at index {idx} out of node range [0, {num_nodes - 1}]."
                    )
                if u == v:
                    raise ValueError(
                        f"Self-loop edge ({u}, {v}) at index {idx} is not allowed."
                    )
                raw_edges.add((min(u, v), max(u, v)))
            return sorted(list(raw_edges))

        raise TypeError(
            f"Unsupported coupling_graph type: {type(graph)}. "
            f"Expected str, list of (int, int) tuples, or torch.Tensor."
        )
parse classmethod
parse(
    graph: Tensor | Sequence[tuple[int, int]] | str,
    num_nodes: int,
) -> list[tuple[int, int]]

Parses and validates graph topologies into canonical sorted edge lists.

Parameters:

Name Type Description Default
graph Tensor | Sequence[tuple[int, int]] | str

Graph topology preset name, adjacency matrix, or edge sequence.

required
num_nodes int

Total number of nodes/qubits (>= 1).

required

Returns:

Type Description
list[tuple[int, int]]

Canonical list of sorted (u, v) edge tuples with 0 <= u < v < num_nodes.

Raises:

Type Description
ValueError

On invalid node count, out-of-bounds index, self-loop, or non-symmetric adjacency matrix.

TypeError

On unsupported input type.

Source code in quanta/torch/continuous.py
@classmethod
def parse(
    cls,
    graph: torch.Tensor | Sequence[tuple[int, int]] | str,
    num_nodes: int,
) -> list[tuple[int, int]]:
    """Parses and validates graph topologies into canonical sorted edge lists.

    Args:
        graph: Graph topology preset name, adjacency matrix, or edge sequence.
        num_nodes: Total number of nodes/qubits (>= 1).

    Returns:
        Canonical list of sorted (u, v) edge tuples with 0 <= u < v < num_nodes.

    Raises:
        ValueError: On invalid node count, out-of-bounds index, self-loop,
            or non-symmetric adjacency matrix.
        TypeError: On unsupported input type.
    """
    if num_nodes < 1:
        raise ValueError(f"num_nodes must be >= 1, got {num_nodes}")

    # 1. String Presets
    if isinstance(graph, str):
        preset = graph.lower().strip()
        if preset in ("complete", "all_to_all", "clique"):
            return [(j, k) for j in range(num_nodes) for k in range(j + 1, num_nodes)]
        if preset in ("ring", "cycle"):
            if num_nodes <= 1:
                return []
            if num_nodes == 2:
                return [(0, 1)]
            edges = [(j, j + 1) for j in range(num_nodes - 1)] + [(0, num_nodes - 1)]
            return sorted(edges)
        if preset in ("line", "chain", "path"):
            return [(j, j + 1) for j in range(num_nodes - 1)]
        if preset == "star":
            return [(0, j) for j in range(1, num_nodes)]
        if preset in ("none", "empty", "disconnected"):
            return []
        raise ValueError(
            f"Unknown coupling_graph preset: '{graph}'. Supported presets: "
            f"'complete', 'ring', 'line', 'star', 'none'."
        )

    # 2. Adjacency Matrix Tensor
    if isinstance(graph, torch.Tensor):
        if graph.ndim != 2 or graph.shape[0] != num_nodes or graph.shape[1] != num_nodes:
            raise ValueError(
                f"Adjacency matrix must have shape ({num_nodes}, {num_nodes}), "
                f"got {tuple(graph.shape)}."
            )
        if not torch.allclose(graph, graph.T):
            raise ValueError("Adjacency matrix must be symmetric (A == A.T).")

        edges_set: set[tuple[int, int]] = set()
        for j in range(num_nodes):
            for k in range(j + 1, num_nodes):
                if graph[j, k] != 0 or graph[k, j] != 0:
                    edges_set.add((j, k))
        return sorted(list(edges_set))

    # 3. Explicit Sequence of Edge Tuples
    if isinstance(graph, (list, tuple)):
        raw_edges: set[tuple[int, int]] = set()
        for idx, edge in enumerate(graph):
            if not isinstance(edge, (list, tuple)) or len(edge) != 2:
                raise ValueError(
                    f"Invalid edge at index {idx}: expected 2-tuple (u, v), got {edge}."
                )
            u, v = int(edge[0]), int(edge[1])
            if u < 0 or u >= num_nodes or v < 0 or v >= num_nodes:
                raise ValueError(
                    f"Edge ({u}, {v}) at index {idx} out of node range [0, {num_nodes - 1}]."
                )
            if u == v:
                raise ValueError(
                    f"Self-loop edge ({u}, {v}) at index {idx} is not allowed."
                )
            raw_edges.add((min(u, v), max(u, v)))
        return sorted(list(raw_edges))

    raise TypeError(
        f"Unsupported coupling_graph type: {type(graph)}. "
        f"Expected str, list of (int, int) tuples, or torch.Tensor."
    )