\(\frac{\partial}{\partial s}q_1 + k_2 q_3 - k_3 q_2 = \lambda \omega_2\),

\(\frac{\partial}{\partial s}q_2 + k_3 q_1 - k_1 q_3 = -\lambda \omega_1\),

\(\frac{\partial}{\partial s}q_3 + k_1 q_2 - k_2 q_1 = -\frac{\partial}{\partial t} \lambda\),

\(\frac{\partial}{\partial t} k_1-\frac{\partial}{\partial s}\omega_1=k_2\omega_3-k_3\omega_2\),

\(\frac{\partial}{\partial t} k_2-\frac{\partial}{\partial s}\omega_2=k_3\omega_1-k_2\omega_3\),

\(\frac{\partial}{\partial t} k_3-\frac{\partial}{\partial s}\omega_3=k_1\omega_2-k_2\omega_1\)

%% Kinematics of Surface Accretive Growth
clear; figure(1); % close all;
mycross = @(a,b) [a(:,2).*b(:,3)-a(:,3).*b(:,2), ...
                  a(:,3).*b(:,1)-a(:,1).*b(:,3), ...
                  a(:,1).*b(:,2)-a(:,2).*b(:,1)];
% vectnormalize = @(v) v./sqrt(sum(v.^2,2));
rad2deg = 180/pi;
explicit = true;
% explicit = false;
%% shape parameters
Nt = 1e3;
dt = 1e-3;
rgap = 10; % need to divide 360
isHalf = false;
s = linspace(0,359,360)'; % angles in one revolution
r0 = [cosd(s),sind(s),zeros(size(s))]/10; % generating shape (circle)

% Cylinder
q1 = @(t) 0;
q2 = @(t) 1;
q3 = @(t) 0;

% Twisted Cone
% q1 = @(t) -.5;
% q2 = @(t) 1;
% q3 = @(t) .5;

% Logarithmic Coil
% dt = 1e-2;
% q1 = @(t) -.1;
% q2 = @(t) (pi+1*cosd(s));
% q3 = @(t) 0;

% Turitella 1
% dt = 1e-2;
% rgap = 2;
% R = .1 + 0.0002*cosd(20*s);
% r0 = R.*[cosd(s), sind(s), zeros(size(s))];
% q1 = @(t) -.1*exp(-.1*t^2);
% q2 = @(t) 1+1*cosd(s);
% q3 = @(t) .5;

% Turitella 2
% dt = 1e-2;
% rgap = 10;
% r0 = [cosd(s),sind(s),zeros(size(s))]/10;
% q1 = @(t) -.1*exp(-.1*t^2);
% q2 = @(t) 1+1*cosd(s-1.5*t*rad2deg);
% q3 = @(t) 0;

% Horn
% Nt = 5e2;
% dt = 5e-3;
% k = @(t) 0.25+0.02*t;
% r0 = [cosd(s),sind(s),zeros(size(s))]/10;
% rgap = 90;
% q1 = @(t) 0.04 - k(t)*sind(s);
% q2 = @(t) 0.75 + 0.2*cosd(s);
% q3 = @(t) k(t)*cosd(s);

% Conch
% Nt = 5e2;
% dt = 1e-2;
% rgap = 2;
% isHalf = true;
% R = 1 + 0.01*cosd(20*s);
% r0 = R.*[2*cosd(s), sind(s), zeros(size(s))];
% q1 = @(t) -abs(R.*cosd(2*s));
% q2 = @(t) 0.1+8*R.*sind(s);
% q3 = @(t) R.*sind(s);

% Bivalve
% dt = 1e-2;
% rgap = 2;
% isHalf = true;
% R = 1 + 0.05*cosd(16*s);
% r0 = R.*[cosd(s), sind(s), zeros(size(s))];
% q1 = @(t) -3;
% q2 = @(t) .1 + 5*(1-.1*t)*R.*sind(s);
% q3 = @(t) 0;

