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
4% [QN,UN,RN,TN,xvec_it,QNt,UNt,TNt,xvec_t,t,iters,runtime] = SOLVER_FLUID_MFQ(sn, options)
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.
10% This solver applies only to single-queue topologies:
11% Source (external arrivals) -> Queue (finite server) -> Sink
14% - Single-queue open system
15% - Single-server (c=1) or infinite-server (c=Inf)
17% - Supported distributions: Exp, Erlang, HyperExp, Cox, APH, MAP, PH, MMDP
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)
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)
36% iters: Number of iterations (always 1 for MAPMAP1)
37% runtime: Execution time in seconds
40% BUTools: Queueing and traffic modeling library
41% https://github.com/ghorvath78/butools
43% See also: solver_fluid_analyzer, fluid_is_single_queue, FluFluQueue
50 % Initialize output arrays
56 % Initialize transient outputs (MFQ is steady-state only)
57 t = [0; options.timespan(2)];
63 QNt{ist, k} = zeros(2, 1);
64 UNt{ist, k} = zeros(2, 1);
65 TNt{ist, k} = zeros(2, 1);
70 xvec_it = {zeros(size(sn.state{1}, 2), 1)};
72 % =========================================================================
74 % =========================================================================
76 [isSingleQueue, fluidInfo] = fluid_is_single_queue(sn);
79 line_error(mfilename, 'MFQ
requires single-queue topology: %s
', fluidInfo.errorMsg);
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
87 line_debug(
'MFQ: Source station=%d, Queue station=%d', sourceIdx, queueIdx);
89 % =========================================================================
90 % EXTRACT PARAMETERS FOR EACH CLASS
91 % =========================================================================
93 % Process each
class independently
97 line_debug(
'MFQ: Processing open class %d', k);
99 % Extract MFQ parameters
100 [Qin, Rin, Qout, Rout, srv0stop, fluidInfo_k] = fluid_extract_params(sn, fluidInfo);
103 lambda = sn.rates(sourceIdx, k);
105 % ===================================================================
106 % DETERMINE IF SIMPLE EXPONENTIAL (M/M/1) OR COMPLEX (MAP/PH)
107 % ===================================================================
109 % Check if both arrival and service are simple exponential (1-state)
110 is_simple_exponential = isscalar(Qin) && isscalar(Qout) && Qin == 0 && Qout == 0;
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);
121 line_warning(mfilename, 'System
is unstable (rho=%.4f >= 1) for class %d', rho, k);
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;
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]
139 % Complex case: call FluFluQueue
141 % Request 2 moments for mean and variance
142 % Set precision to match solver tolerance
143 prec = max(options.tol, 1e-14);
145 line_debug('MFQ: Calling FluFluQueue for class %d with precision %.2e', k, prec);
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);
151 line_error(mfilename, 'FluFluQueue failed for class %d: %s', k, ME.message);
155 % ===============================================================
156 % MAP RESULTS TO LINE METRICS
157 % ===============================================================
159 % Fluid level first moment = mean queue length
160 QN(queueIdx, k) = flMoms(1);
162 % Sojourn time first moment = mean response time
163 RN(queueIdx, k) = stMoms(1);
165 % Throughput from Little's Law: X = L / W
166 if stMoms(1) > GlobalConstants.FineTol
167 TN(queueIdx, k) = flMoms(1) / stMoms(1);
169 TN(queueIdx, k) = lambda;
172 % Utilization: U = lambda * E[S]
173 % Extract mean service time from service process
175 UN(queueIdx, k) = min(1, lambda * (1 / mu));
177 % Set throughput at source equal to queue throughput
178 TN(sourceIdx, k) = TN(queueIdx, k);
180 % ===============================================================
181 % TRANSIENT SOLUTION (TRIVIAL FOR MFQ)
182 % ===============================================================
184 % MFQ provides steady-state solution only
185 % Return trivial transient: jump from 0 to steady-state
187 QNt{queueIdx, k} = [0; QN(queueIdx, k)];
188 UNt{queueIdx, k} = [0; UN(queueIdx, k)];
189 TNt{queueIdx, k} = [0; TN(queueIdx, k)];
191 QNt{sourceIdx, k} = [0; QN(sourceIdx, k)];
192 UNt{sourceIdx, k} = [0; UN(sourceIdx, k)];
193 TNt{sourceIdx, k} = [0; TN(sourceIdx, k)];
195 % ===============================================================
196 % VALIDATION: LITTLE
'S LAW
197 % ===============================================================
199 little_lhs = flMoms(1);
200 little_rhs = lambda * stMoms(1);
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);
209 line_debug('Little
''s Law satisfied
for class %d (error=%.4f%%)
', k, little_error * 100);
214 % Closed class: not supported by MFQ
215 line_warning(mfilename, 'MFQ only supports open
classes. Class %d
is closed, skipping.
', k);
219 % =========================================================================
221 % =========================================================================
223 % System-level metrics (from reference station)
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));
234 % Closed class: CN = N / X (should not reach here)
235 CN(k) = sn.njobs(k) / XN(k);
240 % Assign system metrics to source station for compatibility
241 % (source is the reference station for open classes)
243 if isinf(sn.njobs(k))
244 TN(sourceIdx, k) = XN(k);
248 % Create state vector (dummy for compatibility)
249 xvec_it = {zeros(1, 1)};
251 % Number of iterations is 1 for MFQ (no iteration needed)
254 runtime = toc(runtime_start);
256 line_debug('MFQ completed in %.4f seconds
', runtime);
259function [Qin, Rin, Qout, Rout, srv0stop, fluidInfo] = fluid_extract_params(sn, fluidInfo)
260%FLUID_EXTRACT_PARAMS Extract FluFluQueue parameters from LINE network structure
262 % Use station indices (not node indices)
263 sourceIdx = fluidInfo.sourceStation;
264 queueIdx = fluidInfo.queueStation;
266 % For now, analyze single-class (class 1)
269 % Default boundary behavior: service stops when empty (work-conserving)
272 % Override if specified in options
273 if isfield(fluidInfo, 'srv0stop
')
274 srv0stop = fluidInfo.srv0stop;
277 % =========================================================================
278 % EXTRACT ARRIVAL PROCESS (from Source station)
279 % =========================================================================
282 lambda = sn.rates(sourceIdx, classIdx);
284 if isnan(lambda) || lambda <= 0
285 error('No valid arrival rate
for class %d at source
', classIdx);
288 % Get arrival process
289 arrivalProc = sn.proc{sourceIdx}{classIdx};
291 if isempty(arrivalProc)
292 % Simple Poisson arrival (single state)
295 elseif isa(arrivalProc, 'MMDP
')
296 % MMDP: use Q and R directly (already in BUTools format)
297 Qin = arrivalProc.Q();
298 Rin = arrivalProc.R();
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
307 % Multi-state MAP/MMPP: convert to CTMC format
308 [Qin, Rin] = fluid_dist2butools(arrivalProc, lambda,
true);
312 % =========================================================================
313 % EXTRACT SERVICE PROCESS (from Queue station)
314 % =========================================================================
316 % Get service rates per phase
317 mu_vec = sn.mu{queueIdx}{classIdx};
320 error(
'No valid service rate for class %d at queue', classIdx);
323 % Average service rate (
for normalization)
324 mu_avg = mean(mu_vec);
326 % Get service process
327 serviceProc = sn.proc{queueIdx}{classIdx};
329 if isempty(serviceProc)
330 % Simple exponential service (single phase)
333 elseif isa(serviceProc,
'MMDP')
334 % MMDP: use Q and R directly (already in BUTools format)
335 Qout = serviceProc.Q();
336 Rout = serviceProc.R();
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
345 % Multi-phase: general phase-type service
346 [Qout, Rout] = fluid_dist2butools(serviceProc, mu_avg, false);
350 % =========================================================================
352 % =========================================================================
354 % Validate dimensions
356 % Trivial case: scalar CTMC (Poisson arrival)
357 if Qin ~= 0 || Rin <= 0
358 error('Invalid arrival process parameters
');
361 [n_in, m_in] = size(Qin);
363 error('Qin must be square
');
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
');
372 % Trivial case: scalar CTMC (exponential service)
373 if Qout ~= 0 || Rout <= 0
374 error('Invalid service process parameters
');
377 [n_out, m_out] = size(Qout);
379 error('Qout must be square
');
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
');
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;
394function [Q, R] = fluid_dist2butools(ph_process, rate_scale, is_arrival)
395%FLUID_DIST2BUTOOLS Convert LINE phase-type distribution to BUTools CTMC format
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}
');
406 [n_d0, m_d0] = size(D0);
407 [n_d1, m_d1] = size(D1);
410 error('D0 must be square
');
413 if n_d1 ~= n_d0 || m_d1 ~= m_d0
414 error('D0 and D1 must have the same dimensions
');
417 n = n_d0; % Number of states
419 % Compute generator Q = D0 + D1
422 % Validate Q is a proper generator
424 % Diagonal element should be negative or zero
426 warning('Generator Q has positive diagonal element Q(%d,%d)=%.4e
', i, i, Q(i, i));
429 % Off-diagonal elements should be non-negative
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));
436 % Row sum should be non-positive (≤ 0)
437 rowsum = sum(Q(i, :));
439 warning('Generator Q has positive row sum at row %d: sum=%.4e
', i, rowsum);
443 % Compute rate matrix R
446 % For both arrival and service, use D1 to determine fluid rates
448 R(i, i) = sum(D1(i, :));
451 % Validate rate matrix
452 if any(diag(R) < -1e-14)
453 error('Rate matrix R has negative diagonal entries
');
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
');