% Jake Bobowsk
% July 26, 2017
% Created using MATLAB R2014a

clearvars;
format longE;

% Sometimes a two sets of experimental data depend on the same model
% parameters.  Consider, for example, the LRC resonator.  One could measure
% both the magnitude and phase of the voltage across the resistor as a
% function of frequency.  Both of these quantities depend on the resonant
% frequency of the system and a loss parameter.  The magnitude and phase
% data could be fit independently to determine two sets of the parameters.
% Perhaps you could then take a weighted average of the results to produce
% a single 'best' value for the resonant frequency and loss parameter.
% However, a better approach is to simultaneously fit the two data sets and
% directly extract a single best-fit value for each parameter.  This
% tutorial shows you how to implement a simultaneous fits to multiple data
% sets that, each having their own model, but sharing at least some of the
% same best-fit parameters.

% This tutorial makes use of a MATLAB script called 'nlinmultifit.m' which
% was created by Chen Avinadav.  I will not make an attempt to explain his
% code, but below are the comments provided by the author at the beginning
% of his script:

%NLINMULTIFIT Nonlinear least-squares regression of multiple data sets
%
%	A wrapper function for NLINFIT which allows simulatenous fitting of
%	multiple data sets with shared fitting parameters. See example below.
%
%	Unlike different solutions (using fminsearch or similar functions)
%	this approach enables simple estimation of model predictions and
%	their confidence intervals, as well as confidence intervals on the
%	fitted parameters (using the built-in NLPREDCI and NLPARCI functions).
%
%	INPUT:
% 		x_cell,y_cell: Cell arrays containing the x,y vectors of the fitted
%					   data sets.
% 		mdl_cell: Cell array containing model functions for each data set.
% 		beta0: Vector containing initial guess of the fitted parameters.
% 		options: Structure containing control parameters for NLINFIT (see
%				 help file on NLINFIT for more details).
%		Name,Value pairs: additional options passed to NLINFIT (see help
%						  file on NLINFIT for more details). The 'Weights'
%						  input option may be specified as a cell array,
%						  similar to the input of x_cell and y_cell.
%
%	NOTE:
%		Unless a cell array of weights is explicitly given, NLINMULTIFIT
%		will create a weights vector such that all points in all datasets
%		receive equal weight regardless of each dataset length.
%
%
%	OUTPUT:
%		beta,r,J,Sigma,mse,errorparam,robustw: Direct output from NLINFIT.
%
%	EXAMPLE:
% 		% Generate X vectors for both data sets
% 		x1 = 0:0.1:10;
% 		x2 = 0:1:10;
%
% 		% Generate Y data with some noise
% 		y1 = cos(2*pi*0.5*x1).*exp(-x1/5) + 0.05*randn(size(x1));
% 		y2 = 0.5 + 2*exp(-(x2/5)) + 0.05*randn(size(x2));
%
% 		% Define fitting functions and parameters, with identical
%		% exponential decay for both data sets
% 		mdl1 = @(beta,x) cos(2*pi*beta(1)*x).*exp(-x/beta(2));
% 		mdl2 = @(beta,x) beta(4) + beta(3)*exp(-(x/beta(2)));
%
% 		% Prepare input for NLINMULTIFIT and perform fitting
% 		x_cell = {x1, x2};
% 		y_cell = {y1, y2};
% 		mdl_cell = {mdl1, mdl2};
% 		beta0 = [1, 1, 1, 1];
% 		[beta,r,J,Sigma,mse,errorparam,robustw] = ...
%					nlinmultifit(x_cell, y_cell, mdl_cell, beta0);
%
% 		% Calculate model predictions and confidence intervals
% 		[ypred1,delta1] = nlpredci(mdl1,x1,beta,r,'covar',Sigma);
% 		[ypred2,delta2] = nlpredci(mdl2,x2,beta,r,'covar',Sigma);
%
% 		% Calculate parameter confidence intervals
% 		ci = nlparci(beta,r,'Jacobian',J);
%
% 		% Plot results
% 		figure;
% 		hold all;
% 		box on;
% 		scatter(x1,y1);
% 		scatter(x2,y2);
% 		plot(x1,ypred1,'Color','blue');
% 		plot(x1,ypred1+delta1,'Color','blue','LineStyle',':');
% 		plot(x1,ypred1-delta1,'Color','blue','LineStyle',':');
% 		plot(x2,ypred2,'Color',[0 0.5 0]);
% 		plot(x2,ypred2+delta2,'Color',[0 0.5 0],'LineStyle',':');
% 		plot(x2,ypred2-delta2,'Color',[0 0.5 0],'LineStyle',':');
%
%	AUTHOR:
%		Chen Avinadav
%		mygiga (at) gmail
%

