LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_remove_class.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_API_SN_SN_REMOVE_CLASS_H
6#define LINE_API_SN_SN_REMOVE_CLASS_H
7
8/**
9 * @file
10 * @ingroup api_sn
11 * Drop one job class from a model, port of `ModelAdapter.removeClass`.
12 *
13 * The reference is `matlab/src/lang/@@MNetwork/removeClass.m` (the mutating
14 * form) with `matlab/src/io/@@ModelAdapter/removeClass.m` as its non-mutating
15 * wrapper; the JAR twins are `Network.removeClass` / `Network.withoutClass`
16 * and the python ones `Network.remove_class` / `Network.without_class`. This
17 * port has only the NON-MUTATING form, because a `NetworkStruct` is a value
18 * here rather than a handle: `V = sn_remove_class(sn, r)` leaves `sn` intact,
19 * which is what `withoutClass` buys the reference, and a caller who wants the
20 * mutating form assigns over its own struct.
21 *
22 * WHAT IT IS FOR. Ablation studies (solve the model without class r and read
23 * off what r was contributing) and per-chain decomposition, where one class is
24 * peeled at a time. Both need the REST of the model to stay solvable, which is
25 * why every per-class table has to be re-indexed rather than merely blanked.
26 *
27 * WHY THIS IS A SLICE AND NOT A DELETION. Every per-class table in the struct
28 * is POSITIONAL: `service[i][r]`, `st.schedparam[r]`, `nd.routing[r]`, the
29 * (K x K) matrix of a ClassSwitch, the (r,s) key of a routing block. Blanking
30 * entry r and leaving the tables at width K would leave a model whose class
31 * list is one shorter than the tables that index it, and the refresh would then
32 * read class r+1's service under class r's name. That is the same defect the
33 * reference hit from the other side -- MATLAB's `removeClass` used to leave the
34 * (K x K) class-switching mask at its old size, and `refreshRoutingMatrix`
35 * indexed `sn.refstat` out of range on the next `getStruct` -- so every table
36 * is sliced here and the struct is then re-refreshed from the sliced stage-one
37 * data.
38 *
39 * ONLY STAGE-ONE DATA IS TOUCHED. `network_builder.h` states the split: stage
40 * one constructs, stage two (`refresh_*`) derives. So this file slices the
41 * constructed tables (classes, service, the station and node per-class vectors,
42 * the ClassSwitch matrices, the routing blocks) and then calls
43 * `refresh_struct()`, which re-derives `rates`, `scv`, `disabled`, `chains`,
44 * `inchain`, `visits`, `cap`, `rt`, `rtnodes` and the rest at the new width.
45 * Nothing derived is patched by hand, so no derived field can be left stale.
46 *
47 * WHAT IS REFUSED, and why refusing beats slicing. The reference removes a
48 * class from five node kinds (`Node`, `Station`, `ServiceStation`, `Source`,
49 * `ClassSwitch`) and REFUSES on a Cache, whose item state is indexed by class
50 * and lives in the model state rather than in the node. This port keeps that
51 * refusal verbatim and adds one for every other construct that carries a class
52 * INDEX in something other than a plain per-class vector -- a fork's per-class
53 * matrices, a transition's modes, a region's per-class caps, a retrial or
54 * polling or PAS block, a signal's target class, a class-dependent scaling
55 * function whose argument is a population vector of width K. Those cannot be
56 * re-indexed by slicing, and slicing around them would hand back a model that
57 * looks well formed and is not. `tag_chain` refuses the same way and for the
58 * same reason.
59 *
60 * ARITHMETIC: none. Structural.
61 */
62
63#include <cstddef>
64#include <map>
65#include <string>
66#include <utility>
67#include <vector>
68
71#include "line/num/number.h"
72#include "line/util/error.h"
73#include "line/util/matrix.h"
74
75namespace line {
76namespace api {
77
78namespace remove_class_detail {
79
80/**
81 * Erase class `r` (1-based) from a table that IS per-class of this model.
82 *
83 * The `size() == K` guard is the reference's own: `@@MNetwork` tests
84 * `numel(self.classCap) == K` before slicing, because these vectors are
85 * OPTIONAL -- an empty one means the station declares none of that property at
86 * all, and a vector of some other width belongs to something that is not the
87 * class axis and must be left alone.
88 */
89template <class V>
90void erase_slot(std::vector<V>& v, std::size_t K, std::size_t r) {
91 if (v.size() != K) return;
92 v.erase(v.begin() + static_cast<std::ptrdiff_t>(r - 1));
93}
94
95/** Drop row and column `r` (1-based) of a (K x K) matrix. */
96template <class T>
97void erase_row_col(Matrix<T>& C, std::size_t K, std::size_t r) {
98 if (C.rows() != K || C.cols() != K) return;
99 if (K <= 1) {
100 C = Matrix<T>();
101 return;
102 }
103 Matrix<T> R(K - 1, K - 1, num_traits<T>::from_int(0));
104 std::size_t ai = 0;
105 for (std::size_t a = 0; a < K; ++a) {
106 if (a == r - 1) continue;
107 std::size_t bi = 0;
108 for (std::size_t b = 0; b < K; ++b) {
109 if (b == r - 1) continue;
110 R(ai, bi) = C(a, b);
111 ++bi;
112 }
113 ++ai;
114 }
115 C = R;
116}
117
118/**
119 * Re-index a list of CLASS INDICES: drop `r` and shift what came after it.
120 *
121 * `Source.markedClasses` is such a list, not a per-class flag vector, so
122 * erasing position r would drop the wrong entry.
123 */
124inline void reindex_class_list(std::vector<std::size_t>& list, std::size_t r) {
125 std::vector<std::size_t> keep;
126 keep.reserve(list.size());
127 for (std::size_t a = 0; a < list.size(); ++a) {
128 const std::size_t m = list[a];
129 if (m == r) continue;
130 keep.push_back(m > r ? m - 1 : m);
131 }
132 list.swap(keep);
133}
134
135/** Everything whose class indexing this transform cannot rewrite, named. */
136template <class T>
137void remove_class_check(const qn::NetworkStruct<T>& sn) {
138 // The reference's own refusal, with its own wording: the cache item state
139 // is indexed by class and lives in the model state, not in the node.
140 if (!sn.nodeparam.empty())
141 throw UnsupportedError(
142 "sn_remove_class: cannot dynamically remove classes in models with caches. You need "
143 "to re-instantiate the model.");
144 if (sn.has_fork() || !sn.fj.empty() || !sn.forkparam.empty() || !sn.joindecl.empty())
145 throw UnsupportedError(
146 "sn_remove_class: a Fork carries per-class branch probabilities and forking levels, "
147 "and a Join a per-class quorum, none of which is a plain per-class vector; rebuild "
148 "the model without the class instead");
149 if (!sn.transparam.empty() || !sn.initmarking.empty() || !sn.statespace.empty() ||
150 !sn.stateprior.empty())
151 throw UnsupportedError(
152 "sn_remove_class: a Petri-net Place or Transition indexes its modes, its arc "
153 "multiplicities and its initial marking by token class; rebuild the model without "
154 "the class instead");
155 if (!sn.regions.empty())
156 throw UnsupportedError(
157 "sn_remove_class: a finite-capacity region carries a per-class capacity row and "
158 "per-class weights; rebuild the model without the class instead");
159 if (!sn.retrialparam.empty())
160 throw UnsupportedError("sn_remove_class: a retrial orbit is parameterized per class");
161 if (!sn.pollingparam.empty())
162 throw UnsupportedError(
163 "sn_remove_class: a polling station serves one buffer per class and carries a "
164 "per-class switchover schedule");
165 if (!sn.pasparam.empty())
166 throw UnsupportedError(
167 "sn_remove_class: a pass-and-swap station is parameterized by the ordered microstate "
168 "of class indices, which no slice can rewrite");
169 if (!sn.setupparam.empty())
170 throw UnsupportedError("sn_remove_class: a setup / delayoff schedule is per class");
171 if (!sn.reward.empty())
172 throw UnsupportedError(
173 "sn_remove_class: a reward function names its classes and would be silently "
174 "re-pointed by the shift");
175 for (std::size_t r = 0; r < sn.issignal.size(); ++r)
176 if (sn.issignal[r])
177 throw UnsupportedError(
178 "sn_remove_class: a G-network signal names a TARGET class, which the shift would "
179 "re-point at a different class");
180 for (std::size_t r = 0; r < sn.syncreply.size(); ++r)
181 if (sn.syncreply[r] != 0)
182 throw UnsupportedError(
183 "sn_remove_class: a synchronous reply names its reply class, which the shift "
184 "would re-point at a different class");
185 if (sn.gdscaling)
186 throw UnsupportedError(
187 "sn_remove_class: a global-dependence function takes a population vector of width "
188 "nclasses and cannot be narrowed");
189 for (std::size_t i = 0; i < sn.stations.size(); ++i) {
190 if (sn.stations[i].cdscaling || sn.stations[i].jdscaling)
191 throw UnsupportedError("sn_remove_class: station '" + sn.stations[i].name +
192 "' carries a class- or joint-dependent scaling function, "
193 "whose argument is a population vector of width nclasses");
194 if (sn.stations[i].svc_rate_fun)
195 throw UnsupportedError("sn_remove_class: station '" + sn.stations[i].name +
196 "' carries a service-rate function of the ordered microstate "
197 "of class indices, which no slice can rewrite");
198 }
199}
200
201} // namespace remove_class_detail
202
203/**
204 * The model without class `cls` (1-based), leaving `sn` untouched.
205 *
206 * @param sn a model; it need not be refreshed, since the result is refreshed here
207 * @param cls the 1-based class to remove
208 */
209template <class T>
211 using namespace remove_class_detail;
212 const std::size_t K = sn.classes.size();
213 if (cls == 0 || cls > K)
214 throw InputError("sn_remove_class: class index " + std::to_string(cls) +
215 " is out of range");
216 // The reference errors rather than returning an empty model, and says why.
217 if (K <= 1)
218 throw InputError(
219 "The network has a single class, it cannot be removed from the model.");
220 remove_class_check(sn);
221
223 const std::size_t r = cls;
224
225 // ---- nodes: the output (routing) strategy, `Node.removeJobClass` --------
226 for (std::size_t i = 0; i < V.nodes.size(); ++i) {
227 qn::NodeDef& nd = V.nodes[i];
228 erase_slot(nd.routing, K, r);
229 erase_slot(nd.routing_weights, K, r);
230 erase_slot(nd.routing_param, K, r);
231 }
232
233 // ---- ClassSwitch: the (K x K) matrix, `ClassSwitch.removeJobClass` ------
234 //
235 // Sliced, NOT renormalised, which is the reference's `csFun(remaining,
236 // remaining)`. Renormalising would invent a switching probability the model
237 // never declared, so a row that summed to one only because of the removed
238 // class is left SUB-STOCHASTIC. That is a real consequence and not a
239 // theoretical one: removing a class that another class switches INTO leaves
240 // the switcher with nowhere to go at that node, and what the refresh then
241 // derives is a model with a leak rather than an error. Remove the whole
242 // chain, or rebuild, when the class is a switch target.
243 for (typename std::map<std::size_t, Matrix<T> >::iterator it = V.csmatrix.begin();
244 it != V.csmatrix.end(); ++it)
245 erase_row_col(it->second, K, r);
246
247 // ---- stations: Station / ServiceStation / Source.removeJobClass ---------
248 for (std::size_t i = 0; i < V.stations.size(); ++i) {
249 qn::Station<T>& st = V.stations[i];
250 // Station: the per-class buffer, blocking rule and patience.
251 erase_slot(st.classcap, K, r);
252 erase_slot(st.droprule, K, r);
253 erase_slot(st.patience, K, r);
254 erase_slot(st.impatience, K, r);
255 erase_slot(st.orbit_impatience, K, r);
256 erase_slot(st.balking, K, r);
257 erase_slot(st.batch_reject, K, r);
258 erase_slot(st.immfeed, K, r);
259 // ServiceStation: the scheduling parameter, and the service processes
260 // held inside the server section.
261 erase_slot(st.schedparam, K, r);
262 erase_slot(st.server_parallelism, K, r);
263 erase_slot(st.departure_discipline, K, r);
264 erase_slot(st.cdscalingpeak, K, r);
265 erase_slot(st.jdscalingpeak, K, r);
266 for (std::size_t k = 0; k < st.server_types.size(); ++k) {
267 erase_slot(st.server_types[k].compatible, K, r);
268 erase_slot(st.server_types[k].service, K, r);
269 }
270 // Source: the arrival batch and the marked classes. The arrival
271 // PROCESS is the Source's row of `service` and is sliced below with
272 // every other station's, since this port keeps the two in one table
273 // exactly as the reference's `sn.rates` does.
274 erase_slot(st.arrival_batch, K, r);
275 reindex_class_list(st.marked_classes, r);
276 }
277
278 // ---- the service / arrival table ---------------------------------------
279 for (std::size_t i = 0; i < V.service.size(); ++i) erase_slot(V.service[i], K, r);
280
281 // ---- the class list, and the class indices classes hold ----------------
282 V.classes.erase(V.classes.begin() + static_cast<std::ptrdiff_t>(r - 1));
283 for (std::size_t q = 0; q < V.classes.size(); ++q) {
284 std::size_t& sp = V.classes[q].spawn;
285 if (sp == r) {
286 // The spawned class is gone, so the completion spawns nothing. 0 is
287 // this port's absent-index sentinel (MATLAB stores -1).
288 sp = 0;
289 } else if (sp > r) {
290 --sp;
291 }
292 }
293
294 // ---- the routing blocks, keyed by the (r,s) class pair ------------------
295 //
296 // A block that departs in the removed class or arrives in it is dropped
297 // whole; the survivors keep their probabilities and only their KEY shifts.
298 // `Peff` is not sliced but cleared: `refresh_routing` rebuilds it from `P`
299 // on every call, so slicing it would be work that is immediately discarded.
300 std::map<std::pair<std::size_t, std::size_t>, Matrix<T> > P2;
301 for (typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T> >::const_iterator it =
302 V.P.begin();
303 it != V.P.end(); ++it) {
304 const std::size_t a = it->first.first, b = it->first.second;
305 if (a == r || b == r) continue;
306 P2[std::make_pair(a > r ? a - 1 : a, b > r ? b - 1 : b)] = it->second;
307 }
308 V.P.swap(P2);
309 V.Peff.clear();
310
311 V.refresh_struct();
312 return V;
313}
314
315/**
316 * The same, naming the class.
317 *
318 * THE LOOKUP BY NAME IS NOT A CONVENIENCE. `ModelAdapter.removeClass` works on
319 * a COPY of the model, whose class objects are distinct from the caller's, and
320 * the JAR's identity-only lookup therefore returned the model unchanged and
321 * silently: a defect found the first time the two entry points were exercised
322 * together. A caller here holding a class of a struct that has since been
323 * copied, tagged or transformed is in exactly that position, and the name is
324 * the one handle that survives those.
325 */
326template <class T>
328 for (std::size_t r = 0; r < sn.classes.size(); ++r)
329 if (sn.classes[r].name == name) return sn_remove_class(sn, r + 1);
330 throw InputError("sn_remove_class: the model has no class named '" + name + "'");
331}
332
333} // namespace api
334} // namespace line
335
336#endif // LINE_API_SN_SN_REMOVE_CLASS_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::map< std::pair< std::size_t, std::size_t >, Matrix< T > > P
P[(r,s)] is an (nnodes x nnodes) block; absent means all zero.
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
void refresh_struct()
The whole chain, in MATLAB's refreshStruct order.
std::map< std::pair< std::size_t, std::size_t >, Matrix< T > > Peff
The routing after refresh_routing() has expanded the non-PROB strategies and folded the class switche...
std::vector< NodeDef > nodes
every node, in creation order
std::map< std::size_t, Matrix< T > > csmatrix
The class-switch matrix of a ClassSwitch node, by 1-based NODE index.
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.
qn::NetworkStruct< T > sn_remove_class(const qn::NetworkStruct< T > &sn, std::size_t cls)
The model without class cls (1-based), leaving sn untouched.
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
A node of the network.
std::vector< int > routing_param
The scalar parameter of a parameterized dispatcher, per class: the d of a power-of-d (SQ) choice.
std::vector< std::map< std::size_t, double > > routing_weights
The per-destination weights of a WRROBIN dispatcher, per class: a map from 1-based destination NODE i...
std::vector< RoutingStrategy > routing
sn.routing, per class.
One station of the network.
std::vector< Distrib< T > > orbit_impatience
Queue.setOrbitImpatience(class, dist): abandonment from the RETRIAL ORBIT, which is a different popul...
std::vector< T > jdscalingpeak
sn.jdscalingpeak for this station: the declared peak joint-dependent scaling per class.
std::vector< BalkingParam > balking
std::vector< T > batch_reject
Queue.setBatchRejectProbability: per-class rejection of a whole batch.
std::vector< Distrib< T > > patience
Queue.setPatience(class, dist): the abandonment timer of a WAITING job, with impatience[r] naming whi...
std::vector< std::size_t > server_parallelism
Queue.setServerParallelism(class, n): the servers a job seizes for the whole of its service,...
std::vector< int > droprule
Per-class blocking rule as an INT, with 0 meaning "not set".
std::vector< T > cdscalingpeak
sn.cdscalingpeak for this station: the DECLARED peak rate scaling per class, empty when the station i...
std::vector< std::size_t > marked_classes
Source.markedClasses: the 1-based class of each mark of an MMAP arrival.
std::vector< lang::ImpatienceType > impatience
std::vector< T > schedparam
sn.schedparam, per class: the DPS / GPS weight, or the SEPT / LEPT rank.
std::vector< double > classcap
Per-class buffer from setChainCapacity; infinite where unset.
std::vector< lang::DepartureDiscipline > departure_discipline
Place.departureDiscipline, per class.
std::vector< Distrib< T > > arrival_batch
Source.setArrivalBatch(class, dist): the batch-size law released at each arrival epoch.
std::vector< bool > immfeed
Node-level immediate feedback, per class; empty when the station sets none.
std::vector< ServerType > server_types