function [psi]  = runGradientDescent(this,psi,Params,Transf,VDk,V,Observ)
    
    format long;

    AdditionalParams.GradientDescentMethod = this.GradientDescentMethod;
    SimulationMode                         = this.SimulationMode;

    switch this.GradientDescentMethod
        case 'HeavyBall'
            % Convergence Criteria:
            alpha           = 1E-5;
            beta            = 0.9;
            epsilon         = 1E-6;
        
            Observ.residual = 1; 
            Observ.res      = 1;
            psi_old         = psi;         % Previous psi value (for heavy-ball method)
            
            % Live Plotter
            if this.PlotLive
                Plotter.plotLive(psi,Params,Transf,Observ,SimulationMode,AdditionalParams)
                drawnow
            end
        
            % Minimization Loop
            for idx      = 1:this.MaxIterationsForGD
                
                % Compute gradient
                J            = compute_gradient(psi, Params, Transf, VDk, V);
        
                % Calculate chemical potential
                muchem       = real(inner_product(J, psi, Transf)) / inner_product(psi, psi, Transf);
                
                % Calculate residual and check convergence
                diff         = J - (muchem * psi);
                residual     = inner_product(diff, diff, Transf);
                
                if residual < epsilon
                    fprintf('Convergence reached at iteration %d\n', idx);
                    break;
                else
                
                    % Update psi using heavy-ball method
                    psi_new  = (1 + beta) * psi - alpha * J - beta * psi_old;
                    psi_old  = psi;
                    
                    % Normalize psi
                    Norm     = inner_product(psi_new, psi_new, Transf);
                    psi      = psi_new / sqrt(Norm);
                    
                    % Write output at specified intervals
                    if mod(idx,100) == 0        
                
                        % Collect change in energy
                        E               = this.Calculator.calculateTotalEnergy(psi,Params,Transf,VDk,V) / Norm;
                        Observ.EVec     = [Observ.EVec E];
                
                        % Collect chemical potentials
                        Observ.mucVec   = [Observ.mucVec muchem];
                
                        % Collect Normalized residuals
                        res             = this.Calculator.calculateNormalizedResiduals(psi,Params,Transf,VDk,V,muchem);
                        Observ.residual = [Observ.residual res];
                        Observ.res_idx  = Observ.res_idx + 1;
                        
                        if this.PlotLive
                            Plotter.plotLive(psi,Params,Transf,Observ,SimulationMode,AdditionalParams)
                            drawnow
                        end
            
                        save(sprintf(strcat(this.SaveDirectory, '/Run_%03i/psi_gs.mat'),Params.njob),'psi','muchem','Observ','Transf','Params','VDk','V');
                    end
                end
            end
            
            % Check if max iterations were hit without convergence
            last_iteration_number = idx;
            if last_iteration_number == this.MaxIterationsForGD
                fprintf('Max iterations reached without convergence. Final chemical potential: %.6f, Residual: %.6f\n', muchem, residual);
            else
                fprintf('Converged in %d iterations. Final chemical potential: %.6f\n', last_iteration_number, muchem);
            end
            
            % Change in Energy
            E              = this.Calculator.calculateTotalEnergy(psi,Params,Transf,VDk,V) / Norm;
            Observ.EVec    = [Observ.EVec E];
        
            save(sprintf(strcat(this.SaveDirectory, '/Run_%03i/psi_gs.mat'),Params.njob),'psi','muchem','Observ','Transf','Params','VDk','V');
            disp('Completed and saved!');
        
        case 'NonLinearCGD'
            
            % Convergence Criteria:
            epsilon           = 1E-13;
            
            % Iteration Counter:
            i                 = 1;
            Observ.residual   = 1; 
            Observ.res        = 1; 
            Observ.theta      = NaN;
        
            % Initialize the PrematureExitFlag to false
            PrematureExitFlag = false;
            
            % Live plotter
            if this.PlotLive
                Plotter.plotLive(psi,Params,Transf,Observ,SimulationMode,AdditionalParams)
                drawnow
            end
            
            % Minimization Loop
            while true
                % Compute gradient
                J                = compute_gradient(psi, Params, Transf, VDk, V);
                % Calculate chemical potential
                muchem           = real(inner_product(J, psi, Transf)) / inner_product(psi, psi, Transf);
                % Calculate residual
                residual         = J - (muchem * psi);
                
                % Energy convergence check every 100 steps
                if mod(i,100) == 0 && length(Observ.EVec) > 1
                    energydifference = abs(Observ.EVec(end) - Observ.EVec(end-1));
                    if energydifference <= epsilon
                        disp('Tolerance reached: Energy difference is below the specified epsilon.');
                        PrematureExitFlag = true;
                        break;
                    end
                end
            
                if i >= this.MaxIterationsForGD
                    disp('Maximum number of iterations for CGD reached.');
                    PrematureExitFlag = true;
                    break;
                end
                
                % Initialize search direction if first iteration
                if i == 1
                    d            = -residual;
                else % Compute beta via Polak-Ribiere and create new direction
                    residual_new = residual;
                    beta         = compute_beta(residual_new, residual_old, Transf);
                    d            = -residual_new + beta * p_old;
                end
                
                residual_old     = residual;
                proj             = inner_product(d, psi, Transf);
                p                = d - (proj * psi);
                p_old            = p;
                
                % Compute optimal theta to generate psi in the direction of minimum in the energy landscape
                theta            = compute_optimal_theta(p, muchem, psi, Params, Transf, VDk, V);
                
                % Update solution
                gamma            = 1 / sqrt(inner_product(p, p, Transf));
                psi              = (cos(theta).*psi) + (sin(theta).*(p*gamma));
                
                % Normalize psi
                Norm             = abs(inner_product(psi, psi, Transf));
                psi              = psi / sqrt(Norm);
                i                = i + 1;

                % Calculate chemical potential with new psi
                J                = compute_gradient(psi, Params, Transf, VDk, V);
                muchem           = real(inner_product(J, psi, Transf)) / Norm;
                
                if mod(i,100) == 0        
            
                    % Collect Energy value
                    E               = this.Calculator.calculateTotalEnergy(psi,Params,Transf,VDk,V) / Norm;
                    Observ.EVec     = [Observ.EVec E];
            
                    % Collect Chemical potential value
                    Observ.mucVec   = [Observ.mucVec muchem];
            
                    % Collect Normalized residuals
                    res             = this.Calculator.calculateNormalizedResiduals(psi,Params,Transf,VDk,V,muchem);
                    Observ.residual = [Observ.residual res];
                    Observ.res_idx  = Observ.res_idx + 1;

                     % Collect calculated thetas
                    Observ.theta    = [Observ.theta theta];
                    
                    % Live plotter
                    if this.PlotLive
                        Plotter.plotLive(psi,Params,Transf,Observ,SimulationMode,AdditionalParams)
                        drawnow
                    end
                    save(sprintf(strcat(this.SaveDirectory, '/Run_%03i/psi_gs.mat'),Params.njob),'psi','muchem','Observ','Transf','Params','VDk','V');
                end
            end
            
            % Check if loop ended prematurely
            if PrematureExitFlag
                disp('Optimizer ended prematurely without convergence to a minimum.');
            else
                fprintf('Minimum found! Number of Iterations for Convergence: %d\n\n', i);
            end
            
            % Change in Energy
            E              = this.Calculator.calculateTotalEnergy(psi,Params,Transf,VDk,V) / Norm;
            Observ.EVec    = [Observ.EVec E];
        
            save(sprintf(strcat(this.SaveDirectory, '/Run_%03i/psi_gs.mat'),Params.njob),'psi','muchem','Observ','Transf','Params','VDk','V');
            disp('Completed and saved!');
    end
