LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_fluid_matrix.m
1function [QN,UN,RN,TN,xvec_it,QNt,UNt,TNt,xvec_t,t,iters,runtime] = solver_fluid_matrix(sn, options)
2
3% [QN,UN,RN,TN,CN,RUNTIME] = SOLVER_FLUID_MATRIX(QN, OPTIONS)
4
5% Copyright (c) 2012-2026, Imperial College London
6% All rights reserved.
7
8M = sn.nstations; %number of stations
9K = sn.nclasses; %number of classes
10pie = sn.pie;
11PH = sn.proc;
12P_full = sn.rt; % Full routing matrix (stateful nodes)
13NK = sn.njobs'; %initial population
14S = sn.nservers;
15infServers = isinf(S);
16S(infServers) = sum(NK);
17nphases = sn.phases;
18%refstat = sn.refstat; % reference station
19weights = ones(M,K);
20
21% Extract station-to-station routing matrix from stateful-to-stateful matrix
22% using stochastic complementation to resolve routing through non-station
23% stateful nodes (e.g., Router nodes)
24station_indices = [];
25for ist = 1:M
26 isf = sn.stationToStateful(ist);
27 for r = 1:K
28 station_indices = [station_indices, (isf-1)*K + r];
29 end
30end
31P = dtmc_stochcomp(P_full, station_indices);
32
33% Remove Sink->Source feedback routing for open classes
34% In open networks, jobs exit at Sink and should not recirculate back to Source.
35% The routing matrix includes this feedback (added by getRoutingMatrix.m for
36% pseudo-closed network analysis), but it causes incorrect flow balance in the
37% fluid ODE formulation because arrivals are already accounted for via Alambda.
38for src_ist = 1:M
39 if sn.sched(src_ist) == SchedStrategy.EXT
40 % This is a Source station - remove feedback routing TO it for open classes
41 for r = 1:K
42 % Check if this is an open class with external arrivals
43 if ~isnan(sn.rates(src_ist, r)) && sn.rates(src_ist, r) > 0
44 % Zero out routing TO this Source for this class from all other stations
45 src_col = (src_ist - 1) * K + r; % Column index in P for (Source, class r)
46 for from_ist = 1:M
47 if from_ist ~= src_ist % Don't modify Source's own outgoing routing
48 for from_r = 1:K
49 from_row = (from_ist - 1) * K + from_r;
50 P(from_row, src_col) = 0; % Remove feedback to Source
51 end
52 end
53 end
54 end
55 end
56 end
57end
58
59% ODE building as per Ruuskanen et al., PEVA 151 (2021).
60Psi = [];
61A = [];
62B = [];
63for ist=1:M
64 for r=1:K
65 if nphases(ist,r)==0
66 Psi = blkdiag(Psi,0);
67 B = blkdiag(B,0);
68 A = blkdiag(A,NaN);
69 else
70 Psi = blkdiag(Psi,PH{ist}{r}{1});
71 B = blkdiag(B,sum(PH{ist}{r}{2},2));
72 A = blkdiag(A,pie{ist}{r}');
73 end
74 end
75end
76W = Psi + B*P*A';
77
78% Build arrival rate vector Aλ for mixed/open networks (Ruuskanen et al., PEVA 151 (2021), Eq. 7)
79% Following the ground truth implementation: Source is conceptually excluded from state space.
80% Arrivals go directly to queue phases (not Source phases) weighted by routing probabilities.
81% This matches dx = W' * theta + A * lambda where lambda represents arrivals INTO queues.
82
83% First, identify Source stations and their arrival rates per class
84source_arrivals = zeros(M, K); % source_arrivals(src, r) = arrival rate at source src for class r
85for src_ist = 1:M
86 if sn.sched(src_ist) == SchedStrategy.EXT
87 for r = 1:K
88 if ~isnan(sn.rates(src_ist, r)) && sn.rates(src_ist, r) > 0
89 source_arrivals(src_ist, r) = sn.rates(src_ist, r);
90 end
91 end
92 end
93end
94
95% Build Alambda: arrivals go to QUEUE phases (where jobs route from Source), not Source phases
96Alambda_full = zeros(size(A,1), 1);
97state = 0;
98for ist=1:M
99 for r=1:K
100 if nphases(ist,r) > 0
101 if sn.sched(ist) == SchedStrategy.EXT
102 % Source station: do NOT add arrivals here (arrivals go to downstream queues)
103 state = state + nphases(ist,r);
104 else
105 % Queue station: check if it receives arrivals from any Source
106 arrival_rate_to_queue = 0;
107 for src_ist = 1:M
108 if source_arrivals(src_ist, r) > 0
109 % Get routing probability from Source to this queue for this class
110 src_row = (src_ist - 1) * K + r;
111 queue_col = (ist - 1) * K + r;
112 routing_prob = P(src_row, queue_col);
113 arrival_rate_to_queue = arrival_rate_to_queue + source_arrivals(src_ist, r) * routing_prob;
114 end
115 end
116
117 if arrival_rate_to_queue > 0
118 % Apply arrivals according to entrance probability ζ^{i,r}
119 for k=1:nphases(ist,r)
120 state = state + 1;
121 Alambda_full(state) = pie{ist}{r}(k) * arrival_rate_to_queue;
122 end
123 else
124 state = state + nphases(ist,r);
125 end
126 end
127 else
128 % Add placeholder for disabled class to match W matrix structure
129 state = state + 1;
130 % Alambda_full(state) stays 0 since there are no arrivals
131 end
132 end
133end
134
135% remove disabled transitions
136keep = find(~isnan(sum(W,1)));
137W = W(keep,:);
138W = W(:,keep);
139Alambda = Alambda_full(keep); % Also filter arrival vector
140
141% Auto-detect extreme stiffness and enable hide_immediate proactively.
142% When W has rates spanning many orders of magnitude (e.g., 1e8 from immediate
143% class-switching in LN layers), LSODA's finite-difference Jacobian fails.
144% Stochastic complementation removes immediate states, reducing stiffness.
145hide_imm_explicitly_set = isfield(options.config, 'hide_immediate');
146if hide_imm_explicitly_set
147 hide_imm_requested = options.config.hide_immediate;
148else
149 % Auto-detect stiffness only when hide_immediate is not explicitly configured
150 hide_imm_requested = false;
151 nonZeroDiag = abs(diag(W));
152 nonZeroDiag = nonZeroDiag(nonZeroDiag > 0);
153 if ~isempty(nonZeroDiag)
154 stiffness_ratio = max(nonZeroDiag) / min(nonZeroDiag);
155 if stiffness_ratio > 1e4
156 hide_imm_requested = true;
157 end
158 end
159end
160
161% Eliminate immediate transitions if requested or auto-detected
162state_map_imm = [];
163W_pre_elim = [];
164Alambda_pre_elim = [];
165if hide_imm_requested
166 W_pre_elim = W; % Save for Alambda correction
167 Alambda_pre_elim = Alambda;
168 [W, state_map_imm] = eliminate_immediate_matrix(W, sn, options);
169end
170
171Qa = []; % state mapping to queues (called Q(a) in Ruuskanen et al.)
172SQC = zeros(M*K,0); % to compute per-class queue length at the end
173SUC = zeros(M*K,0); % to compute per-class utilizations at the end
174STC = zeros(M*K,0); % to compute per-class throughput at the end
175x0_build = []; % Build x0 with same structure as W matrix
176%x0 = []; % initial state
177state = 0;
178init_sol_idx = 0; % Index into options.init_sol for enabled classes
179for ist=1:M
180 for r=1:K
181 if nphases(ist,r)==0
182 % Add placeholder for disabled transition (matching W matrix structure)
183 state = state + 1;
184 Qa(1,state) = ist; %#ok<*AGROW>
185 SQC((ist-1)*K+r,state) = 0; % No queue contribution
186 SUC((ist-1)*K+r,state) = 0; % No utilization contribution
187 STC((ist-1)*K+r,state) = 0; % No throughput contribution
188 x0_build(state,1) = 0; % No initial population
189 else
190 for k=1:nphases(ist,r)
191 state = state + 1;
192 Qa(1,state) = ist;
193 if isnan(sn.rates(ist,r))
194 % Class has phases but is disabled (NaN rate) -
195 % solver_fluid_initsol skips these, so do not
196 % increment init_sol_idx
197 SQC((ist-1)*K+r,state) = 0;
198 SUC((ist-1)*K+r,state) = 0;
199 STC((ist-1)*K+r,state) = 0;
200 x0_build(state,1) = 0;
201 else
202 init_sol_idx = init_sol_idx + 1;
203 SQC((ist-1)*K+r,state) = 1;
204 SUC((ist-1)*K+r,state) = 1/S(ist);
205 STC((ist-1)*K+r,state) = sum(sn.proc{ist}{r}{2}(k,:));
206 x0_build(state,1) = options.init_sol(init_sol_idx);
207 end
208 end
209 end
210 % code to initialize all jobs at ref station
211 %if i == refstat(r)
212 % x0 = [x0; NK(r)*pie{i}{r}']; % initial probability of PH
213 %else
214 % x0 = [x0; zeros(nphases(i,r),1)];
215 %end
216 end
217end
218x0 = x0_build;
219
220% Apply keep filtering for disabled transitions (NaN in W matrix)
221Qa = Qa(keep);
222SQC = SQC(:, keep);
223SUC = SUC(:, keep);
224STC = STC(:, keep);
225x0 = x0(keep);
226
227% Save full matrices before immediate elimination for metric reconstruction
228Qa_full = Qa;
229STC_full = STC;
230imm_states_in_keep = []; % Indices of immediate states within keep-filtered space
231
232% Apply state mapping if immediate transitions were eliminated
233if ~isempty(state_map_imm)
234 % Identify eliminated (immediate) states
235 imm_states_in_keep = setdiff(1:length(Qa), state_map_imm);
236
237 Qa = Qa(state_map_imm);
238 SQC = SQC(:, state_map_imm);
239 SUC = SUC(:, state_map_imm);
240 STC = STC(:, state_map_imm);
241 x0 = x0(state_map_imm);
242
243 % Correct Alambda for arrivals directed at eliminated immediate states.
244 % From quasi-steady-state assumption (dx_I/dt = 0):
245 % lambda_red = lambda_T - Q_IT' * (Q_II')^{-1} * lambda_I
246 % where T = timed states, I = immediate states.
247 lambda_T = Alambda_pre_elim(state_map_imm);
248 lambda_I = Alambda_pre_elim(imm_states_in_keep);
249 if any(lambda_I ~= 0)
250 Q_IT = W_pre_elim(imm_states_in_keep, state_map_imm);
251 Q_II = W_pre_elim(imm_states_in_keep, imm_states_in_keep);
252 Alambda = lambda_T - Q_IT' * (Q_II' \ lambda_I);
253 else
254 Alambda = lambda_T;
255 end
256end
257
258% Build SQ matrix to compute total queue length per station in ODEs
259% SQ(s,:) sums all states at the same station as state s
260nstates = length(x0);
261SQ = zeros(nstates, nstates);
262for s = 1:nstates
263 ist = Qa(s);
264 SQ(s, Qa == ist) = 1; % Sum all states at the same station
265end
266
267% Identify Source station states (EXT scheduler)
268% For Source stations, theta should be 0.0 to effectively bypass Source in dynamics.
269% This matches the ground truth implementation where Source is excluded from state space.
270% Arrivals are injected directly into queue phases via Alambda.
271isSourceState = false(nstates, 1);
272for s = 1:nstates
273 ist = Qa(s);
274 if sn.sched(ist) == SchedStrategy.EXT
275 isSourceState(s) = true;
276 x0(s) = 0; % Initialize Source phases to 0 (no mass at Source)
277 end
278end
279
280%x0
281
282tol = options.tol;
283timespan = options.timespan;
284itermax = options.iter_max;
285odeopt = odeset('AbsTol', tol, 'RelTol', tol, 'NonNegative', 1:length(x0));
286nonZeroRates = abs(W(abs(W)>0)); nonZeroRates=nonZeroRates(:);
287if isempty(nonZeroRates)
288 trange = [timespan(1), timespan(2)];
289 if ~isfinite(trange(2))
290 trange(2) = 1;
291 end
292else
293 trange = [timespan(1),min(timespan(2),abs(10*itermax/min(nonZeroRates)))];
294end
295
296% Check if p-norm smoothing should be used (pstar parameter)
297use_pnorm = isfield(options, 'pstar') && ~isempty(options.pstar) || ...
298 (isfield(options.config, 'pstar') && ~isempty(options.config.pstar));
299
300if use_pnorm
301 % Get pstar values - expand scalar to per-station array
302 if isfield(options, 'pstar') && ~isempty(options.pstar)
303 pstar_val = options.pstar;
304 else
305 pstar_val = options.config.pstar;
306 end
307 if isscalar(pstar_val)
308 pstar_val = pstar_val * ones(M, 1);
309 end
310 % Create per-phase pstar array (pQa) using the filtered Qa mapping
311 pQa = pstar_val(Qa(:)); % Use Qa which is already filtered by keep and state_map_imm
312 Sa_pnorm = S(Qa(:)); % Column vector for pnorm_ode
313end
314
315T0 = tic;
316iters = 1;
317ode_failed = false;
318try
319 if use_pnorm
320 % p-norm smoothing ODE as per Ruuskanen et al., PEVA 151 (2021)
321 % ghat = 1 / (1 + (x/c)^p)^(1/p) where x is queue length, c is servers, p is pstar
322 % dx/dt = W^T * θ̂(x,p) + Aλ (Eq. 27 for mixed networks)
323 ode_pnorm_func = @(t,x) pnorm_ode(x, W, SQ, Sa_pnorm, pQa, Alambda, isSourceState);
324 if options.stiff
325 [t, xvec_t] = ode_solve_stiff(ode_pnorm_func, trange, x0, odeopt, options);
326 else
327 [t, xvec_t] = ode_solve(ode_pnorm_func, trange, x0, odeopt, options);
328 end
329 else
330 % Standard matrix method without smoothing
331 % dx/dt = W^T * θ(x) + Aλ (Eq. 12 for mixed networks)
332 Sa_ode = S(Qa(:)); % Column vector for element-wise operations
333 % Define theta function with special handling for Source stations
334 theta_func = @(x) compute_theta(x, SQ, Sa_ode, isSourceState);
335 if options.stiff
336 [t, xvec_t] = ode_solve_stiff(@(t,x) W'*theta_func(x) + Alambda, trange, x0, odeopt, options);
337 else
338 [t, xvec_t] = ode_solve(@(t,x) W'*theta_func(x) + Alambda, trange, x0, odeopt, options);
339 end
340 end
341catch me
342 if contains(me.identifier, 'lsoda')
343 ode_failed = true;
344 else
345 rethrow(me);
346 end
347end
348
349% On LSODA failure, retry with hide_immediate toggled (only once)
350if ode_failed
351 is_retry = isfield(options.config, 'lsoda_retry') && options.config.lsoda_retry;
352 if ~is_retry
353 options_retry = options;
354 options_retry.config.lsoda_retry = true;
355 if hide_imm_requested
356 % hide_immediate was active (auto-detected or explicit) and failed — retry without it
357 options_retry.config.hide_immediate = false;
358 else
359 % Normal solve failed — retry with hide_immediate
360 options_retry.config.hide_immediate = true;
361 end
362 [QN,UN,RN,TN,xvec_it,QNt,UNt,TNt,xvec_t,t,iters,runtime] = solver_fluid_matrix(sn, options_retry);
363 return;
364 else
365 % Both attempts failed — return empty lastSol to signal failure
366 warning('lsoda:failed', 'LSODA failed on both normal and hide_immediate attempts');
367 QN = NaN(M, K);
368 UN = NaN(M, K);
369 RN = NaN(M, K);
370 TN = NaN(M, K);
371 xvec_it = {}; % empty signals failure to caller (runAnalyzer checks isempty)
372 QNt = cell(M, K);
373 UNt = cell(M, K);
374 TNt = cell(M, K);
375 xvec_t = zeros(1, sum(nphases(:)));
376 t = 0;
377 iters = 0;
378 runtime = toc(T0);
379 return;
380 end
381end
382runtime = toc(T0);
383
384Tmax = size(xvec_t,1);
385QNtmp = cell(1,Tmax);
386UNtmp = cell(1,Tmax);
387RNtmp = cell(1,Tmax);
388TNtmp = cell(1,Tmax);
389Sa = S(Qa(:)); % Column vector for element-wise operations
390S = repmat(S,1,K)'; S=S(:);
391for j=1:Tmax
392 x = xvec_t(j,:)';
393 QNtmp{j} = zeros(K,M);
394 TNtmp{j} = zeros(K,M);
395 UNtmp{j} = zeros(K,M);
396 RNtmp{j} = zeros(K,M);
397
398 QNtmp{j}(:) = SQC*x;
399 % Use compute_theta for consistent handling of Source stations
400 theta_j = compute_theta(x, SQ, Sa, isSourceState);
401 TNtmp{j}(:) = STC*theta_j;
402 UNtmp{j}(:) = SUC*theta_j;
403 % Little's law is invalid in transient so this vector is not returned
404 % except the last element as an approximation of the actual RN
405 RNtmp{j}(:) = QNtmp{j}(:)./TNtmp{j}(:);
406
407 QNtmp{j} = QNtmp{j}';
408 UNtmp{j} = UNtmp{j}';
409 RNtmp{j} = RNtmp{j}';
410 TNtmp{j} = TNtmp{j}';
411end
412% steady state metrics
413for j=1:Tmax
414 QNtmp{j} = QNtmp{j}(:);
415 UNtmp{j} = UNtmp{j}(:);
416 RNtmp{j} = RNtmp{j}(:);
417 TNtmp{j} = TNtmp{j}(:);
418end
419
420% compute cell array with time-varying metrics for stations and classes
421QNtmp = cell2mat(QNtmp)';
422UNtmp = cell2mat(UNtmp)';
423RNtmp = cell2mat(RNtmp)';
424TNtmp = cell2mat(TNtmp)';
425QNt = cell(M,K);
426UNt = cell(M,K);
427RNt = cell(M,K);
428TNt = cell(M,K);
429for ist=1:M
430 for r=1:K
431 QNt{ist,r} = QNtmp(:,(r-1)*M+ist);
432 UNt{ist,r} = UNtmp(:,(r-1)*M+ist);
433 RNt{ist,r} = RNtmp(:,(r-1)*M+ist);
434 TNt{ist,r} = TNtmp(:,(r-1)*M+ist);
435 end
436end
437QN = reshape(QNtmp(end,:),M,K);
438UN = reshape(UNtmp(end,:),M,K);
439RN = reshape(RNtmp(end,:),M,K);
440TN = reshape(TNtmp(end,:),M,K);
441
442% Set throughput at Source stations to arrival rates for open classes
443% Source stations have theta = 0 in the ODE (to bypass Source in dynamics),
444% but their throughput should equal the external arrival rate.
445for ist=1:M
446 if sn.sched(ist) == SchedStrategy.EXT
447 for r=1:K
448 if ~isnan(sn.rates(ist, r)) && sn.rates(ist, r) > 0
449 TN(ist, r) = sn.rates(ist, r);
450 end
451 end
452 end
453end
454
455% Reconstruct throughput for eliminated immediate states using flow conservation
456if ~isempty(imm_states_in_keep)
457 % For each eliminated state, throughput = sum of incoming throughputs via routing
458 for idx = 1:length(imm_states_in_keep)
459 s = imm_states_in_keep(idx);
460 ist = Qa_full(s); % Station of this eliminated state
461 % Find which class this state belongs to
462 r = 0;
463 state_count = 0;
464 for rr = 1:K
465 state_count = state_count + nphases(ist, rr);
466 if s <= sum(Qa_full == ist & (1:length(Qa_full)) <= state_count)
467 r = rr;
468 break;
469 end
470 end
471 if r == 0
472 % Fallback: find class by examining STC_full
473 for rr = 1:K
474 if STC_full((ist-1)*K+rr, s) > 0
475 r = rr;
476 break;
477 end
478 end
479 end
480 if r > 0
481 % Compute incoming throughput using routing matrix P
482 % T(ist, r) = sum over all (j, l) of T(j, l) * P((j,l) -> (ist,r))
483 incoming_tput = 0;
484 for j = 1:M
485 for l = 1:K
486 p_jl_ir = P((j-1)*K+l, (ist-1)*K+r);
487 if p_jl_ir > 0
488 incoming_tput = incoming_tput + TN(j, l) * p_jl_ir;
489 end
490 end
491 end
492 % Also add external arrivals if this is a source station
493 if sn.sched(ist) == SchedStrategy.EXT && ~isnan(sn.rates(ist, r))
494 incoming_tput = incoming_tput + sn.rates(ist, r);
495 end
496 TN(ist, r) = incoming_tput;
497 end
498 end
499end
500
501xvec_it = {xvec_t(end,:)};
502end
503
504function dxdt = pnorm_ode(x, W, SQ, Sa, pQa, Alambda, isSourceState)
505% PNORM_ODE - ODE derivative using p-norm smoothing
506% As per Ruuskanen et al., PEVA 151 (2021)
507% dxdt = W' * (x .* ghat) + Aλ (Eq. 27)
508% where ghat = 1 / (1 + (sumXQa/Sa)^pQa)^(1/pQa)
509
510sumXQa = GlobalConstants.FineTol + SQ * x;
511ghat = zeros(size(x));
512for i = 1:length(x)
513 xVal = sumXQa(i);
514 cVal = Sa(i);
515 pVal = pQa(i);
516 if cVal > 0 && pVal > 0
517 ghatVal = 1.0 / (1 + (xVal / cVal)^pVal)^(1/pVal);
518 if isnan(ghatVal)
519 ghat(i) = 0;
520 else
521 ghat(i) = ghatVal;
522 end
523 else
524 ghat(i) = 1;
525 end
526end
527
528% Compute effective rate (x .* ghat), with special handling for Source stations
529theta_eff = x .* ghat;
530% For Source stations, override to 0.0 to bypass Source in dynamics
531% (matching ground truth where Source is excluded from state space)
532theta_eff(isSourceState) = 0.0;
533
534dxdt = W' * theta_eff + Alambda;
535end
536
537function theta = compute_theta(x, SQ, Sa, isSourceState)
538% COMPUTE_THETA - Compute theta vector for fluid ODE
539% For regular stations: theta = x./(SQ*x) .* min(Sa, SQ*x)
540% For Source stations (EXT scheduler): theta = 0.0
541% - Source is conceptually excluded from state space (matching ground truth)
542% - Arrivals are injected directly into queues via Alambda
543% - Setting theta = 0 prevents Source from contributing to W' * theta
544
545sumXQa = GlobalConstants.FineTol + SQ * x;
546theta = x ./ sumXQa .* min(Sa, sumXQa);
547
548% Override theta for Source stations
549% Source stations have theta = 0.0 to bypass Source in dynamics
550theta(isSourceState) = 0.0;
551end
Definition mmt.m:124