function censusapp2024(arg)
%CENSUSAPP2024 Predict the US population beyond 2020.
% This example began as an exercise in "Computer Methods for
% Mathematical Computations", by Forsythe, Malcolm and Moler,
% published by Prentice-Hall in 1977.  The software was written
% in Fortran, the predominate technical programming language at 
% the time.  Since 1970, the data has been updated every ten years.
% Today, MATLAB makes it easier to  vary parameters and visualize
% results, but the underlying mathematical principles are unchanged:
%
%    Using polynomials of even modest degree to predict
%    the future by extrapolating data is a risky business.
%
% The data are from the decennial census of the United States for the
% years 1900 to 2020.  The task is to extrapolate beyond 2020.
%
% In addition to polynomials of various degrees, you can choose
% interpolation by a cubic spline or two shape-preserving Hermite 
% cubics, and least squares fits by an exponential, a logistic curve
% and an exponential of an exponential.
%
%   spline  C2, "not-a-knot".
%   pchip   C1, shape-preserving.
%   makima  C1, modified Akima.
%
%   exponential  exp(b*t+c)
%   logistic     a./(1+exp(-b*(t-c)))
%   gompertz     a*exp(-b*exp(-c*t))
%
% Error estimates account for errors in the data, but not in the model.
% R^2 is the proportion of variation in data predicted by the model.
%
% Usage:
%   censusapp2024           Figure width 800.
%   censusapp2024(figsize)  Figure width 320 + 480*figsize.
%   censusapp2024(method)   Callback
%
%   Date: 19-May-2024.
%   Copyright 2014-2024 Cleve Moler

% Census data for 1900 to 2020.
t = (1900:10:2020)';
p = [ 75.995  91.972 105.711 123.203 131.669 150.697 ...
     179.323 203.212 226.505 249.633 281.422 308.746 ...
     331.449]';

x = (1890:2040)';      % Evaluation years
w = 2030;              % Extrapolation target
z = 343;               % Guess w population
d = 0;                 % Polynomial degree
r2 = 0;                % R^2
level = 0;             % Noise level

models = {'census','polynomial','pchip','spline','makima', ...
          'exponential','logistic','gompertz'}';

if nargin == 0 || nargin == 1 && isscalar(arg)
   if nargin == 0
       figsize = 1;
   else
       figsize = arg;
   end
   model = 'census';
   h = init_graphics;
   noise = rand*randn(size(p));
   set(gcf,'UserData',{p,h,noise,figsize});
else
   model = arg;
   fud = get(gcf,'UserData');
   [p,h,noise,figsize] = deal(fud{:});
   level = h.noise.UserData;
   p = p + level*noise;
   h.plot(1).YData = p; 
end

% Update plot with new model

w = h.predict.UserData;
x = (1890:max(2040,w+30))';
switch model  
   case 'census'
      y = NaN*x;
      z = 324.790 + 2.64*(w-2017) + 0.0042*(w-2017)^2;
   case 'polynomial'
      [y,z,r2] = poly(h,t,p,x,w);
   case {'pchip','spline','makima'}
      [y,z] = hermite(model,t,p,x,w);
      r2 = 1;  % Exact fit
   case {'exponential','logistic','gompertz'}
      [y,z,r2] = expo(model,t,p,x,w);
end

update_graphics(model,h,x,y,w,z,r2)

