End-to-end system identification: a user-defined model

This example takes a pharmacokinetic model from a plain MATLAB function all the way to confidence intervals on its parameters, using a user-defined model rather than Simulink or Symbolic Math.
The point is not that the fit succeeds. It is that a good fit tells you almost nothing about whether your parameters are identifiable — and that the toolbox will tell you the difference if you ask it.
A Python notebook version of this example runs the same computation with the same section numbering, so the two can be read side by side.

The model

A one-compartment model with first-order absorption, the standard description of an orally administered drug:
Three factors are to be identified, with the dose mg known:
The implementation is an ordinary function file. It returns an ODE-solver-shaped struct (sol.x, sol.y) so that gsua_eval can interpolate it onto any set of sampling times, exactly as it would for a model integrated with ode45:
type pkAbsorptionModel.m
function sol = pkAbsorptionModel(params, domain, ~) %PKABSORPTIONMODEL One-compartment pharmacokinetic model with first-order absorption. % % sol = pkAbsorptionModel(params, domain, opt) % % Plasma concentration after a single oral dose D: % % c(t) = D*ka / (V*(ka-ke)) * ( exp(-ke*t) - exp(-ka*t) ) % % params = [ka; ke; V] % ka <-- absorption rate constant (1/h) % ke <-- elimination rate constant (1/h) % V <-- apparent volume of distribution (L) % domain = [t0 tf] time span in hours. % % Returns an ODE-solver-shaped struct (sol.x, sol.y) so that GSUA_EVAL can % interpolate the solution onto any xdata, exactly as it does for a model % integrated with ode45 or dde23. The model is closed-form, so the dense % output costs nothing. % % ka is deliberately bounded above ke in the examples that use this model: % at ka == ke the expression above is singular (its limit is the finite % D*ka*t*exp(-ka*t)/V, but the formula itself is 0/0), and swapping the two % leaves c(t) unchanged -- the classic flip-flop ambiguity, which would make % the parameters unidentifiable for an uninteresting algebraic reason rather % than an experimental-design one. % % The unused third input is the 'opt' configuration slot documented in the % user guide's "Time-dependent functions" section. It is required, not % optional: gsua_eval calls a time-dependent user function as % fun(pars,domain,opt), so a two-input function is rejected at evaluation % time even though gsua_userdefined will happily build a handle for one. % The toolbox's own Examples/user_dependent.m takes the same three inputs. % % See also GSUA_DATAPREP, GSUA_EVAL. D = 100; % administered dose (mg), known ka = params(1); ke = params(2); V = params(3); sol.x = linspace(domain(1), domain(2), 400); sol.y = (D*ka)./(V*(ka-ke)) .* (exp(-ke*sol.x) - exp(-ka*sol.x)); end

1. Preparing the environment

gsua_dataprep builds the summary table T that every other toolbox function consumes. For a user-defined model it needs the function name, the factor bounds, and the time domain.
% Factor bounds: one row per factor, [lower upper].
ranges = [0.6 3.0 % ka absorption rate (1/h)
0.05 0.5 % ke elimination rate (1/h)
5 40]; % V volume of distribution (L)
% 'domain' is the time span the model is integrated over; 'names'/'out_names' are
% cosmetic but propagate into every plot and table the toolbox produces.
[T,~] = gsua_dataprep('pkAbsorptionModel', ranges, 'domain', [0 24], ...
'names', {'ka','ke','V'}, 'out_names', {'concentration'});
Setting environment to work with user-defined function
T
T = 3×2 table
RangeNominal
12
1 ka0.600031.8000
2 ke0.05000.50000.2750
3 V54022.5000
The bounds are deliberately kept above the bounds. At the closed form is singular, and swapping the two leaves unchanged — the classic flip-flop ambiguity. Excluding it keeps this example about experimental design rather than an algebraic accident.

2. Synthetic data

