LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getSensitivityTable.m
1function [SensTable, sens] = getSensitivityTable(self, varargin)
2% GETSENSITIVITYTABLE Layer-wise performance sensitivities of a layered network.
3%
4% [SENSTABLE, SENS] = GETSENSITIVITYTABLE(SELF) solves the layered model and
5% then delegates to each layer solver, returning the concatenation of the layer
6% tables with a leading Layer column. Every row is therefore a (Layer, Station,
7% JobClass) triple carrying the derivative of that row's mean measures with
8% respect to that station-class service RATE in that layer:
9% dTput_dRate, dRespT_dRate, dQLen_dRate, dUtil_dRate.
10%
11% [...] = GETSENSITIVITYTABLE(SELF, 'method', M, 'step', H, 'scheme', S) passes
12% the options through to the layer solvers unchanged, with the same meaning as
13% in NetworkSolver/getSensitivityTable: each layer independently takes the
14% analytic branch where its own solver supports it and the model is in scope,
15% and finite differences otherwise. SENS is a cell array of the layer second
16% outputs, indexed by layer.
17%
18% IMPORTANT, on what these derivatives mean. Each entry is a derivative WITHIN
19% ITS LAYER, taken with the layer parameters that the fixed point produced held
20% fixed. It is a partial derivative of the layer submodel, not the total
21% derivative of the layered model: perturbing a host demand in one layer moves
22% the think times, populations and service rates of the other layers through
23% the fixed-point map, and that indirect term is not included here. The layer
24% table is the right object for attributing a bottleneck inside a layer, and
25% the wrong one for predicting the effect of a parameter change on the solved
26% layered model. For the latter, finite-difference the LayeredNetwork itself.
27
28if (GlobalConstants.DummyMode)
29 SensTable = [];
30 sens = {};
31 return
32end
33
34% Under lang='java' and lang='python' this solver owns no layer ensemble at
35% all: the constructor hands the whole LayeredNetwork to the JAR (self.obj) or
36% to the native python SolverLN (pyMode), and self.solvers stays empty. The
37% loop below would then walk zero layers and return an EMPTY table rather than
38% an error, so both backends are dispatched explicitly here.
39if ~isempty(self.obj)
40 [SensTable, sens] = i_javaSensitivityTable(self, varargin{:});
41 return
42elseif ~isempty(self.pyMode) && self.pyMode
43 [SensTable, sens] = PYLINE.getLNSensitivityTable(self.model, self.options, varargin{:});
44 return
45end
46
47% The layer solvers must sit at the converged fixed point, so the fixed-point
48% loop is run first if it has not been already. The test is on self.results,
49% the per-iteration layer results that iterate() fills in, not on self.result:
50% a backend that produces the layered averages without visiting the layers
51% (see above) would otherwise leave every layer submodel at its build-time
52% parameterization, and the derivatives would be those of a different model.
53if isempty(self.results)
54 self.iterate();
55end
56
57E = self.getNumberOfModels();
58Layer = {}; Station = {}; JobClass = {};
59dTput = []; dRespT = []; dQLen = []; dUtil = [];
60sens = cell(1, E);
61methods = cell(1, E);
62
63for e = 1:E
64 solver = self.solvers{e};
65 if isempty(solver)
66 continue
67 end
68 [T, sens{e}] = solver.getSensitivityTable(varargin{:});
69 methods{e} = T.Properties.UserData.method;
70 layerName = self.ensemble{e}.getName();
71 for r = 1:height(T)
72 Layer{end+1, 1} = layerName; %#ok<AGROW>
73 Station{end+1, 1} = T.Station{r}; %#ok<AGROW>
74 JobClass{end+1, 1} = T.JobClass{r}; %#ok<AGROW>
75 dTput(end+1, 1) = T.dTput_dRate(r); %#ok<AGROW>
76 dRespT(end+1, 1) = T.dRespT_dRate(r); %#ok<AGROW>
77 dQLen(end+1, 1) = T.dQLen_dRate(r); %#ok<AGROW>
78 dUtil(end+1, 1) = T.dUtil_dRate(r); %#ok<AGROW>
79 end
80end
81
82SensTable = table(Layer, Station, JobClass, dTput, dRespT, dQLen, dUtil, ...
83 'VariableNames', {'Layer', 'Station', 'JobClass', 'dTput_dRate', ...
84 'dRespT_dRate', 'dQLen_dRate', 'dUtil_dRate'});
85
86% One branch label per layer, plus a summary that is 'mixed' when the layers
87% did not all take the same branch.
88present = methods(~cellfun(@isempty, methods));
89if isempty(present)
90 summary = '';
91elseif all(strcmp(present, present{1}))
92 summary = present{1};
93else
94 summary = 'mixed';
95end
96SensTable.Properties.UserData = struct('method', summary, 'layerMethods', {methods});
97end
98
99function [SensTable, sens] = i_javaSensitivityTable(self, varargin)
100% I_JAVASENSITIVITYTABLE lang='java': delegate to the JAR SolverLN, which owns
101% the layer ensemble in this mode, and marshal its LayeredNetworkSensitivityTable
102% back into the same MATLAB table and UserData that the native branch returns.
103opt = i_parseSensOptions(varargin{:});
104step = opt.step;
105if isempty(step)
106 step = NaN; % NaN selects the JAR-side default, as in the Java signature
107end
108jt = self.obj.getSensitivityTable(opt.method, step, opt.scheme);
109
110Layer = i_javaStringList(jt.getLayerNames());
111Station = i_javaStringList(jt.getStationNames());
112JobClass = i_javaStringList(jt.getClassNames());
113dTput = i_javaDoubleList(jt.getDTput());
114dRespT = i_javaDoubleList(jt.getDRespT());
115dQLen = i_javaDoubleList(jt.getDQLen());
116dUtil = i_javaDoubleList(jt.getDUtil());
117
118SensTable = table(Layer, Station, JobClass, dTput, dRespT, dQLen, dUtil, ...
119 'VariableNames', {'Layer', 'Station', 'JobClass', 'dTput_dRate', ...
120 'dRespT_dRate', 'dQLen_dRate', 'dUtil_dRate'});
121
122methods = i_javaStringList(jt.getLayerMethods())';
123jsens = jt.getLayerSens();
124sens = cell(1, numel(methods));
125for e = 1:numel(sens)
126 sens{e} = i_javaSensStruct(jsens.get(e-1));
127end
128SensTable.Properties.UserData = struct('method', char(jt.getMethod()), ...
129 'layerMethods', {methods});
130end
131
132function opt = i_parseSensOptions(varargin)
133% I_PARSESENSOPTIONS Same name-value contract as
134% @NetworkSolver/getSensitivityTable, parsed here because the bridged branches
135% need the values rather than passing varargin straight through.
136opt = struct('method', 'auto', 'step', [], 'scheme', 'forward');
137if mod(numel(varargin), 2) ~= 0
138 line_error(mfilename, 'Options must be given as name-value pairs.');
139end
140for a = 1:2:numel(varargin)
141 name = lower(varargin{a});
142 if ~isfield(opt, name)
143 line_error(mfilename, sprintf('Unknown option ''%s''.', varargin{a}));
144 end
145 opt.(name) = varargin{a+1};
146end
147if ischar(opt.method), opt.method = lower(opt.method); end
148if ischar(opt.scheme), opt.scheme = lower(opt.scheme); end
149if ~ismember(opt.method, {'auto', 'exact', 'fd'})
150 line_error(mfilename, 'The method must be one of ''auto'', ''exact'', ''fd''.');
151end
152if ~ismember(opt.scheme, {'forward', 'central'})
153 line_error(mfilename, 'The scheme must be ''forward'' or ''central''.');
154end
155end
156
157function c = i_javaStringList(jlist)
158% I_JAVASTRINGLIST java.util.List<String> -> column cell of char. A null entry
159% (a layer with no solver) becomes '' so that the cellfun(@isempty) tests and
160% the strcmp summary above behave as on the native branch.
161n = jlist.size();
162c = cell(n, 1);
163for i = 1:n
164 e = jlist.get(i-1);
165 if isempty(e)
166 c{i} = '';
167 else
168 c{i} = char(e);
169 end
170end
171end
172
173function v = i_javaDoubleList(jlist)
174% I_JAVADOUBLELIST java.util.List<Double> -> column double vector.
175n = jlist.size();
176v = zeros(n, 1);
177for i = 1:n
178 v(i) = double(jlist.get(i-1));
179end
180end
181
182function s = i_javaSensStruct(jsens)
183% I_JAVASENSSTRUCT jline.io.Ret.pfqnSens -> the pfqn_sens struct that the
184% native MATLAB branch returns as its second output. Empty for a layer that
185% took the finite-difference branch, which carries no analytic Jacobian.
186if isempty(jsens)
187 s = [];
188 return
189end
190s = struct();
191s.X = JLINE.from_jline_matrix(jsens.X);
192s.Q = JLINE.from_jline_matrix(jsens.Q);
193s.U = JLINE.from_jline_matrix(jsens.U);
194s.R = JLINE.from_jline_matrix(jsens.R);
195s.dX = JLINE.from_jline_matrix(jsens.dX);
196s.dQ = i_javaMatrixArray3(jsens.dQ);
197s.dU = i_javaMatrixArray3(jsens.dU);
198s.dR = i_javaMatrixArray3(jsens.dR);
199% The JAR keeps the parameter descriptors as three parallel int arrays with
200% type 0 = L / 1 = Z and station -1 for Z; pfqn_sens.m returns one 1 x P struct
201% array with .type 'L'/'Z', .station (0 for Z) and .class. Convert, so that a
202% caller reading sens{e}.params(p) does not have to know which backend ran.
203jType = double(jsens.paramType(:))';
204jStation = double(jsens.paramStation(:))';
205jClass = double(jsens.paramClass(:))';
206P = numel(jType);
207params = struct('type', cell(1, P), 'station', cell(1, P), 'class', cell(1, P));
208for p = 1:P
209 if jType(p) == 0
210 params(p).type = 'L';
211 params(p).station = jStation(p) + 1; % 0-based station -> 1-based
212 else
213 params(p).type = 'Z';
214 params(p).station = 0;
215 end
216 params(p).class = jClass(p) + 1; % 0-based class -> 1-based
217end
218s.params = params;
219if ~isempty(jsens.QVar)
220 s.QVar = JLINE.from_jline_matrix(jsens.QVar);
221end
222if ~isempty(jsens.QTotVar)
223 s.QTotVar = JLINE.from_jline_matrix(jsens.QTotVar);
224end
225s.QCovAsym = double(jsens.QCovAsym);
226if ~isempty(jsens.QCov)
227 % QCov[i][r] is an M x R matrix of Cov[n(i,r),n(j,s)]; pfqn_sens.m returns
228 % the same content as one M x R x M x R array, so index it that way here.
229 M = numel(jsens.QCov);
230 firstRow = jsens.QCov(1);
231 Rn = size(JLINE.from_jline_matrix(firstRow(1)), 2);
232 s.QCov = zeros(M, Rn, M, Rn);
233 for i = 1:M
234 row = jsens.QCov(i);
235 for r = 1:Rn
236 s.QCov(i, r, :, :) = JLINE.from_jline_matrix(row(r));
237 end
238 end
239end
240end
241
242function A = i_javaMatrixArray3(jarr)
243% I_JAVAMATRIXARRAY3 Matrix[] of P entries, each M x R -> M x R x P array,
244% matching the shape pfqn_sens returns in MATLAB (dQ(ist,c,p)).
245P = numel(jarr);
246if P == 0
247 A = [];
248 return
249end
250first = JLINE.from_jline_matrix(jarr(1));
251A = zeros(size(first, 1), size(first, 2), P);
252A(:, :, 1) = first;
253for p = 2:P
254 A(:, :, p) = JLINE.from_jline_matrix(jarr(p));
255end
256end
Definition Station.m:245