% ------------------------------------------------

    function h = init_graphics
        figw = 320 + 480*figsize;
        figh = 3/4*figw;
        fontsize = 5 + 9*figsize;
        fig = gcf;
        fig.Position = [300 150 figw figh];
        fig.Name = 'censusapp2024';
        fig.MenuBar = 'none';
        fig.NumberTitle = 'off';
        shg
        clf
        h.plot = plot(t,p,'bo', x,0*x,'k-', ...
           w,0,'.', [x;NaN;x],[x;NaN;x],'m:');
        lw = 1+.06*(fontsize-5);
        h.plot(1).LineWidth = lw;
        h.plot(1).MarkerSize = fontsize-2;
        h.plot(2).LineWidth = lw;
        h.plot(4).LineWidth = lw;
        h.plot(3).MarkerSize = 3*fontsize;
        h.plot(3).Color = [0 2 0]/3;  % darkgreen
        h.plot(4).Color = [2 0 2]/3;  % darkmagenta;
        h.title = ['Predict U.S. Population in ' int2str(w)];
        axis([1890 2060 0 450]);

        x0 = .18; % Upper left corner of gui
        y0 = .82;
        dx = .08;
        dy = .07;
        dw = .05;
        h.model = uicontrol('style','popup','string',models, ...
            'units','normal','position',[x0 y0-3*dy .24 dw], ...
            'fontsize',fontsize,'fontweight','bold','callback',@model_cb);
        h.extrap = text(w,z+20,'predict','horiz','right', ...
            'color',[0 2 0]/3,'fontsize',fontsize,'fontweight','bold');
        h.predict = uibut('push',sprintf('predict = %d',w), ...
            [x0+dw y0 .18 dw], [],[],w);
        h.noise = uibut('push',sprintf('noise = %d',level), ...
            [x0+dw y0-dy .18 dw], [],[],level);
        h.deg = uibut('push',sprintf('degree = %d',d), ... 
            [x0+dw y0-2*dy .18 dw], [],[],d); 
        uibut('push','-', [x0 y0 dw dw], @predict_cb,[],[]);
        uibut('push','+', [x0+3*dx y0 dw dw], @predict_cb,[],[]);        
        uibut('push','-', [x0 y0-dy dw dw], @noise_cb,[],[]);
        uibut('push','+', [x0+3*dx y0-dy dw dw], @noise_cb,[],[]);
        h.minus = uibut('push','-', [x0 y0-2*dy dw dw], @degree_cb,[],[]);
        h.plus = uibut('push','+', [x0+3*dx y0-2*dy dw dw], @degree_cb,[],[]);
        h.r2txt = uibut('push','R^2',[x0+.06 y0-4*dy .18 dw],[],[],1);
        h.r2 = uibut('toggle','R^2',[x0 y0-4*dy dw dw],@r2_cb,1,[]);
        h.err = uibut('toggle','errs',[x0 y0-5*dy dw .05],@errs_cb,0,[]);
        if figsize >= 0.5
            uibut('push','info',[.91 .90 .08 .06],@info_cb,[],[]);
            uibut('push','help',[.91 .82 .08 .06],@help_cb,[],[]);
            uibut('push','restart',[.91 .74 .08 .06],@restart_cb,[],[]);
            ylabel('Millions')
        end
    end

    function update_graphics(model,h,x,y,w,z,r2)

        % Update plot

        h.plot(2).XData = x;
        h.plot(2).YData = y;
        h.plot(3).Visible = 'on';
        h.plot(3).XData = w;
        h.plot(3).YData = z;
        h.title = title(['Predict U.S. Population in ' int2str(w)]);
        h.title.FontSize = h.predict.FontSize+2;
        ax = axis;
        h.extrap.Position = [w,min(max(z+20,30),ax(4)-30)];
        h.extrap.Visible = 'on';
        h.extrap.String = sprintf('%6.1f',z);
        
        % Update controls
        
        switch model
           case 'census'
               h.extrap.String = 'predict';
        end

        % Display error estimates if requested
        
        if h.err.Value == 1
           errest = errorestimates(model,t,p,x,y);
           h.plot(4).Visible = 'on';
           h.plot(4).XData = [x;NaN;x];
           h.plot(4).YData = errest;
        else
           h.plot(4).Visible = 'off';
        end
        
        if h.r2.Value == 1 
           h.r2txt.String = sprintf('%8.6f',r2);
        else
           h.r2txt.Visible = 'off';
        end 
    end
    
    function h = uibut(style,string,position,callback,value,userdata)
        h = uicontrol(Style = style, ...
            String = string, ...
            Units = "normalized", ...
            Position = position, ...
            Callback = callback, ...
            Value = value, ...
            Userdata = userdata, ...
            Fontsize = 5 + 9*figsize, ...
            FontWeight = "bold", ...
            BackgroundColor = "w");
    end

    function [y,z,r2] = poly(h,t,p,x,w)
        d = h.deg.UserData;
        s = (t-1960)/60;   
        c = polyfit(s,p,d);
        u = polyval(c,s);
        s = (x-1960)/60;   
        y = polyval(c,s);
        s = (w-1960)/60;   
        z = polyval(c,s);
        r2 = 1-norm(p-u)^2/norm(p-mean(p))^2;
    end

    function [y,z] = hermite(model,t,p,x,w)
        switch model
           case 'pchip'
              y = pchip(t,p,x);
              z = pchip(t,p,w);
           case 'spline'
              y = spline(t,p,x);
              z = spline(t,p,w);
           case 'makima'
              y = makima(t,p,x);
              z = makima(t,p,w);
        end
    end

    function [y,z,r2] = expo(model,t,p,x,w)
        switch model
            case 'exponential'
                c = polyfit(log(t),log(p),1);
                y = exp(polyval(c,log(x)));
                z = exp(polyval(c,log(w)));
                u = exp(polyval(c,log(t)));
            case 'logistic'
                logistic = @(a,b,c,t) a./(1+exp(-b*(t-c)));
                fun = @(a) logistic_fit(a,t,p);
                a = fminbnd(fun,500,1000);
                [~,b,c] = logistic_fit(a,t,p);
                y = logistic(a,b,c,x);
                z = logistic(a,b,c,w);
                u = logistic(a,b,c,t);
            case 'gompertz'
                gompertz = @(a,b,c,t) a*exp(-b*exp(-c*t));
                fun = @(a)gompertz_fit(a,t,p);
                a = fminbnd(fun,1000,6000);
                [~,b,c] = gompertz_fit(a,t,p);
                y = gompertz(a,b,c,x);
                z = gompertz(a,b,c,w);
                u = gompertz(a,b,c,t);
        end
        r2 = 1-norm(p-u)^2/norm(p-mean(p))^2;
    end

    function [rnorm,b,c] = logistic_fit(a,t,p)
    % LOGISTIC_FIT.  Objective function for one dimensional search.
    % [rnorm,b,c] = logistic_fit(a,t,p)
    %    fits p with a./(1+exp(-b*(t-c))).
    %    Returns norm(fit - p).
    % Uses linear fit of log(a./p-1) by -b*(t-c).
    % Call a = fminbnd(@(a)logistic_fit(a,t,p),a_1,a_2).
        logistic = @(a,b,c,t) a./(1+exp(-b*(t-c)));
        A = [t ones(size(t))];
        rhs = log(a./p-1);
        f = A\rhs;
        b = -f(1);
        c = f(2)/b;
        r = logistic(a,b,c,t) - p;
        rnorm = norm(r);
    end

    function [rnorm,b,c] = gompertz_fit(a,t,p)
    %GOMPERTZ_FIT.  Objective function for one dimensional search.
    % [rnorm,b,c] = gompertz_fit(a,t,p)
    %    fits p with a*exp(-b*exp(-c*t))).
    %    Returns norm(fit - p).
    % Uses linear fit of log(log(a/p)) by log(-b)-c*t.
    % Call a = fminbnd(@(a)gompertz_fit(a,t,p),a_1,a_2).
        gompertz = @(a,b,c,t) a*exp(-b*exp(-c*t));
        A = [t ones(size(t))];
        rhs = log(log(a./p));
        f = A\rhs;
        b = exp(f(2));
        c = -f(1);
        r = gompertz(a,b,c,t) - p;
        rnorm = norm(r);
    end 
   
    function model_cb(~,~)
        m = get(h.model,'value');
        censusapp2024(models{m})
    end
 
    function predict_cb(arg,~)
       sig = 2*(get(arg,'string') == '+') - 1;
       w = get(h.predict,'userdata');
       w = w + sig*(1 + 9*(w > 2039));
       axis([1890 max(2030,w+20) 0 max(450,3*(w-1890))])
       h.predict.String = sprintf('predict = %d',w);
       h.predict.UserData = w;
       model_cb
    end

    function noise_cb(arg,~)
       sig = 2*(get(arg,'string') == '+') - 1;
       level = get(h.noise,'userdata');
       level = max(0,level + 10*sig);
       h.noise.String = sprintf('noise = %d',level);
       h.noise.UserData = level;
       model_cb
    end

    function degree_cb(arg,~)
       sig = 2*(get(arg,'string') == '+') - 1;
       d = get(h.deg,'userdata');
       d = min(12,max(0,d + sig));
       h.deg.String = sprintf('degree = %d',d);
       h.deg.UserData = d;
       model_cb
    end

    function r2_cb(arg,~)
        onoff = {'on','off'};
        h.r2txt.Visible = onoff{2-get(arg,'value')};
    end

    function errs_cb(~,~)
        model_cb
    end

    function info_cb(~,~)
        web(['https://blogs.mathworks.com/cleve/2024/05/19/' ...
                 'a-sixty-year-old-program-for-predicting-the-future'], ...
            '-notoolbar'); 
    end

    function help_cb(~,~)
        helpwin('censusapp2024')
    end

    function restart_cb(~,~)
        censusapp2024
    end

    function errest = errorestimates(model,t,p,x,y)
    % Provide error estimates for censusapp2024
        switch model
           case 'polynomial'
              d = get(h.deg,'userdata');
              if d > 0
                 V(:,d+1) = ones(size(t));
                 s = (t-1960)/60;
                 for j = d:-1:1
                    V(:,j) = s.*V(:,j+1);
                 end
                 [Q,R] = qr(V);
                 r = p - V*(R\(Q'*p));
                 R = R(1:d+1,:);
                 RI = inv(R);
                 E = zeros(length(x),d+1);
                 s = (x-1960)/60;
                 for j = 1:d+1
                    E(:,j) = polyval(RI(:,j),s);
                 end
                 rho = norm(r,inf)+1;
                 e = rho*sqrt(1+diag(E*E'));
                 errest = [y-e; NaN; y+e];
              else
                 errest = [y-NaN; NaN; y+NaN];
              end
           case {'exponential','logistic','gompertz'}
              V = [ones(size(t)) log(t)];
              [Q,R] = qr(V);
              q = R\(Q'*log(p));
              r = log(p) - V*q;
              E = [ones(size(x)) log(x)]/R(1:2,1:2);
              rho = norm(r,inf);
              e = rho*sqrt(1+diag(E*E'));
              errest = [y.*exp(-e); NaN; y.*exp(e)];
           case {'pchip','spline','makima'}
              n = length(t);
              I = eye(n,n);
              E = zeros(length(x),n);
              for j = 1:n
                 switch model
                    case 'pchip'
                       E(:,j) = pchip(t,I(:,j),x);
                    case 'spline'
                       E(:,j) = spline(t,I(:,j),x);                    
                    case 'makima'
                       E(:,j) = makima(t,I(:,j),x);                    
                 end
              end
              rho = 1;
              e = rho*sqrt(1+diag(E*E'));
              errest = [y-e; NaN; y+e];
           otherwise
              errest = [y; NaN; y];
        end % switch
    end % errorestimates
end % censusapp2024