import numpy as np
from scipy.linalg import sqrtm
from scipy.stats import unitary_group
from tqdm import tqdm

def random_pure_state(dim_a, dim_b):
    """
    Generate a random pure state for a bipartite system.

    The random pure state is uniformly distributed by the Haar (Fubini-Study) measure on the unit sphere $S^{dim_a * dim_b - 1}$. (Invariant under the unitary group $U(dim_a) \times U(dim_b)$)
    
    Args:
        dim_a (int): Dimension of subsystem A
        dim_b (int): Dimension of subsystem B
        
    Returns:
        numpy.ndarray: Random pure state vector of shape (dim_a * dim_b,)
    """
    # Total dimension of the composite system
    dim_total = dim_a * dim_b
    
    # Generate non-zero random complex vector
    while True:
        state = np.random.normal(size=(dim_total,)) + 1j * np.random.normal(size=(dim_total,))
        if np.linalg.norm(state) > 0:
            break
    
    # Normalize the state
    state = state / np.linalg.norm(state)
    
    return state

def von_neumann_entropy_bipartite_pure_state(state, dim_a, dim_b):
    """
    Calculate the von Neumann entropy of the reduced density matrix.
    
    Args:
        state (numpy.ndarray): Pure state vector
        dim_a (int): Dimension of subsystem A
        dim_b (int): Dimension of subsystem B
        
    Returns:
        float: Von Neumann entropy
    """
    # Reshape state vector to matrix form
    state_matrix = state.reshape(dim_a, dim_b)
    
    # Calculate reduced density matrix of subsystem A
    rho_a = np.dot(state_matrix, state_matrix.conj().T)
    
    # Calculate eigenvalues
    eigenvals = np.linalg.eigvalsh(rho_a)
    
    # Remove very small eigenvalues (numerical errors)
    eigenvals = eigenvals[eigenvals > 1e-15]
    
    # Calculate von Neumann entropy
    entropy = -np.sum(eigenvals * np.log2(eigenvals))
    
    return np.real(entropy)

def sample_and_calculate(dim_a, dim_b, n_samples=1000):
    """
    Sample random pure states (generate random co) and calculate their von Neumann entropy.
    
    Args:
        dim_a (int): Dimension of subsystem A
        dim_b (int): Dimension of subsystem B
        n_samples (int): Number of samples to generate
        
    Returns:
        numpy.ndarray: Array of entropy values
    """
    entropies = np.zeros(n_samples)
    
    for i in tqdm(range(n_samples), desc=f"Sampling states (d_A={dim_a}, d_B={dim_b})", leave=False):
        state = random_pure_state(dim_a, dim_b)
        entropies[i] = von_neumann_entropy_bipartite_pure_state(state, dim_a, dim_b)
    
    return entropies

# Example usage:
if __name__ == "__main__":
    # Example: 2-qubit system
    dim_a, dim_b = 50,100
    
    # Generate single random state and calculate entropy
    state = random_pure_state(dim_a, dim_b)
    entropy = von_neumann_entropy_bipartite_pure_state(state, dim_a, dim_b)
    print(f"Single state entropy: {entropy}")
    
    # Sample multiple states
    entropies = sample_and_calculate(dim_a, dim_b, n_samples=1000)
    print(f"Expected entropy: {np.mean(entropies)}")
    print(f"Theoretical entropy: {np.log2(max(dim_a, dim_b))}")
    print(f"Standard deviation: {np.std(entropies)}")
