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