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
4% [QN,UN,RN,TN,XVEC_IT,QNT,UNT,TNT,XVEC_T,T,ITERS,RUNTIME] = SOLVER_FLUID_DIFFUSION(SN, OPTIONS)
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.
11% - Closed multiclass queueing networks only
12% - Scheduling: PS, FCFS, INF (delay stations)
13% - Single-server or infinite-server stations
16% - iter_max: Number of simulation steps (
default: 10000)
17% - timestep: Time step for Euler-Maruyama integration (default: 0.01)
19% Copyright (c) 2012-2026, Imperial College London
24M = sn.nstations; % number of stations
25K = sn.nclasses; % number of
classes
26N_vec = sn.njobs
'; % population per class (column vector)
28%% Model validation - diffusion only supports closed networks
30 line_error(mfilename, 'Diffusion method only supports closed queueing networks (no open
classes).
');
33% Check for sources or sinks (not allowed in closed networks for diffusion)
35 if sn.sched(ist) == SchedStrategy.EXT
36 line_error(mfilename, 'Diffusion method does not support Source
nodes. Use closed networks only.
');
40% Check scheduling disciplines - only PS, FCFS, INF supported
43 case {SchedStrategy.PS, SchedStrategy.FCFS, SchedStrategy.INF, SchedStrategy.SIRO}
46 line_error(mfilename, sprintf('Diffusion method does not support scheduling strategy %s at station %d.
', ...
47 SchedStrategy.toText(sn.sched(ist)), ist));
51% Check for single-server or infinite-server only
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)));
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;
64 steps = 10000; % default number of steps
68if isfield(options, 'timestep') && ~isempty(options.timestep)
69 dt = options.timestep;
71 dt = 0.01; % default time step
74% Total simulation time
is derived from steps and dt
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
82 if sn.rates(ist, r) > 0 && ~isnan(sn.rates(ist, r))
83 mu_inv(ist, r) = 1 / sn.rates(ist, r);
85 % If no service for this class at this station, set to Inf
86 % (effectively no departures)
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
98% Extract station-to-station routing from the full routing table
99P_full = sn.rt; % Full routing matrix (stateful
nodes, class-blocked)
101% Build station indices mapping
104 isf = sn.stationToStateful(ist);
106 station_indices = [station_indices, (isf-1)*K + r];
110% Extract the station-level routing matrix
111P_stations = P_full(station_indices, station_indices);
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
118 idx_from = sub2ind([M K], ist_from, r_from);
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);
131%% Run diffusion approximation
132line_debug('Diffusion solver starting: steps=%d, dt=%g, nstations=%d, nclasses=%d', steps, dt, M, K);
134[Xavg, Xt] = solver_fluid_diffusion_core(mu_inv,
P, N_vec, T, dt);
136runtime = toc(runtime_start);
137iters = 1; % Diffusion runs a single trajectory
141t = (0:steps-1)' * dt;
143% Queue lengths (steady-state average)
146% Transient queue lengths
150 QNt{ist, r} = squeeze(Xt(ist, r, :));
154% Throughputs: TN = QN * service_rate (
for PS/INF)
using Little
's law approximation
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);
164 % For single server, throughput limited by server capacity
165 TN(ist, r) = min(QN(ist, r), 1) / mu_inv(ist, r);
171% Transient throughputs
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);
179 TNt{ist, r} = min(QNt{ist, r}, 1) / mu_inv(ist, r);
182 TNt{ist, r} = zeros(steps, 1);
191 S = sn.nservers(ist);
194 % Infinite server: utilization = queue length
195 UN(ist, r) = QN(ist, r);
196 UNt{ist, r} = QNt{ist, r};
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);
205% Response times using Little's law: RN = QN / TN
210 RN(ist, r) = QN(ist, r) / TN(ist, r);
215% State vectors
for compatibility
219line_debug(
'Diffusion solver completed in %g seconds', runtime);
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
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
235% Xavg : (M x K) matrix of time-averaged queue lengths
236% Xt : (M x K x steps) tensor of queue lengths over time
238[M, K] = size(mu_inv);
242Xt = zeros(M, K, steps);
244% Initial state: distribute jobs evenly across stations
for each class
246 Xt(:, r, 1) = N_vec(r) / M;
249% Initialize running sum
for time-average
250Xavg = Xt(:, :, 1) / steps;
252% Drift function handle
253drift_fn = @(x) diffusion_drift(x, mu_inv,
P, M, K);
255% Euler-Maruyama simulation
257 x_prev = Xt(:, :, step-1);
259 % Brownian motion increment
260 dW = sqrt(dt) * randn(M, K);
263 drift = drift_fn(x_prev);
265 % Euler-Maruyama update
266 x_new = x_prev + drift * dt + dW;
268 % Reflect at zero (queue lengths cannot be negative)
269 x_new = max(x_new, 0);
271 % Project back to fixed population per
class (closed network constraint)
273 total_r = sum(x_new(:, r));
275 x_new(:, r) = x_new(:, r) * N_vec(r) / total_r;
277 % Re-distribute evenly
if all became zero
278 x_new(:, r) = N_vec(r) / M;
283 Xt(:, :, step) = x_new;
285 % Accumulate
for time-average
286 Xavg = Xavg + x_new / steps;
292function d = diffusion_drift(x, mu_inv,
P, M, K)
293% DIFFUSION_DRIFT Calculate net flow (drift)
for each (station,
class) pair
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
303% d : M x K matrix of drift values
309 idx_out = sub2ind([M K], ist, r);
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);
318 % Inflow: sum of arrivals from all other (station, class) pairs
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);
328 d(ist, r) = inflow - out_flow;