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).
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);
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% The stored swap graph
is the raw (undirected)
class-compatibility graph. The
69% IS estimator needs
the GLOBAL placement-order DAG H that defines
the recurrent
70% communicating
class D; derive it from
the P&S dynamics on
the single-job-per-
71%
class instance (PAS_SWAP2ORDER).
Station 2
is the reversed suffix inside
72% PFQN_PAS_IS, so a single global order suffices.
73G1 = swapG{1};
if isempty(G1), G1 = zeros(K, K); end
74G2 = swapG{2};
if isempty(G2), G2 = zeros(K, K); end
75H = pas_swap2order({G1, G2}, {svc{1}, svc{2}}, ones(1, K));
77% ---- per-
class visits (chain ==
class); require unit
visits ----------------
80 c = find(sn.chains(:, r));
83 isf = sn.stationToStateful(ist);
84 V(ist, r) = vis(isf, r);
86 vref = V(sn.refstat(r), r);
88 V(:, r) = V(:, r) / vref;
93 if N(r) > 0 && abs(V(ist, r) - 1) > 1e-9
94 line_error(mfilename,
'solver_nc_pas_is requires unit per-class visits (station %d, class %d, V=%g).', ist, r, V(ist, r));
99% ---- OI rank-rate handles on a per-
class count vector ----------------------
100% svcRateFun(c) takes an ordered microstate list;
for an OI station it
is
101% permutation-invariant, so evaluate it on a canonical microstate
for count n.
102rate1 = @(n) svc{1}(pas_is_microstate(n));
103rate2 = @(n) svc{2}(pas_is_microstate(n));
106% ---- IS options; fix a base seed so
the N and N-e_r runs share randoms -----
108if ~isfield(isopt,
'samples') || isempty(isopt.samples)
109 if isfield(options, 'iter_max') && ~isempty(options.iter_max) && options.iter_max > 1
110 isopt.samples = options.iter_max;
115if ~isfield(isopt, 'seed') || isempty(isopt.seed)
116 if isfield(options, 'seed') && ~isempty(options.seed)
117 isopt.seed = options.seed;
123% ---- normalizing constant and mean queue lengths at population N -----------
124[G, lG, Qpas] = pfqn_pas_is(N, mu, H, isopt);
130% ---- per-class throughput X_r = G(N - e_r)/G(N) (common random numbers) ----
134 er = zeros(1, K); er(r) = 1;
135 Gr = pfqn_pas_is(N - er, mu, H, isopt);
142% ---- throughput, utilization, response time --------------------------------
148 T(ist, r) = X(r) * V(ist, r);
152 S = sn.nservers(ist);
153 if ~isfinite(S) || S <= 0, S = 1; end
156 er = zeros(1, K); er(r) = 1;
157 muR = mu{ist}(er); % rank rate with only
class r present
159 U(ist, r) = T(ist, r) / muR / S;
167 R(ist, r) = Q(ist, r) / T(ist, r);
179runtime = toc(Tstart);
182% ==========================================================================
183function c = pas_is_microstate(n)
184% Canonical ordered microstate holding n_r copies of
class r (n a
count
185% vector). For an order-independent station
the rank rate
is invariant to
the
186% ordering, so
this representative suffices to evaluate svcRateFun(c).
187c = repelem(1:numel(n), round(n));