1function [model, info] = infer_lqn(model, paramSpec, obsSpec, Z, options)
2% INFER_LQN Identify hidden LQN parameters from measured performance data.
4% [MODEL, INFO] = INFER_LQN(MODEL, PARAMSPEC, OBSSPEC, Z, OPTIONS) estimates
5%
the LQN parameters named in PARAMSPEC (activity host demands and/or task
6% think times) of
the LayeredNetwork MODEL from
the sequence of performance
7% measurements Z,
using an Extended Kalman Filter over
the observation model
8% defined by OBSSPEC. It implements Zheng, Yang, Woodside, Litoiu, Iszlai,
9%
"Tracking Time-Varying Parameters in Software Systems with Extended Kalman
10% Filters", CASCON 2005. A single measurement
column with OPTIONS.QFac = 0
11% reduces to one-shot least-squares calibration.
14% PARAMSPEC :
struct array of parameters to estimate; see INFER_LQN_SETPARAMS.
15% OBSSPEC :
struct array of observed metrics; see INFER_LQN_GETOBS.
16% Z : (no x nsteps) measurements, no == numel(OBSSPEC).
17% OPTIONS :
struct with optional fields (defaults in parentheses):
18% solver (@SolverLN) observation-model solver constructor
19% solveropts ([]) options struct passed to
the solver
20% QFac (0.1) drift-noise
factor, Q_ii=(QFac*a0_i*cvA)^2
21% RFac (0.2) meas.-noise
factor, R_ii=((RFac*zbar_i)/1.96)^2/gammaT
22% cvA (1) parameter drift coefficient of variation
23% gammaT ([]) T/Tstar ratio for R (else from T and Tstar, else 1)
24% T, Tstar ([]) measurement interval and system constant
25% Q, R ([]) explicit covariances (override the above)
26% P0 ([]) initial covariance (default diag((0.5*a0)^2))
27% a0 ([]) initial estimate (default: current model values)
28% aTrue ([]) ground truth, enables
the Ea RMS metric
29% fdStep (1e-3), fdFloor (1e-6), clampPositive (true), verbose (false)
32% MODEL :
the LayeredNetwork with
the final parameter estimate applied.
33% INFO : EKF result struct (see INFER_LQN_EKF) augmented with ahat, a0,
36% Copyright (c) 2012-2026, Imperial College London
39if nargin < 5, options = struct(); end
43 line_error(mfilename, 'Row count of Z must equal numel(obsSpec).');
46% observation-model solver and its (silent) options
47solverCtor = infer_lqn_optget(options, 'solver', @SolverLN);
48solveropts = infer_lqn_optget(options, 'solveropts', []);
51 solveropts = feval([func2str(solverCtor) '.defaultOptions']);
52 solveropts.verbose = VerboseLevel.SILENT;
58% initial parameter estimate (default: current model values)
59a0 = infer_lqn_optget(options, 'a0', []);
61 a0 = infer_lqn_getparams(model, paramSpec);
65% drift covariance Q (eq 9a) and measurement covariance R (eq 9b)
66QFac = infer_lqn_optget(options, 'QFac', 0.1);
67RFac = infer_lqn_optget(options, 'RFac', 0.2);
68cvA = infer_lqn_optget(options, 'cvA', 1);
69gammaT = infer_lqn_optget(options, 'gammaT', []);
71 T = infer_lqn_optget(options, 'T', []);
72 Tstar = infer_lqn_optget(options, 'Tstar', []);
73 if ~isempty(T) && ~isempty(Tstar)
80Q = infer_lqn_optget(options, 'Q', []);
82 qd = (QFac * abs(a0) * cvA).^2; % eq 9a, mean(a_i) ~ a0_i
83 Q = diag(max(qd, eps));
85R = infer_lqn_optget(options, 'R', []);
87 zbar = mean(Z, 2); % mean of z_i across steps
88 rd = ((RFac * abs(zbar)) / 1.96).^2 / gammaT; % eq 9b
89 R = diag(max(rd, eps));
91P0 = infer_lqn_optget(options, 'P0', []);
93 P0 = diag(max((0.5 * abs(a0)).^2, eps));
96% observation model h(a): inject params, solve the LQN, read the metrics
97hfun = @(a) evalObs(model, paramSpec, obsSpec, a, solverCtor, solveropts);
100 'fdStep', infer_lqn_optget(options, 'fdStep', 1e-3), ...
101 'fdFloor', infer_lqn_optget(options, 'fdFloor', 1e-6), ...
102 'clampPositive', infer_lqn_optget(options, 'clampPositive', true), ...
103 'aTrue', infer_lqn_optget(options, 'aTrue', []), ...
104 'verbose', infer_lqn_optget(options, 'verbose', false));
106[ahat, info] = infer_lqn_ekf(hfun, a0, P0, Z, Q, R, ekfopts);
113% apply the final estimate to the returned model
114model = infer_lqn_setparams(model, paramSpec, ahat(:, end));
117function z = evalObs(model, paramSpec, obsSpec, a, solverCtor, solveropts)
118% Inject parameter vector a, solve the LQN, extract the observation vector.
119model = infer_lqn_setparams(model, paramSpec, a);
120if isempty(solveropts)
121 solver = solverCtor(model);
123 solver = solverCtor(model, solveropts);
125[QN, UN, RN, TN] = solver.getEnsembleAvg();
126lsn = model.getStruct();
127metrics = struct('QLen', QN, 'Util', UN, 'RespT', RN, 'Tput', TN);
128z = infer_lqn_getobs(lsn.names, metrics, obsSpec);
131function a0 = infer_lqn_getparams(model, paramSpec)
132% Read the current values of the parameters named in paramSpec.
133np = numel(paramSpec);
136 switch lower(paramSpec(i).type)
138 act = infer_lqn_findbyname(model.activities, paramSpec(i).name);
140 line_error(mfilename, sprintf('Activity ''%s'' not found.', paramSpec(i).name));
142 a0(i) = act.hostDemandMean;
144 tsk = infer_lqn_findbyname(model.tasks, paramSpec(i).name);
146 line_error(mfilename, sprintf('Task ''%s'' not found.', paramSpec(i).name));
148 a0(i) = tsk.thinkTimeMean;
150 line_error(mfilename, sprintf('Unknown parameter type ''%s''.', paramSpec(i).type));