1function [Q,U,R,T,C,X,lG,runtime,iter,method] = solver_nc_pas_is_analyzer(sn, options)
2% [Q,U,R,T,C,X,LG,RUNTIME,ITER,METHOD] = SOLVER_NC_PAS_IS_ANALYZER(SN, OPTIONS)
4% Importance-sampling (IS) normalizing-constant analysis of a closed two-station
5% pass-and-swap (
P&S) tandem with a non-empty swap graph (Casale, Comte &
6% Dorsman, 2026). With a genuine swap graph the ordered-state chain
is reducible
7% (Comte & Dorsman, 2021, arXiv:2009.12299); the recurrent communicating
class
8% carries a per-
class product
form pi(c) = Phi_1(c_1) Phi_2(c_2)/G_C, and its
9% constant G_C
is estimated by the
auto-normalized IS routine PFQN_PAS_IS. This
10%
is the Monte-Carlo counterpart of SOLVER_NC_OI_ANALYZER
for the
case that the
11% exact OI convolution does not apply (non-empty swap graph).
13%
Station 1
is the upstream
P&S queue (prefix of the ordering), station 2 the
14% downstream queue (reversed suffix); the swap graph
is a
class-level
property
15% read from the upstream station. Both stations must be OI/
P&S with unit per-
16%
class visits (the balanced-fairness framework). Mean per-class queue lengths
17% come directly from PFQN_PAS_IS (
auto-normalized IS, same samples
for numerator
18% and denominator). Per-
class throughput uses the balanced-fairness ratio
19% X_r = G(N - e_r)/G(N) with common random numbers across the N and N-e_r runs
20%
for variance reduction; utilization, response time, and system time follow the
21% same conventions as SOLVER_NC_OI_ANALYZER.
23% Copyright (c) 2012-2026, Imperial College London
27method =
'is'; % importance sampling, specialized to OI/
P&S stations
32% ---- reject
class switching (
P&S rank rates are per raw
class) -------------
34 if numel(sn.inchain{c}) > 1
35 line_error(mfilename,
'solver_nc_pas_is requires one class per chain (no class switching).');
38if any(isinf(sn.njobs))
39 line_error(mfilename, 'solver_nc_pas_is requires a closed queueing network.');
42 line_error(mfilename, 'solver_nc_pas_is models a two-station pass-and-swap tandem (got %d stations).', M);
44N = round(sn.njobs(:)');
46% ---- classify stations and read swap graph ---------------------------------
50 if sn.sched(ist) ~= SchedStrategy.PAS && sn.sched(ist) ~= SchedStrategy.OI
51 line_error(mfilename, 'solver_nc_pas_is requires both stations to be OI/PAS (station %d
is not).', ist);
53 ind = sn.stationToNode(ist);
54 if ind < 1 || ind > numel(sn.nodeparam) || ~isstruct(sn.nodeparam{ind})
55 line_error(mfilename,
'station %d has no OI/PAS node parameters.', ist);
57 svc{ist} = sn.nodeparam{ind}.svcRateFun;
59 line_error(mfilename,
'OI/PAS station %d has no service rate function; set it via setService(@(c) ...).', ist);
61 if isfield(sn.nodeparam{ind},
'swapGraph')
62 swapG{ist} = sn.nodeparam{ind}.swapGraph;
68% derive the global placement-order DAG H (recurrent communicating
class) from
69% the
P&S dynamics via pas_swap2order; see _kb/06-solver-catalog.md (NC section)
70G1 = swapG{1};
if isempty(G1), G1 = zeros(K, K); end
71G2 = swapG{2};
if isempty(G2), G2 = zeros(K, K); end
72H = pas_swap2order({G1, G2}, {svc{1}, svc{2}}, ones(1, K));
74% ---- per-
class visits (chain ==
class); require unit
visits ----------------
77 c = find(sn.chains(:, r));
80 isf = sn.stationToStateful(ist);
81 V(ist, r) = vis(isf, r);
83 vref = V(sn.refstat(r), r);
85 V(:, r) = V(:, r) / vref;
90 if N(r) > 0 && abs(V(ist, r) - 1) > 1e-9
91 line_error(mfilename,
'solver_nc_pas_is requires unit per-class visits (station %d, class %d, V=%g).', ist, r, V(ist, r));
96% ---- OI rank-rate handles on a per-
class count vector ----------------------
97% svcRateFun(c) takes an ordered microstate list;
for an OI station it
is
98% permutation-invariant, so evaluate it on a canonical microstate
for count n.
99rate1 = @(n) svc{1}(pas_is_microstate(n));
100rate2 = @(n) svc{2}(pas_is_microstate(n));
103% ---- IS options; fix a base seed so the N and N-e_r runs share randoms -----
105if ~isfield(isopt,
'samples') || isempty(isopt.samples)
106 if isfield(options, 'iter_max') && ~isempty(options.iter_max) && options.iter_max > 1
107 isopt.samples = options.iter_max;
112if ~isfield(isopt, 'seed') || isempty(isopt.seed)
113 if isfield(options, 'seed') && ~isempty(options.seed)
114 isopt.seed = options.seed;
120% ---- normalizing constant and mean queue lengths at population N -----------
121[G, lG, Qpas] = pfqn_pas_is(N, mu, H, isopt);
127% ---- per-class throughput X_r = G(N - e_r)/G(N) (common random numbers) ----
131 er = zeros(1, K); er(r) = 1;
132 Gr = pfqn_pas_is(N - er, mu, H, isopt);
139% ---- throughput, utilization, response time --------------------------------
145 T(ist, r) = X(r) * V(ist, r);
149 S = sn.nservers(ist);
150 if ~isfinite(S) || S <= 0, S = 1; end
153 er = zeros(1, K); er(r) = 1;
154 muR = mu{ist}(er); % rank rate with only
class r present
156 U(ist, r) = T(ist, r) / muR / S;
164 R(ist, r) = Q(ist, r) / T(ist, r);
176runtime = toc(Tstart);
179% ==========================================================================
180function c = pas_is_microstate(n)
181% Canonical ordered microstate holding n_r copies of
class r (n a count
182% vector). For an order-independent station the rank rate
is invariant to the
183% ordering, so
this representative suffices to evaluate svcRateFun(c).
184c = repelem(1:numel(n), round(n));