1function [runtime, tranSysState, tranSync] = runAnalyzer(self, options)
2% [RUNTIME, TRANSYSSTATE] = RUNANALYZER()
4% Run the LDES solver on the queueing network model.
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.
15 options = self.getOptions;
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);
27if isa(self.model,
'LayeredNetwork')
28 self.obj.getAvg(); % runs the LN LDES analyzer (Java ensemble backend)
33self.runAnalyzerChecks(options);
34Solver.resetRandomGeneratorSeed(options.seed);
36line_debug('LDES solver starting: samples=%d, seed=%d', options.samples, options.seed);
38sn = self.model.getStruct;
41[confintEnabled, ~] = Solver.parseConfInt(options.confint);
43isTransient = isfield(options, 'timespan') && numel(options.timespan) >= 2 && ...
44 isfinite(options.timespan(2));
48 extraFlags = {
'--timespan', ...
49 sprintf(
'%.10g,%.10g', options.timespan(1), options.timespan(2)), ...
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.
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;
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));
75data = self.solveCli(options, extraFlags);
76if ~isstruct(data) || ~isfield(data, 'metrics
')
77 line_error(mfilename, 'LDES engine returned no metrics.
');
81 parseTransient(self, data, M, R, options);
83 parseSteady(self, data, sn, M, R, confintEnabled, options);
89% =========================================================================
91function parseSteady(self, data, sn, M, R, confintEnabled, options)
92% PARSESTEADY Marshal steady-state result JSON onto the solver.
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);
103 line_error('runAnalyzer
', 'LDES result metrics could not be parsed.
');
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;
113if isfield(data, 'runtime
') && isscalar(data.runtime) && isnumeric(data.runtime)
114 runtime = data.runtime;
119% Finite capacity region (FCR) metrics: append region rows after the stations.
123if F > 0 && isfield(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);
131 ANfcr = NaN(F, R); % arrival rate not applicable to FCR rows
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;
151 line_printf('LDES samples: %8d\n
', options.samples);
154self.setAvgResults(QN, UN, RN, TN, AN, WN, CN, XN, runtime, options.method, options.samples);
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;
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
');
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([]));
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
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, [], []);
210% =========================================================================
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.
218if isfield(data, 'runtime
') && isscalar(data.runtime) && isnumeric(data.runtime)
219 runtime = data.runtime;
222if ~isfield(data, 'transient
') || isempty(data.transient) || ~isfield(data.transient, 't
')
224 line_printf('LDES transient analysis produced no trajectory.\n
');
228tran = data.transient;
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
236% Empty cells become scalar NaN so downstream indexing (getTranAvg) is uniform.
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
250self.setTranAvgResults(QNt, UNt, RNt, TNt, CNt, XNt, runtime);
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);
257 if ~isscalar(QNt{ist, r}) && ~isnan(QNt{ist, r}(end, 1))
258 QN(ist, r) = QNt{ist, r}(end, 1);
260 if ~isscalar(UNt{ist, r}) && ~isnan(UNt{ist, r}(end, 1))
261 UN(ist, r) = UNt{ist, r}(end, 1);
263 if ~isscalar(TNt{ist, r}) && ~isnan(TNt{ist, r}(end, 1))
264 TN(ist, r) = TNt{ist, r}(end, 1);
268self.setAvgResults(QN, UN, RN, TN, AN, WN, CN, XN, runtime, options.method, 0);
271 line_printf('LDES transient analysis complete, timespan = [%g, %g]\n
', ...
272 options.timespan(1), options.timespan(2));
276% =========================================================================