1function [result, x, fval, exitflag] = qrf_rsrd(params, objective, sense)
2% QRF_RSRD - Quadratic Reduction Framework
for RS-
RD blocking networks
4% MATLAB port of
the AMPL model qrboundsrsrd_skel.mod
7% [result, x, fval, exitflag] = qrf_rsrd(params)
8% [result, x, fval, exitflag] = qrf_rsrd(params, objective)
9% [result, x, fval, exitflag] = qrf_rsrd(params, objective, sense)
12% params - Structure with model parameters:
13% .M - Number of queues
14% .N - Total population
15% .F - [M x 1] Capacity of each queue
16% .K - [M x 1] Number of phases
for each queue
17% .mu - {M x 1} cell, each mu{i}
is K(i) x K(i) completion rates
18% .v - {M x 1} cell, each v{i}
is K(i) x K(i) background rates
19% .r - [M x M] Routing probabilities
20% .alpha - (optional) {M x 1} cell, each alpha{i}
is [N x 1] load-dependent rates
22% objective - (optional)
'U1min' (
default),
'U1max', or queue index 1..M
23% sense - (optional)
'min' (
default) or
'max'
26% result - Structure with results:
27% .U - [M x 1] Utilization of each queue
28% .Ueff - [M x 1] Effective utilization
29% .pb - [M x 1] Blocking probability
30% .p2 - Decision variable tensor (marginal probabilities)
31% x - Raw solution vector
32% fval - Objective function value
33% exitflag - Solver exit flag
35 if nargin < 2 || isempty(objective)
38 if nargin < 3 || isempty(sense)
50 if isfield(params,
'verbose')
51 verbose = params.verbose;
56 % Default alpha (load-independent)
57 if isfield(params, 'alpha') && ~isempty(params.alpha)
62 alpha{i} = ones(N, 1);
66 % Compute transition rates q(i,j,k,h,n)
67 % q{i,j}
is a K(i) x K(i) x (N+1) array
71 q{i,j} = zeros(K(i), K(i), N+1);
74 for n = 1:N % n=0 gives q=0
76 q{i,j}(ki, hi, n+1) = r(i,j) * mu{i}(ki, hi) * alpha{i}(n);
78 q{i,j}(ki, hi, n+1) = (v{i}(ki, hi) + r(i,i) * mu{i}(ki, hi)) * alpha{i}(n);
86 %% Build variable indexing
87 % p2(j, nj, kj, i, ni, hi)
for j in 1:M, nj in 0:F(j), kj in 1:K(j),
88 % i in 1:M, ni in 0:F(i), hi in 1:K(i)
90 % Count variables and build index
map
91 if verbose; fprintf(
'Building variable index map...\n'); end
95 p2idx{j} = cell(F(j)+1, K(j), M);
99 p2idx{j}{nj+1, kj, i} = zeros(F(i)+1, K(i));
102 varCount = varCount + 1;
103 p2idx{j}{nj+1, kj, i}(ni+1, hi) = varCount;
112 % U and Ueff variables: U(i,k,ni) and Ueff(i,k,ni)
for ni >= 1
114 Ueffidx = cell(M, 1);
116 Uidx{i} = zeros(K(i), F(i));
117 Ueffidx{i} = zeros(K(i), F(i));
120 varCount = varCount + 1;
121 Uidx{i}(ki, ni) = varCount;
122 varCount = varCount + 1;
123 Ueffidx{i}(ki, ni) = varCount;
128 % pb(i): blocking-probability variables, AMPL `var pb{i in 1..M} >=0, <=1`
131 varCount = varCount + 1;
136 if verbose; fprintf(
'Total variables: %d (p2: %d, U/Ueff/pb: %d)\n', nVars, nP2Vars, nVars - nP2Vars); end
144 % Helper to get variable index
145 getIdx = @(j, nj, kj, i, ni, hi) p2idx{j}{nj+1, kj, i}(ni+1, hi);
146 getUIdx = @(i, ki, ni) Uidx{i}(ki, ni);
147 getUeffIdx = @(i, ki, ni) Ueffidx{i}(ki, ni);
149 if verbose; fprintf(
'Building constraints...\n'); end
151 %% ONE: Normalization - sum over nj, kj equals 1
for each j
152 if verbose; fprintf(
' ONE constraints...\n'); end
154 row = zeros(1, nVars);
157 idx = getIdx(j, nj, kj, j, nj, kj);
165 %% ZERO constraints - fix infeasible states to zero
166 if verbose; fprintf(
' ZERO constraints...\n'); end
167 lb = zeros(nVars, 1);
169 % U and Ueff bounds are [0, 1]
173 ub(getUIdx(i, ki, ni)) = 1;
174 ub(getUeffIdx(i, ki, ni)) = 1;
185 idx = getIdx(j, nj, kj, i, ni, hi);
187 % ZERO1: i==j, nj==ni, h<>k
188 if i == j && nj == ni && hi ~= kj
192 % ZERO2: i==j, nj<>ni
193 if i == j && nj ~= ni
197 % ZERO3: i<>j, nj+ni > N
198 if i ~= j && nj + ni > N
202 % ZERO6: i<>j, N-nj-ni > sum of other capacities
204 sumOtherF = sum(F) - F(i) - F(j);
205 if N - nj - ni > sumOtherF
215 % ZERO7: N-nj > sum of other capacities (
for diagonal)
218 sumOtherF = sum(F) - F(j);
219 if N - nj > sumOtherF
220 idx = getIdx(j, nj, kj, j, nj, kj);
227 %% SYMMETRY: p2(i,ni,hi,j,nj,kj) = p2(j,nj,kj,i,ni,hi)
228 if verbose; fprintf(
' SYMMETRY constraints...\n'); end
234 continue; % avoid duplicate constraints
238 idx1 = getIdx(j, nj, kj, i, ni, hi);
239 idx2 = getIdx(i, ni, hi, j, nj, kj);
241 row = zeros(1, nVars);
254 %% MARGINALS: p2(j,nj,kj,j,nj,kj) = sum over ni,hi of p2(j,nj,kj,i,ni,hi)
255 if verbose; fprintf(
' MARGINALS constraints...\n'); end
263 row = zeros(1, nVars);
264 idx_diag = getIdx(j, nj, kj, j, nj, kj);
268 idx = getIdx(j, nj, kj, i, ni, hi);
269 row(idx) = row(idx) - 1;
279 %% UCLASSIC: U(i,k,ni) = p2(i,ni,k,i,ni,k)
280 if verbose; fprintf(
' UCLASSIC constraints...\n'); end
284 row = zeros(1, nVars);
285 idx_U = getUIdx(i, ki, ni);
286 idx_p2 = getIdx(i, ni, ki, i, ni, ki);
295 %% UEFFS: Ueff(i,k,ni) = p2(i,ni,k,i,ni,k) - sum_{j: r(i,j)>0} r(i,j)*p2(i,ni,k,j,F(j),h)
296 if verbose; fprintf(
' UEFFS constraints...\n'); end
300 row = zeros(1, nVars);
301 idx_Ueff = getUeffIdx(i, ki, ni);
302 idx_p2_diag = getIdx(i, ni, ki, i, ni, ki);
304 row(idx_p2_diag) = -1;
307 if j ~= i && r(i,j) > 0
309 idx_block = getIdx(i, ni, ki, j, F(j), hj);
310 row(idx_block) = r(i,j);
320 %% PBLOCK: pb(i) = sum_{k,ni>=1} (U(i,k,ni) - Ueff(i,k,ni))
321 if verbose; fprintf(
' PBLOCK constraints...\n'); end
323 row = zeros(1, nVars);
327 row(getUIdx(i, ki, ni)) = row(getUIdx(i, ki, ni)) - 1;
328 row(getUeffIdx(i, ki, ni)) = row(getUeffIdx(i, ki, ni)) + 1;
335 %% PBB: pb(i) <= sum_{j<>i, r(i,j)>0} sum_h p2(j,F(j),h,j,F(j),h)
336 if verbose; fprintf(
' PBB constraints...\n'); end
338 row = zeros(1, nVars);
341 if j ~= i && r(i,j) > 0
343 idx = getIdx(j, F(j), hj, j, F(j), hj);
344 row(idx) = row(idx) - 1;
348 Aineq = [Aineq; row];
352 %% THM2: Phase balance (Theorem 1 - THM:sdeffective)
353 % sum_{ni} (sum_{j<>i, h<>k} q(i,j,k,h,ni)*Ueff(i,k,ni) + sum_{h<>k} q(i,i,k,h,ni)*U(i,k,ni))
354 % = sum_{ni} (sum_{j<>i, h<>k} q(i,j,h,k,ni)*Ueff(i,h,ni) + sum_{h<>k} q(i,i,h,k,ni)*U(i,h,ni))
355 if verbose; fprintf(
' THM2 (Phase balance) constraints...\n'); end
358 row = zeros(1, nVars);
361 % Terms with Ueff (j<>i)
370 coef = q{i,j}(ki, hi, ni+1);
371 idx_Ueff = getUeffIdx(i, ki, ni);
372 row(idx_Ueff) = row(idx_Ueff) + coef;
375 % Terms with U (j==i, h<>k)
380 coef = q{i,i}(ki, hi, ni+1);
381 idx_p2 = getIdx(i, ni, ki, i, ni, ki);
382 row(idx_p2) = row(idx_p2) + coef;
385 % RHS terms (subtract)
387 % Terms with Ueff (j<>i)
396 coef = q{i,j}(hi, ki, ni+1);
397 idx_Ueff = getUeffIdx(i, hi, ni);
398 row(idx_Ueff) = row(idx_Ueff) - coef;
401 % Terms with U (j==i, h<>k)
406 coef = q{i,i}(hi, ki, ni+1);
407 idx_p2 = getIdx(i, ni, hi, i, ni, hi);
408 row(idx_p2) = row(idx_p2) - coef;
416 %% THM1: Population constraint (Theorem 2)
417 % sum_i sum_ni sum_hi ni * p2(j,nj,kj,i,ni,hi) = N * p2(j,nj,kj,j,nj,kj)
418 if verbose; fprintf(
' THM1 (Population) constraints...\n'); end
419 % THM1
is AGGREGATED over nj >= 1: one row per (j,kj), matching
the
420 % authoritative AMPL model (qrboundsrsrd_skel.mod / example_rsrd.mod):
421 % THM1 {j,k}: sum_i sum_{nj>=1} sum_{ni>=1} sum_h ni*p2[j,nj,k,i,ni,h]
422 % = N * sum_{nj>=1} p2[j,nj,k,j,nj,k]
423 % Emitting this per-nj instead (one row for each nj)
is STRICTLY STRONGER --
424 %
the per-nj equalities imply
the aggregate but not conversely -- and it
425 % over-tightens
the polytope: on
the paper
's M=5,N=20 RS-RD example the
426 % per-nj form gives U1min=0.92508 against the published 0.87058.
429 row = zeros(1, nVars);
431 idx_diag = getIdx(j, nj, kj, j, nj, kj);
432 row(idx_diag) = row(idx_diag) - N;
434 for ni = 1:F(i) % ni >= 1
436 idx = getIdx(j, nj, kj, i, ni, hi);
437 row(idx) = row(idx) + ni;
447 %% THM1c: population balance for the empty-queue case nj = 0 (i ~= j)
448 % THM1c {j,k}: sum_{i<>j} sum_{ni>=1} sum_h ni*p2[j,0,k,i,ni,h]
449 % = N * p2[j,0,k,j,0,k]
450 if verbose; fprintf(' THM1c (nj=0 population) constraints...\n
'); end
453 row = zeros(1, nVars);
454 idx_diag = getIdx(j, 0, kj, j, 0, kj);
455 row(idx_diag) = row(idx_diag) - N;
460 for ni = 1:F(i) % ni >= 1
462 idx = getIdx(j, 0, kj, i, ni, hi);
463 row(idx) = row(idx) + ni;
472 %% THM3a: Marginal balance for ni in 1:F(i)-1
473 if verbose; fprintf(' THM3a (Marginal balance) constraints...\n
'); end
476 row = zeros(1, nVars);
477 % LHS: arrivals to queue i
486 idx = getIdx(j, nj, kj, i, ni, ui);
487 row(idx) = row(idx) + q{j,i}(kj, hj, nj+1);
493 % RHS: departures from queue i at ni+1
502 idx = getIdx(i, ni+1, ki, j, nj, uj);
503 row(idx) = row(idx) - q{i,j}(ki, hi, ni+2);
514 %% THM3b: Marginal balance for ni=0, per phase
515 if verbose; fprintf(' THM3b (Marginal balance ni=0) constraints...\n
'); end
518 row = zeros(1, nVars);
519 % LHS: arrivals to queue i at ni=0
527 idx = getIdx(j, nj, kj, i, 0, ui);
528 row(idx) = row(idx) + q{j,i}(kj, hj, nj+1);
533 % RHS: departures from queue i at ni=1
541 idx = getIdx(i, 1, ki, j, nj, hj);
542 row(idx) = row(idx) - q{i,j}(ki, ui, 2);
552 %% QBAL: Queue balance constraint
553 % This is a complex balance equation that tightens the bounds
554 if verbose; fprintf(' QBAL (Queue balance) constraints...\n
'); end
557 row = zeros(1, nVars);
559 % LHS Term 1: sum{h<>k} sum{j<>i} sum{ni} sum{u} sum{nj} q[i,j,k,h,ni]*ni*p2[i,ni,k,j,nj,u]
571 coef = q{i,j}(ki, hi, ni+1) * ni;
572 idx = getIdx(i, ni, ki, j, nj, uj);
573 row(idx) = row(idx) + coef;
580 % LHS Term 2: sum{h<>k} sum{ni} q[i,i,k,h,ni]*ni*p2[i,ni,k,i,ni,k]
586 coef = q{i,i}(ki, hi, ni+1) * ni;
587 idx = getIdx(i, ni, ki, i, ni, ki);
588 row(idx) = row(idx) + coef;
592 % LHS Term 3: sum{j<>i} sum{h} sum{ni} sum{u} sum{nj<=min(F(j)-1,N-ni)} q[i,j,h,k,ni]*p2[i,ni,h,j,nj,u]
600 maxNj = min(F(j)-1, N-ni);
602 coef = q{i,j}(hi, ki, ni+1);
603 idx = getIdx(i, ni, hi, j, nj, uj);
604 row(idx) = row(idx) + coef;
611 % RHS Term 1: sum{j<>i} sum{h} sum{ni<=F(i)-1} sum{u} sum{nj>=1} q[j,i,h,u,nj]*p2[i,ni,k,j,nj,h]
620 coef = q{j,i}(hj, uj, nj+1);
621 idx = getIdx(i, ni, ki, j, nj, hj);
622 row(idx) = row(idx) - coef;
629 % RHS Term 2: sum{h<>k} sum{ni} q[i,i,h,k,ni]*ni*p2[i,ni,h,i,ni,h]
635 coef = q{i,i}(hi, ki, ni+1) * ni;
636 idx = getIdx(i, ni, hi, i, ni, hi);
637 row(idx) = row(idx) - coef;
641 % RHS Term 3: sum{h<>k} sum{j<>i} sum{ni} sum{u} sum{nj} q[i,j,h,k,ni]*ni*p2[i,ni,h,j,nj,u]
653 coef = q{i,j}(hi, ki, ni+1) * ni;
654 idx = getIdx(i, ni, hi, j, nj, uj);
655 row(idx) = row(idx) - coef;
667 %% THM4: Queue-length bound inequality (Theorem 5)
668 % sum_t sum_ht sum_nj sum_nt nt*p2(j,nj,kj,t,nt,ht) >= N * sum_hi sum_nj sum_ni p2(j,nj,kj,i,ni,hi)
669 if verbose; fprintf(' THM4 (Queue-length bound) constraints...\n
'); end
673 row = zeros(1, nVars);
674 % LHS: sum_t sum_ht sum_nj sum_nt nt * p2
678 for nt = 1:F(t) % nt >= 1
679 idx = getIdx(j, nj, kj, t, nt, ht);
680 row(idx) = row(idx) + nt;
688 for ni = 1:F(i) % ni >= 1
689 idx = getIdx(j, nj, kj, i, ni, hi);
690 row(idx) = row(idx) - N;
694 Aineq = [Aineq; -row]; % >= becomes <= with negation
700 %% Build objective function
701 if verbose; fprintf('Building objective function...\n
'); end
705 if strcmp(objective, 'U1min
') || strcmp(objective, 'U1max
')
708 error('Unknown objective: %s
', objective);
711 targetQueue = objective;
714 % Utilization = sum over k, n of p2(i,n,k,i,n,k)
715 for ki = 1:K(targetQueue)
716 for ni = 1:F(targetQueue)
717 idx = getIdx(targetQueue, ni, ki, targetQueue, ni, ki);
722 if strcmp(sense, 'max
')
727 if verbose; fprintf('Solving LP with %d variables and %d equality + %d inequality constraints...\n
', ...
728 nVars, size(Aeq, 1), size(Aineq, 1)); end
730 % LP algorithm. R2025a's default
'dual-simplex-highs' is broken in some
731 % installs (errors
"Unrecognized field name optimstatus"), so an
732 % interior-point variant
is required. On
the badly-scaled QRF instances
733 %
'interior-point-legacy' is markedly more accurate than
'interior-point';
734 % on
the paper
's BAS model it cuts the error against the published GLPK
735 % optimum from 4.3e-05/8.0e-06 to 8.5e-07/9.2e-10. The residual is solver
736 % tolerance, not formulation: the minimum lands above and the maximum below
737 % the GLPK vertex, and both gaps shrink together as accuracy rises.
738 % Override via params.lpAlgorithm.
739 if isfield(params, 'lpAlgorithm
') && ~isempty(params.lpAlgorithm)
740 lpAlgorithm = params.lpAlgorithm;
742 lpAlgorithm = 'interior-point-legacy
';
745 options = optimoptions('linprog
', 'Display
', 'final', 'Algorithm
', lpAlgorithm);
747 options = optimoptions('linprog
', 'Display
', 'off
', 'Algorithm
', lpAlgorithm);
750 [x, fval, exitflag] = linprog(c, Aineq, bineq, Aeq, beq, lb, ub, options);
752 if strcmp(sense, 'max
')
758 result.objective = fval;
759 result.exitflag = exitflag;
761 % Whether the metric fields below can be filled is a property of the
762 % solution vector, not of exitflag. exitflag 0 (iteration limit) still
763 % returns a usable interior point -- that is the normal outcome on badly
764 % scaled instances such as the reference BAS network -- whereas a solve that
765 % breaks down returns an empty or non-finite x, which must never be
766 % consumed. Predicate on x accordingly, and say so rather than skipping
767 % silently. (Observed here: exitflag -4 comes back with x empty.)
768 hasSolution = ~isempty(x) && all(isfinite(x));
769 if ~isempty(x) && ~hasSolution
770 warning('qrf_rsrd:nonFiniteSolution
', ...
771 'linprog returned a non-finite solution (exitflag %d); U and
the other metric fields are left unpopulated.
', ...
776 % Compute utilizations from U and Ueff variables
777 result.U = zeros(M, 1);
778 result.Ueff = zeros(M, 1);
779 result.pb = zeros(M, 1);
782 % U(i) = sum over k, ni of U(i,k,ni)
785 idx_U = getUIdx(i, ki, ni);
786 idx_Ueff = getUeffIdx(i, ki, ni);
787 result.U(i) = result.U(i) + x(idx_U);
788 result.Ueff(i) = result.Ueff(i) + x(idx_Ueff);
791 result.pb(i) = result.U(i) - result.Ueff(i);
794 % Store p2 as a function handle for easy access
795 result.getP2 = @(j, nj, kj, i, ni, hi) x(getIdx(j, nj, kj, i, ni, hi));
798 if verbose; fprintf('\n=== Results ===\n
'); end
799 if verbose; fprintf('Objective value: %f\n
', fval); end
800 if verbose; fprintf('Exit flag: %d\n
', exitflag); end
801 if hasSolution && verbose
802 fprintf('\nUtilizations:\n
');
804 fprintf(' Queue %d: U = %.6f, Ueff = %.6f, pb = %.6f\n
', ...
805 i, result.U(i), result.Ueff(i), result.pb(i));