% In the example below, the signal reflected (S11) from a short section of
% coaxial cable terminated by an inductive loop was measured.  S11 has both
% real and imaginary components that depend on the same set of parameters.
% We will simulataneously fit the real and imaginary parts to their
% respective models and extract a single set of best fit parameters.

% Import the data and plot it.  The data file has three columns: 1. frequency, 2. real
% component of S11, and 3. imaginary component of S11.
M = dlmread('S11 - real imag.dat');
fdata = M(:,1)';
Sreal = M(:,2)';
Simag = M(:,3)';
plot(fdata, Sreal, 'or');
hold on;
plot(fdata, Simag, 'ob');

xlabel('Frequency (Hz)');
ylabel('S11');
title('Initial Guess at Parameters');
legend('real','imaginary')

% Here we define some quantities that will be needed.
% Z0 is the characteristic impedance of the coaxial cable.
% L1 is a guess at the inductance of the loop at the end of the cable
% q is a time that is determined by the speed of light, the length of the
% coaxial cable, and the dielectric constant of the insulator between the
% inner and outer conductors of the cable.  Again, I'm making a guess at
% its value.
% When we do the fits, L1 and q will be the common fit parameters shared by
% the models of the real and imaginary parts of the reflection coefficient
% S11.
Z0 = 50; % ohms
L1 = 0.2e-9; % henries
q =3.2e-9; % seconds

% Xin is an expression for the reactance of the lenght of coaxial cable
% terminated by a loop of inductance L1.
Xin = Z0*(2*pi*fdata*L1/Z0 + tan(q*fdata))./(1-(2*pi*fdata*L1/Z0).*tan(q*fdata));

% Here are the models for the real and imaginary parts of S11.
S11real = ((Xin/Z0).^2-1)./((Xin/Z0).^2+1);
S11imag = 2*(Xin/Z0)./((Xin/Z0).^2+1);

% Before starting the fit routine, we'll plot the model on top of the data
% to see if your guesses at the values of L1 and q are reasonable.  If
% they're not too bad, we'll use them at the initial parameters at the
% start of the fit.
plot(fdata, S11real, '--k');
plot(fdata, S11imag, '--k');
hold off;

% Let's clear everything and start setting up for the simulataneous fit.
clearvars;
format longE;

% Import the data.
M = dlmread('S11 - real imag.dat');
fdata = M(:,1)';
Sreal = M(:,2)';
Simag = M(:,3)';
figure();
plot(fdata,Sreal,'or');
hold on;
plot(fdata,Simag,'ob');

xlabel('Frequency (Hz)');
ylabel('S11');
title('Simulataneous Fit Result');
legend('real','imaginary')

% To do the fitting, we will rely on the nlinmultifit.m script.  The
% nlinmultifit.m script must be placed in the same directory as script that
% we are currently working in (simultaneous_fits.m)

% When we execute nlinmultifit, we will have to pass to it the x and y data
% for each of our data sets.  We will pass two vectors each for x and y.
% These vectors will be stored in "cells".  Curly braces are used to
% construct cells.  In our example, the two datasets have the same x
% values, so we enter fdata twice in the x_cell.  The y_cell has the real
% and imaginary S11 values.  If you wanted to simultaneously fit to three
% or more datasets, you would just have to enter the correct number of row
% vectors in each of the two cells.
x_cell = {fdata, fdata};
y_cell = {Sreal, Simag};

