1function [result, x, fval, exitflag] = mapqn_bnd_lr_pf(params, objective_queue, sense)
2% MAPQN_BND_LR_PF - Product-Form Linear Reduction Bounds
4% MATLAB port of bnd_lr_pf.py
6% Computes lower/upper bounds on utilization
for closed queueing networks
7%
using a linear programming formulation that exploits product-form
8% structure (no phases, scalar service rates per queue).
11% [result, x, fval, exitflag] = mapqn_bnd_lr_pf(params)
12% [result, x, fval, exitflag] = mapqn_bnd_lr_pf(params, objective_queue)
13% [result, x, fval, exitflag] = mapqn_bnd_lr_pf(params, objective_queue, sense)
16% params - Structure with model parameters:
17% .M - Number of queues
18% .N - Total population
19% .mu - [M x 1] Service rates (scalar per queue)
20% .r - [M x M] Routing probability matrix
21% .verbose - (optional)
boolean,
default true
23% objective_queue - (optional) Queue index
for objective (1-based),
default 1
24% sense - (optional)
'min' (
default) or
'max'
27% result - Structure with results:
28% .objective - Objective function value
29% .exitflag - Solver exit flag
30% .U - [M x 1] Utilizations
31% .Q - [M x 1] Mean queue lengths
32% x - Raw solution vector
33% fval - Objective function value
34% exitflag - Solver exit flag
36 if nargin < 2 || isempty(objective_queue)
39 if nargin < 3 || isempty(sense)
48 if isfield(params,
'verbose')
49 verbose = params.verbose;
54 % Compute transition rates q(i,j) = r(i,j) * mu(i)
58 q(i,j) = r(i,j) * mu(i);
62 %% Build variable indexing
63 % Variables (all 1-based):
66 % C(j,i) for j in 1:M, i in 1:M
67 % p1(j,i,ni) for j in 1:M, i in 1:M, ni in 0:N
68 % p1c(j,i,ni) for j in 1:M, i in 1:M, ni in 0:N
70 if verbose; fprintf('Building variable index
map...\n'); end
73 % U variables: utilization
76 varCount = varCount + 1;
80 % Q variables: mean queue length
83 varCount = varCount + 1;
87 % C variables: conditional queue lengths C(j,i)
91 varCount = varCount + 1;
96 % p1 variables: marginal probabilities p1(j,i,ni), ni in 0:N
97 p1idx = zeros(M, M, N+1);
101 varCount = varCount + 1;
102 p1idx(j, i, ni+1) = varCount;
107 % p1c variables: complementary marginal p1c(j,i,ni), ni in 0:N
108 p1cidx = zeros(M, M, N+1);
112 varCount = varCount + 1;
113 p1cidx(j, i, ni+1) = varCount;
119 if verbose; fprintf('Total variables: %d\n', nVars); end
122 lb = zeros(nVars, 1);
146 ub(p1idx(j, i, ni+1)) = 1;
155 ub(p1cidx(j, i, ni+1)) = 1;
164 if verbose; fprintf('Building constraints...\n'); end
166 %% ZER1: p1(j,j,0) = 0 for all j (via upper bound)
167 if verbose; fprintf(' ZER1 constraints...\n'); end
169 ub(p1idx(j, j, 0+1)) = 0;
172 %% ZER3: p1(j,i,N) = 0 for j ~= i (via upper bound)
173 if verbose; fprintf(' ZER3 constraints...\n'); end
177 ub(p1idx(j, i, N+1)) = 0;
182 %% CEQU: C(j,j) = Q(j) for all j
183 if verbose; fprintf(' CEQU constraints...\n'); end
185 row = zeros(1, nVars);
192 %% ONE1: sum over ni of (p1(j,i,ni) + p1c(j,i,ni)) = 1 for each (j,i)
193 if verbose; fprintf(' ONE1 constraints...\n'); end
196 row = zeros(1, nVars);
198 row(p1idx(j, i, ni+1)) = 1;
199 row(p1cidx(j, i, ni+1)) = 1;
206 %% UTIL: U(i) = sum over t,nt of p1(i,t,nt) for each (i,t)
207 if verbose; fprintf(' UTIL constraints...\n'); end
210 row = zeros(1, nVars);
213 row(p1idx(i, t, nt+1)) = -1;
220 %% QLEN: Q(i) = sum over ni of ni*p1(i,i,ni) for each i
221 if verbose; fprintf(' QLEN constraints...\n'); end
223 row = zeros(1, nVars);
226 row(p1idx(i, i, ni+1)) = -ni;
232 %% CLEN: C(j,i) = sum over ni of ni*p1(j,i,ni) for each (j,i)
233 if verbose; fprintf(' CLEN constraints...\n'); end
236 row = zeros(1, nVars);
239 row(p1idx(j, i, ni+1)) = -ni;
246 %% MPCB: sum over i of C(j,i) = N*U(j) for each j
247 if verbose; fprintf(' MPCB constraints...\n'); end
249 row = zeros(1, nVars);
258 %% POPC: sum over i of Q(i) = N
259 if verbose; fprintf(' POPC constraint...\n'); end
260 row = zeros(1, nVars);
267 %% GFFL0: Global flow for ni=0
268 % For each i: sum over j~=i of q(j,i)*p1(j,i,0) = sum over j~=i of q(i,j)*p1(i,i,1)
269 if verbose; fprintf(' GFFL0 constraints...\n'); end
271 row = zeros(1, nVars);
274 row(p1idx(j, i, 0+1)) = row(p1idx(j, i, 0+1)) + q(j,i);
275 row(p1idx(i, i, 1+1)) = row(p1idx(i, i, 1+1)) - q(i,j);
282 %% GFFL: Global flow for ni in 1..N-1
283 % For each (i,ni): sum over j~=i of q(j,i)*p1(j,i,ni) = sum over j~=i of q(i,j)*p1(i,i,ni+1)
284 if verbose; fprintf(' GFFL constraints...\n'); end
287 row = zeros(1, nVars);
290 row(p1idx(j, i, ni+1)) = row(p1idx(j, i, ni+1)) + q(j,i);
291 row(p1idx(i, i, ni+1+1)) = row(p1idx(i, i, ni+1+1)) - q(i,j);
299 %% UJNT: Joint probability symmetry
300 % sum over ni=1..N of p1(j,i,ni) = sum over nj=1..N of p1(i,j,nj) for each (i,j)
301 if verbose; fprintf(' UJNT constraints...\n'); end
304 row = zeros(1, nVars);
306 row(p1idx(j, i, ni+1)) = row(p1idx(j, i, ni+1)) + 1;
309 row(p1idx(i, j, nj+1)) = row(p1idx(i, j, nj+1)) - 1;
316 %% QBAL: Queue balance
317 % For each i: sum over j~=i of (q(i,j)*U(i) - q(j,i)*sum_nj(p1(i,j,nj)) - q(j,i)*p1(j,i,0)) = 0
318 if verbose; fprintf(' QBAL constraints...\n'); end
320 row = zeros(1, nVars);
323 row(Uidx(i)) = row(Uidx(i)) + q(i,j);
325 row(p1idx(i, j, nj+1)) = row(p1idx(i, j, nj+1)) - q(j,i);
327 row(p1idx(j, i, 0+1)) = row(p1idx(j, i, 0+1)) - q(j,i);
334 %% Build objective function
335 if verbose; fprintf('Building objective function...\n'); end
337 c(Uidx(objective_queue)) = 1;
339 if strcmp(sense, 'max')
344 if verbose; fprintf('Solving LP with %d variables and %d equality constraints...\n', ...
345 nVars, size(Aeq, 1)); end
347 % LP algorithm. R2025a's default 'dual-simplex-highs'
is broken in some
348 % installs (errors "Unrecognized field name optimstatus"), so an
349 % interior-point variant
is required here as elsewhere in this family.
351 % THIS FILE DELIBERATELY DIFFERS FROM qrf_bas.m AND THE OTHER mapqn_bnd_*
352 % FILES, WHICH DEFAULT TO 'interior-point-legacy'. DO NOT HARMONIZE THEM.
353 % The sign of
the tradeoff
is OPPOSITE here. On
the badly-scaled QRF/BAS
354 % instances 'interior-point-legacy'
is markedly
the more accurate of
the
355 % two (see
the comment in qrf_bas.m for
the measured errors). On
the LPs
356 % this file builds it FAILS OUTRIGHT: for
the M=2, N=2, mu=[1,1] tandem it
357 % returns exitflag -2, "no feasible point found", on an LP that
is
358 % demonstrably feasible -- glpsol solves this function's own assembled Aeq,
359 % beq, lb, ub to 2/3 in BOTH senses, matching
the MVA reference and
the
360 % Python bnd_lr_pf, and
the exact product-form point satisfies every
361 % constraint block by hand. 'interior-point' returns 2/3 with exitflag 1.
362 % It
is also exact on this file's other test instances (
the N=3 symmetric
363 % tandem at 0.75, and
the 3-queue asymmetric case, reproduced to ~1e-6), so
364 % it
is the correct primary here, not a fallback.
366 % Consequence worth knowing: a negative exitflag from this family
is not
367 % evidence that
the model
is infeasible. The [0.5, 1.0] that
368 % 'interior-point-legacy' reported on
the N=2 tandem was
the residue of a
369 % failed solve, not a loose bound. Cross-check against glpsol before
370 % believing an exitflag -2.
372 % Override via params.lpAlgorithm.
373 if isfield(params, 'lpAlgorithm') && ~isempty(params.lpAlgorithm)
374 lpAlgorithm = params.lpAlgorithm;
376 lpAlgorithm = 'interior-point';
379 options = optimoptions('linprog', 'Display', 'final', 'Algorithm', lpAlgorithm);
381 options = optimoptions('linprog', 'Display', 'off', 'Algorithm', lpAlgorithm);
384 [x, fval, exitflag] = linprog(c, [], [], Aeq, beq, lb, ub, options);
386 if strcmp(sense, 'max')
392 result.objective = fval;
393 result.exitflag = exitflag;
395 % Whether
the metric fields below can be filled
is a property of
the
396 % solution vector, not of exitflag. exitflag 0 (iteration limit) still
397 % returns a usable interior point -- that
is the normal outcome on badly
398 % scaled instances such as
the reference BAS network -- whereas a solve that
399 % breaks down returns an empty or non-finite x, which must never be
400 % consumed. Predicate on x accordingly, and say so rather than skipping
401 % silently. (Observed here: exitflag -4 comes back with x empty.)
402 hasSolution = ~isempty(x) && all(isfinite(x));
403 if ~isempty(x) && ~hasSolution
404 warning('mapqn_bnd_lr_pf:nonFiniteSolution', ...
405 'linprog returned a non-finite solution (exitflag %d); U and
the other metric fields are left unpopulated.', ...
410 result.U = zeros(M, 1);
411 result.Q = zeros(M, 1);
414 result.U(i) = x(Uidx(i));
415 result.Q(i) = x(Qidx(i));
419 if verbose; fprintf('\n=== Results ===\n'); end
420 if verbose; fprintf('Objective value: %f\n', fval); end
421 if verbose; fprintf('Exit flag: %d\n', exitflag); end
422 if hasSolution && verbose
423 fprintf('\nUtilizations:\n');
425 fprintf(' Queue %d: U = %.6f\n', i, result.U(i));
427 fprintf('\nMean queue lengths:\n');
429 fprintf(' Queue %d: Q = %.6f\n', i, result.Q(i));