% Ammonite
% dt = 1e-3;
% isHalf = false;
% Nt = 2.8e3;
% rgap = 10;
% r0 = [cosd(s),sind(s),zeros(size(s))]/10;
% kg = @(t) 1+4*cos(150*t); % should be k_g of eqn (7.66)-(7.67)
% q1 = @(t) -.13*(1-1*exp(-10*t)).*kg(t);
% q2 = @(t) (pi+(1.05+2*exp(-t))*cosd(s));
% q3 = @(t) -.05*exp(-.1*t^2);
%% explicit planar shape invariant stepping
if explicit
r = r0;
track1 = zeros(Nt,3);
track2 = zeros(Nt,3);
track1(1,:) = r(1,:);
track2(1,:) = r(181,:);
lambda = 1;
if isHalf
  Nr = 180;
  shell.v = r((0:rgap:Nr)+1,:);
  f_1layer = [1:Nr/rgap; 2:Nr/rgap+1];
else
  Nr = 360;
  shell.v = r((0:rgap:Nr-1)+1,:);
  f_1layer = [1:Nr/rgap; 2:Nr/rgap 1];
end
shell.f = [];
shell.t = zeros(Nr/rgap+isHalf*1,1);

% figure('position',[0 0 1e3 1e3]);
clf; set(gcf,'position',[0 0 1e3 1e3]);
hold all; axis tight; axis equal; axis off; view(3); camlight; material dull
light('Position',[-1 0 0],'Style','local')
light('Position',[270 -10 -1],'Style','local')
ph = patch('faces',shell.f,'vertices',shell.v,'facevertexCData',shell.t,...
  'facecolor','interp','edgecolor','none','FaceLighting','gouraud');
if ~isHalf
t1h = plot3(track1(:,1),track1(:,2),track1(:,3),'k','linewidth',2);
t2h = plot3(track2(:,1),track2(:,2),track2(:,3),'r','linewidth',2);
end

for i = 1:Nt
% for i = 1
  lambda_old = lambda;
  medial_old = mean(r,1);
