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
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.
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)
16% sn: Updated network structure with converted processes
18% Copyright (c) 2012-2026, Imperial College London
21% Get non-Markovian conversion method from options (
default 'bernstein')
22if isfield(options,
'config') && isfield(options.config,
'nonmkv')
23 nonmkvMethod = options.config.nonmkv;
25 nonmkvMethod = 'bernstein';
28% If method
is 'none', return without any conversion
29if strcmpi(nonmkvMethod, 'none')
33% Get number of phases from options (default 20)
34if isfield(options, 'config') && isfield(options.config, 'nonmkvorder')
35 nPhases = options.config.nonmkvorder;
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;
47% Markovian ProcessType IDs (no conversion needed)
48markovianTypes = [ProcessType.EXP, ProcessType.ERLANG, ProcessType.HYPEREXP, ...
49 ProcessType.PH, ProcessType.APH, ProcessType.MAP, ...
51 ProcessType.COXIAN, ProcessType.COX2, ProcessType.MMPP2, ...
52 ProcessType.IMMEDIATE, ProcessType.DISABLED];
59 procType = sn.procid(ist, r);
61 % Skip if procType
is NaN (e.g., for Transition
nodes in SPNs)
66 % Skip if already Markovian, disabled, or immediate
67 if any(procType == markovianTypes)
71 % Non-Markovian: need conversion (unless preserveDet for Det)
72 distName = ProcessType.toText(procType);
73 targetMean = 1 / sn.rates(ist, r);
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
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);
86 % Get PDF based on distribution type and stored parameters
87 origProc = sn.proc{ist}{r};
90 case ProcessType.GAMMA
93 pdf_func = @(x) gampdf(x, shape, scale);
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);
100 case ProcessType.LOGNORMAL
103 pdf_func = @(x) lognpdf(x, mu, sigma);
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);
111 case ProcessType.UNIFORM
112 minVal = origProc{1};
113 maxVal = origProc{2};
114 pdf_func = @(x) (x >= minVal & x <= maxVal) / (maxVal - minVal);
117 % Deterministic: use Erlang approximation (preserveDet
case already handled above)
118 MAP = map_erlang(targetMean, nPhases);
119 sn = updateSnForMAP(sn, ist, r, MAP, nPhases);
123 % Generic fallback: Erlang approximation
124 MAP = map_erlang(targetMean, nPhases);
125 sn = updateSnForMAP(sn, ist, r, MAP, nPhases);
129 % Apply Bernstein approximation and rescale to target mean
130 MAP = map_bernstein(pdf_func, nPhases);
131 MAP = map_scale(MAP, targetMean);
133 % Update the network structure
for the converted MAP
134 actualPhases = size(MAP{1}, 1);
135 sn = updateSnForMAP(sn, ist, r, MAP, actualPhases);
141function sn = updateSnForMAP(sn, ist, r, MAP, nPhases)
142% UPDATESNFORMAP Update all network structure fields
for converted MAP
144% Updates proc, procid, phases, phasessz, phaseshift, mu, phi, pie, nvars, state
146% Save old phasessz before updating (needed
for state expansion)
147oldPhases = sn.phasessz(ist, r);
149% Update process representation
150sn.proc{ist}{r} = MAP;
151% Use PH
for Sources (they don
't need MAP local variable tracking), MAP for others
152if sn.sched(ist) == SchedStrategy.EXT
153 sn.procid(ist, r) = ProcessType.PH;
155 sn.procid(ist, r) = ProcessType.MAP;
157sn.phases(ist, r) = nPhases;
159% Update phasessz and phaseshift (derived from phases)
160sn.phasessz(ist, r) = max(nPhases, 1);
161% Recompute phaseshift for this station (cumulative sum across classes)
162sn.phaseshift(ist, :) = [0, cumsum(sn.phasessz(ist, :))];
164% Update mu (rates from -diag(D0))
165sn.mu{ist}{r} = -diag(MAP{1});
167% Update phi (completion probabilities: sum(D1,2) / -diag(D0))
168D0_diag = -diag(MAP{1});
169D1_rowsum = sum(MAP{2}, 2);
170sn.phi{ist}{r} = D1_rowsum ./ D0_diag;
172% Update pie (initial phase distribution)
173sn.pie{ist}{r} = map_pie(MAP);
175% Update nvars for MAP local variable (skip for Sources - they don't use local vars)
176ind = sn.stationToNode(ist);
177if sn.sched(ist) ~= SchedStrategy.EXT
178 sn.nvars(ind, r) = sn.nvars(ind, r) + 1;
179 % Expand state vector (pass old and
new phases
for proper expansion)
180 sn = expandStateForMAP(sn, ind, r, oldPhases, nPhases);
183% Add PHASE sync
event if phases > 1 and not already present
185 sn = addPhaseSyncIfNeeded(sn, ind, r);
189function sn = expandStateForMAP(sn, ind, r, oldPhases, newPhases)
190% EXPANDSTATEFORMAP Expand state vector
for MAP conversion
192% When converting a non-Markovian distribution to MAP, the state vector
193% needs to be expanded in two ways:
194% 1. The server portion (space_srv) needs additional columns
for extra phases
195% 2. The local variable portion (space_var) needs a column
for MAP phase memory
197% State format: [space_buf | space_srv | space_var]
198% - space_srv has sum(phasessz) columns total
199% - space_var has sum(nvars) columns total
201isf = sn.nodeToStateful(ind);
202if isf <= 0 || isempty(sn.state) || isempty(sn.state{isf})
206ist = sn.nodeToStation(ind);
207nRows = size(sn.state{isf}, 1);
209% Calculate state vector structure before conversion
210% V = number of local variables (already incremented
for this class)
211V = sum(sn.nvars(ind, :));
212V_old = V - 1; % Before we added the
new local var
214% K = phases
array for this station (already updated
for this class)
215K = sn.phasessz(ist, :);
218K_old(r) = oldPhases; % What it was before
219sumK_old = sum(K_old);
221% Phaseshift tells us where each
class's phases start
222% For class r, server phases are at positions phaseshift(r)+1 to phaseshift(r)+K(r)
223% But phaseshift has already been updated, so compute old positions
224Ks_old = [0, cumsum(K_old)];
226% Current state dimensions
227currentCols = size(sn.state{isf}, 2);
229% Calculate buffer size (state columns before space_srv)
230% Expected: currentCols = bufSize + sumK_old + V_old
231bufSize = currentCols - sumK_old - V_old;
236% Extract state portions
238 space_buf = sn.state{isf}(:, 1:bufSize);
240 space_buf = zeros(nRows, 0);
244 space_srv = sn.state{isf}(:, bufSize+1:bufSize+sumK_old);
246 space_srv = zeros(nRows, 0);
250 space_var = sn.state{isf}(:, bufSize+sumK_old+1:end);
252 space_var = zeros(nRows, 0);
255% Expand space_srv: insert (newPhases - oldPhases) zeros after class r's position
256phasesToAdd = newPhases - oldPhases;
258 % Position where
class r's phases end (in space_srv)
259 insertPos = Ks_old(r) + oldPhases;
261 % Insert zeros
for the
new phases
262 space_srv_new = [space_srv(:, 1:insertPos), ...
263 zeros(nRows, phasesToAdd), ...
264 space_srv(:, insertPos+1:end)];
266 space_srv_new = space_srv;
269% Expand space_var: add 1 column
for the MAP local variable
270space_var_new = [space_var, ones(nRows, 1)];
273sn.state{isf} = [space_buf, space_srv_new, space_var_new];
275% Also update space
if it exists
276if isfield(sn,
'space') && ~isempty(sn.space) && ~isempty(sn.space{isf})
277 nRowsSpace = size(sn.space{isf}, 1);
278 currentColsSpace = size(sn.space{isf}, 2);
280 % Same calculation
for space
281 bufSizeSpace = currentColsSpace - sumK_old - V_old;
287 space_buf_s = sn.space{isf}(:, 1:bufSizeSpace);
289 space_buf_s = zeros(nRowsSpace, 0);
293 space_srv_s = sn.space{isf}(:, bufSizeSpace+1:bufSizeSpace+sumK_old);
295 space_srv_s = zeros(nRowsSpace, 0);
299 space_var_s = sn.space{isf}(:, bufSizeSpace+sumK_old+1:end);
301 space_var_s = zeros(nRowsSpace, 0);
305 space_srv_s_new = [space_srv_s(:, 1:insertPos), ...
306 zeros(nRowsSpace, phasesToAdd), ...
307 space_srv_s(:, insertPos+1:end)];
309 space_srv_s_new = space_srv_s;
312 space_var_s_new = [space_var_s, ones(nRowsSpace, 1)];
313 sn.space{isf} = [space_buf_s, space_srv_s_new, space_var_s_new];
317function sn = addPhaseSyncIfNeeded(sn, ind, r)
318% ADDPHASESYNCIFNEEDED Add a PHASE sync
event for converted MAP
320% When a non-Markovian distribution
is converted to a MAP with multiple phases,
321% we need to add a PHASE sync
event so that phase transitions can occur
324% Check
if PHASE sync already exists
for this node/
class
325phaseSyncExists =
false;
326local = sn.nnodes + 1;
329 for s = 1:length(sn.sync)
330 if ~isempty(sn.sync{s}) && ~isempty(sn.sync{s}.active) && ~isempty(sn.sync{s}.active{1})
331 activeEvent = sn.sync{s}.active{1};
332 if activeEvent.event == EventType.PHASE && activeEvent.node == ind && activeEvent.class == r
333 phaseSyncExists =
true;
340% Add PHASE sync
if not present
342 newSync =
struct(
'active', cell(1),
'passive', cell(1));
343 newSync.active{1} = Event(EventType.PHASE, ind, r);
344 newSync.passive{1} = Event(EventType.LOCAL, local, r, 1.0);
345 sn.sync{end+1, 1} = newSync;