%% Compute and plot Zernike Polynomials

NumberOfGridPoints   = 100;            % Resolution

plotZernike(2, 0, NumberOfGridPoints)  % Defocus (Z2^0)
%%
plotZernike(2, 2, NumberOfGridPoints)  % Astigmatism (Z2^2)
%%
plotZernike(3, 1, NumberOfGridPoints)  % Coma (Z3^1, x-direction)
%%
plotZernike(3, -1, NumberOfGridPoints) % Coma (Z3^-1, y-direction)
%%
plotZernike(4, 0, NumberOfGridPoints)  % Spherical Aberration (Z4^0)

%% Compute and plot Aberrated PSF, Image
NumberOfGridPoints   = 256;                         % Number of grid points per side
PupilRadius          = 10E-3;                       % Radius of pupil [m]
Length               = 32E-6;                       % Total size of the grid [m]
GridSpacing          = Length / NumberOfGridPoints; % Grid spacing [m]
Wavelength           = 421E-9;                      % Optical wavelength [m]
FocalLength          = 3*PupilRadius;               % Focal Length

% Generate PSF
C                    = [1.5, 0.0, 0.1, -0.1, 0.5];  % Zernike coefficients
PSF                  = generateAberratedPSF(C, Wavelength, PupilRadius, NumberOfGridPoints, GridSpacing, FocalLength);

xvals                = (-NumberOfGridPoints/2 : NumberOfGridPoints/2-1) * GridSpacing * 1E6;
yvals                = (-NumberOfGridPoints/2 : NumberOfGridPoints/2-1) * GridSpacing * 1E6;
plotImage(xvals, yvals, PSF, 'PSF');

% Generate object
% Object               = generateObject(Wavelength, NumberOfGridPoints, GridSpacing, FocalLength);

% Convolve the object with the PSF to simulate imaging
% Image                = convolveObjectWithPSF(abs(Object).^2, PSF);
% plotImage(xvals, yvals, Image, 'Image of Object formed by convolving with the PSF');
%% Functions

function Z = computeZernikePolynomials(n, m, r, theta)
    % Zernike polynomial function for radial and angular coordinates (r, theta)
    % Input: 
    %   n - radial order
    %   m - azimuthal frequency
    %   r - radial coordinate (normalized to unit circle, 0 <= r <= 1)
    %   theta - angular coordinate (angle in radians)
    %
    % Output:
    %   Z - Zernike polynomial value at (r, theta)
    
    if n == 2 && m == 0
        % Defocus (Z2^0)
        Z = sqrt(3) * (2 * r.^2 - 1);
    elseif n == 2 && m == 2
        % Vertical Astigmatism (Z2^2)
        Z = sqrt(6) * r.^2 .* cos(2 * theta);
    elseif n == 3 && m == 1
        % Horizontal Coma (Z3^1)
        Z = sqrt(8) * (3 * r.^3 - 2 * r) .* cos(theta);
    elseif n == 3 && m == -1
        % Vertical Coma (Z3^-1)
        Z = sqrt(8) * (3 * r.^3 - 2 * r) .* sin(theta);
    elseif n == 4 && m == 0
        % Spherical Aberration (Z4^0)
        Z = sqrt(5) * (6 * r.^4 - 6 * r.^2 + 1);
    else
        % Default to zero if no known Zernike polynomial matches
        Z = 0;
    end
end

function plotZernike(n, m, NumberOfGridPoints)
    % n: radial order
    % m: azimuthal frequency
    % NumberOfGridPoints: number of points for plotting
    
    % Create a grid of (r, theta) coordinates
    [theta, r] = meshgrid(linspace(0, 2*pi, NumberOfGridPoints), linspace(0, 1, NumberOfGridPoints));
    
    % Calculate the Zernike polynomial for the given (n, m)
    Z = computeZernikePolynomials(n, m, r, theta);
    
    % Convert polar to Cartesian coordinates for plotting
    [X, Y] = pol2cart(theta, r);
    
    % Plot the Zernike polynomial using a surface plot
    figure(1)
    clf
    set(gcf,'Position',[50 50 950 750])
    set(gca,'FontSize',16,'Box','On','Linewidth',2);
    surf(X, Y, Z, 'EdgeColor', 'none');
    colormap jet;
    colorbar;
    title(sprintf('Zernike Polynomial Z_{%d}^{%d}', n, m), 'Interpreter', 'tex', 'FontSize', 16);
    xlabel('X', 'FontSize', 16);
    ylabel('Y', 'FontSize', 16);
    zlabel('Zernike Value', 'FontSize', 16);
    axis equal;
    shading interp;
