LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_analyzer.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_ANALYZER_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_ANALYZER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_ctmc_analyzer.m` and the parts of `@@SolverCTMC/runAnalyzer.m`
12 * that surround one solve: the method gate, the open-model CUTOFF, the state
13 * space and synchronization construction, the stationary solve with its
14 * reducible-generator handling, and the mapping onto the AvgTable columns.
15 *
16 * WHAT THE CUTOFF IS, AND WHY IT IS NOT A TOLERANCE. A closed class carries its
17 * own population, so its lattice is finite. An open class does not, so the
18 * chain is infinite and the reference TRUNCATES it at `options.cutoff` jobs per
19 * class. The answer is therefore the exact stationary law of a DIFFERENT chain,
20 * one the truncation defined, and it converges to the model's only as the
21 * cutoff grows. That is why the reference prints a warning on every open model
22 * and why the banner here reports the cutoff it used: a CTMC number for an open
23 * model is not a number without it.
24 *
25 * REDUCIBILITY. `space_generator` enumerates every state the ENCODING admits,
26 * and the dynamics need not reach all of them; the generator is then reducible
27 * and pi is not unique. The reference resolves this by keeping the weakly
28 * connected component of the INITIAL state, and falling back to the largest
29 * component when the initial state is not in the space at all. Solving the whole
30 * generator instead -- which is what `ctmc_solve` does on its own, splitting
31 * per component and renormalizing -- spreads mass over states the model can
32 * never occupy and moves every reported mean.
33 */
34
36#include <algorithm>
37#include <cmath>
38#include <cstddef>
39#include <limits>
40#include <map>
41#include <string>
42#include <vector>
43
50#include "line/lang/qn/fj_tag.h"
54#include "line/lang/qn/state.h"
63#include "line/util/error.h"
64#include "line/util/matrix.h"
65
66namespace line {
67namespace ctmc {
68
69/**
70 * `SolverOptions.m:107`: the per-class state-space cutoff SolverCTMC defaults an
71 * open or mixed model to. It is a SOLVER default, not a fallback computed from
72 * the model, and the reference's `ceil(6000^(1/(M*K)))` is reached only from an
73 * explicitly infinite request. See `resolve_cutoff`.
74 */
75constexpr std::size_t CTMC_DEFAULT_CUTOFF = 10;
76
77/**
78 * The SolverCTMC knobs this port honours.
79 *
80 * `cutoff` is per-class when `cutoff_vec` is given and uniform otherwise; a
81 * negative `cutoff` means "not given" and takes the reference's solver default
82 * `CTMC_DEFAULT_CUTOFF`, while an infinite one takes its automatic value,
83 * `ceil(6000^(1/(M*K)))`.
84 */
86 std::string method = "default";
87 /**
88 * Keep the per-synchronization EVENT FILTRATION alongside Q.
89 *
90 * Off by default because it costs one n x n matrix per synchronization,
91 * which on a model with many routing pairs dwarfs the generator itself. The
92 * response-time CDF needs it, since splitting Q on one event cannot be done
93 * after the contributions have been summed.
94 */
95 bool keep_filtration = false;
96 double cutoff = -1.0; ///< < 0 = not given
97 std::vector<std::size_t> cutoff_vec; ///< per-class override, or empty
98 /**
99 * `options.cutoff` AS A (station x class) MATRIX, or empty.
100 *
101 * The reference accepts a matrix wherever a model needs a different
102 * truncation per station -- oqn_cs_routing writes `[1,1,0;3,3,0;0,0,3]`,
103 * which bounds each queue only in the classes it actually serves. Reducing
104 * that to its per-class maximum enumerates a far larger chain and answers a
105 * different model, so it is carried whole and applied per station in
106 * `space_capacity_c`.
107 */
108 std::vector<std::vector<std::size_t>> cutoff_mat;
109 std::size_t state_max = 3000000; ///< refuse a space larger than this
110 /// `options.force`: downgrade the memory pre-gate's refusal to a warning.
111 bool force = false;
112 /// `options.memorySafetyFraction`: share of available memory a solve may target.
114 /**
115 * `options.config.nonmkvorder`: the phase budget `sn_nonmarkov_toph` spends
116 * on a non-Markovian service law. The reference default is 20, and it is a
117 * real cost here -- every phase multiplies the state space.
118 */
119 std::size_t nonmkv_order = 20;
120 /**
121 * `options.timestep`: the FIXED OUTPUT STEP of a transient analysis.
122 *
123 * <= 0 means adaptive, which is the reference's default and its `[]`. It
124 * changes WHERE the solution is reported and not how it is computed: the
125 * integrator takes the same steps either way, and the grid points are read
126 * off its interpolant. A caller comparing two transients needs the second,
127 * because two adaptive solves land on different time vectors.
128 */
129 double timestep = -1.0;
130 /**
131 * `options.config.transient_method`: "ode" (the default) integrates the
132 * forward equation, "fau" marches fast adaptive uniformization
133 * (`mc::ctmc_fau`) over the output grid.
134 *
135 * It is a config key rather than a value of `method` because it changes no
136 * stationary answer -- it is the transient path only -- and because the
137 * reference's valid-method list is enumerated by its sanity harness, which
138 * then wants a recorded baseline per method.
139 */
140 std::string transient_method = "ode";
141 /**
142 * `options.config.fau_epsilon`: total probability mass the whole grid may
143 * discard under "fau". It is divided by the number of steps, each step
144 * removing mass and none putting any back, so the accumulated defect stays
145 * below it.
146 */
147 double fau_epsilon = 1e-6;
148 /// `options.config.fau_delta`: occupancy below which a state is dropped.
149 double fau_delta = 1e-12;
150 /// `options.config.fau_ngrid`: output grid size when `timestep` is unset.
151 std::size_t fau_ngrid = 100;
152 /**
153 * `options.config.chain_aggregation`: solve the CHAIN-AGGREGATED model.
154 *
155 * The state space grows with the per-class populations, so collapsing every
156 * chain onto a single class is the standard way to make an otherwise
157 * intractable multiclass model solvable: `api::sn_aggregate_chains` builds
158 * the collapsed model and `mva::sn_deaggregate_chain_results` maps the
159 * chain-level metrics back through alpha. EXACT on a product-form model, an
160 * approximation otherwise, since one aggregate service law replaces the
161 * per-class ones. Off by default: a caller who needs the exact multiclass
162 * answer must pay the state space, not discover the trade after the fact.
163 */
164 bool chain_aggregation = false;
165
166 /**
167 * `options.config.transform='lc'`: solve by LOAD CONCEALMENT.
168 *
169 * Birman-Kogan Algorithm 2 as a model transformation: each chain is solved
170 * on its own against the residual capacity the others leave it, with the
171 * single-chain subproblem a real struct this analyzer solves. Distinct from
172 * the `pfqn_bklc` KERNEL, which sweeps a demand matrix and stays the fast
173 * path; see `tr::transform_solve_lc`.
174 */
175 bool load_concealment = false;
176
177 /** Sweep cap for an iterated transformation; the kernel's own default. */
178 std::size_t transform_iter_max = 1000;
179 /**
180 * `options.config.fes_stations`: 1-BASED station indices to collapse into a
181 * flow-equivalent server before the chain is enumerated. Empty by default.
182 *
183 * The collapsed stations' own metrics are recovered by conditioning on the
184 * FES population, so the table still names every station of the model the
185 * caller built; see `solver_ctmc_fes_aggregation`.
186 */
187 std::vector<std::size_t> fes_stations;
188};
189
190/**
191 * Everything one CTMC solve produces.
192 *
193 * `chain` is the generator RESTRICTED to the component that was kept, together
194 * with the state space and the per-state arrival and departure rates on it. The
195 * rates are carried rather than recomputed because Q has already summed every
196 * synchronization's contribution into one entry, and a per-class rate cannot be
197 * recovered from that sum afterwards.
198 */
199template <class T>
202 std::vector<T> pi; ///< stationary distribution over chain.space
204 std::vector<std::size_t> cutoff; ///< the per-class cutoff actually used
205 std::string actualmethod = "default";
206 /**
207 * `fjclassmap` when the model was fork-join, empty otherwise: the ORIGINAL
208 * class of each auxiliary sibling class of the AUGMENTED struct that `chain`
209 * and `pi` are indexed by. `avg` is already folded back onto the original
210 * classes, so this is what relates the two index spaces.
211 */
212 std::vector<std::size_t> fjclassmap;
213 /**
214 * What the chain says about the model's Cache nodes: exact, since the hit
215 * and miss shares are read off the stationary law rather than approximated.
216 * Empty on a model with no Cache.
217 */
219 /** Set when the chain is a reducible mixture solved from an invented seed. */
220 std::string warning;
221};
222
223/**
224 * Port of `SolverCTMC.listValidMethods`.
225 *
226 * `exact` is an explicit ALIAS for the default state-space path: it pins the
227 * intent at the call site so an example or test cannot be re-baselined by a
228 * later change of what `default` selects. It must stay behaviourally identical
229 * to `default` -- nothing below branches on the name -- and that equivalence is
230 * the point of the alias.
231 *
232 * `mdd` and the `cftp` pair never build the explicit generator, so they are
233 * served by `solver_ctmc_mdd_analyzer` and `solver_ctmc_cftp` and return before
234 * this file's state-space machinery is reached; they appear here only because
235 * this is the list the gate below is read against.
236 */
237inline std::vector<std::string> list_valid_methods() {
238 return {"default", "exact", "gpu", "mdd", "cftp", "cftp.approx"};
239}
240
241/** True for a method whose analyzer is not the explicit-generator one. */
242inline bool is_stateless_method(const std::string& method) {
243 return method == "mdd" || method == "cftp" || method == "cftp.approx";
244}
245
246/**
247 * Port of `runAnalyzerChecks`' method gate.
248 *
249 * `gpu` IS ACCEPTED AND FALLS BACK, because that is what the reference does.
250 * `ctmc_solve.m:224-238` wraps the `gpuArray` solve in a try/catch and, when the
251 * GPU is absent or the solve throws, warns "GPU either not available or
252 * execution failed. Switching to default method." and runs the same `Qnnz' \
253 * bnnz` the default takes. A host with no Parallel Computing Toolbox therefore
254 * gets the EXACT answer from `SolverCTMC(model,'gpu')`, and refusing it here
255 * made a model MATLAB answers unanswerable under `lang='cpp'`. The fallback is
256 * reported through `CtmcSolution::warning` rather than silently, so a caller
257 * still learns the named backend did not run.
258 *
259 * `mdd` and `cftp` ARE refused here, because reaching THIS analyzer with one of
260 * those names means the caller routed a generator-free method into the
261 * generator path: the answer would be the enumerated one under a reported
262 * method that never ran. Their own entry points do not call this gate.
263 */
264inline void check_method(const std::string& method) {
265 const std::vector<std::string> valid = list_valid_methods();
266 if (std::find(valid.begin(), valid.end(), method) == valid.end())
267 throw UnsupportedError("SolverCTMC: the '" + method +
268 "' method is unsupported by this solver");
269 if (is_stateless_method(method))
270 throw UnsupportedError("SolverCTMC: the '" + method +
271 "' method does not enumerate a state space and is served by its "
272 "own analyzer, not by solver_ctmc_analyzer");
273}
274
275/** The reference's fallback warning, or empty when the method needs none. */
276inline std::string method_fallback_warning(const std::string& method) {
277 if (method != "gpu") return std::string();
278 return "ctmc_solve: GPU either not available or execution failed. Switching to default "
279 "method.";
280}
281
282/**
283 * Refuse the constructs this port generates a chain for but does not MODEL.
284 *
285 * WHY A GATE AND NOT A TODO. A construct this port generates a chain for but does
286 * not MODEL comes back as a complete, plausible AvgTable computed from a chain
287 * that is not the model's, and a caller has no way to tell. Where the reference
288 * declares the construct supported, silence here would show two different answers
289 * with no indication which is wrong.
290 *
291 * As of this change the list is empty of structural gaps: fork-join goes through
292 * `fj_tag`, class and joint dependence through `cd_factor`, and BAS through the
293 * blocked marker. What remains are the DECLARATION defects below -- a dependence
294 * handle with no peak -- plus the refusals stated by name elsewhere
295 * (`getSymbolicGenerator`, a matrix-exponential `getProb`, `getCdfRespT` on an
296 * open model, and every region rule but DROP and WAITQ).
297 */
298template <class T>
300 // Immediate feedback is a SAMPLE-PATH property: the fed-back job keeps the
301 // server it just used instead of releasing it and re-queueing. The
302 // reference expresses that in `State.afterEvent` through its
303 // `immfeed_selfloop` argument; nothing here does, so the generator would
304 // route the completion out and back like any other visit and return a
305 // confident answer for a model with no feedback in it. Unlike SolverMVA and
306 // SolverNC, which WARN because a mean-value argument cannot express the
307 // held server either way, an exact chain either has the arc or is wrong.
308 if (sn.has_immediate_feedback())
309 throw UnsupportedError(
310 "SolverCTMC: immediate feedback (setImmediateFeedback) is not ported. The reference "
311 "keeps the server across the fed-back service through State.afterEvent's immfeed "
312 "self-loop, which this port's event generator has no arc for, so the chain would "
313 "release and re-queue the job instead; use SolverMVA or SolverNC, which warn and "
314 "approximate it");
315 // Server breakdowns are the same shape of gap, and they became REACHABLE on
316 // 2026-08-15 when `sn.breakdownparam` was added for the LDES engine. The
317 // reference's SolverCTMC does model them -- `State.afterEventStation`'s
318 // FAILURE and REPAIR branches, on the shared trailing marker column that
319 // `refreshLocalVars` reserves -- and `ctmc_feature_set` transcribes that
320 // declaration, so without this the chain would be built with the server
321 // permanently up and the answer would look ordinary.
322 if (sn.has_breakdown())
323 throw UnsupportedError(
324 "SolverCTMC: server breakdowns (setBreakdown) are not ported. The reference carries "
325 "the up/down status in the station's trailing local-state column and fires FAILURE "
326 "and REPAIR against it (State.afterEventStation); this port allocates that column for "
327 "a cache, a polling controller or the BAS blocked marker only, so the chain would be "
328 "built as though the server never failed; use SolverLDES, whose native engine "
329 "simulates the outage");
330 // Class dependence requires a DECLARED peak, and unlike the handle itself
331 // the peak cannot be derived: `cd_factor` scales the generator correctly
332 // without it, but the utilization column would then silently fall back to the
333 // busy-server probability, which at a station whose beta emulates extra
334 // servers is not a utilization. `getLimitedClassDependencePeak` makes it
335 // mandatory in the reference, so a missing peak is a model defect.
336 for (std::size_t i = 0; i < sn.stations.size(); ++i) {
337 if (sn.stations[i].cdscaling && sn.stations[i].cdscalingpeak.empty())
338 throw InputError(
339 "SolverCTMC: station '" + sn.stations[i].name +
340 "' declares class-dependent service without a peak rate. Utilization at a "
341 "class-dependent station is reported as T/mu/peak, so pass the peak to "
342 "setClassDependence");
343 if (sn.stations[i].jdscaling && sn.stations[i].jdscalingpeak.empty())
344 throw InputError(
345 "SolverCTMC: station '" + sn.stations[i].name +
346 "' declares joint-dependent service without a peak rate; pass the peak to "
347 "setJointDependence");
348 }
349 // BAS / BBS / RSRD are SOLVED, not refused, and the two tiers are the
350 // reference's, not a shortcut here:
351 //
352 // BAS gets the blocked marker. `refresh_bas_blocking` reserves the column,
353 // the generator emits the become-blocked edge, the departure handler clears
354 // it at 1e7 and `solver_ctmc_avg_from_pi` moves the held job to its
355 // destination. The chain is exact.
356 //
357 // BBS and RSRD get the DISABLED DEPARTURE only -- `arrival_is_lost` returns
358 // false, so the completion that would move the job is simply not generated
359 // until room frees. `declaresBlockedMarker` tests BAS alone, so the
360 // reference does the same and does not distinguish repetitive service from
361 // a random redraw of the destination. The server freezes with the job in
362 // it, which is the right occupancy but not the right sample path for RSRD.
363 //
364 // `solver_ctmc_waitq` still refuses all three on ITS path: the WAITQ
365 // generator augments the state with a per-region token FIFO and would have to
366 // carry the marker through that augmentation as well.
367}
368
369namespace analyzer_detail {
370
371/**
372 * The per-class cutoff of `@@SolverCTMC/runAnalyzer.m`.
373 *
374 * A closed class never consults it; an open one takes the caller's value.
375 *
376 * THE DEFAULT IS 10, NOT THE 6000-STATE BUDGET. `SolverOptions.m:107` sets
377 * `options.cutoff = 10` for SolverCTMC, so `runAnalyzer`'s
378 * `ceil(6000^(1/(M*K)))` fallback is reached only when the caller explicitly
379 * asks for an INFINITE cutoff, which is the one value the truncation cannot
380 * honour. Taking the fallback as the default instead put this port one lattice
381 * step below the reference on every open model and the answers differed in the
382 * third digit with nothing to show for it: on the Exp(0.5) -> three-FCFS-queue
383 * tandem the budget gives 9 and QLen at Q1 came out 0.9437940 against the
384 * reference's 0.9446411, while cutoff 10 reproduces it to every digit.
385 */
386template <class T>
387std::vector<std::size_t> resolve_cutoff(const NetworkStruct<T>& sn, const CtmcOptions& opt) {
388 const std::size_t M = sn.nstations, K = sn.nclasses;
389 std::vector<std::size_t> cut(K, 0);
390 bool any_open = false;
391 for (std::size_t k = 0; k < K; ++k)
392 if (!std::isfinite(sn.njobs()[k])) any_open = true;
393 if (!any_open) return cut;
394
395 if (!opt.cutoff_mat.empty()) {
396 // `spaceGenerator.m`: the LATTICE height of an open class is the largest
397 // bound any station grants it, `max(capacityc(:,r))`; the per-station
398 // entries then bound each node individually.
399 if (opt.cutoff_mat.size() != M)
400 throw InputError("SolverCTMC: the cutoff matrix must have one row per station");
401 for (std::size_t k = 0; k < K; ++k) {
402 if (std::isfinite(sn.njobs()[k])) continue;
403 for (std::size_t i = 0; i < M; ++i) {
404 if (opt.cutoff_mat[i].size() != K)
405 throw InputError(
406 "SolverCTMC: the cutoff matrix must have one column per class");
407 cut[k] = std::max(cut[k], opt.cutoff_mat[i][k]);
408 }
409 }
410 return cut;
411 }
412 if (!opt.cutoff_vec.empty()) {
413 if (opt.cutoff_vec.size() != K)
414 throw InputError("SolverCTMC: the per-class cutoff must have one entry per class");
415 return opt.cutoff_vec;
416 }
417 std::size_t c;
418 if (opt.cutoff > 0 && std::isfinite(opt.cutoff)) {
419 c = static_cast<std::size_t>(opt.cutoff);
420 } else if (opt.cutoff < 0) {
422 } else {
423 const double e = 1.0 / static_cast<double>(M * K);
424 c = static_cast<std::size_t>(std::ceil(std::pow(6000.0, e)));
425 if (c < 1) c = 1;
426 }
427 for (std::size_t k = 0; k < K; ++k)
428 if (!std::isfinite(sn.njobs()[k])) cut[k] = c;
429 return cut;
430}
431
432/**
433 * Port of the `maxPending` rule of `State.spaceGeneratorNodes`.
434 *
435 * Block B of a delayed-hit cache counts the secondary requests merged onto an
436 * in-flight fetch, and an EXACT solver has to enumerate it, so it needs a
437 * truncation level: the closed population bounds it where there is one, the
438 * state-space cutoff where the model is open. `after_event_cache` reads the same
439 * field and REFUSES a merge past the level rather than landing in a state the
440 * enumeration does not hold -- which is why the two must be set together.
441 *
442 * @return false when no cache carries a retrieval system, leaving `out` untouched
443 */
444template <class T>
445bool set_retrieval_truncation(const NetworkStruct<T>& sn, const std::vector<std::size_t>& cutoff,
446 NetworkStruct<T>& out) {
447 bool any = false;
448 for (typename std::map<std::size_t, qn::CacheParam<T>>::const_iterator ci =
449 sn.nodeparam.begin();
450 ci != sn.nodeparam.end(); ++ci)
451 if (ci->second.retrieval_capacity > 0 && !ci->second.retrieval_classes.empty()) any = true;
452 if (!any) return false;
453 long lvl = 0;
454 if (sn.nclosedjobs() > 0) {
455 lvl = static_cast<long>(sn.nclosedjobs()) - 1;
456 } else {
457 std::size_t mx = 0;
458 for (std::size_t k = 0; k < cutoff.size(); ++k) mx = std::max(mx, cutoff[k]);
459 lvl = static_cast<long>(mx) - 1;
460 }
461 if (lvl < 0) lvl = 0;
462 out = sn;
463 for (typename std::map<std::size_t, qn::CacheParam<T>>::iterator ci = out.nodeparam.begin();
464 ci != out.nodeparam.end(); ++ci)
465 if (ci->second.retrieval_capacity > 0) ci->second.max_pending_retrieval = lvl;
466 return true;
467}
468
469/**
470 * Port of `solver_ctmc.m:47-62`: a G-network SIGNAL NEVER RESIDES at a station,
471 * so its per-class capacity there is zero. A REPLY signal is exempt -- it
472 * completes a synchronous call and queues like an ordinary job.
473 *
474 * WHY THE LATTICE CANNOT DISCOVER THIS ON ITS OWN, and why leaving it undiscovered
475 * is not merely slow. `space_generator` walks every marginal admitted by
476 * `classcap` and, at an FCFS-family station, enumerates the ORDERED buffer of each
477 * one, so an uncapped signal class multiplies the buffer alphabet: a cutoff of c
478 * over two classes gives on the order of 2^c sequences where the model has c+1
479 * states. Every one of them is unreachable -- the arrival handler annihilates the
480 * signal instead of seating it -- so the answer is right and only the cost is
481 * wrong, which is exactly what makes it dangerous: it is invisible in the results.
482 *
483 * IT ALSO REPAIRS THE MEMORY GATE, which is the part that turns this from a
484 * slowdown into a kill. `ctmc_state_space_logsize` already drops signal classes
485 * from its buffered set (ctmc_state_space_logsize.h:176), so without this the gate
486 * sizes the SMALL space and clears a generation of the large one. Measured on the
487 * `test_ctmc_signal_util` G-network at the cutoff of 30 its tests ask for, the
488 * unfixed enumeration was OOM-killed at 43.6 GB resident.
489 *
490 * @return false when the model declares no annihilating signal, leaving `out` untouched
491 */
492template <class T>
493bool annihilate_signal_capacity(const NetworkStruct<T>& sn, NetworkStruct<T>& out) {
494 std::vector<bool> annihilated(sn.nclasses, false);
495 bool any = false;
496 for (std::size_t k = 0; k < sn.nclasses && k < sn.issignal.size(); ++k) {
497 if (!sn.issignal[k]) continue;
498 if (k < sn.signaltype.size() && sn.signaltype[k] == lang::SignalType::REPLY) continue;
499 annihilated[k] = true;
500 any = true;
501 }
502 if (!any) return false;
503 out = sn;
504 for (std::size_t i = 0; i < out.stations.size() && i < out.classcap.size(); ++i) {
505 // The Source is where a signal is BORN, so its capacity there is what the
506 // arrival stream is drawn from and must not be zeroed.
507 if (out.stations[i].sched == SchedStrategy::EXT) continue;
508 for (std::size_t k = 0; k < annihilated.size() && k < out.classcap[i].size(); ++k)
509 if (annihilated[k]) out.classcap[i][k] = 0.0;
510 }
511 return true;
512}
513
514/**
515 * Port of the cache write-back of `solver_ctmc_analyzer.m:333-450`.
516 *
517 * WHAT A CACHE'S HIT AND MISS SHARES ARE, ON A CHAIN. A read leaves the cache in
518 * its configured hit class or its miss class and in no other, so the two
519 * departure rates of those classes at the cache node ARE the hit and the miss
520 * flows, and normalizing by their sum gives the shares. Nothing here reads the
521 * routing matrix, whose cache entries `refresh_routing` resolved to a uniform
522 * split that is not the answer.
523 *
524 * THE DELAYED HIT IS A TRANSITION REWARD, NOT A STATE REWARD, and that is the
525 * whole reason this is not two lines. A merged secondary request departs in the
526 * HIT class, so the hit-class rate above is (true hits + delayed hits) and
527 * something has to split it. A fetch of item i completes on exactly the
528 * transitions that clear block A bit i, and each such transition releases the
529 * block-B count of item i, so the delayed rate is the pi-weighted sum of
530 * (count held) x (rate out on those transitions). The alternative identity
531 * lambda_i * phi_i is only PASTA-exact and would be wrong on a closed model.
532 *
533 * `phi` and `d1` are ordinary state rewards of the same two blocks and give the
534 * per-item DelayedHitQLen columns; `latency` is left empty, as the reference
535 * leaves `actualresidt` NaN.
536 */
537template <class T>
538solvers::CacheMetrics<T> cache_metrics(const NetworkStruct<T>& sn, const CtmcResult<T>& r,
539 const std::vector<T>& pi) {
540 solvers::CacheMetrics<T> out =
541 solvers::cache_metrics_of(sn, std::vector<T>(), std::vector<T>(), std::vector<T>(),
542 std::vector<T>(), Matrix<T>(), Matrix<T>(), std::vector<T>());
543 if (out.caches.empty() || r.space.empty()) return out;
544 const std::size_t K = sn.nclasses, ns = r.space.size();
545 const double dnan = std::numeric_limits<double>::quiet_NaN();
546
547 for (std::size_t c = 0; c < out.caches.size(); ++c) {
548 solvers::CacheNodeMetrics<T>& m = out.caches[c];
549 const std::size_t isf = sn.stateful_index(m.node);
550 if (isf == 0) continue;
551 const qn::CacheParam<T>& cp = sn.nodeparam.find(m.node)->second;
552
553 std::vector<double> tn(K, 0.0);
554 for (std::size_t s = 0; s < ns && s < pi.size(); ++s) {
555 const double p = num_traits<T>::to_double(pi[s]);
556 if (p == 0) continue;
557 if (isf - 1 >= r.dep_rates[s].size()) continue;
558 for (std::size_t k = 0; k < K && k < r.dep_rates[s][isf - 1].size(); ++k)
559 tn[k] += p * num_traits<T>::to_double(r.dep_rates[s][isf - 1][k]);
560 }
561
562 const std::size_t n = cp.nitems;
563 std::size_t tcc = 0;
564 for (std::size_t l = 0; l < cp.itemcap.size(); ++l)
565 if (cp.itemcap[l] > 0) tcc += static_cast<std::size_t>(cp.itemcap[l]);
566 std::vector<std::size_t> rcl, rci, rco;
567 if (cp.retrieval_capacity > 0) qn::cache_retrieval_class_map(cp, rcl, rci, rco);
568 const std::size_t lvw = r.space[0].local[isf - 1].size();
569 const bool retr = !rcl.empty() && lvw >= tcc + n + rcl.size();
570 std::vector<double> drate(K, 0.0);
571
572 // TIME-STATIONARY per-item occupancy of each list. The contents block of the
573 // local vector holds the item index resident in each cache position, so
574 // P(item i is held by list l) is a state reward of pi. This is the
575 // TIME-WEIGHTED law, the counterpart of the EMBEDDED (per-request) one the
576 // NC/MVA cache algorithms return; the two coincide only under PASTA.
577 // The local row is [per-class counts | contents | block A | block B], so the
578 // contents block sits at the offset the trailing retrieval blocks leave.
579 const std::size_t tail = cp.retrieval_capacity > 0 ? n + rcl.size() : 0;
580 if (n > 0 && tcc > 0 && lvw >= tcc + tail) {
581 const std::size_t coff = lvw - (tcc + tail);
582 const std::size_t h = cp.itemcap.size();
583 Matrix<T> ip(n, h + 1);
584 std::vector<double> acc(n * h, 0.0);
585 std::vector<char> present(n, 0);
586 for (std::size_t s = 0; s < ns && s < pi.size(); ++s) {
587 const double p = num_traits<T>::to_double(pi[s]);
588 if (p == 0) continue;
589 const std::vector<T>& lv = r.space[s].local[isf - 1];
590 std::size_t off = coff;
591 for (std::size_t l = 0; l < h; ++l) {
592 const std::size_t cap =
593 cp.itemcap[l] > 0 ? static_cast<std::size_t>(cp.itemcap[l]) : 0;
594 std::fill(present.begin(), present.end(), 0);
595 for (std::size_t q = 0; q < cap; ++q) {
596 const long it = static_cast<long>(num_traits<T>::to_double(lv[off + q]));
597 if (it >= 1 && static_cast<std::size_t>(it) <= n)
598 present[static_cast<std::size_t>(it) - 1] = 1;
599 }
600 for (std::size_t i = 0; i < n; ++i)
601 if (present[i]) acc[i * h + l] += p;
602 off += cap;
603 }
604 }
605 for (std::size_t i = 0; i < n; ++i) {
606 double miss = 1.0;
607 for (std::size_t l = 0; l < h; ++l) {
608 const double v = acc[i * h + l];
609 ip(i, l + 1) = num_traits<T>::from_double(v);
610 miss -= v;
611 }
612 ip(i, 0) = num_traits<T>::from_double(miss);
613 }
614 m.itemprob = ip;
615 }
616
617 if (retr) {
618 const std::size_t aoff = lvw - (n + rcl.size()), boff = aoff + n;
619 std::vector<double> phi(n, 0.0), d1(n, 0.0);
620 for (std::size_t s = 0; s < ns && s < pi.size(); ++s) {
621 const double p = num_traits<T>::to_double(pi[s]);
622 if (p == 0) continue;
623 const std::vector<T>& lv = r.space[s].local[isf - 1];
624 for (std::size_t i = 0; i < n; ++i)
625 if (num_traits<T>::to_double(lv[aoff + i]) != 0) phi[i] += p;
626 for (std::size_t j = 0; j < rcl.size(); ++j)
627 d1[rci[j] - 1] += p * num_traits<T>::to_double(lv[boff + j]);
628 }
629 m.delayedhitqlen.assign(n, num_traits<T>::from_int(0));
630 m.delayedhitqlenfull.assign(n, num_traits<T>::from_int(0));
631 for (std::size_t i = 0; i < n; ++i) {
632 m.delayedhitqlen[i] = num_traits<T>::from_double(d1[i]);
633 m.delayedhitqlenfull[i] = num_traits<T>::from_double(d1[i] + phi[i]);
634 }
635 for (std::size_t s = 0; s < ns && s < pi.size(); ++s) {
636 const double p = num_traits<T>::to_double(pi[s]);
637 if (p == 0) continue;
638 const std::vector<T>& lv = r.space[s].local[isf - 1];
639 for (std::size_t j = 0; j < rcl.size(); ++j) {
640 const std::size_t i = rci[j] - 1;
641 const double held = num_traits<T>::to_double(lv[boff + j]);
642 if (held <= 0 || num_traits<T>::to_double(lv[aoff + i]) == 0) continue;
643 double completes = 0.0;
644 for (std::size_t b = 0; b < ns; ++b) {
645 if (b == s) continue;
646 const double q = num_traits<T>::to_double(r.Q(s, b));
647 if (q == 0) continue;
648 if (num_traits<T>::to_double(r.space[b].local[isf - 1][aoff + i]) == 0)
649 completes += q;
650 }
651 if (rco[j] - 1 < K) drate[rco[j] - 1] += p * held * completes;
652 }
653 }
654 }
655
656 m.hitprob.assign(K, num_traits<T>::from_double(dnan));
657 m.missprob.assign(K, num_traits<T>::from_double(dnan));
658 if (retr) m.delayedprob.assign(K, num_traits<T>::from_double(dnan));
659 for (std::size_t k = 0; k < K; ++k) {
660 if (k >= cp.hitclass.size() || k >= cp.missclass.size()) continue;
661 const std::size_t h = cp.hitclass[k], mi = cp.missclass[k];
662 if (h == 0 || mi == 0 || h > K || mi > K) continue;
663 const double denom = tn[h - 1] + tn[mi - 1];
664 if (denom <= 0) continue;
665 // Capped at the hit-class flow: the two are the same measurement of
666 // the same transitions, and a floating-point excess would report a
667 // negative true-hit share rather than a zero one.
668 const double d = retr ? std::min(drate[k], tn[h - 1]) : 0.0;
669 m.hitprob[k] = num_traits<T>::from_double((tn[h - 1] - d) / denom);
670 m.missprob[k] = num_traits<T>::from_double(tn[mi - 1] / denom);
671 if (retr) m.delayedprob[k] = num_traits<T>::from_double(d / denom);
672 }
673 }
674 return out;
675}
676
677/**
678 * Strip the reference's leading infinite-population column from a DECLARED
679 * Source row, so a state written by MATLAB, the JAR or native python names a
680 * row this port's enumerated space actually holds.
681 *
682 * The reference writes a Source as `[Inf | one phase block per class]` --
683 * `[Inf 1 0 0 0 0 0]` for a Source generating the first of six classes -- and
684 * this port's `from_marginal_core`, which the state-space walk runs, writes the
685 * phase blocks alone. The two therefore differ by exactly one column, and an
686 * untranslated row is one wider than every row of the space, so no lookup can
687 * match it and the padding rule cannot help: it left-pads a NARROW candidate
688 * and refuses a wide one. `rewardModel_mm1k` came back from its M2C row as
689 * `solver_ctmc_transient_analyzer: the initial state is not contained in the
690 * state space` while the JAR and native-python rows, each reading its own
691 * encoding, passed.
692 *
693 * The phase block is KEPT rather than rebuilt: a Source with a MAP arrival has
694 * several rows in its local space and which one the model declared is the
695 * caller's statement, not a detail to regenerate.
696 */
697template <class T>
698void strip_reference_source_column(const NetworkStruct<T>& sn, std::size_t ind,
699 std::vector<T>& row) {
700 const std::size_t ist = sn.nodes[ind - 1].station;
701 if (ist == 0 || ist > sn.stations.size()) return;
702 if (sn.stations[ist - 1].nodetype != NodeType::Source &&
703 sn.stations[ist - 1].sched != SchedStrategy::EXT)
704 return;
705 std::size_t w = 0;
706 for (std::size_t r = 0; r < sn.nclasses; ++r) w += sn.phasessz_of(ist, r + 1);
707 // Width is the discriminator rather than the leading value: an exact
708 // arithmetic has no infinity, so the reference's marker arrives as the
709 // MaxInt clamp `to_marginal` uses and cannot be tested for.
710 if (row.size() == w + 1) row.erase(row.begin());
711}
712
713/**
714 * The model's default initial state: every closed class's jobs at its reference
715 * station, everything else empty, which is `Network.initDefault`.
716 *
717 * The first row `from_marginal_node` emits for that marginal is taken. It is
718 * used for two things -- seeding the reachable walk, and choosing a connected
719 * component -- and for both any row with the right marginal serves, since rows
720 * sharing a marginal are mutually reachable by construction.
721 *
722 * @return false when some node admits no state at all for that marginal
723 */
724template <class T>
725bool default_init_state(const NetworkStruct<T>& sn, NetState<T>& init) {
726 const std::size_t R = sn.nclasses;
727 const std::vector<std::size_t>& sfn = sn.stateful_nodes;
728 init.local.assign(sfn.size(), std::vector<T>());
729 for (std::size_t f = 0; f < sfn.size(); ++f) {
730 const std::size_t ind = sfn[f];
731 // A DECLARED STATE WINS OVER THE DEFAULT MARKING, which is the whole
732 // point of declaring one: `setState` is used precisely to move the jobs
733 // off their reference stations, and rebuilding the default here would
734 // answer `getProbSysAggr` for a state the caller did not name. The pair
735 // (statespace, stateprior) is what the writers emit and the reader
736 // stores; the FIRST row is the state when the prior is the trivial one,
737 // and a genuine distribution over several rows is not a single initial
738 // state at all, so only the one-row case is taken.
739 const typename std::map<std::size_t, Matrix<T>>::const_iterator sp =
740 sn.statespace.find(ind);
741 if (sp != sn.statespace.end() && sp->second.rows() == 1 && sp->second.cols() > 0) {
742 std::vector<T> row(sp->second.cols());
743 for (std::size_t c = 0; c < sp->second.cols(); ++c) row[c] = sp->second(0, c);
744 strip_reference_source_column(sn, ind, row);
745 // A ROW OF EXACTLY ONE ENTRY PER CLASS IS A MARGINAL, NOT AN
746 // ENCODED ROW, and the two differ wherever a class is DISABLED at
747 // the station. The reference keeps a column for every class
748 // (`sn.phases` is 1 at a class it does not serve), while this port
749 // drops the block outright (`phases_of` is 0), so a Delay serving
750 // one of three classes has a width-3 row there and a width-1 row
751 // here, LEFT-PADDED to the encoding width. Installing the
752 // reference's row verbatim then puts the jobs in another class's
753 // block: on tut06_cache_lru_zipf the single client job landed where
754 // nothing could serve it and the sample path deadlocked at once,
755 // which is what a state nobody can leave looks like. Rebuilding it
756 // through `from_marginal_node_first` is the same construction the
757 // default branch below uses, so the two agree by construction.
758 std::vector<std::size_t> marg(R, 0), mph(R, 1);
759 const std::size_t mist = sn.nodes[ind - 1].station;
760 bool rebuilt = false;
761 if (row.size() == R && mist != 0) {
762 for (std::size_t r = 0; r < R; ++r) {
763 const double v = num_traits<T>::to_double(row[r]);
764 marg[r] = v > 0.0 ? static_cast<std::size_t>(v + 0.5) : 0;
765 mph[r] = sn.phasessz_of(mist, r + 1);
766 }
767 std::vector<T> built;
768 if (from_marginal_node_first(sn, ind, marg, mph, built) && !built.empty()) {
769 init.local[f] = built;
770 rebuilt = true;
771 }
772 }
773 if (!rebuilt) init.local[f] = row;
774 continue;
775 }
776 const std::size_t ist = sn.nodes[ind - 1].station;
777 std::vector<std::size_t> nmarg(R, 0), ph(R, 1);
778 if (ist != 0) {
779 for (std::size_t r = 0; r < R; ++r) ph[r] = sn.phasessz_of(ist, r + 1);
780 if (sn.stations[ist - 1].nodetype == NodeType::Source) {
781 // The infinite reservoir: one job per class in service, and only
782 // for the classes the Source generates -- the same guard
783 // `space_generator` needs. Without it this returns false on any
784 // model with a downstream-only class, the initial state is
785 // reported as absent, and the reducible-component selection
786 // silently falls back to "largest component": a plausible answer
787 // for the wrong chain.
788 for (std::size_t r = 0; r < R; ++r)
789 if (!sn.disabled[ist - 1][r]) nmarg[r] = 1;
790 } else {
791 for (std::size_t r = 0; r < R; ++r) {
792 const double nj = sn.njobs()[r];
793 if (std::isfinite(nj) && sn.classes[r].refstat == ist)
794 nmarg[r] = static_cast<std::size_t>(nj);
795 }
796 // A PLACE'S DECLARED MARKING IS THE INITIAL STATE, and it is not
797 // derivable from a class population: an OPEN class has none, so
798 // every Place of an open SPN started empty however many tokens
799 // the model put there. On `spn_open_sevenplaces` that dropped
800 // P1's two tokens and P5's one; T4 needs a P5 token to fire, so
801 // the marking never moved past P3 and the chain reported Tput 0
802 // with P3 pinned at the cutoff. `state_initial_occupancy` is the
803 // same accessor `space_capacity_c` sizes the lattice with, so the
804 // two agree on what the model declared.
805 for (std::size_t r = 0; r < R; ++r) {
806 const std::size_t m0 = qn::state_initial_occupancy(sn, ind, r);
807 if (m0 > nmarg[r]) nmarg[r] = m0;
808 }
809 }
810 }
811 if (!from_marginal_node_first(sn, ind, nmarg, ph, init.local[f])) return false;
812 }
813 return true;
814}
815
816/** The index in `space` of the default initial state, or npos. */
817template <class T>
818std::size_t init_state_index(const NetworkStruct<T>& sn, const std::vector<NetState<T>>& space) {
819 const std::size_t npos = static_cast<std::size_t>(-1);
820 if (space.empty()) return npos;
821 NetState<T> init;
822 if (!default_init_state(sn, init)) return npos;
823 for (std::size_t f = 0; f < init.local.size(); ++f) {
824 // Every state of a node carries the node's widest row, left-padded with
825 // zeros; a candidate built at the natural width would never match.
826 const std::size_t w = space[0].local[f].size();
827 if (init.local[f].size() > w) return npos;
828 if (init.local[f].size() < w)
829 init.local[f].insert(init.local[f].begin(), w - init.local[f].size(),
830 num_traits<T>::from_int(0));
831 }
832 const std::vector<double> key = ctmc_detail::state_key(init);
833 for (std::size_t s = 0; s < space.size(); ++s)
834 if (ctmc_detail::state_key(space[s]) == key) return s;
835 return npos;
836}
837
838/**
839 * The initial DISTRIBUTION over `space`: the product of the declared per-node
840 * priors, or a point mass on the default initial state where none is declared.
841 *
842 * Port of the initial-state loop of `@@SolverCTMC/runAnalyzer.m`, which walks
843 * the cartesian product of the per-node state spaces, weights each combination
844 * by the product of its per-node priors, integrates the forward equation once
845 * per combination and SUMS the trajectories with those weights. Every quantity
846 * the transient analyzer reports is linear in pi(t), and pi(t) is linear in
847 * pi(0), so seeding the mixture and integrating ONCE gives the same answer at a
848 * fraction of the cost -- and on ONE time grid, where the reference has to
849 * interpolate its separate adaptive grids onto their union to add them.
850 *
851 * A combination the enumerated space does not contain is an error rather than a
852 * dropped term: it means the declared space and the reachable one disagree, and
853 * renormalizing over what is left would answer for a different prior.
854 *
855 * @return false when a node admits no state at all, as `default_init_state` does
856 */
857template <class T>
858bool init_state_distribution(const NetworkStruct<T>& sn, const std::vector<NetState<T>>& space,
859 std::vector<T>& pi0) {
860 const std::size_t npos = static_cast<std::size_t>(-1);
861 pi0.assign(space.size(), num_traits<T>::from_int(0));
862 if (space.empty()) return false;
863 NetState<T> base;
864 if (!default_init_state(sn, base)) return false;
865 const std::vector<std::size_t>& sfn = sn.stateful_nodes;
866
867 // Per stateful node: the rows it may start in and their probabilities. A
868 // node with no declared prior contributes its default row alone.
869 std::vector<std::vector<std::vector<T>>> rows(sfn.size());
870 std::vector<std::vector<double>> wts(sfn.size());
871 for (std::size_t f = 0; f < sfn.size(); ++f) {
872 const typename std::map<std::size_t, Matrix<T>>::const_iterator ss =
873 sn.statespace.find(sfn[f]);
874 const typename std::map<std::size_t, std::vector<T>>::const_iterator sp =
875 sn.stateprior.find(sfn[f]);
876 if (ss == sn.statespace.end() || sp == sn.stateprior.end() || ss->second.rows() == 0 ||
877 ss->second.rows() != sp->second.size()) {
878 rows[f].push_back(base.local[f]);
879 wts[f].push_back(1.0);
880 continue;
881 }
882 for (std::size_t r = 0; r < ss->second.rows(); ++r) {
883 const double w = num_traits<T>::to_double(sp->second[r]);
884 if (!(w > 0.0)) continue; // a zero-probability row is not a state to visit
885 std::vector<T> row(ss->second.cols());
886 for (std::size_t c = 0; c < ss->second.cols(); ++c) row[c] = ss->second(r, c);
887 strip_reference_source_column(sn, sfn[f], row);
888 rows[f].push_back(row);
889 wts[f].push_back(w);
890 }
891 if (rows[f].empty()) return false;
892 }
893
894 std::vector<std::size_t> pick(sfn.size(), 0);
895 double total = 0.0;
896 for (;;) {
897 NetState<T> cand = base;
898 double w = 1.0;
899 for (std::size_t f = 0; f < sfn.size(); ++f) {
900 cand.local[f] = rows[f][pick[f]];
901 w *= wts[f][pick[f]];
902 // Every state of a node carries the node's widest row, left-padded
903 // with zeros; a candidate at the natural width would never match.
904 const std::size_t wd = space[0].local[f].size();
905 if (cand.local[f].size() > wd) return false;
906 if (cand.local[f].size() < wd)
907 cand.local[f].insert(cand.local[f].begin(), wd - cand.local[f].size(),
908 num_traits<T>::from_int(0));
909 }
910 std::size_t idx = npos;
911 const std::vector<double> key = ctmc_detail::state_key(cand);
912 for (std::size_t s = 0; s < space.size() && idx == npos; ++s)
913 if (ctmc_detail::state_key(space[s]) == key) idx = s;
914 if (idx == npos) return false;
915 pi0[idx] = T(pi0[idx] + num_traits<T>::from_double(w));
916 total += w;
917
918 std::size_t f = 0;
919 for (; f < sfn.size(); ++f) {
920 if (++pick[f] < rows[f].size()) break;
921 pick[f] = 0;
922 }
923 if (f == sfn.size()) break;
924 }
925 if (!(total > 0.0)) return false;
926 // The declared priors are per node and need not multiply to one; the
927 // reference sums the weighted trajectories without renormalizing, so a
928 // prior that already sums to one is unchanged and one that does not is
929 // reported as the reference reports it.
930 return true;
931}
932
933/** Weakly connected components of the generator, as a per-state label. */
934template <class T>
935std::vector<std::size_t> weak_components(const Matrix<T>& Q, std::size_t& ncomp) {
936 const std::size_t n = Q.rows();
937 const std::size_t npos = static_cast<std::size_t>(-1);
938 std::vector<std::size_t> comp(n, npos);
939 ncomp = 0;
940 for (std::size_t s = 0; s < n; ++s) {
941 if (comp[s] != npos) continue;
942 std::vector<std::size_t> stack{s};
943 comp[s] = ncomp;
944 while (!stack.empty()) {
945 const std::size_t u = stack.back();
946 stack.pop_back();
947 for (std::size_t v = 0; v < n; ++v) {
948 if (comp[v] != npos) continue;
949 // The DIAGONAL is minus the row sum and is nonzero at almost
950 // every state, so it must not be read as an edge to itself.
951 if (v == u) continue;
952 if (num_traits<T>::to_double(Q(u, v)) != 0 ||
953 num_traits<T>::to_double(Q(v, u)) != 0) {
954 comp[v] = comp[u];
955 stack.push_back(v);
956 }
957 }
958 }
959 ++ncomp;
960 }
961 return comp;
962}
963
964/** Restrict a CtmcResult to a subset of its states, keeping their order. */
965template <class T>
966CtmcResult<T> restrict_to(const CtmcResult<T>& r, const std::vector<std::size_t>& wset) {
967 CtmcResult<T> out;
968 out.Q = Matrix<T>(wset.size(), wset.size(), num_traits<T>::from_int(0));
969 for (std::size_t a = 0; a < wset.size(); ++a)
970 for (std::size_t b = 0; b < wset.size(); ++b) out.Q(a, b) = r.Q(wset[a], wset[b]);
971 out.space.reserve(wset.size());
972 out.arv_rates.reserve(wset.size());
973 out.dep_rates.reserve(wset.size());
974 for (std::size_t a = 0; a < wset.size(); ++a) {
975 out.space.push_back(r.space[wset[a]]);
976 out.arv_rates.push_back(r.arv_rates[wset[a]]);
977 out.dep_rates.push_back(r.dep_rates[wset[a]]);
978 }
979 // The filtration is indexed by the SAME rows as Q, so it has to be
980 // restricted with it or every CDF built from it would index the wrong
981 // states. Its diagonal is NOT rebuilt: a filtration entry is a rate the
982 // event contributed, not a generator, and it has no diagonal to speak of.
983 out.filt.reserve(r.filt.size());
984 for (std::size_t f = 0; f < r.filt.size(); ++f) {
985 Matrix<T> F(wset.size(), wset.size(), num_traits<T>::from_int(0));
986 for (std::size_t a = 0; a < wset.size(); ++a)
987 for (std::size_t b = 0; b < wset.size(); ++b) F(a, b) = r.filt[f](wset[a], wset[b]);
988 out.filt.push_back(F);
989 }
990 // The submatrix of a generator is not a generator: the rates that left the
991 // component have to come off the diagonal, or the rows no longer sum to zero
992 // and the stationary solve is of a matrix that is not a chain.
993 make_infgen(out.Q);
994 return out;
995}
996
997} // namespace analyzer_detail
998
999/**
1000 * Build the chain of ONE struct, solve it, reduce it.
1001 *
1002 * The state space is the FULL encoding space, as the reference's
1003 * `options.config.state_space_gen` default of 'default' asks for, so the
1004 * reducible handling below is on the normal path and not an edge case. Two
1005 * exceptions force the reachable walk instead: a global synchronization (an SPN)
1006 * and a fork firing list.
1007 *
1008 * Split out of `solver_ctmc_analyzer` so that the fork-join wrapper can run it on
1009 * the AUGMENTED struct without the gates firing twice.
1010 */
1011template <class T>
1013 const std::vector<qn::FjSync<T>>& fjsync) {
1014 CtmcSolution<T> out;
1015 out.cutoff = analyzer_detail::resolve_cutoff(sn_in, opt);
1016 // A delayed-hit cache is enumerated under a declared merge truncation, and
1017 // the events are held to the same one; every other model walks `sn_in`.
1018 NetworkStruct<T> sn_trunc;
1019 const bool retr = analyzer_detail::set_retrieval_truncation(sn_in, out.cutoff, sn_trunc);
1020 const NetworkStruct<T>& sn_r = retr ? sn_trunc : sn_in;
1021 // A signal class holds no place in a station's buffer, and saying so BEFORE
1022 // the memory gate is what keeps the gate's estimate and the generator's
1023 // enumeration measuring the same space; see `annihilate_signal_capacity`.
1024 NetworkStruct<T> sn_sig;
1025 const bool sig = analyzer_detail::annihilate_signal_capacity(sn_r, sn_sig);
1026 const NetworkStruct<T>& sn = sig ? sn_sig : sn_r;
1027
1028 // THE MEMORY PRE-GATE, `@SolverCTMC/runAnalyzer.m:150-164`. It runs BEFORE
1029 // any generation because `state_max` is enforced inside the generator loop,
1030 // so on its own it enumerates up to three million states before refusing --
1031 // and three million is a fixed count that knows nothing about how much
1032 // memory this host actually has. `mdd` and the `cftp` pair never reach here;
1033 // see the exemption note in ctmc_memory_gate.h, which is load-bearing.
1034 {
1035 mc::CtmcSizeOptions szopt;
1036 szopt.cutoff = opt.cutoff;
1037 const double log_nstates = mc::ctmc_state_space_logsize(sn, szopt);
1038 const mc::CtmcGateResult gate =
1039 mc::ctmc_memory_gate(log_nstates, opt.force, opt.memory_safety_fraction);
1040 if (!gate.ok) throw UnsupportedError(gate.message + " Stopping SolverCTMC.");
1041 }
1042
1043 const std::vector<Sync<T>> sync = refresh_sync(sn);
1044 const std::vector<qn::GlobalSync<T>> gsync = refresh_global_sync(sn);
1045
1046 // AN SPN NEEDS THE REACHABLE WALK, not the lattice enumeration. A firing
1047 // does not conserve the per-chain population, and a Transition's state is
1048 // per-MODE, so no population marginal produces the states in which a mode is
1049 // firing: `from_marginal_node` emits only the all-idle row. Enumerating the
1050 // lattice therefore yields a space every ENABLE lands outside of, and the
1051 // generator comes out empty. The reference forces
1052 // `options.config.state_space_gen = 'reachable'` for exactly this class of
1053 // model; the condition here is the presence of a global synchronization,
1054 // which is the same set.
1055 // A FORK FIRING LIST forces the same choice as an SPN, for the same reason:
1056 // the lattice enumeration walks the per-chain population, and a firing does
1057 // not conserve it -- one parent becomes B siblings in classes the lattice
1058 // gives population 0. The reference sets
1059 // `options.config.state_space_gen = 'reachable'` on every fork-join model.
1060 std::vector<NetState<T>> space;
1061 if (!gsync.empty() || !fjsync.empty()) {
1062 NetState<T> init;
1063 if (!analyzer_detail::default_init_state(sn, init))
1064 throw UnsupportedError(
1065 "SolverCTMC: the model's initial marking admits no state; check the Place "
1066 "populations against the class reference stations");
1067 // THE CUTOFF TRAVELS WITH THE WALK, or an open model never terminates:
1068 // the Source keeps producing and the walk runs to `state_max` instead of
1069 // answering. It is the same bound the lattice arm below applies.
1070 space = reachable_space_generator(sn, init, sync, gsync, opt.state_max, fjsync,
1071 out.cutoff, opt.cutoff_mat);
1072 } else {
1073 space = space_generator(sn, out.cutoff, opt.state_max, opt.cutoff_mat);
1074 }
1075 if (space.empty())
1076 throw UnsupportedError(
1077 "SolverCTMC: the state space is empty; no state satisfies the model's capacities");
1078 // A DROP region censors the chain: the states it forbids are simply never
1079 // occupied, so removing them and letting `make_infgen` re-close the rows IS
1080 // the censored chain. WAITQ and the blocking rules augment the state
1081 // instead and are refused by name inside.
1082 space = ctmc_filter_regions(sn, space);
1083 CtmcResult<T> r = solver_ctmc(sn, space, sync, gsync, opt.keep_filtration, fjsync);
1084 // ELIMINATE THE VANISHING STATES, `solver_ctmc.m:812`. Until this ran the
1085 // port returned the UNREDUCED chain: a Router or Fork pass-through, a firable
1086 // Join and an immediate SPN mode all kept their GlobalConstants::Immediate
1087 // row, so `-a states` and `-a gen` reported a state space the other three
1088 // codebases had already complemented away -- six states against four on
1089 // `fj_tiny_closed`, the two extras carrying 6.1e-09 of the mass each. `-a avg`
1090 // agreed to the digits printed for exactly that reason, which is why the gap
1091 // stayed invisible unless the chain itself was asked for.
1093
1094 const std::size_t npos = static_cast<std::size_t>(-1);
1095 // The initial state is looked up in the REDUCED space: the enumeration index
1096 // no longer addresses the same rows once the vanishing ones are gone.
1097 std::size_t init = analyzer_detail::init_state_index(sn, r.space);
1098
1099 std::size_t ncomp = 0;
1100 const std::vector<std::size_t> comp = analyzer_detail::weak_components(r.Q, ncomp);
1101 if (ncomp > 1) {
1102 std::size_t pick;
1103 if (init != npos) {
1104 pick = comp[init];
1105 } else {
1106 // The initial state is not representable in this space -- an SPN
1107 // whose ENABLE states were folded away, for one -- so the reference
1108 // keeps the largest component instead of failing.
1109 std::vector<std::size_t> sizes(ncomp, 0);
1110 for (std::size_t s = 0; s < comp.size(); ++s) ++sizes[comp[s]];
1111 pick = static_cast<std::size_t>(
1112 std::max_element(sizes.begin(), sizes.end()) - sizes.begin());
1113 }
1114 std::vector<std::size_t> wset;
1115 for (std::size_t s = 0; s < comp.size(); ++s)
1116 if (comp[s] == pick) wset.push_back(s);
1117 // The seed moves WITH the restriction: keeping the pre-restriction row
1118 // would point the block decomposition at whatever state now sits there.
1119 std::size_t moved = npos;
1120 if (init != npos)
1121 for (std::size_t k = 0; k < wset.size(); ++k)
1122 if (wset[k] == init) {
1123 moved = k;
1124 break;
1125 }
1126 init = moved;
1127 r = analyzer_detail::restrict_to(r, wset);
1128 }
1129
1130 // Every stationary solve goes through the block decomposition, as
1131 // `solver_ctmc_analyzer.m` does: the irreducible case is the degenerate one
1132 // BSCC / no transient states, so no dispatch can disagree with the
1133 // algorithm about whether the chain is reducible. `ctmc_solve` splits a
1134 // reducible generator into WEAK components and renormalizes across them,
1135 // which is not the answer the declared initial state selects.
1136 line::util::LineConsole::step("infinitesimal generator built: %zu states",
1137 static_cast<std::size_t>(r.Q.rows()));
1138 line::util::LineConsole::step("solving for the stationary distribution");
1139 const CtmcStationaryResult<T> st = ctmc_stationary(r.Q, init);
1140 line::util::LineConsole::step("stationary distribution obtained, computing the mean metrics");
1141 out.pi = st.pi;
1142 out.warning = st.warning;
1143 // `gpu` ran on the CPU here, exactly as it does on a MATLAB host with no
1144 // GPU. Say so, and do not lose a warning the stationary solve already left.
1145 {
1146 const std::string fb = method_fallback_warning(opt.method);
1147 if (!fb.empty()) out.warning = out.warning.empty() ? fb : out.warning + " " + fb;
1148 }
1149 out.avg = solver_ctmc_avg_from_pi(sn, r, out.pi);
1150 out.cache = analyzer_detail::cache_metrics(sn, r, out.pi);
1151 out.chain = r;
1152 out.actualmethod = opt.method;
1153 return out;
1154}
1155
1156
1157/**
1158 * Port of `solver_ctmc_analyzer.m` plus the fork-join wrapper of
1159 * `@@SolverCTMC/runAnalyzer.m`.
1160 *
1161 * A fork-join model is solved on the TAG-AUGMENTED copy and the sibling classes
1162 * are folded back at the end. The chain and the stationary vector returned are the
1163 * AUGMENTED ones, as the reference's `result.space` is: they are indexed by a
1164 * class set the caller did not declare, which is why `fjclassmap` comes back with
1165 * them rather than being discarded.
1166 */
1167template <class T>
1169 check_method(opt.method);
1170
1171 // `@@SolverCTMC/runAnalyzer.m:126` converts the non-Markovian service laws
1172 // to a Markovian surrogate on its own copy of the struct. The conversion
1173 // runs before the support check, because what the check must see is the
1174 // struct the generator will actually be built from.
1175 //
1176 // THE PH FIT IS FORCED HERE, where the reference takes its CME default.
1177 // MATLAB's CTMC assembles a RATIONAL generator and can carry a matrix
1178 // exponential; this port assembles an ordinary one, and an ME's D0 has
1179 // off-diagonal entries that are not rates, so the chain it builds is not a
1180 // Markov chain. Measured on a closed Delay + Gamma(4, 0.5) FCFS model with
1181 // two jobs: the CME fit gives Util 1.075 -- IMPOSSIBLE for one server --
1182 // and a throughput of 0.538 at the delay against 0.400 at the queue, i.e.
1183 // flow that does not balance around a cycle. The PH fit gives Util 0.950,
1184 // balanced throughput 0.475 and QLen 1.525, against MATLAB's 0.954, 0.477
1185 // and 1.523.
1186 //
1187 // The residual 0.4% IS the divergence from the reference, and it is the
1188 // price of the honest fit: MATLAB matches two moments exactly with its ME
1189 // (scv 0.25) where the Bernstein PH lands at scv 0.323. Closing it needs
1190 // the CTMC to handle a rational generator, which is a separate change.
1191 NetworkStruct<T> converted;
1192 const NetworkStruct<T>* snp = &sn_in;
1193 if constexpr (num_traits<T>::has_transcendental) {
1194 if (api::sn_has_nonmarkov(sn_in, false)) {
1195 converted = sn_in;
1197 no.order = opt.nonmkv_order;
1198 no.phfit = api::PhFit::Ph;
1199 api::sn_nonmarkov_toph(converted, no);
1200 snp = &converted;
1201 }
1202 }
1203 const NetworkStruct<T>& sn = *snp;
1204
1206 // runAnalyzerChecks' universal feature gate, AFTER ctmc_check_support so the
1207 // declaration-defect messages, which the declared set also withholds, still
1208 // win over the gate's generic one.
1209 qn::feature_gate("SolverCTMC", qn::ctmc_feature_set(opt.method), sn);
1210
1211 if (!tr::has_fork_join(sn))
1212 return solve_struct(sn, opt, std::vector<qn::FjSync<T>>());
1213
1214 const qn::FjTagged<T> fjt = qn::fj_tag(sn);
1215 CtmcSolution<T> out = solve_struct(fjt.V, opt, fjt.fjsync);
1216 tr::fj_foldback(sn, out.avg, fjt.fjclassmap, fjt.korig);
1217 out.fjclassmap = fjt.fjclassmap;
1218 return out;
1219}
1220
1221/**
1222 * Port of `@@SolverCTMC/runAnalyzer.m`'s result assembly: solve, then apply the
1223 * metric filter `@@NetworkSolver/getAvg` puts between the analyzer and the
1224 * caller, so the table is the same shape SolverMVA and SolverNC print.
1225 *
1226 * The response-time mask the product-form runners apply -- drop a metric whose
1227 * response time is below the tolerance -- is NOT applied here. A CTMC reports
1228 * what the chain does, and a station a class genuinely visits with a tiny
1229 * response time is a real measurement rather than a numerical artefact of a
1230 * fixed point that did not converge there.
1231 */
1232template <class T>
1234 const std::string& method) {
1235 const std::size_t M = sn.nstations, K = sn.nclasses;
1236
1237 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
1238 for (std::size_t i = 0; i < M; ++i)
1239 if (sn.stations[i].nodetype == NodeType::Source)
1240 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
1241
1243 o.QN = mva::filter_metric(sn, d.avg.QN, mva::MetricKind::QLen, nullptr);
1244 o.UN = mva::filter_metric(sn, d.avg.UN, mva::MetricKind::Util, nullptr);
1245 o.RN = mva::filter_metric(sn, d.avg.RN, mva::MetricKind::RespT, nullptr);
1246 o.TN = mva::filter_metric(sn, d.avg.TN, mva::MetricKind::Tput, nullptr);
1248 mva::MetricKind::ResidT, nullptr);
1250 &srcmask);
1251 o.CN = d.avg.CN;
1252 o.XN = d.avg.XN;
1253 o.cache = d.cache;
1254 o.method = method;
1256 o.iter = 1;
1257 return o;
1258}
1259
1260template <class T>
1261mva::AvgResult<T> solver_ctmc_run_analyzer(const NetworkStruct<T>& sn, const CtmcOptions& opt);
1262
1263/**
1264 * Solve the CHAIN-AGGREGATED model and map its metrics back to the classes.
1265 *
1266 * `api::sn_aggregate_chains` collapses every chain onto a single class, class
1267 * switching disappearing with it, and `mva::sn_deaggregate_chain_results` maps
1268 * chain-level metrics back through alpha, the per-station share of the chain's
1269 * visits each class carries. Both transforms existed in all four codebases with
1270 * no solver consumer; this is that consumer.
1271 *
1272 * WHAT IS TRADED. Exactness on a non-product-form model: one aggregate service
1273 * law, fitted to the alpha-weighted first two moments, replaces the per-class
1274 * ones. On a product-form model the chain IS the unit MVA and convolution
1275 * already solve in, so the answer is exact and the state space is the smaller
1276 * one.
1277 */
1278template <class T>
1280 const CtmcOptions& opt) {
1281 // Driven by tr::transform_solve_chains, so the aggregate is solved through
1282 // an inner-solve seam rather than a hard-wired call to this analyzer.
1283 // Clearing the flag states that the aggregate must not be re-aggregated,
1284 // rather than relying on the caller's nchains < nclasses guard to decline.
1285 CtmcOptions sub = opt;
1286 sub.chain_aggregation = false;
1288 sn,
1289 [&sub](const NetworkStruct<T>& subsn) { return solver_ctmc_run_analyzer(subsn, sub); },
1290 opt.method);
1291}
1292
1293/**
1294 * Solve by LOAD CONCEALMENT, the iterated transformation.
1295 *
1296 * `tr::transform_solve_lc` sweeps the chains in Gauss-Seidel order, solving each
1297 * concealed single-chain struct with THIS analyzer. Clearing the flag on the
1298 * inner options states that a subproblem must not be concealed again, rather
1299 * than relying on its one-chain shape to decline.
1300 */
1301template <class T>
1303 const CtmcOptions& opt) {
1304 CtmcOptions sub = opt;
1305 sub.load_concealment = false;
1307 sn,
1308 [&sub](const NetworkStruct<T>& subsn) { return solver_ctmc_run_analyzer(subsn, sub); },
1309 opt.method, opt.transform_iter_max);
1310}
1311
1312/**
1313 * Solve with a station subset replaced by a FLOW-EQUIVALENT SERVER, then
1314 * recover the collapsed stations' own metrics by conditioning.
1315 *
1316 * `api::fes_aggregate` has existed in all four codebases with no solver
1317 * consumer at all: it was exercised by examples and tests only, so nothing in
1318 * the solver stack depended on it. Flow-equivalent aggregation is the standard
1319 * route to HIERARCHICAL DECOMPOSITION -- a subnetwork is solved in isolation
1320 * and enters the outer chain as a single load-dependent station, which is what
1321 * makes an otherwise intractable state space tractable.
1322 *
1323 * The reduced model answers for the surviving stations directly. For a
1324 * collapsed station the answer is the Chandy-Herzog-Woo conditional sum
1325 * E[Q_i] = sum_n P(N_fes = n) * Q_i(n), with P read off the reduced chain's
1326 * stationary law and Q_i(n) from the isolated subnetwork
1327 * (`fes::fes_compute_metrics`). Throughput needs no conditioning: flow is fixed
1328 * by the routing and an exact reduction leaves the chain throughput unchanged.
1329 *
1330 * EXACT when the collapsed subnetwork is product-form, which is the condition
1331 * `fes_aggregate` already imposes; an approximation otherwise, and the
1332 * state-space saving is the reason to accept that.
1333 */
1334template <class T>
1336 const CtmcOptions& opt) {
1337 const std::size_t M = sn.nstations, K = sn.nclasses;
1338 const std::vector<std::size_t>& subset = opt.fes_stations;
1339 if (subset.size() < 2)
1340 throw InputError(
1341 "options.config.fes_stations must name at least two stations: collapsing one "
1342 "station into a flow-equivalent server saves nothing.");
1343 if (subset.size() >= M)
1344 throw InputError(
1345 "options.config.fes_stations names every station: there is no complement left "
1346 "to solve.");
1347 for (std::size_t i : subset)
1348 if (i < 1 || i > M)
1349 throw InputError("options.config.fes_stations must be 1-based station indices in 1.." +
1350 std::to_string(M) + ".");
1351
1353 const fes::FesDeaggInfo<T>& info = agg.deagg;
1354 const NetworkStruct<T>& snRed = agg.model.get_struct();
1355
1356 CtmcOptions sub = opt;
1357 sub.fes_stations.clear();
1358 const CtmcSolution<T> red = solver_ctmc_analyzer(snRed, sub);
1359 const mva::AvgResult<T> redAvg = solver_ctmc_avg_table(snRed, red, sub.method);
1360
1361 // P(N_fes = n). The aggregate state space carries K columns per STATION, at
1362 // (ist-1)*K + k, so the FES's block is the one at its station index.
1363 const Matrix<T> SSq = ctmc_state_space_aggr(snRed, red.chain.space);
1364 const std::size_t fesIst = snRed.nodes[info.fesNode - 1].station;
1365 std::size_t tableSize = 1;
1366 for (int c : info.cutoffs) tableSize *= static_cast<std::size_t>(c + 1);
1367 std::vector<double> Pn(tableSize, 0.0);
1368 for (std::size_t r = 0; r < SSq.rows(); ++r) {
1369 std::vector<int> nvec(K, 0);
1370 for (std::size_t k = 0; k < K; ++k)
1371 nvec[k] = static_cast<int>(
1372 std::llround(num_traits<T>::to_double(SSq(r, (fesIst - 1) * K + k))));
1373 Pn[fes::ljd_linearize(nvec, info.cutoffs) - 1] +=
1374 r < red.pi.size() ? num_traits<T>::to_double(red.pi[r]) : 0.0;
1375 }
1376
1379
1380 const T zero = num_traits<T>::from_int(0);
1382 out.QN = Matrix<T>(M, K, zero);
1383 out.UN = Matrix<T>(M, K, zero);
1384 out.RN = Matrix<T>(M, K, zero);
1385 out.TN = Matrix<T>(M, K, zero);
1386
1387 for (std::size_t a = 0; a < info.complementIndices.size(); ++a) {
1388 const std::size_t i = info.complementIndices[a] - 1;
1389 for (std::size_t k = 0; k < K; ++k) {
1390 out.QN(i, k) = redAvg.QN(a, k);
1391 out.UN(i, k) = redAvg.UN(a, k);
1392 out.TN(i, k) = redAvg.TN(a, k);
1393 }
1394 }
1395
1396 const std::size_t Msub = info.subsetIndices.size();
1397 Matrix<T> Qsub(Msub, K, zero), Usub(Msub, K, zero);
1398 for (std::size_t idx = 0; idx < tableSize; ++idx) {
1399 if (!(Pn[idx] > 0.0)) continue;
1400 const T w = num_traits<T>::from_double(Pn[idx]);
1401 for (std::size_t a = 0; a < Msub && a < cm.QN[idx].rows(); ++a)
1402 for (std::size_t k = 0; k < K; ++k) {
1403 Qsub(a, k) += T(w * cm.QN[idx](a, k));
1404 Usub(a, k) += T(w * cm.UN[idx](a, k));
1405 }
1406 }
1407 for (std::size_t a = 0; a < Msub; ++a) {
1408 const std::size_t i = info.subsetIndices[a] - 1;
1409 for (std::size_t k = 0; k < K; ++k) {
1410 out.QN(i, k) = Qsub(a, k);
1411 out.UN(i, k) = Usub(a, k);
1412 // Flow through a station is fixed by the routing, so it is the FES's
1413 // throughput scaled by the ratio of ORIGINAL visit ratios.
1414 T ratio = zero;
1415 for (std::size_t c = 0; c < sn.nchains; ++c) {
1416 const std::size_t isf = sn.stateful_of_station(i + 1) - 1;
1417 const std::size_t isfFes = snRed.stateful_of_station(fesIst) - 1;
1418 if (isf < sn.visits[c].rows() && isfFes < snRed.visits[c].rows() &&
1419 snRed.visits[c](isfFes, k) > zero)
1420 ratio += T(sn.visits[c](isf, k) / snRed.visits[c](isfFes, k));
1421 }
1422 out.TN(i, k) = T(redAvg.TN(fesIst - 1, k) * ratio);
1423 }
1424 }
1425
1426 out.CN.assign(K, zero);
1427 out.XN = redAvg.XN;
1428 for (std::size_t i = 0; i < M; ++i)
1429 for (std::size_t k = 0; k < K; ++k) {
1430 if (out.TN(i, k) > zero) out.RN(i, k) = T(out.QN(i, k) / out.TN(i, k));
1431 out.CN[k] += out.RN(i, k);
1432 }
1433 out.AN = mva::sn_get_arvr_from_tput(sn, out.TN);
1435 out.method = opt.method;
1436 out.actualmethod = opt.method + "/fes";
1437 return out;
1438}
1439
1440/** Solve and format in one call, for a caller with no use for the chain. */
1441template <class T>
1443 // Chain aggregation, opt-in and only where the transform is not the identity.
1444 if (opt.load_concealment) return solver_ctmc_load_concealment(sn, opt);
1445 if (opt.chain_aggregation && sn.nchains < sn.nclasses)
1447 // Flow-equivalent server aggregation, opt-in.
1448 if (!opt.fes_stations.empty()) return solver_ctmc_fes_aggregation(sn, opt);
1450}
1451
1452} // namespace ctmc
1453} // namespace line
1454
1455#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_ANALYZER_H
What a solver observed about the Cache nodes of a model.
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t stateful_of_station(std::size_t st) const
std::vector< NodeDef > nodes
every node, in creation order
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
static void step(const char *fmt,...)
Write one progress line.
Host-aware memory pre-gate for SolverCTMC.
Steady-state distribution of a continuous-time Markov chain.
Worst-case log-size of the CTMC state space induced by a NetworkStruct.
Port of matlab/src/solvers/CTMC/ctmc_stationary.m: the single entry point for the stationary distribu...
The exception types the port throws.
Flow-equivalent-server aggregation: replace a station subset by one station.
Per-station metrics of the ISOLATED subnetwork at every population state, the companion of fes_comput...
Port of matlab/src/api/fj/sn_fj_validate.m and matlab/src/io/@@ModelAdapter/fjtag....
Fork-join TAG AUGMENTATION: the fold-back half of the transform/lift pair that CTMC and SSA share.
Running progress log of a LINE solver run (the "solver console").
Dense matrix and non-owning view.
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.
mva::AvgResult< T > solver_ctmc_fes_aggregation(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Solve with a station subset replaced by a FLOW-EQUIVALENT SERVER, then recover the collapsed stations...
void ctmc_eliminate_vanishing(CtmcResult< T > &res)
Port of the "now remove immediate transitions" block of solver_ctmc.m (:812-870): eliminate the vanis...
mva::AvgResult< T > solver_ctmc_chain_aggregation(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Solve the CHAIN-AGGREGATED model and map its metrics back to the classes.
void check_method(const std::string &method)
Port of runAnalyzerChecks' method gate.
std::vector< std::string > list_valid_methods()
Port of SolverCTMC.listValidMethods.
void ctmc_check_support(const NetworkStruct< T > &sn)
Refuse the constructs this port generates a chain for but does not MODEL.
CtmcAvg< T > solver_ctmc_avg_from_pi(const NetworkStruct< T > &sn, const CtmcResult< T > &r, const std::vector< T > &pivec)
Port of solver_ctmc_avg_from_pi: map a state distribution to mean metrics.
void make_infgen(Matrix< T > &Q)
Port of ctmc_makeinfgen: turn an off-diagonal rate matrix into a generator.
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...
mva::AvgResult< T > solver_ctmc_load_concealment(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Solve by LOAD CONCEALMENT, the iterated transformation.
std::vector< NetState< T > > ctmc_filter_regions(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space)
The states of space a DROP region admits, in their original order.
CtmcResult< T > solver_ctmc(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space, const std::vector< Sync< T > > &sync, const std::vector< qn::GlobalSync< T > > &gsync=std::vector< qn::GlobalSync< T > >(), bool want_filtration=false, const std::vector< qn::FjSync< T > > &fjsync=std::vector< qn::FjSync< T > >())
Port of the generator assembly of solver_ctmc.m.
CtmcSolution< T > solve_struct(const NetworkStruct< T > &sn_in, const CtmcOptions &opt, const std::vector< qn::FjSync< T > > &fjsync)
Build the chain of ONE struct, solve it, reduce it.
std::string method_fallback_warning(const std::string &method)
The reference's fallback warning, or empty when the method needs none.
CtmcSolution< T > solver_ctmc_analyzer(const NetworkStruct< T > &sn_in, const CtmcOptions &opt)
Port of solver_ctmc_analyzer.m plus the fork-join wrapper of @@SolverCTMC/runAnalyzer....
mva::AvgResult< T > solver_ctmc_run_analyzer(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Solve and format in one call, for a caller with no use for the chain.
CtmcStationaryResult< T > ctmc_stationary(const Matrix< T > &Q, std::size_t init_index=static_cast< std::size_t >(-1))
constexpr std::size_t CTMC_DEFAULT_CUTOFF
SolverOptions.m:107: the per-class state-space cutoff SolverCTMC defaults an open or mixed model to.
bool is_stateless_method(const std::string &method)
True for a method whose analyzer is not the explicit-generator one.
Matrix< T > ctmc_state_space_aggr(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space)
Port of StateSpaceAggr: the per-(station, class) job counts of every state, as an (nstates x nstation...
std::vector< NetState< T > > reachable_space_generator(const NetworkStruct< T > &sn, const NetState< T > &init, const std::vector< Sync< T > > &sync, const std::vector< qn::GlobalSync< T > > &gsync=std::vector< qn::GlobalSync< T > >(), std::size_t maxst=3000000, const std::vector< qn::FjSync< T > > &fjsync=std::vector< qn::FjSync< T > >(), const std::vector< std::size_t > &cutoff=std::vector< std::size_t >(), const std::vector< std::vector< std::size_t > > &cutoff_mat=std::vector< std::vector< std::size_t > >())
Port of State.reachableSpaceGenerator: the states reachable from init.
std::size_t ljd_linearize(const std::vector< int > &nvec, const std::vector< int > &cutoffs)
Linearized index of a per-class population vector, for Limited Joint Dependence (LJD) tables.
FesConditionalMetrics< T > fes_compute_metrics(const Matrix< T > &L, const std::vector< int > &mi, const std::vector< bool > &isDelay, const std::vector< int > &cutoffs)
Per-station metrics of the ISOLATED subnetwork at every population state, the companion of fes_comput...
FesAggregateResult< T > fes_aggregate(const qn::NetworkStruct< T > &sn, const std::vector< std::size_t > &subsetIndices, const FesOptions &options=FesOptions())
Flow-equivalent-server aggregation: replace a station subset by one station.
@ REPLY
completes a synchronous call, releasing a held server
Definition lang_types.h:168
constexpr double CTMC_DEFAULT_SAFETY_FRACTION
Fraction of available memory the solver may target.
CtmcGateResult ctmc_memory_gate(double log_nstates, bool force=false, double safety_fraction=CTMC_DEFAULT_SAFETY_FRACTION)
Decide whether a state space of log-size log_nstates can be solved here.
double ctmc_state_space_logsize(const qn::NetworkStruct< T > &sn, const CtmcSizeOptions &opt=CtmcSizeOptions())
Worst-case log state-space size of sn.
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
Matrix< T > filter_metric(const qn::NetworkStruct< T > &L, const Matrix< T > &metric, MetricKind kind, const std::vector< std::vector< bool > > *zero_mask)
Port of filterMetric: what @@NetworkSolver/getAvg does between the analyzer and the caller.
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
void cache_retrieval_class_map(const CacheParam< T > &cp, std::vector< std::size_t > &rc_list, std::vector< std::size_t > &rc_items, std::vector< std::size_t > &rc_orig)
Port of State.cacheRetrievalClassMap: the canonical order of a cache's retrieval classes,...
bool from_marginal_node_first(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases, std::vector< T > &out)
The FIRST row from_marginal_node emits, BUILT rather than enumerated.
Definition state.h:2005
std::size_t state_initial_occupancy(const NetworkStruct< T > &sn, std::size_t ind, std::size_t r)
Port of State.initialOccupancy: the class-r jobs node ind holds in the DECLARED initial state,...
Definition state.h:2154
FeatureSet ctmc_feature_set(const std::string &method)
SolverCTMC.getFeatureSet, the reference's 104 MATLAB names in full.
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.
FjTagged< T > fj_tag(const NetworkStruct< T > &sn)
Port of ModelAdapter.fjtag.
Definition fj_tag.h:302
CacheMetrics< T > cache_metrics_of(const qn::NetworkStruct< T > &sn, const std::vector< T > &hitprob, const std::vector< T > &missprob, const std::vector< T > &delayedprob, const std::vector< T > &latency, const Matrix< T > &hitproblist, const Matrix< T > &itemprob, const std::vector< T > &listcost)
Assemble CacheMetrics from what a cache analyzer returned.
mva::AvgResult< T > transform_solve_lc(const qn::NetworkStruct< T > &sn, InnerSolve inner_solve, const std::string &method, std::size_t iter_max=1000)
LOAD CONCEALMENT (Birman-Kogan Algorithm 2) as a transformation, and the first ITERATED one.
void fj_foldback(const qn::NetworkStruct< T > &sn, Avg &a, const std::vector< std::size_t > &fjclassmap, std::size_t korig)
Reduce the augmented metrics onto the original classes.
mva::AvgResult< T > transform_solve_chains(const qn::NetworkStruct< T > &sn, InnerSolve inner_solve, const std::string &method)
Chain aggregation: collapse every chain onto a single class, solve, and map the chain metrics back on...
bool has_fork_join(const qn::NetworkStruct< T > &sn)
Whether the model needs the tag augmentation at all.
A queueing network and its refreshed NetworkStruct.
Collapse every chain onto one class, port of ModelAdapter.aggregateChains.
Replace every non-Markovian service and firing law by a Markovian surrogate.
Port of solver_ctmc.m: the infinitesimal generator of a queueing network, assembled from the enumerat...
Finite Capacity Regions in SolverCTMC: the DROP rule, as a filter on the enumerated state space,...
The DECLARED side of the gate: one feature set per solver.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
Port of the MATLAB +State package: the encoding that turns a station's state row into marginal job co...
Port of the event half of MATLAB's +State package: the successor states an event produces at one node...
options.config.nonmkv and friends.
std::size_t order
nonmkvorder, the phase budget
PhFit phfit
which surrogate family
The mean performance metrics a stationary vector maps to.
The SolverCTMC knobs this port honours.
bool force
options.force: downgrade the memory pre-gate's refusal to a warning.
double memory_safety_fraction
options.memorySafetyFraction: share of available memory a solve may target.
std::vector< std::size_t > cutoff_vec
per-class override, or empty
std::size_t nonmkv_order
options.config.nonmkvorder: the phase budget sn_nonmarkov_toph spends on a non-Markovian service law.
bool load_concealment
options.config.transform='lc': solve by LOAD CONCEALMENT.
std::size_t transform_iter_max
Sweep cap for an iterated transformation; the kernel's own default.
std::vector< std::vector< std::size_t > > cutoff_mat
options.cutoff AS A (station x class) MATRIX, or empty.
std::vector< std::size_t > fes_stations
options.config.fes_stations: 1-BASED station indices to collapse into a flow-equivalent server before...
std::size_t state_max
refuse a space larger than this
double fau_delta
options.config.fau_delta: occupancy below which a state is dropped.
double cutoff
< 0 = not given
double timestep
options.timestep: the FIXED OUTPUT STEP of a transient analysis.
std::string transient_method
options.config.transient_method: "ode" (the default) integrates the forward equation,...
std::size_t fau_ngrid
options.config.fau_ngrid: output grid size when timestep is unset.
bool chain_aggregation
options.config.chain_aggregation: solve the CHAIN-AGGREGATED model.
double fau_epsilon
options.config.fau_epsilon: total probability mass the whole grid may discard under "fau".
bool keep_filtration
Keep the per-synchronization EVENT FILTRATION alongside Q.
The generator, the state space it is indexed by, and the event rates.
Definition solver_ctmc.h:57
std::vector< NetState< T > > space
row i of Q is space[i]
Definition solver_ctmc.h:59
Matrix< T > Q
(n x n) infinitesimal generator
Definition solver_ctmc.h:58
Everything one CTMC solve produces.
std::string warning
Set when the chain is a reducible mixture solved from an invented seed.
std::vector< std::size_t > cutoff
the per-class cutoff actually used
solvers::CacheMetrics< T > cache
What the chain says about the model's Cache nodes: exact, since the hit and miss shares are read off ...
std::vector< std::size_t > fjclassmap
fjclassmap when the model was fork-join, empty otherwise: the ORIGINAL class of each auxiliary siblin...
std::vector< T > pi
stationary distribution over chain.space
std::vector< T > pi
stationary distribution, length N
std::string warning
Empty unless the chain is an UNSEEDED reducible mixture.
What fes_aggregate returns.
The per-population tables the conditional sum is taken over.
std::vector< Matrix< T > > QN
Indexed by the 0-based linearized population state; each (M_sub x K).
std::vector< Matrix< T > > UN
Indexed by the 0-based linearized population state; each (M_sub x K).
Everything needed to map an FES result back onto the original model.
Matrix< T > isolatedDemands
(M_sub x K)
std::vector< std::size_t > subsetIndices
1-based, as given
std::vector< std::size_t > complementIndices
1-based
std::vector< bool > isolatedIsDelay
std::vector< int > isolatedServers
std::size_t fesNode
1-based node index of the FES in the new model
std::vector< int > cutoffs
The gate verdict, plus the message the caller reports either way.
Options the estimator reads; only the cutoff matters.
double cutoff
< 0 or non-finite = not given, take the solver default
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
Matrix< T > UN
utilization
Matrix< T > WN
residence time, per job
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
solvers::CacheMetrics< T > cache
What the cache branches observed, EMPTY on a model with no Cache node and on every solver that does n...
Matrix< T > AN
arrival rate
One fork firing synchronization: sn.fjsync{k}.
The augmented struct and everything needed to read its results back.
Definition fj_tag.h:71
std::vector< FjSync< T > > fjsync
Definition fj_tag.h:76
NetworkStruct< T > V
Definition fj_tag.h:72
std::size_t korig
Definition fj_tag.h:78
std::vector< std::size_t > fjclassmap
fjclassmap[a-1] is the ORIGINAL class of auxiliary class a, 0 for originals.
Definition fj_tag.h:74
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2140
Every Cache node of the model, in node order; empty on a model with none.
Solver-agnostic driver of a model TRANSFORMATION, the sibling of solvers/mva/fj_driver....