sandbox/easystab/easyPYstab/Demo_EigenvalueProblem_Diffusion1D.py

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-

    Demonstration of how to solve eigenvalue problems with easyPYstab This program considers the 1D diffusion equation for T(x,t)

    d T / d t = mu d^2 T / d x^2 on the interval x in [0, L] with BC T(x=0,t) = T(x=L,t) = 0

    We look for eigenvalue/eigenmode solutions

    T(x,t) = (x)_n e^{lambda_n t}

    (_n, lambda_n) are the solutions of

    lambda_n _n = A _n

    where A is the discretized version of the second derivative operator

    Created on Thu Dec 11 08:17:37 2025 (starting from a previous program in Matlab/Octave)

    @author: fabred

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    
    import numpy as np
    import math
    import scipy.linalg as la
    import matplotlib.pyplot as plt
    
    import EasyStab as ES
    
    
    
    #### PART 0 : parameters and theoretical solution ####
    plt.close('all')
    N = 20         # number of gridpoints
    L = np.pi      # domain length
    mu = 1.0       # diffusion coefficient
    
    # theoretical eigenvalues
    n = np.arange(1, N+1)
    s_theory = - mu * (np.pi**2) / (L**2) * n**2
    
    
    
    #### PART I : Finite differences ####
    print("\nPart I : resolution with Finite Differences \n ")
    
    # discretization
    x,dx,dxx,w = ES.dif1D(Dtype='fd',N=N,parameters = {"a":0,"b":L})
    
    # build matrices A and B
    I = np.eye(N+1)
    A = mu * dxx.copy()
    B = I.copy()
    # Modify fist/last rows to impose BC
    for ib in [0, N]:
        B[ib, :] = 0.0
        A[ib, :] = 0.0
        A[ib, ib] = -1.0
    
    # solve generalized eigenvalue problem
    s,U = la.eig(A, B)
    # sort by decreasing real part (i.e. least negative eigenvalues first)
    idx = np.argsort(-np.real(s))
    s = s[idx]
    U = U[:, idx]
    
    
    # Prepare a figure with two subplots 
    fig, axes = plt.subplots(2, 1)
    
    # Plot theoretical vs computed eigenvalues
    axes[0].plot(np.real(s_theory), np.zeros_like(s_theory), 'ro', label='theory')
    axes[0].plot(np.real(s), np.imag(s), 'g+',markeredgewidth=2, label='computed (FD)')
    axes[0].set_xlim([-25.001, 1])
    axes[0].set_ylim([-1, 5])
    axes[0].set_xlabel('Re(λ)')
    axes[0].set_ylabel('Im(λ)')
    axes[0].set_title('Eigenvalues spectrum')
    axes[0].legend()
    axes[0].grid(True)
    
    # Plot first few eigenmodes
    for ind in range(min(5, U.shape[1])):
        axes[1].plot(x, np.real(U[:, ind]), '+-', label=f'mode {ind} (real part)')
        if np.any(np.imag(U[:, ind]) != 0):
            axes[1].plot(x, np.imag(U[:, ind]), '--', label=f'mode {ind} (imag part)')
    axes[1].set_xlabel('x')
    axes[1].set_ylabel('T(x)')
    axes[1].set_title('First eigenmodes')
    axes[1].grid(True)
    axes[1].legend()
    fig.tight_layout()
    fig.show()
    
    #### PART II : using Chebyshev discretization ####
    plt.pause(0.001)
    print("\nPart II : use Chebychev to compare \n ")
    input("Click 'enter' to continue...")
    
    # build Cnhebyshev discretization, then redo everything
    x,dx,dxx,w = ES.dif1D(Dtype="Cheb",N=N,parameters = {"a":0,"b":L})
    
    # build matrices A and B
    I = np.eye(N+1)
    A = mu * dxx.copy()
    B = I.copy()
    for ib in [0, N]:
        B[ib, :] = 0.0
        A[ib, :] = 0.0
        A[ib, ib] = -1.0
    # solve generalized eigenvalue problem, sort and filter
    s,U = la.eig(A, B)
    idx = np.argsort(-np.real(s))
    s = s[idx]
    U = U[:, idx]
    
    
    # Add results on the previous figure
    axes[0].plot(np.real(s), np.imag(s), 'bx',markeredgewidth=2, label='computed (Cheb)')
    axes[0].legend()
    for ind in range(min(5, U.shape[1])):
        axes[1].plot(x, np.real(U[:, ind]), 'x--')
    fig.show()
    
    
    
    #### PART III : time loop with initial condition ####
    plt.pause(0.001)
    print("\nPart III : construction of the solution of initial value problem with projection on eigenmodes \n ")
    input("Click 'enter' to continue...")
    
    # Time evolution from initial condition (random)
    n_modes = min(10, U.shape[1])
    a = np.random.randn(n_modes)*.4
    a[2] = 1.0
    
    t_list = np.linspace(0, 1.0, 51)
    for t in t_list:
        q = np.zeros(N+1)
        for j in range (n_modes): 
          if math.isfinite(s[j]):
            q += np.real(U[:,j] * (a[j] * np.exp(s[j] * t)))
        plt.clf()
        plt.plot(x, q,'b-', label='T(x,t)')
        # optionally plot each component
        for im in range(n_modes):
            plt.plot(x, (U[:, im] * a[im] * np.exp(s[im] * t)), 'r--', alpha=0.3)
        if t==0: 
            qrange = [min(q),max(q)]
            plt.ylim(qrange)
            plt.xlabel('x')
            plt.ylabel('T(x,0)')
            plt.title("Initial condition and Fourier components \n (type enter to start time loop)")
            plt.pause(0.01)
            input("Click 'enter' to start simulation...")
        plt.title(f"t = {t:.3f}")
        plt.ylabel('T(x,t)')
        plt.grid(True)
        plt.ylim(qrange)
        plt.pause(0.1)
    plt.show()