1function [Qt, Ut, Tt] = solver_mam_ldqbd_transient(sn, options)
2% SOLVER_MAM_LDQBD_TRANSIENT Transient analysis of open queues via standard QBD
4% Supports single-
class open models (Source -> Queue -> Sink).
5% For M/M/c: scalar QBD levels (any number of servers).
6% For M/PH/1: m-phase QBD levels (single server only).
8% Infinite capacity: libQBD adaptive Taylor series.
9% Finite capacity: direct matrix exponentiation (expm).
11% Copyright (c) 2012-2026, Imperial College London
14%% Validate model structure
19 line_error(mfilename,
'Transient QBD method requires a single-class model.');
24 line_error(mfilename, 'Transient QBD method
requires an open model.
');
28sourceIdx = find(sn.sched == SchedStrategy.EXT);
29queueIdx = find(sn.sched == SchedStrategy.FCFS);
31if numel(sourceIdx) ~= 1 || numel(queueIdx) ~= 1
32 line_error(mfilename, 'Transient QBD method
requires exactly one Source and one FCFS Queue.
');
36lambda = sn.rates(sourceIdx, 1);
37PH_queue = sn.proc{queueIdx}{1};
38nServers = sn.nservers(queueIdx);
39bufCap = sn.cap(queueIdx);
41if numel(PH_queue{1}) == 1
46 nPhases = size(PH_queue{1}, 1);
50 alpha = map_pie(PH_queue);
51 t_exit = -D0 * ones(nPhases, 1);
54if isPH && nServers > 1
55 line_error(mfilename, 'Transient QBD with PH service supports single-server only.
');
59T_start = options.timespan(1);
60T_end = options.timespan(2);
61T_duration = T_end - T_start;
64 %% Finite capacity: assemble full generator Q and use expm
68 % M/M/c/N: scalar levels, (Cap+1) x (Cap+1) generator
73 dep = min(n, nServers) * mu;
74 arr = lambda * (n < Cap);
76 Q(idx, idx - 1) = dep;
79 Q(idx, idx + 1) = arr;
81 Q(idx, idx) = -(dep + arr);
84 % M/PH/1/N: level 0 is 1-dim, levels 1..Cap are nPhases-dim
85 dim = 1 + Cap * nPhases;
90 Q(1, 2:1+nPhases) = lambda * alpha;
93 rows = (1 + (n-1)*nPhases + 1):(1 + n*nPhases);
95 % Internal transitions (D0) and arrivals
97 Q(rows, rows) = D0 - lambda * eye(nPhases);
102 % Arrivals: level n -> n+1
104 next_rows = rows + nPhases;
105 Q(rows, next_rows) = lambda * eye(nPhases);
108 % Departures: level n -> n-1
112 prev_rows = rows - nPhases;
113 Q(rows, prev_rows) = D1;
119 nTimePoints = min(101, max(11, round(T_duration * 10)));
120 times = linspace(T_start, T_end, nTimePoints)';
121 dt = T_duration / (nTimePoints - 1);
123 % Initial distribution: empty queue
127 % Compute transient distributions via iterative expm
130 queue_lengths = zeros(nTimePoints, 1);
131 util_values = zeros(nTimePoints, 1);
132 tput_values = zeros(nTimePoints, 1);
135 for t_idx = 1:nTimePoints
136 % Extract metrics from pi_t
137 q = 0; u = 0; tput = 0;
145 idx_s = 1 + (n-1)*nPhases + 1;
146 idx_e = 1 + n*nPhases;
147 p_n = sum(pi_t(idx_s:idx_e));
152 u = u + (min(n, nServers) / nServers) * p_n;
154 tput = tput + min(n, nServers) * mu * p_n;
156 idx_s = 1 + (n-1)*nPhases + 1;
157 idx_e = 1 + n*nPhases;
158 tput = tput + pi_t(idx_s:idx_e) * t_exit;
162 queue_lengths(t_idx) = q;
163 util_values(t_idx) = u;
164 tput_values(t_idx) = tput;
166 % Advance to next time point
167 if t_idx < nTimePoints
173 %% Infinite capacity: use libQBD adaptive Taylor series
177 % M/M/c: scalar QBD levels
179 proc.add_zero_level(-lambda, lambda);
181 % Levels 1..c-1: boundary (service rate = n*mu)
184 proc.add_level(dep, -(lambda + dep), lambda);
187 % Repeating level c+: service rate = c*mu
189 proc.add_final_level(dep, -(lambda + dep));
191 % M/PH/1: level 0 scalar, levels 1+ have nPhases phases
192 proc.add_zero_level(-lambda, lambda * alpha);
194 % Level 1: boundary (A_minus
is m x 1, back to scalar level 0)
196 A1_0 = D0 - lambda * eye(nPhases);
197 A1_plus = lambda * eye(nPhases);
198 proc.add_level(A1_minus, A1_0, A1_plus);
200 % Repeating level 2+: A_minus = D1 (m x m)
201 proc.add_final_level(D1, D0 - lambda * eye(nPhases));
204 % Solve
using libQBD adaptive Taylor series
206 solver = libqbd.TaylorSeriesAdaptive(proc, {1}, tol, T_duration);
208 ref_times = solver.get_reference_times() + T_start;
209 ref_dists = solver.get_reference_dists();
210 nTimePoints = numel(ref_times);
211 times = ref_times(:);
213 queue_lengths = zeros(nTimePoints, 1);
214 util_values = zeros(nTimePoints, 1);
215 tput_values = zeros(nTimePoints, 1);
217 for t_idx = 1:nTimePoints
218 dist = ref_dists{t_idx};
219 nLevels = numel(dist);
221 q = 0; u = 0; tput = 0;
223 p_n = sum(dist{n + 1});
225 u = u + (min(n, nServers) / nServers) * p_n;
227 tput = tput + min(n, nServers) * mu * p_n;
229 tput = tput + dist{n + 1} * t_exit;
232 queue_lengths(t_idx) = q;
233 util_values(t_idx) = u;
234 tput_values(t_idx) = tput;
238%% Package results in [metric, time] format
244Qt{queueIdx, 1} = [queue_lengths, times];
245Ut{queueIdx, 1} = [util_values, times];
246Tt{queueIdx, 1} = [tput_values, times];