1function [ok, QN, UN, RN, TN, CN, XN] = solver_mam_mapmap1_exact(sn)
2% SOLVER_MAM_MAPMAP1_EXACT Exact MAP/MAP/1 solution
for a single-
class,
3% single-server, open Source -> FCFS Queue -> Sink model.
5% The decomposition methods (dec.source/dec.mmap) approximate this queue: they
6% either fit
the arrival to a simpler process or treat
the service as a renewal
7% phase-type, discarding autocorrelation. When
the arrival or
the service
is a
8% genuinely correlated (non-renewal) MAP, this routine returns
the exact mean
9% queue length via
the matrix-geometric MAP/MAP/1 solver (Q_CT_MAP_MAP_1), which
10% carries both phase processes across arrivals and departures.
12% Returns ok=false (and empty metrics) when
the model
is not an exactly-solvable
13% single MAP/MAP/1 queue, so
the caller falls back to
the decomposition methods.
15% Copyright (c) 2012-2026, Imperial College London
17ok = false; QN = []; UN = []; RN = []; TN = []; CN = []; XN = [];
25 return; % open model only
28sourceIdx = find(sn.sched == SchedStrategy.EXT);
29queueIdx = find(sn.sched == SchedStrategy.FCFS);
30if numel(sourceIdx) ~= 1 || numel(queueIdx) ~= 1
33if sn.nservers(queueIdx) ~= 1
36% The arrival seen by
the queue equals
the source MAP only when
the queue
is fed
37% directly by
the source (no intermediate stations): require that
the source and
38%
the queue are
the only stations.
39if any(~ismember(1:M, [sourceIdx queueIdx]))
43arrProc = sn.proc{sourceIdx}{1};
44svcProc = sn.proc{queueIdx}{1};
45if numel(arrProc) < 2 || numel(svcProc) < 2
48Da0 = arrProc{1}; Da1 = arrProc{2};
49Ds0 = svcProc{1}; Ds1 = svcProc{2};
51% Q_CT_MAP_MAP_1
requires both processes to be genuine MAPs, i.e. (D0,D1) must be
52% a Markovian generator pair. A RAP or an ME process
is a non-Markovian
53% generalization whose D0 may carry negative off-diagonal entries: it
is a valid
54% point process but not a MAP, and BuTools rejects it outright. Leave those to
the
55% fallback path rather than erroring out of an exact routine that cannot take them.
56if ~is_markovian_map(Da0, Da1) || ~is_markovian_map(Ds0, Ds1)
60% Only fire when
the decomposition methods are inexact, i.e.
the arrival or
the
61% service
is a genuinely correlated (non-renewal) MAP. Renewal PH/PH/1
is already
62% solved exactly by
the existing decomposition path.
63if is_renewal_map(Da0, Da1) && is_renewal_map(Ds0, Ds1)
67lambda = map_lambda({Da0, Da1});
68mu = map_lambda({Ds0, Ds1});
70 return; % unstable or degenerate: leave to
the fallback path
73ql = Q_CT_MAP_MAP_1(Da0, Da1, Ds0, Ds1,
'MaxNumComp', 100000);
75EN = sum((0:numel(ql)-1)
' .* ql);
78QN = zeros(M, K); UN = zeros(M, K); RN = zeros(M, K); TN = zeros(M, K);
79TN(sourceIdx, 1) = lambda;
80TN(queueIdx, 1) = lambda;
83RN(queueIdx, 1) = EN / lambda;
89function tf = is_markovian_map(D0, D1)
90% A MAP has non-negative off-diagonal rates in D0, non-negative rates in D1, and
91% (D0+D1) is an infinitesimal generator (zero row sums). A RAP/ME violates the
92% sign conditions while still defining a valid point process.
94tol = 1e-9 * max(1, max(abs([D0(:); D1(:)])));
95offDiag = D0(~eye(ns) > 0);
96tf = all(offDiag >= -tol) && all(D1(:) >= -tol) ...
97 && all(abs(sum(D0 + D1, 2)) <= tol);
100function tf = is_renewal_map(D0, D1)
101% Single definition in mam_is_renewal_map, shared with solver_mam_basic, which
102% needs the same predicate to keep a correlated arrival off the marginal-only
104tf = mam_is_renewal_map(D0, D1);