1function [SensTable, sens] = getSensitivityTable(self, varargin)
2% GETSENSITIVITYTABLE Layer-wise performance sensitivities of a layered network.
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.
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.
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.
28if (GlobalConstants.DummyMode)
34% see _kb/06-solver-catalog.md (LN section) for rationale
36 [SensTable, sens] = i_javaSensitivityTable(self, varargin{:});
38elseif ~isempty(self.pyMode) && self.pyMode
39 [SensTable, sens] = PYLINE.getLNSensitivityTable(self.model, self.options, varargin{:});
43% see _kb/06-solver-catalog.md (LN section) for rationale
44if isempty(self.results)
48E = self.getNumberOfModels();
49Layer = {}; Station = {}; JobClass = {};
50dTput = []; dRespT = []; dQLen = []; dUtil = [];
55 solver = self.solvers{e};
59 [T, sens{e}] = solver.getSensitivityTable(varargin{:});
60 methods{e} = T.Properties.UserData.method;
61 layerName = self.ensemble{e}.getName();
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>
73SensTable = table(Layer, Station, JobClass, dTput, dRespT, dQLen, dUtil, ...
74 'VariableNames
', {'Layer
', 'Station', 'JobClass
', 'dTput_dRate
', ...
75 'dRespT_dRate
', 'dQLen_dRate
', 'dUtil_dRate
'});
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));
82elseif all(strcmp(present, present{1}))
87SensTable.Properties.UserData = struct('method
', summary, 'layerMethods
', {methods});
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{:});
97 step = NaN; % NaN selects the JAR-side default, as in the Java signature
99jt = self.obj.getSensitivityTable(opt.method, step, opt.scheme);
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());
109SensTable = table(Layer, Station, JobClass, dTput, dRespT, dQLen, dUtil, ...
110 'VariableNames
', {'Layer
', 'Station', 'JobClass
', 'dTput_dRate
', ...
111 'dRespT_dRate
', 'dQLen_dRate
', 'dUtil_dRate
'});
113methods = i_javaStringList(jt.getLayerMethods())';
114jsens = jt.getLayerSens();
115sens = cell(1, numel(methods));
117 sens{e} = i_javaSensStruct(jsens.get(e-1));
119SensTable.Properties.UserData =
struct(
'method', char(jt.getMethod()), ...
120 'layerMethods', {methods});
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.');
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}));
136 opt.(name) = varargin{a+1};
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''.');
143if ~ismember(opt.scheme, {
'forward',
'central'})
144 line_error(mfilename,
'The scheme must be ''forward'' or ''central''.');
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.
164function v = i_javaDoubleList(jlist)
165% I_JAVADOUBLELIST java.util.List<Double> ->
column double vector.
169 v(i) = double(jlist.get(i-1));
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.
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(:))
';
198params = struct('type
', cell(1, P), 'station
', cell(1, P), 'class
', cell(1, P));
201 params(p).type = 'L
';
202 params(p).station = jStation(p) + 1; % 0-based station -> 1-based
204 params(p).type = 'Z
';
205 params(p).station = 0;
207 params(p).class = jClass(p) + 1; % 0-based class -> 1-based
210if ~isempty(jsens.QVar)
211 s.QVar = JLINE.from_jline_matrix(jsens.QVar);
213if ~isempty(jsens.QTotVar)
214 s.QTotVar = JLINE.from_jline_matrix(jsens.QTotVar);
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);
227 s.QCov(i, r, :, :) = JLINE.from_jline_matrix(row(r));
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)).
241first = JLINE.from_jline_matrix(jarr(1));
242A = zeros(size(first, 1), size(first, 2), P);
245 A(:, :, p) = JLINE.from_jline_matrix(jarr(p));