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% see _kb/06-solver-catalog.md (LN section) for rationale
35if ~isempty(self.obj)
36 [SensTable, sens] = i_javaSensitivityTable(self, varargin{:});
37 return
38elseif ~isempty(self.pyMode) && self.pyMode
39 [SensTable, sens] = PYLINE.getLNSensitivityTable(self.model, self.options, varargin{:});
40 return
41end
42
43% see _kb/06-solver-catalog.md (LN section) for rationale
44if isempty(self.results)
45 self.iterate();
46end
47
48E = self.getNumberOfModels();
49Layer = {}; Station = {}; JobClass = {};
50dTput = []; dRespT = []; dQLen = []; dUtil = [];
51sens = cell(1, E);
52methods = cell(1, E);
53
54for e = 1:E
55 solver = self.solvers{e};
56 if isempty(solver)
57 continue
58 end
59 [T, sens{e}] = solver.getSensitivityTable(varargin{:});
60 methods{e} = T.Properties.UserData.method;
61 layerName = self.ensemble{e}.getName();
62 for r = 1:height(T)
63 Layer{end+1, 1} = layerName; %#ok<AGROW>
64 Station{end+1, 1} = T.Station{r}; %#ok<AGROW>
65 JobClass{end+1, 1} = T.JobClass{r}; %#ok<AGROW>
66 dTput(end+1, 1) = T.dTput_dRate(r); %#ok<AGROW>
67 dRespT(end+1, 1) = T.dRespT_dRate(r); %#ok<AGROW>
68 dQLen(end+1, 1) = T.dQLen_dRate(r); %#ok<AGROW>
69 dUtil(end+1, 1) = T.dUtil_dRate(r); %#ok<AGROW>
70 end
71end
72
73SensTable = table(Layer, Station, JobClass, dTput, dRespT, dQLen, dUtil, ...
74 'VariableNames', {'Layer', 'Station', 'JobClass', 'dTput_dRate', ...
75 'dRespT_dRate', 'dQLen_dRate', 'dUtil_dRate'});
76
77% One branch label per layer, plus a summary that is 'mixed' when the layers
78% did not all take the same branch.
79present = methods(~cellfun(@isempty, methods));
80if isempty(present)
81 summary = '';
82elseif all(strcmp(present, present{1}))
83 summary = present{1};
84else
85 summary = 'mixed';
86end
87SensTable.Properties.UserData = struct('method', summary, 'layerMethods', {methods});
88end
89
90function [SensTable, sens] = i_javaSensitivityTable(self, varargin)
91% I_JAVASENSITIVITYTABLE lang='java': delegate to the JAR SolverLN, which owns
92% the layer ensemble in this mode, and marshal its LayeredNetworkSensitivityTable
93% back into the same MATLAB table and UserData that the native branch returns.
94opt = i_parseSensOptions(varargin{:});
95step = opt.step;
96if isempty(step)
97 step = NaN; % NaN selects the JAR-side default, as in the Java signature
98end
99jt = self.obj.getSensitivityTable(opt.method, step, opt.scheme);
100
101Layer = i_javaStringList(jt.getLayerNames());
102Station = i_javaStringList(jt.getStationNames());
103JobClass = i_javaStringList(jt.getClassNames());
104dTput = i_javaDoubleList(jt.getDTput());
105dRespT = i_javaDoubleList(jt.getDRespT());
106dQLen = i_javaDoubleList(jt.getDQLen());
107dUtil = i_javaDoubleList(jt.getDUtil());
108
109SensTable = table(Layer, Station, JobClass, dTput, dRespT, dQLen, dUtil, ...
110 'VariableNames', {'Layer', 'Station', 'JobClass', 'dTput_dRate', ...
111 'dRespT_dRate', 'dQLen_dRate', 'dUtil_dRate'});
112
113methods = i_javaStringList(jt.getLayerMethods())';
114jsens = jt.getLayerSens();
115sens = cell(1, numel(methods));
116for e = 1:numel(sens)
117 sens{e} = i_javaSensStruct(jsens.get(e-1));
118end
119SensTable.Properties.UserData = struct('method', char(jt.getMethod()), ...
120 'layerMethods', {methods});
121end
122
123function opt = i_parseSensOptions(varargin)
124% I_PARSESENSOPTIONS Same name-value contract as
125% @NetworkSolver/getSensitivityTable, parsed here because the bridged branches
126% need the values rather than passing varargin straight through.
127opt = struct('method', 'auto', 'step', [], 'scheme', 'forward');
128if mod(numel(varargin), 2) ~= 0
129 line_error(mfilename, 'Options must be given as name-value pairs.');
130end
131for a = 1:2:numel(varargin)
132 name = lower(varargin{a});
133 if ~isfield(opt, name)
134 line_error(mfilename, sprintf('Unknown option ''%s''.', varargin{a}));
135 end
136 opt.(name) = varargin{a+1};
137end
138if ischar(opt.method), opt.method = lower(opt.method); end
139if ischar(opt.scheme), opt.scheme = lower(opt.scheme); end
140if ~ismember(opt.method, {'auto', 'exact', 'fd'})
141 line_error(mfilename, 'The method must be one of ''auto'', ''exact'', ''fd''.');
142end
143if ~ismember(opt.scheme, {'forward', 'central'})
144 line_error(mfilename, 'The scheme must be ''forward'' or ''central''.');
145end
146end
147
148function c = i_javaStringList(jlist)
149% I_JAVASTRINGLIST java.util.List<String> -> column cell of char. A null entry
150% (a layer with no solver) becomes '' so that the cellfun(@isempty) tests and
151% the strcmp summary above behave as on the native branch.
152n = jlist.size();
153c = cell(n, 1);
154for i = 1:n
155 e = jlist.get(i-1);
156 if isempty(e)
157 c{i} = '';
158 else
159 c{i} = char(e);
160 end
161end
162end
163
164function v = i_javaDoubleList(jlist)
165% I_JAVADOUBLELIST java.util.List<Double> -> column double vector.
166n = jlist.size();
167v = zeros(n, 1);
168for i = 1:n
169 v(i) = double(jlist.get(i-1));
170end
171end
172
173function s = i_javaSensStruct(jsens)
174% I_JAVASENSSTRUCT jline.io.Ret.pfqnSens -> the pfqn_sens struct that the
175% native MATLAB branch returns as its second output. Empty for a layer that
176% took the finite-difference branch, which carries no analytic Jacobian.
177if isempty(jsens)
178 s = [];
179 return
180end
181s = struct();
182s.X = JLINE.from_jline_matrix(jsens.X);
183s.Q = JLINE.from_jline_matrix(jsens.Q);
184s.U = JLINE.from_jline_matrix(jsens.U);
185s.R = JLINE.from_jline_matrix(jsens.R);
186s.dX = JLINE.from_jline_matrix(jsens.dX);
187s.dQ = i_javaMatrixArray3(jsens.dQ);
188s.dU = i_javaMatrixArray3(jsens.dU);
189s.dR = i_javaMatrixArray3(jsens.dR);
190% The JAR keeps the parameter descriptors as three parallel int arrays with
191% type 0 = L / 1 = Z and station -1 for Z; pfqn_sens.m returns one 1 x P struct
192% array with .type 'L'/'Z', .station (0 for Z) and .class. Convert, so that a
193% caller reading sens{e}.params(p) does not have to know which backend ran.
194jType = double(jsens.paramType(:))';
195jStation = double(jsens.paramStation(:))';
196jClass = double(jsens.paramClass(:))';
197P = numel(jType);
198params = struct('type', cell(1, P), 'station', cell(1, P), 'class', cell(1, P));
199for p = 1:P
200 if jType(p) == 0
201 params(p).type = 'L';
202 params(p).station = jStation(p) + 1; % 0-based station -> 1-based
203 else
204 params(p).type = 'Z';
205 params(p).station = 0;
206 end
207 params(p).class = jClass(p) + 1; % 0-based class -> 1-based
208end
209s.params = params;
210if ~isempty(jsens.QVar)
211 s.QVar = JLINE.from_jline_matrix(jsens.QVar);
212end
213if ~isempty(jsens.QTotVar)
214 s.QTotVar = JLINE.from_jline_matrix(jsens.QTotVar);
215end
216s.QCovAsym = double(jsens.QCovAsym);
217if ~isempty(jsens.QCov)
218 % QCov[i][r] is an M x R matrix of Cov[n(i,r),n(j,s)]; pfqn_sens.m returns
219 % the same content as one M x R x M x R array, so index it that way here.
220 M = numel(jsens.QCov);
221 firstRow = jsens.QCov(1);
222 Rn = size(JLINE.from_jline_matrix(firstRow(1)), 2);
223 s.QCov = zeros(M, Rn, M, Rn);
224 for i = 1:M
225 row = jsens.QCov(i);
226 for r = 1:Rn
227 s.QCov(i, r, :, :) = JLINE.from_jline_matrix(row(r));
228 end
229 end
230end
231end
232
233function A = i_javaMatrixArray3(jarr)
234% I_JAVAMATRIXARRAY3 Matrix[] of P entries, each M x R -> M x R x P array,
235% matching the shape pfqn_sens returns in MATLAB (dQ(ist,c,p)).
236P = numel(jarr);
237if P == 0
238 A = [];
239 return
240end
241first = JLINE.from_jline_matrix(jarr(1));
242A = zeros(size(first, 1), size(first, 2), P);
243A(:, :, 1) = first;
244for p = 2:P
245 A(:, :, p) = JLINE.from_jline_matrix(jarr(p));
246end
247end