function [r, gd, gdd] = stackgdd(ks, ds, n0, ns, dns, ddns, theta, pol, approx)
% STACKGDD  Calculate complex reflection, group delay and GDD of
% arbitrary dielectric structure using exact analytic methods,
% including all relevent material derivatives.
%
% [r, gd, gdd] = stackgdd(ks, ds, n0, ns, dns, ddns, theta, pol)
%
% Input:
% ks: wavenumber column vector (1/um)
% ds: thickness row vector (um)
% ns(length(ks),length(ds)): index matrix, from first layer to substrate
% dns(length(ks),length(ds)): first order index derivatives at ks
% ddns(length(ks),length(ds)): second order index derivatives at ks
% n0: initial index, assumed to be dispersionless
% theta: incidence angle (radians)
% pol: {'TE','TM'}
% approx: {'none', 'nodp'}  (for testing purposes)
%
% Output:
% r(ks): complex reflection coefficient
% gd(ks): group delay (fs)
% gdd(ks): first order dispersion (fs^2)

% Implemenation Notes:
%
% This code is optimized more for simplicity and readability than speed.
%
% Each T matrix goes from the end of the last layer through the Lth layer.
% Only the first two elements of the transfer matrix are kept
% at each point due to the symmetry of T matrices.
%
% All matrices are arranged with wavenumbers in columns.


% Initialization and input handling.
if (nargin < 8) pol = 'TE'; end
if (nargin < 7) theta = 0; end
if (nargin < 9) approx = 'none'; end
nk = length(ks);  % number of wavelengths
n = length(ds);  % number of layers
k = ks(:);  % ensure k is column vector of wavenumbers
ds = [NaN, ds(:).', 0];  % create dummy layers for outside and substrate
ns = [n0*ones(nk,1), ns];
if (dns == 0)
  dns = zeros(nk, n);
else
  dns = [zeros(nk,1), dns];
end
if (ddns == 0)
  ddns = zeros(nk, n);
else
  ddns = [zeros(nk,1), ddns];
end
c = .2997924580;  % um/fs

% Initialize loop variables.
n0sinth2 = (n0*sin(theta))^2;
T = [ones(nk,1), zeros(nk,1)];  % initial matrix
dT = zeros(nk,2);  % initial 1st derivative matrix
ddT = zeros(nk,2);
neffL = sqrt(ns(:,1).^2 - n0sinth2);  % neff of ambient medium (dispersionless)
dneffL = zeros(nk,1);
ddneffL = zeros(nk,1);
%%% Step through structure, calculating all wavelengths in parallel.
for L = 2:n,
  % Initialize local variables.
  d = ds(L);
  id = 1i*d;  % layer thickness times i

  % Calculate effective index quantities.
  neffLL = neffL; dneffLL = dneffL; ddneffLL = ddneffL;
  nL = ns(:,L); dnL = dns(:,L); ddnL = ddns(:,L);
  neffL = sqrt(nL.^2 - n0sinth2);
  dneffL = nL.*dnL./neffL;
  ddneffL = (dnL.^2 - dneffL.^2 + nL.*ddnL)./neffL.^2;

  % Calculate pTE (needed regardless of polarization).
  pTE = neffLL./neffL;
  dpTE = (dneffLL - pTE.*dneffL)./neffL;
  ddpTE = (ddneffLL - 2*dneffL.*dpTE - pTE.*ddneffL)./neffL;

  % Calculate p = pTM or p = pTE as needed.
  if (pol == 'TM')
    % Calculate p0, which is just p (as above) for normal incidence.
    LL = L - 1;
    nLL = ns(:,LL); dnLL = dns(:,LL); ddnLL = ddns(:,LL);
    p0 = nLL./nL;
    dp0 = (dnLL - p0.*dnL)./nL;
    ddp0 = (ddnLL - 2*dnL.*dp0 - p0.*ddnL)./nL;

    % Calculate p = pTM from p0 and pTE.
    p = pTE./p0.^2;
    dp = (p0.*dpTE - 2*pTE.*dp0)./p0.^3;
    ddp = (6*pTE.*dp0.^2 - 4*p0.*dp0.*dpTE - 2*p0.*pTE.*ddp0 + ...
      p0.^2.*ddpTE)./p0.^4;
  else  % pol == 'TE'
    p = pTE;
    dp = dpTE;
    ddp = ddpTE;
  end
  if (approx == 'nodp')
    dp = 0;
    ddp = 0;
  end
  pp = 1 + p;
  pm = 1 - p;

  % Find transfer matrices for current layer.
  ephi = exp(-id*k.*neffL);  % phasor of layer
  TL = [ephi.*pp, ephi.*pm];  % current layer matrix elements
  dterm = -id*(neffL + k.*dneffL);
  dTL = [ephi.*(pp.*dterm + dp), ephi.*(pm.*dterm - dp)];  % /2 ?
  ddTL(:,1) = ephi.*(d*(-pp.*(2i.*dneffL + d*(neffL + k.*dneffL).^2) - ...
    2i*(neffL + k.*dneffL).*dp - 1i*k.*pp.*ddneffL) + ddp)/2;
  ddTL(:,2) = ephi.*(d*(-d*neffL.^2.*pm + ...
    2*neffL.*(-d*k.*pm.*dneffL + 1i*dp) + ...
    dneffL.*(-pm.*(2i + d*k.^2.*dneffL) + 2i*k.*dp) - ...
    1i*k.*pm.*ddneffL) - ddp)/2;

  % Calculate new matrices for full structure up to L.
  Tnew = tmatrixprod(TL, T);
  dTnew = tmatrixprod(TL, dT) + tmatrixprod(dTL, T);
  ddTnew = tmatrixprod(ddTL, T) + 2*tmatrixprod(dTL, dT) + ...
    tmatrixprod(TL, ddT);
  T = Tnew; dT = dTnew; ddT = ddTnew;
end

% Calculate complex reflectance and group delay from total T matrices.
r = -T(:,2)./T(:,1);  % complex reflection coefficient
dr = (T(:,2).*dT(:,1) - dT(:,2).*T(:,1))./T(:,1).^2;
ddr = (T(:,2).*(-2*dT(:,1).^2 + T(:,1).*ddT(:,1)) + ...
  T(:,1).*(2*dT(:,1).*dT(:,2) - T(:,1).*ddT(:,2)))./T(:,1).^3;
dphi = (imag(dr).*real(r) - real(dr).*imag(r))./abs(r).^2;
dmag = (real(dr).*real(r) + imag(dr).*imag(r))./abs(r);
ddphi = (imag(ddr).*real(r) - real(ddr).*imag(r))./abs(r).^2 - ...
  2*dmag.*dphi./abs(r);
gd = dphi/c;
gdd = ddphi/c^2;


function c = tmatrixprod(b, a)
% Compute the top row of the products of two T matrices given their top
% rows, as in c = b*a. Parallelized along columns.
c = [a(:,1).*b(:,1) + conj(a(:,2)).*b(:,2), ...
  a(:,2).*b(:,1) + conj(a(:,1)).*b(:,2)];
