LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
sn_nonmarkov_toph.m
1function sn = sn_nonmarkov_toph(sn, options)
2% SN = SN_NONMARKOV_TOPH(SN, OPTIONS)
3% Convert non-Markovian distributions to PH using specified approximation method
4%
5% This function scans all service and arrival processes in the network
6% structure and converts non-Markovian distributions to Markovian Arrival
7% Processes (MAPs) using the specified approximation method.
8%
9% Input:
10% sn: Network structure from getStruct()
11% options: Solver options structure with fields:
12% - config.nonmkv: Method for conversion ('none', 'bernstein')
13% - config.nonmkvorder: Number of phases for approximation (default 20)
14%
15% Output:
16% sn: Updated network structure with converted processes
17%
18% Copyright (c) 2012-2026, Imperial College London
19% All rights reserved.
20
21% Get non-Markovian conversion method from options (default 'bernstein')
22if isfield(options, 'config') && isfield(options.config, 'nonmkv')
23 nonmkvMethod = options.config.nonmkv;
24else
25 nonmkvMethod = 'bernstein';
26end
27
28% If method is 'none', return without any conversion
29if strcmpi(nonmkvMethod, 'none')
30 return;
31end
32
33% Get number of phases from options (default 20)
34if isfield(options, 'config') && isfield(options.config, 'nonmkvorder')
35 nPhases = options.config.nonmkvorder;
36else
37 nPhases = 20;
38end
39
40% Check if we should preserve deterministic distributions for exact MAP/D/c analysis
41if isfield(options, 'config') && isfield(options.config, 'preserveDet')
42 preserveDet = options.config.preserveDet;
43else
44 preserveDet = false;
45end
46
47% Markovian ProcessType IDs (no conversion needed)
48markovianTypes = [ProcessType.EXP, ProcessType.ERLANG, ProcessType.HYPEREXP, ...
49 ProcessType.PH, ProcessType.APH, ProcessType.MAP, ...
50 ProcessType.DMAP, ProcessType.MMAP, ...
51 ProcessType.ME, ProcessType.RAP, ...
52 ProcessType.COXIAN, ProcessType.COX2, ProcessType.MMPP2, ...
53 ProcessType.IMMEDIATE, ProcessType.DISABLED];
54
55M = sn.nstations;
56K = sn.nclasses;
57
58for ist = 1:M
59 for r = 1:K
60 procType = sn.procid(ist, r);
61
62 % Skip if procType is NaN (e.g., for Transition nodes in SPNs)
63 if isnan(procType)
64 continue;
65 end
66
67 % Skip if already Markovian, disabled, or immediate
68 if any(procType == markovianTypes)
69 continue;
70 end
71
72 % Skip NHPP (non-homogeneous Poisson): it is not a renewal
73 % distribution to phase-type approximate. Its piecewise-constant
74 % intensity is honoured by the fluid rate multiplier
75 % (solver_fluid_ratemult); the process keeps its single-phase nominal
76 % (time-average) representation here. Erlang-approximating it would
77 % expand phasessz while the Source state (no server-phase column) stays
78 % put, corrupting State.toMarginal.
79 if procType == ProcessType.NHPP
80 continue;
81 end
82
83 % Non-Markovian: need conversion (unless preserveDet for Det)
84 distName = ProcessType.toText(procType);
85 targetMean = 1 / sn.rates(ist, r);
86
87 % Check if we should skip Det conversion for exact MAP/D/c analysis
88 if procType == ProcessType.DET && preserveDet
89 % Skip Det - will be handled by exact MAP/D/c solver
90 continue;
91 end
92
93 % Issue warning for distributions that will be converted
94 line_warning(mfilename, ...
95 'Distribution %s at station %d class %d is non-Markovian and will be converted to PH (%d phases).\n', ...
96 distName, ist, r, nPhases);
97
98 % Get PDF based on distribution type and stored parameters
99 origProc = sn.proc{ist}{r};
100
101 switch procType
102 case ProcessType.GAMMA
103 shape = origProc{1};
104 scale = origProc{2};
105 pdf_func = @(x) gampdf(x, shape, scale);
106
107 case ProcessType.WEIBULL
108 shape_param = origProc{1}; % r
109 scale_param = origProc{2}; % alpha
110 pdf_func = @(x) wblpdf(x, scale_param, shape_param);
111
112 case ProcessType.LOGNORMAL
113 mu = origProc{1};
114 sigma = origProc{2};
115 pdf_func = @(x) lognpdf(x, mu, sigma);
116
117 case ProcessType.PARETO
118 shape_param = origProc{1}; % alpha
119 scale_param = origProc{2}; % k (minimum value)
120 % Pareto PDF: alpha * k^alpha / x^(alpha+1) for x >= k
121 pdf_func = @(x) (x >= scale_param) .* shape_param .* scale_param.^shape_param ./ x.^(shape_param + 1);
122
123 case ProcessType.UNIFORM
124 minVal = origProc{1};
125 maxVal = origProc{2};
126 pdf_func = @(x) (x >= minVal & x <= maxVal) / (maxVal - minVal);
127
128 case ProcessType.DET
129 % Deterministic: use Erlang approximation (preserveDet case already handled above)
130 MAP = map_erlang(targetMean, nPhases);
131 sn = updateSnForMAP(sn, ist, r, MAP, nPhases);
132 continue;
133
134 otherwise
135 % Generic fallback: Erlang approximation
136 MAP = map_erlang(targetMean, nPhases);
137 sn = updateSnForMAP(sn, ist, r, MAP, nPhases);
138 continue;
139 end
140
141 % Apply Bernstein approximation and rescale to target mean
142 MAP = map_bernstein(pdf_func, nPhases);
143 MAP = map_scale(MAP, targetMean);
144
145 % Update the network structure for the converted MAP
146 actualPhases = size(MAP{1}, 1);
147 sn = updateSnForMAP(sn, ist, r, MAP, actualPhases);
148 end
149end
150
151% ---------------------------------------------------------------------
152% SPN Transition firing distributions
153% ---------------------------------------------------------------------
154% Walk every Transition node and convert non-Markovian firing
155% distributions (Det / Gamma / Weibull / Pareto / Uniform / Lognormal)
156% to a phase-type approximation. Mirrors the per-station loop above.
157if isfield(sn, 'nodeparam') && ~isempty(sn.nodeparam)
158 for ind = 1:sn.nnodes
159 if ind > length(sn.nodeparam) || isempty(sn.nodeparam{ind})
160 continue;
161 end
162 if sn.nodetype(ind) ~= NodeType.Transition
163 continue;
164 end
165 nparam = sn.nodeparam{ind};
166 nmodes_t = nparam.nmodes;
167 for m = 1:nmodes_t
168 % Markovian distributions (Exp/Erlang/HyperExp/PH/APH/...) are
169 % populated with a valid (D0,D1) PH and a finite firingphases by
170 % refreshPetriNetNodes. Skip them here.
171 phases_m = NaN;
172 if length(nparam.firingphases) >= m
173 phases_m = nparam.firingphases(m);
174 end
175 if ~isnan(phases_m) && phases_m > 0
176 continue;
177 end
178
179 % Resolve the original distribution's process-type id and target mean.
180 procidT = nparam.firingprocid(m);
181 if isnan(procidT)
182 continue;
183 end
184 if any(procidT == markovianTypes)
185 continue;
186 end
187
188 % Pull the user-supplied parameters from firingproc{m}.
189 origProc = nparam.firingproc{m};
190 if ~iscell(origProc) || isempty(origProc)
191 continue;
192 end
193
194 distName = ProcessType.toText(procidT);
195 line_warning(mfilename, ...
196 'Firing distribution %s at Transition node %d mode %d is non-Markovian and will be converted to PH (%d phases).\n', ...
197 distName, ind, m, nPhases);
198
199 switch procidT
200 case ProcessType.GAMMA
201 shape = origProc{1}; scale = origProc{2};
202 targetMean_t = shape * scale;
203 pdf_func = @(x) gampdf(x, shape, scale);
204 case ProcessType.WEIBULL
205 % Stored order matches MATLAB Weibull.getProcess: {r, alpha}
206 rWb = origProc{1}; alphaWb = origProc{2};
207 targetMean_t = alphaWb * gamma(1 + 1/rWb);
208 pdf_func = @(x) wblpdf(x, alphaWb, rWb);
209 case ProcessType.LOGNORMAL
210 muL = origProc{1}; sigmaL = origProc{2};
211 targetMean_t = exp(muL + sigmaL^2/2);
212 pdf_func = @(x) lognpdf(x, muL, sigmaL);
213 case ProcessType.PARETO
214 alphaP = origProc{1}; kP = origProc{2};
215 if alphaP > 1
216 targetMean_t = alphaP * kP / (alphaP - 1);
217 else
218 targetMean_t = NaN;
219 end
220 pdf_func = @(x) (x >= kP) .* alphaP .* kP.^alphaP ./ x.^(alphaP + 1);
221 case ProcessType.UNIFORM
222 minV = origProc{1}; maxV = origProc{2};
223 targetMean_t = (minV + maxV) / 2;
224 pdf_func = @(x) (x >= minV & x <= maxV) / (maxV - minV);
225 case ProcessType.DET
226 targetMean_t = origProc{1};
227 if preserveDet
228 continue;
229 end
230 MAP = map_erlang(targetMean_t, nPhases);
231 sn = updateNodeparamForMAP(sn, ind, m, MAP);
232 continue;
233 otherwise
234 continue;
235 end
236
237 if ~isfinite(targetMean_t) || targetMean_t <= 0
238 MAP = map_erlang(1.0, nPhases);
239 else
240 try
241 MAP = map_bernstein(pdf_func, nPhases);
242 MAP = map_scale(MAP, targetMean_t);
243 catch
244 MAP = map_erlang(targetMean_t, nPhases);
245 end
246 end
247
248 sn = updateNodeparamForMAP(sn, ind, m, MAP);
249 end
250 end
251end
252
253end
254
255
256function sn = updateNodeparamForMAP(sn, ind, m, MAP)
257% UPDATENODEPARAMFORMAP Update Transition mode firing process to a
258% phase-type representation. Mirrors updateSnForMAP for stations.
259nparam = sn.nodeparam{ind};
260actualPhases = size(MAP{1}, 1);
261
262nparam.firingproc{m} = MAP;
263nparam.firingphases(m) = actualPhases;
264nparam.firingpie{m} = map_pie(MAP);
265nparam.firingprocid(m) = ProcessType.MAP;
266
267sn.nodeparam{ind} = nparam;
268end
269
270function sn = updateSnForMAP(sn, ist, r, MAP, nPhases)
271% UPDATESNFORMAP Update all network structure fields for converted MAP
272%
273% Updates proc, procid, phases, phasessz, phaseshift, mu, phi, pie, nvars, state
274
275% Save old phasessz before updating (needed for state expansion)
276oldPhases = sn.phasessz(ist, r);
277
278% Update process representation
279sn.proc{ist}{r} = MAP;
280% The conversion methods (map_bernstein, map_erlang) always produce a renewal
281% PH (D1 = exit_rates * pie), so tag as PH at every station: renewal processes
282% need no MAP phase-restart local variable, and PH service is supported by all
283% scheduling policies (MAP service is FCFS-only, see State.fromMarginal).
284sn.procid(ist, r) = ProcessType.PH;
285sn.phases(ist, r) = nPhases;
286
287% Update phasessz and phaseshift (derived from phases)
288sn.phasessz(ist, r) = max(nPhases, 1);
289% Recompute phaseshift for this station (cumulative sum across classes)
290sn.phaseshift(ist, :) = [0, cumsum(sn.phasessz(ist, :))];
291
292% Update mu (rates from -diag(D0))
293sn.mu{ist}{r} = -diag(MAP{1});
294
295% Update phi (completion probabilities: sum(D1,2) / -diag(D0))
296D0_diag = -diag(MAP{1});
297D1_rowsum = sum(MAP{2}, 2);
298sn.phi{ist}{r} = D1_rowsum ./ D0_diag;
299
300% Update pie (initial phase distribution)
301sn.pie{ist}{r} = map_pie(MAP);
302
303% Expand the server-phase columns of any pre-initialized state (renewal PH:
304% no local-variable column is added, skip for Sources which have no local state)
305ind = sn.stationToNode(ist);
306if sn.sched(ist) ~= SchedStrategy.EXT
307 sn = expandStateForMAP(sn, ind, r, oldPhases, nPhases);
308end
309
310% Add PHASE sync event if phases > 1 and not already present
311if nPhases > 1
312 sn = addPhaseSyncIfNeeded(sn, ind, r);
313end
314end
315
316function sn = expandStateForMAP(sn, ind, r, oldPhases, newPhases)
317% EXPANDSTATEFORMAP Expand state vector for renewal-PH conversion
318%
319% When converting a non-Markovian distribution to a renewal PH, the server
320% portion (space_srv) of any pre-initialized state needs additional columns
321% for the extra phases. No local-variable column is needed (renewal PH has
322% no phase memory across jobs).
323%
324% State format: [space_buf | space_srv | space_var]
325% - space_srv has sum(phasessz) columns total
326% - space_var has sum(nvars) columns total
327
328isf = sn.nodeToStateful(ind);
329if isf <= 0 || isempty(sn.state) || isempty(sn.state{isf})
330 return;
331end
332
333ist = sn.nodeToStation(ind);
334nRows = size(sn.state{isf}, 1);
335
336% Calculate state vector structure (nvars is unchanged by the conversion)
337V_old = sum(sn.nvars(ind, :));
338
339% K = phases array for this station (already updated for this class)
340K = sn.phasessz(ist, :);
341sumK_new = sum(K);
342K_old = K;
343K_old(r) = oldPhases; % What it was before
344sumK_old = sum(K_old);
345
346% Phaseshift tells us where each class's phases start
347% For class r, server phases are at positions phaseshift(r)+1 to phaseshift(r)+K(r)
348% But phaseshift has already been updated, so compute old positions
349Ks_old = [0, cumsum(K_old)];
350
351% Current state dimensions
352currentCols = size(sn.state{isf}, 2);
353
354% Calculate buffer size (state columns before space_srv)
355% Expected: currentCols = bufSize + sumK_old + V_old
356bufSize = currentCols - sumK_old - V_old;
357if bufSize < 0
358 bufSize = 0;
359end
360
361% Extract state portions
362if bufSize > 0
363 space_buf = sn.state{isf}(:, 1:bufSize);
364else
365 space_buf = zeros(nRows, 0);
366end
367
368if sumK_old > 0
369 space_srv = sn.state{isf}(:, bufSize+1:bufSize+sumK_old);
370else
371 space_srv = zeros(nRows, 0);
372end
373
374if V_old > 0
375 space_var = sn.state{isf}(:, bufSize+sumK_old+1:end);
376else
377 space_var = zeros(nRows, 0);
378end
379
380% Expand space_srv: insert (newPhases - oldPhases) zeros after class r's position
381phasesToAdd = newPhases - oldPhases;
382if phasesToAdd > 0
383 % Position where class r's phases end (in space_srv)
384 insertPos = Ks_old(r) + oldPhases;
385
386 % Insert zeros for the new phases
387 space_srv_new = [space_srv(:, 1:insertPos), ...
388 zeros(nRows, phasesToAdd), ...
389 space_srv(:, insertPos+1:end)];
390else
391 space_srv_new = space_srv;
392end
393
394% Reconstruct state (space_var unchanged: renewal PH adds no local variable)
395sn.state{isf} = [space_buf, space_srv_new, space_var];
396
397% Also update space if it exists
398if isfield(sn, 'space') && ~isempty(sn.space) && ~isempty(sn.space{isf})
399 nRowsSpace = size(sn.space{isf}, 1);
400 currentColsSpace = size(sn.space{isf}, 2);
401
402 % Same calculation for space
403 bufSizeSpace = currentColsSpace - sumK_old - V_old;
404 if bufSizeSpace < 0
405 bufSizeSpace = 0;
406 end
407
408 if bufSizeSpace > 0
409 space_buf_s = sn.space{isf}(:, 1:bufSizeSpace);
410 else
411 space_buf_s = zeros(nRowsSpace, 0);
412 end
413
414 if sumK_old > 0
415 space_srv_s = sn.space{isf}(:, bufSizeSpace+1:bufSizeSpace+sumK_old);
416 else
417 space_srv_s = zeros(nRowsSpace, 0);
418 end
419
420 if V_old > 0
421 space_var_s = sn.space{isf}(:, bufSizeSpace+sumK_old+1:end);
422 else
423 space_var_s = zeros(nRowsSpace, 0);
424 end
425
426 if phasesToAdd > 0
427 space_srv_s_new = [space_srv_s(:, 1:insertPos), ...
428 zeros(nRowsSpace, phasesToAdd), ...
429 space_srv_s(:, insertPos+1:end)];
430 else
431 space_srv_s_new = space_srv_s;
432 end
433
434 sn.space{isf} = [space_buf_s, space_srv_s_new, space_var_s];
435end
436end
437
438function sn = addPhaseSyncIfNeeded(sn, ind, r)
439% ADDPHASESYNCIFNEEDED Add a PHASE sync event for converted MAP
440%
441% When a non-Markovian distribution is converted to a MAP with multiple phases,
442% we need to add a PHASE sync event so that phase transitions can occur
443% during simulation.
444
445% Check if PHASE sync already exists for this node/class
446phaseSyncExists = false;
447local = sn.nnodes + 1;
448
449if ~isempty(sn.sync)
450 for s = 1:length(sn.sync)
451 if ~isempty(sn.sync{s}) && ~isempty(sn.sync{s}.active) && ~isempty(sn.sync{s}.active{1})
452 activeEvent = sn.sync{s}.active{1};
453 if activeEvent.event == EventType.PHASE && activeEvent.node == ind && activeEvent.class == r
454 phaseSyncExists = true;
455 break;
456 end
457 end
458 end
459end
460
461% Add PHASE sync if not present
462if ~phaseSyncExists
463 newSync = struct('active', cell(1), 'passive', cell(1));
464 newSync.active{1} = Event(EventType.PHASE, ind, r);
465 newSync.passive{1} = Event(EventType.LOCAL, local, r, 1.0);
466 sn.sync{end+1, 1} = newSync;
467end
468end
Definition fjtag.m:157
Definition Station.m:245