LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_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_CTMC_SOLVER_CTMC_CFTP_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_CFTP_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The `cftp` and `cftp.approx` methods of SolverCTMC: stationary analysis of a
12 * closed single-class product-form network by PERFECT SAMPLING rather than
13 * state-space enumeration.
14 *
15 * Port of matlab/src/solvers/CTMC/solver_ctmc_cftp.m. Reference: S. Kijima and
16 * T. Matsui, "Approximate/Perfect Samplers for Closed Jackson Networks", Winter
17 * Simulation Conference 2005; the sampler itself is `pfqn::pfqn_cftp`.
18 *
19 * WHAT KIND OF NUMBER THIS IS. States are drawn iid from the EXACT stationary
20 * distribution, so there is no truncation and no cutoff, but the means are
21 * sample averages and carry Monte Carlo error O(samples^(-1/2)). Diffing a
22 * `cftp` row against an exact solver at solver tolerance therefore reads as a
23 * defect and is not one -- it is the same contract as the SSA row. `cftp.approx`
24 * additionally drops exactness of the DRAW, running the paper's rapidly-mixing
25 * sampler M_A for a deterministic number of updates instead of coupling from the
26 * past; its running time is bounded where perfect sampling's is not.
27 *
28 * WHY THE MODEL CLASS IS GATED SO NARROWLY. The sampler's balance function
29 * encodes the closed single-class product form and nothing else, so a model
30 * outside that class is REFUSED rather than approximated: the sampler would
31 * return states of a different network and the estimator would converge, with
32 * shrinking error bars, to the wrong answer.
33 *
34 * THE TWO ESTIMATORS ARE NOT INTERCHANGEABLE, and the split is deliberate.
35 * Throughput is taken at the REFERENCE station and propagated through the visit
36 * ratios, which matches the CTMC convention (XN is the arrival rate at the
37 * reference station) and keeps flow balance, Little's law and C = N/X exact in
38 * the reported table. Utilization instead keeps its own estimator
39 * E[min(n_i,c_i)]/c_i, which is unbiased and confined to [0,1] by construction,
40 * whereas deriving it from the reference-station throughput lets Monte Carlo
41 * error push a saturated station above one.
42 */
43
44#include <algorithm>
45#include <cmath>
46#include <cstddef>
47#include <cstdint>
48#include <map>
49#include <string>
50#include <vector>
51
56#include "line/num/number.h"
60#include "line/util/error.h"
61#include "line/util/matrix.h"
62
63namespace line {
64namespace ctmc {
65
66// NetworkStruct, NodeType and SchedStrategy come in from solver_ctmc.h.
67
68/** The knobs of one perfect-sampling run. */
70 /** Number of iid stationary draws; the reference has no default here. */
71 std::size_t samples = 0;
72 /** Stream seed, so a row is reproducible within this port. */
73 unsigned long seed = 23000;
74};
75
76/** What one `cftp` solve produces beside the means. */
77template <class T>
80 /** (samples x stations) the sampled states, one draw per row. */
82 /** (samples) per-draw coalescence horizon, or the mixing steps of M_A. */
83 std::vector<long> horizon;
84 /** The distinct sampled states, aligned with `paggr`. */
85 std::vector<std::vector<int>> distinct_states;
86 /** Empirical probability of each distinct sampled state. */
87 std::vector<T> paggr;
88 std::string actualmethod = "cftp";
89};
90
91namespace cftp_detail {
92
93/** Port of the reference's method-string to sampler map. */
94inline pfqn::CftpMethod cftp_sampler(const std::string& method) {
95 if (method == "cftp" || method == "cftp.exact" || method.empty())
97 if (method == "cftp.approx") return pfqn::CftpMethod::Approx;
98 throw InputError("SolverCTMC(cftp): unknown cftp variant '" + method +
99 "'. Use 'cftp' or 'cftp.approx'");
100}
101
102/**
103 * Can the cftp perfect sampler be asked for this model?
104 *
105 * The model-class gate asked as a predicate rather than thrown. `cftp_assert`
106 * below refuses with it, and the feature-set / AUTO report reaches the same
107 * call, so that a caller sees the verdict before paying for a run. A second
108 * copy of the rules is how the report and the run drift into two answers.
109 *
110 * The sampler is exact only on the closed single-class product form its balance
111 * function encodes; anything else must be refused, not approximated. What the
112 * feature registry CAN name is also declared in `qn::ctmc_feature_set("cftp")`;
113 * this predicate carries the structural rules the registry has no name for --
114 * the class count, the station count and the phase count.
115 *
116 * @param sn the refreshed struct of the model
117 * @return an empty string when the sampler may run, else the refusal
118 */
119template <class T>
120std::string cftp_supports_reason(const NetworkStruct<T>& sn) {
121 const T zero = num_traits<T>::from_int(0);
122 const std::size_t M = sn.nstations;
123 if (sn.nclasses != 1)
124 return (
125 "SolverCTMC(cftp): the cftp method supports single-class models only, this model has " +
126 std::to_string(sn.nclasses) + " classes");
127 const std::vector<double> njobs = sn.njobs();
128 if (!std::isfinite(njobs[0]) || njobs[0] < 1.0)
129 return ("SolverCTMC(cftp): the cftp method supports closed models only, "
130 "with a finite positive population");
131 if (M < 2)
132 return ("SolverCTMC(cftp): the cftp method requires at least two stations");
133 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind) {
134 const NodeType t = sn.nodes[ind].nodetype;
135 if (t != NodeType::Queue && t != NodeType::Delay && t != NodeType::Router)
136 return (
137 "SolverCTMC(cftp): the cftp method supports Queue, Delay and Router nodes only, "
138 "node " +
139 std::to_string(ind + 1) + " is of a different type");
140 }
141 for (std::size_t i = 0; i < M; ++i) {
142 const SchedStrategy s = sn.stations[i].sched;
143 if (!(s == SchedStrategy::INF || s == SchedStrategy::PS || s == SchedStrategy::FCFS ||
144 s == SchedStrategy::SIRO || s == SchedStrategy::LCFSPR))
145 return (
146 "SolverCTMC(cftp): the cftp method requires a product-form scheduling strategy "
147 "(INF, PS, FCFS, SIRO, LCFSPR) at station " +
148 std::to_string(i + 1));
149 if (sn.phases_of(i + 1, 1) > 1)
150 return (
151 "SolverCTMC(cftp): the cftp method requires exponential service times, station " +
152 std::to_string(i + 1) + " has " + std::to_string(sn.phases_of(i + 1, 1)) +
153 " phases");
154 if (std::isfinite(sn.cap[i]) && sn.cap[i] < njobs[0])
155 return (
156 "SolverCTMC(cftp): the cftp method requires infinite buffers, station " +
157 std::to_string(i + 1) + " has capacity " + std::to_string(sn.cap[i]));
158 if (!(sn.rates(i, 0) > zero))
159 return ("SolverCTMC(cftp): the cftp method requires a finite positive "
160 "service rate at station " +
161 std::to_string(i + 1));
162 if (!sn.stations[i].lldscaling.empty() || sn.stations[i].cdscaling ||
163 sn.stations[i].jdscaling)
164 return ("SolverCTMC(cftp): the cftp method does not support "
165 "load-dependent, class-dependent or joint-dependent service "
166 "rates");
167 }
168 if (!sn.regions.empty())
169 return (
170 "SolverCTMC(cftp): the cftp method does not support finite capacity regions");
171 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
172 for (std::size_t r = 0; r < sn.nodes[ind].routing.size(); ++r) {
173 const lang::RoutingStrategy rs = sn.nodes[ind].routing[r];
176 return (
177 "SolverCTMC(cftp): the cftp method requires Markovian routing (PROB, RAND), "
178 "node " +
179 std::to_string(ind + 1) + " uses a state-dependent strategy");
180 }
181 return std::string();
182}
183
184/**
185 * Port of the reference's model-class gate.
186 *
187 * The sampler is exact only on the closed single-class product form its balance
188 * function encodes; anything else must be refused, not approximated.
189 */
190template <class T>
191void cftp_assert(const NetworkStruct<T>& sn) {
192 const std::string reason = cftp_supports_reason(sn);
193 if (!reason.empty()) throw UnsupportedError(reason);
194}
195
196} // namespace cftp_detail
197
198/**
199 * The `cftp` model-class gate as a public predicate.
200 *
201 * The one call a REPORT can make: `auto_family_refusal` reaches the structural
202 * rules the feature set has no name for (the class count, the station count and
203 * the phase count) through this, and `solver_ctmc_cftp` refuses through the
204 * same body, so a pair the report offers is a pair the sampler runs.
205 *
206 * @param sn the refreshed struct of the model
207 * @return an empty string when the sampler may run, else the refusal
208 */
209template <class T>
211 return cftp_detail::cftp_supports_reason(sn);
212}
213
214/**
215 * Solve with the `cftp` / `cftp.approx` method.
216 *
217 * @param sn the refreshed struct of a CLOSED single-class product-form network
218 * @param opt the SolverCTMC knobs; `method` picks the sampler
219 * @param cftpopt the run length and the stream
220 */
221template <class T>
223 const CtmcCftpOptions& cftpopt) {
225 "solver_ctmc_cftp draws random states and forms the station balance functions "
226 "in the log domain, neither of which exists in exact rational arithmetic");
227 const T zero = num_traits<T>::from_int(0);
228 const std::size_t M = sn.nstations, R = sn.nclasses;
229
230 const pfqn::CftpMethod sampler = cftp_detail::cftp_sampler(opt.method);
231 cftp_detail::cftp_assert(sn);
232 if (cftpopt.samples < 1)
233 throw InputError("SolverCTMC(cftp): the cftp method requires a finite positive number of "
234 "samples; pass --samples");
235
237 std::vector<T> L(M, zero);
238 for (std::size_t i = 0; i < M; ++i) L[i] = ch.Lchain(i, 0);
239 const int N = static_cast<int>(ch.Nchain[0]);
240 std::vector<int> S(M, 1);
241 for (std::size_t i = 0; i < M; ++i)
242 S[i] = sn.stations[i].sched == SchedStrategy::INF
244 : static_cast<int>(sn.stations[i].nservers);
245
246 pfqn::McRng rng(static_cast<std::uint64_t>(cftpopt.seed));
247 const pfqn::CftpResult<T> s = pfqn::pfqn_cftp(L, N, S, cftpopt.samples, sampler, rng);
248
250 o.avg.QN = Matrix<T>(M, R, zero);
251 o.avg.UN = Matrix<T>(M, R, zero);
252 o.avg.RN = Matrix<T>(M, R, zero);
253 o.avg.TN = Matrix<T>(M, R, zero);
254 o.avg.XN.assign(R, zero);
255 o.avg.CN.assign(R, zero);
256
257 // E[min(n_i, c_i)]: the busy-server count, which is the utilization law's
258 // numerator and, at the reference station, the throughput law's.
259 std::vector<T> busy(M, zero);
260 for (std::size_t i = 0; i < M; ++i) {
261 T acc = zero;
262 for (std::size_t r = 0; r < cftpopt.samples; ++r) {
263 const int n = s.X(r, i);
264 const int cap = S[i] == pfqn::cftp_inf_servers ? n : (n < S[i] ? n : S[i]);
265 acc += num_traits<T>::from_int(cap);
266 }
267 busy[i] = T(acc / num_traits<T>::from_int(static_cast<int>(cftpopt.samples)));
268 }
269
270 const std::size_t iref = ch.refstatchain[0] - 1;
271 if (ch.STchain(iref, 0) > zero && ch.Vchain(iref, 0) > zero)
272 o.avg.XN[0] = T(busy[iref] / ch.STchain(iref, 0) / ch.Vchain(iref, 0));
273 if (o.avg.XN[0] > zero)
274 o.avg.CN[0] = T(num_traits<T>::from_int(N) / o.avg.XN[0]);
275
276 for (std::size_t i = 0; i < M; ++i) {
277 o.avg.QN(i, 0) = s.Q[i];
278 o.avg.TN(i, 0) = T(ch.Vchain(i, 0) * o.avg.XN[0]);
279 o.avg.UN(i, 0) = sn.stations[i].sched == SchedStrategy::INF
280 ? o.avg.QN(i, 0)
281 : T(busy[i] / num_traits<T>::from_double(sn.stations[i].nservers));
282 if (o.avg.TN(i, 0) > zero) o.avg.RN(i, 0) = T(o.avg.QN(i, 0) / o.avg.TN(i, 0));
283 }
284
285 // The empirical law over the DISTINCT draws, the reference's [SSq, pAggr].
286 // Ordered by the state tuple so two runs of the same tape list them alike.
287 std::map<std::vector<int>, std::size_t> counts;
288 for (std::size_t r = 0; r < cftpopt.samples; ++r) {
289 std::vector<int> row(M, 0);
290 for (std::size_t i = 0; i < M; ++i) row[i] = s.X(r, i);
291 ++counts[row];
292 }
293 for (std::map<std::vector<int>, std::size_t>::const_iterator it = counts.begin();
294 it != counts.end(); ++it) {
295 o.distinct_states.push_back(it->first);
296 o.paggr.push_back(T(num_traits<T>::from_int(static_cast<int>(it->second)) /
297 num_traits<T>::from_int(static_cast<int>(cftpopt.samples))));
298 }
299
300 o.states = s.X;
301 o.horizon = s.horizon;
302 o.actualmethod = opt.method.empty() ? std::string("cftp") : opt.method;
303 return o;
304}
305
306/** Solve with `cftp` and format, for a caller with no use for the sampled states. */
307template <class T>
309 const CtmcCftpOptions& cftpopt) {
310 const CtmcCftpSolution<T> s = solver_ctmc_cftp(sn, opt, cftpopt);
312 d.avg = s.avg;
314 return solver_ctmc_avg_table(sn, d, opt.method);
315}
316
317} // namespace ctmc
318} // namespace line
319
320#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_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.
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.
mva::AvgResult< T > solver_ctmc_avg_table(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const std::string &method)
Port of @@SolverCTMC/runAnalyzer.m's result assembly: solve, then apply the metric filter @@NetworkSo...
std::string solver_ctmc_cftp_supports(const NetworkStruct< T > &sn)
The cftp model-class gate as a public predicate.
CtmcCftpSolution< T > solver_ctmc_cftp(const NetworkStruct< T > &sn, const CtmcOptions &opt, const CtmcCftpOptions &cftpopt)
Solve with the cftp / cftp.approx method.
mva::AvgResult< T > solver_ctmc_cftp_run_analyzer(const NetworkStruct< T > &sn, const CtmcOptions &opt, const CtmcCftpOptions &cftpopt)
Solve with cftp and format, for a caller with no use for the sampled states.
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:389
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
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
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.
Port of solver_ctmc.m: the infinitesimal generator of a queueing network, assembled from the enumerat...
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
The mean performance metrics a stationary vector maps to.
The knobs of one perfect-sampling run.
std::size_t samples
Number of iid stationary draws; the reference has no default here.
unsigned long seed
Stream seed, so a row is reproducible within this port.
What one cftp solve produces beside the means.
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.
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.
The SolverCTMC knobs this port honours.
Everything one CTMC solve produces.
The metrics getAvg returns, after filtering.
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
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