LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_cftp.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_NC_SOLVER_NC_CFTP_H
6#define LINE_SOLVERS_NC_SOLVER_NC_CFTP_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The `cftp` and `cftp.approx` methods of SolverNC: stationary analysis of a
12 * closed single-class product-form network by PERFECT SAMPLING from its balance
13 * function, rather than by evaluating the normalizing constant.
14 *
15 * WHY NC AND NOT CTMC. The sampler never builds a generator: it draws states of
16 * the Gordon-Newell product form directly from the station balance functions,
17 * and its gate is exactly the product-form envelope NC already declares. It was
18 * a SolverCTMC method until 2026-09-27 and moved here in all four codebases.
19 * It yields no normalizing constant, so `lG` is left NaN, as for `morrison`.
20 *
21 * Port of matlab/src/solvers/NC/solver_nc_cftp.m. Reference: S. Kijima and
22 * T. Matsui, "Approximate/Perfect Samplers for Closed Jackson Networks", Winter
23 * Simulation Conference 2005; the sampler itself is `pfqn::pfqn_cftp`.
24 *
25 * WHAT KIND OF NUMBER THIS IS. States are drawn iid from the EXACT stationary
26 * distribution, so there is no truncation and no cutoff, but the means are
27 * sample averages and carry Monte Carlo error O(samples^(-1/2)). Diffing a
28 * `cftp` row against an exact solver at solver tolerance therefore reads as a
29 * defect and is not one -- it is the same contract as the SSA row. `cftp.approx`
30 * additionally drops exactness of the DRAW, running the paper's rapidly-mixing
31 * sampler M_A for a deterministic number of updates instead of coupling from the
32 * past; its running time is bounded where perfect sampling's is not.
33 *
34 * WHY THE MODEL CLASS IS GATED SO NARROWLY. The sampler's balance function
35 * encodes the closed single-class product form and nothing else, so a model
36 * outside that class is REFUSED rather than approximated: the sampler would
37 * return states of a different network and the estimator would converge, with
38 * shrinking error bars, to the wrong answer.
39 *
40 * THE TWO ESTIMATORS ARE NOT INTERCHANGEABLE, and the split is deliberate.
41 * Throughput is taken at the REFERENCE station and propagated through the visit
42 * ratios, which matches the SolverCTMC convention (XN is the arrival rate at
43 * the reference station) and keeps flow balance, Little's law and C = N/X exact in
44 * the reported table. Utilization instead keeps its own estimator
45 * E[min(n_i,c_i)]/c_i, which is unbiased and confined to [0,1] by construction,
46 * whereas deriving it from the reference-station throughput lets Monte Carlo
47 * error push a saturated station above one.
48 */
49
50#include <algorithm>
51#include <cmath>
52#include <cstddef>
53#include <cstdint>
54#include <limits>
55#include <map>
56#include <string>
57#include <vector>
58
63#include "line/num/number.h"
66#include "line/util/error.h"
67#include "line/util/matrix.h"
68
69namespace line {
70namespace nc {
71
72/** The knobs of one perfect-sampling run. */
74 /**
75 * Number of iid stationary draws, SolverOptions('NC').samples by default. A
76 * zero is refused rather than defaulted: the draw IS the answer here.
77 */
78 std::size_t samples = 100000;
79 /** Stream seed, so a row is reproducible within this port. */
80 unsigned long seed = 23000;
81};
82
83/** The means of one `cftp` solve, before the SolverNC metric filter. */
84template <class T>
85struct NcCftpAvg {
86 Matrix<T> QN, UN, RN, TN; ///< (nstations x nclasses)
87 std::vector<T> XN, CN; ///< (nclasses) system throughput and response time
88};
89
90/** What one `cftp` solve produces beside the means. */
91template <class T>
94 /** (samples x stations) the sampled states, one draw per row. */
96 /** (samples) per-draw coalescence horizon, or the mixing steps of M_A. */
97 std::vector<long> horizon;
98 /** The distinct sampled states, aligned with `paggr`. */
99 std::vector<std::vector<int>> distinct_states;
100 /** Empirical probability of each distinct sampled state. */
101 std::vector<T> paggr;
102 std::string actualmethod = "cftp";
103};
104
105namespace cftp_detail {
106
107/** Port of the reference's method-string to sampler map. */
108inline pfqn::CftpMethod cftp_sampler(const std::string& method) {
109 if (method == "cftp" || method == "cftp.exact" || method.empty())
111 if (method == "cftp.approx") return pfqn::CftpMethod::Approx;
112 throw InputError("SolverNC(cftp): unknown cftp variant '" + method +
113 "'. Use 'cftp' or 'cftp.approx'");
114}
115
116/**
117 * Can the cftp perfect sampler be asked for this model?
118 *
119 * The model-class gate asked as a predicate rather than thrown. `cftp_assert`
120 * below refuses with it, and the feature-set / AUTO report reaches the same
121 * call, so that a caller sees the verdict before paying for a run. A second
122 * copy of the rules is how the report and the run drift into two answers.
123 *
124 * The sampler is exact only on the closed single-class product form its balance
125 * function encodes; anything else must be refused, not approximated. What the
126 * feature registry CAN name is also declared in `qn::nc_feature_set("cftp")`;
127 * this predicate carries the structural rules the registry has no name for --
128 * the class count, the station count and the phase count.
129 *
130 * @param sn the refreshed struct of the model
131 * @return an empty string when the sampler may run, else the refusal
132 */
133template <class T>
134std::string cftp_supports_reason(const qn::NetworkStruct<T>& sn) {
135 const T zero = num_traits<T>::from_int(0);
136 const std::size_t M = sn.nstations;
137 if (sn.nclasses != 1)
138 return (
139 "SolverNC(cftp): the cftp method supports single-class models only, this model has " +
140 std::to_string(sn.nclasses) + " classes");
141 const std::vector<double> njobs = sn.njobs();
142 if (!std::isfinite(njobs[0]) || njobs[0] < 1.0)
143 return ("SolverNC(cftp): the cftp method supports closed models only, "
144 "with a finite positive population");
145 if (M < 2)
146 return ("SolverNC(cftp): the cftp method requires at least two stations");
147 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind) {
148 const lang::NodeType t = sn.nodes[ind].nodetype;
151 return (
152 "SolverNC(cftp): the cftp method supports Queue, Delay and Router nodes only, "
153 "node " +
154 std::to_string(ind + 1) + " is of a different type");
155 }
156 for (std::size_t i = 0; i < M; ++i) {
157 const lang::SchedStrategy s = sn.stations[i].sched;
161 return (
162 "SolverNC(cftp): the cftp method requires a product-form scheduling strategy "
163 "(INF, PS, FCFS, SIRO, LCFSPR) at station " +
164 std::to_string(i + 1));
165 if (sn.phases_of(i + 1, 1) > 1)
166 return (
167 "SolverNC(cftp): the cftp method requires exponential service times, station " +
168 std::to_string(i + 1) + " has " + std::to_string(sn.phases_of(i + 1, 1)) +
169 " phases");
170 if (std::isfinite(sn.cap[i]) && sn.cap[i] < njobs[0])
171 return (
172 "SolverNC(cftp): the cftp method requires infinite buffers, station " +
173 std::to_string(i + 1) + " has capacity " + std::to_string(sn.cap[i]));
174 if (!(sn.rates(i, 0) > zero))
175 return ("SolverNC(cftp): the cftp method requires a finite positive "
176 "service rate at station " +
177 std::to_string(i + 1));
178 if (!sn.stations[i].lldscaling.empty() || sn.stations[i].cdscaling ||
179 sn.stations[i].jdscaling)
180 return ("SolverNC(cftp): the cftp method does not support "
181 "load-dependent, class-dependent or joint-dependent service "
182 "rates");
183 }
184 if (!sn.regions.empty())
185 return (
186 "SolverNC(cftp): the cftp method does not support finite capacity regions");
187 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
188 for (std::size_t r = 0; r < sn.nodes[ind].routing.size(); ++r) {
189 const lang::RoutingStrategy rs = sn.nodes[ind].routing[r];
192 return (
193 "SolverNC(cftp): the cftp method requires Markovian routing (PROB, RAND), "
194 "node " +
195 std::to_string(ind + 1) + " uses a state-dependent strategy");
196 }
197 return std::string();
198}
199
200/**
201 * Port of the reference's model-class gate.
202 *
203 * The sampler is exact only on the closed single-class product form its balance
204 * function encodes; anything else must be refused, not approximated.
205 */
206template <class T>
207void cftp_assert(const qn::NetworkStruct<T>& sn) {
208 const std::string reason = cftp_supports_reason(sn);
209 if (!reason.empty()) throw UnsupportedError(reason);
210}
211
212} // namespace cftp_detail
213
214/**
215 * The `cftp` model-class gate as a public predicate.
216 *
217 * The one call a REPORT can make: `auto_family_refusal` reaches the structural
218 * rules the feature set has no name for (the class count, the station count and
219 * the phase count) through this, and `solver_nc_cftp` refuses through the
220 * same body, so a pair the report offers is a pair the sampler runs.
221 *
222 * @param sn the refreshed struct of the model
223 * @return an empty string when the sampler may run, else the refusal
224 */
225template <class T>
227 return cftp_detail::cftp_supports_reason(sn);
228}
229
230/**
231 * Solve with the `cftp` / `cftp.approx` method.
232 *
233 * @param sn the refreshed struct of a CLOSED single-class product-form network
234 * @param opt the SolverNC knobs; `method` picks the sampler
235 * @param cftpopt the run length and the stream
236 */
237template <class T>
239 const NcCftpOptions& cftpopt) {
241 "solver_nc_cftp draws random states and forms the station balance functions "
242 "in the log domain, neither of which exists in exact rational arithmetic");
243 const T zero = num_traits<T>::from_int(0);
244 const std::size_t M = sn.nstations, R = sn.nclasses;
245
246 const pfqn::CftpMethod sampler = cftp_detail::cftp_sampler(opt.method);
247 cftp_detail::cftp_assert(sn);
248 if (cftpopt.samples < 1)
249 throw InputError("SolverNC(cftp): the cftp method requires a finite positive number of "
250 "samples; pass --samples");
251
253 std::vector<T> L(M, zero);
254 for (std::size_t i = 0; i < M; ++i) L[i] = ch.Lchain(i, 0);
255 const int N = static_cast<int>(ch.Nchain[0]);
256 std::vector<int> S(M, 1);
257 for (std::size_t i = 0; i < M; ++i)
258 S[i] = sn.stations[i].sched == lang::SchedStrategy::INF
260 : static_cast<int>(sn.stations[i].nservers);
261
262 pfqn::McRng rng(static_cast<std::uint64_t>(cftpopt.seed));
263 const pfqn::CftpResult<T> s = pfqn::pfqn_cftp(L, N, S, cftpopt.samples, sampler, rng);
264
266 o.avg.QN = Matrix<T>(M, R, zero);
267 o.avg.UN = Matrix<T>(M, R, zero);
268 o.avg.RN = Matrix<T>(M, R, zero);
269 o.avg.TN = Matrix<T>(M, R, zero);
270 o.avg.XN.assign(R, zero);
271 o.avg.CN.assign(R, zero);
272
273 // E[min(n_i, c_i)]: the busy-server count, which is the utilization law's
274 // numerator and, at the reference station, the throughput law's.
275 std::vector<T> busy(M, zero);
276 for (std::size_t i = 0; i < M; ++i) {
277 T acc = zero;
278 for (std::size_t r = 0; r < cftpopt.samples; ++r) {
279 const int n = s.X(r, i);
280 const int cap = S[i] == pfqn::cftp_inf_servers ? n : (n < S[i] ? n : S[i]);
281 acc += num_traits<T>::from_int(cap);
282 }
283 busy[i] = T(acc / num_traits<T>::from_int(static_cast<int>(cftpopt.samples)));
284 }
285
286 const std::size_t iref = ch.refstatchain[0] - 1;
287 if (ch.STchain(iref, 0) > zero && ch.Vchain(iref, 0) > zero)
288 o.avg.XN[0] = T(busy[iref] / ch.STchain(iref, 0) / ch.Vchain(iref, 0));
289 if (o.avg.XN[0] > zero)
290 o.avg.CN[0] = T(num_traits<T>::from_int(N) / o.avg.XN[0]);
291
292 for (std::size_t i = 0; i < M; ++i) {
293 o.avg.QN(i, 0) = s.Q[i];
294 o.avg.TN(i, 0) = T(ch.Vchain(i, 0) * o.avg.XN[0]);
295 o.avg.UN(i, 0) = sn.stations[i].sched == lang::SchedStrategy::INF
296 ? o.avg.QN(i, 0)
297 : T(busy[i] / num_traits<T>::from_double(sn.stations[i].nservers));
298 if (o.avg.TN(i, 0) > zero) o.avg.RN(i, 0) = T(o.avg.QN(i, 0) / o.avg.TN(i, 0));
299 }
300
301 // The empirical law over the DISTINCT draws, the reference's [SSq, pAggr].
302 // Ordered by the state tuple so two runs of the same tape list them alike.
303 std::map<std::vector<int>, std::size_t> counts;
304 for (std::size_t r = 0; r < cftpopt.samples; ++r) {
305 std::vector<int> row(M, 0);
306 for (std::size_t i = 0; i < M; ++i) row[i] = s.X(r, i);
307 ++counts[row];
308 }
309 for (std::map<std::vector<int>, std::size_t>::const_iterator it = counts.begin();
310 it != counts.end(); ++it) {
311 o.distinct_states.push_back(it->first);
312 o.paggr.push_back(T(num_traits<T>::from_int(static_cast<int>(it->second)) /
313 num_traits<T>::from_int(static_cast<int>(cftpopt.samples))));
314 }
315
316 o.states = s.X;
317 o.horizon = s.horizon;
318 o.actualmethod = opt.method.empty() ? std::string("cftp") : opt.method;
319 return o;
320}
321
322/**
323 * Solve with `cftp` taking the run length and the stream from `opt`, the
324 * `options.samples` / `options.seed` every SolverNC estimator reads.
325 */
326template <class T>
328 NcCftpOptions cftpopt;
329 cftpopt.samples = opt.samples;
330 cftpopt.seed = opt.seed;
331 return solver_nc_cftp(sn, opt, cftpopt);
332}
333
334/**
335 * A `cftp` solve in the shape every other SolverNC analyzer returns, so the
336 * runner's metric filter applies to it unchanged. `lG` is NaN: the sampler
337 * yields no normalizing constant, the same convention as `morrison`. The
338 * sampled states, the empirical law over the distinct ones and the per-draw
339 * horizons ride along in the `cftp_*` fields, the reference's space/spaceAggr,
340 * sampled probabilities, cftpSamples and cftpHorizon.
341 */
342template <class T>
345 d.sol.Q = s.avg.QN;
346 d.sol.U = s.avg.UN;
347 d.sol.R = s.avg.RN;
348 d.sol.Tp = s.avg.TN;
349 d.sol.X = s.avg.XN;
350 d.sol.C = s.avg.CN;
351 d.sol.method = s.actualmethod;
352 d.sol.lG = std::numeric_limits<double>::quiet_NaN();
354 d.cftp_states = s.states;
355 d.cftp_horizon = s.horizon;
357 d.cftp_prob = s.paggr;
358 return d;
359}
360
361} // namespace nc
362} // namespace line
363
364#endif // LINE_SOLVERS_NC_SOLVER_NC_CFTP_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t phases_of(std::size_t ist, std::size_t r) const
sn.phases(i,r): the order of the process representation.
std::vector< double > cap
sn.cap and sn.classcap: the total and per-class buffers.
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< NodeDef > nodes
every node, in creation order
std::vector< Region > regions
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense matrix and non-owning view.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
NcCftpSolution< T > solver_nc_cftp(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const NcCftpOptions &cftpopt)
Solve with the cftp / cftp.approx method.
NcSolution< T > solver_nc_cftp_solution(const NcCftpSolution< T > &s)
A cftp solve in the shape every other SolverNC analyzer returns, so the runner's metric filter applie...
std::string solver_nc_cftp_supports(const qn::NetworkStruct< T > &sn)
The cftp model-class gate as a public predicate.
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
constexpr int cftp_inf_servers
Sentinel for an infinite-server (delay) station, the reference's S = Inf.
Definition pfqn_cftp.h:67
CftpMethod
Which sampler to run.
Definition pfqn_cftp.h:70
@ Cftp
exact, monotone coupling from the past
Definition pfqn_cftp.h:71
@ Approx
the rapidly-mixing approximate sampler M_A
Definition pfqn_cftp.h:72
CftpResult< T > pfqn_cftp(const std::vector< T > &L, int N, const std::vector< int > &S, std::size_t nsamples, CftpMethod method, McRng &rng)
Perfect stationary state sampling for closed single-class multiserver product-form networks,...
Definition pfqn_cftp.h:144
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Perfect stationary state sampling for closed single-class multiserver product-form networks,...
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
Chain aggregation and de-aggregation.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
std::vector< std::size_t > refstatchain
(C) 1-based reference station
Definition sn_chain.h:53
std::vector< double > Nchain
(C) population, infinite for an open chain
Definition sn_chain.h:51
Matrix< T > STchain
(M x C) mean service time
Definition sn_chain.h:48
Matrix< T > Lchain
(M x C) demand
Definition sn_chain.h:47
Matrix< T > Vchain
(M x C) visits
Definition sn_chain.h:49
The means of one cftp solve, before the SolverNC metric filter.
std::vector< T > CN
(nclasses) system throughput and response time
std::vector< T > XN
Matrix< T > TN
(nstations x nclasses)
The knobs of one perfect-sampling run.
std::size_t samples
Number of iid stationary draws, SolverOptions('NC').samples by default.
unsigned long seed
Stream seed, so a row is reproducible within this port.
What one cftp solve produces beside the means.
Matrix< int > states
(samples x stations) the sampled states, one draw per row.
std::vector< long > horizon
(samples) per-draw coalescence horizon, or the mixing steps of M_A.
std::vector< std::vector< int > > distinct_states
The distinct sampled states, aligned with paggr.
std::vector< T > paggr
Empirical probability of each distinct sampled state.
The [Q,U,R,T,C,X,lG] of the reference, plus the algorithm that ran.
Definition nc_types.h:113
std::vector< std::vector< int > > cftp_space
Definition nc_types.h:158
mva::MvaSolution< T > sol
Definition nc_types.h:114
std::vector< T > cftp_prob
Definition nc_types.h:159
std::string actualmethod
Definition nc_types.h:115
Matrix< int > cftp_states
Set only by the cftp / cftp.approx route (solver_nc_cftp.h), EMPTY for every other method: the sample...
Definition nc_types.h:156
std::vector< long > cftp_horizon
Definition nc_types.h:157
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
Return value of pfqn_cftp, mirroring [Q, X, T].
Definition pfqn_cftp.h:77
Matrix< int > X
(nsamples x M) sampled states, rows sum to K
Definition pfqn_cftp.h:79
std::vector< T > Q
(M) empirical mean queue length
Definition pfqn_cftp.h:78
std::vector< long > horizon
(nsamples) coalescence horizon or step count
Definition pfqn_cftp.h:80