end

function PSF = generateAberratedPSF(C, Wavelength, PupilRadius, NumberOfGridPoints, GridSpacing, FocalLength)
    % C is the vector of Zernike coefficients [C_defocus, C_astigmatism, C_coma, C_spherical, ...]
    % NumberOfGridPoints is the number of points for the grid (NxN grid)
    % PupilRadius is the radius of the pupil aperture
    
    % Create a grid of (x, y) of pupil-plane coordinates in Cartesian space
    [X, Y] = meshgrid((-NumberOfGridPoints/2 : NumberOfGridPoints/2-1) * GridSpacing);
    
    % Convert (x, y) to polar coordinates (r, theta)
    [theta, ~] = cart2pol(X, Y);

    % Pupil function
    P = generatePupil(Wavelength, PupilRadius, NumberOfGridPoints, GridSpacing, FocalLength);
    
    % Wavefront error from Zernike polynomials
    W = C(1) * computeZernikePolynomials(2, 0, P, theta) + ...  % Defocus (Z2^0)
        C(2) * computeZernikePolynomials(2, 2, P, theta) + ...  % Astigmatism (Z2^2)
        C(3) * computeZernikePolynomials(3, 1, P, theta) + ...  % Coma (Z3^1, x-direction)
        C(4) * computeZernikePolynomials(3, -1, P, theta) + ... % Coma (Z3^-1, y-direction)
        C(5) * computeZernikePolynomials(4, 0, P, theta);       % Spherical Aberration (Z4^0)
    
    W(P>1)       = 0;
    E            = exp(1i*2*pi*W); % Complex amplitude
    E(P>1)       = 0;              % Impose aperture size

    % Fourier transform of the pupil function with aberrations
    PSF          = abs(calculateFFT2(E)).^2;
    PSF          = PSF/sum(PSF(:));
end

function plotImage(xvals, yvals, image, titlestring)
    figure
    clf
    set(gcf,'Position',[50 50 950 750])
    set(gca,'FontSize',16,'Box','On','Linewidth',2);
    imagesc(xvals, yvals, image);
    colorbar;
    colormap jet;
    title(titlestring, 'FontSize', 16);
    xlabel('X', 'FontSize', 16);
    ylabel('Y', 'FontSize', 16);
end

function P_Norm = generatePupil(Wavelength, PupilRadius, NumberOfGridPoints, GridSpacing, FocalLength)
    [X,Y]       = meshgrid((-NumberOfGridPoints/2 : NumberOfGridPoints/2-1) * ((Wavelength * FocalLength) / (NumberOfGridPoints * GridSpacing)));
    P           = sqrt(X.^2 + Y.^2);
    P_Norm      = P/PupilRadius;  % Pupil normalized by aperture radius
    assert(max(P_Norm(:))>=sqrt(2),'Sampling is not sufficient to reconstruct the entire wavefront.');
end

function G = calculateFFT2(g)
    G = fftshift(fft2(ifftshift(g)));
end

%{

function g = calculateInverseFFT2(G)
    g = ifftshift(ifft2(fftshift(G)));
end

function obj = generateObject(Wavelength, NumberOfGridPoints, GridSpacing, FocalLength)
    % Image-plane coordinates
    [U, V] = meshgrid((-NumberOfGridPoints/2 : NumberOfGridPoints/2-1) * ((Wavelength * FocalLength) / (NumberOfGridPoints * GridSpacing)));
    obj    = ;
end

function C = convolveObjectWithPSF(A, B)
    C = calculateInverseFFT2(calculateFFT2(A) .* calculateFFT2(B));
end

%}

