1function RTret = solver_fluid_passage_time(sn, options)
2% RTRET = SOLVER_FLUID_PASSAGE_TIME(QN, OPTIONS)
4% Copyright (c) 2012-2026, Imperial College London
7iter_max = options.iter_max;
12M = sn.nstations; %number of stations
13K = sn.nclasses; %number of
classes
14N = sn.nclosedjobs; %population
22 %Set number of servers in delay station = population
34 slowrate(j,k) = min(slowrate(j,k),min(Lambda{j}{k}(:))); %service completion (exit) rates in each phase
38%% response time analysis - starting from fixed point found
41nChains = size(chains,1);
45 if sn.nodetype(sn.stationToNode(i)) ~= NodeType.Source
46 for k = 1:nChains %once
for each chain
47 idxClassesInChain = find(chains(k,:)==1);
48 for c = idxClassesInChain
50 [Kc, ode_h_c, y0_c, phases_c, fluid_c] = generate_odes_passagetime(k,c);
52 % determine max integration time
53 nonZeroRates = slowrate(:);
54 nonZeroRates = nonZeroRates( nonZeroRates > tol );
55 T = abs(100/min(nonZeroRates)); % solve ode until T = 100 events with slowest exit rate
57 % indices of
new classes at station i
59 for j = i %
this used to be at all stations
60 idxN = [idxN sum(sum(phases_c(1:j-1,: ) )) + sum(phases_c(j,1:K)) + [1:sum(phases_c(j,K+1:Kc))] ];
69 odeopt = odeset(
'AbsTol', tol,
'RelTol', tol,
'NonNegative', 1:length(y0_c));
70 while iter <= iter_max && finished == 0
72 % solve ode - ymean_t_iter
is the transient solution in stage e
75 [t_iter, ymean_t_iter] = ode_solve_stiff(ode_h_c, trange, y0_c, odeopt, options);
77 [t_iter, ymean_t_iter] = ode_solve(ode_h_c, trange, y0_c, odeopt, options);
80 line_printf(
'\nODE solver failed. Fluid solver switching to default initialization.');
81 odeopt = odeset(
'AbsTol', tol,
'RelTol', tol,
'NonNegative', 1:length(y0_c));
83 [t_iter, ymean_t_iter] = ode_solve(ode_h_c, trange, y0_c, odeopt, options);
91 fullt = [fullt; t_iter+tref];
92 fully = [fully; ymean_t_iter];
94 fullt = [fullt; t_iter(2:end)+tref];
95 fully = [fully; ymean_t_iter(2:end,:)];
97 if sum(ymean_t_iter(end,idxN )) < 10e-10
100 tref = tref + t_iter(end);
101 y0_c = ymean_t_iter(end,:);
104 % retrieve response time CDF
for class c
107 RT{i,c,2} = 1 - sum(fully(:,idxN ),2)/fluid_c;
109 RT{i,c,2} = ones(size(fullt));
112 %% Adaptive CDF Refinement - detect and refine large CDF jumps
114 maxCdfJump = 0.001; % Maximum allowed CDF jump (0.1%)
115 maxRefinementIterations = 100; % Limit to prevent infinite loops
119 while keepRefining && refinementIter < maxRefinementIterations
120 keepRefining = false;
121 cdfValues =
RT{i,c,2};
122 for row = 2:length(cdfValues)
123 cdfJump = cdfValues(row) - cdfValues(row-1);
124 if cdfJump > maxCdfJump
125 refinementIter = refinementIter + 1;
127 % Refine
this interval
130 numRefinedPoints = 20;
131 refinedT = linspace(t1, t2, numRefinedPoints)
';
132 refinedStates = zeros(numRefinedPoints, size(fully,2));
134 % Integrate to refined time points
135 startState = fully(row-1,:);
136 for rp = 1:numRefinedPoints
139 [~, tempState] = ode_solve_stiff(ode_h_c, [t1, refinedT(rp)], startState, odeopt, options);
141 [~, tempState] = ode_solve(ode_h_c, [t1, refinedT(rp)], startState, odeopt, options);
143 refinedStates(rp,:) = max(0, tempState(end,:));
145 % Linear interpolation fallback
146 alpha = (refinedT(rp) - t1) / (t2 - t1);
147 refinedStates(rp,:) = fully(row-1,:) + alpha * (fully(row,:) - fully(row-1,:));
151 % Merge refined points using ODE-computed states
152 newFullt = [fullt(1:row-1); refinedT(2:end); fullt(row+1:end)];
153 newFully = [fully(1:row-1,:); refinedStates(2:end,:); fully(row+1:end,:)];
159 RT{i,c,2} = 1 - sum(fully(:,idxN),2)/fluid_c;
161 line_printf('INFO: Added %d refined points between t=%.6f and t=%.6f\n
', numRefinedPoints, t1, t2);
162 keepRefining = true; % Continue refining
163 break; % Restart the inner loop with updated arrays
168 %% Extended Time Interval Logic - extend if first CDF > 1%
169 maxExtendIterations = 10;
171 while RT{i,c,2}(1) > 0.01 && extendIter < maxExtendIterations
172 extendIter = extendIter + 1;
173 extendedT = T * (1 + extendIter);
175 % Re-run with extended time
178 [t_ext, y_ext] = ode_solve_stiff(ode_h_c, [T0, extendedT], y0_c, odeopt, options);
180 [t_ext, y_ext] = ode_solve(ode_h_c, [T0, extendedT], y0_c, odeopt, options);
185 RT{i,c,2} = 1 - sum(fully(:,idxN),2)/fluid_c;
187 break; % Stop extending on error
194 line_warning(mfilename,'Maximum number of iterations reached when computing the response time distribution at station %d in
class %d.\n
',i,c);
195 line_warning(mfilename,'Response time distributions may be inaccurate. Try increasing option.iter_max (currently at %s).\n
',num2str(iter_max));
205 if sn.nodetype(sn.stationToNode(i)) ~= NodeType.Source
207 RTret{i,c} = [RT{i,c,2},RT{i,c,1}];
208 if ~isempty(RTret{i,c}) && RTret{i,c}(end,1) < 0.995
209 line_warning(mfilename,'CDF at station %d in
class %d computed only %.3f percent of the total mass.\n
',i,c,RTret{i,c}(end,1)*100);
216 function [Kc, ode_h_c, y0_c, phases_c, fluid_c] = generate_odes_passagetime(k,c)
217 Kc = K + 1; % add a single new class
218 numTranClasses = Kc - K;
219 idxTranCl = zeros(1,K); % indices of the transient class corresponding to each class in the original model for chain k
220 idxTranCl(chains(k,:)==1) = K+1:Kc; % this is just K+1 since we are adding a single new class, but this format may be generalizable
223 new_mu{m,1} = cell(1,Kc);
227 new_pi{m,1} = cell(1,Kc);
229 new_rt = zeros(M*Kc, M*Kc); % new routing table
235 new_mu{j}{k} = Lambda{j}{k};
237 new_mu{j}{K+1} = Lambda{j}{c};
239 % completion probabilities
241 new_pi{j}{k} = Pi{j}{k};
243 new_pi{j}{K+1} = Pi{j}{c};
247 new_proc{j}{r} = PH{j}{r};
249 new_proc{j}{K+1} = PH{j}{c};
252 % routing/switching probabilities
253 % among basic classes
256 new_rt(l:Kc:end,m:Kc:end) = rt(l:K:end,m:K:end);
260 % copy routing table from the original to the transient classes (forward)
261 for l = 1:numTranClasses
262 for m = 1:numTranClasses
263 if sum(sum(rt(c:K:end,idxClassesInChain(m):K:end))) > 0
264 new_rt(K+l:Kc:end,K+m:Kc:end) = rt(c:K:end,idxClassesInChain(m):K:end);
269 %phases of transient classes
270 phases_c = zeros(M,Kc);
271 phases_c(:,1:K) = phases;
272 phases_c(:,K+1) = phases(:,c);
274 % identify classes in chain that complete
275 completingClassesInChain = c;
277 %determine final classes (leaves in the class graph)
278 for s = completingClassesInChain' %
for each completing
class
279 %routing matrix from a transient
class that completes
is diverted back into the original
classes
280 for l = idxClassesInChain
281 for j = 1:sn.nstations
282 %
return fluid to original
class
283 new_rt((i-1)*Kc+idxTranCl(c), (j-1)*Kc+l) = rt((i-1)*K+c, (j-1)*K+l);
284 %
delete corresponding transition among transient
classes
285 new_rt((i-1)*Kc+idxTranCl(c), (j-1)*Kc+idxTranCl(l)) = 0;
290 % setup the ODEs
for the
new QN
291 % options.method =
'statedep'; %
default doesn
't seem to work in some models
292 [ode_h_c, ~] = solver_fluid_odes(sn, N, new_mu', new_pi
', new_proc, new_rt, S, sn.sched, sn.schedparam, options);
294 % setup initial point
295 y0_c = zeros(1, sum(sum(phases_c(:,:))));
297 for j = 1:sn.nstations
298 for l = 1:sn.nclasses
299 idxNew_jl = sum(sum(phases_c(1:j-1,:))) + sum(phases_c(j,1:l-1));
300 idxNew_jt = sum(sum(phases_c(1:j-1,:))) + sum(phases_c(j,1:idxTranCl(l)-1));
301 idx_jl = sum(sum(phases(1:j-1,:))) + sum(phases(j,1:l-1));
303 y0_c( idxNew_jt + 1 ) = sum(y0(idx_jl+1:idx_jl + phases(j,l))); % mass in phases all moved back into phase 1
304 fluid_c = fluid_c + sum(y0(idx_jl+1:idx_jl + phases(j,l)));
305 else % leave mass as it is
306 y0_c( idxNew_jl + 1: idxNew_jl + phases_c(j,l) ) = y0(idx_jl+1:idx_jl + phases(j,l));