LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_fluid_mfq.m
1function [QN,UN,RN,TN,xvec_it,QNt,UNt,TNt,xvec_t,t,iters,runtime] = solver_fluid_mfq(sn, options)
2%SOLVER_FLUID_MFQ Solves single-queue open systems using BUTools MAPMAP1
3%
4% [QN,UN,RN,TN,xvec_it,QNt,UNt,TNt,xvec_t,t,iters,runtime] = SOLVER_FLUID_MFQ(sn, options)
5%
6% Uses the BUTools library's FluFluQueue function to analyze MAP/MAP/1 queues.
7% Provides exact steady-state queue length and sojourn time moments for
8% single-queue open systems with phase-type arrivals and service.
9%
10% This solver applies only to single-queue topologies:
11% Source (external arrivals) -> Queue (finite server) -> Sink
12%
13% Applicability:
14% - Single-queue open system
15% - Single-server (c=1) or infinite-server (c=Inf)
16% - No feedback loops
17% - Supported distributions: Exp, Erlang, HyperExp, Cox, APH, MAP, PH, MMDP
18%
19% Parameters:
20% sn (struct): Network structure from Network.getStruct()
21% options (struct): Solver options including:
22% - method: 'mfq' (should be set by dispatcher)
23% - tol: Numerical tolerance (default: 1e-14)
24%
25% Returns:
26% QN (MxK): Mean queue lengths at each station
27% UN (MxK): Utilizations at each station
28% RN (MxK): Mean response times at each station
29% TN (MxK): Throughputs at each station
30% xvec_it (cell): Final state vector for compatibility
31% QNt (MxK cell): Transient queue lengths (empty for MAPMAP1)
32% UNt (MxK cell): Transient utilizations (empty for MAPMAP1)
33% TNt (MxK cell): Transient throughputs (empty for MAPMAP1)
34% xvec_t: Transient state vectors (empty for MAPMAP1)
35% t: Time vector
36% iters: Number of iterations (always 1 for MAPMAP1)
37% runtime: Execution time in seconds
38%
39% References:
40% BUTools: Queueing and traffic modeling library
41% https://github.com/ghorvath78/butools
42%
43% See also: solver_fluid_analyzer, fluid_is_single_queue, FluFluQueue
44
45 runtime_start = tic;
46
47 M = sn.nstations;
48 K = sn.nclasses;
49
50 % Initialize output arrays
51 QN = zeros(M, K);
52 UN = zeros(M, K);
53 RN = zeros(M, K);
54 TN = zeros(M, K);
55
56 % Initialize transient outputs (MFQ is steady-state only)
57 t = [0; options.timespan(2)];
58 QNt = cell(M, K);
59 UNt = cell(M, K);
60 TNt = cell(M, K);
61 for ist = 1:M
62 for k = 1:K
63 QNt{ist, k} = zeros(2, 1);
64 UNt{ist, k} = zeros(2, 1);
65 TNt{ist, k} = zeros(2, 1);
66 end
67 end
68
69 xvec_t = [];
70 xvec_it = {zeros(size(sn.state{1}, 2), 1)};
71
72 % =========================================================================
73 % TOPOLOGY VALIDATION
74 % =========================================================================
75
76 [isSingleQueue, fluidInfo] = fluid_is_single_queue(sn);
77
78 if ~isSingleQueue
79 line_error(mfilename, 'MFQ requires single-queue topology: %s', fluidInfo.errorMsg);
80 end
81
82 % Get station indices (for accessing sn arrays)
83 sourceIdx = fluidInfo.sourceStation;
84 queueIdx = fluidInfo.queueStation;
85 % Note: sinkIdx is a node index, but we don't need to access station arrays for sink
86
87 line_debug('MFQ: Source station=%d, Queue station=%d', sourceIdx, queueIdx);
88
89 % =========================================================================
90 % EXTRACT PARAMETERS FOR EACH CLASS
91 % =========================================================================
92
93 % Process each class independently
94 for k = 1:K
95 if isinf(sn.njobs(k))
96 % Open class
97 line_debug('MFQ: Processing open class %d', k);
98
99 % Extract MFQ parameters
100 [Qin, Rin, Qout, Rout, srv0stop, fluidInfo_k] = fluid_extract_params(sn, fluidInfo);
101
102 % Get arrival rate
103 lambda = sn.rates(sourceIdx, k);
104
105 % ===================================================================
106 % DETERMINE IF SIMPLE EXPONENTIAL (M/M/1) OR COMPLEX (MAP/PH)
107 % ===================================================================
108
109 % Check if both arrival and service are simple exponential (1-state)
110 is_simple_exponential = isscalar(Qin) && isscalar(Qout) && Qin == 0 && Qout == 0;
111
112 if is_simple_exponential
113 % Simple M/M/1 case: use analytical formulas
114 % FluFluQueue doesn't handle trivial 1-state CTMCs well
115 line_debug('MFQ: Using analytical M/M/1 formulas for class %d', k);
116
117 mu = fluidInfo_k.mu;
118 rho = lambda / mu;
119
120 if rho >= 1
121 line_warning(mfilename, 'System is unstable (rho=%.4f >= 1) for class %d', rho, k);
122 flMoms = [Inf, Inf];
123 stMoms = [Inf, Inf];
124 else
125 % M/M/1 analytical formulas
126 % E[L] = rho / (1 - rho)
127 % E[W] = 1 / (mu - lambda)
128 % Var[L] = rho / (1 - rho)^2
129 % Var[W] = 1 / (mu - lambda)^2
130 L_mean = rho / (1 - rho);
131 L_var = rho / (1 - rho)^2;
132 W_mean = 1 / (mu - lambda);
133 W_var = 1 / (mu - lambda)^2;
134
135 flMoms = [L_mean, L_mean^2 + L_var]; % E[L], E[L^2]
136 stMoms = [W_mean, W_mean^2 + W_var]; % E[W], E[W^2]
137 end
138 else
139 % Complex case: call FluFluQueue
140 try
141 % Request 2 moments for mean and variance
142 % Set precision to match solver tolerance
143 prec = max(options.tol, 1e-14);
144
145 line_debug('MFQ: Calling FluFluQueue for class %d with precision %.2e', k, prec);
146
147 % Call FluFluQueue to get fluid level moments and sojourn time moments
148 [flMoms, stMoms] = FluFluQueue(Qin, Rin, Qout, Rout, srv0stop, ...
149 'flMoms', 2, 'stMoms', 2, 'prec', prec);
150 catch ME
151 line_error(mfilename, 'FluFluQueue failed for class %d: %s', k, ME.message);
152 end
153 end
154
155 % ===============================================================
156 % MAP RESULTS TO LINE METRICS
157 % ===============================================================
158
159 % Fluid level first moment = mean queue length
160 QN(queueIdx, k) = flMoms(1);
161
162 % Sojourn time first moment = mean response time
163 RN(queueIdx, k) = stMoms(1);
164
165 % Throughput from Little's Law: X = L / W
166 if stMoms(1) > GlobalConstants.FineTol
167 TN(queueIdx, k) = flMoms(1) / stMoms(1);
168 else
169 TN(queueIdx, k) = lambda;
170 end
171
172 % Utilization: U = lambda * E[S]
173 % Extract mean service time from service process
174 mu = fluidInfo_k.mu;
175 UN(queueIdx, k) = min(1, lambda * (1 / mu));
176
177 % Set throughput at source equal to queue throughput
178 TN(sourceIdx, k) = TN(queueIdx, k);
179
180 % ===============================================================
181 % TRANSIENT SOLUTION (TRIVIAL FOR MFQ)
182 % ===============================================================
183
184 % MFQ provides steady-state solution only
185 % Return trivial transient: jump from 0 to steady-state
186
187 QNt{queueIdx, k} = [0; QN(queueIdx, k)];
188 UNt{queueIdx, k} = [0; UN(queueIdx, k)];
189 TNt{queueIdx, k} = [0; TN(queueIdx, k)];
190
191 QNt{sourceIdx, k} = [0; QN(sourceIdx, k)];
192 UNt{sourceIdx, k} = [0; UN(sourceIdx, k)];
193 TNt{sourceIdx, k} = [0; TN(sourceIdx, k)];
194
195 % ===============================================================
196 % VALIDATION: LITTLE'S LAW
197 % ===============================================================
198
199 little_lhs = flMoms(1);
200 little_rhs = lambda * stMoms(1);
201
202 if little_rhs > 0
203 little_error = abs(little_lhs - little_rhs) / little_rhs;
204 if little_error > 0.01 % 1% tolerance
205 line_warning(mfilename, ...
206 'Little''s Law violation for class %d: L=%.6f vs λW=%.6f (error=%.2f%%)', ...
207 k, little_lhs, little_rhs, little_error * 100);
208 else
209 line_debug('Little''s Law satisfied for class %d (error=%.4f%%)', k, little_error * 100);
210 end
211 end
212
213 else
214 % Closed class: not supported by MFQ
215 line_warning(mfilename, 'MFQ only supports open classes. Class %d is closed, skipping.', k);
216 end
217 end
218
219 % =========================================================================
220 % FINALIZE OUTPUT
221 % =========================================================================
222
223 % System-level metrics (from reference station)
224 XN = zeros(1, K);
225 CN = zeros(1, K);
226
227 for k = 1:K
228 if sn.refstat(k) > 0 % Ignore artificial classes
229 XN(k) = TN(sn.refstat(k), k);
230 if isinf(sn.njobs(k))
231 % Open class: CN = sum of response times
232 CN(k) = sum(RN(:, k));
233 else
234 % Closed class: CN = N / X (should not reach here)
235 CN(k) = sn.njobs(k) / XN(k);
236 end
237 end
238 end
239
240 % Assign system metrics to source station for compatibility
241 % (source is the reference station for open classes)
242 for k = 1:K
243 if isinf(sn.njobs(k))
244 TN(sourceIdx, k) = XN(k);
245 end
246 end
247
248 % Create state vector (dummy for compatibility)
249 xvec_it = {zeros(1, 1)};
250
251 % Number of iterations is 1 for MFQ (no iteration needed)
252 iters = 1;
253
254 runtime = toc(runtime_start);
255
256 line_debug('MFQ completed in %.4f seconds', runtime);
257end
258
259function [Qin, Rin, Qout, Rout, srv0stop, fluidInfo] = fluid_extract_params(sn, fluidInfo)
260%FLUID_EXTRACT_PARAMS Extract FluFluQueue parameters from LINE network structure
261
262 % Use station indices (not node indices)
263 sourceIdx = fluidInfo.sourceStation;
264 queueIdx = fluidInfo.queueStation;
265
266 % For now, analyze single-class (class 1)
267 classIdx = 1;
268
269 % Default boundary behavior: service stops when empty (work-conserving)
270 srv0stop = true;
271
272 % Override if specified in options
273 if isfield(fluidInfo, 'srv0stop')
274 srv0stop = fluidInfo.srv0stop;
275 end
276
277 % =========================================================================
278 % EXTRACT ARRIVAL PROCESS (from Source station)
279 % =========================================================================
280
281 % Get arrival rate
282 lambda = sn.rates(sourceIdx, classIdx);
283
284 if isnan(lambda) || lambda <= 0
285 error('No valid arrival rate for class %d at source', classIdx);
286 end
287
288 % Get arrival process
289 arrivalProc = sn.proc{sourceIdx}{classIdx};
290
291 if isempty(arrivalProc)
292 % Simple Poisson arrival (single state)
293 Qin = 0;
294 Rin = lambda;
295 elseif isa(arrivalProc, 'MMDP')
296 % MMDP: use Q and R directly (already in BUTools format)
297 Qin = arrivalProc.Q();
298 Rin = arrivalProc.R();
299 else
300 % MAP or PH arrival process
301 % Check if it's a simple exponential (1 state)
302 if size(arrivalProc{1}, 1) == 1
303 % Single state: Poisson arrival
304 Qin = 0;
305 Rin = lambda;
306 else
307 % Multi-state MAP/MMPP: convert to CTMC format
308 [Qin, Rin] = fluid_dist2butools(arrivalProc, lambda, true);
309 end
310 end
311
312 % =========================================================================
313 % EXTRACT SERVICE PROCESS (from Queue station)
314 % =========================================================================
315
316 % Get service rates per phase
317 mu_vec = sn.mu{queueIdx}{classIdx};
318
319 if isempty(mu_vec)
320 error('No valid service rate for class %d at queue', classIdx);
321 end
322
323 % Average service rate (for normalization)
324 mu_avg = mean(mu_vec);
325
326 % Get service process
327 serviceProc = sn.proc{queueIdx}{classIdx};
328
329 if isempty(serviceProc)
330 % Simple exponential service (single phase)
331 Qout = 0;
332 Rout = mu_avg;
333 elseif isa(serviceProc, 'MMDP')
334 % MMDP: use Q and R directly (already in BUTools format)
335 Qout = serviceProc.Q();
336 Rout = serviceProc.R();
337 else
338 % PH or Coxian service process
339 % Check if it's a simple exponential (1 phase)
340 if size(serviceProc{1}, 1) == 1
341 % Single phase: exponential service
342 Qout = 0;
343 Rout = mu_avg;
344 else
345 % Multi-phase: general phase-type service
346 [Qout, Rout] = fluid_dist2butools(serviceProc, mu_avg, false);
347 end
348 end
349
350 % =========================================================================
351 % VALIDATION
352 % =========================================================================
353
354 % Validate dimensions
355 if isscalar(Qin)
356 % Trivial case: scalar CTMC (Poisson arrival)
357 if Qin ~= 0 || Rin <= 0
358 error('Invalid arrival process parameters');
359 end
360 else
361 [n_in, m_in] = size(Qin);
362 if n_in ~= m_in
363 error('Qin must be square');
364 end
365 [n_rin, m_rin] = size(Rin);
366 if n_rin ~= n_in || m_rin ~= n_in
367 error('Rin dimensions do not match Qin');
368 end
369 end
370
371 if isscalar(Qout)
372 % Trivial case: scalar CTMC (exponential service)
373 if Qout ~= 0 || Rout <= 0
374 error('Invalid service process parameters');
375 end
376 else
377 [n_out, m_out] = size(Qout);
378 if n_out ~= m_out
379 error('Qout must be square');
380 end
381 [n_rout, m_rout] = size(Rout);
382 if n_rout ~= n_out || m_rout ~= n_out
383 error('Rout dimensions do not match Qout');
384 end
385 end
386
387 % Store extracted parameters in fluidInfo for later use
388 fluidInfo.lambda = lambda;
389 fluidInfo.mu = mu_avg;
390 fluidInfo.arrivalProc = arrivalProc;
391 fluidInfo.serviceProc = serviceProc;
392end
393
394function [Q, R] = fluid_dist2butools(ph_process, rate_scale, is_arrival)
395%FLUID_DIST2BUTOOLS Convert LINE phase-type distribution to BUTools CTMC format
396
397 % Extract phase-type matrices
398 if ~iscell(ph_process) || length(ph_process) < 2
399 error('ph_process must be a cell array {D0, D1}');
400 end
401
402 D0 = ph_process{1};
403 D1 = ph_process{2};
404
405 % Check dimensions
406 [n_d0, m_d0] = size(D0);
407 [n_d1, m_d1] = size(D1);
408
409 if n_d0 ~= m_d0
410 error('D0 must be square');
411 end
412
413 if n_d1 ~= n_d0 || m_d1 ~= m_d0
414 error('D0 and D1 must have the same dimensions');
415 end
416
417 n = n_d0; % Number of states
418
419 % Compute generator Q = D0 + D1
420 Q = D0 + D1;
421
422 % Validate Q is a proper generator
423 for i = 1:n
424 % Diagonal element should be negative or zero
425 if Q(i, i) > 1e-14
426 warning('Generator Q has positive diagonal element Q(%d,%d)=%.4e', i, i, Q(i, i));
427 end
428
429 % Off-diagonal elements should be non-negative
430 for j = 1:n
431 if i ~= j && Q(i, j) < -1e-14
432 warning('Generator Q has negative off-diagonal element Q(%d,%d)=%.4e', i, j, Q(i, j));
433 end
434 end
435
436 % Row sum should be non-positive (≤ 0)
437 rowsum = sum(Q(i, :));
438 if rowsum > 1e-14
439 warning('Generator Q has positive row sum at row %d: sum=%.4e', i, rowsum);
440 end
441 end
442
443 % Compute rate matrix R
444 R = zeros(n, n);
445
446 % For both arrival and service, use D1 to determine fluid rates
447 for i = 1:n
448 R(i, i) = sum(D1(i, :));
449 end
450
451 % Validate rate matrix
452 if any(diag(R) < -1e-14)
453 error('Rate matrix R has negative diagonal entries');
454 end
455
456 % Check if R is actually diagonal (off-diagonal should be zero)
457 off_diag_sum = sum(sum(abs(R - diag(diag(R)))));
458 if off_diag_sum > 1e-12
459 warning('Rate matrix R is not diagonal, taking diagonal part only');
460 R = diag(diag(R));
461 end
462end