LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
runAnalyzer.m
1function [runtime, tranSysState, tranSync] = runAnalyzer(self, options)
2% [RUNTIME, TRANSYSSTATE] = RUNANALYZER()
3%
4% Run the LDES solver on the queueing network model.
5%
6% Communication with the LDES engine is fully JSON-mediated: the model is
7% serialized to model.json (linemodel_save) and the engine is run as a
8% subprocess ("solve model.json -o result.json"); the result JSON is parsed
9% back here. No in-process Java object marshalling (JPype/JLINE) is used for
10% regular Network models. LayeredNetwork (LQN) models are still simulated by
11% the Java LDES ensemble backend (self.obj), which is a separate path.
12
13T0 = tic;
14if nargin < 2
15 options = self.getOptions;
16end
17
18% options.events (DES event budget) overrides options.samples when set;
19% samples remains accepted as a deprecated alias for the event budget.
20if isfield(options,'events') && ~isempty(options.events) && isfinite(options.events) && options.events > 0
21 options.samples = round(options.events);
22end
23
24tranSysState = [];
25tranSync = [];
26
27if isa(self.model, 'LayeredNetwork')
28 self.obj.getAvg(); % runs the LN LDES analyzer (Java ensemble backend)
29 runtime = toc(T0);
30 return
31end
32
33self.runAnalyzerChecks(options);
34Solver.resetRandomGeneratorSeed(options.seed);
35
36line_debug('LDES solver starting: samples=%d, seed=%d', options.samples, options.seed);
37
38sn = self.model.getStruct;
39M = sn.nstations;
40R = sn.nclasses;
41[confintEnabled, ~] = Solver.parseConfInt(options.confint);
42
43isTransient = isfield(options, 'timespan') && numel(options.timespan) >= 2 && ...
44 isfinite(options.timespan(2));
45
46extraFlags = {};
47if isTransient
48 extraFlags = {'--timespan', ...
49 sprintf('%.10g,%.10g', options.timespan(1), options.timespan(2)), ...
50 '--trajectory'};
51 % Ensemble transient: a single realization is not the transient mean
52 % E[N](t) (no time-ergodicity at fixed t). When replications > 1 is
53 % requested, drive the engine's parallel analyzer via --replications so
54 % the per-bucket QNt/UNt/TNt are averaged across independent replications.
55 % Read from options.replications or options.config.replications; default
56 % (absent/<=1) keeps the single-path run.
57 reps = 0;
58 if isfield(options, 'replications') && ~isempty(options.replications)
59 reps = options.replications;
60 elseif isfield(options, 'config') && isfield(options.config, 'replications') ...
61 && ~isempty(options.config.replications)
62 reps = options.config.replications;
63 end
64 if reps > 1
65 extraFlags{end+1} = '--replications';
66 extraFlags{end+1} = sprintf('%d', round(reps));
67 if isfield(options, 'config') && isfield(options.config, 'numthreads') ...
68 && ~isempty(options.config.numthreads) && options.config.numthreads > 0
69 extraFlags{end+1} = '--numthreads';
70 extraFlags{end+1} = sprintf('%d', round(options.config.numthreads));
71 end
72 end
73end
74
75data = self.solveCli(options, extraFlags);
76if ~isstruct(data) || ~isfield(data, 'metrics')
77 line_error(mfilename, 'LDES engine returned no metrics.');
78end
79
80if isTransient
81 parseTransient(self, data, M, R, options);
82else
83 parseSteady(self, data, sn, M, R, confintEnabled, options);
84end
85
86runtime = toc(T0);
87end
88
89% =========================================================================
90
91function parseSteady(self, data, sn, M, R, confintEnabled, options)
92% PARSESTEADY Marshal steady-state result JSON onto the solver.
93
94QN = ldesJson2mat(data.metrics.QN, M, R);
95UN = ldesJson2mat(data.metrics.UN, M, R);
96RN = ldesJson2mat(data.metrics.RN, M, R);
97TN = ldesJson2mat(data.metrics.TN, M, R);
98AN = ldesJson2mat(data.metrics.AN, M, R);
99WN = ldesJson2mat(data.metrics.WN, M, R);
100CN = ldesJson2mat(data.metrics.CN, 1, R);
101XN = ldesJson2mat(data.metrics.XN, 1, R);
102if isempty(QN)
103 line_error('runAnalyzer', 'LDES result metrics could not be parsed.');
104end
105
106% Fork-Join quorum sibling-drop rate (station-indexed), folded into
107% getAvgLossTable at the Join rows. Absent unless the model has a quorum Join.
108DropRateJoin = ldesJson2mat(ldesGetField(data.metrics, 'DropRateJoin', []), M, R);
109if ~isempty(DropRateJoin)
110 self.result.DropRateJoin = DropRateJoin;
111end
112
113if isfield(data, 'runtime') && isscalar(data.runtime) && isnumeric(data.runtime)
114 runtime = data.runtime;
115else
116 runtime = 0;
117end
118
119% Finite capacity region (FCR) metrics: append region rows after the stations.
120F = sn.nregions;
121WeightNfcr = [];
122MemOccNfcr = [];
123if F > 0 && isfield(data, 'fcr')
124 fcr = data.fcr;
125 QNfcr = ldesJson2mat(ldesGetField(fcr, 'QNfcr', []), F, R);
126 UNfcr = ldesJson2mat(ldesGetField(fcr, 'UNfcr', []), F, R);
127 RNfcr = ldesJson2mat(ldesGetField(fcr, 'RNfcr', []), F, R);
128 TNfcr = ldesJson2mat(ldesGetField(fcr, 'TNfcr', []), F, R);
129 WNfcr = ldesJson2mat(ldesGetField(fcr, 'WNfcr', []), F, R);
130 if ~isempty(QNfcr)
131 ANfcr = NaN(F, R); % arrival rate not applicable to FCR rows
132 QN = [QN; QNfcr];
133 UN = [UN; UNfcr];
134 RN = [RN; RNfcr];
135 TN = [TN; TNfcr];
136 WN = [WN; WNfcr];
137 AN = [AN; ANfcr];
138 WeightNfcr = ldesJson2mat(ldesGetField(fcr, 'WeightNfcr', []), F, R);
139 MemOccNfcr = ldesJson2mat(ldesGetField(fcr, 'MemOccNfcr', []), F, R);
140 if isempty(WeightNfcr), WeightNfcr = zeros(F, R); end
141 if isempty(MemOccNfcr), MemOccNfcr = zeros(F, R); end
142 % Region carried throughput and drop rate for getAvgRegionLossTable.
143 DropRateNfcr = ldesJson2mat(ldesGetField(fcr, 'DropRateNfcr', []), F, R);
144 if isempty(DropRateNfcr), DropRateNfcr = zeros(F, R); end
145 self.result.FCR.TNfcr = TNfcr;
146 self.result.FCR.DropRateNfcr = DropRateNfcr;
147 end
148end
149
150if options.verbose
151 line_printf('LDES samples: %8d\n', options.samples);
152end
153
154self.setAvgResults(QN, UN, RN, TN, AN, WN, CN, XN, runtime, options.method, options.samples);
155
156% FCR-only weighted-occupation (Total Weight) and memory-occupation metrics:
157% NaN at station rows, populated at the FCR rows (M+1 .. M+F).
158self.result.Avg.Weight = NaN(M + F, R);
159self.result.Avg.MemOcc = NaN(M + F, R);
160if F > 0 && ~isempty(WeightNfcr)
161 self.result.Avg.Weight(M+1:M+F, :) = WeightNfcr;
162 self.result.Avg.MemOcc(M+1:M+F, :) = MemOccNfcr;
163end
164
165% Marshal cache hit/miss/latency from the result JSON onto the Cache nodes.
166% Clear any stale split first, then set each field independently so a single
167% missing field does not discard the others (order-independence).
168haveCacheMetrics = isfield(data, 'cacheMetrics');
169for ind = 1:sn.nnodes
170 if sn.nodetype(ind) == NodeType.Cache
171 mcache = self.model.nodes{ind};
172 mcache.setResultHitProb(sparse([]));
173 mcache.setResultMissProb(sparse([]));
174 mcache.setResultDelayedHitProb(sparse([]));
175 mcache.setResultResidT(sparse([]));
176 if haveCacheMetrics
177 cname = matlab.lang.makeValidName(char(mcache.getName()));
178 if isfield(data.cacheMetrics, cname)
179 cm = data.cacheMetrics.(cname);
180 hp = ldesJson2mat(ldesGetField(cm, 'hit', []), [], []);
181 mp = ldesJson2mat(ldesGetField(cm, 'miss', []), [], []);
182 dhp = ldesJson2mat(ldesGetField(cm, 'delayed', []), [], []);
183 lp = ldesJson2mat(ldesGetField(cm, 'latency', []), [], []);
184 hpl = ldesJson2mat(ldesGetField(cm, 'hitList', []), [], []);
185 ip = ldesJson2mat(ldesGetField(cm, 'itemProb', []), [], []);
186 if ~isempty(hp), mcache.setResultHitProb(hp); end
187 if ~isempty(mp), mcache.setResultMissProb(mp); end
188 if ~isempty(dhp), mcache.setResultDelayedHitProb(dhp); end
189 if ~isempty(lp), mcache.setResultResidT(lp); end
190 if ~isempty(hpl), mcache.setResultHitProbList(hpl); end
191 if ~isempty(ip), mcache.setResultItemProb(ip); end
192 end
193 end
194 end
195end
196
197% Confidence intervals (the result JSON always carries them when requested).
198if confintEnabled && isfield(data, 'confidenceIntervals')
199 ci = data.confidenceIntervals;
200 QNCI = ldesJson2mat(ldesGetField(ci, 'QNCI', []), M, R);
201 UNCI = ldesJson2mat(ldesGetField(ci, 'UNCI', []), M, R);
202 RNCI = ldesJson2mat(ldesGetField(ci, 'RNCI', []), M, R);
203 TNCI = ldesJson2mat(ldesGetField(ci, 'TNCI', []), M, R);
204 ANCI = ldesJson2mat(ldesGetField(ci, 'ANCI', []), M, R);
205 WNCI = ldesJson2mat(ldesGetField(ci, 'WNCI', []), M, R);
206 self.setAvgResultsCI(QNCI, UNCI, RNCI, TNCI, ANCI, WNCI, [], []);
207end
208end
209
210% =========================================================================
211
212function parseTransient(self, data, M, R, options)
213% PARSETRANSIENT Marshal the transient trajectory JSON onto the solver.
214% data.transient carries t (time vector) and QNt/UNt/TNt as nested
215% [station][class] arrays, each an (nTimePoints x 2) [value, time] matrix.
216
217runtime = 0;
218if isfield(data, 'runtime') && isscalar(data.runtime) && isnumeric(data.runtime)
219 runtime = data.runtime;
220end
221
222if ~isfield(data, 'transient') || isempty(data.transient) || ~isfield(data.transient, 't')
223 if options.verbose
224 line_printf('LDES transient analysis produced no trajectory.\n');
225 end
226 return;
227end
228tran = data.transient;
229
230QNt = ldesTrajCell(ldesGetField(tran, 'QNt', []), M, R);
231UNt = ldesTrajCell(ldesGetField(tran, 'UNt', []), M, R);
232TNt = ldesTrajCell(ldesGetField(tran, 'TNt', []), M, R);
233RNt = cell(M, R); % response-time transient not computed by LDES
234CNt = cell(1, R);
235XNt = cell(1, R);
236% Empty cells become scalar NaN so downstream indexing (getTranAvg) is uniform.
237for ist = 1:M
238 for r = 1:R
239 if isempty(QNt{ist, r}), QNt{ist, r} = NaN; end
240 if isempty(UNt{ist, r}), UNt{ist, r} = NaN; end
241 if isempty(TNt{ist, r}), TNt{ist, r} = NaN; end
242 RNt{ist, r} = NaN;
243 end
244end
245for r = 1:R
246 CNt{1, r} = NaN;
247 XNt{1, r} = NaN;
248end
249
250self.setTranAvgResults(QNt, UNt, RNt, TNt, CNt, XNt, runtime);
251
252% Steady-state estimates from the final transient values.
253QN = NaN(M, R); UN = NaN(M, R); RN = NaN(M, R); TN = NaN(M, R);
254WN = NaN(M, R); AN = NaN(M, R); CN = NaN(1, R); XN = NaN(1, R);
255for ist = 1:M
256 for r = 1:R
257 if ~isscalar(QNt{ist, r}) && ~isnan(QNt{ist, r}(end, 1))
258 QN(ist, r) = QNt{ist, r}(end, 1);
259 end
260 if ~isscalar(UNt{ist, r}) && ~isnan(UNt{ist, r}(end, 1))
261 UN(ist, r) = UNt{ist, r}(end, 1);
262 end
263 if ~isscalar(TNt{ist, r}) && ~isnan(TNt{ist, r}(end, 1))
264 TN(ist, r) = TNt{ist, r}(end, 1);
265 end
266 end
267end
268self.setAvgResults(QN, UN, RN, TN, AN, WN, CN, XN, runtime, options.method, 0);
269
270if options.verbose
271 line_printf('LDES transient analysis complete, timespan = [%g, %g]\n', ...
272 options.timespan(1), options.timespan(2));
273end
274end
275
276% =========================================================================