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