Source code for qig.pair_operators

"""
Operator bases for entangled pairs in quantum exponential families.

This module provides generators for su(d²) Lie algebras, which act on
the Hilbert space of a pair of d-level systems (e.g., two qubits for d=2).
These operators can generate entangled states, unlike local operators.
"""

import numpy as np
from typing import List, Optional, Tuple


[docs] def gell_mann_generators(d: int) -> List[np.ndarray]: """ Generate the d²-1 Gell-Mann matrices for su(d). These are traceless Hermitian matrices that generalize the Pauli matrices. For d=2, this gives the three Pauli matrices. For d=3, this gives the eight Gell-Mann matrices. The construction follows the standard pattern: - Symmetric matrices: ``|j⟩⟨k| + |k⟩⟨j|`` for j < k - Antisymmetric matrices: ``-i(|j⟩⟨k| - |k⟩⟨j|)`` for j < k - Diagonal matrices: ``sum_{l=0}^{j-1} |l⟩⟨l| - j|j⟩⟨j|`` for j = 1,...,d-1 Parameters ---------- d : int Dimension of the Hilbert space Returns ------- List[np.ndarray] List of d²-1 traceless Hermitian matrices of size d×d Notes ----- The matrices are normalized to have Tr(λ_a λ_b) = 2δ_{ab}. """ generators = [] # Symmetric matrices: |j⟩⟨k| + |k⟩⟨j| for j < k for j in range(d): for k in range(j + 1, d): sym = np.zeros((d, d), dtype=complex) sym[j, k] = 1 sym[k, j] = 1 generators.append(sym) # Antisymmetric matrices: -i(|j⟩⟨k| - |k⟩⟨j|) for j < k for j in range(d): for k in range(j + 1, d): asym = np.zeros((d, d), dtype=complex) asym[j, k] = -1j asym[k, j] = 1j generators.append(asym) # Diagonal matrices: sqrt(2/(j(j+1))) * (sum_{l=0}^{j-1} |l⟩⟨l| - j|j⟩⟨j|) for j in range(1, d): diag = np.zeros((d, d), dtype=complex) # Sum over l = 0, ..., j-1 for l in range(j): diag[l, l] = 1 # Subtract j times |j⟩⟨j| diag[j, j] = -j # Normalize: Tr(diag²) = j + j² = j(j+1) # We want Tr = 2, so multiply by sqrt(2/(j(j+1))) diag = diag * np.sqrt(2.0 / (j * (j + 1))) generators.append(diag) # Verify we have the right number assert len(generators) == d**2 - 1, f"Expected {d**2-1} generators, got {len(generators)}" # Verify all are traceless and Hermitian for i, g in enumerate(generators): assert np.abs(np.trace(g)) < 1e-10, f"Generator {i} not traceless: Tr = {np.trace(g)}" assert np.allclose(g, g.conj().T), f"Generator {i} not Hermitian" return generators
[docs] def pair_basis_generators(d: int) -> List[np.ndarray]: """ Generate su(d²) generators for a pair of d-level systems. These act on the d²-dimensional Hilbert space of a pair (e.g., two qubits give d=2, d²=4, with 15 generators in su(4)). Parameters ---------- d : int Dimension of each individual system (e.g., 2 for qubits, 3 for qutrits) Returns ------- List[np.ndarray] List of d⁴-1 traceless Hermitian matrices of size d²×d² Examples -------- >>> # For a qubit pair >>> generators = pair_basis_generators(d=2) >>> len(generators) 15 >>> generators[0].shape (4, 4) """ return gell_mann_generators(d**2)
[docs] def bell_state(d: int, k: int = 0) -> np.ndarray: """ Create a maximally entangled state for a pair of d-level systems. Returns the state vector ``|Φₖ⟩ = (1/√d) ∑_{j=0}^{d-1} |j, (j+k) mod d⟩``. For k=0, this is the standard Bell state ``(1/√d) ∑_j |jj⟩``. Parameters ---------- d : int Dimension of each subsystem k : int, default=0 Bell state index (0 to d-1). Different k give orthogonal states. k=0: ``|00⟩ + |11⟩ + ...`` (standard) k=1: ``|01⟩ + |12⟩ + |20⟩ + ...`` (cyclic shift) Returns ------- np.ndarray State vector of length d², normalized Notes ----- All d Bell states are maximally entangled with the same properties: - Global purity: S(ρ) = 0 - Marginals: ρ_A = ρ_B = I/d - Entanglement: maximal for dimension d The states are mutually orthogonal: ``⟨Φₖ|Φₗ⟩ = δₖₗ`` Examples -------- >>> psi = bell_state(d=2, k=0) # |00⟩ + |11⟩ >>> psi array([0.70710678+0.j, 0. +0.j, 0. +0.j, 0.70710678+0.j]) >>> psi = bell_state(d=2, k=1) # |01⟩ + |10⟩ >>> psi array([0. +0.j, 0.70710678+0.j, 0.70710678+0.j, 0. +0.j]) """ if not 0 <= k < d: raise ValueError(f"k must be in range [0, d-1]=[0, {d-1}], got {k}") psi = np.zeros(d**2, dtype=complex) for j in range(d): # |j, (j+k) mod d⟩ corresponds to index j*d + ((j+k) mod d) second_index = (j + k) % d psi[j * d + second_index] = 1.0 psi = psi / np.sqrt(d) return psi
[docs] def bell_state_density_matrix(d: int) -> np.ndarray: """ Create the density matrix of a maximally entangled pair state. Returns ``ρ = |Φ⟩⟨Φ|`` where ``|Φ⟩ = (1/√d) ∑_j |jj⟩``. This is a pure state (Tr(ρ²) = 1) that is globally pure but locally maximally mixed: both marginals have entropy log(d). Parameters ---------- d : int Dimension of each subsystem Returns ------- np.ndarray Density matrix of size d²×d², positive semidefinite with trace 1 Notes ----- Properties of the Bell state: - Global purity: S(ρ) = 0 - Local marginals: ρ_A = ρ_B = I/d (maximally mixed) - Marginal entropies: S(ρ_A) = S(ρ_B) = log(d) - Mutual information: I = 2log(d) (maximal) """ psi = bell_state(d) return np.outer(psi, psi.conj())
[docs] def multi_pair_basis(n_pairs: int, d: int) -> Tuple[List[np.ndarray], List[int]]: """ Generate direct sum operator basis for n independent pairs. Creates operators of the form F_α^(k) = I ⊗ ... ⊗ F_α ⊗ ... ⊗ I, where F_α is at position k. Parameters ---------- n_pairs : int Number of independent pairs d : int Dimension of each subsystem (e.g., 2 for qubits) Returns ------- operators : List[np.ndarray] List of n(d⁴-1) operators, each of size (d²)ⁿ × (d²)ⁿ pair_indices : List[int] Index of which pair each operator acts on Notes ----- The resulting Fisher metric will be block-diagonal, with one block per pair. Cross-pair elements vanish: G_{(k,i),(k',j)} = 0 for k≠k'. Examples -------- >>> # Two qubit pairs >>> ops, indices = multi_pair_basis(n_pairs=2, d=2) >>> len(ops) 30 >>> ops[0].shape (16, 16) >>> indices[:15] # First 15 operators act on pair 0 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] >>> indices[15:] # Next 15 operators act on pair 1 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] """ # Get generators for a single pair pair_generators = pair_basis_generators(d) n_params_per_pair = len(pair_generators) # Total Hilbert space dimension D = d**(2 * n_pairs) # Size of each pair's Hilbert space D_pair = d**2 operators = [] pair_indices = [] for k in range(n_pairs): for F_alpha in pair_generators: # Build I ⊗ ... ⊗ F_α ⊗ ... ⊗ I # Start with F_α for pair k op = F_alpha # Tensor with identity for pairs before k for _ in range(k): op = np.kron(np.eye(D_pair), op) # Tensor with identity for pairs after k for _ in range(n_pairs - k - 1): op = np.kron(op, np.eye(D_pair)) operators.append(op) pair_indices.append(k) assert len(operators) == n_pairs * n_params_per_pair assert len(pair_indices) == len(operators) return operators, pair_indices
[docs] def product_of_bell_states( n_pairs: int, d: int, bell_indices: Optional[List[int]] = None ) -> np.ndarray: """ Create a product state of n maximally entangled pairs. Returns ``|Ψ⟩ = |Φₖ₁⟩⊗|Φₖ₂⟩⊗...⊗|Φₖₙ⟩`` where each ``|Φₖ⟩`` is a Bell state. Parameters ---------- n_pairs : int Number of pairs d : int Dimension of each subsystem bell_indices : list of int, optional Which Bell state (k=0,...,d-1) to use for each pair. If None, uses k=0 (standard Bell state) for all pairs. Returns ------- np.ndarray State vector of length (d²)ⁿ Notes ----- This is the "origin" of the inaccessible game. Properties: - Globally pure: S(ρ) = 0 - All 2n marginals maximally mixed: S(ρ_i) = log(d) for all i - Pairs are entangled, but no cross-pair entanglement - Total marginal entropy: C = 2n·log(d) Different ``bell_indices`` give orthogonal product states that share the same constraint value C and marginal entropies, but represent different "origins" in the exponential family. Examples -------- >>> # Standard: |Φ₀⟩⊗|Φ₀⟩ = (|00⟩+|11⟩)⊗(|00⟩+|11⟩) >>> psi = product_of_bell_states(n_pairs=2, d=2) >>> # Mixed: |Φ₀⟩⊗|Φ₁⟩ = (|00⟩+|11⟩)⊗(|01⟩+|10⟩) >>> psi = product_of_bell_states(n_pairs=2, d=2, bell_indices=[0, 1]) """ if bell_indices is None: bell_indices = [0] * n_pairs if len(bell_indices) != n_pairs: raise ValueError( f"bell_indices must have length {n_pairs}, got {len(bell_indices)}" ) for i, k in enumerate(bell_indices): if not 0 <= k < d: raise ValueError( f"bell_indices[{i}]={k} out of range [0, {d-1}]" ) # Tensor product of Bell states with specified indices result = bell_state(d, k=bell_indices[0]) for i in range(1, n_pairs): result = np.kron(result, bell_state(d, k=bell_indices[i])) return result
if __name__ == "__main__": # Test su(4) generators for qubit pair print("Testing su(4) generators for qubit pair:") generators = pair_basis_generators(d=2) print(f"Number of generators: {len(generators)} (expected 15)") print(f"Shape: {generators[0].shape} (expected (4,4))") # Test Bell state print("\nTesting Bell state:") psi = bell_state(d=2) print(f"Bell state: {psi}") print(f"Norm: {np.linalg.norm(psi):.6f} (expected 1)") rho = bell_state_density_matrix(d=2) print(f"Purity Tr(ρ²): {np.trace(rho @ rho):.6f} (expected 1)") # Test marginals rho_A = np.array([[rho[0,0] + rho[1,1], rho[0,2] + rho[1,3]], [rho[2,0] + rho[3,1], rho[2,2] + rho[3,3]]]) print(f"Marginal ρ_A:\n{rho_A}") print(f"Should be I/2: {np.allclose(rho_A, np.eye(2)/2)}") # Test multi-pair basis print("\nTesting multi-pair basis for 2 qubit pairs:") ops, indices = multi_pair_basis(n_pairs=2, d=2) print(f"Number of operators: {len(ops)} (expected 30)") print(f"Shape: {ops[0].shape} (expected (16,16))") print(f"Pair indices (first 5): {indices[:5]}") print(f"Pair indices (last 5): {indices[-5:]}")