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% 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.
40 [SensTable, sens] = i_javaSensitivityTable(self, varargin{:});
42elseif ~isempty(self.pyMode) && self.pyMode
43 [SensTable, sens] = PYLINE.getLNSensitivityTable(self.model, self.options, varargin{:});
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)
57E = self.getNumberOfModels();
58Layer = {}; Station = {}; JobClass = {};
59dTput = []; dRespT = []; dQLen = []; dUtil = [];
64 solver = self.solvers{e};
68 [T, sens{e}] = solver.getSensitivityTable(varargin{:});
69 methods{e} = T.Properties.UserData.method;
70 layerName = self.ensemble{e}.getName();
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>
82SensTable = table(Layer, Station, JobClass, dTput, dRespT, dQLen, dUtil, ...
83 'VariableNames
', {'Layer
', 'Station', 'JobClass
', 'dTput_dRate
', ...
84 'dRespT_dRate
', 'dQLen_dRate
', 'dUtil_dRate
'});
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));
91elseif all(strcmp(present, present{1}))
96SensTable.Properties.UserData = struct('method
', summary, 'layerMethods
', {methods});
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{:});
106 step = NaN; % NaN selects the JAR-side default, as in the Java signature
108jt = self.obj.getSensitivityTable(opt.method, step, opt.scheme);
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());
118SensTable = table(Layer, Station, JobClass, dTput, dRespT, dQLen, dUtil, ...
119 'VariableNames
', {'Layer
', 'Station', 'JobClass
', 'dTput_dRate
', ...
120 'dRespT_dRate
', 'dQLen_dRate
', 'dUtil_dRate
'});
122methods = i_javaStringList(jt.getLayerMethods())';
123jsens = jt.getLayerSens();
124sens = cell(1, numel(methods));
126 sens{e} = i_javaSensStruct(jsens.get(e-1));
128SensTable.Properties.UserData =
struct(
'method', char(jt.getMethod()), ...
129 'layerMethods', {methods});
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.');
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}));
145 opt.(name) = varargin{a+1};
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''.');
152if ~ismember(opt.scheme, {
'forward',
'central'})
153 line_error(mfilename,
'The scheme must be ''forward'' or ''central''.');
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.
173function v = i_javaDoubleList(jlist)
174% I_JAVADOUBLELIST java.util.List<Double> ->
column double vector.
178 v(i) = double(jlist.get(i-1));
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.
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(:))
';
207params = struct('type
', cell(1, P), 'station
', cell(1, P), 'class
', cell(1, P));
210 params(p).type = 'L
';
211 params(p).station = jStation(p) + 1; % 0-based station -> 1-based
213 params(p).type = 'Z
';
214 params(p).station = 0;
216 params(p).class = jClass(p) + 1; % 0-based class -> 1-based
219if ~isempty(jsens.QVar)
220 s.QVar = JLINE.from_jline_matrix(jsens.QVar);
222if ~isempty(jsens.QTotVar)
223 s.QTotVar = JLINE.from_jline_matrix(jsens.QTotVar);
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);
236 s.QCov(i, r, :, :) = JLINE.from_jline_matrix(row(r));
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)).
250first = JLINE.from_jline_matrix(jarr(1));
251A = zeros(size(first, 1), size(first, 2), P);
254 A(:, :, p) = JLINE.from_jline_matrix(jarr(p));