%   basis_old = basis;
  
  t = (i-1)*dt;
  [T,N,B] = FrenetSerret(r,s,false); % not assuming planar director frames yet
  dr = q1(t).*N + q2(t).*B + q3(t).*T;
  rnew = r + dr*dt; % naive stepping (very unstable)
  
  % recalculate assuming planarity and shape invariance
  medial = mean(rnew,1); % new medial position
  lambda = norm(diff(rnew([0 180]+1,:),1))/2; % naive dilation factor
  lnodilate = r + (q2(t).*B + q3(t).*T)*dt; % growth without dilation
  lnodilate = norm(diff(lnodilate([0 180]+1,:),1))/2; % size error from finite time stepping
  lambda = lambda/lnodilate*lambda_old; % corrected dilation
  basis = vectnormalize(rnew([0 90]+1,:) - medial); % new planar basis
  r = lambda*(basis(1,:).*r0(:,1) + basis(2,:).*r0(:,2)) + medial;
  
  track1(i,:) = r(1,:);
  track2(i,:) = r(181,:);
  if isHalf
    shell.v = [shell.v; r((0:rgap:Nr)+1,:)];
  else
    shell.v = [shell.v; r((0:rgap:Nr-1)+1,:)];
  end
  shell.f = [shell.f; [f_1layer; flipud(f_1layer)+Nr/rgap]'+Nr*(i-1)/rgap];
  shell.t = [shell.t; ones(Nr/rgap+isHalf*1,1)*t];
  if ~mod(i,1e2) || i==1
    if ~isHalf
    set(t1h,'xdata',track1(:,1),'ydata',track1(:,2),'zdata',track1(:,3));
    set(t2h,'xdata',track2(:,1),'ydata',track2(:,2),'zdata',track2(:,3));
    end
    set(ph,'faces',shell.f,'vertices',shell.v,'facevertexCData',shell.t);
    drawnow;
  end
end
else
%% direct numerical integration (can be unstable)
[tt,rsol] = ode45(@(t,y) growth(t,y,s,q1,q2,q3),linspace(0,Nt*dt,Nt),reshape(r0,[],1));
% [tt,rsol] = ode15s(@(t,y) growth(t,y,s,q1,q2,q3),linspace(0,Nt*dt,Nt),reshape(r0,[],1));

r = reshape(rsol(1,:),[],3);
track1 = zeros(Nt,3);
track2 = zeros(Nt,3);
track1(1,:) = r(1,:);
track2(1,:) = r(181,:);
lambda = 1;
if isHalf
  Nr = 180;
  shell.v = r((0:rgap:Nr)+1,:);
  f_1layer = [1:Nr/rgap; 2:Nr/rgap+1];
else
  Nr = 360;
  shell.v = r((0:rgap:Nr-1)+1,:);
  f_1layer = [1:Nr/rgap; 2:Nr/rgap 1];
end
shell.f = [];
shell.t = zeros(Nr/rgap+isHalf*1,1);

% figure('position',[0 0 1e3 1e3]);
clf; set(gcf,'position',[0 0 1e3 1e3]);
hold all; axis tight; axis equal; axis off; view(3); camlight; material dull
light('Position',[-1 0 0],'Style','local')
ph = patch('faces',shell.f,'vertices',shell.v,'facevertexCData',shell.t,...
  'facecolor','interp','edgecolor','none','FaceLighting','gouraud');
t1h = plot3(track1(:,1),track1(:,2),track1(:,3),'k','linewidth',2);
t2h = plot3(track2(:,1),track2(:,2),track2(:,3),'r','linewidth',2);

for i = 1:Nt
  t = tt(i);
  r = reshape(rsol(i,:),[],3);
  track1(i,:) = r(1,:);
  track2(i,:) = r(181,:);
  if isHalf
    shell.v = [shell.v; r((0:rgap:Nr)+1,:)];
  else
    shell.v = [shell.v; r((0:rgap:Nr-1)+1,:)];
  end
  shell.f = [shell.f; [f_1layer; flipud(f_1layer)+Nr/rgap]'+Nr*(i-1)/rgap];
  shell.t = [shell.t; ones(Nr/rgap+isHalf*1,1)*t];
  if ~mod(i,1e2) || i==1
    if ~isHalf
    set(t1h,'xdata',track1(:,1),'ydata',track1(:,2),'zdata',track1(:,3));
    set(t2h,'xdata',track2(:,1),'ydata',track2(:,2),'zdata',track2(:,3));
    end
    set(ph,'faces',shell.f,'vertices',shell.v,'facevertexCData',shell.t);
    drawnow;
  end
end
end

function [drr] = growth(t,rr,s,q1,q2,q3)
  r = reshape(rr,[],3);
  [T,N,B] = FrenetSerret(r,s,true);
  dr = q1(t).*N + q2(t).*B + q3(t).*T;
  drr = reshape(dr,[],1);
end
%% utility functions
function [T,N,B] = FrenetSerret(r,s,planar)
mycross = @(a,b) [a(:,2).*b(:,3)-a(:,3).*b(:,2), ...
                  a(:,3).*b(:,1)-a(:,1).*b(:,3), ...
                  a(:,1).*b(:,2)-a(:,2).*b(:,1)];
% vectnormalize = @(v) v./sqrt(sum(v.^2,2));
% mygrad = @(v,s) (circshift(v,-2)-v)./mod((circshift(s,-2)-s),360);
mygrad = @(v,s) (circshift(v,-1)-circshift(v,1))./mod((circshift(s,-1)-circshift(s,1)),360);

T = zeros(size(r)); N = zeros(size(r));
for i = 1:3
  T(:,i) = mygrad(r(:,i),s);
end
T = vectnormalize(T);

if planar % assumes planarity
  medial = mean(r,1);
  b = mean(vectnormalize(mycross(r(91,:)-medial,r(1,:)-medial)),1); % assume planarity
  N = vectnormalize(mycross(T,b));
  B = repmat(b,numel(s),1);
else
  for i = 1:3
    N(:,i) = mygrad(T(:,i),s);
  end
  N = vectnormalize(N);
  B = mycross(T,N);
end

end

function vhat = vectnormalize(v)
  vnorm = sqrt(sum(v.^2,2));
  vhat = v./vnorm;
  vhat(vnorm==0,:) = 0;
end

Have fun playing with this code!