LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_mfq.m
1function [QN,UN,RN,TN,xvec_it,QNt,UNt,TNt,xvec_t,t,iters,runtime] = solver_mfq(sn, options)
2%SOLVER_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_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 line_warning(mfilename, 'MFQ requires Markov-modulated arrival or service, switching to analytical formulas.');
117
118 mu = fluidInfo_k.mu;
119 rho = lambda / mu;
120
121 if rho >= 1
122 line_warning(mfilename, 'System is unstable (rho=%.4f >= 1) for class %d', rho, k);
123 flMoms = [Inf, Inf];
124 stMoms = [Inf, Inf];
125 else
126 % M/M/1 analytical formulas
127 % E[L] = rho / (1 - rho)
128 % E[W] = 1 / (mu - lambda)
129 % Var[L] = rho / (1 - rho)^2
130 % Var[W] = 1 / (mu - lambda)^2
131 L_mean = rho / (1 - rho);
132 L_var = rho / (1 - rho)^2;
133 W_mean = 1 / (mu - lambda);
134 W_var = 1 / (mu - lambda)^2;
135
136 flMoms = [L_mean, L_mean^2 + L_var]; % E[L], E[L^2]
137 stMoms = [W_mean, W_mean^2 + W_var]; % E[W], E[W^2]
138 end
139 else
140 % Complex case: call FluFluQueue
141 try
142 % Request 2 moments for mean and variance
143 % Set precision to match solver tolerance
144 prec = max(options.tol, 1e-14);
145
146 line_debug('MFQ: Calling FluFluQueue for class %d with precision %.2e', k, prec);
147
148 % Call FluFluQueue to get fluid level moments and sojourn time moments
149 [flMoms, stMoms] = FluFluQueue(Qin, Rin, Qout, Rout, srv0stop, ...
150 'flMoms', 2, 'stMoms', 2, 'prec', prec);
151 catch ME
152 line_error(mfilename, 'FluFluQueue failed for class %d: %s', k, ME.message);
153 end
154 end
155
156 % ===============================================================
157 % MAP RESULTS TO LINE METRICS
158 % ===============================================================
159
160 % Fluid level first moment = mean queue length
161 QN(queueIdx, k) = flMoms(1);
162
163 % Sojourn time first moment = mean response time
164 RN(queueIdx, k) = stMoms(1);
165
166 % Throughput from Little's Law: X = L / W
167 if stMoms(1) > GlobalConstants.FineTol
168 TN(queueIdx, k) = flMoms(1) / stMoms(1);
169 else
170 TN(queueIdx, k) = lambda;
171 end
172
173 % Utilization: U = lambda * E[S]
174 % Extract mean service time from service process
175 mu = fluidInfo_k.mu;
176 UN(queueIdx, k) = min(1, lambda * (1 / mu));
177
178 % Set throughput at source equal to queue throughput
179 TN(sourceIdx, k) = TN(queueIdx, k);
180
181 % ===============================================================
182 % TRANSIENT SOLUTION (TRIVIAL FOR MFQ)
183 % ===============================================================
184
185 % MFQ provides steady-state solution only
186 % Return trivial transient: jump from 0 to steady-state
187
188 QNt{queueIdx, k} = [0; QN(queueIdx, k)];
189 UNt{queueIdx, k} = [0; UN(queueIdx, k)];
190 TNt{queueIdx, k} = [0; TN(queueIdx, k)];
191
192 QNt{sourceIdx, k} = [0; QN(sourceIdx, k)];
193 UNt{sourceIdx, k} = [0; UN(sourceIdx, k)];
194 TNt{sourceIdx, k} = [0; TN(sourceIdx, k)];
195
196 % ===============================================================
197 % VALIDATION: LITTLE'S LAW
198 % ===============================================================
199
200 little_lhs = flMoms(1);
201 little_rhs = lambda * stMoms(1);
202
203 if little_rhs > 0
204 little_error = abs(little_lhs - little_rhs) / little_rhs;
205 if little_error > 0.01 % 1% tolerance
206 line_warning(mfilename, ...
207 'Little''s Law violation for class %d: L=%.6f vs λW=%.6f (error=%.2f%%)', ...
208 k, little_lhs, little_rhs, little_error * 100);
209 else
210 line_debug('Little''s Law satisfied for class %d (error=%.4f%%)', k, little_error * 100);
211 end
212 end
213
214 else
215 % Closed class: not supported by MFQ
216 line_warning(mfilename, 'MFQ only supports open classes. Class %d is closed, skipping.', k);
217 end
218 end
219
220 % =========================================================================
221 % FINALIZE OUTPUT
222 % =========================================================================
223
224 % System-level metrics (from reference station)
225 XN = zeros(1, K);
226 CN = zeros(1, K);
227
228 for k = 1:K
229 if sn.refstat(k) > 0 % Ignore artificial classes
230 XN(k) = TN(sn.refstat(k), k);
231 if isinf(sn.njobs(k))
232 % Open class: CN = sum of response times
233 CN(k) = sum(RN(:, k));
234 else
235 % Closed class: CN = N / X (should not reach here)
236 CN(k) = sn.njobs(k) / XN(k);
237 end
238 end
239 end
240
241 % Assign system metrics to source station for compatibility
242 % (source is the reference station for open classes)
243 for k = 1:K
244 if isinf(sn.njobs(k))
245 TN(sourceIdx, k) = XN(k);
246 end
247 end
248
249 % Create state vector (dummy for compatibility)
250 xvec_it = {zeros(1, 1)};
251
252 % Number of iterations is 1 for MFQ (no iteration needed)
253 iters = 1;
254
255 runtime = toc(runtime_start);
256
257 line_debug('MFQ completed in %.4f seconds', runtime);
258end
259
260function [Qin, Rin, Qout, Rout, srv0stop, fluidInfo] = fluid_extract_params(sn, fluidInfo)
261%FLUID_EXTRACT_PARAMS Extract FluFluQueue parameters from LINE network structure
262
263 % Use station indices (not node indices)
264 sourceIdx = fluidInfo.sourceStation;
265 queueIdx = fluidInfo.queueStation;
266
267 % For now, analyze single-class (class 1)
268 classIdx = 1;
269
270 % Default boundary behavior: service stops when empty (work-conserving)
271 srv0stop = true;
272
273 % Override if specified in options
274 if isfield(fluidInfo, 'srv0stop')
275 srv0stop = fluidInfo.srv0stop;
276 end
277
278 % =========================================================================
279 % EXTRACT ARRIVAL PROCESS (from Source station)
280 % =========================================================================
281
282 % Get arrival rate
283 lambda = sn.rates(sourceIdx, classIdx);
284
285 if isnan(lambda) || lambda <= 0
286 error('No valid arrival rate for class %d at source', classIdx);
287 end
288
289 % Get arrival process
290 arrivalProc = sn.proc{sourceIdx}{classIdx};
291
292 if isempty(arrivalProc)
293 % Simple Poisson arrival (single state)
294 Qin = 0;
295 Rin = lambda;
296 elseif isa(arrivalProc, 'MMDP')
297 % MMDP: use Q and R directly (already in BUTools format)
298 Qin = arrivalProc.Q();
299 Rin = arrivalProc.R();
300 else
301 % MAP or PH arrival process
302 % Check if it's a simple exponential (1 state)
303 if size(arrivalProc{1}, 1) == 1
304 % Single state: Poisson arrival
305 Qin = 0;
306 Rin = lambda;
307 else
308 % Multi-state MAP/MMPP: convert to CTMC format
309 [Qin, Rin] = fluid_dist2butools(arrivalProc, lambda, true);
310 end
311 end
312
313 % =========================================================================
314 % EXTRACT SERVICE PROCESS (from Queue station)
315 % =========================================================================
316
317 % Get service rates per phase
318 mu_vec = sn.mu{queueIdx}{classIdx};
319
320 if isempty(mu_vec)
321 error('No valid service rate for class %d at queue', classIdx);
322 end
323
324 % Average service rate (for normalization)
325 mu_avg = mean(mu_vec);
326
327 % Get service process
328 serviceProc = sn.proc{queueIdx}{classIdx};
329
330 if isempty(serviceProc)
331 % Simple exponential service (single phase)
332 Qout = 0;
333 Rout = mu_avg;
334 elseif isa(serviceProc, 'MMDP')
335 % MMDP: use Q and R directly (already in BUTools format)
336 Qout = serviceProc.Q();
337 Rout = serviceProc.R();
338 else
339 % PH or Coxian service process
340 % Check if it's a simple exponential (1 phase)
341 if size(serviceProc{1}, 1) == 1
342 % Single phase: exponential service
343 Qout = 0;
344 Rout = mu_avg;
345 else
346 % Multi-phase: general phase-type service
347 [Qout, Rout] = fluid_dist2butools(serviceProc, mu_avg, false);
348 end
349 end
350
351 % =========================================================================
352 % VALIDATION
353 % =========================================================================
354
355 % Validate dimensions
356 if isscalar(Qin)
357 % Trivial case: scalar CTMC (Poisson arrival)
358 if Qin ~= 0 || Rin <= 0
359 error('Invalid arrival process parameters');
360 end
361 else
362 [n_in, m_in] = size(Qin);
363 if n_in ~= m_in
364 error('Qin must be square');
365 end
366 [n_rin, m_rin] = size(Rin);
367 if n_rin ~= n_in || m_rin ~= n_in
368 error('Rin dimensions do not match Qin');
369 end
370 end
371
372 if isscalar(Qout)
373 % Trivial case: scalar CTMC (exponential service)
374 if Qout ~= 0 || Rout <= 0
375 error('Invalid service process parameters');
376 end
377 else
378 [n_out, m_out] = size(Qout);
379 if n_out ~= m_out
380 error('Qout must be square');
381 end
382 [n_rout, m_rout] = size(Rout);
383 if n_rout ~= n_out || m_rout ~= n_out
384 error('Rout dimensions do not match Qout');
385 end
386 end
387
388 % Store extracted parameters in fluidInfo for later use
389 fluidInfo.lambda = lambda;
390 fluidInfo.mu = mu_avg;
391 fluidInfo.arrivalProc = arrivalProc;
392 fluidInfo.serviceProc = serviceProc;
393end
394
395function [Q, R] = fluid_dist2butools(ph_process, rate_scale, is_arrival)
396%FLUID_DIST2BUTOOLS Convert LINE phase-type distribution to BUTools CTMC format
397
398 % Extract phase-type matrices
399 if ~iscell(ph_process) || length(ph_process) < 2
400 error('ph_process must be a cell array {D0, D1}');
401 end
402
403 D0 = ph_process{1};
404 D1 = ph_process{2};
405
406 % Check dimensions
407 [n_d0, m_d0] = size(D0);
408 [n_d1, m_d1] = size(D1);
409
410 if n_d0 ~= m_d0
411 error('D0 must be square');
412 end
413
414 if n_d1 ~= n_d0 || m_d1 ~= m_d0
415 error('D0 and D1 must have the same dimensions');
416 end
417
418 n = n_d0; % Number of states
419
420 % Compute generator Q = D0 + D1
421 Q = D0 + D1;
422
423 % Validate Q is a proper generator
424 for i = 1:n
425 % Diagonal element should be negative or zero
426 if Q(i, i) > 1e-14
427 warning('Generator Q has positive diagonal element Q(%d,%d)=%.4e', i, i, Q(i, i));
428 end
429
430 % Off-diagonal elements should be non-negative
431 for j = 1:n
432 if i ~= j && Q(i, j) < -1e-14
433 warning('Generator Q has negative off-diagonal element Q(%d,%d)=%.4e', i, j, Q(i, j));
434 end
435 end
436
437 % Row sum should be non-positive (≤ 0)
438 rowsum = sum(Q(i, :));
439 if rowsum > 1e-14
440 warning('Generator Q has positive row sum at row %d: sum=%.4e', i, rowsum);
441 end
442 end
443
444 % Compute rate matrix R
445 R = zeros(n, n);
446
447 % For both arrival and service, use D1 to determine fluid rates
448 for i = 1:n
449 R(i, i) = sum(D1(i, :));
450 end
451
452 % Validate rate matrix
453 if any(diag(R) < -1e-14)
454 error('Rate matrix R has negative diagonal entries');
455 end
456
457 % Check if R is actually diagonal (off-diagonal should be zero)
458 off_diag_sum = sum(sum(abs(R - diag(diag(R)))));
459 if off_diag_sum > 1e-12
460 warning('Rate matrix R is not diagonal, taking diagonal part only');
461 R = diag(diag(R));
462 end
463end