% We now define our model functions.
% Z0 = 50 ohms is a constant (the characteristic impedance of the coaxial
% cable.
% f is defined as a variable.  It represents frequency, our independent
% variable.
% Our model will have two parameters:
% b(1) = L1 is the inductance of the loop at the end of the coaxial cable.
% b(2) = q is a time determined by the length l of the coaxial cable, the
% speed of light c, and the dielectric constant e of the insulator filling
% the space between the inner and outer conductors [q = 2l/(c/sqrt(e))]
Z0 = 50;
syms f
b = sym('b', [1 2]);

% These next few steps are, admittedly, a bit tricky.  Like in the nonlinear fit
% tutorial, we need to define our fit models as functions.  It took me
% quite a while to figure out a way to make this work...

% First, just enter the models as regular functions.  Remember, b(1)
% represents L1 and b(2) represents q.  Also, any time you need to divide
% by a vector or raise a vector to a power, put a dot (.) in front of the
% operator.  This tells MATLAB that the operation should be performed
% element by element rather than using some kind of vector product.
% Notice also that I have scaled b(1) by 1e-9.  This has been done so that
% L1 will be determined in units of nanohenries.  b(2) has been scaled by
% the same factor so that q will be determined in units of nanoseconds.  I
% have done this so that b(1) and b(2) will come out to be on the order of
% 1 or 10 (0.1 would be okay too).  In general, it is good practice to
% scale your fit parameters so that they will not be too different.  i.e.
% you don't want the fit routine to have to deal with one parameter that is
% on the order of 1e6 and another that is 1e-12.  I always try to get my
% fit parameters to come out to be between 0.1 and 10.
Xin = Z0*(2*pi*f*b(1)*1e-9/Z0 + tan(b(2)*1e-9*f))./(1-(2*pi*f*b(1)*1e-9/Z0).*tan(b(2)*1e-9*f));
S11real = ((Xin/Z0).^2-1)./((Xin/Z0).^2+1);
S11imag = 2*(Xin/Z0)./((Xin/Z0).^2+1);

% Here's the bit that took me will to work out...
% matlabFunction converts our S11real expression into a function of b1, b2,
% and f.  Unfortunately, the nonlinear fit routine requirs a function of a
% vector b and scalar f. The @(b,f) line converts our function of three
% scalars (b1, b2, and f) into a function of vector b and scalar f.  b is a
% tow-component vector.  The first component is b(1) (i.e. L1) and the
% second component is b(2) (i.e q).
S11Rfcn = matlabFunction(S11real);
mdlreal = @(b,f) S11Rfcn(b(1), b(2), f);

% Do the same to construct the model function for the imaginary component
% of S11.
S11Ifcn = matlabFunction(S11imag);
mdlimag = @(b,f) S11Ifcn(b(1), b(2), f);

% Just like we made cells for the x and y data, we make a cell of the model
% functions.
mdl_cell = {mdlreal, mdlimag};

% beta0 is vector of initial guesses at the two parameters L1 and q,
% respectively.  We use the values from the plots that we generated right
% at the start of this tutorial.  We know that the guesses aren't perfect,
% but hopefully they'll be good enough.  If not, we'll have to try another
% set of guesses...
beta0 = [0.2, 3.2];

% In the data used in this tutorial, all data points are going to be
% weighted equally (i.e. there's no great variation in the y uncertainties
% as frequency is swept).  However, in general you would have y
% uncertainties for each set of data that would be changing in some way.
% In that case, you should define weights for each dataset that are
% determined from one over the square of the y uncertainties.  Then you
% would construct a weights cell.

% To demonstrate the use of weights, I define two weights vectors in which
% all elements are one, i.e. equally weighted points.  Then, I make the
% weights cell called w_cell
weights1 = ones(1, length(fdata));
weights2 = ones(1, length(fdata));
w_cell = {weights1, weights2};

% Finally, here is where we call nlinmultifit.m to do the simultaneous
% fits.  To nlinmultifit, we pass x_cell, y_cell, mdl_cell, beta0 (the
% initial guess at the parameters), we use the 'Weights' option, and then
% pass w_cell.
[beta,r,J,Sigma,mse,errorparam,robustw] = ...
					nlinmultifit(x_cell, y_cell, mdl_cell, beta0, 'Weights', w_cell);

% If you didn't want to do a weighted fit, you could just implement nlinmultifit as follows:
% [beta,r,J,Sigma,mse,errorparam,robustw] = ...
%					nlinmultifit(x_cell, y_cell, mdl_cell, beta0);

% These next two lines are used to calculate model predictions.
[ypred1,delta1] = nlpredci(mdlreal,fdata,beta,r,'covar',Sigma);
[ypred2,delta2] = nlpredci(mdlimag,fdata,beta,r,'covar',Sigma);

% We can now plot the best-fit results with the original data.
plot(fdata,ypred1,'Color','k','LineWidth',2);
plot(fdata,ypred2,'Color','k','LineWidth',2);
hold off;
% The fits look very good!

% Here are the calculations of parameter 95% confidence intervals.
ci = nlparci(beta,r,'Jacobian',J);

% To estimate the best-fit parameters, I take the average of the confidence
% bounds.  I use have the difference of the confidence bounds to estimate
% the uncertainty in the best-fit parameters.
L1 = (ci(1,2) + ci(1,1))/2 % nH
dL1 = (ci(1,2) - ci(1,1))/2 % nH
q = (ci(2,2) + ci(2,1))/2 % ns
dq = (ci(2,2) - ci(2,1))/2 % ns

% L1 = 11.995 +/- 0.017 nH
% q = 2.3131 +/- 0.0007 ns
L1 =

     1.199497925822889e+01


dL1 =

     1.736682191388006e-02


q =

     2.313058330242801e+00


dq =

     6.960790543395490e-04