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.COXIAN, ProcessType.COX2, ProcessType.MMPP2, ...
51 ProcessType.IMMEDIATE, ProcessType.DISABLED];
52
53M = sn.nstations;
54K = sn.nclasses;
55
56for ist = 1:M
57 for r = 1:K
58 procType = sn.procid(ist, r);
59
60 % Skip if procType is NaN (e.g., for Transition nodes in SPNs)
61 if isnan(procType)
62 continue;
63 end
64
65 % Skip if already Markovian, disabled, or immediate
66 if any(procType == markovianTypes)
67 continue;
68 end
69
70 % Non-Markovian: need conversion (unless preserveDet for Det)
71 distName = ProcessType.toText(procType);
72 targetMean = 1 / sn.rates(ist, r);
73
74 % Check if we should skip Det conversion for exact MAP/D/c analysis
75 if procType == ProcessType.DET && preserveDet
76 % Skip Det - will be handled by exact MAP/D/c solver
77 continue;
78 end
79
80 % Issue warning for distributions that will be converted
81 line_warning(mfilename, ...
82 'Distribution %s at station %d class %d is non-Markovian and will be converted to PH (%d phases).\n', ...
83 distName, ist, r, nPhases);
84
85 % Get PDF based on distribution type and stored parameters
86 origProc = sn.proc{ist}{r};
87
88 switch procType
89 case ProcessType.GAMMA
90 shape = origProc{1};
91 scale = origProc{2};
92 pdf_func = @(x) gampdf(x, shape, scale);
93
94 case ProcessType.WEIBULL
95 shape_param = origProc{1}; % r
96 scale_param = origProc{2}; % alpha
97 pdf_func = @(x) wblpdf(x, scale_param, shape_param);
98
99 case ProcessType.LOGNORMAL
100 mu = origProc{1};
101 sigma = origProc{2};
102 pdf_func = @(x) lognpdf(x, mu, sigma);
103
104 case ProcessType.PARETO
105 shape_param = origProc{1}; % alpha
106 scale_param = origProc{2}; % k (minimum value)
107 % Pareto PDF: alpha * k^alpha / x^(alpha+1) for x >= k
108 pdf_func = @(x) (x >= scale_param) .* shape_param .* scale_param.^shape_param ./ x.^(shape_param + 1);
109
110 case ProcessType.UNIFORM
111 minVal = origProc{1};
112 maxVal = origProc{2};
113 pdf_func = @(x) (x >= minVal & x <= maxVal) / (maxVal - minVal);
114
115 case ProcessType.DET
116 % Deterministic: use Erlang approximation (preserveDet case already handled above)
117 MAP = map_erlang(targetMean, nPhases);
118 sn = updateSnForMAP(sn, ist, r, MAP, nPhases);
119 continue;
120
121 otherwise
122 % Generic fallback: Erlang approximation
123 MAP = map_erlang(targetMean, nPhases);
124 sn = updateSnForMAP(sn, ist, r, MAP, nPhases);
125 continue;
126 end
127
128 % Apply Bernstein approximation and rescale to target mean
129 MAP = map_bernstein(pdf_func, nPhases);
130 MAP = map_scale(MAP, targetMean);
131
132 % Update the network structure for the converted MAP
133 actualPhases = size(MAP{1}, 1);
134 sn = updateSnForMAP(sn, ist, r, MAP, actualPhases);
135 end
136end
137
138end
139
140function sn = updateSnForMAP(sn, ist, r, MAP, nPhases)
141% UPDATESNFORMAP Update all network structure fields for converted MAP
142%
143% Updates proc, procid, phases, phasessz, phaseshift, mu, phi, pie, nvars, state
144
145% Save old phasessz before updating (needed for state expansion)
146oldPhases = sn.phasessz(ist, r);
147
148% Update process representation
149sn.proc{ist}{r} = MAP;
150% Use PH for Sources (they don't need MAP local variable tracking), MAP for others
151if sn.sched(ist) == SchedStrategy.EXT
152 sn.procid(ist, r) = ProcessType.PH;
153else
154 sn.procid(ist, r) = ProcessType.MAP;
155end
156sn.phases(ist, r) = nPhases;
157
158% Update phasessz and phaseshift (derived from phases)
159sn.phasessz(ist, r) = max(nPhases, 1);
160% Recompute phaseshift for this station (cumulative sum across classes)
161sn.phaseshift(ist, :) = [0, cumsum(sn.phasessz(ist, :))];
162
163% Update mu (rates from -diag(D0))
164sn.mu{ist}{r} = -diag(MAP{1});
165
166% Update phi (completion probabilities: sum(D1,2) / -diag(D0))
167D0_diag = -diag(MAP{1});
168D1_rowsum = sum(MAP{2}, 2);
169sn.phi{ist}{r} = D1_rowsum ./ D0_diag;
170
171% Update pie (initial phase distribution)
172sn.pie{ist}{r} = map_pie(MAP);
173
174% Update nvars for MAP local variable (skip for Sources - they don't use local vars)
175ind = sn.stationToNode(ist);
176if sn.sched(ist) ~= SchedStrategy.EXT
177 sn.nvars(ind, r) = sn.nvars(ind, r) + 1;
178 % Expand state vector (pass old and new phases for proper expansion)
179 sn = expandStateForMAP(sn, ind, r, oldPhases, nPhases);
180end
181
182% Add PHASE sync event if phases > 1 and not already present
183if nPhases > 1
184 sn = addPhaseSyncIfNeeded(sn, ind, r);
185end
186end
187
188function sn = expandStateForMAP(sn, ind, r, oldPhases, newPhases)
189% EXPANDSTATEFORMAP Expand state vector for MAP conversion
190%
191% When converting a non-Markovian distribution to MAP, the state vector
192% needs to be expanded in two ways:
193% 1. The server portion (space_srv) needs additional columns for extra phases
194% 2. The local variable portion (space_var) needs a column for MAP phase memory
195%
196% State format: [space_buf | space_srv | space_var]
197% - space_srv has sum(phasessz) columns total
198% - space_var has sum(nvars) columns total
199
200isf = sn.nodeToStateful(ind);
201if isf <= 0 || isempty(sn.state) || isempty(sn.state{isf})
202 return;
203end
204
205ist = sn.nodeToStation(ind);
206nRows = size(sn.state{isf}, 1);
207
208% Calculate state vector structure before conversion
209% V = number of local variables (already incremented for this class)
210V = sum(sn.nvars(ind, :));
211V_old = V - 1; % Before we added the new local var
212
213% K = phases array for this station (already updated for this class)
214K = sn.phasessz(ist, :);
215sumK_new = sum(K);
216K_old = K;
217K_old(r) = oldPhases; % What it was before
218sumK_old = sum(K_old);
219
220% Phaseshift tells us where each class's phases start
221% For class r, server phases are at positions phaseshift(r)+1 to phaseshift(r)+K(r)
222% But phaseshift has already been updated, so compute old positions
223Ks_old = [0, cumsum(K_old)];
224
225% Current state dimensions
226currentCols = size(sn.state{isf}, 2);
227
228% Calculate buffer size (state columns before space_srv)
229% Expected: currentCols = bufSize + sumK_old + V_old
230bufSize = currentCols - sumK_old - V_old;
231if bufSize < 0
232 bufSize = 0;
233end
234
235% Extract state portions
236if bufSize > 0
237 space_buf = sn.state{isf}(:, 1:bufSize);
238else
239 space_buf = zeros(nRows, 0);
240end
241
242if sumK_old > 0
243 space_srv = sn.state{isf}(:, bufSize+1:bufSize+sumK_old);
244else
245 space_srv = zeros(nRows, 0);
246end
247
248if V_old > 0
249 space_var = sn.state{isf}(:, bufSize+sumK_old+1:end);
250else
251 space_var = zeros(nRows, 0);
252end
253
254% Expand space_srv: insert (newPhases - oldPhases) zeros after class r's position
255phasesToAdd = newPhases - oldPhases;
256if phasesToAdd > 0
257 % Position where class r's phases end (in space_srv)
258 insertPos = Ks_old(r) + oldPhases;
259
260 % Insert zeros for the new phases
261 space_srv_new = [space_srv(:, 1:insertPos), ...
262 zeros(nRows, phasesToAdd), ...
263 space_srv(:, insertPos+1:end)];
264else
265 space_srv_new = space_srv;
266end
267
268% Expand space_var: add 1 column for the MAP local variable
269space_var_new = [space_var, ones(nRows, 1)];
270
271% Reconstruct state
272sn.state{isf} = [space_buf, space_srv_new, space_var_new];
273
274% Also update space if it exists
275if isfield(sn, 'space') && ~isempty(sn.space) && ~isempty(sn.space{isf})
276 nRowsSpace = size(sn.space{isf}, 1);
277 currentColsSpace = size(sn.space{isf}, 2);
278
279 % Same calculation for space
280 bufSizeSpace = currentColsSpace - sumK_old - V_old;
281 if bufSizeSpace < 0
282 bufSizeSpace = 0;
283 end
284
285 if bufSizeSpace > 0
286 space_buf_s = sn.space{isf}(:, 1:bufSizeSpace);
287 else
288 space_buf_s = zeros(nRowsSpace, 0);
289 end
290
291 if sumK_old > 0
292 space_srv_s = sn.space{isf}(:, bufSizeSpace+1:bufSizeSpace+sumK_old);
293 else
294 space_srv_s = zeros(nRowsSpace, 0);
295 end
296
297 if V_old > 0
298 space_var_s = sn.space{isf}(:, bufSizeSpace+sumK_old+1:end);
299 else
300 space_var_s = zeros(nRowsSpace, 0);
301 end
302
303 if phasesToAdd > 0
304 space_srv_s_new = [space_srv_s(:, 1:insertPos), ...
305 zeros(nRowsSpace, phasesToAdd), ...
306 space_srv_s(:, insertPos+1:end)];
307 else
308 space_srv_s_new = space_srv_s;
309 end
310
311 space_var_s_new = [space_var_s, ones(nRowsSpace, 1)];
312 sn.space{isf} = [space_buf_s, space_srv_s_new, space_var_s_new];
313end
314end
315
316function sn = addPhaseSyncIfNeeded(sn, ind, r)
317% ADDPHASESYNCIFNEEDED Add a PHASE sync event for converted MAP
318%
319% When a non-Markovian distribution is converted to a MAP with multiple phases,
320% we need to add a PHASE sync event so that phase transitions can occur
321% during simulation.
322
323% Check if PHASE sync already exists for this node/class
324phaseSyncExists = false;
325local = sn.nnodes + 1;
326
327if ~isempty(sn.sync)
328 for s = 1:length(sn.sync)
329 if ~isempty(sn.sync{s}) && ~isempty(sn.sync{s}.active) && ~isempty(sn.sync{s}.active{1})
330 activeEvent = sn.sync{s}.active{1};
331 if activeEvent.event == EventType.PHASE && activeEvent.node == ind && activeEvent.class == r
332 phaseSyncExists = true;
333 break;
334 end
335 end
336 end
337end
338
339% Add PHASE sync if not present
340if ~phaseSyncExists
341 newSync = struct('active', cell(1), 'passive', cell(1));
342 newSync.active{1} = Event(EventType.PHASE, ind, r);
343 newSync.passive{1} = Event(EventType.LOCAL, local, r, 1.0);
344 sn.sync{end+1, 1} = newSync;
345end
346end
Definition mmt.m:92