LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ag_runner.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_AG_SOLVER_AG_RUNNER_H
6#define LINE_SOLVERS_AG_SOLVER_AG_RUNNER_H
7
8/**
9 * @file solver_ag_runner.h
10 * @ingroup line_solvers
11 * @brief The gates and the dispatch of the agent-based (RCAT) solver.
12 *
13 * The twin of solver_mam_runner.h for SolverAG: the valid method list, the
14 * structural gate that decides whether the RCAT decomposition can represent the
15 * model at all, and the conversion every AG method needs before the fixed point.
16 *
17 * These gates used to live in SolverMAM, which was answering for two unrelated
18 * decompositions at once: RCAT decomposes the MODEL into cooperating agents,
19 * every other MAM method decomposes its TRAFFIC. They share the shape of the
20 * answer and nothing else.
21 */
22
23#include <algorithm>
24#include <cmath>
25#include <string>
26#include <vector>
27
34#include "line/util/error.h"
36
37namespace line {
38namespace ag {
39
40namespace runner_detail {
41
42/** Every method SolverAG serves. */
43inline std::vector<std::string> list_valid_methods() {
44 // the names and their order are this solver's row of the method registry (doc/line-methods.json)
45 return ::line::method_registry::methods("AG");
46}
47
48/** An unlisted method is refused rather than silently resolved. */
49inline void check_method(const std::string& method) {
50 const std::vector<std::string> valid = list_valid_methods();
51 if (std::find(valid.begin(), valid.end(), method) == valid.end()) {
52 std::string names;
53 for (std::size_t i = 0; i < valid.size(); ++i) {
54 names += (i ? ", " : "") + valid[i];
55 }
56 throw InputError("SolverAG: unknown method '" + method + "'; valid methods are "
57 + names);
58 }
59}
60
61/**
62 * `SolverAG.supportsModelMethod`: the structural gate of the RCAT decomposition,
63 * as the REASON it refuses, empty when the model is admissible.
64 *
65 * RCAT gives every agent a service-phase and an arrival-phase dimension, so any
66 * law with a genuine (D0,D1) Markovian representation is admissible; see
67 * ag::rcat_supports_process for what is not. A signal is a trigger with no
68 * service, so its service entry is never read; only its Source arrival rate is,
69 * and that one must stay exponential because the removal is folded into the
70 * agent as a scalar rate.
71 *
72 * A STRING RATHER THAN A THROW, the shape `ba::method_refusal` and
73 * `mam::mam_model_method_refusal` already carry: the AUTO report has to ASK the
74 * question without raising, so that a pair it offers is a pair the run accepts.
75 * `check_model_method` below is the throwing form the analyzer keeps.
76 */
77template <class T>
78std::string method_refusal(const qn::NetworkStruct<T>& L, const std::string& method) {
80 for (std::size_t i = 0; i < L.nstations; ++i) {
81 for (std::size_t r = 0; r < L.nclasses; ++r) {
82 // has_service_law, not disabled: a Join has rates = Inf and no
83 // service law, and would otherwise be refused out of hand.
84 if (!L.has_service_law(i, r) || !(L.rates(i, r) > num_traits<T>::from_int(0)))
85 continue;
86 const bool is_signal = r < L.issignal.size() && L.issignal[r];
87 const bool is_source = L.stations[i].nodetype == qn::NodeType::Source;
88 if (is_signal && !is_source) continue;
89 if (is_signal) {
90 if (L.service[i][r].type == ProcessType::EXP) continue;
91 return std::string(
92 "SolverAG: the " + method +
93 " method needs an exponential signal arrival process (a removal signal "
94 "is folded into the agent as a scalar rate), but station '" +
95 L.stations[i].name + "' class '" + L.classes[r].name + "' is " +
96 lang::process_to_text(L.service[i][r].type) +
97 ". Use SolverMAM (-s mam, method 'dec.source') for such models");
98 }
99 if (rcat_supports_process(L.service[i][r].type)) continue;
100 return std::string(
101 "SolverAG: the " + method +
102 " method supports processes with a Markovian (D0,D1) representation only "
103 "(RCAT builds a phase dimension per agent out of it), but station '" +
104 L.stations[i].name + "' class '" + L.classes[r].name + "' is " +
105 lang::process_to_text(L.service[i][r].type) +
106 ". Use SolverMAM (-s mam, method 'dec.source') for such models");
107 }
108 }
109 // ROUTING MUST BE STATE INDEPENDENT, and the featset gate above CANNOT check
110 // this, which is why the explicit loop stays. build_rcat reads sn.rt once and
111 // stores it as RcatAction::prob; for RROBIN, WRROBIN, JSQ, SQ or SDR that entry
112 // is only a state-independent PLACEHOLDER (SyncEvent::prob in state_events.h
113 // says so, and the generator is meant to re-evaluate the split per state), so
114 // solving from it answers a DIFFERENT model at a plausible number.
115 //
116 // WHY THE FEATSET MISSES IT: `used_lang_features` registers a routing feature
117 // only inside the Queue/Delay branch and the Router/Dispatcher one. The Source
118 // branch deliberately does not -- "MATLAB registers no discipline and no
119 // routing for it" -- so a JSQ declared ON A SOURCE, which is the canonical JSQ
120 // model, never reaches the DECLARED-vs-USED comparison at all. That omission is
121 // faithful to the reference, so it cannot simply be widened here; it means the
122 // gate and this loop cover different ground and both are needed.
123 for (std::size_t nd = 0; nd < L.nodes.size(); ++nd) {
124 for (std::size_t r = 0; r < L.nodes[nd].routing.size(); ++r) {
125 const lang::RoutingStrategy rs = L.nodes[nd].routing[r];
128 continue;
129 }
130 return std::string("SolverAG: the ") + method + " method needs state-independent "
131 "routing (RCAT folds the split into a fixed action probability), but node '" +
132 L.nodes[nd].name + "' routes class '" + L.classes[r].name + "' by " +
134 ", whose share depends on the network state. Use SolverCTMC, SolverSSA or "
135 "SolverLDES, which re-evaluate the split per state";
136 }
137 }
138
139 // build_rcat never reads sn.nservers, so a c-server station would be driven
140 // at rho = lambda/mu instead of lambda/(c mu).
141 for (std::size_t i = 0; i < L.nstations; ++i) {
142 if (std::isfinite(L.stations[i].nservers) && L.stations[i].nservers > 1.0) {
143 return std::string(
144 "SolverAG: the " + method +
145 " method supports single-server stations only (RCAT does not model "
146 "sn.nservers), but station '" + L.stations[i].name + "' has " +
147 std::to_string(static_cast<long long>(L.stations[i].nservers)) +
148 " servers. Use SolverMAM (-s mam, method 'dec.source') for multiserver "
149 "models");
150 }
151 }
152
153 // A FINITE BUFFER IS NOT SOMETHING RCAT CAN CARRY, and it was being answered
154 // rather than refused: nothing under solvers/ag reads st.cap or st.classcap,
155 // so a capped station was decomposed as an unbounded one and the table
156 // reported the UNCONSTRAINED figures (a closed 2-job tandem with cap 1 on the
157 // second queue returned the same numbers with and without the cap, 1.09 jobs
158 // in a buffer of 1). Lowering the agent's level bound (nlev = njobs(r)+1) to
159 // the buffer would not fix it: the top-level boundary is a self-loop, which
160 // LOSES the arrival, whereas a closed job refused at a full buffer must BLOCK
161 // the upstream departure, and that coupling is exactly the independence RCAT
162 // assumes. see _kb/06-solver-catalog.md
163 return qn::binding_capacity_reason(std::string("SolverAG"), L);
164}
165
166/** The refusal above as the analyzer takes it: raise when there is one. */
167template <class T>
168void check_model_method(const qn::NetworkStruct<T>& L, const std::string& method) {
169 const std::string why = method_refusal(L, method);
170 if (!why.empty()) throw UnsupportedError(ErrorKind{"Model"}, why);
171}
172
173} // namespace runner_detail
174
175/**
176 * Every method SolverAG serves, as the other families expose theirs.
177 *
178 * `auto_family_methods` asks each family for its own list rather than keeping a
179 * second copy, so the "ag" row of the AUTO report is this list and cannot drift
180 * from the one `check_method` refuses against.
181 */
182inline std::vector<std::string> list_valid_methods() {
183 return runner_detail::list_valid_methods();
184}
185
186/**
187 * The gates and the dispatch of SolverAG's runAnalyzer.
188 *
189 * The conversion is not method-dependent, unlike SolverMAM's: EVERY AG method
190 * builds a CTMC per agent out of (D0,D1), so a preserved Det would reach it with
191 * no matrix at all and be read back as its mean rate, and a concentrated matrix
192 * exponential is not a generator at all.
193 */
194template <class T>
196 runner_detail::check_method(opt.method);
197 runner_detail::check_model_method(L, opt.method);
198 // runAnalyzerChecks' universal feature gate. `ag_feature_set` transcribes
199 // SolverAG.getFeatureSet name for name and has existed since the port landed,
200 // but NOTHING CALLED IT until 2026-08-19: its only two references were its own
201 // definition and a comment in mam_feature_set, so every other family here was
202 // gated and AG was not.
203 //
204 // AFTER check_model_method, following SolverCTMC: a solver's own check names
205 // WHY a construct cannot be served, and the gate only names the construct. For
206 // a matrix exponential that is the difference between "supports processes with
207 // a Markovian (D0,D1) representation only (RCAT builds a phase dimension per
208 // agent out of it)" and a bare feature name -- the set withholds ME either way,
209 // so ordering decides only which message the caller reads. Reversing these two
210 // lines is a silent downgrade of every AG refusal that has a specific reason.
211 qn::feature_gate("SolverAG", qn::ag_feature_set(opt.method), L);
212
213 // sn_nonmarkov_toph evaluates densities and fits moments, so it exists only
214 // for a transcendental arithmetic and STATIC-ASSERTS otherwise. The guard is
215 // `if constexpr` rather than a run-time test because the CLI instantiates
216 // this template for the exact Rational backend as well, and an unguarded
217 // call would fail to COMPILE there rather than refuse at run time. Under an
218 // exact arithmetic solver_ag itself raises the refusal, which is where it
219 // belongs: it is the fixed point's tolerance and the QBD's square root that
220 // cannot be represented, not the conversion.
221 qn::NetworkStruct<T> converted;
222 const qn::NetworkStruct<T>* Lp = &L;
223 if constexpr (num_traits<T>::has_transcendental) {
224 if (api::nonmkv_enabled(opt.nonmkv) && api::sn_has_nonmarkov(L, /*preserve_det=*/false)) {
225 converted = L;
227 no.order = opt.nonmkv_order;
228 no.preserve_det = false;
230 api::sn_nonmarkov_toph(converted, no);
231 Lp = &converted;
232 }
233 }
234
235 return solver_ag(*Lp, opt);
236}
237
238} // namespace ag
239} // namespace line
240
241#endif // LINE_SOLVERS_AG_SOLVER_AG_RUNNER_H
Options of the agent-based (RCAT) solver.
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
The exception types the port throws.
The language-feature gate: what a MODEL uses against what a SOLVER declares.
The method names of each solver, in listing order.
AgResult< T > solver_ag_solve(const qn::NetworkStruct< T > &L, const AgOptions &opt)
The gates and the dispatch of SolverAG's runAnalyzer.
std::vector< std::string > list_valid_methods()
Every method SolverAG serves, as the other families expose theirs.
AgResult< T > solver_ag(const qn::NetworkStruct< T > &L, const AgOptions &opt, std::size_t max_states=0)
Port of solver_ag.m.
Definition solver_ag.h:1362
bool rcat_supports_process(lang::ProcessType t)
The process types the RCAT construction can give a phase dimension to.
Definition solver_ag.h:1313
bool sn_has_nonmarkov(const qn::NetworkStruct< T > &sn, bool preserve_det=false)
Whether any law in the struct would be replaced, so a caller can skip copying the struct when there i...
bool nonmkv_enabled(const std::string &method)
Resolve options.config.nonmkv to NonmarkovOptions::enabled.
@ Ph
Bernstein density fit: a genuine phase-type, shape-carrying.
void sn_nonmarkov_toph(qn::NetworkStruct< T > &sn, const NonmarkovOptions &opts=NonmarkovOptions())
Replace every non-Markovian service and firing law by a Markovian surrogate.
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:409
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
FeatureSet ag_feature_set(const std::string &)
SolverAG.getFeatureSet: what the RCAT decomposition can represent.
std::string binding_capacity_reason(const std::string &solver, const NetworkStruct< T > &sn)
The refusal check_binding_capacity raises, as a string, or empty when no buffer binds.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Replace every non-Markovian service and firing law by a Markovian surrogate.
Port of solver_ag.m: the RCAT (Reversed Compound Agent Theorem) analyzers, reached by methods 'inap',...
The DECLARED side of the gate: one feature set per solver.
What the RCAT analyzer returns beyond the metrics.
Definition solver_ag.h:1341
options.config.nonmkv and friends.
std::size_t order
nonmkvorder, the phase budget
PhFit phfit
which surrogate family
bool preserve_det
Leave Det alone for the exact MAP/D/c branch, options.config.preserveDet.