LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_fluid_passage_time.m
1function RTret = solver_fluid_passage_time(sn, options)
2% RTRET = SOLVER_FLUID_PASSAGE_TIME(QN, OPTIONS)
3
4% Copyright (c) 2012-2026, Imperial College London
5% All rights reserved.
6
7iter_max = options.iter_max;
8tol = options.tol;
9y0 = options.init_sol;
10stiff = options.stiff;
11T0 = 0;
12M = sn.nstations; %number of stations
13K = sn.nclasses; %number of classes
14N = sn.nclosedjobs; %population
15Lambda = sn.mu;
16Pi = sn.phi;
17PH = sn.proc;
18rt = sn.rt;
19S = sn.nservers;
20
21for j = 1:M
22 %Set number of servers in delay station = population
23 if isinf(S(j))
24 S(j) = N;
25 end
26end
27
28%% initialization
29phases = sn.phases;
30slowrate = zeros(M,K);
31for j = 1:M
32 for k = 1:K
33 slowrate(j,k) = Inf;
34 slowrate(j,k) = min(slowrate(j,k),min(Lambda{j}{k}(:))); %service completion (exit) rates in each phase
35 end
36end
37
38%% response time analysis - starting from fixed point found
39%stiff = 1;
40chains = sn.chains;
41nChains = size(chains,1);
42
43RT = [];
44for i = 1:sn.nstations
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
49 if phases(i,c) > 0
50 [Kc, ode_h_c, y0_c, phases_c, fluid_c] = generate_odes_passagetime(k,c);
51
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
56
57 % indices of new classes at station i
58 idxN = [];
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))] ];
61 end
62
63 %% ODE analysis
64 fullt = [];
65 fully = [];
66 iter = 1;
67 finished = 0;
68 tref = 0;
69 odeopt = odeset('AbsTol', tol, 'RelTol', tol, 'NonNegative', 1:length(y0_c));
70 while iter <= iter_max && finished == 0
71 trange = [T0, T];
72 % solve ode - ymean_t_iter is the transient solution in stage e
73 try
74 if stiff
75 [t_iter, ymean_t_iter] = ode_solve_stiff(ode_h_c, trange, y0_c, odeopt, options);
76 else
77 [t_iter, ymean_t_iter] = ode_solve(ode_h_c, trange, y0_c, odeopt, options);
78 end
79 catch ME
80 line_printf('\nODE solver failed. Fluid solver switching to default initialization.');
81 odeopt = odeset('AbsTol', tol, 'RelTol', tol, 'NonNegative', 1:length(y0_c));
82 %try
83 [t_iter, ymean_t_iter] = ode_solve(ode_h_c, trange, y0_c, odeopt, options);
84 %catch
85 % %keyboard
86 %end
87 end
88 %%%
89 iter = iter + 1;
90 if isempty(fullt)
91 fullt = [fullt; t_iter+tref];
92 fully = [fully; ymean_t_iter];
93 else
94 fullt = [fullt; t_iter(2:end)+tref];
95 fully = [fully; ymean_t_iter(2:end,:)];
96 end
97 if sum(ymean_t_iter(end,idxN )) < 10e-10
98 finished = 1;
99 end
100 tref = tref + t_iter(end);
101 y0_c = ymean_t_iter(end,:);
102 end
103
104 % retrieve response time CDF for class c
105 RT{i,c,1} = fullt;
106 if fluid_c > 0
107 RT{i,c,2} = 1 - sum(fully(:,idxN ),2)/fluid_c;
108 else
109 RT{i,c,2} = ones(size(fullt));
110 end
111
112 %% Adaptive CDF Refinement - detect and refine large CDF jumps
113 if fluid_c > 0
114 maxCdfJump = 0.0005; % Maximum allowed CDF jump (0.05%)
115 maxRefinementIterations = 5; % Limit to prevent infinite loops
116 refinementIter = 0;
117
118 keepRefining = true;
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;
126
127 % Refine this interval
128 t1 = fullt(row-1);
129 t2 = fullt(row);
130 numRefinedPoints = 20;
131 refinedT = linspace(t1, t2, numRefinedPoints)';
132 refinedStates = zeros(numRefinedPoints, size(fully,2));
133
134 % Integrate to refined time points
135 startState = fully(row-1,:);
136 for rp = 1:numRefinedPoints
137 try
138 if stiff
139 [~, tempState] = ode_solve_stiff(ode_h_c, [t1, refinedT(rp)], startState, odeopt, options);
140 else
141 [~, tempState] = ode_solve(ode_h_c, [t1, refinedT(rp)], startState, odeopt, options);
142 end
143 refinedStates(rp,:) = max(0, tempState(end,:));
144 catch
145 % Linear interpolation fallback
146 alpha = (refinedT(rp) - t1) / (t2 - t1);
147 refinedStates(rp,:) = fully(row-1,:) + alpha * (fully(row,:) - fully(row-1,:));
148 end
149 end
150
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,:)];
154 fullt = newFullt;
155 fully = newFully;
156
157 % Recompute CDF
158 RT{i,c,1} = fullt;
159 RT{i,c,2} = 1 - sum(fully(:,idxN),2)/fluid_c;
160
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
164 end
165 end
166 end
167
168 %% Extended Time Interval Logic - extend if first CDF > 1%
169 maxExtendIterations = 10;
170 extendIter = 0;
171 while RT{i,c,2}(1) > 0.01 && extendIter < maxExtendIterations
172 extendIter = extendIter + 1;
173 extendedT = T * (1 + extendIter);
174
175 % Re-run with extended time
176 try
177 if stiff
178 [t_ext, y_ext] = ode_solve_stiff(ode_h_c, [T0, extendedT], y0_c, odeopt, options);
179 else
180 [t_ext, y_ext] = ode_solve(ode_h_c, [T0, extendedT], y0_c, odeopt, options);
181 end
182 fullt = t_ext;
183 fully = y_ext;
184 RT{i,c,1} = fullt;
185 RT{i,c,2} = 1 - sum(fully(:,idxN),2)/fluid_c;
186 catch
187 break; % Stop extending on error
188 end
189 end
190 end
191
192 if iter > iter_max
193 line_printf('\n');
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));
196 end
197 end
198 end
199 end
200 end
201end
202
203RTret = {};
204for i=1:sn.nstations
205 if sn.nodetype(sn.stationToNode(i)) ~= NodeType.Source
206 for c=1:sn.nclasses
207 if size(RT,1) >= i && size(RT,2) >= c && size(RT,3) >= 2 && ~isempty(RT{i,c,1})
208 RTret{i,c} = [RT{i,c,2},RT{i,c,1}];
209 if ~isempty(RTret{i,c}) && RTret{i,c}(end,1) < 0.995
210 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);
211 end
212 end
213 end
214 end
215end
216return
217
218 function [Kc, ode_h_c, y0_c, phases_c, fluid_c] = generate_odes_passagetime(k,c)
219 Kc = K + 1; % add a single new class
220 numTranClasses = Kc - K;
221 idxTranCl = zeros(1,K); % indices of the transient class corresponding to each class in the original model for chain k
222 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 = cell(M,1);
224 for m=1:M
225 new_mu{m,1} = cell(1,Kc);
226 end
227 new_pi = cell(M,1);
228 for m=1:M
229 new_pi{m,1} = cell(1,Kc);
230 end
231 new_rt = zeros(M*Kc, M*Kc); % new routing table
232 new_proc = PH;
233
234 for j=1:sn.nstations
235 % service rates
236 for k=1:K
237 new_mu{j}{k} = Lambda{j}{k};
238 end
239 new_mu{j}{K+1} = Lambda{j}{c};
240
241 % completion probabilities
242 for k=1:K
243 new_pi{j}{k} = Pi{j}{k};
244 end
245 new_pi{j}{K+1} = Pi{j}{c};
246
247 % phd distribution
248 for r=1:nChains
249 new_proc{j}{r} = PH{j}{r};
250 end
251 new_proc{j}{K+1} = PH{j}{c};
252 end
253
254 % routing/switching probabilities
255 % among basic classes
256 for l = 1:K
257 for m = 1:K
258 new_rt(l:Kc:end,m:Kc:end) = rt(l:K:end,m:K:end);
259 end
260 end
261
262 % copy routing table from the original to the transient classes (forward)
263 for l = 1:numTranClasses
264 for m = 1:numTranClasses
265 if sum(sum(rt(c:K:end,idxClassesInChain(m):K:end))) > 0
266 new_rt(K+l:Kc:end,K+m:Kc:end) = rt(c:K:end,idxClassesInChain(m):K:end);
267 end
268 end
269 end
270
271 %phases of transient classes
272 phases_c = zeros(M,Kc);
273 phases_c(:,1:K) = phases;
274 phases_c(:,K+1) = phases(:,c);
275
276 % identify classes in chain that complete
277 completingClassesInChain = c;
278
279 %determine final classes (leaves in the class graph)
280 for s = completingClassesInChain' % for each completing class
281 %routing matrix from a transient class that completes is diverted back into the original classes
282 for l = idxClassesInChain
283 for j = 1:sn.nstations
284 % return fluid to original class
285 new_rt((i-1)*Kc+idxTranCl(c), (j-1)*Kc+l) = rt((i-1)*K+c, (j-1)*K+l);
286 % delete corresponding transition among transient classes
287 new_rt((i-1)*Kc+idxTranCl(c), (j-1)*Kc+idxTranCl(l)) = 0;
288 end
289 end
290 end
291
292 % setup the ODEs for the new QN
293 % options.method = 'statedep'; % default doesn't seem to work in some models
294 [ode_h_c, ~] = solver_fluid_odes(sn, N, new_mu', new_pi', new_proc, new_rt, S, sn.sched, sn.schedparam, options);
295
296 % setup initial point
297 y0_c = zeros(1, sum(sum(phases_c(:,:))));
298 fluid_c = 0;
299 for j = 1:sn.nstations
300 for l = 1:sn.nclasses
301 idxNew_jl = sum(sum(phases_c(1:j-1,:))) + sum(phases_c(j,1:l-1));
302 idxNew_jt = sum(sum(phases_c(1:j-1,:))) + sum(phases_c(j,1:idxTranCl(l)-1));
303 idx_jl = sum(sum(phases(1:j-1,:))) + sum(phases(j,1:l-1));
304 if i == j && l==c
305 y0_c( idxNew_jt + 1 ) = sum(y0(idx_jl+1:idx_jl + phases(j,l))); % mass in phases all moved back into phase 1
306 fluid_c = fluid_c + sum(y0(idx_jl+1:idx_jl + phases(j,l)));
307 else % leave mass as it is
308 y0_c( idxNew_jl + 1: idxNew_jl + phases_c(j,l) ) = y0(idx_jl+1:idx_jl + phases(j,l));
309 end
310 end
311 end
312 end
313
314end