end

%% Modules

function J      = compute_gradient(psi, Params, Transf, VDk, V)
    % === Precompute quantities ===
    absPsi2   = abs(psi).^2;        % Density |ψ|^2
    absPsi3   = abs(psi).^3;        % |ψ|^3
    N         = Params.N;

    % === Kinetic energy operator: -∇²/2 ===
    KEop      = 0.5 * (Transf.KX.^2 + Transf.KY.^2 + Transf.KZ.^2);
    HKin      = @(w) ifftn(KEop .* fftn(w));

    % === External potential (already scaled by ℏω₀) ===
    HV        = @(w) V .* w;

    % === Contact interaction: C|ψ|² ===
    C         = Params.gs * N;
    Hint      = @(w) (C * absPsi2) .* w;

    % === Dipole-dipole interaction: D U_dd * |ψ|² ===
    frho      = fftn(absPsi2);
    Phi_dd    = ifftn(frho .* VDk);
    D         = Params.gdd * N;
    Hddi      = @(w) (D * Phi_dd) .* w;

    % === Quantum fluctuations: Q|ψ|³ ===
    Q         = Params.gammaQF * N^(3/2);
    Hqf       = @(w) (Q * absPsi3) .* w;

    % === Total Hamiltonian operator ===
    H         = @(w) HKin(w) + HV(w) + Hint(w) + Hddi(w) + Hqf(w);

    % === Compute gradient ===
    J         = H(psi);
end

function g      = compute_g(psi, p, Params, VDk)
    % === Precompute necessary quantities ===
    rho       = real(psi .* conj(p));                  % Mixed density
    absPsi    = abs(psi);                              % Magnitude of wavefunction
    N         = Params.N;

    % === Contact interaction term ===
    C         = Params.gs * N;
    g_contact = @(w) (C * rho) .* w;

    % === Dipole-dipole interaction term ===
    rhotilde  = fftn(rho);
    Phi_dd    = ifftn(rhotilde .* VDk);                % Convolution with DDI kernel
    D         = Params.gdd * N;
    g_ddi     = @(w) (D * Phi_dd) .* w;

    % === Quantum fluctuation (LHY) correction ===
    Q         = Params.gammaQF * N^(3/2);              % LHY prefactor
    g_qf      = @(w) ((3/2) * Q * absPsi .* rho) .* w;

    % === Total effective operator applied to psi ===
    g_total   = @(w) g_contact(w) + g_ddi(w) + g_qf(w);

    % === Multiply by factor of 2, as per gradient expression ===
    g         = 2 * g_total(psi);
end

% Optimal direction
function theta  = compute_optimal_theta(p, muchem, psi, Params, Transf, VDk, V)
    
    Hpsi        = compute_gradient(psi, Params, Transf, VDk, V);
    Hp          = compute_gradient(p, Params, Transf, VDk, V);
    g           = compute_g(psi, p, Params, VDk);
    gamma       = 1 / sqrt(inner_product(p, p, Transf));
    numerator   = gamma * real(inner_product(p, Hpsi, Transf));
    denominator = muchem - (gamma^2 * (real(inner_product(p, Hp, Transf)) + real(inner_product(g, p, Transf)))); 

    theta       = numerator / denominator;
end

% Optimal step size via Polak-Ribiere
function beta   = compute_beta(residual_new, residual_old, Transf)
    beta        = inner_product(residual_new, (residual_new - residual_old), Transf) / inner_product(residual_old, residual_old, Transf);
    beta        = max(0, real(beta));
end

function s      = inner_product(u, v, Transf)
    s           = sum(conj(u(:)) .* v(:)) * Transf.dx * Transf.dy * Transf.dz;
end