Working from synthetic data means the truth is known, so the confidence intervals can be checked rather than merely reported.
rng(0,'twister') % fix the noise draw so the page reproduces
truth = [1.2; 0.25; 15]; % the values we will try to recover
T.Nominal = truth;
xdata = [0.25 0.5 1 1.5 2 3 4 6 8 10 12 16 20 24]; % sampling schedule (hours)
% gsua_eval(values, T, xdata, ydata, parallel, show, verbose): the trailing two false
% flags suppress the automatic diagnostic plot and the "Progress: N %" print.
clean = gsua_eval(truth, T, xdata, [], false, false, false);
ydata = clean .* (1 + 0.08*randn(size(clean))); % 8% proportional measurement noise
tdense = linspace(0.05, 24, 300); % dense grid, for drawing curves only
plot(tdense, gsua_eval(truth, T, tdense, [], false, false, false), 'LineWidth', 1.5)
hold on
plot(xdata, ydata, 'ko', 'MarkerFaceColor', 'k', 'MarkerSize', 5)
hold off
xlabel('time (h)')
ylabel('concentration (mg/L)')
legend('true model','measurements','Location','northeast')
title('Simulated single-dose concentration data')
grid on

3. Can these parameters be estimated at all?

Before spending any optimizer budget it is worth asking whether the data is even reachable: does it fall inside the range of behaviours the model can produce over the factor bounds declared in section 1? If it does not, no amount of optimization will help — the model structure or the bounds are wrong, and that has to be fixed first. This is the reachability check that opens the toolbox's semi-automated identification cycle.
gsua_dmatrix samples the factor box and gsua_ua runs the Monte-Carlo ensemble over those samples. gsua_ua also applies Monte-Carlo filtering automatically and plots it, which is what the two figures below show: for each factor, how the low and high halves of its sampled range map onto model output.
M0 = gsua_dmatrix(T, 500); % 500 samples of the factor box
Y0 = gsua_ua(M0, T, 'xdata', xdata, 'ynom', ydata, 'parallel', false, 'verbose', false);
gsua_covmetric reduces that ensemble to a 5–95% band and scores it. The containment fraction — how much of the measured data actually falls inside the band — is the number to read at this stage.
[cost_data, cost_band, P5, ~, P95] = gsua_covmetric(Y0, ydata, 'margin', 0.1);
reachable = mean(ydata >= P5 & ydata <= P95);
table(reachable, median(P95-P5), cost_data, cost_band, ...
'VariableNames', {'contained','median_band_width','cost_data','cost_band'})
ans = 1×4 table
containedmedian_band_widthcost_datacost_band
115.0314116.41432.4182e+07
All of the data lies inside the reachable band, so estimation is worth attempting. Note that cost_data and cost_band are both far above 1 here, and that is expected rather than alarming: they are normalized against a tight tolerance and are meaningful as a post-convergence check (section 7), not as a pass/fail gate against a prior range this wide. The band is enormous — a median width of about 5 mg/L against data that never exceeds 4.5 — which is exactly what an uninformative prior looks like before any fitting.
plot(xdata, P5, 'Color', [0.4 0.4 0.4], 'LineWidth', 1)
hold on
plot(xdata, P95, 'Color', [0.4 0.4 0.4], 'LineWidth', 1)
plot(xdata, ydata, 'ko', 'MarkerFaceColor', 'k', 'MarkerSize', 5)
hold off
xlabel('time (h)')
ylabel('concentration (mg/L)')
legend('5th percentile','95th percentile','measurements','Location','northeast')
title(sprintf('Reachable band before fitting (%.0f%% of data contained)', 100*reachable))
grid on

4. Estimating the parameters

