LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_fluid_diffusion.m
1function [QN,UN,RN,TN,xvec_it,QNt,UNt,TNt,xvec_t,t,iters,runtime] = solver_fluid_diffusion(sn, options)
2% SOLVER_FLUID_DIFFUSION Diffusion approximation for closed multiclass BCMP networks
3%
4% [QN,UN,RN,TN,XVEC_IT,QNT,UNT,TNT,XVEC_T,T,ITERS,RUNTIME] = SOLVER_FLUID_DIFFUSION(SN, OPTIONS)
5%
6% Implements diffusion approximation using the Euler-Maruyama method for SDEs.
7% This method models queue lengths as continuous variables with Brownian noise,
8% providing stochastic extensions to deterministic fluid approximations.
9%
10% Supported models:
11% - Closed multiclass queueing networks only
12% - Scheduling: PS, FCFS, INF (delay stations)
13% - Single-server or infinite-server stations
14%
15% Options:
16% - iter_max: Number of simulation steps (default: 10000)
17% - timestep: Time step for Euler-Maruyama integration (default: 0.01)
18%
19% Copyright (c) 2012-2026, Imperial College London
20% All rights reserved.
21
22runtime_start = tic;
23
24M = sn.nstations; % number of stations
25K = sn.nclasses; % number of classes
26N_vec = sn.njobs'; % population per class (column vector)
27
28%% Model validation - diffusion only supports closed networks
29if any(isinf(N_vec))
30 line_error(mfilename, 'Diffusion method only supports closed queueing networks (no open classes).');
31end
32
33% Check for sources or sinks (not allowed in closed networks for diffusion)
34for ist = 1:M
35 if sn.sched(ist) == SchedStrategy.EXT
36 line_error(mfilename, 'Diffusion method does not support Source nodes. Use closed networks only.');
37 end
38end
39
40% Check scheduling disciplines - only PS, FCFS, INF supported
41for ist = 1:M
42 switch sn.sched(ist)
43 case {SchedStrategy.PS, SchedStrategy.FCFS, SchedStrategy.INF, SchedStrategy.SIRO}
44 % supported
45 otherwise
46 line_error(mfilename, sprintf('Diffusion method does not support scheduling strategy %s at station %d.', ...
47 SchedStrategy.toText(sn.sched(ist)), ist));
48 end
49end
50
51% Check for single-server or infinite-server only
52for ist = 1:M
53 if sn.nservers(ist) > 1 && ~isinf(sn.nservers(ist))
54 line_error(mfilename, sprintf('Diffusion method only supports single-server (c=1) or infinite-server stations. Station %d has %d servers.', ...
55 ist, sn.nservers(ist)));
56 end
57end
58
59%% Extract diffusion-specific options from existing solver options
60% Use iter_max for number of simulation steps
61if isfield(options, 'iter_max') && ~isempty(options.iter_max)
62 steps = options.iter_max;
63else
64 steps = 10000; % default number of steps
65end
66
67% Use timestep for dt
68if isfield(options, 'timestep') && ~isempty(options.timestep)
69 dt = options.timestep;
70else
71 dt = 0.01; % default time step
72end
73
74% Total simulation time is derived from steps and dt
75T = steps * dt;
76
77%% Build inverse service rate matrix mu_inv (M x K)
78% mu_inv(k,r) = average service time for class r at station k
79mu_inv = zeros(M, K);
80for ist = 1:M
81 for r = 1:K
82 if sn.rates(ist, r) > 0 && ~isnan(sn.rates(ist, r))
83 mu_inv(ist, r) = 1 / sn.rates(ist, r);
84 else
85 % If no service for this class at this station, set to Inf
86 % (effectively no departures)
87 mu_inv(ist, r) = Inf;
88 end
89 end
90end
91
92%% Build routing matrix P ((M*K) x (M*K))
93% P(i,j) is probability of routing from flattened state i to flattened state j
94% Flattening: index = (station-1)*K + class for station-major ordering
95% However, bcmp_diffusion uses sub2ind([K R], k, r) which is column-major [queue, class]
96% So we use sub2ind([M K], ist, r) for consistency
97
98% Extract station-to-station routing from the full routing table
99P_full = sn.rt; % Full routing matrix (stateful nodes, class-blocked)
100
101% Build station indices mapping
102station_indices = [];
103for ist = 1:M
104 isf = sn.stationToStateful(ist);
105 for r = 1:K
106 station_indices = [station_indices, (isf-1)*K + r];
107 end
108end
109
110% Extract the station-level routing matrix
111P_stations = P_full(station_indices, station_indices);
112
113% Reshape to (M*K) x (M*K) in the format expected by diffusion
114% The routing matrix needs to be in [station, class] flattened order
115P = zeros(M*K, M*K);
116for ist_from = 1:M
117 for r_from = 1:K
118 idx_from = sub2ind([M K], ist_from, r_from);
119 for ist_to = 1:M
120 for r_to = 1:K
121 idx_to = sub2ind([M K], ist_to, r_to);
122 % Map from station-blocked format to sub2ind format
123 src_idx = (ist_from-1)*K + r_from;
124 dst_idx = (ist_to-1)*K + r_to;
125 P(idx_from, idx_to) = P_stations(src_idx, dst_idx);
126 end
127 end
128 end
129end
130
131%% Run diffusion approximation
132line_debug('Diffusion solver starting: steps=%d, dt=%g, nstations=%d, nclasses=%d', steps, dt, M, K);
133
134[Xavg, Xt] = solver_fluid_diffusion_core(mu_inv, P, N_vec, T, dt);
135
136runtime = toc(runtime_start);
137iters = 1; % Diffusion runs a single trajectory
138
139%% Extract results
140steps = size(Xt, 3);
141t = (0:steps-1)' * dt;
142
143% Queue lengths (steady-state average)
144QN = Xavg;
145
146% Transient queue lengths
147QNt = cell(M, K);
148for ist = 1:M
149 for r = 1:K
150 QNt{ist, r} = squeeze(Xt(ist, r, :));
151 end
152end
153
154% Throughputs: TN = QN * service_rate (for PS/INF) using Little's law approximation
155TN = zeros(M, K);
156for ist = 1:M
157 for r = 1:K
158 if mu_inv(ist, r) > 0 && ~isinf(mu_inv(ist, r))
159 % For infinite servers, throughput = queue_length * service_rate
160 % For single server PS, this is an approximation
161 if isinf(sn.nservers(ist))
162 TN(ist, r) = QN(ist, r) / mu_inv(ist, r);
163 else
164 % For single server, throughput limited by server capacity
165 TN(ist, r) = min(QN(ist, r), 1) / mu_inv(ist, r);
166 end
167 end
168 end
169end
170
171% Transient throughputs
172TNt = cell(M, K);
173for ist = 1:M
174 for r = 1:K
175 if mu_inv(ist, r) > 0 && ~isinf(mu_inv(ist, r))
176 if isinf(sn.nservers(ist))
177 TNt{ist, r} = QNt{ist, r} / mu_inv(ist, r);
178 else
179 TNt{ist, r} = min(QNt{ist, r}, 1) / mu_inv(ist, r);
180 end
181 else
182 TNt{ist, r} = zeros(steps, 1);
183 end
184 end
185end
186
187% Utilizations
188UN = zeros(M, K);
189UNt = cell(M, K);
190for ist = 1:M
191 S = sn.nservers(ist);
192 for r = 1:K
193 if isinf(S)
194 % Infinite server: utilization = queue length
195 UN(ist, r) = QN(ist, r);
196 UNt{ist, r} = QNt{ist, r};
197 else
198 % Single server: utilization = min(queue_length, 1)
199 UN(ist, r) = min(QN(ist, r) / S, 1);
200 UNt{ist, r} = min(QNt{ist, r} / S, 1);
201 end
202 end
203end
204
205% Response times using Little's law: RN = QN / TN
206RN = zeros(M, K);
207for ist = 1:M
208 for r = 1:K
209 if TN(ist, r) > 0
210 RN(ist, r) = QN(ist, r) / TN(ist, r);
211 end
212 end
213end
214
215% State vectors for compatibility
216xvec_it = {Xavg(:)};
217xvec_t = Xt;
218
219line_debug('Diffusion solver completed in %g seconds', runtime);
220
221end
222
223%% Core diffusion algorithm
224function [Xavg, Xt] = solver_fluid_diffusion_core(mu_inv, P, N_vec, T, dt)
225% SOLVER_FLUID_DIFFUSION_CORE Core diffusion approximation for BCMP/Kelly networks
226%
227% Inputs:
228% mu_inv : (M x K) matrix of inverse service rates (average service time)
229% P : ((M*K) x (M*K)) routing matrix (flattened, column-major)
230% N_vec : (K x 1) vector of total jobs per class
231% T : total simulation time
232% dt : time step
233%
234% Output:
235% Xavg : (M x K) matrix of time-averaged queue lengths
236% Xt : (M x K x steps) tensor of queue lengths over time
237
238[M, K] = size(mu_inv);
239steps = floor(T/dt);
240
241% Initialize storage
242Xt = zeros(M, K, steps);
243
244% Initial state: distribute jobs evenly across stations for each class
245for r = 1:K
246 Xt(:, r, 1) = N_vec(r) / M;
247end
248
249% Initialize running sum for time-average
250Xavg = Xt(:, :, 1) / steps;
251
252% Drift function handle
253drift_fn = @(x) diffusion_drift(x, mu_inv, P, M, K);
254
255% Euler-Maruyama simulation
256for step = 2:steps
257 x_prev = Xt(:, :, step-1);
258
259 % Brownian motion increment
260 dW = sqrt(dt) * randn(M, K);
261
262 % Drift term
263 drift = drift_fn(x_prev);
264
265 % Euler-Maruyama update
266 x_new = x_prev + drift * dt + dW;
267
268 % Reflect at zero (queue lengths cannot be negative)
269 x_new = max(x_new, 0);
270
271 % Project back to fixed population per class (closed network constraint)
272 for r = 1:K
273 total_r = sum(x_new(:, r));
274 if total_r > 0
275 x_new(:, r) = x_new(:, r) * N_vec(r) / total_r;
276 else
277 % Re-distribute evenly if all became zero
278 x_new(:, r) = N_vec(r) / M;
279 end
280 end
281
282 % Store state
283 Xt(:, :, step) = x_new;
284
285 % Accumulate for time-average
286 Xavg = Xavg + x_new / steps;
287end
288
289end
290
291%% Drift function
292function d = diffusion_drift(x, mu_inv, P, M, K)
293% DIFFUSION_DRIFT Calculate net flow (drift) for each (station, class) pair
294%
295% Inputs:
296% x : M x K matrix of current queue lengths
297% mu_inv : M x K matrix of average service times
298% P : (M*K x M*K) routing matrix
299% M : Number of stations
300% K : Number of classes
301%
302% Output:
303% d : M x K matrix of drift values
304
305d = zeros(M, K);
306
307for ist = 1:M
308 for r = 1:K
309 idx_out = sub2ind([M K], ist, r);
310
311 % Outflow: departure rate from (station, class)
312 if mu_inv(ist, r) > 0 && ~isinf(mu_inv(ist, r))
313 out_flow = x(ist, r) / mu_inv(ist, r);
314 else
315 out_flow = 0;
316 end
317
318 % Inflow: sum of arrivals from all other (station, class) pairs
319 inflow = 0;
320 for j = 1:(M*K)
321 [ist_j, r_j] = ind2sub([M K], j);
322 if mu_inv(ist_j, r_j) > 0 && ~isinf(mu_inv(ist_j, r_j))
323 inflow = inflow + (x(ist_j, r_j) / mu_inv(ist_j, r_j)) * P(j, idx_out);
324 end
325 end
326
327 % Net drift
328 d(ist, r) = inflow - out_flow;
329 end
330end
331
332end
Definition mmt.m:92