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% Require source-adjacent queue (only two stations); see _kb/06-solver-catalog.md for rationale
37if any(~ismember(1:M, [sourceIdx queueIdx]))
41arrProc = sn.proc{sourceIdx}{1};
42svcProc = sn.proc{queueIdx}{1};
43if numel(arrProc) < 2 || numel(svcProc) < 2
46Da0 = arrProc{1}; Da1 = arrProc{2};
47Ds0 = svcProc{1}; Ds1 = svcProc{2};
49% Genuine MAPs only (RAP/ME left to fallback); see _kb/06-solver-catalog.md
for rationale
50if ~is_markovian_map(Da0, Da1) || ~is_markovian_map(Ds0, Ds1)
54% Fire only
for non-renewal (correlated) processes; see _kb/06-solver-catalog.md
for rationale
55if is_renewal_map(Da0, Da1) && is_renewal_map(Ds0, Ds1)
59lambda = map_lambda({Da0, Da1});
60mu = map_lambda({Ds0, Ds1});
62 return; % unstable or degenerate: leave to the fallback path
65ql = Q_CT_MAP_MAP_1(Da0, Da1, Ds0, Ds1,
'MaxNumComp', 100000);
67EN = sum((0:numel(ql)-1)
' .* ql);
70QN = zeros(M, K); UN = zeros(M, K); RN = zeros(M, K); TN = zeros(M, K);
71TN(sourceIdx, 1) = lambda;
72TN(queueIdx, 1) = lambda;
75RN(queueIdx, 1) = EN / lambda;
81function tf = is_markovian_map(D0, D1)
82% A MAP has non-negative off-diagonal rates in D0, non-negative rates in D1, and
83% (D0+D1) is an infinitesimal generator (zero row sums). A RAP/ME violates the
84% sign conditions while still defining a valid point process.
86tol = 1e-9 * max(1, max(abs([D0(:); D1(:)])));
87offDiag = D0(~eye(ns) > 0);
88tf = all(offDiag >= -tol) && all(D1(:) >= -tol) ...
89 && all(abs(sum(D0 + D1, 2)) <= tol);
92function tf = is_renewal_map(D0, D1)
93% Single definition in mam_is_renewal_map, shared with solver_mam_basic, which
94% needs the same predicate to keep a correlated arrival off the marginal-only
96tf = mam_is_renewal_map(D0, D1);