gsua_pe runs a multistart estimation: 'N',20 restarts the optimizer from twenty different points in the factor space, which is how you find out whether the problem has one solution or several.
% 'margin',0.1 selects the correlation-penalized cost and records the margin on the
% output table, so functions further down the pipeline can recover what was scored.
% 'timer',false suppresses the progress readout.
[T3,res3] = gsua_pe(T, xdata, ydata, 'solver','lsqc', 'N',20, 'margin',0.1, 'timer',false);
Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum found. Optimization completed because the size of the gradient is less than the value of the optimality tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum found. Optimization completed because the size of the gradient is less than the value of the optimality tolerance. <stopping criteria details> Local minimum found. Optimization completed because the size of the gradient is less than the value of the optimality tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum found. Optimization completed because the size of the gradient is less than the value of the optimality tolerance. <stopping criteria details> Local minimum found. Optimization completed because the size of the gradient is less than the value of the optimality tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details>
Est3 = T3.Estlsqc; % 3 x 20: one column per multistart run
table(truth, Est3(:,1), 'VariableNames', {'true','estimated'}, 'RowNames', T3.Properties.RowNames)
ans = 3×2 table
trueestimated
1 ka1.20001.4161
2 ke0.25000.2169
3 V1516.5679
Every one of the twenty runs converged to the same cost, and the fitted curve passes cleanly through the data. On most projects this is where the analysis would stop.
plot(tdense, gsua_eval(Est3(:,1), T3, tdense, [], false, false, false), 'LineWidth', 1.5)
hold on
plot(xdata, ydata, 'ko', 'MarkerFaceColor', 'k', 'MarkerSize', 5)
hold off
xlabel('time (h)')
ylabel('concentration (mg/L)')
legend('fitted model','measurements','Location','northeast')
title(sprintf('Fit with all three factors free (cost = %.4g)', min(res3)))
grid on

5. Diagnosing identifiability

