1function pas_cyclic_ctmc_vs_bruteforce()
2% PAS_CYCLIC_CTMC_VS_BRUTEFORCE Validate LINE
's CTMC solver on a closed
3% cyclic network of two pass-and-swap (PAS) queues against the exact
4% product-form (order-independent) brute-force enumeration.
6% Topology: --> Queue1 --> Queue2 --> (cyclic, R closed classes)
8% PAS queues are order-independent, so the network is product-form. The
9% brute force enumerates the whole ordered state space to obtain the
10% normalizing constant G, hence exact per-class throughputs and mean
11% queue lengths, which are compared against SolverCTMC.
13if exist('Network
','class') ~= 8
17% ---- shared model parameters ------------------------------------------
18% Both stations must be valid order-independent (OI) queues: the TOTAL
19% service rate mu_i(c) depends only on the customer multiset.
20% station 1: M/M/k OI queue, class-independent -> mu1(c) = min(n,k1)*s1
21% station 2: infinite-server, class-dependent -> mu2(c) = sum_j beta2(c_j)
22K = [2 2]; % per-class closed populations
23s1 = 1.0; k1 = 2; % station 1: 2-server OI (M/M/2)
24beta2 = [1.5 1.0]; % station 2: per-class infinite-server rates
27mu1 = @(c) min(numel(c(c>0)), k1) * s1;
28mu2 = @(c) sum(beta2(c(c>0)));
30% ---- (1) brute-force product-form reference ---------------------------
31[Gbf, Xbf, Qbf] = bruteforce(K, mu1, mu2);
33fprintf('Brute-force normalizing constant G = %.12g\n\n
', Gbf);
35% ---- (2) LINE closed PAS network solved by CTMC -----------------------
36model = Network('PAScyclic
');
37q1 = Queue(model, 'PASQueue1
', SchedStrategy.PAS);
38q2 = Queue(model, 'PASQueue2
', SchedStrategy.PAS);
42 jobclass{r} = ClosedClass(model, sprintf('Class%d
', r), K(r), q1);
45% Same total service-rate functions as the brute force; empty swap graph
46% (plain order-independent queue -- result is swap-graph invariant).
49q1.setSwapGraph(zeros(R)); q1.setNumberOfServers(k1); q1.setCap(sum(K));
50q2.setSwapGraph(zeros(R)); q2.setNumberOfServers(sum(K)); q2.setCap(sum(K));
52% Cyclic routing q1 -> q2 -> q1 for every class.
53P = model.initRoutingMatrix;
55 P{jobclass{r}}(q1, q2) = 1.0;
56 P{jobclass{r}}(q2, q1) = 1.0;
60T = CTMC(model, 'cutoff
', sum(K)).getAvgTable;
63% ---- (3) compare ------------------------------------------------------
64% Cyclic network with one visit per station => per-station per-class
65% throughput equals the chain throughput X_r; mean queue length per
66% (station,class) is Q_{i,r}.
67Xctmc = zeros(1,R); Qctmc = zeros(2,R);
68stationName = {'PASQueue1
','PASQueue2
'};
70 cname = sprintf('Class%d
', r);
72 row = strcmp(string(T.Station), stationName{i}) & ...
73 strcmp(string(T.JobClass), cname);
74 Qctmc(i,r) = T.QLen(row);
76 Xctmc(r) = T.Tput(row);
81fprintf('\n--- Throughput (per
class) ---\n
');
82fprintf('%-8s %14s %14s %10s\n
', 'class', 'brute-force
', 'CTMC
', 'abs.err
');
84 fprintf('Class%-3d %14.10f %14.10f %10.2e\n
', ...
85 r, Xbf(r), Xctmc(r), abs(Xbf(r)-Xctmc(r)));
88fprintf('\n--- Mean queue length (station,
class) ---\n
');
89fprintf('%-10s %-8s %14s %14s %10s\n
', 'station
','class','brute-force
','CTMC
','abs.err
');
92 fprintf('%-10s Class%-3d %14.10f %14.10f %10.2e\n
', ...
93 stationName{i}, r, Qbf(i,r), Qctmc(i,r), abs(Qbf(i,r)-Qctmc(i,r)));
98errX = max(abs(Xbf - Xctmc));
99errQ = max(abs(Qbf(:) - Qctmc(:)));
100fprintf('\nCTMC: max|dX| = %.3e max|dQ| = %.3e\n
', errX, errQ);
101assert(errX <= tol && errQ <= tol, ...
102 'CTMC vs brute-force mismatch: max|dX|=%.3e max|dQ|=%.3e
', errX, errQ);
103fprintf('PASS: CTMC matches brute-force product form within %.1e.\n
', tol);
105% ---- (4) LDES discrete-event simulation (matches within sim noise) ----
106% The same closed PAS network solved by the LINE Discrete Event Simulator.
107% LDES is a stochastic simulator, so agreement is expected only to within
108% simulation noise (Monte-Carlo confidence interval), not to machine eps.
109Tl = LDES(model, 'samples
', 2e5, 'seed
', 23000, 'verbose
', false).getAvgTable;
110Xldes = zeros(1,R); Qldes = zeros(2,R);
112 cname = sprintf('Class%d
', r);
114 row = strcmp(string(Tl.Station), stationName{i}) & ...
115 strcmp(string(Tl.JobClass), cname);
116 Qldes(i,r) = Tl.QLen(row);
118 Xldes(r) = Tl.Tput(row);
123fprintf('\n--- LDES vs brute-force (simulation, 2e5 samples) ---\n
');
124fprintf('%-12s %-8s %14s %14s %10s\n
','metric
','class','brute-force
','LDES
','rel.err
');
126 fprintf('Tput Class%-3d %14.6f %14.6f %9.2f%%\n
', ...
127 r, Xbf(r), Xldes(r), 100*abs(Xbf(r)-Xldes(r))/Xbf(r));
131 fprintf('QLen %-7s Class%-3d %14.6f %14.6f %9.2f%%\n
', ...
132 stationName{i}, r, Qbf(i,r), Qldes(i,r), 100*abs(Qbf(i,r)-Qldes(i,r))/Qbf(i,r));
136tolSim = 0.02; % 2% relative tolerance (simulation)
137relX = max(abs(Xbf - Xldes) ./ Xbf);
138relQ = max(abs(Qbf(:) - Qldes(:)) ./ Qbf(:));
139fprintf('\nLDES: max rel|dX| = %.2f%% max rel|dQ| = %.2f%%\n
', 100*relX, 100*relQ);
140assert(relX <= tolSim && relQ <= tolSim, ...
141 'LDES vs brute-force exceeds %.0f%%: relX=%.2f%% relQ=%.2f%%
', ...
142 100*tolSim, 100*relX, 100*relQ);
143fprintf('PASS: LDES matches brute-force within %.0f%% (simulation noise).\n
', 100*tolSim);
146% =======================================================================
147function [G, X, Q] = bruteforce(K, mu1, mu2)
148% Exact product-form normalizing constant G, per-class throughputs X
149% (X_r = G(K-1_r)/G(K)), and mean queue lengths Q(i,r) by full ordered
150% state-space enumeration of the two order-independent stations.
152e1 = ones(1,R); e2 = ones(1,R);
153G = norm_const(K, e1, e2, mu1, mu2);
157 Km = K; Km(r) = Km(r) - 1;
158 X(r) = norm_const(Km, e1, e2, mu1, mu2) / G; % e_r = 1
161% Q(1,r) = (1/G) sum_m m_r S1(m) S2(K-m); Q(2,r) = K_r - Q(1,r).
163splits = enum_splits(K);
164for s = 1:size(splits,1)
166 S1 = oi_sum(m, e1, mu1);
167 S2 = oi_sum(K - m, e2, mu2);
168 Q(1,:) = Q(1,:) + m .* (S1 * S2);
174function G = norm_const(K, e1, e2, mu1, mu2)
176splits = enum_splits(K);
177for s = 1:size(splits,1)
179 G = G + oi_sum(m, e1, mu1) * oi_sum(K - m, e2, mu2);
183function S = oi_sum(counts, e, mu)
184S = oi_dfs(counts, [], 1.0, e, mu);
187function S = oi_dfs(counts, prefix, w, e, mu)
192for r = 1:numel(counts)
194 c2 = counts; c2(r) = c2(r) - 1;
196 S = S + oi_dfs(c2, p2, w * e(r) / mu(p2), e, mu);
201function m = pas_rate(c, beta, k)
202% Total OI service rate of the ordered prefix c (zero-padded, 1-based
203% class ids) for a k-server PAS station.
209served = cc(1:min(n,k));
210m = sum(beta(served));
213function splits = enum_splits(K)
216splits = zeros(nrows, R);
222 idx(r) = idx(r) + 1; break;