1function [result, x, fval, exitflag] = mapqn_bnd_qr(params, objective_queue, objective_phase, sense)
2% MAPQN_BND_QR - General Quadratic Reduction Bounds
for MAP Queueing Networks
4% MATLAB port of
the Python file bnd_qr.py
7% [result, x, fval, exitflag] = mapqn_bnd_qr(params)
8% [result, x, fval, exitflag] = mapqn_bnd_qr(params, objective_queue)
9% [result, x, fval, exitflag] = mapqn_bnd_qr(params, objective_queue, objective_phase)
10% [result, x, fval, exitflag] = mapqn_bnd_qr(params, objective_queue, objective_phase, sense)
13% params - Structure with model parameters:
14% .M - Number of queues
15% .N - Total population
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% .verbose - (optional)
boolean,
default true
22% objective_queue - (optional) Queue index to optimize (1-based),
default 1
23% objective_phase - (optional) Phase index to optimize (1-based),
default 1
24% sense - (optional)
'min' or
'max',
default 'max'
27% result - Structure with results:
28% .objective - Objective function value
29% .exitflag - Solver exit flag
30% .U - [M x max(K)] Utilization matrix
31% .IT - [M x max(K)] Idle time matrix
32% .Q - [M x max(K)] Queue length matrix
33% .getP2 - Function handle to extract p2 values
34% x - Raw solution vector
35% fval - Objective function value
36% exitflag - Solver exit flag
38 if nargin < 2 || isempty(objective_queue)
41 if nargin < 3 || isempty(objective_phase)
44 if nargin < 4 || isempty(sense)
55 if isfield(params,
'verbose')
56 verbose = params.verbose;
63 % Compute transition rates q{i,j}(k,h)
67 q{i,j} = zeros(K(i), K(i));
71 q{i,j}(ki, hi) = r(i,j) * mu{i}(ki, hi);
73 q{i,j}(ki, hi) = v{i}(ki, hi) + r(i,i) * mu{i}(ki, hi);
80 %% Build variable indexing
81 if verbose; fprintf(
'Building variable index map...\n'); end
84 % U(i,k) variables: utilization at queue i, phase k
85 Uidx = zeros(M, maxK);
88 varCount = varCount + 1;
89 Uidx(i, k) = varCount;
93 % IT(i,k) variables: idle time at queue i, phase k
94 ITidx = zeros(M, maxK);
97 varCount = varCount + 1;
98 ITidx(i, k) = varCount;
102 % UP(j,k,i,h) variables: utilization products
103 UPidx = zeros(M, maxK, M, maxK);
108 varCount = varCount + 1;
109 UPidx(j, kj, i, hi) = varCount;
115 % QP(j,k,i,h) variables: queue-length products
116 QPidx = zeros(M, maxK, M, maxK);
121 varCount = varCount + 1;
122 QPidx(j, kj, i, hi) = varCount;
128 % Q(i,k) variables: mean queue length
129 Qidx = zeros(M, maxK);
132 varCount = varCount + 1;
133 Qidx(i, k) = varCount;
137 % C(j,k,i) variables: conditional queue lengths
138 Cidx = zeros(M, maxK, M);
142 varCount = varCount + 1;
143 Cidx(j, kj, i) = varCount;
148 % I_var(j,k,i) variables: conditional idle lengths
149 Iidx = zeros(M, maxK, M);
153 varCount = varCount + 1;
154 Iidx(j, kj, i) = varCount;
159 % p1(j,k,i,ni,h) variables: marginal probabilities, ni from 0 to N
160 % Stored as p1idx(j, k, i, ni+1, h)
161 p1idx = zeros(M, maxK, M, N+1, maxK);
167 varCount = varCount + 1;
168 p1idx(j, kj, i, ni+1, hi) = varCount;
175 % p1c(j,k,i,ni,h) variables: complementary marginal probabilities, ni from 0 to N
176 % Stored as p1cidx(j, k, i, ni+1, h)
177 p1cidx = zeros(M, maxK, M, N+1, maxK);
183 varCount = varCount + 1;
184 p1cidx(j, kj, i, ni+1, hi) = varCount;
191 % p2(j,nj,k,i,ni,h) variables: joint probabilities, nj from 0 to N, ni from 0 to N
192 % Stored as p2idx(j, nj+1, k, i, ni+1, h)
193 p2idx = zeros(M, N+1, maxK, M, N+1, maxK);
200 varCount = varCount + 1;
201 p2idx(j, nj+1, kj, i, ni+1, hi) = varCount;
210 if verbose; fprintf(
'Total variables: %d\n', nVars); end
213 lb = zeros(nVars, 1);
216 % Set upper bounds
for each variable type
228 ub(UPidx(j, kj, i, hi)) = 1;
229 ub(QPidx(j, kj, i, hi)) = N;
237 ub(Cidx(j, kj, i)) = N;
238 ub(Iidx(j, kj, i)) = N;
247 ub(p1idx(j, kj, i, ni+1, hi)) = 1;
248 ub(p1cidx(j, kj, i, ni+1, hi)) = 1;
260 ub(p2idx(j, nj+1, kj, i, ni+1, hi)) = 1;
274 if verbose; fprintf(
'Building constraints...\n'); end
276 %% ZER1: p1(j,k,j,0,k) = 0
for all j,k (via upper bounds)
277 if verbose; fprintf(
' ZER1 constraints...\n'); end
280 idx = p1idx(j, k, j, 0+1, k);
285 %% ZER2: p1(j,k,j,nj,h) = 0
for h ~= k, all j,k,nj (via upper bounds)
286 if verbose; fprintf(
' ZER2 constraints...\n'); end
292 idx = p1idx(j, k, j, nj+1, h);
300 %% ZER3: p1(j,k,i,N,h) = 0
for j ~= i, all j,k,i,h (via upper bounds)
301 if verbose; fprintf(
' ZER3 constraints...\n'); end
307 idx = p1idx(j, k, i, N+1, h);
315 %% ZER4: p1c(j,k,j,nj,h) = 0
for nj >= 1, all j,k,nj,h (via upper bounds)
316 if verbose; fprintf(
' ZER4 constraints...\n'); end
321 idx = p1cidx(j, k, j, nj+1, h);
328 %% ZER5: p2(j,nj,k,j,nj,h) = 0
for h ~= k (AMPL ZERO1) (via upper bounds)
329 % The joint variables carry
the same structural zeros as their p1
330 % projections: a station cannot be in two phases at once, cannot hold two
331 % different populations at once, and two stations cannot jointly hold more
332 % than
the closed population.
333 if verbose; fprintf(
' ZER5 (p2 phase consistency) constraints...\n'); end
339 ub(p2idx(j, nj+1, k, j, nj+1, h)) = 0;
346 %% ZER6: p2(j,nj,k,j,ni,h) = 0
for ni ~= nj (AMPL ZERO2) (via upper bounds)
347 if verbose; fprintf(
' ZER6 (p2 population consistency) constraints...\n'); end
354 ub(p2idx(j, nj+1, k, j, ni+1, h)) = 0;
362 %% ZER7: p2(j,nj,k,i,ni,h) = 0
for i ~= j and nj+ni > N (AMPL ZERO3)
363 if verbose; fprintf(
' ZER7 (p2 population cap) constraints...\n'); end
372 ub(p2idx(j, nj+1, k, i, ni+1, h)) = 0;
382 %% CEQU: C(j,k,j) = Q(j,k)
for all j,k
383 if verbose; fprintf(
' CEQU constraints...\n'); end
386 row = zeros(1, nVars);
387 row(Cidx(j, k, j)) = 1;
388 row(Qidx(j, k)) = -1;
394 %% ONE1: sum over kj,hi,ni of (p1 + p1c) = 1
for each (j,i)
395 if verbose; fprintf(
' ONE1 constraints...\n'); end
398 row = zeros(1, nVars);
402 row(p1idx(j, kj, i, ni+1, hi)) = 1;
403 row(p1cidx(j, kj, i, ni+1, hi)) = 1;
412 %% UTLB: U(i,k) = sum over t,nt,h of p1(i,k,t,nt,h)
for each (i,k,t)
413 if verbose; fprintf(
' UTLB constraints...\n'); end
417 row = zeros(1, nVars);
421 row(p1idx(i, k, t, nt+1, h)) = -1;
430 %% UTLC: IT(i,k) = sum over t,nt,h of p1c(i,k,t,nt,h)
for each (i,k,t)
431 if verbose; fprintf(
' UTLC constraints...\n'); end
435 row = zeros(1, nVars);
436 row(ITidx(i, k)) = 1;
439 row(p1cidx(i, k, t, nt+1, h)) = -1;
448 %% QLEN: Q(i,k) = sum over ni of ni*p1(i,k,i,ni,k)
for each (i,k)
449 if verbose; fprintf(
' QLEN constraints...\n'); end
452 row = zeros(1, nVars);
455 row(p1idx(i, k, i, ni+1, k)) = row(p1idx(i, k, i, ni+1, k)) - ni;
462 %% SRVB: Service balance
463 % sum{j,h} q{i,j}(k,h)*U(i,k) = sum{j,h} q{i,j}(h,k)*U(i,h)
for each (i,k)
464 % Stations with a single phase give an identically zero row, so skip them.
465 if verbose; fprintf(
' SRVB constraints...\n'); end
471 row = zeros(1, nVars);
474 row(Uidx(i, k)) = row(Uidx(i, k)) + q{i,j}(k, h);
475 row(Uidx(i, h)) = row(Uidx(i, h)) - q{i,j}(h, k);
483 %% POPC: sum over i,k of Q(i,k) = N
484 if verbose; fprintf(
' POPC constraint...\n'); end
485 row = zeros(1, nVars);
494 %% ONE: sum over k of (U(j,k) + IT(j,k)) = 1
for each j
495 if verbose; fprintf(
' ONE constraints...\n'); end
497 row = zeros(1, nVars);
500 row(ITidx(j, k)) = 1;
506 %% PCL2: sum over i,j,ni>=1,nj>=1,h,k of ni*nj*p2(i,ni,h,j,nj,k) = N^2
507 if verbose; fprintf(
' PCL2 constraint...\n'); end
508 row = zeros(1, nVars);
515 idx = p2idx(i, ni+1, h, j, nj+1, k);
516 row(idx) = row(idx) + ni * nj;
526 %% PI21: p1(j,k,i,ni,h) = sum over nj=1..N of p2(j,nj,k,i,ni,h)
527 if verbose; fprintf(
' PI21 constraints...\n'); end
533 row = zeros(1, nVars);
534 row(p1idx(j, k, i, ni+1, h)) = 1;
536 idx = p2idx(j, nj+1, k, i, ni+1, h);
537 row(idx) = row(idx) - 1;
547 %% PI22: p1c(j,k,i,ni,h) = p2(j,0,k,i,ni,h)
548 if verbose; fprintf(
' PI22 constraints...\n'); end
554 row = zeros(1, nVars);
555 row(p1cidx(j, k, i, ni+1, h)) = 1;
556 row(p2idx(j, 0+1, k, i, ni+1, h)) = -1;
565 %% PI23: p2(i,ni,h,j,nj,k) = p2(j,nj,k,i,ni,h) (symmetry)
566 % Only generate
for j < i, or (j == i and nj < ni), to avoid redundancy
567 if verbose; fprintf(
' PI23 (symmetry) constraints...\n'); end
574 % Only generate constraint
if (j,nj,k) < (i,ni,h) in lex order
575 if j < i || (j == i && nj < ni) || (j == i && nj == ni && k < h)
576 idx1 = p2idx(i, ni+1, h, j, nj+1, k);
577 idx2 = p2idx(j, nj+1, k, i, ni+1, h);
579 row = zeros(1, nVars);
593 %% CLEN: C(j,k,i) = sum over ni,h of ni*p1(j,k,i,ni,h)
for each (j,k,i)
594 % Without
this the C variables are defined only on
the diagonal by CEQU,
595 % which leaves CUB1/CUB2 vacuous
for i ~= j.
596 if verbose; fprintf(
' CLEN constraints...\n'); end
600 row = zeros(1, nVars);
601 row(Cidx(j, k, i)) = 1;
604 idx = p1idx(j, k, i, ni+1, h);
605 row(idx) = row(idx) - ni;
614 %% MARG: p2(j,nj,k,j,nj,k) = sum over ni<=N-nj,h of p2(j,nj,k,i,ni,h)
615 % AMPL MARGINALS. ONE1 only imposes
the aggregate over nj, so
the
616 % per-population marginal consistency
is a separate family.
617 if verbose; fprintf(
' MARG constraints...\n'); end
623 row = zeros(1, nVars);
624 row(p2idx(j, nj+1, k, j, nj+1, k)) = 1;
627 idx = p2idx(j, nj+1, k, i, ni+1, h);
628 row(idx) = row(idx) - 1;
639 %% THM2: sum over i,ni>=1,h of ni*p2(j,nj,k,i,ni,h) = N*p2(j,nj,k,j,nj,k)
640 % AMPL THM2,
the queue-length theorem conditioned on (j,nj,k). Summing it
641 % over nj >= 1 recovers
the aggregate form sum_i C(j,k,i) = N*U(j,k).
642 if verbose; fprintf(
' THM2 constraints...\n'); end
646 row = zeros(1, nVars);
650 idx = p2idx(j, nj+1, k, i, ni+1, h);
651 row(idx) = row(idx) + ni;
655 idx = p2idx(j, nj+1, k, j, nj+1, k);
656 row(idx) = row(idx) - N;
663 %% THM30: level-crossing balance at an empty station, per arrival phase
664 % AMPL THM30. Rate into {n_i = 0, phase_i = u} from a busy neighbour equals
665 %
the rate out of {n_i = 1} through a completion at i.
666 if verbose; fprintf(
' THM30 constraints...\n'); end
669 row = zeros(1, nVars);
675 idx = p2idx(j, nj+1, k, i, 0+1, u);
676 row(idx) = row(idx) + q{j,i}(k, h);
683 idx = p2idx(j, nj+1, h, i, 1+1, k);
684 row(idx) = row(idx) - q{i,j}(k, u);
695 %% THM3: level-crossing balance between n_i and n_i+1
696 % AMPL THM3. This
is the family that ties station i
's arrival rate to its
697 % departure rate; without it nothing prevents a station from being idle with
698 % probability one, which is why the minimum utilization collapsed to zero.
699 if verbose; fprintf(' THM3 constraints...\n
'); end
702 row = zeros(1, nVars);
709 idx = p2idx(j, nj+1, k, i, ni+1, u);
710 row(idx) = row(idx) + q{j,i}(k, h);
719 idx = p2idx(j, nj+1, u, i, ni+1+1, k);
720 row(idx) = row(idx) - q{i,j}(k, h);
732 %% UUB1: sum over k of U(i,k) <= 1 for each i (inequality)
733 if verbose; fprintf(' UUB1 constraints...\n
'); end
735 row = zeros(1, nVars);
739 Aineq = [Aineq; row];
743 %% QUB1: Q(j,k) <= N*U(j,k) for each (j,k) (inequality)
744 if verbose; fprintf(' QUB1 constraints...\n
'); end
747 row = zeros(1, nVars);
749 row(Uidx(j, k)) = -N;
750 Aineq = [Aineq; row];
755 %% CUB1: C(j,k,i) <= sum over h of Q(i,h) for each (j,k,i) (inequality)
756 if verbose; fprintf(' CUB1 constraints...\n
'); end
760 row = zeros(1, nVars);
761 row(Cidx(j, k, i)) = 1;
763 row(Qidx(i, h)) = -1;
765 Aineq = [Aineq; row];
771 %% CUB2: C(j,k,i) <= N*U(j,k) for each (j,k,i) (inequality)
772 if verbose; fprintf(' CUB2 constraints...\n
'); end
776 row = zeros(1, nVars);
777 row(Cidx(j, k, i)) = 1;
778 row(Uidx(j, k)) = -N;
779 Aineq = [Aineq; row];
785 %% THM4: N*P(j busy in k, i nonempty) <= sum_t C(j,k,t) (inequality)
787 if verbose; fprintf(' THM4 constraints...\n
'); end
791 row = zeros(1, nVars);
795 idx = p2idx(j, nj+1, k, i, ni+1, h);
796 row(idx) = row(idx) + N;
804 idx = p2idx(j, nj+1, k, t, nt+1, h);
805 row(idx) = row(idx) - nt;
810 Aineq = [Aineq; row];
816 %% Build objective function
817 if verbose; fprintf('Building objective function...\n
'); end
819 c(Uidx(objective_queue, objective_phase)) = 1;
821 if strcmp(sense, 'max
')
827 fprintf('Solving LP with %d variables and %d equality + %d inequality constraints...\n
', ...
828 nVars, size(Aeq, 1), size(Aineq, 1));
831 % LP algorithm. R2025a's
default 'dual-simplex-highs' is broken in some
832 % installs (errors
"Unrecognized field name optimstatus"), so an
833 % interior-point variant
is required.
'interior-point-legacy' is more
834 % accurate on loosely constrained instances, but once
the balance families
835 % are present it declares
the (feasible) system infeasible with exitflag -2
836 % and returns
the bound box -- e.g. U1 in [0,1] instead of
the exact
837 % [0.75,0.75] on
the K=1 symmetric tandem.
'interior-point' solves those to
838 % ~1e-6 and agrees with
the GLPK optimum of
the reference AMPL model, so it
839 %
is the default. Override via params.lpAlgorithm.
840 if isfield(params,
'lpAlgorithm') && ~isempty(params.lpAlgorithm)
841 lpAlgorithm = params.lpAlgorithm;
843 lpAlgorithm = 'interior-point';
846 options = optimoptions('linprog', 'Display', 'final', 'Algorithm', lpAlgorithm);
848 options = optimoptions('linprog', 'Display', 'off', 'Algorithm', lpAlgorithm);
851 [x, fval, exitflag] = linprog(c, Aineq, bineq, Aeq, beq, lb, ub, options);
853 if strcmp(sense, 'max')
859 result.objective = fval;
860 result.exitflag = exitflag;
862 % Whether
the metric fields below can be filled
is a property of
the
863 % solution vector, not of exitflag. exitflag 0 (iteration limit) still
864 % returns a usable interior point -- that
is the normal outcome on badly
865 % scaled instances such as
the reference BAS network -- whereas a solve that
866 % breaks down returns an empty or non-finite x, which must never be
867 % consumed. Predicate on x accordingly, and say so rather than skipping
868 % silently. (Observed here: exitflag -4 comes back with x empty.)
869 hasSolution = ~isempty(x) && all(isfinite(x));
870 if ~isempty(x) && ~hasSolution
871 warning('mapqn_bnd_qr:nonFiniteSolution', ...
872 'linprog returned a non-finite solution (exitflag %d); U and
the other metric fields are left unpopulated.', ...
877 % Compute utilizations, idle times, queue lengths
878 result.U = zeros(M, maxK);
879 result.IT = zeros(M, maxK);
880 result.Q = zeros(M, maxK);
884 result.U(i, k) = x(Uidx(i, k));
885 result.IT(i, k) = x(ITidx(i, k));
886 result.Q(i, k) = x(Qidx(i, k));
890 % Function handle to extract p2 values: p2(j,nj,k,i,ni,h)
891 result.getP2 = @(j, nj, k, i, ni, h) x(p2idx(j, nj+1, k, i, ni+1, h));
894 if verbose; fprintf('\n=== Results ===\n'); end
895 if verbose; fprintf('Objective value: %f\n', fval); end
896 if verbose; fprintf('Exit flag: %d\n', exitflag); end
897 if hasSolution && verbose
898 fprintf('\nUtilizations:\n');
900 fprintf(' Queue %d: U = [', i);
902 fprintf('%.6f ', result.U(i, k));
906 fprintf('\nIdle times:\n');
908 fprintf(' Queue %d: IT = [', i);
910 fprintf('%.6f ', result.IT(i, k));
914 fprintf('\nQueue lengths:\n');
916 fprintf(' Queue %d: Q = [', i);
918 fprintf('%.6f ', result.Q(i, k));