The correlation between the repeated estimates is the first warning sign. Values near mean the factors trade off against each other: many different combinations reproduce the same curve.
array2table(corr(Est3'), 'VariableNames', T3.Properties.RowNames, 'RowNames', T3.Properties.RowNames)
ans = 3×3 table
kakeV
1 ka1-0.99910.9996
2 ke-0.99911-0.9999
3 V0.9996-0.99991
gsua_likelihood turns that into something actionable. It profiles each factor — stepping it away from the estimate while re-fitting all the others at every step — and reports the interval over which the fit stays statistically acceptable.
% Positional arguments: (T, xdata, ydata, alpha, step, margin, tol1, tol2, limit,
% reps, show, parallel, saver, pars). alpha = 0.95 is the confidence level; limit is
% the bisection budget per bound; pars = [] profiles every factor.
%
% margin is a RELATIVE STANDARD DEVIATION OFFSET BY ONE here: gsua_likelihood uses
% (margin-1) as the assumed relative noise, so 1.08 means the 8% noise the data
% actually carries. Passing 0.1 would assert 90% noise -- and would also make
% gsua_pe's internal +1 offset positive, silently switching its inner refit from the
% likelihood to plain least squares. The Python port drops the offset, so its
% margin=0.08 is this margin=1.08.
Tci3 = gsua_likelihood(T3, xdata, ydata, 0.95, 0.05, 1.08, 0.01, 0.01, 15, 1, false, false, false, [], false);
Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details>
table(Tci3.Range(:,1), Tci3.Range(:,2), Tci3.Range(:,2)-Tci3.Range(:,1), ranges(:,1), ranges(:,2), ...
'VariableNames', {'CI_low','CI_high','width','prior_low','prior_high'}, ...
'RowNames', T3.Properties.RowNames)
ans = 3×5 table
CI_lowCI_highwidthprior_lowprior_high
1 ka0.96751.44750.48000.60003
2 ke0.23750.25940.02190.05000.5000
3 V13.875016.56252.6875540
This is the result worth stopping on. The correlation between and is : the two rate constants are very nearly a single degree of freedom, and the intervals are correspondingly loose for a fit this good. A model that reproduces the data this well is still telling us that these three factors are barely separable from one oral concentration curve, which is a textbook pharmacokinetic result rather than a failure of the optimizer.

6. The remedy: fix what another experiment already knows

The standard resolution is to measure V separately, in an intravenous study where it is directly identifiable, and then estimate only the two rate constants. In this toolbox a factor is fixed by giving it a degenerate range; fixed factors drop out of T entirely, so the table returned below has two rows rather than three.
rangesFixed = [0.6 3.0; 0.05 0.5; 15 15]; % V pinned at its known value
[Tf,~] = gsua_dataprep('pkAbsorptionModel', rangesFixed, 'domain', [0 24], ...
'names', {'ka','ke','V'}, 'out_names', {'concentration'});
Setting environment to work with user-defined function
Tf.Nominal = truth(1:height(Tf)); % only the free factors remain
[T2,res2] = gsua_pe(Tf, xdata, ydata, 'solver','lsqc', 'N',20, 'margin',0.1, 'timer',false);
Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details> Local minimum possible. lsqcurvefit stopped because the final change in the sum of squares relative to its initial value is less than the value of the function tolerance. <stopping criteria details>
Tci2 = gsua_likelihood(T2, xdata, ydata, 0.95, 0.05, 1.08, 0.01, 0.01, 15, 1, false, false, false, [], false);
Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum possible. Constraints satisfied. fmincon stopped because the size of the current step is less than the value of the step size tolerance and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details> Local minimum found that satisfies the constraints. Optimization completed because the objective function is non-decreasing in feasible directions, to within the value of the optimality tolerance, and constraints are satisfied to within the value of the constraint tolerance. <stopping criteria details>
table(truth(1:2), T2.Estlsqc(:,1), Tci2.Range(:,1), Tci2.Range(:,2), Tci2.Range(:,2)-Tci2.Range(:,1), ...
'VariableNames', {'true','estimated','CI_low','CI_high','width'}, ...
'RowNames', T2.Properties.RowNames)
ans = 2×5 table
trueestimatedCI_lowCI_highwidth
1 ka1.20001.19490.99751.35750.3600
2 ke0.25000.24930.24250.25310.0106
Both remaining factors are now recovered close to the truth, and both intervals sit well inside their bounds instead of running to them.

7. The two runs side by side

Comparing the two fits on the quantities people usually conflate:
summary = table([min(res3); corr2free(Est3); Tci3.Range(1,2)-Tci3.Range(1,1); Tci3.Range(2,2)-Tci3.Range(2,1)], ...
[min(res2); corr2free(T2.Estlsqc); Tci2.Range(1,2)-Tci2.Range(1,1); Tci2.Range(2,2)-Tci2.Range(2,1)], ...
'VariableNames', {'all_three_free','V_fixed'}, ...
'RowNames', {'best cost','corr(ka,ke)','CI width ka','CI width ke'})
summary = 4×2 table
all_three_freeV_fixed
1 best cost0.99031.1071
2 corr(ka,ke)-0.99910.9642
3 CI width ka0.48000.3600
4 CI width ke0.02190.0106
bar([Tci3.Range(1:2,2)-Tci3.Range(1:2,1), Tci2.Range(:,2)-Tci2.Range(:,1)])
set(gca,'XTickLabel', T2.Properties.RowNames)
ylabel('95% confidence interval width')
legend('all three free','V fixed','Location','northeast')
title('Fixing one factor sharpens the other two')
grid on

8. What this example shows

Fixing V made the fit slightly worse and the science considerably better: the correlation between and collapses, both intervals tighten, and the estimates move onto the truth.
Cost measures how well a curve passes through points. It does not measure whether the factors that produced that curve could have been recovered. Only the identifiability analysis answers that, which is why it belongs inside the workflow rather than after it.
The companion symbolic-math example reaches the same conclusion from the opposite direction: there, the dataset that fits better is the one whose parameters are less identifiable.
Where a multistart run does spread across the factor space instead of converging to a single point, gsua_ia adds correlation heatmaps, fit-quality filtering and detection of multiple global minima; gsua_covmetric scores an uncertainty band for the same confounding signature; and gsua_dmatrix with 'Method','Joint' propagates the accepted set without destroying its correlation structure.
function r = corr2free(E)
% Correlation between the first two factors across the multistart pool.
R = corr(E');
r = R(1,2);
end