LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ln.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_LN_SOLVER_LN_H
6#define LINE_SOLVERS_LN_SOLVER_LN_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverLN: layered decomposition of a layered queueing network.
12 *
13 * Port of the matlab/src/solvers/LN SolverLN class folder -- construct, buildLayers,
14 * buildLayersRecursive, init, initInterlock, converged, analyze, post,
15 * updateMetrics, updatePopulations, updateThinkTimes, updateLayers,
16 * updateRoutingProbabilities, getEntryServiceMatrix and getEnsembleAvg --
17 * driven by the EnsembleSolver iteration in @@EnsembleSolver/iterate.m.
18 *
19 * The method is a Picard iteration over a fixed point. Each processor and each
20 * software task becomes a closed queueing network (a "layer") in which the
21 * element is the server and its callers are the customers; solving all layers
22 * gives new residence times, which become the service times of the calls one
23 * level up and the think times of the callers one level down, and the layers
24 * are re-solved until the queue lengths stop moving.
25 *
26 * WHAT IS IMPLEMENTED. Every path a plain layered model takes is here:
27 * synchronous calls, entry selection by throughput ratio, multi-entry tasks,
28 * reference tasks with think time, infinite-server tasks and processors,
29 * server replication, AND-forks and AND-joins, and the LQNS V5 interlock
30 * analysis. Forwarding is solved too: construct() runs the
31 * lqn_fwd_rendezvous rewrite of lqn_helpers.h (:306), which flattens every
32 * forwarding chain into caller-side pseudo rendezvous arcs, so nothing below
33 * this point needs a forwarding case. Asynchronous calls and entry open
34 * arrivals are solved by giving the layer its own Source/Sink pair and an
35 * open chain (build_layer's async_here/open_entries construction, roughly
36 * :622-668). Cache tasks and item entries are solved by a Cache node placed
37 * in the host layer, with the hit and miss branches switching class at the
38 * server (build_layer's iscachelayer path, :591-616 and the read walk at
39 * :883-908). Setup tasks with a setup time are solved by charging the cold
40 * start to the ENTRY, at the probability the thread was found powered down
41 * (setup_charge, added to entry_servt and to the caller's think time); no layer
42 * station carries a SetupDelayOffParam. Admission constraints are solved by a Region on
43 * the server station, which forces that layer's fixed point through
44 * SolverCTMC rather than MVA or fluid (:1051-1096, dispatch at :1753-1760).
45 * reject_unsupported() (:433) is kept but empty: every construct the
46 * reference LQN model can carry is now accounted for here. A few narrower
47 * combinations remain unsupported and are refused where they are discovered
48 * instead of up front: a fork sharing a layer with an open stream (:492-496),
49 * a fork's fixed point outside layer_solver 'mva' (:1723-1728), and a fluid
50 * layer or an AND-join's order-statistic fit under a non-double/non-real
51 * arithmetic backend (:120-126, :2198-2201).
52 *
53 * WHAT ELSE THIS CLASS ANSWERS, beyond the mean-value table. `options.method`
54 * selects between three DIFFERENT questions rather than three routes to one:
55 * `default` is the mean-based update; `moment3` is updateMetricsMomentBased,
56 * which fits an APH to each layer's response-time CDF and convolves along the
57 * entry's activity sequence, so it additionally reports a per-entry response
58 * time DISTRIBUTION through get_cdf_respt(); `mw.upper` / `mw.lower` report
59 * Majumdar-Woodside robust box bounds and never solve a layer at all.
60 * get_tran_avg() is the layered transient, decoupled (demands frozen at the
61 * fixed point) or coupled (waveform relaxation over time-varying demands
62 * injected through the fluid rate schedule). get_sensitivity_table() delegates
63 * to each layer's own sensitivity table; every derivative in it is a partial
64 * WITHIN its layer, not a total derivative of the layered model.
65 *
66 * THE LAYER ENGINE is `layer_solver`: `mva`, `nc`, `fluid` or `ssa`, the C++
67 * spelling of the reference's solver FACTORY. They converge to DIFFERENT fixed
68 * points, because each feeds different demands back into the next outer sweep.
69 * `ssa` is noisy, so the deterministic convergence test is replaced by
70 * LnStochController (Robbins-Monro relaxation, Polyak-Ruppert reporting);
71 * running it under the deterministic test would simply reach iter_max.
72 *
73 * PHASE-2 ACTIVITIES ARE SOLVED, not refused. `servt` keeps both phases, so the
74 * server's utilization is unchanged, while `residt` carries the CALLER's view:
75 * phase 1 in full plus the phase-2 time the caller is actually overtaken by,
76 * with the overtaking probability from lqn_analyzers.h's
77 * lqn_overtake_prob_markov. Every phase-2 branch below is gated on `has_phase2`
78 * and is inert on a single-phase model.
79 *
80 * ARITHMETIC. All model quantities are T. Iteration counters, populations,
81 * multiplicities and the convergence tolerances are double, matching the
82 * reference: they are properties of the algorithm and of the model's integer
83 * structure, not quantities whose precision is under study.
84 */
85
87#include <algorithm>
88#include <cctype>
89#include <cmath>
90#include <functional>
91#include <iostream>
92#include <limits>
93#include <map>
94#include <memory>
95#include <set>
96#include <string>
97#include <vector>
98
104#include "line/api/mam/aph_fit.h"
116#include "line/api/lqn/lqn_ph.h"
126#include "line/util/error.h"
127#include "line/util/expm.h"
128#include "line/util/linalg.h"
129#include "line/util/matrix.h"
130
131namespace line {
132namespace ln {
133
134using lang::CallType;
135using lang::Distrib;
136using lang::GlobalConstants;
138using lang::LqnElement;
139using lang::NodeType;
143using lqn::LqnCallGroup;
144using lqn::LqnStruct;
145
146/** One (station, class) rate trajectory injected into a layer's closing ODE. */
148
149namespace detail {
150
151/**
152 * Run the fluid analyzer on one layer and write its metrics into the shape the
153 * MVA layer path returns.
154 *
155 * Overloaded rather than gated: LSODA is double-only, so the template below is
156 * what any other backend resolves to, and it refuses by name instead of failing
157 * to compile inside a branch that could never run.
158 */
159inline void ln_fluid_solve(const qn::NetworkStruct<double>& L, const fluid::FluidOptions& fo,
161 // THE RUNNER, NOT THE SWITCH. `solver_fluid` is the ungated port of
162 // solver_fluid_analyzer.m and cannot reach `minnormal`, `rmf`, `refined` or
163 // `kp` at all; `solver_fluid_run_analyzer` is the port of runAnalyzer's resolution,
164 // which is what `LN(model, @(m) Fluid(m))` runs in the reference. Calling
165 // the switch here silently ran the FIRST-ORDER `matrix` method in every
166 // layer: on lqn_basic that reported T3's residence as its bare demand
167 // (0.02 against MATLAB's 0.0206615) because the hard min charges no
168 // queueing below the server count, and the LN table then disagreed with
169 // MATLAB by 3.3% on a metric neither codebase flagged.
171 for (std::size_t i = 0; i < L.nstations; ++i)
172 for (std::size_t r = 0; r < L.nclasses; ++r) {
173 out.Q(i, r) = s.QN(i, r);
174 out.U(i, r) = s.UN(i, r);
175 out.R(i, r) = s.RN(i, r);
176 out.Tp(i, r) = s.TN(i, r);
177 }
178 for (std::size_t r = 0; r < L.nclasses && r < s.XN.size(); ++r) {
179 out.X[r] = s.XN[r];
180 out.C[r] = s.CN[r];
181 }
182 out.method = s.method;
183 out.iter = static_cast<int>(s.iters);
184}
185template <class T>
186void ln_fluid_solve(const qn::NetworkStruct<T>&, const fluid::FluidOptions&,
188 throw UnsupportedError(
189 "SolverLN: a fluid layer integrates its drift with LSODA, whose coefficients assume "
190 "double precision; rerun with --arith double or use layer_solver 'mva'");
191}
192
193/**
194 * The response-time CDF of every (station, class) of one layer, which is what
195 * `SolverFluid(ensemble{e}).getCdfRespT` returns in the reference.
196 *
197 * The layer is integrated once to its fixed point and the converged state is
198 * the marking the passage starts from, so the law reported is the STATIONARY
199 * response time and not the one seen from an arbitrary initial condition. That
200 * is `options.init_sol = odeStateVec` in `@@SolverFLD/getCdfRespT`.
201 *
202 * A pair the class never visits comes back as the degenerate curve at zero,
203 * which `fluid_passage_time` already returns; the moment extraction above reads
204 * it as a mean of zero and the caller drops the term, as MATLAB's
205 * `m1 > CoarseTol` test does.
206 */
207inline std::vector<std::vector<fluid::FluidPassage>> ln_fluid_cdf_respt(
209 fluid::FluidOptions o = fo;
210 // THE SWITCH HERE, DELIBERATELY, unlike ln_fluid_solve above. By the same
211 // argument this should be the runner, but the reference for `moment3` on
212 // this path is the JAR (MATLAB does not finish the method on the model the
213 // golden was taken from), and the JAR's entry response times agree with the
214 // first-order switch to 5e-3 and not with the resolved method, which moves
215 // E:e0 from 2.2748 to 2.4964. Switching it blind would replace a measured
216 // agreement with an unmeasured one; it needs a JAR-side reading first.
218 std::vector<std::vector<fluid::FluidPassage>> out(
219 L.nstations, std::vector<fluid::FluidPassage>(L.nclasses));
220 for (std::size_t i = 1; i <= L.nstations; ++i) {
221 if (L.stations[i - 1].nodetype == qn::NodeType::Source) continue;
222 for (std::size_t r = 1; r <= L.nclasses; ++r) {
223 if (L.disabled[i - 1][r - 1]) continue;
224 out[i - 1][r - 1] = fluid::fluid_passage_time(L, s.xvec, i, r, o.tol, 201, s.closure);
225 }
226 }
227 return out;
228}
229template <class T>
230std::vector<std::vector<fluid::FluidPassage>> ln_fluid_cdf_respt(const qn::NetworkStruct<T>&,
231 const fluid::FluidOptions&) {
232 throw UnsupportedError(
233 "SolverLN: the response-time CDF a layer contributes to the 'moment3' update is a fluid "
234 "passage time, integrated by LSODA in double precision; rerun with --arith double");
235}
236
237/**
238 * One layer's transient trajectory, the `getTranAvg` of a layer.
239 *
240 * `out_grid` REPLACES the uniform grid, and it is not a convenience: SolverENV
241 * sums a layered stage's exit average against the environment's holding-time
242 * CDF, so the trajectory has to be evaluated where that CDF puts its mass
243 * rather than where the horizon happens to spread points. It also makes every
244 * layer of one stage report on the SAME grid, which is what lets the layers be
245 * assembled into one block-diagonal transient with a single time base.
246 */
247inline std::vector<fluid::FluidTranPoint> ln_fluid_transient(
248 const qn::NetworkStruct<double>& L, const fluid::FluidOptions& fo, double t_end,
249 std::size_t points, const std::vector<double>& out_grid = std::vector<double>()) {
250 return fluid::solver_fluid_transient(L, fo, t_end, points, out_grid);
251}
252template <class T>
253std::vector<fluid::FluidTranPoint> ln_fluid_transient(const qn::NetworkStruct<T>&,
254 const fluid::FluidOptions&, double,
255 std::size_t,
256 const std::vector<double>& =
257 std::vector<double>()) {
258 throw UnsupportedError(
259 "SolverLN: the layered transient integrates each layer's drift with LSODA, which is "
260 "double precision by construction; rerun with --arith double");
261}
262
263} // namespace detail
264
265/** Options of SolverLN. Defaults are SolverOptions('LN'). */
266struct LnOptions {
267 int iter_max = 200;
268 double iter_tol = 5e-3;
269 double tol = 1e-4;
270 bool interlocking = true;
271 /**
272 * `options.config.interlock_method`: how the interlock is TRACKED once
273 * `interlocking` is on. `ilrate` is the post-hoc rate discount of
274 * update_populations (the default and the historical method); `refpath`
275 * merges the callers that are the same reference customers into ONE client
276 * chain running down the reference path, replacing the discount; `none`
277 * applies no correction. A request refpath cannot carry DOWNGRADES to
278 * `ilrate`, see ln_interlock_method.
279 */
280 std::string interlock_method = "ilrate";
281 /** `options.config.interlock_maxpaths`: refpath refuses a layer carrying more routes into it. */
282 double interlock_maxpaths = 32.0;
283 /**
284 * `options.config.interlock_refpath_scope`: `merging` transforms only the
285 * layers where two or more callers descend from one reference task; `all`
286 * also those with a lone caller below a reference task.
287 */
288 std::string interlock_refpath_scope = "merging";
289 std::string relax = "fixed";
290 double relax_factor = 0.5;
291 /** Options handed to each layer solver; SolverMVA defaults. */
293 /**
294 * Which solver runs each layer: `mva`, `nc`, `fluid` or `ssa`.
295 *
296 * The reference names this by passing a solver FACTORY,
297 * `LN(model, @(m) Fluid(m, opt))`, so the choice is per-ensemble and not
298 * per-layer; this field is the same choice under a name, because a C++
299 * template cannot take a MATLAB function handle.
300 *
301 * THEY DO NOT CONVERGE TO THE SAME PLACE. Each engine feeds different
302 * service demands back into the next outer iteration, so the ensembles
303 * reach different fixed points rather than the same one by different
304 * routes. `ssa` in particular is a NOISY layer solver: the deterministic
305 * convergence test cannot terminate against its standard error, so it is
306 * driven with `LnStochController` (lqn_analyzers.h) instead.
307 */
308 std::string layer_solver = "mva";
309 /** Options handed to each layer when `layer_solver` is `fluid`. */
311 /** Options handed to each layer when `layer_solver` is `nc`. */
313 /** Options handed to each layer when `layer_solver` is `ssa`. */
315 /**
316 * `options.method`, which selects WHAT is reported and not merely how:
317 *
318 * `default` the mean-based update (updateMetricsDefault)
319 * `moment3` the APH moment-based update (updateMetricsMomentBased),
320 * which additionally produces a per-entry response-time
321 * distribution, `get_cdf_respt()`
322 * `mw.upper` Majumdar-Woodside robust box bounds INSTEAD of a fixed
323 * `mw.lower` point: no layer is ever solved, and every metric the
324 * bound does not define is reported as undefined
325 */
326 std::string method = "default";
327 /**
328 * `options.config.ln_transient`: how the per-layer transients are coupled.
329 * `decoupled` freezes the inter-layer demands at the converged fixed point;
330 * `coupled` reconciles them by waveform relaxation. Iteration 0 of the
331 * coupled relaxation IS the decoupled answer.
332 */
333 std::string ln_transient = "coupled";
334 /** `options.config.ln_transient_iter_max` and `..._tol` of the relaxation. */
336 double ln_transient_tol = 1e-2;
337 /**
338 * `options.config.ln_transient_channels`: which inter-layer coupling is
339 * injected, `both`, `thinkt` (client delay only) or `callservt`
340 * (synchronous-call service only). Used to isolate each channel's share of
341 * the coupled transient.
342 */
343 std::string ln_transient_channels = "both";
344 /** `options.timespan(2)`: the transient horizon; infinite means none is set. */
345 double timespan_end = std::numeric_limits<double>::infinity();
346 /** Output points per layer trajectory, and the relaxation's own grid size. */
347 std::size_t tran_points = 101;
348 /**
349 * An EXPLICIT output grid for the layered transient, replacing the uniform
350 * `tran_points` one when it is not empty.
351 *
352 * SolverENV is what asks for it: a layered stage's exit metric is a
353 * Riemann-Stieltjes sum against the environment's holding-time CDF, and a
354 * uniform grid over the horizon resolves the horizon rather than the
355 * sojourn. Interpolating afterwards cannot recover resolution the
356 * trajectory never had, so the points are asked for instead --
357 * `SolverEnv::stage_grid` builds them and `set_tran_grid` installs them.
358 * The grid must be increasing and start at zero; LSODA takes it as given.
359 */
360 std::vector<double> tran_grid;
361};
362
363/** Per-layer results of one iteration, the [QN,UN,RN,TN,AN,WN] of getAvg. */
364template <class T>
367};
368
369/** The LQN-level answer, indexed by element 1..nidx. */
370template <class T>
372 std::vector<T> QN, UN, RN, TN, AN, WN;
374 int iterations = 0;
375 bool converged = false;
376 /**
377 * True when the numbers are a BOUND (`method` = `mw.upper` / `mw.lower`)
378 * rather than the fixed point. A bound defines throughput and processor
379 * utilization and nothing else, so the other four measures come back
380 * undefined; reporting zeros there would be a claim.
381 */
382 bool is_bound = false;
383};
384
385/** A CDF sampled on a grid, the [F, t] pair MATLAB's evalCDF returns. */
386struct LnCdf {
387 std::vector<double> t, cdf;
388 bool empty() const { return t.empty(); }
389};
390
391/**
392 * One layer's block of the layered transient.
393 *
394 * The reference assembles the layers into one BLOCK-DIAGONAL cell array whose
395 * off-diagonal blocks are empty, so the blocks themselves carry the whole
396 * answer; keeping them apart also keeps each layer's own time grid, which the
397 * block-diagonal form has nowhere to put.
398 */
400 std::vector<double> t; ///< the output grid, shared by every series below
401 /// [station][class][point]
402 std::vector<std::vector<std::vector<double>>> QN, UN, TN;
403};
404
405/**
406 * `LayeredNetwork.layerBlocks`: where each layer's block sits in the aggregate.
407 *
408 * An LQN has no stations and classes of its own -- SolverLN builds them, one
409 * network per layer -- so the flat (station, class) view a caller like
410 * SolverENV needs is the BLOCK-DIAGONAL UNION of the layer networks, in layer
411 * order. `roff[e]`/`coff[e]` are layer e's 0-based row/column offset in that
412 * union and `msz[e]`/`ksz[e]` its size; `M`/`K` are the totals.
413 *
414 * The off-diagonal blocks pair a station of one layer with a class of another
415 * and stand for nothing at all. The reference leaves them as EMPTY cells; this
416 * port fills them with zeros, which is what the reference's consumers make of
417 * an empty cell (`tranTimeBase_` returns nothing and the metric stays 0).
418 */
420 std::vector<std::size_t> roff, coff, msz, ksz;
421 std::size_t M = 0, K = 0;
422};
423
424/** The layered transient: one block per layer, plus how it was produced. */
426 std::vector<LnTranLayer> layers;
427 std::string mode; ///< "coupled" or "decoupled"
428 long iterations = 0; ///< waveform-relaxation sweeps; 0 when decoupled
429 double gap = 0.0; ///< final sup-norm trajectory change, coupled only
430};
431
432/** getSensitivityTable of the ensemble: the layer tables under a Layer column. */
433template <class T>
435 struct Row {
436 std::string layer, station, jobclass;
438 };
439 std::vector<Row> rows;
440 /** Per layer, the branch that layer took; empty for a layer with no solver. */
441 std::vector<std::string> layer_methods;
442 /** The summary label: the common branch, or "mixed" when they differ. */
443 std::string method;
444 /** Per layer, the analytic Jacobian where that layer produced one. */
445 std::vector<sens::SensTable<T>> layer_tables;
446};
447
448/**
449 * Overtaking probability at a server entry, defined in lqn_analyzers.h.
450 *
451 * DECLARED, not included: lqn_analyzers.h needs LayerResult and SolverLN
452 * complete (LnStochController holds a vector of the first and two refusals take
453 * the second), so it must be parsed AFTER this class. The definition arrives
454 * through the include at the foot of this file, which is why the declaration
455 * has to stand here -- an unqualified call from a member function would
456 * otherwise find nothing, LqnStruct's associated namespace being line::lqn.
457 */
458template <class T>
459T lqn_overtake_prob_markov(const LqnStruct<T>& lqn, const std::vector<T>& servt,
460 const std::vector<T>& callresidt, const std::vector<T>& tput,
461 std::size_t eidx, const T& xj);
462
463/** `options.config.stochiter_*` of SolverOptions.m, with its defaults. */
465 long burnin = 5; ///< Picard iterations before the step decay starts
466 double a0 = 1.0; ///< Robbins-Monro step immediately after burn-in
467 double alpha = 0.6; ///< step decay exponent, in (0.5, 1]
468 long conseq = 3; ///< consecutive sub-tolerance iterations required to stop
469 double iter_tol = 5e-3;
470 /** Relaxation in force during burn-in, i.e. whatever init left in place. */
471 double relax_burnin = 1.0;
472};
473
474/**
475 * The Robbins-Monro / Polyak-Ruppert controller, defined in lqn_analyzers.h.
476 *
477 * DECLARED HERE, not included: same mutual dependence as above. `iterate` holds
478 * one through a shared_ptr rather than by value so that this class stays
479 * complete without it -- a by-value member would need the definition at the
480 * point the class template is instantiated, and the definition arrives at the
481 * foot of this file.
482 *
483 * ITS CONFIG CANNOT BE DEFERRED THE SAME WAY. `iterate` names LnStochConfig by
484 * value, and that name does not depend on T, so it is looked up and required
485 * COMPLETE when the template is parsed rather than when it is instantiated --
486 * the shared_ptr trick only defers the class template beside it.
487 */
488template <class T>
490
491template <class T>
492class SolverLN {
493public:
494 /**
495 * Port of `SolverLN.listValidMethods`.
496 *
497 * Each name states the LAYERING and the ENCODING; `ln_requested_method`
498 * normalises the alias spellings ("ph", "cs", "srvncs", "flatcs",
499 * "squashed", "squashed.ph") onto these, and they are left out here to keep
500 * the list unambiguous, exactly as the reference does.
501 */
502 static std::vector<std::string> list_valid_methods() {
503 return {"srvn", "srvn.ph", "srvn.cs", "flat", "flat.cs", "flat.ph", "moment3", "default"};
504 }
505
506 SolverLN(const LqnStruct<T>& lqn_in, const LnOptions& options) : lqn(lqn_in), opt(options) {
507 // A LAYER SOLVER NEVER NARRATES, the same invariant the MATLAB, JAR and
508 // python ports stamp on each constructed layer solver. The fixed point
509 // runs every layer once per iteration, so a layer left at the caller's
510 // verbosity would print its own banner nlayers*iter_max times and bury
511 // the layered narration the caller actually asked for. `ssa` is the one
512 // layer engine here carrying a verbosity knob of its own; the level is
513 // forced rather than trusted so a library caller cannot set it.
514 // SolverLN's own reporting is unaffected.
515 opt.layer_ssa.verbose = false;
516 // interlock_method itself is validated by ln_interlock_method, where it is resolved
517 for (char& ch : opt.interlock_refpath_scope)
518 ch = char(std::tolower(static_cast<unsigned char>(ch)));
519 if (opt.interlock_refpath_scope != "merging" && opt.interlock_refpath_scope != "all")
520 throw InputError("Unknown config.interlock_refpath_scope '" +
521 opt.interlock_refpath_scope + "', use 'merging' or 'all'.");
522 if (std::isnan(opt.interlock_maxpaths) || opt.interlock_maxpaths < 0.0)
523 throw InputError("config.interlock_maxpaths must be a non-negative number.");
524 construct();
525 }
526
527 /** Port of getEnsembleAvg: run the iteration and aggregate onto LQN elements. */
529 // getAvgTable.m:611-624 answers the two bound requests WITHOUT running
530 // the fixed point at all: a box bound is a statement about the model,
531 // not about an iterate, so solving first and discarding the answer would
532 // only cost time and invite the two to be confused.
533 if (opt.method == "mw.upper" || opt.method == "mw.lower") return box_bounds();
534 iterate();
535 // the layers of "srvn.ph" carry one class per caller task, so the
536 // per-element results are rebuilt analytically -- see aggregate_ph
537 return is_ph_encoding() ? aggregate_ph() : aggregate();
538 }
539
540 /**
541 * Port of @@SolverLN/getCdfRespT: the per-entry response-time distribution.
542 *
543 * ONLY THE `moment3` METHOD PRODUCES ONE. The reference reacts to an empty
544 * table by re-running getAvg under method='moment3' and restoring the
545 * caller's method afterwards, and so does this: the distribution is the
546 * whole point of that method, and the mean-based update has no distribution
547 * to report, not a coarser one.
548 *
549 * Indexed by entry NUMBER 1..nentries, as the reference's
550 * `entrycdfrespt{eidx - (nhosts+ntasks)}` is.
551 */
552 std::vector<LnCdf> get_cdf_respt() {
553 if (lqn.nentries == 0) return entrycdfrespt;
554 if (entrycdfrespt.size() <= lqn.nentries || entrycdfrespt[1].empty()) {
555 // The distribution pass reads the ROUTING encoding of the activity
556 // graph, which srvn.ph layers do not carry: re-running the iteration
557 // over them would reconstruct the wrong topology rather than a
558 // coarser answer, and returning the empty table would report no
559 // distribution at all. Refuse by name, as the reference does
560 // (@SolverLN/getCdfRespT).
561 if (is_ph_encoding())
562 throw UnsupportedError(
563 "getCdfRespT needs the routing encoding of the activity graph, which "
564 "method='srvn.ph' does not build. Rebuild the solver with "
565 "method='srvn.cs' or method='moment3'.");
566 // BOTH the option and the RESOLVED method have to move. update_metrics
567 // dispatches on `lnmethod`, which build_layers resolved once, so
568 // flipping `opt.method` alone leaves the mean-based update in place and
569 // the table empty -- which is what this getter did between the alias
570 // landing and 2026-08-11. The routing layers already built serve
571 // moment3 unchanged, so only the update pass changes.
572 const std::string saved = opt.method;
573 const std::string saved_resolved = lnmethod;
574 opt.method = "moment3";
575 lnmethod = "moment3";
576 iterate();
577 aggregate();
578 opt.method = saved;
579 lnmethod = saved_resolved;
580 }
581 return entrycdfrespt;
582 }
583
584 /**
585 * Port of @@SolverLN/getTranAvg: the block-diagonal aggregate transient.
586 *
587 * `opt.ln_transient` selects the coupling; both modes return the same
588 * layout, and iteration 0 of the coupled relaxation IS the decoupled
589 * answer.
590 */
592 if (opt.ln_transient == "decoupled") return tran_avg_decoupled();
593 if (opt.ln_transient == "coupled") return tran_avg_coupled();
594 throw InputError("SolverLN: unknown ln_transient mode '" + opt.ln_transient +
595 "' (use 'coupled' or 'decoupled')");
596 }
597
598 /**
599 * Port of @@SolverLN/getSensitivityTable: solve the ensemble, then
600 * concatenate each layer solver's own table under a leading Layer column.
601 *
602 * WHAT THESE DERIVATIVES MEAN, and the reference is emphatic about it: each
603 * entry is a partial derivative WITHIN ITS LAYER, taken with the layer
604 * parameters the fixed point produced held fixed. Perturbing a host demand
605 * moves the think times, populations and demands of every other layer
606 * through the fixed-point map, and that indirect term is NOT included. The
607 * table attributes a bottleneck inside a layer; it does not predict the
608 * effect of a parameter change on the solved layered model.
609 */
611 if (results.empty()) iterate();
612 LnSensTable<T> out;
613 out.layer_methods.assign(ensemble.size(), std::string());
614 out.layer_tables.resize(ensemble.size());
615 for (std::size_t e = 0; e < ensemble.size(); ++e) {
616 sens::SensOptions so = sopt;
617 so.simulation = opt.layer_solver == "ssa";
618 const bool exact_available =
619 opt.layer_solver == "mva" || opt.layer_solver == "nc";
621 ensemble[e], so, exact_available && !fj_tr[e].active(),
622 [this, e]() { return this->solve_layer(e); });
623 out.layer_methods[e] = t.method;
624 for (const sens::SensRow<T>& r : t.rows) {
625 typename LnSensTable<T>::Row row;
626 row.layer = ensemble[e].name;
627 row.station = r.station;
628 row.jobclass = r.jobclass;
629 row.dTput = r.dTput;
630 row.dRespT = r.dRespT;
631 row.dQLen = r.dQLen;
632 row.dUtil = r.dUtil;
633 out.rows.push_back(row);
634 }
635 out.layer_tables[e] = t;
636 }
637 // One branch label per layer, plus a summary that is `mixed` when the
638 // layers did not all take the same branch.
639 for (const std::string& m : out.layer_methods) {
640 if (m.empty()) continue;
641 if (out.method.empty()) out.method = m;
642 else if (out.method != m) out.method = "mixed";
643 }
644 return out;
645 }
646
647 /**
648 * Majumdar-Woodside robust box bounds, reported in the shape of a solution.
649 *
650 * `mw.upper` reports the upper bound of both throughput and processor
651 * utilization, `mw.lower` the lower bound of both. Everything the bound
652 * does not define -- queue length, response time, residence time, arrival
653 * rate -- is left UNDEFINED rather than zeroed.
654 */
656 const bool upper = opt.method != "mw.lower";
657 // Fully qualified: the member `lqn` shadows the namespace of the same
658 // name inside this class, so an unqualified `lqn::` would not compile.
659 const ::line::lqn::LqnBoxBounds<T> b = ::line::lqn::lqn_boxbounds(lqn);
661 const std::size_t N = lqn.nidx;
662 auto blank = [&](std::vector<T>& v, std::vector<bool>& d) {
663 v.assign(N + 1, Tzero());
664 d.assign(N + 1, false);
665 };
666 blank(s.QN, s.defined_Q);
667 blank(s.UN, s.defined_U);
668 blank(s.RN, s.defined_R);
669 blank(s.TN, s.defined_T);
670 blank(s.AN, s.defined_A);
671 blank(s.WN, s.defined_W);
672 for (std::size_t i = 1; i <= N; ++i) {
673 if (b.defined_T[i]) {
674 s.TN[i] = upper ? b.TN_up[i] : b.TN_lo[i];
675 s.defined_T[i] = true;
676 }
677 if (b.defined_U[i]) {
678 s.UN[i] = upper ? b.UN_up[i] : b.UN_lo[i];
679 s.defined_U[i] = true;
680 }
681 }
682 s.iterations = 0;
683 s.converged = true; // a bound needs no fixed point to have converged to
684 s.is_bound = true;
685 return s;
686 }
687
688 std::size_t nlayers() const { return ensemble.size(); }
689 const std::vector<qn::Layer<T>>& layers() const { return ensemble; }
690
691 /**
692 * Port of `LayeredNetwork.layerBlocks`: the block-diagonal layout of the
693 * layers in the aggregate (station x class) view.
694 *
695 * Available as soon as the solver is constructed -- `build_layers` runs in
696 * the constructor -- which is what lets SolverENV compare the shapes of its
697 * stages before solving any of them.
698 */
701 const std::size_t E = ensemble.size();
702 b.roff.assign(E, 0);
703 b.coff.assign(E, 0);
704 b.msz.assign(E, 0);
705 b.ksz.assign(E, 0);
706 for (std::size_t e = 0; e < E; ++e) {
707 b.roff[e] = b.M;
708 b.coff[e] = b.K;
709 b.msz[e] = ensemble[e].nstations;
710 b.ksz[e] = ensemble[e].nclasses;
711 b.M += b.msz[e];
712 b.K += b.ksz[e];
713 }
714 return b;
715 }
716
717 /**
718 * Port of `LayeredNetwork.initFromMarginal`: split an aggregate (M x K)
719 * mean queue-length matrix into per-layer blocks and warm-start each layer
720 * from its own.
721 *
722 * WHY THIS IS RECORDED AND NOT APPLIED HERE, which is the trap the JAR and
723 * python ports both hit: the layered fixed point RESETS its layers as it
724 * converges, so a warm start installed before the solve does not survive
725 * it. The blocks are therefore kept and replayed by `run_layer_transient`,
726 * i.e. after the fixed point and immediately before each layer's transient
727 * -- the steady solve ignores an initial state, the transient does not.
728 *
729 * THE BLOCK IS NOT ROUNDED. The reference rounds a replayed block onto the
730 * integer lattice for every layer engine EXCEPT the fluid one, and the
731 * layered transient exists only over fluid layers (`require_transient_ready`
732 * refuses the rest), so the branch that would round has no reachable case
733 * here. Rounding a fluid state would quantize the very quantity SolverENV's
734 * fixed point is iterating on.
735 *
736 * An empty matrix clears the warm start, so a caller can put the layers
737 * back on their default initial state without rebuilding the solver.
738 */
740 const std::size_t E = ensemble.size();
741 layer_tran_init.assign(E, std::vector<double>());
742 if (n.empty()) return;
743 const LnLayerBlocks b = layer_blocks();
744 if (n.rows() != b.M || n.cols() != b.K)
745 throw InputError(
746 "SolverLN::init_from_marginal: the marginal is " + std::to_string(n.rows()) + "x" +
747 std::to_string(n.cols()) + " where the block-diagonal union of the layers is " +
748 std::to_string(b.M) + "x" + std::to_string(b.K) +
749 "; the aggregate view is layerBlocks', not the LQN element count");
750 for (std::size_t e = 0; e < E; ++e) {
751 const qn::Layer<T>& L = ensemble[e];
753 std::vector<double> y(lay.nstates, 0.0);
754 bool any = false;
755 for (std::size_t i = 0; i < L.nstations && i < b.msz[e]; ++i)
756 for (std::size_t r = 0; r < L.nclasses && r < b.ksz[e]; ++r) {
757 if (!lay.enabled[i][r]) continue;
758 const double q = std::max(0.0, n(b.roff[e] + i, b.coff[e] + r));
759 y[lay.qidx[i][r]] = q;
760 if (q > 0.0) any = true;
761 }
762 // An all-zero block is NOT a warm start: it is the absence of one,
763 // and installing it would empty a closed layer whose population the
764 // fixed point conserves. The reference reaches the same place by
765 // never calling initFromMarginal before the first stage solve.
766 if (any) layer_tran_init[e] = y;
767 }
768 }
769
770 /** Install the explicit output grid of the layered transient; see `LnOptions::tran_grid`. */
771 void set_tran_grid(const std::vector<double>& g) { opt.tran_grid = g; }
772
773 /** Diagnostic access to the per-iteration layer results, for the regression. */
774 const std::vector<std::vector<LayerResult<T>>>& iteration_results() const { return results; }
775 const std::vector<T>& state_servt() const { return servt; }
776 const std::vector<T>& state_residt() const { return residt; }
777 const std::vector<T>& state_tput() const { return tput; }
778 const std::vector<T>& state_thinkt() const { return thinkt; }
779 const std::vector<T>& state_callservt() const { return callservt; }
780 const std::vector<T>& state_callresidt() const { return callresidt; }
781 const std::vector<Distrib<T>>& state_servtproc() const { return servtproc; }
782 const std::vector<Distrib<T>>& state_thinktproc() const { return thinktproc; }
783 const std::vector<Distrib<T>>& state_callservtproc() const { return callservtproc; }
784 const std::vector<T>& state_util() const { return util; }
785 /** Per iteration, the layer-summed max queue-length change the convergence test reads (index 0 unused). */
786 const std::vector<double>& state_maxitererr() const { return maxitererr; }
787 /** The method the layers were built for: "srvn.ph", "srvn.cs" or "moment3". */
788 const std::string& state_lnmethod() const { return lnmethod; }
789 /**
790 * The interlock tracking method this run USES: "ilrate", "refpath" or
791 * "none". Recorded by build_layers and, when no layer merged a reference
792 * chain, demoted from "refpath" to "ilrate" at the end of construct.
793 */
794 const std::string& state_interlock_method() const { return interlockMethod; }
795 /** Per reference-path stage key, the stage mean the last iteration installed. */
796 const std::vector<double>& state_refpath_stages() const { return refpath_stage_prev; }
797
798 /**
799 * Port of SolverLN.interlockMethodFor: the method a solve would use, asked
800 * through the same query build_layers asks. Like the reference it does not
801 * see the construct-time demotion of a refpath that merged nothing; that
802 * recorded answer is state_interlock_method().
803 */
805 return ln_interlock_method(lqn, opt.interlock_method, opt.interlocking, lnmethod);
806 }
807
808private:
809 // state
812
813 std::vector<qn::Layer<T>> ensemble; ///< compacted, layer e is ensemble[e]
814 std::vector<long> idxhash; ///< (nidx+1) element -> layer index, -1 if none
815 std::vector<bool> ignore; ///< (nidx+1) element in a component with no ref task
816
817 // update maps, rows of [layerElement, elementOrCall, node, class]
818 struct UpdRow {
819 std::size_t idx, aidx, node, cls;
820 };
821 std::vector<UpdRow> servt_map, thinkt_map, call_map, actthinkt_map;
822 /**
823 * Source arrival of an async call class, `aidx` holding the call index.
824 *
825 * Kept apart from call_map because the two update different things about
826 * the same class: call_map carries the SERVICE at the server replicas,
827 * this one the arrival RATE at the Source, which is the caller activity's
828 * throughput and so moves with the fixed point. Entry open arrivals have
829 * no row here at all -- theirs is exogenous and never reseeded.
830 */
831 std::vector<UpdRow> arv_call_map;
832 /** [idx, tidx_caller, eidx, nodefrom, nodeto, classfrom, classto] */
833 struct RouteRow {
834 std::size_t idx, tidx_caller, eidx, nodefrom, nodeto, cfrom, cto;
835 };
836 std::vector<RouteRow> route_map;
837 std::vector<std::size_t> unique_route_idx;
838 std::vector<std::size_t> route_reset, svc_reset;
839
840 /** The interlock tracking method in force, resolved once by build_layers. */
841 std::string interlockMethod = "ilrate";
842 /**
843 * Reference-path stage rows, refpath_classes_updmap of the reference:
844 * [idx, node, class] name the stage, `kind` 1 = residt(elem), 2 =
845 * callresidt(elem)/entryvisits(fromentry), 3 = threadWait(elem), each term
846 * multiplied by `sign`.
847 */
848 struct RefPathRow {
849 std::size_t idx, node, cls;
850 int kind;
851 std::size_t elem;
852 double sign;
853 std::size_t fromentry;
854 };
855 std::vector<RefPathRow> refpath_map;
856 /** Unique (idx, node, class) of refpath_map, sorted, and each row's key. */
857 std::vector<std::array<std::size_t, 3>> refpath_keys;
858 std::vector<std::size_t> refpath_group;
859 /** Per key, the previous iteration's stage mean; NaN until the first update. */
860 std::vector<double> refpath_stage_prev;
861 std::vector<T> refpath_stage_prev_v;
862 /**
863 * Per layer ELEMENT, the per-class normalising TASK class (1-based, 0 = use
864 * the chain's reference class), attribute.normclass of the reference. Only
865 * the refpath layers fill it; see ln_layer_refcell.
866 */
867 std::map<std::size_t, std::vector<std::size_t>> layer_normclass;
868 /** Invocations of an entry per invocation of its task; starts at 1. */
869 std::vector<T> entryvisits;
870
871 Matrix<double> njobs; ///< (NT+1 x NT+1) population of caller tidx in layer idx
872
873 // per-element metric state
874 std::vector<T> servt, residt, tput, util, thinkt;
875 std::vector<T> callservt, callresidt;
876 /**
877 * Phase-2 state, the reference's hasPhase2 / servt_ph1 / servt_ph2 /
878 * prOvertake. The split is of `servt` and exists only while `has_phase2`;
879 * `prOvertake` is indexed by entry NUMBER 1..nentries, as the reference
880 * indexes it, not by element index.
881 */
882 bool has_phase2 = false;
883 std::vector<T> servt_ph1, servt_ph2, prOvertake;
884 /** Last entry service and its phase-2 part, resolve_entry_service -> overtaking split. */
885 std::vector<T> entry_servt_last, entry_servt_ph2;
886 /** Tasks whose own layer was collapsed to one representative replica. */
887 std::set<std::size_t> single_replica_tasks;
888 std::vector<Distrib<T>> servtproc, thinkproc, thinktproc, tputproc, callservtproc;
889 /**
890 * `moment3` state: the response-time CDF of each activity (by element) and
891 * of each call, the fitted per-entry law and the CDF read off it. All empty
892 * under the default method, which never forms a distribution at all.
893 */
894 std::vector<fluid::FluidPassage> servtcdf, callservtcdf;
895 std::vector<LnCdf> entrycdfrespt;
896 std::vector<mam::AphPair<T>> entryproc;
897 /** Per layer, the response-time CDFs of the converged pass; a lazy cache. */
898 std::vector<std::vector<std::vector<fluid::FluidPassage>>> cdf_repo;
899 std::vector<double> servt_prev, residt_prev, tput_prev, thinkt_prev;
900 std::vector<double> callservt_prev, callresidt_prev;
901 std::vector<T> servt_prev_v, residt_prev_v, tput_prev_v, thinkt_prev_v, callservt_prev_v;
902 Matrix<T> servtmatrix; ///< (nidx+ncalls) x (nidx+ncalls) entry reachability
903
904 // interlock tables
905 Matrix<T> il_all, il_ph1;
906 std::vector<std::vector<std::size_t>> il_common_entries, il_src_all, il_src_ph2;
907 /**
908 * Per-layer interlock matrix of Franks (1999), Eq. (4.7), CLASS-indexed. A host layer
909 * whose engine carries the correction inside its own MVA takes the matrix instead of
910 * having its residence times scaled after the fact; empty for every other layer.
911 */
912 std::vector<std::vector<std::vector<double>>> layer_interlock;
913 std::vector<double> il_num_sources;
914
915 std::vector<std::vector<LayerResult<T>>> results; ///< [iteration][layer]
916 /**
917 * The newest iterate as the layers returned it, before `converged` folded the moving
918 * average into it. MATLAB averages a copy (iterate.m keeps its own raw history and
919 * hands it back to self.results every pass), so the window, the error test and the
920 * averaged servt in updateMetricsDefault all read RAW past iterates; this puts the raw
921 * values back once the iterate is no longer the newest.
922 */
923 std::vector<LayerResult<T>> results_raw_last;
924 bool results_raw_valid = false;
925 std::vector<Matrix<T>> layer_init_sol;
926 /**
927 * Per layer, the ODE state `init_from_marginal` recorded, replayed by
928 * `run_layer_transient`. Empty for a layer with no warm start.
929 */
930 std::vector<std::vector<double>> layer_tran_init;
931 /**
932 * Per layer, the fork-join transform SolverMVA solves instead of the layer
933 * itself (inactive for a layer with no fork), and the auxiliary arrival
934 * rates its fixed point iterates on. See fj_mmt.h.
935 *
936 * `fj_lambda` is the `self.fjForkLambda` of the reference and is deliberately
937 * NOT reset between outer iterations: the transform is rebuilt cold on every
938 * outer pass but the iterate it converged to last time is a far better
939 * starting point than FineTol, and the reference warm-starts it the same way
940 * (options.config.fj_warmstart, true by default).
941 */
942 std::vector<mva::FjMmt<T>> fj_tr;
943 std::vector<std::vector<T>> fj_lambda;
944 double relax_omega = 1.0;
945 /** Live only while a NOISY layer engine is in force; null otherwise. */
946 std::shared_ptr<LnStochController<T>> stoch_ctl;
947 long averagingstart = -1;
948 bool hasconverged = false;
949 /** method='moment3': true once the distribution pass has formed the entry laws. */
950 bool moment_pass_done = false;
951 std::vector<double> maxitererr;
952 int iterations_done = 0;
953 bool did_converge = false;
954
955 std::size_t NT() const { return lqn.tshift + lqn.ntasks; }
956
957 static T Tzero() { return num_traits<T>::from_int(0); }
958 static T Tone() { return num_traits<T>::from_int(1); }
959 static double dbl(const T& x) { return num_traits<T>::to_double(x); }
960
961 // -----------------------------------------------------------------------
962 // construct
963 // -----------------------------------------------------------------------
964 void construct() {
965 // Forwarding rewrite, SolverLN.m:162-164: flatten every forwarding
966 // chain into caller-side pseudo rendezvous calls BEFORE anything else
967 // reads lqn, so build_layers/detect_phase2/reject_unsupported all see
968 // plain rendezvous arcs. The raw FWD calls survive in the struct but
969 // must not contribute blocking after this point (lqn_helpers.h).
971 reject_unsupported();
972 detect_phase2();
973
974 const std::size_t N = lqn.nidx;
975 ignore.assign(N + 1, false);
976 // weakly connected components of graph + graph'; a component with no
977 // reference task is unreachable and its elements are ignored
978 std::vector<long> comp(N + 1, -1);
979 long ncomp = 0;
980 for (std::size_t v = 1; v <= N; ++v) {
981 if (comp[v] >= 0) continue;
982 std::vector<std::size_t> stack{v};
983 comp[v] = ncomp;
984 while (!stack.empty()) {
985 const std::size_t u = stack.back();
986 stack.pop_back();
987 for (std::size_t w : lqn.graph.succ(u))
988 if (comp[w] < 0) {
989 comp[w] = ncomp;
990 stack.push_back(w);
991 }
992 for (std::size_t w : lqn.graph.pred(u))
993 if (comp[w] < 0) {
994 comp[w] = ncomp;
995 stack.push_back(w);
996 }
997 }
998 ++ncomp;
999 }
1000 if (ncomp > 1) {
1001 std::vector<bool> has_ref(ncomp, false);
1002 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1003 const std::size_t tidx = lqn.tshift + t;
1004 if (lqn.sched[tidx] == SchedStrategy::REF) has_ref[comp[tidx]] = true;
1005 }
1006 for (std::size_t v = 1; v <= N; ++v)
1007 if (!has_ref[comp[v]]) ignore[v] = true;
1008 }
1009
1010 servtproc.assign(N + 1, Distrib<T>::disabled_dist());
1011 thinkproc.assign(N + 1, Distrib<T>::disabled_dist());
1012 thinktproc.assign(N + 1, Distrib<T>::disabled_dist());
1013 tputproc.assign(N + 1, Distrib<T>::disabled_dist());
1014 for (std::size_t i = 1; i <= N; ++i) {
1015 servtproc[i] = lqn.hostdem[i];
1016 thinkproc[i] = lqn.think[i];
1017 }
1018 callservtproc.assign(lqn.ncalls + 1, Distrib<T>::disabled_dist());
1019 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
1020 callservtproc[c] = lqn.hostdem[lqn.callpair_dst[c]];
1021
1022 // Invocations of an entry per invocation of its task, filled by
1023 // resolve_entry_service; 1 because iteration 1 runs before any measurement
1024 entryvisits.assign(N + 1, Tone());
1025 refpath_map.clear();
1026 layer_normclass.clear();
1027
1028 njobs = Matrix<double>(NT() + 1, NT() + 1, 0.0);
1029 build_layers();
1030
1031 // layers whose routing or service must be re-derived after an update
1032 std::vector<bool> rr(NT() + 1, false), sr(NT() + 1, false);
1033 for (const RouteRow& r : route_map) rr[r.idx] = true;
1034 for (const UpdRow& r : thinkt_map) sr[r.idx] = true;
1035 for (const UpdRow& r : call_map) sr[r.idx] = true;
1036 // refpath REMOVES the think-time row of every merged caller and the
1037 // client-side call row of every re-expanded descent, so a layer whose only
1038 // per-iteration change was one of those would otherwise never be
1039 // refresh_rates()'d and would freeze at its build-time seed. Its stage
1040 // rows put it back.
1041 for (const RefPathRow& r : refpath_map) sr[r.idx] = true;
1042 // a moved arrival rate reweights the Sink closure, which only
1043 // refresh_chains rebuilds, so these layers need the routing reset
1044 for (const UpdRow& r : arv_call_map) rr[r.idx] = true;
1045 for (std::size_t i = 1; i <= NT(); ++i) {
1046 if (rr[i] && idxhash[i] >= 0) route_reset.push_back(std::size_t(idxhash[i]));
1047 if (sr[i] && idxhash[i] >= 0) svc_reset.push_back(std::size_t(idxhash[i]));
1048 }
1049 std::sort(route_reset.begin(), route_reset.end());
1050 route_reset.erase(std::unique(route_reset.begin(), route_reset.end()), route_reset.end());
1051 std::sort(svc_reset.begin(), svc_reset.end());
1052 svc_reset.erase(std::unique(svc_reset.begin(), svc_reset.end()), svc_reset.end());
1053
1054 // The reference-path stage keys, unique((idx, node, class), 'rows').
1055 refpath_keys.clear();
1056 refpath_group.clear();
1057 for (const RefPathRow& r : refpath_map) refpath_keys.push_back({r.idx, r.node, r.cls});
1058 std::sort(refpath_keys.begin(), refpath_keys.end());
1059 refpath_keys.erase(std::unique(refpath_keys.begin(), refpath_keys.end()),
1060 refpath_keys.end());
1061 for (const RefPathRow& r : refpath_map) {
1062 const std::array<std::size_t, 3> key{r.idx, r.node, r.cls};
1063 refpath_group.push_back(std::size_t(
1064 std::lower_bound(refpath_keys.begin(), refpath_keys.end(), key) -
1065 refpath_keys.begin()));
1066 }
1067 if (refpath_map.empty() && interlockMethod == "refpath") {
1068 // Not one layer merged a reference chain, so the build is the one
1069 // 'none' produces, and so would the solve be: post() suppresses the
1070 // rate discount whenever the method is 'refpath'. Switching a
1071 // correction off and declining to replace it is strictly worse than
1072 // either method alone, so the method is demoted here, before init()
1073 // decides whether to build the interlock tables.
1074 interlockMethod = "ilrate";
1075 }
1076 refpath_stage_prev.assign(refpath_keys.size(), std::numeric_limits<double>::quiet_NaN());
1077 refpath_stage_prev_v.assign(refpath_keys.size(), Tzero());
1078 }
1079
1080 /** Reject, by name, every construct this port does not implement. */
1081 // Phase-2 detection, SolverLN.m:165-173. Deliberately NOT inside
1082 // reject_unsupported: that method is const because it only refuses, and
1083 // detection sets state. Marking the flag mutable would have compiled and
1084 // left a validator that silently mutates the solver.
1085 void detect_phase2() {
1086 has_phase2 = false;
1087 for (std::size_t a = 1; a <= lqn.nacts; ++a)
1088 if (lqn.actphase[a] > 1) has_phase2 = true;
1089 }
1090
1091 /**
1092 * True when `eidx` is the destination of a raw forwarding call.
1093 *
1094 * buildLayersRecursive.m:186-192 keeps this test even though
1095 * lqn_fwd_rendezvous has already run, and so must this port:
1096 * the rewrite walks chains out of SYNC calls only, so an entry whose
1097 * forwarder is reached asynchronously gets no pseudo arc and would
1098 * otherwise be dropped from the layer along with all of its work.
1099 */
1100 bool is_fwd_target(std::size_t eidx) const {
1101 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
1102 if (lqn.calltype[c] == CallType::FWD && lqn.callpair_dst[c] == eidx) return true;
1103 return false;
1104 }
1105
1106 /**
1107 * Total exogenous rate into the entries of task `tidx`, zero unless the arrival is
1108 * the ONLY way in.
1109 *
1110 * A task nobody calls has no task layer, so update_think_times never sets its
1111 * surrogate delay and its caller class cycles against an Immediate one. Carrying the
1112 * arrival as an open stream ON TOP of that unthrottled chain loads the host twice:
1113 * lqn_open_arrival read the processor at 0.68 where lqns, lqsim and LDES all give
1114 * 0.32. The chain is the representation that honours the thread pool, so build_layer
1115 * drops the stream for these tasks and update_think_times closes the chain on this
1116 * rate, as the reference does for a forwarding target. With a caller or a forwarding
1117 * source the stream needs a class of its own and this returns 0.
1118 */
1119 double open_arrival_rate_of(std::size_t tidx) const {
1120 if (lqn.isref[tidx]) return 0.0;
1121 for (std::size_t e : lqn.entriesof[tidx])
1122 if (lqn.issynccaller.any_col(e) || lqn.isasynccaller.any_col(e) || is_fwd_target(e))
1123 return 0.0;
1124 double rate = 0.0;
1125 for (std::size_t e : lqn.entriesof[tidx]) {
1126 if (!lqn.has_arrival[e]) continue;
1127 const double m = dbl(lqn.arrival[e].mean);
1128 if (std::isfinite(m) && m > GlobalConstants::FineTol) rate += 1.0 / m;
1129 }
1130 return rate;
1131 }
1132
1133 /** True when layer `e` carries a Cache node. */
1134 bool has_cache_node(std::size_t e) const {
1135 for (std::size_t n = 0; n < ensemble[e].nodes.size(); ++n)
1136 if (ensemble[e].nodes[n].nodetype == NodeType::Cache) return true;
1137 return false;
1138 }
1139
1140 /** True when `aidx` is an activity bound to an entry nobody calls synchronously. */
1141 bool async_only_activity(std::size_t aidx) const {
1142 if (aidx <= lqn.ashift || aidx > lqn.ashift + lqn.nacts) return false;
1143 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
1144 const std::size_t eidx = lqn.eshift + e;
1145 if (lqn.graph.get(eidx, aidx) == Tzero()) continue;
1146 return lqn.isasynccaller.any_col(eidx) && !lqn.issynccaller.any_col(eidx);
1147 }
1148 return false;
1149 }
1150
1151 /**
1152 * Reject, by name, every construct this port does not implement.
1153 *
1154 * NOTHING IS LEFT. Forwarding, asynchronous calls, entry open arrivals,
1155 * admission constraints, cache tasks and setup tasks are each solved.
1156 * The method is kept rather than deleted because it is the place a new
1157 * refusal belongs, and because a layer-build refusal must be raised HERE,
1158 * before construct() has read anything out of the struct, rather than
1159 * halfway through building a layer.
1160 */
1161 void reject_unsupported() const {}
1162
1163 /**
1164 * The interlock tracking method, resolved ONCE for the encoding just chosen,
1165 * for the same reason lnmethod is: what update_populations and init_interlock
1166 * do can never disagree with what was built into the layers.
1167 */
1168 void resolve_interlock_method() {
1169 const LnInterlockChoice ch =
1170 ln_interlock_method(lqn, opt.interlock_method, opt.interlocking, lnmethod);
1171 interlockMethod = ch.method;
1172 if (!ch.why.empty()) std::cerr << "[LINE] Warning: " << ch.why << "\n";
1173 }
1174
1175 // -----------------------------------------------------------------------
1176 // buildLayers
1177 // -----------------------------------------------------------------------
1178 void build_layers() {
1179 // Method resolution. A method name carries both the LAYERING and the
1180 // ENCODING: "srvn.ph" replaces the routing encoding of the activity graph
1181 // by a composed phase-type server law, "srvn" is the alias that takes it
1182 // where it can serve the model and "srvn.cs" otherwise. The choice is
1183 // made ONCE, here, and every later dispatch reads lnmethod.
1184 // See _kb/06-solver-catalog.md (LN section).
1185 const std::string requested = ln_requested_method(opt.method);
1186 assert_call_groups(requested == "flat.cs");
1187 if (requested == "flat.cs") {
1188 lnmethod = "flat.cs";
1189 resolve_interlock_method();
1190 build_flat_layer();
1191 return;
1192 }
1193 if (requested == "flat.ph") {
1194 // the squashed layering with the composed law: ONE submodel holding
1195 // every server, and a caller visiting each of them once per
1196 // invocation. The feature gate is the srvn.ph one plus the refusals a
1197 // single submodel carries -- see ph_flat_server_set.
1198 ph_laws_ready = false;
1199 lnmethod = "flat.ph";
1200 build_layers_ph(true);
1201 return;
1202 }
1203 if (requested == "srvn.ph" || requested == "srvn") {
1204 ph_laws_ready = false;
1205 if (requested == "srvn.ph" || probe_srvn_ph()) {
1206 lnmethod = "srvn.ph";
1207 build_layers_ph();
1208 return;
1209 }
1210 }
1211 lnmethod = (requested == "moment3") ? "moment3" : "srvn.cs";
1212 resolve_interlock_method();
1213 std::vector<qn::Layer<T>> raw(NT() + 1);
1214 std::vector<bool> present(NT() + 1, false);
1215
1216 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx) {
1217 if (ignore[hidx]) continue;
1218 build_layer(raw[hidx], {hidx}, lqn.tasksof[hidx], true, false);
1219 present[hidx] = true;
1220 }
1221 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1222 const std::size_t tidx = lqn.tshift + t;
1223 if (ignore[tidx] || lqn.isref[tidx]) continue;
1224 bool any_caller = lqn.iscaller.any_row(tidx) || lqn.iscaller.any_col(tidx);
1225 if (!any_caller) continue;
1226 // tasks that call some entry of tidx
1227 std::vector<std::size_t> callers;
1228 for (std::size_t ct = 1; ct <= lqn.ntasks; ++ct) {
1229 const std::size_t c = lqn.tshift + ct;
1230 bool calls = false;
1231 for (std::size_t e : lqn.entriesof[tidx])
1232 if (lqn.iscaller.get(c, e)) calls = true;
1233 if (calls) callers.push_back(c);
1234 }
1235 if (callers.empty()) continue;
1236 build_layer(raw[tidx], {tidx}, callers, false, false);
1237 present[tidx] = true;
1238 }
1239
1240 idxhash.assign(lqn.nidx + 1, -1);
1241 long next = 0;
1242 for (std::size_t i = 1; i <= NT(); ++i)
1243 if (present[i]) {
1244 idxhash[i] = next++;
1245 ensemble.push_back(std::move(raw[i]));
1246 }
1247 layer_init_sol.assign(ensemble.size(), Matrix<T>());
1248 build_fork_views();
1249 }
1250
1251 /**
1252 * The processors and called tasks that become stations of the flat layer.
1253 *
1254 * Squashing is refused rather than approximated where an element carries
1255 * state that only a submodel of its own can hold: a REPLICATED element
1256 * would need one station per copy inside a layer whose routing addresses it
1257 * once, a CACHE task needs the Cache node in the host layer its reads queue
1258 * at, and a SETUP task's delay-off belongs to the station that powers down.
1259 */
1260 /**
1261 * Reject a routed call group under any layering or layer solver that cannot
1262 * carry it.
1263 *
1264 * Two conditions, and both are refusals rather than degradations. The
1265 * squashed layering is needed because under `srvn` each target lives in a
1266 * submodel of its own and is replaced, in the caller's submodel, by a
1267 * surrogate delay -- no node ever has arcs to more than one of them, so
1268 * there is nothing to dispatch among. A layer solver that resolves the
1269 * strategy from the STATE is needed because `refresh_routing` expands
1270 * RROBIN and JSQ into a uniform probability split for the matrix solvers,
1271 * and returning that split under a round-robin label misreports a
1272 * deterministic policy as a coin. In this port only `ssa` resolves them.
1273 */
1274 void assert_call_groups(bool flat) const {
1275 if (lqn.callgroups.empty()) return;
1276 if (!flat)
1277 throw UnsupportedError(
1278 "Call groups routed by a routing strategy require the squashed layering; use "
1279 "method='flat'. Under srvn the targets never share a submodel, so the dispatch "
1280 "order cannot be represented.");
1281 if (opt.layer_solver != "ssa")
1282 throw UnsupportedError(
1283 "Routed call groups need a layer solver that resolves the strategy from the "
1284 "state; set layer_solver='ssa'. MVA, NC and FLD read the routing matrix, into "
1285 "which refresh_routing has expanded the strategy as a uniform split, and would "
1286 "return that split under a round-robin or JSQ label.");
1287 }
1288
1289 std::vector<std::size_t> flat_server_set() const {
1290 std::vector<std::size_t> servers;
1291 for (std::size_t i = 1; i <= NT(); ++i) {
1292 if (lqn.repl[i] > 1.0)
1293 throw UnsupportedError(
1294 "Flat layering does not support replicated processors or tasks, use the "
1295 "default 'srvn' layering.");
1296 if (lqn.iscache[i])
1297 throw UnsupportedError(
1298 "Flat layering does not support cache tasks, use the default 'srvn' "
1299 "layering.");
1300 if (lqn.hassetup[i])
1301 throw UnsupportedError(
1302 "Flat layering does not support setup tasks, use the default 'srvn' "
1303 "layering.");
1304 }
1305 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx)
1306 if (!ignore[hidx] && !lqn.tasksof[hidx].empty()) servers.push_back(hidx);
1307 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1308 const std::size_t tidx = lqn.tshift + t;
1309 if (ignore[tidx] || lqn.isref[tidx]) continue;
1310 if (!lqn.iscaller.any_row(tidx) && !lqn.iscaller.any_col(tidx)) continue;
1311 bool has_task_caller = false;
1312 for (std::size_t eidx : lqn.entriesof[tidx])
1313 for (std::size_t c = 1; c <= lqn.ntasks; ++c)
1314 if (lqn.iscaller.get(lqn.tshift + c, eidx)) has_task_caller = true;
1315 if (has_task_caller) servers.push_back(tidx);
1316 }
1317 if (servers.empty())
1318 throw InputError(
1319 "Flat layering found no server: the model has no processor with tasks.");
1320 return servers;
1321 }
1322
1323 /**
1324 * Build the single squashed layer holding every processor and called task.
1325 *
1326 * Every served element resolves to layer 0, which is at once the host layer
1327 * and the task layer, so a consumer that walks `idxhash` finds the same
1328 * network for a processor and for a task and must read the station off
1329 * `server_idx_of` rather than off `serverIdx`.
1330 */
1331 void build_flat_layer() {
1332 const std::vector<std::size_t> flat_servers = flat_server_set();
1333 std::vector<std::size_t> flat_callers;
1334 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1335 const std::size_t tidx = lqn.tshift + t;
1336 if (!ignore[tidx]) flat_callers.push_back(tidx);
1337 }
1338 qn::Layer<T> layer;
1339 build_layer(layer, flat_servers, flat_callers, false, true);
1340 ensemble.clear();
1341 ensemble.push_back(std::move(layer));
1342 idxhash.assign(lqn.nidx + 1, -1);
1343 for (std::size_t s : flat_servers) idxhash[s] = 0;
1344 layer_init_sol.assign(ensemble.size(), Matrix<T>());
1345 build_fork_views();
1346 }
1347
1348 /**
1349 * Build, for every layer that has a fork, the transformed model SolverMVA
1350 * actually solves, and seed its auxiliary arrival rates.
1351 */
1352 void build_fork_views() {
1353 fj_tr.assign(ensemble.size(), mva::FjMmt<T>());
1354 fj_lambda.assign(ensemble.size(), {});
1355 for (std::size_t e = 0; e < ensemble.size(); ++e) {
1356 if (!ensemble[e].has_fork()) continue;
1357 // fj_mmt mints its own Source/Sink pair and would silently overwrite
1358 // sourceIdx/sinkNode, detaching the open stream already routed there.
1359 // NOT a parity gap: the reference does not solve this combination
1360 // either, it fails inside the layer solve with "Arrays have
1361 // incompatible sizes" (checked 2026-07-29 on a fork+async model).
1362 // Refusing by name is the better answer, so this stays.
1363 if (ensemble[e].sourceIdx != 0)
1364 throw UnsupportedError(
1365 "SolverLN: layer '" + ensemble[e].name +
1366 "' carries both an AND fork and an open stream (an async call or an entry "
1367 "arrival); the fork-join transform needs a Source of its own");
1368 fj_tr[e] = mva::fj_mmt(ensemble[e]);
1369 fj_lambda[e].assign(fj_tr[e].V.classes.size() + 1,
1370 num_traits<T>::from_double(GlobalConstants::FineTol));
1371 }
1372 }
1373
1374 /**
1375 * Port of matlab/src/lang/layered/lqn_dep_layer_handle.m: lift a
1376 * per-operand rate handle onto the classes of a layer station.
1377 *
1378 * COLS[j] lists the layer classes (0-based) through which operand j occupies
1379 * the station; CHAINCOLS is the same list in the CHAIN index space, filled
1380 * once refresh_chains has run. Solvers evaluate the handle in either space
1381 * -- CTMC and the exact recursions pass a per-class vector, the AMVA and NC
1382 * chain recursions a per-chain one -- so the handle reads the LENGTH of what
1383 * it is given to decide, aggregates the operand populations in that space,
1384 * and answers a vector of the same length, since the caller indexes the
1385 * answer with the index it passed in. An index belonging to no operand keeps
1386 * the neutral scaling 1.
1387 */
1388 static lang::CdScaling<T> layer_dep_handle(
1389 const lang::CdScaling<T>& f, const std::vector<std::vector<std::size_t>>& cols,
1390 const std::shared_ptr<std::vector<std::vector<std::size_t>>>& chaincols, std::size_t R) {
1391 return [f, cols, chaincols, R](const std::vector<T>& n) -> std::vector<T> {
1392 const std::size_t L = n.size();
1393 const std::vector<std::vector<std::size_t>>& use =
1394 (L == R || chaincols->empty()) ? cols : *chaincols;
1395 std::vector<T> nop(use.size(), num_traits<T>::from_int(0));
1396 for (std::size_t j = 0; j < use.size(); ++j)
1397 for (std::size_t k = 0; k < use[j].size(); ++k)
1398 if (use[j][k] < L) nop[j] = T(nop[j] + n[use[j][k]]);
1399 const std::vector<T> w = f(nop);
1400 std::vector<T> v(L, num_traits<T>::from_int(1));
1401 if (w.empty()) return v;
1402 for (std::size_t j = 0; j < use.size(); ++j) {
1403 const T wj = w[j < w.size() ? j : w.size() - 1];
1404 for (std::size_t k = 0; k < use[j].size(); ++k)
1405 if (use[j][k] < L) v[use[j][k]] = wj;
1406 }
1407 return v;
1408 };
1409 }
1410
1411 /** Spread a per-operand peak rate onto the layer classes. Twin of layerPeak. */
1412 static std::vector<T> layer_peak(const std::vector<T>& peakPerOperand,
1413 const std::vector<std::vector<std::size_t>>& cols,
1414 std::size_t R) {
1415 std::vector<T> peak(R, num_traits<T>::from_int(1));
1416 if (peakPerOperand.empty()) return peak;
1417 for (std::size_t j = 0; j < cols.size(); ++j) {
1418 const T pj = peakPerOperand[j < peakPerOperand.size() ? j : peakPerOperand.size() - 1];
1419 for (std::size_t k = 0; k < cols[j].size(); ++k)
1420 if (cols[j][k] < R) peak[cols[j][k]] = pj;
1421 }
1422 return peak;
1423 }
1424
1425 /**
1426 * Give every class of a host layer the priority of the task that owns it.
1427 *
1428 * Applies to each processor in idxSet that schedules by priority (HOL, 'pri', ...).
1429 * lqns serves a LARGER task priority first while a class priority of 0 is the highest,
1430 * so the class priority is max(prio on the host) - prio(task). A layer solver with a
1431 * priority arm (MVA, CTMC, ...) then applies it. Without this every class tied at 0
1432 * and a priority processor ran as FCFS. Port of MATLAB ln_host_task_priorities.m.
1433 */
1434 void apply_host_task_priorities(qn::Layer<T>& m, const std::vector<std::size_t>& idxSet) const {
1435 using qn::SchedStrategy;
1436 auto is_prio = [](SchedStrategy s) {
1437 return s == SchedStrategy::HOL || s == SchedStrategy::FCFSPRPRIO ||
1438 s == SchedStrategy::FCFSPIPRIO || s == SchedStrategy::LCFSPRIO ||
1439 s == SchedStrategy::LCFSPRPRIO || s == SchedStrategy::LCFSPIPRIO ||
1440 s == SchedStrategy::PSPRIO || s == SchedStrategy::DPSPRIO ||
1441 s == SchedStrategy::GPSPRIO;
1442 };
1443 const std::size_t R = m.classes.size();
1444 std::vector<std::size_t> owner(R + 1, 0); // 1-based class -> owning task, 0 = none
1445 auto own = [&](std::size_t k, std::size_t t) {
1446 if (k >= 1 && k <= R && owner[k] == 0) owner[k] = t;
1447 };
1448 for (const auto& a : m.attr_tasks) own(a.first, a.second);
1449 for (const auto& a : m.attr_entries) own(a.first, lqn.parent[a.second]);
1450 for (const auto& a : m.attr_activities) own(a.first, lqn.parent[a.second]);
1451 for (const auto& a : m.attr_calls) own(a[0], lqn.parent[a[2]]);
1452 for (std::size_t s : idxSet) {
1453 if (s < 1 || s > lqn.nhosts || !is_prio(lqn.sched[s])) continue;
1454 int pmax = 0;
1455 for (std::size_t t = lqn.tshift + 1; t <= lqn.tshift + lqn.ntasks; ++t)
1456 if (lqn.parent[t] == s) pmax = std::max(pmax, lqn.prio[t]);
1457 for (std::size_t k = 1; k <= R; ++k)
1458 if (owner[k] != 0 && lqn.parent[owner[k]] == s)
1459 m.classes[k - 1].prio = pmax - lqn.prio[owner[k]];
1460 }
1461 }
1462
1463 /**
1464 * Port of buildLayersRecursive.
1465 *
1466 * The layer holds a client Delay carrying the callers' think times and the
1467 * time they spend elsewhere, and `nreplicas` copies of the server station.
1468 * A class is created for every task, entry, activity and call that the
1469 * callers' activity graphs reach, and the graph traversal lays down the
1470 * routing that moves a job between those classes.
1471 */
1472 void build_layer(qn::Layer<T>& m, const std::vector<std::size_t>& idxSet,
1473 const std::vector<std::size_t>& callers, bool ishostlayer, bool flat) {
1474 const T one = Tone();
1475 // The layer key: the model name, the ensemble slot and the column every
1476 // update map is written under. Under `srvn` it is the layer's only
1477 // served element; under `flat` it is the first of them.
1478 const std::size_t idx = idxSet[0];
1479 m.name = flat ? lqn.hashnames[idx] + ".Flat" : lqn.hashnames[idx];
1480 m.flat = flat;
1481
1482 const double rawrepl = lqn.repl[idx];
1483 std::size_t nreplicas = 1;
1484 if (!flat && rawrepl > 1.0 && !callers.empty()) {
1485 // A replicated server is pooled into ONE station instead of one per
1486 // replica when every caller already addresses all of its replicas:
1487 // on a host layer that is the callers replicating in step with it,
1488 // on a task layer it is a declared fan-out covering every replica.
1489 // With no fan-out declared the lookup is 0 < rawrepl and the layer
1490 // materialises the replicas, which is the reference's answer too.
1491 bool reduce = true;
1492 if (ishostlayer) {
1493 for (std::size_t c : callers)
1494 if (lqn.repl[c] != rawrepl) reduce = false;
1495 } else {
1496 for (std::size_t c : callers)
1497 if (lqn.fanout_at(c, idx) < rawrepl) reduce = false;
1498 }
1499 nreplicas = reduce ? 1 : static_cast<std::size_t>(std::llround(rawrepl));
1500 if (reduce && !ishostlayer) single_replica_tasks.insert(idx);
1501 }
1502 const bool reduce_fanout = (nreplicas == 1 && rawrepl > 1.0 && !callers.empty());
1503 const std::vector<double>& mult = lqn.maxmult;
1504
1505 // A client Delay exists unless the layer is a task layer reached only
1506 // by asynchronous callers, which this port refuses upstream.
1507 m.clientIdx = m.add_station(qn::Station<T>{"Clients", NodeType::Delay, SchedStrategy::INF,
1508 std::numeric_limits<double>::infinity(), false, 0});
1509 const std::size_t clientNode = m.node_of_station(m.clientIdx);
1510 m.serverIdx = m.clientIdx + 1;
1511 // One station (times its replicas) per SERVED ELEMENT of this layer.
1512 // Under `srvn` that is one element and `srv[idx] == server`; under
1513 // `flat` every processor and called task of the model is here.
1514 std::vector<std::vector<std::size_t>> srv(lqn.nidx + 1);
1515 std::vector<std::vector<std::size_t>> srvnode(lqn.nidx + 1);
1516 m.server_idx_of.assign(lqn.nidx + 1, 0);
1517 for (std::size_t sidx : idxSet) {
1518 const bool sishost = sidx <= lqn.nhosts;
1519 srv[sidx].resize(nreplicas);
1520 for (std::size_t r = 0; r < nreplicas; ++r) {
1521 qn::Station<T> st;
1522 st.name = r == 0 ? lqn.hashnames[sidx]
1523 : lqn.hashnames[sidx] + "." + std::to_string(r + 1);
1524 // inf-scheduled Queue as Delay node rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
1525 st.nodetype =
1526 lqn.sched[sidx] == SchedStrategy::INF ? NodeType::Delay : NodeType::Queue;
1527 st.sched = lqn.sched[sidx];
1528 // setNumberOfServers no-op rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
1529 st.nservers = lqn.sched[sidx] == SchedStrategy::INF
1530 ? std::numeric_limits<double>::infinity()
1531 : mult[sidx];
1532 st.attr_ishost = flat ? sishost : ishostlayer;
1533 st.attr_idx = sidx;
1534 srv[sidx][r] = m.add_station(st);
1535 srvnode[sidx].push_back(m.node_of_station(srv[sidx][r]));
1536 }
1537 m.server_idx_of[sidx] = srv[sidx][0];
1538 if (sishost)
1539 m.host_stations.push_back(srv[sidx][0]);
1540 else
1541 m.task_stations.push_back(srv[sidx][0]);
1542 }
1543 // the layer's own server, the sole server under `srvn`
1544 const std::vector<std::size_t>& server = srv[idx];
1545 const std::vector<std::size_t>& serverNode = srvnode[idx];
1546
1547 /** Stations of ELEM when it is served in this layer, empty otherwise. */
1548 auto servers_for = [&](std::size_t elem) -> const std::vector<std::size_t>& {
1549 static const std::vector<std::size_t> none;
1550 if (elem >= 1 && elem <= lqn.nidx && !srv[elem].empty()) return srv[elem];
1551 return none;
1552 };
1553 auto server_nodes_for = [&](std::size_t elem) -> const std::vector<std::size_t>& {
1554 static const std::vector<std::size_t> none;
1555 if (elem >= 1 && elem <= lqn.nidx && !srvnode[elem].empty()) return srvnode[elem];
1556 return none;
1557 };
1558 /** True when the processor of task TIDX is a server of this layer. */
1559 auto host_is_server = [&](std::size_t tidx_) {
1560 return !servers_for(lqn.parent[tidx_]).empty();
1561 };
1562 /** True when TIDX calls an entry served by this layer. */
1563 auto is_layer_client = [&](std::size_t tidx_) {
1564 for (std::size_t s : idxSet)
1565 for (std::size_t e : lqn.entriesof[s])
1566 if (lqn.issynccaller.get(tidx_, e)) return true;
1567 return false;
1568 };
1569 /** Declare a class at every server station of this layer. */
1570 auto set_all_servers = [&](std::size_t cl, const Distrib<T>& d) {
1571 for (std::size_t s : idxSet)
1572 for (std::size_t st : srv[s]) m.set_service(st, cl, d);
1573 };
1574
1575 // Routed call groups, resolved from target ENTRIES to the call indices
1576 // that reach them: a group is ONE dispatch with n destinations, so its
1577 // members share a call class and a dispatch class that holds the job at
1578 // the client while the target is picked (callGroupsByCidx in
1579 // buildLayersRecursive.m).
1580 std::vector<std::size_t> group_of_call(lqn.ncalls + 1, 0);
1581 std::vector<std::vector<std::size_t>> group_members(lqn.callgroups.size() + 1);
1582 for (std::size_t g = 0; g < lqn.callgroups.size(); ++g) {
1583 const LqnCallGroup& grp = lqn.callgroups[g];
1584 for (std::size_t tgt : grp.targets)
1585 for (std::size_t cidx : lqn.callsof[grp.caller])
1586 if (lqn.callpair_dst[cidx] == tgt && lqn.calltype[cidx] == CallType::SYNC &&
1587 group_of_call[cidx] == 0) {
1588 group_of_call[cidx] = g + 1;
1589 group_members[g + 1].push_back(cidx);
1590 break;
1591 }
1592 }
1593 // per group: the Router node, the dispatch class and the group class
1594 std::vector<std::size_t> grp_router(lqn.callgroups.size() + 1, 0);
1595 std::vector<std::size_t> grp_dispatch(lqn.callgroups.size() + 1, 0);
1596 std::vector<std::size_t> grp_class(lqn.callgroups.size() + 1, 0);
1597 // (router node, dispatch class, group) triples whose strategy is
1598 // installed once the routing is written
1599 std::vector<std::array<std::size_t, 3>> routed_group_sites;
1600
1601 // Fork/Router/Join construction rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
1602 std::vector<std::size_t> acts_in_caller;
1603 for (std::size_t c : callers)
1604 for (std::size_t a : lqn.actsof[c]) acts_in_caller.push_back(a);
1605 bool hasfork = false, hasjoin = false;
1606 std::size_t maxfanout = 1;
1607 for (std::size_t a : acts_in_caller) {
1608 if (lqn.actposttype[a] == PrecedenceType::POST_AND) hasfork = true;
1609 if (lqn.actpretype[a] == PrecedenceType::PRE_AND) hasjoin = true;
1610 std::size_t nand = 0;
1611 for (std::size_t sx : lqn.graph.succ(a))
1612 if (lqn.actposttype[sx] == PrecedenceType::POST_AND) ++nand;
1613 if (nand > maxfanout) maxfanout = nand;
1614 }
1615 std::size_t forkNode = 0, joinNode = 0, joinStation = 0;
1616 std::vector<std::size_t> forkRouter;
1617 if (hasfork) {
1618 forkNode = m.add_node("Fork_PostAnd", NodeType::Fork, false);
1619 for (std::size_t f = 1; f <= maxfanout; ++f)
1620 forkRouter.push_back(
1621 m.add_node("Fork_PostAnd_" + std::to_string(f), NodeType::Router, true));
1622 }
1623 if (hasjoin) {
1624 qn::Station<T> js;
1625 js.name = "Join_PreAnd";
1626 js.nodetype = NodeType::Join;
1627 js.sched = SchedStrategy::INF;
1628 js.nservers = std::numeric_limits<double>::infinity();
1629 js.attr_ishost = false;
1630 js.attr_idx = 0;
1631 joinStation = m.add_station(js);
1632 joinNode = m.node_of_station(joinStation);
1633 if (forkNode) m.fj.emplace_back(forkNode, joinNode);
1634 }
1635 // A CACHE LAYER is a HOST layer all of whose callers are cache tasks,
1636 // buildLayersRecursive.m:88. The Cache node lives here and not in the
1637 // cache task's own layer: the item lookup is what the task DOES on its
1638 // processor, so the hit and miss branches have to queue at the same
1639 // server the read arrived at.
1640 bool iscachelayer = !flat && ishostlayer && !callers.empty();
1641 for (std::size_t c : callers)
1642 if (!lqn.iscache[c]) iscachelayer = false;
1643 // A SETUP NO LONGER CHANGES HOW A LAYER IS BUILT (buildLayersRecursive.m,
1644 // 2026-08-11). Wiring the setup/delay-off onto the server station routed
1645 // the layer through the open M/G/1-with-setup QBD, which reads the idle
1646 // period off the Poisson rate 1/X and so powers the thread down far more
1647 // often than a CLOSED layer does -- 12.67% below LDES on lqn_setup -- and
1648 // it charged the restart to the ACTIVITY, where it is not host demand.
1649 // The cold start is charged to the ENTRY instead, with the probability
1650 // that the thread was really found down: see setup_charge().
1651 std::size_t cacheNode = 0;
1652 qn::CacheParam<T> cachepar;
1653 if (iscachelayer) {
1654 const std::size_t ct = callers[0];
1655 cachepar.nitems = lqn.nitems[ct];
1656 cachepar.itemcap = lqn.itemcap[ct];
1657 cachepar.replacestrat = lqn.replacestrat[ct];
1658 cacheNode = m.add_node(lqn.hashnames[ct], NodeType::Cache, true);
1659 }
1660
1661 // The open streams this layer carries, decided BEFORE the Source is
1662 // added: m.serverIdx is the client index plus one and replica r is
1663 // m.serverIdx + r, so a station inserted between them would silently
1664 // renumber the servers. Source and Sink therefore come last.
1665 // An async call is declared only in the layer whose server owns the
1666 // called entry, which is serversFor(parent(dst)) in the reference
1667 // (buildLayersRecursive.m:327) and the same test route_sync_call uses.
1668 std::vector<std::size_t> async_here;
1669 for (std::size_t c : callers)
1670 for (std::size_t aidx : lqn.actsof[c])
1671 for (std::size_t cidx : lqn.callsof[aidx])
1672 if (lqn.calltype[cidx] == CallType::ASYNC &&
1673 !servers_for(lqn.parent[lqn.callpair_dst[cidx]]).empty())
1674 async_here.push_back(cidx);
1675 std::vector<std::size_t> open_entries;
1676 for (std::size_t c : callers) {
1677 // An arrival that is the only way into the task is carried by the caller
1678 // CHAIN, not by a stream: that task has no task layer, so nothing ever sets
1679 // its surrogate delay, and a chain cycling against an Immediate one plus a
1680 // stream loads the host twice -- lqn_open_arrival's processor read 0.68
1681 // against 0.32 from lqns, lqsim and LDES. update_think_times closes the
1682 // chain on the known rate instead, as it does for a forwarding target.
1683 if (open_arrival_rate_of(c) > GlobalConstants::FineTol) continue;
1684 for (std::size_t eidx : lqn.entriesof[c])
1685 if (lqn.has_arrival[eidx]) open_entries.push_back(eidx);
1686 }
1687
1688 std::size_t sourceStation = 0, sourceNode = 0, sinkNode = 0;
1689 if (!async_here.empty() || !open_entries.empty()) {
1690 qn::Station<T> src;
1691 src.name = "Source";
1692 src.nodetype = NodeType::Source;
1693 // EXT keeps solver_mva out of both infSET and qSET: the Source is lambda, not a queue
1694 src.sched = SchedStrategy::EXT;
1695 src.nservers = 1.0;
1696 src.attr_ishost = false;
1697 src.attr_idx = 0;
1698 sourceStation = m.add_station(src);
1699 sourceNode = m.node_of_station(sourceStation);
1700 sinkNode = m.add_node("Sink", NodeType::Sink, false);
1701 m.sourceIdx = sourceStation;
1702 m.sinkNode = sinkNode;
1703 }
1704
1705 /** The stack of entry classes at the forks currently open. */
1706 std::vector<std::size_t> forkClassStack;
1707
1708 // class index of each LQN element and each call, 0 when absent
1709 std::vector<std::size_t> cls(lqn.nidx + 1, 0);
1710 std::vector<std::size_t> callcls(lqn.ncalls + 1, 0);
1711 // `<call>.Aux`, present only for a call whose mean count differs from the
1712 // replica count; 0 elsewhere. See the routing in route_sync_call.
1713 std::vector<std::size_t> auxcallcls(lqn.ncalls + 1, 0);
1714
1715 // A caller carries a closed class when its own PROCESSOR is served here
1716 // and something drives it (it is a reference task, is called, is a
1717 // forwarding target or takes an arrival), or when it calls an entry
1718 // served here. Under `srvn` the first disjunct is exactly the host
1719 // layer and the second exactly the task layer, so this reduces to the
1720 // two-branch test it replaces (buildLayersRecursive.m:217).
1721 auto caller_needs_class = [&](std::size_t tidx_caller) {
1722 if (host_is_server(tidx_caller)) {
1723 if (lqn.isref[tidx_caller]) return true;
1724 for (std::size_t e : lqn.entriesof[tidx_caller])
1725 if (lqn.issynccaller.any_col(e) || lqn.isasynccaller.any_col(e) ||
1726 is_fwd_target(e) || lqn.has_arrival[e])
1727 return true;
1728 }
1729 return is_layer_client(tidx_caller);
1730 };
1731 auto caller_acts_visible = [&](std::size_t tidx_caller) {
1732 return host_is_server(tidx_caller) || is_layer_client(tidx_caller);
1733 };
1734 /**
1735 * The population caller TIDX_ contributes to this layer WITHOUT a
1736 * refpath merge, including the infinite-server fallbacks. Shared by the
1737 * first pass, which also records it in njobs, and by resolve_ref_path,
1738 * which compares it against the chain population before any class exists.
1739 */
1740 auto caller_pool = [&](std::size_t tidx_) -> double {
1741 if (njobs(tidx_, idx) != 0.0) return njobs(tidx_, idx);
1742 // representative-replica scaling rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
1743 const bool single_replica = reduce_fanout || single_replica_tasks.count(tidx_) > 0;
1744 double nj = single_replica ? mult[tidx_] : mult[tidx_] * lqn.repl[tidx_];
1745 if (std::isinf(nj)) {
1746 double s = 0.0;
1747 for (std::size_t c = 1; c <= NT(); ++c)
1748 if (lqn.taskgraph.get(c, tidx_) != Tzero()) s += mult[c];
1749 nj = s;
1750 if (std::isinf(nj)) {
1751 double s2 = 0.0;
1752 for (std::size_t c = 1; c <= NT(); ++c)
1753 if (std::isfinite(mult[c])) s2 += mult[c] * lqn.repl[c];
1754 nj = std::min(s2, 1000.0);
1755 }
1756 }
1757 return nj;
1758 };
1759
1760 // ---- reference-path interlocking (interlock_method = 'refpath') -------
1761 // This layer's client is then not one population per caller but ONE
1762 // chain running from the reference task down through every caller it
1763 // reaches, so two callers that are the same REF customers arriving by
1764 // two routes stop being counted as two. The tables say which callers are
1765 // merged (rp_spliced), which sync calls stop being a surrogate delay and
1766 // become an explicit descent (rp_expand), and which entries above or
1767 // between the callers become hop stages on the client Delay (rp_hop).
1768 // See lqn_ref_routes and _kb/06-solver-catalog.md (LN section).
1769 bool rp_on = false;
1770 std::vector<bool> rp_member(lqn.nidx + 1, false); // task: a caller merged into a chain
1771 std::vector<bool> rp_spliced(lqn.nidx + 1, false); // task: merged AND not the chain head
1772 std::vector<bool> rp_hop(lqn.nidx + 1, false); // entry: a stage of the path
1773 std::vector<bool> rp_expand(lqn.ncalls + 1, false); // call: re-expanded, not a delay
1774 std::vector<std::size_t> rp_child_task(lqn.ncalls + 1, 0); // callee task, when a member
1775 std::vector<std::size_t> rp_child_entry(lqn.ncalls + 1, 0); // callee entry, when a hop
1776 std::vector<T> rp_ret_share(lqn.ncalls + 1, Tzero()); // share of the callee's return
1777 std::vector<T> rp_call_mean(lqn.ncalls + 1, Tzero()); // invocations per execution
1778 std::vector<std::vector<std::size_t>> rp_hop_children(lqn.nidx + 1); // descent order
1779 std::vector<double> rp_hop_seed(lqn.nidx + 1, 0.0); // build-time stage mean
1780 std::vector<std::vector<std::size_t>> rp_member_inbound(lqn.nidx + 1);
1781 std::vector<std::vector<std::size_t>> rp_hop_inbound(lqn.nidx + 1);
1782 std::vector<std::vector<std::size_t>> rp_group_roots;
1783 std::vector<std::size_t> rp_group_reftask;
1784 std::vector<bool> rp_group_head_is_caller;
1785 // classes minted for the path, 0 when absent
1786 std::vector<std::size_t> rp_ref_stage;
1787 std::vector<std::size_t> rp_hop_cls(lqn.nidx + 1, 0), rp_hop_ret(lqn.nidx + 1, 0),
1788 rp_ret_cls(lqn.nidx + 1, 0);
1789 std::vector<std::size_t> rp_gate(lqn.ncalls + 1, 0), rp_aux(lqn.ncalls + 1, 0),
1790 rp_resume(lqn.ncalls + 1, 0);
1791
1792 /** Build-time seed of a hop stage: the entry's own host demand. */
1793 auto host_demand_of_entry = [&](std::size_t eidx_) {
1794 double d = 0.0;
1795 for (std::size_t a_ : lqn.actsof[eidx_]) {
1796 if (lqn.hostdem[a_].disabled) continue;
1797 const double m_ = dbl(lqn.hostdem[a_].mean);
1798 if (std::isfinite(m_)) d += m_;
1799 }
1800 return d;
1801 };
1802 const auto in_idxset = [&](std::size_t t_) {
1803 return std::find(idxSet.begin(), idxSet.end(), t_) != idxSet.end();
1804 };
1805
1806 // Port of resolveRefPath: a pure query over lqn plus the layer's own
1807 // server set, run before any class exists because the first pass needs
1808 // the answer to size the caller populations.
1809 [&]() {
1810 if (interlockMethod != "refpath" || flat || iscachelayer || hasfork || hasjoin) return;
1811 // a Fork flattens every positive alpha in the visit solve; a replicated
1812 // element carries a per-replica njobs convention a shared chain breaks
1813 if (reduce_fanout || nreplicas > 1) return;
1814 for (std::size_t t_ : callers)
1815 if (lqn.repl[t_] > 1.0 || single_replica_tasks.count(t_) > 0) return;
1816 const lqn::LqnRefRoutes<T> R =
1817 lqn::lqn_ref_routes(lqn, callers, opt.interlock_maxpaths, idxSet);
1818 if (!R.why.empty()) {
1819 // named, because the discontinuity is otherwise invisible
1820 std::cerr << "[LINE] Warning: layer '" << lqn.hashnames[idx]
1821 << "' falls back to the 'ilrate' interlock method: " << R.why
1822 << ".\n";
1823 return;
1824 }
1825 std::vector<std::size_t> keep;
1826 for (std::size_t g_ = 0; g_ < R.groups.size(); ++g_) {
1827 const lqn::LqnRefGroup<T>& G = R.groups[g_];
1828 bool wanted = G.members.size() >= 2;
1829 // a lone caller below a reference task: no merge, but the path
1830 // above it becomes explicit instead of a fitted idle time
1831 if (!wanted && opt.interlock_refpath_scope == "all" && !G.head_is_caller &&
1832 !G.members.empty())
1833 wanted = true;
1834 if (!wanted) continue;
1835 // The merge only pays when the chain population it installs is
1836 // STRICTLY below the caller pools it replaces; otherwise the
1837 // group stays on 'ilrate' (17-external-interlock).
1838 double pool = 0.0;
1839 for (std::size_t t_ : G.members) pool += caller_pool(t_);
1840 const double chainPop = mult[G.reftask] * lqn.repl[G.reftask];
1841 if (!(pool > chainPop)) continue;
1842 keep.push_back(g_);
1843 }
1844 if (keep.empty()) return;
1845 // Stage the whole decision, then commit: a refusal found on the
1846 // second group must not leave the first one half-applied.
1847 std::vector<bool> sMember(lqn.nidx + 1, false), sSpliced(lqn.nidx + 1, false),
1848 sHop(lqn.nidx + 1, false), sExpand(lqn.ncalls + 1, false);
1849 std::vector<std::size_t> sChildTask(lqn.ncalls + 1, 0),
1850 sChildEntry(lqn.ncalls + 1, 0);
1851 std::vector<T> sRetShare(lqn.ncalls + 1, Tzero()), sCallMean(lqn.ncalls + 1, Tzero());
1852 std::vector<std::vector<std::size_t>> sHopChildren(lqn.nidx + 1),
1853 sMemberInbound(lqn.nidx + 1), sHopInbound(lqn.nidx + 1);
1854 std::vector<double> sHopSeed(lqn.nidx + 1, 0.0);
1855 std::vector<std::vector<std::size_t>> sRoots;
1856 std::vector<std::size_t> sRefTask;
1857 std::vector<bool> sHeadIsCaller;
1858 for (std::size_t g_ : keep) {
1859 const lqn::LqnRefGroup<T>& G = R.groups[g_];
1860 const std::size_t nE = G.entries.size();
1861 // does this DAG node lead to a caller? reverse topological order
1862 std::vector<bool> leads(G.ismember.begin(), G.ismember.end());
1863 for (std::size_t i_ = nE; i_-- > 0;)
1864 for (const lqn::LqnRefCall<T>& c_ : G.calls)
1865 if (c_.from == i_ && leads[c_.to]) leads[i_] = true;
1866 for (std::size_t t_ : G.members)
1867 if (!caller_needs_class(t_)) return; // no client class to enter
1868 if (G.head_is_caller && !caller_needs_class(G.reftask)) return;
1869 // an infinite reference pool has no chain population
1870 if (!G.head_is_caller && !std::isfinite(lqn.maxmult[G.reftask])) return;
1871 for (std::size_t i_ = 0; i_ < nE; ++i_) {
1872 if (!leads[i_] || G.ismember[i_]) continue;
1873 // a hop served by this layer's own station would be in two places
1874 if (in_idxset(G.etask[i_]) || lqn.repl[G.etask[i_]] > 1.0) return;
1875 }
1876 for (const lqn::LqnRefCall<T>& c_ : G.calls) {
1877 if (!leads[c_.to]) continue;
1878 if (in_idxset(G.etask[c_.to])) return; // callee served by this layer
1879 }
1880 // commit this group into the staging tables
1881 const std::size_t head = G.reftask;
1882 for (std::size_t t_ : G.members) {
1883 sMember[t_] = true;
1884 sSpliced[t_] = !(G.head_is_caller && t_ == head);
1885 }
1886 std::vector<T> inTotMember(lqn.nidx + 1, Tzero()), inTotHop(lqn.nidx + 1, Tzero());
1887 for (const lqn::LqnRefCall<T>& c_ : G.calls) {
1888 if (!leads[c_.to]) continue;
1889 const std::size_t cidx_ = c_.cidx;
1890 const std::size_t fromEidx = G.entries[c_.from];
1891 sExpand[cidx_] = true;
1892 sCallMean[cidx_] = lqn.callproc_mean[cidx_];
1893 sHopChildren[fromEidx].push_back(cidx_);
1894 if (G.ismember[c_.to]) {
1895 const std::size_t tv = G.etask[c_.to];
1896 sChildTask[cidx_] = tv;
1897 sMemberInbound[tv].push_back(cidx_);
1898 inTotMember[tv] = T(inTotMember[tv] + c_.vcall);
1899 } else {
1900 const std::size_t ev = G.entries[c_.to];
1901 sChildEntry[cidx_] = ev;
1902 sHopInbound[ev].push_back(cidx_);
1903 inTotHop[ev] = T(inTotHop[ev] + c_.vcall);
1904 }
1905 sRetShare[cidx_] = c_.vcall; // normalised below
1906 }
1907 for (const lqn::LqnRefCall<T>& c_ : G.calls) {
1908 const std::size_t cidx_ = c_.cidx;
1909 if (!sExpand[cidx_]) continue;
1910 const T tot = sChildTask[cidx_] > 0 ? inTotMember[sChildTask[cidx_]]
1911 : inTotHop[sChildEntry[cidx_]];
1912 sRetShare[cidx_] = dbl(tot) > GlobalConstants::FineTol
1913 ? T(sRetShare[cidx_] / tot)
1914 : Tzero();
1915 }
1916 for (std::size_t i_ = 0; i_ < nE; ++i_)
1917 if (leads[i_] && !G.ismember[i_]) {
1918 const std::size_t ev = G.entries[i_];
1919 sHop[ev] = true;
1920 sHopSeed[ev] = host_demand_of_entry(ev);
1921 }
1922 if (!G.head_is_caller) {
1923 // EVERY entry of the reference task becomes a stage, not only
1924 // those reaching this layer, or the 1/nentries split would
1925 // shrink its denominator and over-drive the chain
1926 for (std::size_t ev : lqn.entriesof[G.reftask])
1927 if (!sHop[ev]) {
1928 sHop[ev] = true;
1929 sHopSeed[ev] = host_demand_of_entry(ev);
1930 }
1931 }
1932 sRoots.push_back(lqn.entriesof[head]);
1933 sRefTask.push_back(head);
1934 sHeadIsCaller.push_back(G.head_is_caller);
1935 }
1936 rp_member = sMember;
1937 rp_spliced = sSpliced;
1938 rp_hop = sHop;
1939 rp_expand = sExpand;
1940 rp_child_task = sChildTask;
1941 rp_child_entry = sChildEntry;
1942 rp_ret_share = sRetShare;
1943 rp_call_mean = sCallMean;
1944 rp_hop_children = sHopChildren;
1945 rp_hop_seed = sHopSeed;
1946 rp_member_inbound = sMemberInbound;
1947 rp_hop_inbound = sHopInbound;
1948 rp_group_roots = sRoots;
1949 rp_group_reftask = sRefTask;
1950 rp_group_head_is_caller = sHeadIsCaller;
1951 rp_on = true;
1952 }();
1953
1954 // ---- first pass: create the classes --------------------------------
1955 for (std::size_t tidx_caller : callers) {
1956 if (caller_needs_class(tidx_caller)) {
1957 double nj = njobs(tidx_caller, idx);
1958 if (nj == 0.0) {
1959 nj = caller_pool(tidx_caller);
1960 njobs(tidx_caller, idx) = nj;
1961 }
1962 // A caller merged into a reference chain brings NO population of
1963 // its own: the customers are the reference task's. Only the CLASS
1964 // is zeroed; njobs still records the pool for update_think_times.
1965 const bool rp_spliced_here = rp_on && rp_spliced[tidx_caller];
1966 qn::JobClass jc;
1967 jc.name = lqn.hashnames[tidx_caller];
1968 jc.type = JobClassType::CLOSED;
1969 jc.population = rp_spliced_here ? 0.0 : nj;
1970 jc.refstat = m.clientIdx;
1971 jc.completes = false;
1972 // ONE reference class per chain: a merged caller yields it to the
1973 // chain head and is normalised through layer_normclass instead
1974 jc.is_ref_class = !rp_spliced_here;
1975 jc.attr_kind = int(LqnElement::TASK);
1976 jc.attr_idx = tidx_caller;
1977 cls[tidx_caller] = m.add_class(jc);
1978 m.attr_tasks.emplace_back(cls[tidx_caller], tidx_caller);
1979 if (lqn.isref[tidx_caller]) {
1980 m.set_service(m.clientIdx, cls[tidx_caller], thinkproc[tidx_caller]);
1981 } else if (rp_spliced_here) {
1982 // the path above this caller is explicit now, so there is no
1983 // idle time left to fit and no update-map row to refresh it
1984 m.set_service(m.clientIdx, cls[tidx_caller], Distrib<T>::immediate());
1985 } else {
1986 // a served task's declared think time is not a per-request
1987 // delay, so the seed carries none either; update_think_times
1988 // replaces this from the first iteration on
1989 m.set_service(m.clientIdx, cls[tidx_caller], Distrib<T>::immediate());
1990 thinkt_map.push_back({idx, tidx_caller, m.clientIdx, cls[tidx_caller]});
1991 }
1992
1993 for (std::size_t eidx : lqn.entriesof[tidx_caller]) {
1994 qn::JobClass ec;
1995 ec.name = lqn.hashnames[eidx];
1996 ec.type = JobClassType::CLOSED;
1997 ec.population = 0.0;
1998 ec.refstat = m.clientIdx;
1999 ec.completes = false;
2000 ec.attr_kind = int(LqnElement::ENTRY);
2001 ec.attr_idx = eidx;
2002 cls[eidx] = m.add_class(ec);
2003 m.attr_entries.emplace_back(cls[eidx], eidx);
2004 m.set_service(m.clientIdx, cls[eidx], Distrib<T>::immediate());
2005 }
2006 }
2007
2008 for (std::size_t aidx : lqn.actsof[tidx_caller]) {
2009 if (caller_acts_visible(tidx_caller)) {
2010 qn::JobClass ac;
2011 ac.name = lqn.hashnames[aidx];
2012 ac.type = JobClassType::CLOSED;
2013 ac.population = 0.0;
2014 ac.refstat = m.clientIdx;
2015 ac.completes = false;
2016 ac.attr_kind = int(LqnElement::ACTIVITY);
2017 ac.attr_idx = aidx;
2018 cls[aidx] = m.add_class(ac);
2019 m.attr_activities.emplace_back(cls[aidx], aidx);
2020 // The host demand is served at the processor's own station
2021 // when this layer holds it; everywhere else the activity is
2022 // a surrogate delay at the client.
2023 const std::size_t hidx = lqn.parent[lqn.parent[aidx]];
2024 if (servers_for(hidx).empty())
2025 m.set_service(m.clientIdx, cls[aidx], servtproc[aidx]);
2026 }
2027 for (std::size_t cidx : lqn.callsof[aidx]) {
2028 if (lqn.calltype[cidx] == CallType::ASYNC) {
2029 // An async call is a stream, not a visit: the caller does
2030 // not block, so it carries no closed class and instead
2031 // drives an open chain of its own out of the Source.
2032 const std::size_t adst = lqn.parent[lqn.callpair_dst[cidx]];
2033 if (servers_for(adst).empty()) continue;
2034 qn::JobClass oc;
2035 oc.name = lqn.callhashnames[cidx];
2036 oc.type = JobClassType::OPEN;
2037 oc.population = std::numeric_limits<double>::infinity();
2038 oc.refstat = sourceStation;
2039 oc.completes = false;
2040 oc.is_ref_class = false;
2041 oc.attr_kind = int(LqnElement::CALL);
2042 oc.attr_idx = cidx;
2043 callcls[cidx] = m.add_class(oc);
2044 m.attr_calls.push_back({callcls[cidx], cidx, lqn.callpair_src[cidx],
2045 lqn.callpair_dst[cidx]});
2046 // A NEGLIGIBLE seed, not the reference's Immediate. The
2047 // rate is replaced from the caller's throughput on the
2048 // first post(), but apply_sink_closure weights the
2049 // Sink->Source arcs by it in between, and Immediate is
2050 // rate 1/0: the closed classes sharing the closure then
2051 // get NaN visits. fj_mmt seeds its own open classes the
2052 // same way, for the same reason.
2053 m.set_service(sourceStation, callcls[cidx],
2055 num_traits<T>::from_double(GlobalConstants::FineTol)));
2056 T minRespTA = Tzero();
2057 for (std::size_t ta : lqn.actsof[adst]) minRespTA += lqn.hostdem[ta].mean;
2058 for (std::size_t st : servers_for(adst)) {
2059 m.set_service(st, callcls[cidx], Distrib<T>::exp_mean(minRespTA));
2060 call_map.push_back({idx, cidx, st, callcls[cidx]});
2061 }
2062 arv_call_map.push_back({idx, cidx, sourceStation, callcls[cidx]});
2063 continue;
2064 }
2065 if (lqn.calltype[cidx] != CallType::SYNC) continue;
2066 const std::size_t gid = group_of_call[cidx];
2067 if (gid != 0) {
2068 // ONE dispatch with n destinations. The members SHARE the
2069 // dispatch class, which is both the class the strategy
2070 // routes and the class that visits the targets: the hop
2071 // must not switch class, because a state-dependent
2072 // routing function is evaluated at zero off the class
2073 // diagonal. The class switch goes on the return arc
2074 // instead, into a group class the job continues in.
2075 if (grp_dispatch[gid] == 0) {
2076 // The strategy is a property of a NODE and routes
2077 // over that node's links, not over one class's arcs,
2078 // so a dedicated Router whose only links are the
2079 // group's targets is the only place where the choice
2080 // is exactly the group's.
2081 const std::string tag =
2082 lqn.hashnames[aidx] + ".Dispatch" + std::to_string(gid);
2083 grp_router[gid] = m.add_node(tag + ".Router", NodeType::Router, true);
2084 qn::JobClass dc;
2085 dc.name = tag;
2086 dc.type = JobClassType::CLOSED;
2087 dc.population = 0.0;
2088 dc.refstat = m.clientIdx;
2089 dc.completes = false;
2090 dc.attr_kind = int(LqnElement::CALL);
2091 dc.attr_idx = cidx;
2092 grp_dispatch[gid] = m.add_class(dc);
2093 m.set_service(m.clientIdx, grp_dispatch[gid], Distrib<T>::immediate());
2094 qn::JobClass gc;
2095 gc.name = lqn.callhashnames[cidx] + ".Group" + std::to_string(gid);
2096 gc.type = JobClassType::CLOSED;
2097 gc.population = 0.0;
2098 gc.refstat = m.clientIdx;
2099 gc.completes = false;
2100 gc.attr_kind = int(LqnElement::CALL);
2101 gc.attr_idx = cidx;
2102 grp_class[gid] = m.add_class(gc);
2103 m.set_service(m.clientIdx, grp_class[gid], Distrib<T>::immediate());
2104 routed_group_sites.push_back(
2105 {grp_router[gid], grp_dispatch[gid], gid});
2106 }
2107 callcls[cidx] = grp_dispatch[gid];
2108 m.attr_calls.push_back({callcls[cidx], cidx, lqn.callpair_src[cidx],
2109 lqn.callpair_dst[cidx]});
2110 for (std::size_t st2 : servers_for(lqn.parent[lqn.callpair_dst[cidx]]))
2111 m.set_service(st2, callcls[cidx], callservtproc[cidx]);
2112 continue;
2113 }
2114 qn::JobClass cc;
2115 cc.name = lqn.callhashnames[cidx];
2116 cc.type = JobClassType::CLOSED;
2117 cc.population = 0.0;
2118 cc.refstat = m.clientIdx;
2119 cc.completes = false;
2120 cc.attr_kind = int(LqnElement::CALL);
2121 cc.attr_idx = cidx;
2122 callcls[cidx] = m.add_class(cc);
2123 m.attr_calls.push_back({callcls[cidx], cidx, lqn.callpair_src[cidx],
2124 lqn.callpair_dst[cidx]});
2125 // An upper bound on the server's response, replaced at the
2126 // first iteration by the measured one. The station seeded is
2127 // the CALLEE's under `flat` and the layer's own under
2128 // `srvn`, where the latter also seeds calls that leave the
2129 // layer -- a phantom rate on a class with no visits here,
2130 // kept because removing it moves the fixed point (see
2131 // seedCallService in buildLayersRecursive.m).
2132 const std::size_t seedidx = flat ? lqn.parent[lqn.callpair_dst[cidx]] : idx;
2133 T minRespT = Tzero();
2134 for (std::size_t ta : lqn.actsof[seedidx]) minRespT += lqn.hostdem[ta].mean;
2135 for (std::size_t st : servers_for(seedidx))
2136 m.set_service(st, callcls[cidx], Distrib<T>::exp_mean(minRespT));
2137 // .Aux class rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
2138 if (lqn.callproc_mean[cidx] !=
2139 num_traits<T>::from_double(double(nreplicas))) {
2140 qn::JobClass xc;
2141 xc.name = lqn.callhashnames[cidx] + ".Aux";
2142 xc.type = JobClassType::CLOSED;
2143 xc.population = 0.0;
2144 xc.refstat = m.clientIdx;
2145 xc.completes = false;
2146 xc.attr_kind = int(LqnElement::CALL);
2147 xc.attr_idx = cidx;
2148 auxcallcls[cidx] = m.add_class(xc);
2149 m.set_service(m.clientIdx, auxcallcls[cidx], Distrib<T>::immediate());
2150 }
2151 }
2152 }
2153 }
2154
2155 // ---- reference-path classes (buildRefPathClasses) ------------------
2156 // Minted with the caller classes. A descent issued from a caller's own
2157 // activity graph REUSES the call classes that already exist as its gate
2158 // and its geometric auxiliary: fresh ones would leave those two with no
2159 // inbound arc at all.
2160 auto rp_class = [&](const std::string& nm, double pop, std::size_t attr) {
2161 qn::JobClass rc;
2162 rc.name = nm;
2163 rc.type = JobClassType::CLOSED;
2164 rc.population = pop;
2165 rc.refstat = m.clientIdx;
2166 rc.completes = false;
2167 rc.is_ref_class = false;
2168 rc.attr_kind = -1;
2169 rc.attr_idx = attr;
2170 const std::size_t k_ = m.add_class(rc);
2171 m.set_service(m.clientIdx, k_, Distrib<T>::immediate());
2172 return k_;
2173 };
2174 if (rp_on) {
2175 for (std::size_t g_ = 0; g_ < rp_group_reftask.size(); ++g_) {
2176 if (rp_group_head_is_caller[g_]) {
2177 rp_ref_stage.push_back(0); // the caller class IS the head
2178 continue;
2179 }
2180 const std::size_t rt = rp_group_reftask[g_];
2181 const std::size_t k_ =
2182 rp_class(lqn.hashnames[rt] + ".RefPath", mult[rt] * lqn.repl[rt], rt);
2183 m.classes[k_ - 1].is_ref_class = true;
2184 m.set_service(m.clientIdx, k_, thinkproc[rt]);
2185 rp_ref_stage.push_back(k_);
2186 }
2187 for (std::size_t ev = 1; ev <= lqn.nidx; ++ev) {
2188 if (!rp_hop[ev]) continue;
2189 const std::size_t k_ = rp_class(lqn.hashnames[ev] + ".RefHop", 0.0, ev);
2190 if (rp_hop_seed[ev] > GlobalConstants::FineTol)
2191 m.set_service(m.clientIdx, k_,
2192 Distrib<T>::exp_mean(num_traits<T>::from_double(rp_hop_seed[ev])));
2193 rp_hop_cls[ev] = k_;
2194 // the entry's own residence, MINUS the descents re-expanded below
2195 // it, PLUS the wait its task's threads queue for
2196 refpath_map.push_back({idx, m.clientIdx, k_, 1, ev, 1.0, 0});
2197 for (std::size_t c_ : rp_hop_children[ev])
2198 refpath_map.push_back({idx, m.clientIdx, k_, 2, c_, -1.0, ev});
2199 refpath_map.push_back({idx, m.clientIdx, k_, 3, lqn.parent[ev], 1.0, 0});
2200 rp_hop_ret[ev] = rp_class(lqn.hashnames[ev] + ".RefHopRet", 0.0, ev);
2201 }
2202 for (std::size_t tv = 1; tv <= lqn.nidx; ++tv)
2203 if (rp_spliced[tv]) rp_ret_cls[tv] = rp_class(lqn.hashnames[tv] + ".RefRet", 0.0, tv);
2204 for (std::size_t cidx_ = 1; cidx_ <= lqn.ncalls; ++cidx_) {
2205 if (!rp_expand[cidx_]) continue;
2206 if (callcls[cidx_] != 0) {
2207 rp_gate[cidx_] = callcls[cidx_];
2208 m.set_service(m.clientIdx, rp_gate[cidx_], Distrib<T>::immediate());
2209 if (auxcallcls[cidx_] != 0) rp_aux[cidx_] = auxcallcls[cidx_];
2210 } else {
2211 rp_gate[cidx_] = rp_class(lqn.callhashnames[cidx_] + ".RefGate", 0.0, cidx_);
2212 if (rp_call_mean[cidx_] != Tone())
2213 rp_aux[cidx_] =
2214 rp_class(lqn.callhashnames[cidx_] + ".RefGate.Aux", 0.0, cidx_);
2215 }
2216 // A MEMBER entered through this gate has its subgraph explicit
2217 // here; the one cost it does not show is the wait for one of its
2218 // own threads, which lives in its own task layer.
2219 if (rp_child_task[cidx_] > 0)
2220 refpath_map.push_back(
2221 {idx, m.clientIdx, rp_gate[cidx_], 3, rp_child_task[cidx_], 1.0, 0});
2222 rp_resume[cidx_] = rp_class(lqn.callhashnames[cidx_] + ".RefResume", 0.0, cidx_);
2223 }
2224 }
2225
2226 // ---- second pass: routing out of the entries -------------------------
2227 struct Ctx {
2228 std::size_t curclass;
2229 int jobpos; // 1 = at the client, 2 = at a server replica, 3 = at the Cache
2230 // The nodes the job stands at when jobpos is atServer. Under `srvn`
2231 // these are always the layer's own server replicas; under `flat`
2232 // they are whichever element's stations the last hop landed on, so
2233 // the position has to be carried rather than assumed.
2234 std::vector<std::size_t> curnodes;
2235 };
2236 const int atClient = 1, atServer = 2, atCache = 3;
2237 std::vector<int> jobposkey(lqn.nidx + 1, atClient);
2238 std::vector<std::size_t> curclasskey(lqn.nidx + 1, 0);
2239 std::vector<std::vector<std::size_t>> curnodeskey(lqn.nidx + 1);
2240
2241 // Port of routeRefPathDescent: one explicit descent into the callee of
2242 // CIDX_, replacing the surrogate delay that would charge its whole
2243 // residence at the client. The multiplicity is a geometric loop per
2244 // edge, never one flat loop of the product, and the m < 1 branch is not
2245 // optional: 1 - 1/m goes negative below one.
2246 auto rp_descent = [&](std::size_t cidx_, Ctx st) -> Ctx {
2247 const std::size_t gate = rp_gate[cidx_], res = rp_resume[cidx_], aux = rp_aux[cidx_];
2248 const T m_ = rp_call_mean[cidx_];
2249 const std::size_t enter = rp_child_task[cidx_] > 0 ? cls[rp_child_task[cidx_]]
2250 : rp_hop_cls[rp_child_entry[cidx_]];
2251 if (st.jobpos == atClient)
2252 m.set_route(st.curclass, gate, clientNode, clientNode, Tone());
2253 else
2254 for (std::size_t nd : st.curnodes) m.set_route(st.curclass, gate, nd, clientNode, Tone());
2255 std::size_t cont;
2256 if (m_ == Tone()) {
2257 m.set_route(gate, enter, clientNode, clientNode, Tone());
2258 cont = res;
2259 } else if (m_ > Tone()) {
2260 m.set_route(gate, enter, clientNode, clientNode, Tone());
2261 m.set_route(res, enter, clientNode, clientNode, T(Tone() - Tone() / m_));
2262 m.set_route(res, aux, clientNode, clientNode, T(Tone() / m_));
2263 cont = aux;
2264 } else {
2265 m.set_route(gate, enter, clientNode, clientNode, m_);
2266 m.set_route(gate, aux, clientNode, clientNode, T(Tone() - m_));
2267 m.set_route(res, aux, clientNode, clientNode, Tone());
2268 cont = aux;
2269 }
2270 st.jobpos = atClient;
2271 st.curnodes.clear();
2272 st.curclass = cont;
2273 return st;
2274 };
2275
2276 std::function<Ctx(std::size_t, std::size_t, Ctx)> recur =
2277 [&](std::size_t tidx_caller, std::size_t aidx, Ctx st) -> Ctx {
2278 jobposkey[aidx] = st.jobpos;
2279 curclasskey[aidx] = st.curclass;
2280 curnodeskey[aidx] = st.curnodes;
2281 const std::vector<std::size_t> nexts = lqn.graph.succ(aidx);
2282 std::size_t lastEntryClass = st.curclass;
2283 // fork pre-state save/restore rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
2284 bool next_is_fork = false;
2285 for (std::size_t sx : nexts)
2286 if (lqn.actposttype[sx] == PrecedenceType::POST_AND) next_is_fork = true;
2287 const Ctx preFork = st;
2288 std::vector<std::size_t> andSuccs;
2289 for (std::size_t sx : nexts)
2290 if (lqn.actposttype[sx] == PrecedenceType::POST_AND) andSuccs.push_back(sx);
2291
2292 for (std::size_t k = 0; k < nexts.size(); ++k) {
2293 const std::size_t nextaidx = nexts[k];
2294 if (next_is_fork) st = preFork;
2295 bool isLoop = lqn.graph.get(aidx, nextaidx) != lqn.dag.get(aidx, nextaidx);
2296 if (lqn.parent[aidx] != lqn.parent[nextaidx]) {
2297 // a call to an entry of another task
2298 std::size_t cidx = 0;
2299 for (std::size_t c : lqn.callsof[aidx])
2300 if (lqn.callpair_dst[c] == nextaidx) cidx = c;
2301 if (cidx == 0) continue;
2302 // FWD lays down no routing: its work is already carried by the pseudo SYNC arc
2303 if (lqn.calltype[cidx] != CallType::SYNC) continue;
2304 const std::size_t gid = group_of_call[cidx];
2305 if (gid != 0) {
2306 // The whole group is routed once, at its FIRST member;
2307 // the others are the same dispatch and lay down nothing.
2308 if (group_members[gid].empty() || group_members[gid][0] != cidx) continue;
2309 // The job is switched into the dispatch class while still
2310 // at the client, so (client, dispatch) carries exactly
2311 // the n arcs the strategy chooses among. The 1/n split
2312 // laid down here is the probabilistic reading a solver
2313 // without state-dependent routing would see; the
2314 // declared strategy replaces it below.
2315 std::vector<std::size_t> tnode2, tstat2, tcall2;
2316 for (std::size_t mc : group_members[gid]) {
2317 const std::size_t tt = lqn.parent[lqn.callpair_dst[mc]];
2318 if (servers_for(tt).empty()) continue;
2319 tnode2.push_back(server_nodes_for(tt)[0]);
2320 tstat2.push_back(servers_for(tt)[0]);
2321 tcall2.push_back(mc);
2322 }
2323 if (tnode2.size() < 2) continue; // not enough of it is here
2324 const std::size_t fromNode2 =
2325 st.jobpos == atClient ? clientNode : st.curnodes[0];
2326 const std::size_t dc = grp_dispatch[gid], gc = grp_class[gid];
2327 m.set_route(st.curclass, dc, fromNode2, grp_router[gid], Tone());
2328 const T share2 =
2329 T(Tone() / num_traits<T>::from_int(int(tnode2.size())));
2330 for (std::size_t d = 0; d < tnode2.size(); ++d) {
2331 m.set_route(dc, dc, grp_router[gid], tnode2[d], share2);
2332 m.set_route(dc, gc, tnode2[d], clientNode, Tone());
2333 m.set_service(tstat2[d], dc, callservtproc[tcall2[d]]);
2334 call_map.push_back({idx, tcall2[d], tstat2[d], dc});
2335 }
2336 st.curclass = gc;
2337 st.jobpos = atClient;
2338 st.curnodes.clear();
2339 continue;
2340 }
2341 if (rp_on && rp_expand[cidx]) {
2342 // the callee is on the reference path into this layer: the
2343 // customer descends into it rather than waiting a delay out
2344 st = rp_descent(cidx, st);
2345 continue;
2346 }
2347 const std::size_t ctgt = lqn.parent[lqn.callpair_dst[cidx]];
2348 st = route_sync_call(m, idx, cidx, st, server_nodes_for(ctgt),
2349 servers_for(ctgt), callcls, auxcallcls, clientNode,
2350 atClient, atServer, flat);
2351 continue;
2352 }
2353 // a successor inside the same task
2354 bool any_entry_succ = false;
2355 for (std::size_t sx : nexts)
2356 if (sx > lqn.eshift && sx <= lqn.eshift + lqn.nentries) any_entry_succ = true;
2357 if (!any_entry_succ) {
2358 st.jobpos = jobposkey[aidx];
2359 st.curclass = curclasskey[aidx];
2360 st.curnodes = curnodeskey[aidx];
2361 } else {
2362 if (k > 0 && nexts[k - 1] > lqn.eshift && nexts[k - 1] <= lqn.eshift + lqn.nentries)
2363 lastEntryClass = st.curclass;
2364 st.jobpos = atClient;
2365 st.curclass = lastEntryClass;
2366 st.curnodes.clear();
2367 }
2368 const T w = lqn.graph.get(aidx, nextaidx);
2369
2370 // THE CACHE READ, buildLayersRecursive.m:743-765. `aidx` is the
2371 // item entry and `nextaidx` the activity bound to it: the job
2372 // goes from the client to the Cache node, which decides the hit
2373 // or the miss and switches the class accordingly, so the read
2374 // class itself is never served anywhere and the branch classes
2375 // pick the work up at the server.
2376 if (iscachelayer && lqn.nitems[aidx] > 0 && cls[nextaidx] != 0) {
2377 const std::size_t readcls = cls[nextaidx];
2378 m.set_route(st.curclass, readcls, clientNode, cacheNode, w);
2379 if (cachepar.pread.size() < m.classes.size())
2380 cachepar.pread.resize(m.classes.size());
2381 cachepar.pread[readcls - 1] = lqn.itemproc[aidx];
2382 const std::vector<std::size_t> hm = lqn.graph.succ(nextaidx);
2383 if (hm.size() != 2)
2384 throw InputError("SolverLN: the cache read '" + lqn.names[nextaidx] +
2385 "' needs exactly one hit and one miss successor");
2386 if (cachepar.hitclass.size() < m.classes.size()) {
2387 cachepar.hitclass.resize(m.classes.size(), 0);
2388 cachepar.missclass.resize(m.classes.size(), 0);
2389 }
2390 cachepar.hitclass[readcls - 1] = cls[hm[0]];
2391 cachepar.missclass[readcls - 1] = cls[hm[1]];
2392 st.jobpos = atCache;
2393 st.curclass = readcls;
2394 st.curnodes.clear();
2395 st = recur(tidx_caller, nextaidx, st);
2396 continue;
2397 }
2398
2399 const bool is_and_join_tail = lqn.actpretype[aidx] == PrecedenceType::PRE_AND;
2400 // the branch index of this successor among the fork's outputs
2401 std::size_t fbranch = 0;
2402 if (next_is_fork)
2403 for (std::size_t q = 0; q < andSuccs.size(); ++q)
2404 if (andSuccs[q] == nextaidx) fbranch = q + 1;
2405
2406 // Where the successor's HOST DEMAND is served: at the station of
2407 // its own processor when this layer holds it, at the client
2408 // otherwise. Under `srvn` that is the host layer's own server
2409 // and nothing else, so this reduces to the `ishostlayer` test
2410 // it replaces.
2411 const std::size_t hidxOf = lqn.parent[lqn.parent[nextaidx]];
2412 const std::vector<std::size_t>& actStations = servers_for(hidxOf);
2413 const std::vector<std::size_t>& actNodes = server_nodes_for(hidxOf);
2414 const bool actAtServer = !actStations.empty();
2415 const std::size_t from = st.jobpos == atClient ? clientNode
2416 : st.jobpos == atCache ? cacheNode
2417 : st.curnodes[0];
2418 // the node a job continues to after this successor is served
2419 for (std::size_t r = 0; r < nreplicas; ++r) {
2420 const std::size_t fromNode =
2421 st.jobpos == atClient ? clientNode
2422 : st.jobpos == atCache ? cacheNode
2423 : st.curnodes[std::min(r, st.curnodes.size() - 1)];
2424 const std::size_t toNode = actAtServer ? actNodes[r] : clientNode;
2425 if (next_is_fork && fbranch > 0) {
2426 m.set_route(st.curclass, st.curclass, fromNode, forkNode, Tone());
2427 if (r == 0) forkClassStack.push_back(st.curclass);
2428 m.set_route(st.curclass, st.curclass, forkNode, forkRouter[fbranch - 1],
2429 Tone());
2430 m.set_route(st.curclass, cls[nextaidx], forkRouter[fbranch - 1], toNode,
2431 Tone());
2432 } else if (is_and_join_tail) {
2433 // rejoin the class the branch was forked from, then
2434 // leave the Join in the successor's class
2435 if (forkClassStack.empty())
2436 throw InputError("SolverLN: an AND join has no matching fork in '" +
2437 lqn.names[aidx] + "'");
2438 const std::size_t forkClass = forkClassStack.back();
2439 if (r + 1 == nreplicas) forkClassStack.pop_back();
2440 m.set_route(st.curclass, forkClass, fromNode, joinNode, Tone());
2441 m.set_route(forkClass, cls[nextaidx], joinNode, toNode, Tone());
2442 } else {
2443 m.set_route(st.curclass, cls[nextaidx], fromNode, toNode, w);
2444 }
2445 if (actAtServer)
2446 m.set_service(actStations[r], cls[nextaidx], lqn.hostdem[nextaidx]);
2447 }
2448 (void)from;
2449 if (actAtServer) {
2450 st.jobpos = atServer;
2451 st.curclass = cls[nextaidx];
2452 st.curnodes = actNodes;
2453 servt_map.push_back({idx, nextaidx, actStations[0], cls[nextaidx]});
2454 } else {
2455 st.jobpos = atClient;
2456 st.curclass = cls[nextaidx];
2457 st.curnodes.clear();
2458 m.set_service(m.clientIdx, cls[nextaidx], servtproc[nextaidx]);
2459 thinkt_map.push_back({idx, nextaidx, m.clientIdx, cls[nextaidx]});
2460 }
2461 if (aidx != nextaidx && !isLoop) {
2462 st = recur(tidx_caller, nextaidx, st);
2463 // close the branch with a reply back to the caller's class; a
2464 // merged caller replies to its OWN return class instead, which
2465 // hands the customer back to whichever descent invoked it
2466 const std::size_t reply =
2467 (rp_on && rp_spliced[tidx_caller]) ? rp_ret_cls[tidx_caller]
2468 : cls[tidx_caller];
2469 if (st.jobpos == atClient) {
2470 m.set_route(st.curclass, reply, clientNode, clientNode, Tone());
2471 } else {
2472 for (std::size_t nd : st.curnodes)
2473 m.set_route(st.curclass, reply, nd, clientNode, Tone());
2474 }
2475 // .Aux completion-guard rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
2476 if (!is_aux_class(m.classes[st.curclass - 1].name))
2477 m.classes[st.curclass - 1].completes = true;
2478 }
2479 }
2480 return st;
2481 };
2482
2483 for (std::size_t tidx_caller : callers) {
2484 if (!caller_needs_class(tidx_caller)) continue;
2485 const std::vector<std::size_t>& ents = lqn.entriesof[tidx_caller];
2486 const T share = T(Tone() / num_traits<T>::from_int(int(ents.size())));
2487 for (std::size_t eidx : ents) {
2488 m.set_route(cls[tidx_caller], cls[eidx], clientNode, clientNode, share);
2489 if (ents.size() > 1)
2490 route_map.push_back({idx, tidx_caller, eidx, m.clientIdx, m.clientIdx,
2491 cls[tidx_caller], cls[eidx]});
2492 Ctx st{cls[eidx], atClient, {}};
2493 recur(tidx_caller, eidx, st);
2494 }
2495 }
2496
2497 // ---- reference-path arcs no activity graph of this layer writes ------
2498 // routeRefPath: the reference stage, the hop stages and every callee's
2499 // return split.
2500 if (rp_on) {
2501 for (std::size_t g_ = 0; g_ < rp_group_reftask.size(); ++g_) {
2502 if (rp_group_head_is_caller[g_]) continue; // its own graph carries the descent
2503 std::vector<std::size_t> roots;
2504 for (std::size_t r_ : rp_group_roots[g_])
2505 if (rp_hop[r_]) roots.push_back(r_);
2506 if (roots.empty()) continue;
2507 // the REF task picks among its entries: a PROBABILISTIC split
2508 const T share = T(Tone() / num_traits<T>::from_int(int(roots.size())));
2509 for (std::size_t r_ : roots) {
2510 m.set_route(rp_ref_stage[g_], rp_hop_cls[r_], clientNode, clientNode, share);
2511 m.set_route(rp_hop_ret[r_], rp_ref_stage[g_], clientNode, clientNode, Tone());
2512 }
2513 }
2514 // the descents below a hop are a SERIES: one cycle traverses all of them
2515 for (std::size_t ev = 1; ev <= lqn.nidx; ++ev) {
2516 if (!rp_hop[ev]) continue;
2517 Ctx cur{rp_hop_cls[ev], atClient, {}};
2518 for (std::size_t c_ : rp_hop_children[ev]) cur = rp_descent(c_, cur);
2519 m.set_route(cur.curclass, rp_hop_ret[ev], clientNode, clientNode, Tone());
2520 }
2521 // Return arcs, split by the share of the callee's invocations each
2522 // descent contributes: v(p_i)*m_i, exact rather than a heuristic.
2523 for (std::size_t tv = 1; tv <= lqn.nidx; ++tv) {
2524 if (!rp_spliced[tv]) continue;
2525 for (std::size_t c_ : rp_member_inbound[tv])
2526 if (rp_ret_share[c_] > Tzero())
2527 m.set_route(rp_ret_cls[tv], rp_resume[c_], clientNode, clientNode,
2528 rp_ret_share[c_]);
2529 }
2530 for (std::size_t ev = 1; ev <= lqn.nidx; ++ev) {
2531 if (!rp_hop[ev]) continue;
2532 for (std::size_t c_ : rp_hop_inbound[ev])
2533 if (rp_ret_share[c_] > Tzero())
2534 m.set_route(rp_hop_ret[ev], rp_resume[c_], clientNode, clientNode,
2535 rp_ret_share[c_]);
2536 }
2537 }
2538
2539 // ---- open streams, laid down AFTER the activity-graph walk ----------
2540 // buildLayersRecursive.m:473 puts the entry-arrival routing here for the
2541 // same reason: the walk above rewrites whole rows of P and would erase
2542 // an arc written before it. Both kinds route Source -> server -> Sink
2543 // within one class, so they never join a closed class in a chain --
2544 // refresh_chains would reject that, the two carrying different refstats.
2545 if (sourceStation != 0) {
2546 const T nrep = num_traits<T>::from_int(int(nreplicas));
2547 for (std::size_t cidx : async_here) {
2548 const std::size_t oc = callcls[cidx];
2549 if (oc == 0) continue;
2550 // the stream enters the CALLEE's station, which under `srvn` is
2551 // this layer's own server and under `flat` one among many
2552 const std::vector<std::size_t>& anode =
2553 server_nodes_for(lqn.parent[lqn.callpair_dst[cidx]]);
2554 const T callmean = lqn.callproc_mean[cidx];
2555 if (callmean < Tone()) {
2556 // fewer than one call per firing: a single Bernoulli pass
2557 m.set_route(oc, oc, sourceNode, sinkNode, T(Tone() - callmean));
2558 for (std::size_t r = 0; r < anode.size(); ++r) {
2559 m.set_route(oc, oc, sourceNode, anode[r], T(callmean / nrep));
2560 m.set_route(oc, oc, anode[r], sinkNode, Tone());
2561 }
2562 } else {
2563 // callmean visits in expectation, as a geometric self-loop
2564 const T p = T(Tone() / callmean);
2565 for (std::size_t r = 0; r < anode.size(); ++r) {
2566 m.set_route(oc, oc, sourceNode, anode[r], T(Tone() / nrep));
2567 for (std::size_t q = 0; q < anode.size(); ++q)
2568 m.set_route(oc, oc, anode[r], anode[q], T((Tone() - p) / nrep));
2569 m.set_route(oc, oc, anode[r], sinkNode, p);
2570 }
2571 }
2572 }
2573 for (std::size_t eidx : open_entries) {
2574 qn::JobClass eo;
2575 eo.name = lqn.hashnames[eidx] + "_Open";
2576 eo.type = JobClassType::OPEN;
2577 eo.population = std::numeric_limits<double>::infinity();
2578 eo.refstat = sourceStation;
2579 eo.completes = false;
2580 eo.is_ref_class = false;
2581 eo.attr_kind = int(LqnElement::ENTRY);
2582 eo.attr_idx = eidx;
2583 const std::size_t ec = m.add_class(eo);
2584 // the arrival is exogenous and fixed, so it is never reseeded
2585 m.set_service(sourceStation, ec, lqn.arrival[eidx]);
2586 // entries are Immediate; the work is the activity bound to them
2587 std::size_t bound = 0;
2588 for (std::size_t sx : lqn.graph.succ(eidx))
2589 if (bound == 0 && sx > lqn.ashift) bound = sx;
2590 const Distrib<T>& svc = bound != 0 ? servtproc[bound] : servtproc[eidx];
2591 // the arrival enters the processor of the entry's task under
2592 // host layering, and the task's own station under `flat`
2593 const std::vector<std::size_t>& ostat =
2594 flat ? servers_for(lqn.parent[eidx]) : server;
2595 const std::vector<std::size_t>& onode =
2596 flat ? server_nodes_for(lqn.parent[eidx]) : serverNode;
2597 for (std::size_t r = 0; r < ostat.size(); ++r) {
2598 m.set_service(ostat[r], ec, svc);
2599 m.set_route(ec, ec, sourceNode, onode[r], T(Tone() / nrep));
2600 m.set_route(ec, ec, onode[r], sinkNode, Tone());
2601 }
2602 }
2603 }
2604
2605 // ---- admission constraint on the server station ---------------------
2606 // buildLayersRecursive.m:596-625. The constraint is stated over LQN
2607 // elements and has to be re-expressed in the layer's own classes: a
2608 // task occupies its host through the classes of its ACTIVITIES, an
2609 // entry is occupied by the classes of the CALLS that target it. The
2610 // columns are summed into the class, never assigned, because several
2611 // classes can stand for one column.
2612 // Rows of the admission constraint, in the layer's own classes: the
2613 // declared lincon first, then the refpath pool rows.
2614 std::vector<std::vector<T>> Arows;
2615 std::vector<T> brows;
2616 if (idx < lqn.lincon_A.size() && lqn.lincon_A[idx].rows() > 0) {
2617 const Matrix<T>& Aelem = lqn.lincon_A[idx];
2618 Matrix<T> Alayer(Aelem.rows(), m.classes.size(), Tzero());
2619 const std::vector<std::size_t>& constrained =
2620 ishostlayer ? lqn.tasksof[idx] : lqn.entriesof[idx];
2621 for (std::size_t j = 0; j < constrained.size() && j < Aelem.cols(); ++j) {
2622 std::vector<std::size_t> layerClasses;
2623 if (ishostlayer) {
2624 for (std::size_t a : lqn.actsof[constrained[j]])
2625 if (cls[a] != 0) layerClasses.push_back(cls[a]);
2626 } else {
2627 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
2628 if (lqn.callpair_dst[c] == constrained[j] && callcls[c] != 0)
2629 layerClasses.push_back(callcls[c]);
2630 }
2631 for (std::size_t k = 0; k < layerClasses.size(); ++k)
2632 for (std::size_t rr = 0; rr < Aelem.rows(); ++rr)
2633 Alayer(rr, layerClasses[k] - 1) =
2634 T(Alayer(rr, layerClasses[k] - 1) + Aelem(rr, j));
2635 }
2636 for (std::size_t rr = 0; rr < Alayer.rows(); ++rr) {
2637 std::vector<T> row(m.classes.size(), Tzero());
2638 for (std::size_t k = 0; k < m.classes.size(); ++k) row[k] = Alayer(rr, k);
2639 Arows.push_back(row);
2640 brows.push_back(lqn.lincon_b[idx][rr]);
2641 }
2642 }
2643 // A merged caller no longer carries its thread pool as a population, so
2644 // the pool is declared as an admission bound on the server instead, BUT
2645 // ONLY WHEN THE LAYER SOLVER CAN HONOUR ONE. Under the default MVA layer
2646 // solver the pool's other carrier, the thread-acquisition wait charged
2647 // at every descent gate and hop stage, carries it alone; emitting the
2648 // region there anyway would change which engine solves the layer.
2649 if (rp_on && layer_solver_declares_region()) {
2650 std::vector<std::size_t> srvEntries;
2651 for (std::size_t sidx : idxSet)
2652 for (std::size_t e : lqn.entriesof[sidx]) srvEntries.push_back(e);
2653 for (std::size_t tv = 1; tv <= lqn.nidx; ++tv) {
2654 if (!rp_spliced[tv]) continue;
2655 const double cap = lqn.maxmult[tv];
2656 if (!std::isfinite(cap) || cap <= 0.0) continue;
2657 std::vector<T> row(m.classes.size(), Tzero());
2658 bool anyc = false;
2659 if (ishostlayer) {
2660 // a task occupies the host through the classes of its activities
2661 for (std::size_t a_ : lqn.actsof[tv])
2662 if (cls[a_] != 0) {
2663 row[cls[a_] - 1] = Tone();
2664 anyc = true;
2665 }
2666 } else {
2667 // a task occupies the server through the calls it makes into it
2668 for (std::size_t a_ : lqn.actsof[tv])
2669 for (std::size_t c_ : lqn.callsof[a_])
2670 if (std::find(srvEntries.begin(), srvEntries.end(),
2671 lqn.callpair_dst[c_]) != srvEntries.end() &&
2672 callcls[c_] != 0) {
2673 row[callcls[c_] - 1] = Tone();
2674 anyc = true;
2675 }
2676 }
2677 if (anyc) {
2678 Arows.push_back(row);
2679 brows.push_back(num_traits<T>::from_double(cap));
2680 }
2681 }
2682 }
2683 {
2684 bool any = false;
2685 for (const std::vector<T>& row : Arows)
2686 for (const T& v : row)
2687 if (v != Tzero()) any = true;
2688 if (any) {
2689 Matrix<T> Alayer(Arows.size(), m.classes.size(), Tzero());
2690 for (std::size_t rr = 0; rr < Arows.size(); ++rr)
2691 for (std::size_t k = 0; k < m.classes.size(); ++k) Alayer(rr, k) = Arows[rr][k];
2692 // ONE region over every replica, not one each: the constraint
2693 // models a passive resource of the server as a whole (a
2694 // semaphore, a connection pool), so the replicas share tokens.
2695 typename qn::NetworkStruct<T>::Region rg;
2696 const std::size_t M = m.stations.size(), K = m.classes.size();
2697 rg.cap.assign(M, std::vector<double>(K + 1, -1.0));
2698 rg.maxmem.assign(M, -1.0);
2699 rg.members.assign(M, false);
2700 rg.rule.assign(K, lang::DropStrategy::WAITQ);
2701 rg.weight.assign(K, Tone());
2702 rg.size.assign(K, Tone());
2703 for (std::size_t r = 0; r < nreplicas; ++r) rg.members[server[r] - 1] = true;
2704 rg.lincon_A = Alayer;
2705 rg.lincon_b = brows;
2706 m.regions.push_back(rg);
2707 }
2708 }
2709
2710 // The Cache node's parameters are only complete once the walk has seen
2711 // every read: pread, hitclass and missclass are all per READ CLASS, and
2712 // the classes are created as the activity graph is traversed.
2713 if (cacheNode != 0) {
2714 cachepar.pread.resize(m.classes.size());
2715 cachepar.hitclass.resize(m.classes.size(), 0);
2716 cachepar.missclass.resize(m.classes.size(), 0);
2717 m.nodeparam[cacheNode] = cachepar;
2718 }
2719
2720 // The declared dispatch replaces the probabilistic split on the router,
2721 // and ONLY on the (router, dispatch class) pair whose arcs are exactly
2722 // the group's targets. The split stays in P underneath, which is what
2723 // `refresh_routing` re-expands for anything that reads a matrix; the SSA
2724 // engine walks the arcs itself and honours the strategy.
2725 for (const std::array<std::size_t, 3>& site : routed_group_sites) {
2726 qn::NodeDef& nd = m.nodes[site[0] - 1];
2727 if (nd.routing.size() < m.classes.size())
2728 nd.routing.resize(m.classes.size(), RoutingStrategy::PROB);
2729 nd.routing[site[1] - 1] = lqn.callgroups[site[2] - 1].strategy;
2730 }
2731
2732 // ---- queue-dependent service rates on the server station ------------
2733 // buildLayersRecursive.m:700-762. Declared over the server's OPERANDS
2734 // (the tasks of a host, the entries of a task) and re-expressed in the
2735 // layer's own classes, on the same mapping the admission constraint
2736 // uses above. The handles are evaluated in two index spaces -- CTMC and
2737 // the exact recursions pass a per-class vector, the AMVA and NC chain
2738 // recursions a per-chain one -- so each carries both column lists and
2739 // picks by the length of what it is handed. The chain list is only known
2740 // after refresh_chains, hence the shared slot filled just below.
2741 std::vector<std::pair<std::shared_ptr<std::vector<std::vector<std::size_t>>>,
2742 std::vector<std::vector<std::size_t>>>> deferred_chaincols;
2743 for (std::size_t sidx : idxSet) {
2744 const bool hasld = sidx < lqn.lldscaling.size() && !lqn.lldscaling[sidx].empty();
2745 const bool hascd = sidx < lqn.cdscaling.size() && bool(lqn.cdscaling[sidx]);
2746 const bool hasjd = sidx < lqn.jdscaling.size() && bool(lqn.jdscaling[sidx]);
2747 const bool haspools = sidx < lqn.pools.size() && !lqn.pools[sidx].empty();
2748 if (!(hasld || hascd || hasjd || haspools)) continue;
2749 const bool sishost = sidx <= lqn.nhosts;
2750 const std::vector<std::size_t>& operandIdx =
2751 sishost ? lqn.tasksof[sidx] : lqn.entriesof[sidx];
2752 std::vector<std::vector<std::size_t>> cols(operandIdx.size());
2753 for (std::size_t j = 0; j < operandIdx.size(); ++j) {
2754 if (sishost) {
2755 for (std::size_t a : lqn.actsof[operandIdx[j]])
2756 if (cls[a] != 0) cols[j].push_back(cls[a] - 1);
2757 } else {
2758 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
2759 if (lqn.callpair_dst[c] == operandIdx[j] && callcls[c] != 0)
2760 cols[j].push_back(callcls[c] - 1);
2761 }
2762 }
2763 const std::size_t R = m.classes.size();
2764 std::shared_ptr<std::vector<std::vector<std::size_t>>> chaincols =
2765 std::make_shared<std::vector<std::vector<std::size_t>>>();
2766 deferred_chaincols.push_back(std::make_pair(chaincols, cols));
2767 for (std::size_t r = 0; r < nreplicas; ++r) {
2768 // The scalings are written straight onto the Station, as the
2769 // region block above writes m.regions: a Layer is a
2770 // NetworkStruct, not the NetworkBuilder that carries the
2771 // set_*_dependence helpers.
2772 qn::Station<T>& stn = m.stations[srv[sidx][r] - 1];
2773 if (hasld) stn.lldscaling = lqn.lldscaling[sidx];
2774 if (hascd) {
2775 // beta_{i,r} is product form only while an operand maps to a
2776 // single class; where it aggregates several, the same
2777 // scaling is emitted as a joint dependence, numerically
2778 // identical but no longer exact.
2779 bool one_class_each = true;
2780 for (std::size_t j = 0; j < cols.size(); ++j)
2781 if (cols[j].size() > 1) one_class_each = false;
2782 const lang::CdScaling<T> h =
2783 layer_dep_handle(lqn.cdscaling[sidx], cols, chaincols, R);
2784 const std::vector<T> pk = layer_peak(lqn.cdscalingpeak[sidx], cols, R);
2785 if (one_class_each) {
2786 stn.cdscaling = h;
2787 stn.cdscalingpeak = pk;
2788 } else {
2789 stn.jdscaling = h;
2790 stn.jdscalingpeak = pk;
2791 }
2792 }
2793 if (hasjd) {
2794 stn.jdscaling = layer_dep_handle(lqn.jdscaling[sidx], cols, chaincols, R);
2795 stn.jdscalingpeak = layer_peak(lqn.jdscalingpeak[sidx], cols, R);
2796 }
2797 if (haspools) {
2798 // A compatibility declaration IS a rate law: the pools clear
2799 // the activated-server rate of api::sn_compat_rate, order
2800 // independent at every integer state. sn_compat_scaling
2801 // normalises it against the rate the SAME population would
2802 // get under full compatibility, so eta isolates the
2803 // compatibility GRAPH and a fully-compatible pool is the
2804 // neutral eta == 1; the low-occupancy loss stays with the
2805 // solver's own multiserver term.
2806 const lqn::ServerPools<T>& pl = lqn.pools[sidx];
2807 const Matrix<T> compat = pl.compat;
2808 const std::vector<double> counts = pl.counts;
2809 const std::vector<T> rates = pl.rates;
2810 lang::CdScaling<T> etaPool = [compat, counts,
2811 rates](const std::vector<T>& nop) {
2812 return std::vector<T>(
2813 1, api::sn_compat_scaling(compat, counts, rates, nop));
2814 };
2815 stn.jdscaling = layer_dep_handle(etaPool, cols, chaincols, R);
2816 stn.jdscalingpeak = layer_peak(
2817 std::vector<T>(cols.size(), num_traits<T>::from_int(1)), cols, R);
2818 }
2819 }
2820 }
2821
2822 if (rp_on) {
2823 // Per-class normalisation task, read by layer_refclass. A merged chain
2824 // holds more than one caller and only ONE reference class, so without
2825 // this every merged caller's residence would come out scaled by the
2826 // product of the call multiplicities along the path.
2827 std::vector<std::size_t> nc(m.classes.size(), 0);
2828 for (std::size_t tv = 1; tv <= lqn.nidx; ++tv) {
2829 if (!rp_member[tv] || cls[tv] == 0) continue;
2830 const std::size_t own = cls[tv];
2831 std::vector<std::size_t> owned{cls[tv]};
2832 for (std::size_t e_ : lqn.entriesof[tv]) owned.push_back(cls[e_]);
2833 for (std::size_t a_ : lqn.actsof[tv]) {
2834 owned.push_back(cls[a_]);
2835 for (std::size_t c_ : lqn.callsof[a_]) {
2836 owned.push_back(callcls[c_]);
2837 owned.push_back(auxcallcls[c_]);
2838 }
2839 }
2840 // an open class (an async call) has no chain reference rate of its own
2841 for (std::size_t q_ : owned)
2842 if (q_ != 0 && m.classes[q_ - 1].type == JobClassType::CLOSED) nc[q_ - 1] = own;
2843 }
2844 layer_normclass[idx] = nc;
2845 ref_path_audit(m, idx, callers, caller_needs_class, rp_spliced, rp_member, rp_hop,
2846 rp_expand, rp_group_head_is_caller, rp_ref_stage, rp_hop_cls,
2847 rp_hop_ret, rp_ret_cls, rp_gate, rp_aux, rp_resume, cls);
2848 }
2849
2850 // a priority-scheduling processor orders its callers by task priority
2851 apply_host_task_priorities(m, idxSet);
2852 m.refresh_chains();
2853 // The chain columns of every handle emitted above, now that the chains
2854 // exist. The slots are shared with the handles, so filling them here
2855 // reaches every replica without rebuilding a single lambda.
2856 for (std::size_t d = 0; d < deferred_chaincols.size(); ++d) {
2857 const std::vector<std::vector<std::size_t>>& cols = deferred_chaincols[d].second;
2858 std::vector<std::vector<std::size_t>>& out = *deferred_chaincols[d].first;
2859 out.assign(cols.size(), std::vector<std::size_t>());
2860 for (std::size_t j = 0; j < cols.size(); ++j) {
2861 for (std::size_t k = 0; k < cols[j].size(); ++k)
2862 for (std::size_t cc = 0; cc < m.chains.size(); ++cc)
2863 if (cols[j][k] < m.chains[cc].size() && m.chains[cc][cols[j][k]]) {
2864 bool seen = false;
2865 for (std::size_t q = 0; q < out[j].size(); ++q)
2866 if (out[j][q] == cc) seen = true;
2867 if (!seen) out[j].push_back(cc);
2868 }
2869 }
2870 }
2871 // A layer holding a POST_AND fork is a fork-join model like any other,
2872 // and the reference builds it as a `Network`, so buildLayers's
2873 // `getStruct()` runs the MMT node-visit pass on it: layer P:P2 of
2874 // lqn_workflows reports a Join visit of 4.3333, not the 1 the routing
2875 // solve leaves. THE LATER `refresh_chains()` CALLS DO NOT REPEAT IT, and
2876 // that is the reference's behaviour rather than an oversight here:
2877 // SolverLN.m re-runs `refreshChains()` alone after a routing reset,
2878 // which recomputes the visits WITHOUT the pass.
2880 }
2881
2882 /**
2883 * Port of layerSolverDeclaresRegion: true when the layer solver's feature
2884 * set covers an admission constraint. Asked while a layer is being built, so
2885 * it reads the solver CLASS's feature set, as the reference does: NC, FLD
2886 * and SSA declare Region, MVA does not.
2887 */
2888 bool layer_solver_declares_region() const {
2889 return opt.layer_solver == "nc" || opt.layer_solver == "fluid" || opt.layer_solver == "ssa";
2890 }
2891
2892 /**
2893 * Port of refPathAudit. Only the classes refpath wrote are checked: every
2894 * row they leave a node by must sum to one with no negative entry, and the
2895 * layer must carry exactly one reference class per chain it built, which is
2896 * what refresh_chains requires. Warnings, as in the reference.
2897 */
2898 template <class NeedsClass>
2899 void ref_path_audit(const qn::Layer<T>& m, std::size_t idx,
2900 const std::vector<std::size_t>& callers, const NeedsClass& needs_class,
2901 const std::vector<bool>& spliced, const std::vector<bool>& member,
2902 const std::vector<bool>& hop, const std::vector<bool>& expand,
2903 const std::vector<bool>& head_is_caller,
2904 const std::vector<std::size_t>& ref_stage,
2905 const std::vector<std::size_t>& hop_cls,
2906 const std::vector<std::size_t>& hop_ret,
2907 const std::vector<std::size_t>& ret_cls,
2908 const std::vector<std::size_t>& gate, const std::vector<std::size_t>& aux,
2909 const std::vector<std::size_t>& resume,
2910 const std::vector<std::size_t>& cls) const {
2911 std::set<std::size_t> watch;
2912 for (std::size_t k : ref_stage)
2913 if (k) watch.insert(k);
2914 for (std::size_t e = 1; e < hop.size(); ++e)
2915 if (hop[e]) {
2916 if (hop_cls[e]) watch.insert(hop_cls[e]);
2917 if (hop_ret[e]) watch.insert(hop_ret[e]);
2918 }
2919 for (std::size_t t = 1; t < member.size(); ++t)
2920 if (member[t]) {
2921 if (ret_cls[t]) watch.insert(ret_cls[t]);
2922 if (cls[t]) watch.insert(cls[t]);
2923 }
2924 for (std::size_t c = 1; c < expand.size(); ++c)
2925 if (expand[c]) {
2926 if (gate[c]) watch.insert(gate[c]);
2927 if (aux[c]) watch.insert(aux[c]);
2928 if (resume[c]) watch.insert(resume[c]);
2929 }
2930 const std::size_t I = m.nodes.size();
2931 for (std::size_t r : watch) {
2932 for (std::size_t n = 1; n <= I; ++n) {
2933 double tot = 0.0;
2934 bool neg = false;
2935 for (const auto& kv : m.P) {
2936 if (kv.first.first != r || n > kv.second.rows()) continue;
2937 for (std::size_t j = 0; j < kv.second.cols(); ++j) {
2938 const double v = dbl(kv.second(n - 1, j));
2939 if (v < -GlobalConstants::FineTol) neg = true;
2940 tot += v;
2941 }
2942 }
2943 if (neg)
2944 std::cerr << "[LINE] Warning: refpath layer '" << lqn.hashnames[idx]
2945 << "': class '" << m.classes[r - 1].name << "' routes out of node "
2946 << n << " with a negative probability.\n";
2947 if (tot > GlobalConstants::FineTol && std::fabs(tot - 1.0) > 1e-6)
2948 std::cerr << "[LINE] Warning: refpath layer '" << lqn.hashnames[idx]
2949 << "': class '" << m.classes[r - 1].name << "' leaves node " << n
2950 << " with total probability " << tot << ".\n";
2951 }
2952 }
2953 std::size_t nref = 0;
2954 for (const qn::JobClass& jc : m.classes)
2955 if (jc.is_ref_class) ++nref;
2956 std::size_t expect = 0;
2957 for (bool h : head_is_caller)
2958 if (!h) ++expect;
2959 for (std::size_t t : callers)
2960 if (!spliced[t] && needs_class(t)) ++expect;
2961 if (nref != expect)
2962 std::cerr << "[LINE] Warning: refpath layer '" << lqn.hashnames[idx] << "' declares "
2963 << nref << " reference classes where " << expect
2964 << " were built; a chain holding two makes refresh_chains throw.\n";
2965 }
2966
2967 /**
2968 * Port of routeSynchCall (buildLayersRecursive.m).
2969 *
2970 * A synchronous call is made `callmean` times per visit to the calling
2971 * activity, against `nreplicas` server replicas. The branch is chosen by
2972 * callmean ALONE, against 1 and never against nreplicas: a Bernoulli pass
2973 * carries at most one call, more than one needs the geometric loop through
2974 * the `<call>.Aux` class, and the replicas only SPLIT each probability by
2975 * ntgt -- they never relax that bound (buildLayersRecursive.m:1028-1051).
2976 * Comparing against nreplicas instead agrees with the reference only at
2977 * nreplicas == 1; on lqn_sockshop's replicated P2_1 it put a callmean of 1
2978 * on the < branch and doubled T1's throughput and P2_1's utilization.
2979 *
2980 * A second class is needed because a self-loop on the call class would also
2981 * re-enter its service. Which of the two carries the call time depends on
2982 * the direction:
2983 *
2984 * callmean < 1 the mean count is folded into the DEMAND
2985 * (callservt = callmean * W), so the call class must
2986 * be visited exactly once or the time is discounted
2987 * twice; .Aux absorbs the remaining branch.
2988 * callmean > 1 the call class is re-entered, and .Aux is the
2989 * return path that closes the loop with probability
2990 * 1/callmean, giving a mean of callmean visits.
2991 *
2992 * The four cases below are (job at client / at server) x (call targets an
2993 * entry of THIS layer's server / of some other task).
2994 */
2995 struct CtxPair {
2996 std::size_t curclass;
2997 int jobpos;
2998 };
2999
3000 /** The `<call>.Aux` suffix, which is how the reference identifies them too. */
3001 static bool is_aux_class(const std::string& name) {
3002 return name.size() >= 4 && name.compare(name.size() - 4, 4, ".Aux") == 0;
3003 }
3004 template <class Ctx>
3005 Ctx route_sync_call(qn::Layer<T>& m, std::size_t idx, std::size_t cidx, Ctx st,
3006 const std::vector<std::size_t>& tnode,
3007 const std::vector<std::size_t>& tstat,
3008 const std::vector<std::size_t>& callcls,
3009 const std::vector<std::size_t>& auxcallcls, std::size_t clientNode,
3010 int atClient, int atServer, bool flat) {
3011 const T one = Tone();
3012 // The callee's stations in THIS layer, empty when it is served
3013 // elsewhere: under `srvn` that is the layer's own server or nothing,
3014 // under `flat` it is whichever of the many servers the call targets.
3015 const std::size_t ntgt = tnode.size();
3016 const bool to_this_server = ntgt > 0;
3017 const T nrep = num_traits<T>::from_int(int(ntgt > 0 ? ntgt : 1));
3018 const T share = T(one / nrep);
3019 const T callmean = lqn.callproc_mean[cidx];
3020 const std::size_t cc = callcls[cidx];
3021 const std::size_t ax = auxcallcls[cidx];
3022 const bool below = callmean < one;
3023 const bool above = callmean > one;
3024 if (st.jobpos == atClient) {
3025 if (to_this_server) {
3026 if (below) {
3027 m.set_route(st.curclass, ax, clientNode, clientNode, T(one - callmean));
3028 for (std::size_t r = 0; r < ntgt; ++r) {
3029 m.set_route(st.curclass, cc, clientNode, tnode[r], T(callmean / nrep));
3030 m.set_route(cc, cc, tnode[r], clientNode, one);
3031 }
3032 // keeps .Aux attached to the routing graph; carries no time
3033 m.set_route(ax, cc, clientNode, clientNode, one);
3034 } else if (above) {
3035 for (std::size_t r = 0; r < ntgt; ++r) {
3036 m.set_route(st.curclass, cc, clientNode, tnode[r], share);
3037 m.set_route(cc, ax, tnode[r], clientNode, one);
3038 m.set_route(ax, cc, clientNode, tnode[r], T((one - one / callmean) / nrep));
3039 }
3040 m.set_route(ax, cc, clientNode, clientNode, T(one / callmean));
3041 } else {
3042 for (std::size_t r = 0; r < ntgt; ++r) {
3043 m.set_route(st.curclass, cc, clientNode, tnode[r], share);
3044 m.set_route(cc, cc, tnode[r], clientNode, one);
3045 }
3046 }
3047 for (std::size_t r = 0; r < ntgt; ++r) {
3048 m.set_service(tstat[r], cc, callservtproc[cidx]);
3049 call_map.push_back({idx, cidx, tstat[r], cc});
3050 }
3051 m.set_service(m.clientIdx, cc, Distrib<T>::immediate());
3052 st.jobpos = atClient;
3053 st.curnodes.clear();
3054 st.curclass = cc;
3055 } else {
3056 m.set_route(st.curclass, cc, clientNode, clientNode, one);
3057 if (below || above) {
3058 m.set_route(cc, ax, clientNode, clientNode, one);
3059 st.curclass = ax;
3060 } else {
3061 st.curclass = cc;
3062 }
3063 st.jobpos = atClient;
3064 st.curnodes.clear();
3065 m.set_service(m.clientIdx, cc, callservtproc[cidx]);
3066 call_map.push_back({idx, cidx, m.clientIdx, cc});
3067 }
3068 } else {
3069 // the node the job stands at, one per replica of wherever it landed
3070 auto fromNode = [&](std::size_t r) {
3071 return st.curnodes[std::min(r, st.curnodes.size() - 1)];
3072 };
3073 if (to_this_server) {
3074 if (below) {
3075 // The skip and the call merge back in the CALL class at the
3076 // client, exactly as in the atClient branch above. Routing
3077 // the skip into the call class instead and leaving in .Aux
3078 // gives .Aux no inbound arc at all, so its chain has no
3079 // reference class (buildLayersRecursive.m:1100-1118). This
3080 // port did that until 2026-08-11 and the arm is the same
3081 // under both layerings, as it is in the reference.
3082 for (std::size_t r = 0; r < ntgt; ++r) {
3083 m.set_route(st.curclass, ax, fromNode(r), clientNode, T(one - callmean));
3084 m.set_route(st.curclass, cc, fromNode(r), tnode[r], T(callmean / nrep));
3085 m.set_route(cc, cc, tnode[r], clientNode, one);
3086 }
3087 m.set_route(ax, cc, clientNode, clientNode, one);
3088 m.set_service(m.clientIdx, cc, Distrib<T>::immediate());
3089 st.jobpos = atClient;
3090 st.curnodes.clear();
3091 st.curclass = cc;
3092 } else if (above) {
3093 if (flat) {
3094 // the geometric repeat transits the client between
3095 // visits; a self-loop would merge them into one
3096 for (std::size_t r = 0; r < ntgt; ++r) {
3097 m.set_route(st.curclass, cc, fromNode(r), tnode[r], one);
3098 m.set_route(cc, ax, tnode[r], clientNode, one);
3099 m.set_route(ax, cc, clientNode, tnode[r],
3100 T((one - one / callmean) / nrep));
3101 }
3102 m.set_route(ax, cc, clientNode, clientNode, T(one / callmean));
3103 m.set_service(m.clientIdx, cc, Distrib<T>::immediate());
3104 st.curclass = cc;
3105 } else {
3106 for (std::size_t r = 0; r < ntgt; ++r) {
3107 m.set_route(st.curclass, cc, fromNode(r), tnode[r], one);
3108 m.set_route(cc, cc, tnode[r], tnode[r], T(one - one / callmean));
3109 m.set_route(cc, ax, tnode[r], clientNode, T(one / callmean));
3110 }
3111 st.curclass = ax;
3112 }
3113 st.jobpos = atClient;
3114 st.curnodes.clear();
3115 } else {
3116 for (std::size_t r = 0; r < ntgt; ++r)
3117 m.set_route(st.curclass, cc, fromNode(r), tnode[r], one);
3118 if (flat) {
3119 // the reply returns the job to the client, which is
3120 // where the successor restoration expects it
3121 for (std::size_t r = 0; r < ntgt; ++r)
3122 m.set_route(cc, cc, tnode[r], clientNode, one);
3123 m.set_service(m.clientIdx, cc, Distrib<T>::immediate());
3124 st.jobpos = atClient;
3125 st.curnodes.clear();
3126 } else {
3127 st.jobpos = atServer;
3128 st.curnodes = tnode;
3129 }
3130 st.curclass = cc;
3131 }
3132 for (std::size_t r = 0; r < ntgt; ++r) {
3133 m.set_service(tstat[r], cc, callservtproc[cidx]);
3134 call_map.push_back({idx, cidx, tstat[r], cc});
3135 }
3136 } else {
3137 for (std::size_t nd : st.curnodes)
3138 m.set_route(st.curclass, cc, nd, clientNode, one);
3139 if (below || above) {
3140 m.set_route(cc, ax, clientNode, clientNode, one);
3141 st.curclass = ax;
3142 } else {
3143 st.curclass = cc;
3144 }
3145 st.jobpos = atClient;
3146 st.curnodes.clear();
3147 m.set_service(m.clientIdx, cc, callservtproc[cidx]);
3148 call_map.push_back({idx, cidx, m.clientIdx, cc});
3149 }
3150 }
3151 return st;
3152 }
3153
3154 // -----------------------------------------------------------------------
3155 // getEntryServiceMatrix
3156 // -----------------------------------------------------------------------
3157
3158 /**
3159 * Reachability of activities and calls from each entry, as a 0/1 matrix over
3160 * the combined element-and-call index space. Multiplying it by
3161 * [residt; callresidt] sums an entry's whole service into one number.
3162 */
3163 void build_entry_service_matrix() {
3164 const std::size_t dim = lqn.nidx + lqn.ncalls;
3165 servtmatrix = Matrix<T>(dim + 1, dim + 1, Tzero());
3166 std::function<void(std::size_t, std::size_t)> rec = [&](std::size_t aidx, std::size_t eidx) {
3167 for (std::size_t nextaidx : lqn.graph.succ(aidx)) {
3168 const bool isLoop = lqn.graph.get(aidx, nextaidx) != lqn.dag.get(aidx, nextaidx);
3169 if (lqn.parent[aidx] != lqn.parent[nextaidx]) {
3170 for (std::size_t cidx : lqn.callsof[aidx])
3171 if (lqn.calltype[cidx] == CallType::SYNC)
3172 servtmatrix(eidx, lqn.nidx + cidx) = Tone();
3173 } else if (nextaidx != aidx && !isLoop) {
3174 servtmatrix(eidx, nextaidx) = Tone();
3175 rec(nextaidx, eidx);
3176 }
3177 }
3178 };
3179 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
3180 const std::size_t eidx = lqn.eshift + e;
3181 rec(eidx, eidx);
3182 }
3183 }
3184
3185 // -----------------------------------------------------------------------
3186 // initInterlock
3187 // -----------------------------------------------------------------------
3188
3189 /** Port of initInterlock: the LQNS V5 static interlock analysis. */
3190 void init_interlock() {
3191 const std::size_t NE = lqn.nentries;
3192 il_all = Matrix<T>(NE + 1, NE + 1, Tzero());
3193 il_ph1 = Matrix<T>(NE + 1, NE + 1, Tzero());
3194
3195 std::function<void(std::size_t, std::size_t, T, T, std::vector<bool>&, int)> trace =
3196 [&](std::size_t eidx, std::size_t root_e, T pall, T pph1, std::vector<bool>& visited,
3197 int depth) {
3198 if (eidx <= lqn.eshift || eidx > lqn.eshift + NE) return;
3199 const std::size_t e = eidx - lqn.eshift;
3200 if (visited[e]) return;
3201 visited[e] = true;
3202 il_all(root_e, e) = T(il_all(root_e, e) + pall);
3203 il_ph1(root_e, e) = T(il_ph1(root_e, e) + pph1);
3204 for (std::size_t aidx : lqn.actsof[eidx]) {
3205 if (aidx <= lqn.ashift || aidx > lqn.ashift + lqn.nacts) continue;
3206 const std::size_t a = aidx - lqn.ashift;
3207 if (depth > 0 && lqn.actphase[a] > 1) continue;
3208 const bool is_ph1 = lqn.actphase[a] <= 1;
3209 for (std::size_t cidx : lqn.callsof[aidx]) {
3210 if (lqn.calltype[cidx] != CallType::SYNC) continue;
3211 if (!(lqn.callproc_mean[cidx] > Tzero())) continue;
3212 const std::size_t dst = lqn.callpair_dst[cidx];
3213 if (dst <= lqn.eshift || dst > lqn.eshift + NE) continue;
3214 trace(dst, root_e, T(pall * lqn.callproc_mean[cidx]),
3215 is_ph1 ? T(pph1 * lqn.callproc_mean[cidx]) : Tzero(), visited,
3216 depth + 1);
3217 }
3218 }
3219 visited[e] = false;
3220 };
3221 for (std::size_t e = 1; e <= NE; ++e) {
3222 std::vector<bool> visited(NE + 1, false);
3223 trace(lqn.eshift + e, e, Tone(), Tone(), visited, 0);
3224 }
3225
3226 il_common_entries.assign(NT() + 1, {});
3227 il_src_all.assign(NT() + 1, {});
3228 il_src_ph2.assign(NT() + 1, {});
3229 il_num_sources.assign(NT() + 1, 0.0);
3230
3231 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
3232 const std::size_t tidx = lqn.tshift + t;
3233 if (lqn.isref[tidx] || lqn.sched[tidx] == SchedStrategy::INF) continue;
3234 interlock_for_server(tidx);
3235 }
3236 for (std::size_t h = 1; h <= lqn.nhosts; ++h) {
3237 if (lqn.sched[h] == SchedStrategy::INF) continue;
3238 interlock_for_server(h);
3239 }
3240 }
3241
3242 std::vector<std::size_t> server_entry_nums(std::size_t serverIdx) const {
3243 std::vector<std::size_t> out;
3244 if (serverIdx <= lqn.nhosts) {
3245 for (std::size_t tidx : lqn.tasksof[serverIdx])
3246 for (std::size_t se : lqn.entriesof[tidx]) out.push_back(se - lqn.eshift);
3247 } else {
3248 for (std::size_t se : lqn.entriesof[serverIdx]) out.push_back(se - lqn.eshift);
3249 }
3250 return out;
3251 }
3252
3253 std::vector<std::size_t> client_tasks(std::size_t serverIdx) const {
3254 std::vector<std::size_t> out;
3255 if (serverIdx <= lqn.nhosts) return lqn.tasksof[serverIdx];
3256 for (std::size_t se : lqn.entriesof[serverIdx])
3257 for (std::size_t ci : lqn.iscaller.col(se))
3258 if (ci > lqn.tshift && ci <= NT()) out.push_back(ci);
3259 std::sort(out.begin(), out.end());
3260 out.erase(std::unique(out.begin(), out.end()), out.end());
3261 return out;
3262 }
3263
3264 std::vector<std::size_t> call_dst_tasks(std::size_t src_eidx, std::size_t target_e) const {
3265 std::vector<std::size_t> out;
3266 for (std::size_t aidx : lqn.actsof[src_eidx]) {
3267 if (aidx <= lqn.ashift || aidx > lqn.ashift + lqn.nacts) continue;
3268 for (std::size_t cidx : lqn.callsof[aidx]) {
3269 if (lqn.calltype[cidx] != CallType::SYNC) continue;
3270 const std::size_t dst = lqn.callpair_dst[cidx];
3271 const std::size_t de = dst - lqn.eshift;
3272 if (de >= 1 && de <= lqn.nentries && il_all(de, target_e) > Tzero())
3273 out.push_back(lqn.parent[dst]);
3274 }
3275 }
3276 std::sort(out.begin(), out.end());
3277 out.erase(std::unique(out.begin(), out.end()), out.end());
3278 return out;
3279 }
3280
3281 bool is_branch_point(std::size_t srcX, std::size_t entryA, std::size_t srcY,
3282 std::size_t entryB) const {
3283 const std::size_t taskA = lqn.parent[entryA], taskB = lqn.parent[entryB];
3284 const std::size_t taskX = lqn.parent[srcX];
3285 if (taskX == taskA && taskX == taskB) return false;
3286 if (srcX == entryA || srcY == entryB) return true;
3287 const std::vector<std::size_t> dx = call_dst_tasks(srcX, entryA - lqn.eshift);
3288 const std::vector<std::size_t> dy = call_dst_tasks(srcY, entryB - lqn.eshift);
3289 for (std::size_t a : dx)
3290 for (std::size_t b : dy)
3291 if (a != b) return true;
3292 return false;
3293 }
3294
3295 void trace_to_server(std::size_t eidx, std::size_t serverIdx, std::vector<bool>& visited,
3296 std::vector<std::size_t>& itasks, bool isHead) const {
3297 if (eidx <= lqn.eshift || eidx > lqn.eshift + lqn.nentries) return;
3298 const std::size_t e = eidx - lqn.eshift;
3299 if (visited[e]) return;
3300 const std::size_t owner = lqn.parent[eidx];
3301 if (owner == serverIdx) return;
3302 if (serverIdx <= lqn.nhosts && lqn.parent[owner] == serverIdx) return;
3303 visited[e] = true;
3304 bool found = false;
3305 const std::vector<std::size_t> sen = server_entry_nums(serverIdx);
3306 for (std::size_t aidx : lqn.actsof[eidx]) {
3307 if (aidx <= lqn.ashift || aidx > lqn.ashift + lqn.nacts) continue;
3308 for (std::size_t cidx : lqn.callsof[aidx]) {
3309 if (lqn.calltype[cidx] != CallType::SYNC) continue;
3310 const std::size_t dst = lqn.callpair_dst[cidx];
3311 const std::size_t dtask = lqn.parent[dst];
3312 bool reaches = dtask == serverIdx ||
3313 (serverIdx <= lqn.nhosts && lqn.parent[dtask] == serverIdx);
3314 if (!reaches) {
3315 const std::size_t de = dst - lqn.eshift;
3316 for (std::size_t sn : sen)
3317 if (il_all(de, sn) > Tzero()) reaches = true;
3318 }
3319 if (reaches) {
3320 trace_to_server(dst, serverIdx, visited, itasks, false);
3321 found = true;
3322 }
3323 }
3324 }
3325 if (found && !isHead) {
3326 itasks.push_back(owner);
3327 std::sort(itasks.begin(), itasks.end());
3328 itasks.erase(std::unique(itasks.begin(), itasks.end()), itasks.end());
3329 }
3330 visited[e] = false;
3331 }
3332
3333 void interlock_for_server(std::size_t serverIdx) {
3334 const std::vector<std::size_t> sen = server_entry_nums(serverIdx);
3335 if (sen.empty()) return;
3336 const std::vector<std::size_t> cts = client_tasks(serverIdx);
3337 if (cts.empty()) return;
3338
3339 std::vector<std::pair<std::size_t, std::size_t>> pairs; // (task, entry number)
3340 for (std::size_t ct : cts)
3341 for (std::size_t ce : lqn.entriesof[ct]) {
3342 const std::size_t cen = ce - lqn.eshift;
3343 if (cen < 1 || cen > lqn.nentries) continue;
3344 for (std::size_t se : sen)
3345 if (il_all(cen, se) > Tzero()) {
3346 pairs.emplace_back(ct, cen);
3347 break;
3348 }
3349 }
3350 if (pairs.size() < 2) return;
3351
3352 std::vector<std::size_t> common;
3353 for (std::size_t i = 0; i < pairs.size(); ++i)
3354 for (std::size_t j = i + 1; j < pairs.size(); ++j) {
3355 if (pairs[i].first == pairs[j].first) continue;
3356 const std::size_t eA = pairs[i].second, eC = pairs[j].second;
3357 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
3358 const std::size_t tidx = lqn.tshift + t;
3359 for (std::size_t ex : lqn.entriesof[tidx])
3360 for (std::size_t ey : lqn.entriesof[tidx]) {
3361 const std::size_t xn = ex - lqn.eshift, yn = ey - lqn.eshift;
3362 if (xn < 1 || yn < 1 || xn > lqn.nentries || yn > lqn.nentries) continue;
3363 if (il_all(xn, eA) > Tzero() && il_all(yn, eC) > Tzero() &&
3364 is_branch_point(ex, eA + lqn.eshift, ey, eC + lqn.eshift))
3365 common.push_back(ex);
3366 }
3367 }
3368 }
3369 std::sort(common.begin(), common.end());
3370 common.erase(std::unique(common.begin(), common.end()), common.end());
3371 if (common.empty()) return;
3372
3373 std::vector<std::size_t> interlocked;
3374 for (std::size_t ce : common) {
3375 std::vector<bool> visited(lqn.nentries + 1, false);
3376 std::vector<std::size_t> it;
3377 trace_to_server(ce, serverIdx, visited, it, true);
3378 for (std::size_t x : it) interlocked.push_back(x);
3379 }
3380 std::sort(interlocked.begin(), interlocked.end());
3381 interlocked.erase(std::unique(interlocked.begin(), interlocked.end()), interlocked.end());
3382
3383 std::vector<std::size_t> src_all;
3384 for (std::size_t ce : common) src_all.push_back(lqn.parent[ce]);
3385 std::sort(src_all.begin(), src_all.end());
3386 src_all.erase(std::unique(src_all.begin(), src_all.end()), src_all.end());
3387 {
3388 std::vector<std::size_t> diff;
3389 for (std::size_t x : src_all)
3390 if (!std::binary_search(interlocked.begin(), interlocked.end(), x)) diff.push_back(x);
3391 src_all = diff;
3392 }
3393
3394 // Left empty on purpose, and NOT because phase 2 is unsupported: the
3395 // reference computes ph2SrcTasks (initInterlock.m:230-262) and never
3396 // reads it, and its phase-2 source count is behind `if false`
3397 // (initInterlock.m:288-304). Filling it would change no number.
3398 std::vector<std::size_t> src_ph2;
3399 for (std::size_t it : interlocked)
3400 for (std::size_t ie : lqn.entriesof[it])
3401 for (std::size_t ci : lqn.iscaller.col(ie))
3402 if (ci > lqn.tshift && ci <= NT() &&
3403 !std::binary_search(interlocked.begin(), interlocked.end(), ci))
3404 src_all.push_back(ci);
3405 std::sort(src_all.begin(), src_all.end());
3406 src_all.erase(std::unique(src_all.begin(), src_all.end()), src_all.end());
3407
3408 double nsrc = 0.0;
3409 for (std::size_t st : src_all) nsrc += lqn.mult[st];
3410
3411 il_common_entries[serverIdx] = common;
3412 il_src_all[serverIdx] = src_all;
3413 il_src_ph2[serverIdx] = src_ph2;
3414 il_num_sources[serverIdx] = nsrc;
3415 }
3416
3417 // -----------------------------------------------------------------------
3418 // init and the outer iteration
3419 // -----------------------------------------------------------------------
3420 void init() {
3421 const std::size_t N = lqn.nidx;
3422 tput.assign(N + 1, Tzero());
3423 util.assign(N + 1, Tzero());
3424 servt.assign(N + 1, Tzero());
3425 residt.assign(N + 1, Tzero());
3426 thinkt.assign(N + 1, Tzero());
3427 callservt.assign(lqn.ncalls + 1, Tzero());
3428 callresidt.assign(lqn.ncalls + 1, Tzero());
3429 tputproc.assign(N + 1, Distrib<T>::disabled_dist());
3430 servt_ph1.assign(N + 1, Tzero());
3431 servt_ph2.assign(N + 1, Tzero());
3432 prOvertake.assign(lqn.nentries + 1, Tzero());
3433 build_entry_service_matrix();
3434
3435 relax_omega = (opt.relax == "fixed" || opt.relax == "adaptive") ? opt.relax_factor : 1.0;
3436
3437 servt_prev.assign(N + 1, std::numeric_limits<double>::quiet_NaN());
3438 residt_prev.assign(N + 1, std::numeric_limits<double>::quiet_NaN());
3439 tput_prev.assign(N + 1, std::numeric_limits<double>::quiet_NaN());
3440 thinkt_prev.assign(N + 1, std::numeric_limits<double>::quiet_NaN());
3441 callservt_prev.assign(lqn.ncalls + 1, std::numeric_limits<double>::quiet_NaN());
3442 callresidt_prev.assign(lqn.ncalls + 1, std::numeric_limits<double>::quiet_NaN());
3443 servt_prev_v.assign(N + 1, Tzero());
3444 residt_prev_v.assign(N + 1, Tzero());
3445 tput_prev_v.assign(N + 1, Tzero());
3446 thinkt_prev_v.assign(N + 1, Tzero());
3447 callservt_prev_v.assign(lqn.ncalls + 1, Tzero());
3448
3449 unique_route_idx.clear();
3450 for (const RouteRow& r : route_map) unique_route_idx.push_back(r.idx);
3451 std::sort(unique_route_idx.begin(), unique_route_idx.end());
3452 unique_route_idx.erase(std::unique(unique_route_idx.begin(), unique_route_idx.end()),
3453 unique_route_idx.end());
3454
3455 // only 'ilrate' reads the interlock tables, and their static analysis is costly
3456 if (interlockMethod == "ilrate") init_interlock();
3457
3458 maxitererr.assign(opt.iter_max + 2, 0.0);
3459 results_raw_last.clear();
3460 results_raw_valid = false;
3461 averagingstart = -1;
3462 hasconverged = false;
3463 moment_pass_done = false;
3464 results.clear();
3465
3466 servtcdf.assign(N + 1, fluid::FluidPassage());
3467 callservtcdf.assign(lqn.ncalls + 1, fluid::FluidPassage());
3468 entrycdfrespt.assign(lqn.nentries + 1, LnCdf());
3469 entryproc.assign(lqn.nentries + 1, mam::AphPair<T>());
3470 cdf_repo.assign(ensemble.size(), std::vector<std::vector<fluid::FluidPassage>>());
3471 }
3472
3473 /**
3474 * The Picard iteration, with the stochastic controller in place of the
3475 * deterministic test when the layer engine is a simulator.
3476 *
3477 * The two tests are NOT interchangeable and cannot both run: `converged`
3478 * folds a moving average into `results` in place, which is exactly what the
3479 * Polyak-Ruppert average would then be taken of a second time. Whichever
3480 * test is in force therefore owns the results.
3481 */
3482 void iterate() {
3483 init();
3484 const bool stoch = opt.layer_solver == "ssa";
3485 if (stoch) {
3486 LnStochConfig cfg;
3487 cfg.iter_tol = opt.iter_tol;
3488 cfg.relax_burnin = relax_omega;
3489 stoch_ctl = std::make_shared<LnStochController<T>>(cfg);
3490 }
3491 int it = 0;
3492 // converged(0), converged(1), ..., converged(n), as iterate.m calls it: the last
3493 // iterate is tested (and averaged) even when the loop stops on iter_max
3494 bool done = !stoch && converged(it);
3495 while (!done && it < opt.iter_max) {
3496 if (results_raw_valid) {
3497 results.back() = results_raw_last;
3498 results_raw_valid = false;
3499 }
3500 ++it;
3501 results.emplace_back(ensemble.size());
3502 for (std::size_t e = 0; e < ensemble.size(); ++e) analyze(it, e);
3503 post(it);
3504 if (stoch) {
3505 std::vector<double> jobs(ensemble.size(), 0.0);
3506 for (std::size_t e = 0; e < ensemble.size(); ++e)
3507 jobs[e] = ensemble[e].total_jobs();
3508 const bool stop = stoch_ctl->update(it, results.back(), jobs, servt, residt);
3509 // The step this sets is the one the NEXT updateMetrics applies,
3510 // which is why it is read back here and not before the sweep.
3511 relax_omega = stoch_ctl->relax_omega();
3512 if (stop) {
3513 did_converge = true;
3514 hasconverged = true;
3515 break;
3516 }
3517 } else {
3518 done = converged(it);
3519 }
3520 }
3521 iterations_done = it;
3522 // finish(): in Robbins-Monro mode report the averaged iterate, not the
3523 // last (noisy) one.
3524 if (stoch && stoch_ctl && stoch_ctl->averaging_count() > 0) {
3525 const std::vector<LayerResult<T>>& avg = stoch_ctl->averaged_results();
3526 for (std::size_t e = 0; e < results.back().size() && e < avg.size(); ++e)
3527 results.back()[e] = avg[e];
3528 servt = stoch_ctl->averaged_servt();
3529 residt = stoch_ctl->averaged_residt();
3530 }
3531 }
3532
3533 /**
3534 * Port of the filterMetric helper inside @@NetworkSolver/getAvg.m.
3535 *
3536 * This is not cosmetic post-processing: it is where a raw solver matrix
3537 * becomes the result the rest of LINE consumes, and three of its rules
3538 * change numbers that SolverLN then iterates on.
3539 *
3540 * 1. a (station, class) pair the class never visits is zeroed, whether
3541 * because its service is disabled or because its visit ratio is zero;
3542 * 2. anything below FineTol is snapped to zero;
3543 * 3. the caller's zeroMask is applied -- and for the queue length and the
3544 * utilization that mask is `RN < 10*FineTol`, which zeroes both
3545 * wherever the response time is immediate.
3546 *
3547 * Rule 3 is the one that matters most here. Every LQN entry, task and call
3548 * class is served by an Immediate distribution somewhere, whose response
3549 * time is 1e-8, so without it a layer reports the Immediate classes' share
3550 * of the population as real queue length and the LN think-time update reads
3551 * a task utilization that MATLAB reports as zero.
3552 */
3553 Matrix<T> filter_metric(const qn::Layer<T>& L, const Matrix<T>& metric,
3554 const std::vector<std::vector<bool>>* zero_mask) const {
3555 const std::size_t M = L.nstations, K = L.nclasses;
3556 Matrix<T> out(M, K, Tzero());
3557 for (std::size_t i = 0; i < M; ++i)
3558 for (std::size_t k = 0; k < K; ++k)
3559 if (!L.disabled[i][k]) out(i, k) = metric(i, k);
3560 if (zero_mask)
3561 for (std::size_t i = 0; i < M; ++i)
3562 for (std::size_t k = 0; k < K; ++k)
3563 if ((*zero_mask)[i][k]) out(i, k) = Tzero();
3564 for (std::size_t i = 0; i < M; ++i)
3565 for (std::size_t k = 0; k < K; ++k)
3566 if (dbl(out(i, k)) < GlobalConstants::FineTol) out(i, k) = Tzero();
3567 for (std::size_t k = 0; k < K; ++k) {
3568 std::size_t c = L.nchains;
3569 for (std::size_t cc = 0; cc < L.nchains; ++cc)
3570 if (L.chains[cc][k]) c = cc;
3571 if (c == L.nchains) continue;
3572 for (std::size_t i = 0; i < M; ++i)
3573 if (L.visits[c](L.stateful_of_station(i + 1) - 1, k) == Tzero())
3574 out(i, k) = Tzero();
3575 }
3576 return out;
3577 }
3578
3579 /**
3580 * Solve one layer, running the fork-join fixed point when it has a fork.
3581 *
3582 * The driver itself is shared (fj_driver.h); the layer path supplies
3583 * solver_mva_analyzer as the inner solve. The auxiliary arrival rates
3584 * fj_lambda are warm-started across outer iterations, per the reference.
3585 *
3586 * `default` becomes `amva` on a fork layer, which is what mvaDispatch.m does
3587 * for any model whose BASE has a fork: the transformed layer is mixed with
3588 * auxiliary near-zero-rate open classes, and the exact mixed MVA the default
3589 * ladder would otherwise pick degenerates on them.
3590 */
3591 /**
3592 * Run one layer through the fluid analyzer instead of MVA.
3593 *
3594 * WHAT MAKES THIS SOUND AT ALL. The layer handed to a solver is an ordinary
3595 * closed queueing network -- the layering has already replaced every call
3596 * by a class with a service demand -- so any NetworkSolver can solve it,
3597 * and the reference says exactly that by taking a solver factory. The outer
3598 * Picard iteration only ever reads [QN, UN, RN, TN] back, so the fluid
3599 * result maps onto the same shape MVA returns.
3600 *
3601 * WHAT CHANGES, and it is not nothing: the fluid limit is exact only as the
3602 * populations grow, so on a layer of a few jobs it is a genuinely different
3603 * approximation, not a slower route to the same fixed point. It also feeds
3604 * DIFFERENT service demands back into the next outer iteration, so the two
3605 * ensembles converge to different fixed points rather than to the same one
3606 * by different paths.
3607 *
3608 * REFUSED BY NAME: a non-double backend (LSODA is double-only, see
3609 * solver_fluid.h) and a fork layer (`fj_fixed_point` supplies MVA as its
3610 * inner solve, and the auxiliary near-zero-rate open classes it introduces
3611 * are not something the fluid drift represents).
3612 */
3613 /**
3614 * Solve one layer with CTMC and report it in the shape the MVA path returns.
3615 *
3616 * Only reached for a layer carrying a Region. `qn::Layer<T>` derives from
3617 * NetworkStruct, so the layer goes to the analyzer as it stands; the
3618 * generator needs the routing matrix, which the layer does not build until
3619 * asked (see solve_layer_fluid).
3620 */
3621 mva::MvaSolution<T> solve_layer_ctmc(std::size_t e) {
3622 qn::Layer<T>& L = ensemble[e];
3623 L.refresh_rt();
3624 // The capacities the WAITQ generator reads to bound each station's
3625 // marginal are filled by solve_layer, for every engine; the routing
3626 // matrix is not, because only this path and the fluid one need it.
3627 ctmc::CtmcOptions co;
3628 // _run_any, not _run: the plain generator implements DROP only, and a
3629 // region built from an LQN admission constraint carries the WAITQ
3630 // default, whose per-region token FIFO lives in the waitq generator.
3631 const mva::AvgResult<T> a = ctmc::solver_ctmc_run_analyzer_any(L, co);
3632 mva::MvaSolution<T> out;
3633 out.method = "ctmc";
3634 out.iter = 1;
3635 out.Q = a.QN;
3636 out.U = a.UN;
3637 out.R = a.RN;
3638 out.Tp = a.TN;
3639 out.C = a.CN;
3640 out.X = a.XN;
3641 return out;
3642 }
3643
3644 /**
3645 * Solve one layer with SolverMAM's `dec.poisson`, for a setup/delay-off.
3646 *
3647 * The method matters: `dec.poisson` caps the arrival superposition at one
3648 * phase AND skips the fork-join / retrial / ldqbd diversions, so the station
3649 * reaches the setup branch of solver_mam_basic rather than being taken by a
3650 * shape-matching special case. It is what SolverLN.m:183 asks for.
3651 */
3652 mva::MvaSolution<T> solve_layer_mam(std::size_t e) {
3653 qn::Layer<T>& L = ensemble[e];
3654 L.refresh_rt();
3655 L.refresh_capacity();
3656 mam::MamOptions mo;
3657 mo.method = "dec.poisson";
3658 mva::MvaSolution<T> out = mam::mam_dispatch(L, mo).sol;
3659 out.method = "mam";
3660 return out;
3661 }
3662
3663 /**
3664 * Solve one layer with SolverNC, the reference's `LN(model, @(m) NC(m))`.
3665 *
3666 * This is the layer engine the Java CLI's bare `ln` token selects, against
3667 * `ln.mva` for the MVA one. NC evaluates the normalizing constant rather
3668 * than the MVA recursion, so on a product-form layer it is the SAME answer
3669 * reached exactly instead of through the AMVA approximation -- and on a
3670 * layer that is not product form the two differ, which is the whole reason
3671 * the token exists.
3672 */
3673 mva::MvaSolution<T> solve_layer_nc(std::size_t e) {
3674 if (fj_tr[e].active())
3675 throw UnsupportedError(
3676 "SolverLN: layer '" + ensemble[e].name +
3677 "' carries a fork, whose fixed point is driven by MVA in this port; solve this "
3678 "model with layer_solver 'mva'");
3679 qn::Layer<T>& L = ensemble[e];
3680 L.refresh_rt();
3681 // `solver_nc_solve`, NOT `nc_dispatch`: the latter is the INNER network
3682 // solve, the one the caching-queueing decomposition itself calls as its
3683 // `netfun`, and it carries no cache branch at all. The three cache gates
3684 // live in `solver_nc_solve` (solver_nc_runner.h), which is the true
3685 // counterpart of `mva_dispatch` on the branch below. Routed here, a
3686 // cache layer never reached `solver_nc_cacheqn_analyzer` and its Cache
3687 // node was solved as ordinary routing, so the hit/miss split came back
3688 // as the probability `link()` offered -- an even 1/2, independent of
3689 // capacity, item count and replacement strategy, with no warning.
3690 // `.sol` alone is taken, exactly as the MVA branch takes it: the layer
3691 // reads the split off its own station throughputs, and the auxiliary
3692 // `refreshed_struct` both dispatchers also return is unused on either.
3693 mva::MvaSolution<T> out = nc::solver_nc_solve(L, opt.layer_nc).sol;
3694 out.method = "nc";
3695 return out;
3696 }
3697
3698 /**
3699 * Solve one layer by simulation, the reference's `LN(model, @(m) SSA(m))`.
3700 *
3701 * THE LAYER RESULTS ARE THEN NOISY, and the deterministic convergence test
3702 * cannot terminate against noise: its successive-difference error is
3703 * bounded below by the standard error of the estimates. `iterate` therefore
3704 * switches to `LnStochController` whenever this engine is selected, so the
3705 * relaxation decays as Robbins-Monro and the reported iterate is the
3706 * Polyak-Ruppert average. Selecting this engine and keeping the
3707 * deterministic test would simply run to iter_max.
3708 */
3709 mva::MvaSolution<T> solve_layer_ssa(std::size_t e) {
3710 if (fj_tr[e].active())
3711 throw UnsupportedError(
3712 "SolverLN: layer '" + ensemble[e].name +
3713 "' carries a fork, whose fixed point is driven by MVA in this port; solve this "
3714 "model with layer_solver 'mva'");
3715 qn::Layer<T>& L = ensemble[e];
3716 L.refresh_rt();
3717 const ssa::SsaSolution s = ssa::solver_ssa(L, opt.layer_ssa);
3718 mva::MvaSolution<T> out;
3719 out.method = "ssa";
3720 out.iter = 1;
3721 out.Q = Matrix<T>(L.nstations, L.nclasses, Tzero());
3722 out.U = Matrix<T>(L.nstations, L.nclasses, Tzero());
3723 out.R = Matrix<T>(L.nstations, L.nclasses, Tzero());
3724 out.Tp = Matrix<T>(L.nstations, L.nclasses, Tzero());
3725 for (std::size_t i = 0; i < L.nstations; ++i)
3726 for (std::size_t r = 0; r < L.nclasses; ++r) {
3727 out.Q(i, r) = num_traits<T>::from_double(s.QN(i, r));
3728 out.U(i, r) = num_traits<T>::from_double(s.UN(i, r));
3729 out.R(i, r) = num_traits<T>::from_double(s.RN(i, r));
3730 out.Tp(i, r) = num_traits<T>::from_double(s.TN(i, r));
3731 }
3732 out.C.assign(L.nclasses, Tzero());
3733 out.X.assign(L.nclasses, Tzero());
3734 for (std::size_t r = 0; r < L.nclasses && r < s.XN.size(); ++r) {
3735 out.X[r] = num_traits<T>::from_double(s.XN[r]);
3736 out.C[r] = num_traits<T>::from_double(s.CN[r]);
3737 }
3738 return out;
3739 }
3740
3741 mva::MvaSolution<T> solve_layer_fluid(std::size_t e) {
3742 if (fj_tr[e].active())
3743 throw UnsupportedError(
3744 "SolverLN: layer '" + std::to_string(e) +
3745 "' carries a fork, whose fixed point is driven by MVA in this port; solve this "
3746 "model with layer_solver 'mva'");
3747 qn::Layer<T>& L = ensemble[e];
3748 // THE LAYER HAS NO ROUTING MATRIX UNTIL IT IS ASKED FOR ONE. `buildLayers`
3749 // records the wiring with set_route and then calls refresh_chains, which
3750 // reads P directly and computes the VISITS; MVA needs nothing else, so
3751 // `rt` stays empty. Both fluid methods route through `sn.rt` instead, and
3752 // an empty one is not an error anywhere -- it is read as "no transition
3753 // exists", the drift decays to zero and every metric comes back 0. It is
3754 // rebuilt on every solve because update_routing_probabilities can change
3755 // the wiring between outer iterations.
3756 L.refresh_rt();
3757 mva::MvaSolution<T> out;
3758 out.method = "fluid";
3759 out.iter = 1;
3760 out.Q = Matrix<T>(L.nstations, L.nclasses, Tzero());
3761 out.U = Matrix<T>(L.nstations, L.nclasses, Tzero());
3762 out.R = Matrix<T>(L.nstations, L.nclasses, Tzero());
3763 out.Tp = Matrix<T>(L.nstations, L.nclasses, Tzero());
3764 out.C.assign(L.nclasses, Tzero());
3765 out.X.assign(L.nclasses, Tzero());
3766 detail::ln_fluid_solve(L, opt.layer_fluid, out);
3767 return out;
3768 }
3769
3770 mva::MvaSolution<T> solve_layer(std::size_t e) {
3771 // BEFORE ANY ENGINE, AND FOR EVERY ONE OF THEM. `build_layer` stops at
3772 // refresh_chains, so a layer reaches this point with `cap` and
3773 // `classcap` still EMPTY, while the reference hands SolverMVA a struct
3774 // that refreshCapacity has already filled. Every consumer of Kendall's
3775 // K -- `buffer_size`, and through it `has_blocking` and the BCMP gate
3776 // `has_product_form` -- indexes those vectors by station, so an empty
3777 // one is an out-of-range read and not a permissive default. It is
3778 // recomputed on each solve because the layer's class populations move
3779 // between outer iterations, and the buffers are derived from them.
3780 ensemble[e].refresh_capacity();
3781 // A layer carrying an admission constraint goes to CTMC whatever the
3782 // layer solver is, as adaptiveSolverFactory does (SolverLN.m:1047-1089):
3783 // MVA and the fluid path both refuse a Region by name, so leaving the
3784 // layer on them would refuse a model the reference solves. The
3785 // reference then falls back to LDES and SSA; neither is reachable here
3786 // (there is no cpp LDES, and cpp SSA refuses regions), so CTMC is the
3787 // only path and its state-space bound is this port's real limit.
3788 if (!ensemble[e].regions.empty()) return solve_layer_ctmc(e);
3789 // A setup no longer forces the MAM decomposition on the layer
3790 // (SolverLN.m, 2026-08-11): the cold start is charged to the entry by
3791 // setup_charge(), not wired into the station, so the layer is an
3792 // ordinary one and the user's own layer solver serves it.
3793 // A cache layer is dispatched to the integrated caching-queueing
3794 // analyzer, which reads the NODE routing to find what lies downstream
3795 // of the cache. The layer carries none until asked, and it has to be
3796 // rebuilt every pass because entry selection can move it. EVERY layer
3797 // engine below must reach that analyzer, not just the MVA one: this
3798 // refresh runs for all of them, and a branch that then hands the
3799 // rebuilt routing to a solver with no cache gate reports the offered
3800 // hit/miss split instead of the converged one.
3801 if (has_cache_node(e)) ensemble[e].refresh_rt();
3802 if (opt.layer_solver == "fluid") return solve_layer_fluid(e);
3803 if (opt.layer_solver == "nc") return solve_layer_nc(e);
3804 if (opt.layer_solver == "ssa") return solve_layer_ssa(e);
3805 if (opt.layer_solver != "mva")
3806 throw UnsupportedError("SolverLN: layer_solver '" + opt.layer_solver +
3807 "' is not available; use 'mva', 'nc', 'fluid' or 'ssa'");
3808 // Route each layer through the full mva_dispatch ladder, as MATLAB's
3809 // SolverLN does by handing every layer to SolverMVA.runAnalyzer: a plain
3810 // closed layer still lands on branch 13 (solver_mva_analyzer), but a
3811 // layer carrying load- or class-dependent scaling now reaches
3812 // solver_mvald_analyzer, matching the reference rather than silently
3813 // running the flat AMVA.
3814 mva::MvaOptions eopt = opt.layer;
3815 if (e < layer_interlock.size()) eopt.interlock = layer_interlock[e];
3816 if (!fj_tr[e].active())
3817 return mva::mva_dispatch(ensemble[e], eopt, layer_init_sol[e]).sol;
3818 mva::MvaOptions lopt = eopt;
3819 if (lopt.method == "default") lopt.method = "amva";
3820 return mva::fj_fixed_point(
3821 ensemble[e], fj_tr[e], fj_lambda[e], eopt,
3822 [&lopt](qn::NetworkStruct<T>& V) {
3823 return mva::mva_dispatch(V, lopt, Matrix<T>()).sol;
3824 });
3825 }
3826
3827 void analyze(int it, std::size_t e) {
3828 qn::Layer<T>& L = ensemble[e];
3829 const mva::MvaSolution<T> s = solve_layer(e);
3830 LayerResult<T>& r = results[it - 1][e];
3831 r.RN = filter_metric(L, s.R, nullptr);
3832 std::vector<std::vector<bool>> zmask(L.nstations, std::vector<bool>(L.nclasses, false));
3833 for (std::size_t i = 0; i < L.nstations; ++i)
3834 for (std::size_t k = 0; k < L.nclasses; ++k)
3835 zmask[i][k] = dbl(r.RN(i, k)) < 10.0 * GlobalConstants::FineTol;
3836 r.QN = filter_metric(L, s.Q, &zmask);
3837 r.UN = filter_metric(L, s.U, &zmask);
3838 r.TN = filter_metric(L, s.Tp, nullptr);
3839 r.WN = residence_from_response(L, r.RN);
3840 // warm start the next solve of this layer from the chain-aggregated
3841 // queue lengths, as the reference does for SolverMVA layers
3842 // A fork layer is exempt: the reference guards this on the result having
3843 // the LAYER's station count, and the transformed model reports one more
3844 // row (its Source), so the hint is never installed there. A fluid layer
3845 // is exempt too, and for the reference's own reason: `analyze` guards
3846 // the warm start on `strcmp(self.solvers{e}.name, 'SolverMVA')`, because
3847 // init_sol means a queue-length vector to AMVA and a full ODE state
3848 // vector to the fluid solver -- the two are not interchangeable.
3849 if (!fj_tr[e].active() && opt.layer_solver == "mva") {
3850 Matrix<T> Qch(L.nstations, L.nchains, Tzero());
3851 for (std::size_t c = 0; c < L.nchains; ++c)
3852 for (std::size_t i = 0; i < L.nstations; ++i) {
3853 T s2 = Tzero();
3854 for (std::size_t k : L.inchain[c]) s2 += s.Q(i, k - 1);
3855 Qch(i, c) = s2;
3856 }
3857 layer_init_sol[e] = Qch;
3858 }
3859 }
3860
3861 /** Port of sn_get_residt_from_respt: response time scaled by the visit ratio. */
3862 Matrix<T> residence_from_response(const qn::Layer<T>& L, const Matrix<T>& RN) const {
3863 Matrix<T> V(L.nstations, L.nclasses, Tzero());
3864 for (std::size_t c = 0; c < L.nchains; ++c)
3865 for (std::size_t i = 0; i < L.nstations; ++i) {
3866 const std::size_t sf = L.stateful_of_station(i + 1) - 1;
3867 for (std::size_t k = 0; k < L.nclasses; ++k)
3868 V(i, k) = T(V(i, k) + L.visits[c](sf, k));
3869 }
3870 Matrix<T> WN(L.nstations, L.nclasses, Tzero());
3871 for (std::size_t i = 0; i < L.nstations; ++i)
3872 for (std::size_t k = 0; k < L.nclasses; ++k) {
3873 if (L.disabled[i][k]) continue;
3874 if (!(RN(i, k) > Tzero())) continue;
3875 if (dbl(RN(i, k)) < GlobalConstants::FineTol) {
3876 WN(i, k) = RN(i, k);
3877 continue;
3878 }
3879 std::size_t c = 0;
3880 for (std::size_t cc = 0; cc < L.nchains; ++cc)
3881 if (L.chains[cc][k]) c = cc;
3882 const std::size_t rstat = L.classes[k].refstat;
3883 T den = Tzero();
3884 if (L.refclass[c] > 0) {
3885 den = V(rstat - 1, L.refclass[c] - 1);
3886 } else {
3887 for (std::size_t kk : L.inchain[c]) den += V(rstat - 1, kk - 1);
3888 }
3889 if (den == Tzero()) continue;
3890 WN(i, k) = T(RN(i, k) * V(i, k) / den);
3891 }
3892 for (std::size_t i = 0; i < L.nstations; ++i)
3893 for (std::size_t k = 0; k < L.nclasses; ++k)
3894 if (dbl(WN(i, k)) < 10.0 * GlobalConstants::FineTol) WN(i, k) = Tzero();
3895 return WN;
3896 }
3897
3898 void post(int it) {
3899 update_metrics(it);
3900 // Under 'refpath' the merged client chain performs the interlocking
3901 // structurally, and the discount as well would deflate the same wait twice
3902 if (interlockMethod == "ilrate") update_populations(it);
3903 update_think_times(it);
3904 update_layers(it);
3905 update_routing_probabilities(it);
3906 for (std::size_t e : route_reset) {
3907 ensemble[e].refresh_chains();
3908 layer_init_sol[e] = Matrix<T>();
3909 }
3910 // moment3 needs no refreshProcesses here: this port keeps the service
3911 // DISTRIBUTION on the layer and refresh_rates re-reads it, so there is no
3912 // second representation to fall out of step, unlike sn.proc in MATLAB.
3913 for (std::size_t e : svc_reset) ensemble[e].refresh_rates();
3914 }
3915
3916 /** Port of converged.m: moving average of the layer results plus the test. */
3917 bool converged(int it) {
3918 const std::size_t E = ensemble.size();
3919 const int iter_min = std::max<int>(2 * int(E), int(std::ceil(opt.iter_max / 4.0)));
3920 const int wnd_size = std::max(5, int(std::ceil(iter_min / 5.0)));
3921
3922 if (it >= iter_min && int(results.size()) >= wnd_size) {
3923 results_raw_last = results.back();
3924 results_raw_valid = true;
3925 const T w = T(Tone() / num_traits<T>::from_int(wnd_size));
3926 for (std::size_t e = 0; e < E; ++e) {
3927 LayerResult<T>& cur = results[results.size() - 1][e];
3928 auto scale = [&](Matrix<T>& A) {
3929 for (std::size_t i = 0; i < A.rows(); ++i)
3930 for (std::size_t j = 0; j < A.cols(); ++j) A(i, j) = T(A(i, j) * w);
3931 };
3932 Matrix<T> Q = cur.QN, U = cur.UN, R = cur.RN, Tp = cur.TN, W = cur.WN;
3933 scale(Q); scale(U); scale(R); scale(Tp); scale(W);
3934 for (int k = 1; k < wnd_size; ++k) {
3935 const LayerResult<T>& old = results[results.size() - 1 - k][e];
3936 auto add = [&](Matrix<T>& A, const Matrix<T>& B) {
3937 for (std::size_t i = 0; i < A.rows(); ++i)
3938 for (std::size_t j = 0; j < A.cols(); ++j) A(i, j) = T(A(i, j) + B(i, j) * w);
3939 };
3940 add(Q, old.QN); add(U, old.UN); add(R, old.RN); add(Tp, old.TN); add(W, old.WN);
3941 }
3942 cur.QN = Q; cur.UN = U; cur.RN = R; cur.TN = Tp; cur.WN = W;
3943 }
3944 }
3945
3946 if (it > 1) {
3947 double err = 0.0;
3948 for (std::size_t e = 0; e < E; ++e) {
3949 const Matrix<T>& Q = results[results.size() - 1][e].QN;
3950 const Matrix<T>& Q1 = results[results.size() - 2][e].QN;
3951 const double Njobs = ensemble[e].total_jobs();
3952 if (!(Njobs > 0.0)) continue;
3953 double mx = 0.0;
3954 for (std::size_t i = 0; i < Q.rows(); ++i)
3955 for (std::size_t j = 0; j < Q.cols(); ++j)
3956 mx = std::max(mx, std::fabs(dbl(Q(i, j)) - dbl(Q1(i, j))));
3957 err += mx / Njobs;
3958 }
3959 maxitererr[it] = err;
3961 static_cast<long>(it),
3962 "layer iteration %zu: max queue-length change %.3e (tolerance %.3e)",
3963 static_cast<std::size_t>(it), err, opt.iter_tol);
3964 if (it == iter_min) {
3965 line::util::LineConsole::step("started averaging the iterates to aid convergence");
3966 averagingstart = it;
3967 }
3968 }
3969
3970 if (it > iter_min && maxitererr[it] < opt.iter_tol && maxitererr[it - 1] < opt.iter_tol &&
3971 maxitererr[it - 2] < opt.iter_tol) {
3972 if (!hasconverged) {
3973 hasconverged = true;
3974 } else {
3975 did_converge = true;
3976 return true;
3977 }
3978 } else {
3979 hasconverged = false;
3980 }
3981 return false;
3982 }
3983
3984 // -----------------------------------------------------------------------
3985 // updateMetrics
3986 // -----------------------------------------------------------------------
3987 /** Port of updateMetrics.m: the method selects which update runs. */
3988 // -----------------------------------------------------------------------
3989 // Method "srvn.ph": the activity graph of an entry as a phase-type server law
3990 //
3991 // Each layer is a two-station cycle, a client Delay plus the server, with
3992 // one closed class per caller task. The sequencing the default method
3993 // encodes as routing -- a class per entry, per activity and per call, plus
3994 // Fork, Join, Router and ClassSwitch nodes -- is composed instead into a
3995 // single phase-type service law per (layer, caller), by the exact
3996 // series-parallel reduction of Workflow. The layer therefore carries only
3997 // the client/server back-and-forth, and the activity graph survives as a
3998 // distribution.
3999 //
4000 // Port of the MATLAB @SolverLN/buildLayersPH.m, updateLayersPH.m,
4001 // updateMetricsPH.m, updateThinkTimesPH.m, phComposeEntryLaws.m and
4002 // getEnsembleAvgPH.m, of the JAR jline.solvers.ln.SolverLNPH and of the
4003 // Python line_solver.solvers.solver_ln.solver_ln_ph. See
4004 // _kb/06-solver-catalog.md (LN section) for the layering taxonomy.
4005 // -----------------------------------------------------------------------
4006
4007 /**
4008 * The method the layers were actually BUILT for: "srvn.ph", "srvn.cs",
4009 * "flat.cs", "flat.ph" or "moment3". Resolved once in build_layers, because
4010 * the alias "srvn" may fall back; every dispatch reads this and not
4011 * opt.method, so a reconstruction can never disagree with the layers it is
4012 * reading.
4013 */
4014 std::string lnmethod;
4015 /** True once ph_init_laws has composed the per-entry workflows. */
4016 bool ph_laws_ready = false;
4017
4018 /**
4019 * Normalise a method name onto one the solver dispatches on. A method name
4020 * carries TWO decisions: the LAYERING, which fixes what a submodel is, and
4021 * the ENCODING, which fixes how an activity graph is written into it.
4022 *
4023 * "srvn.cs" encodes the activity graph as ROUTING, "srvn.ph" as a composed
4024 * phase-type server law, "srvn" is the alias that takes "srvn.ph" where it
4025 * can serve the model and "srvn.cs" otherwise, "flat.cs" squashes every
4026 * server into one submodel with the routing encoding, "flat.ph" squashes
4027 * them with the composed one ("flat" is the alias of "flat.cs" and resolves
4028 * unconditionally rather than probing "flat.ph", because a model is squashed
4029 * in order to express what only the routing encoding carries), and "moment3"
4030 * is the three-moment distribution pass over the routing layers. "default"
4031 * is the srvn alias; an unrecognised method name takes "srvn.cs".
4032 */
4033 static std::string ln_requested_method(const std::string& method) {
4034 std::string m;
4035 for (char c : method) m += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
4036 if (m.empty() || m == "srvn" || m == "default" || m == "auto") return "srvn";
4037 if (m == "srvn.ph" || m == "ph") return "srvn.ph";
4038 if (m == "srvn.cs" || m == "srvncs" || m == "cs") return "srvn.cs";
4039 if (m == "flat.cs" || m == "flatcs" || m == "flat" || m == "squashed") return "flat.cs";
4040 if (m == "flat.ph" || m == "flatph" || m == "squashed.ph") return "flat.ph";
4041 if (m == "moment3") return "moment3";
4042 // An unrecognised token takes the routing encoding, which is what every
4043 // name other than "moment3" resolved to before the alias existed.
4044 return "srvn.cs";
4045 }
4046
4047 /** True when the layers are the collapsed phase-type ones of "srvn.ph". */
4048 bool is_srvn_ph() const { return lnmethod == "srvn.ph"; }
4049
4050 /**
4051 * True when the layers carry the COMPOSED phase-type server law rather than
4052 * the routing encoding of the activity graph, under either layering. The
4053 * encoding, not the layering, decides which update and reconstruction passes
4054 * run, so every such dispatch asks this and not for one method name.
4055 */
4056 bool is_ph_encoding() const { return lnmethod == "srvn.ph" || lnmethod == "flat.ph"; }
4057
4058 /**
4059 * Station of layer L that stands for LQN element ELEM, falling back to the
4060 * layer's own server when ELEM is not a server there. Under "srvn" the
4061 * fallback is the answer for every element; under "flat.cs" it is the map
4062 * that tells the many servers of one layer apart.
4063 */
4064 static std::size_t station_idx_of(const qn::Layer<T>& L, std::size_t elem) {
4065 if (elem >= 1 && elem < L.server_idx_of.size() && L.server_idx_of[elem] != 0)
4066 return L.server_idx_of[elem];
4067 return L.serverIdx;
4068 }
4069
4070 /**
4071 * Station of layer L that class K (0-based) is served at: the PROCESSOR of
4072 * an activity, the CALLED TASK of a call, the layer's own server otherwise.
4073 */
4074 std::size_t station_idx_of_class(const qn::Layer<T>& L, std::size_t k) const {
4075 const int kind = L.classes[k].attr_kind;
4076 const std::size_t a = L.classes[k].attr_idx;
4077 if (kind == int(LqnElement::ACTIVITY))
4078 return station_idx_of(L, lqn.parent[lqn.parent[a]]);
4079 if (kind == int(LqnElement::CALL))
4080 return station_idx_of(L, lqn.parent[lqn.callpair_dst[a]]);
4081 return L.serverIdx;
4082 }
4083
4084 /**
4085 * Answer whether "srvn.ph" can serve this model, without disturbing the
4086 * solver.
4087 *
4088 * Both the feature gate and the series-parallel reduction can refuse, and
4089 * the second only finds out by composing the per-entry workflows -- work the
4090 * build then reuses, since those laws do not depend on the iterate.
4091 */
4092 bool probe_srvn_ph() {
4093 try {
4094 ph_assert_supported();
4095 ph_init_laws();
4096 ph_laws_ready = true;
4097 return true;
4098 } catch (const std::exception&) {
4099 ph_laws_ready = false;
4100 return false;
4101 }
4102 }
4103
4104 /** One two-station layer, and the caller classes that cycle through it. */
4105 struct PHLayer {
4106 std::size_t idx = 0;
4107 bool ishost = false;
4108 std::vector<std::size_t> callers;
4109 /** 1-based class index of each caller task, 0 when absent. */
4110 std::vector<std::size_t> class_of_caller;
4111 std::size_t nreplicas = 1;
4112 /** 1-based station indices of the server replicas. */
4113 std::vector<std::size_t> qstations;
4114 /** Mean of the law each class is currently served with, by class index. */
4115 std::vector<T> svcmean_by_class;
4116 /** (class index, entry index) or (class index, -call index). */
4117 std::vector<std::pair<std::size_t, long>> open_arrivals;
4118 /**
4119 * Closed population of the MODEL this server sits in. Under "flat.ph"
4120 * that is every caller of the single network, not only the callers of
4121 * this one station, so it is recorded here rather than recomputed.
4122 */
4123 double npop = 0.0;
4124 };
4125
4126 // per-entry workflows and their composed laws
4127 std::vector<workflow::Workflow<T>> ph_wf, ph_wfhost;
4128 std::vector<std::unordered_map<std::size_t, T>> ph_execs;
4129 std::vector<bool> ph_has_wf;
4130 std::vector<workflow::PhLaw<T>> ph_hostlaw, ph_entrylaw;
4131 std::vector<T> ph_hostmean, ph_entrymean, ph_entryscv;
4132 std::vector<T> ph_share, ph_overlap, ph_setupshare, ph_xdemand;
4133 Matrix<T> ph_ncalls, ph_calltime;
4134 std::vector<T> ph_procresid, ph_actthinkt, ph_calltotal;
4135 std::vector<PHLayer> ph_layers; ///< by element index, empty where absent
4136 std::vector<bool> ph_has_layer;
4137
4138 /** Mean of an activity's own think time, 0 when it declares none. */
4139 T act_think_time(std::size_t aidx) const {
4140 if (aidx >= lqn.actthink.size() || lqn.actthink[aidx].disabled) return Tzero();
4141 const double m = dbl(lqn.actthink[aidx].mean);
4142 if (!std::isfinite(m) || m <= GlobalConstants::FineTol) return Tzero();
4143 return lqn.actthink[aidx].mean;
4144 }
4145
4146 /**
4147 * Mean cold start one request of task TIDX pays, 0 when it declares none.
4148 *
4149 * A SetupTask powers a thread down when it goes idle and pays a setup before
4150 * it can serve again. The thread is released at a reply and starts a
4151 * delay-off countdown D of mean d; it powers off only if D expires before
4152 * the next request arrives, and a request arriving first cancels the
4153 * countdown and pays nothing. With the idle interval I seen by one thread
4154 * and exponential D, p = P(D < I) = E[I]/(E[I]+d) and the charge is p*s.
4155 *
4156 * Admission takes an ACTIVE idle thread before it wakes a sleeping one, so
4157 * the pool that actually cycles is only as large as the load needs: with
4158 * offered load b = X*S = rho*mult threads, about max(1,b) stay hot, giving
4159 * E[I] = (max(1,b) - b)/X. Exact at mult = 1; above it the exact answer is
4160 * matrix-analytic (Gandhi, Harchol-Balter and Adan, Performance Evaluation
4161 * 67(11), 2010). Twin of MATLAB lqn_setup_charge.m, the JAR
4162 * SolverLN.setupCharge and the Python SolverLN._setup_charge.
4163 */
4164 double setup_charge(std::size_t tidx) const {
4165 if (tidx >= lqn.hassetup.size() || !lqn.hassetup[tidx]) return 0.0;
4166 const double s = tidx < lqn.setuptime.size() && !lqn.setuptime[tidx].disabled
4167 ? dbl(lqn.setuptime[tidx].mean) : 0.0;
4168 const double d = tidx < lqn.delayofftime.size() && !lqn.delayofftime[tidx].disabled
4169 ? dbl(lqn.delayofftime[tidx].mean) : 0.0;
4170 if (!(s > GlobalConstants::FineTol) || !(d > GlobalConstants::FineTol)) return 0.0;
4171 const double mult = lqn.mult[tidx];
4172 if (!std::isfinite(mult) || mult <= 0.0) return 0.0;
4173 if (tidx >= tput.size() || tidx >= util.size()) return s;
4174 const double X = dbl(tput[tidx]);
4175 if (!std::isfinite(X) || X <= GlobalConstants::FineTol) return s;
4176 double rho = dbl(util[tidx]);
4177 if (!std::isfinite(rho) || rho < 0.0) rho = 0.0;
4178 rho = std::min(rho, 1.0 - GlobalConstants::FineTol);
4179 const double b = rho * mult; // offered load, in threads
4180 const double EI = (std::max(1.0, b) - b) / X; // idle interval of a hot thread
4181 return s * EI / (EI + d);
4182 }
4183
4184 /**
4185 * Probability that a request for entry EIDX finds its task's thread off.
4186 * ONE closure for both methods: setup_charge returns p*s, so p is that over
4187 * s. It also answers p = 1 during construction, before the first solve has
4188 * sized tput or util.
4189 */
4190 double ph_setup_prob(std::size_t eidx) const {
4191 const std::size_t tidx = lqn.parent[eidx];
4192 if (tidx >= lqn.hassetup.size() || !lqn.hassetup[tidx]) return 0.0;
4193 const double s = tidx < lqn.setuptime.size() && !lqn.setuptime[tidx].disabled
4194 ? dbl(lqn.setuptime[tidx].mean) : 0.0;
4195 const double d = tidx < lqn.delayofftime.size() && !lqn.delayofftime[tidx].disabled
4196 ? dbl(lqn.delayofftime[tidx].mean) : 0.0;
4197 if (!(s > GlobalConstants::FineTol) || !(d > GlobalConstants::FineTol)) return 0.0;
4198 return std::min(1.0, std::max(0.0, setup_charge(tidx) / s));
4199 }
4200
4201 /** Divisor that scales a processor utilization into [0,1]. */
4202 double ph_host_servers(std::size_t hidx) const {
4203 if (lqn.sched[hidx] == SchedStrategy::INF) return 1.0;
4204 const double m = lqn.maxmult[hidx];
4205 return (std::isfinite(m) && m > 0.0) ? m : 1.0;
4206 }
4207
4208 /** True when any task calls entry EIDX, synchronously or not. */
4209 bool ph_any_caller_of(std::size_t eidx) const {
4210 return lqn.issynccaller.any_col(eidx) || lqn.isasynccaller.any_col(eidx);
4211 }
4212
4213 /**
4214 * True when an entry arrival is the ONLY way requests reach task TIDX.
4215 * "srvn.ph" refuses forwarding calls outright, so sync/async callers are the
4216 * whole test.
4217 */
4218 bool ph_open_arrival_only(std::size_t tidx) const {
4219 if (lqn.isref[tidx]) return false;
4220 for (std::size_t eidx : lqn.entriesof[tidx])
4221 if (ph_any_caller_of(eidx)) return false;
4222 for (std::size_t eidx : lqn.entriesof[tidx])
4223 if (lqn.has_arrival[eidx]) return true;
4224 return false;
4225 }
4226
4227 /** Asynchronous calls whose target entry belongs to TIDX. */
4228 std::vector<std::size_t> ph_async_calls_into(std::size_t tidx) const {
4229 std::vector<std::size_t> out;
4230 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
4231 if (lqn.calltype[cidx] != CallType::ASYNC) continue;
4232 for (std::size_t e : lqn.entriesof[tidx])
4233 if (lqn.callpair_dst[cidx] == e) { out.push_back(cidx); break; }
4234 }
4235 return out;
4236 }
4237
4238 /**
4239 * Features the collapsed layer cannot represent are refused by name rather
4240 * than silently degraded.
4241 */
4242 void ph_assert_supported(bool flat = false) const {
4243 // The list is a property of the ENCODING, so it is the same under either
4244 // layering; what the squashing adds on top is refused in ph_flat_server_set.
4245 const std::string mname = flat ? "flat.ph" : "srvn.ph";
4246 // PHASE 2 IS ASKED HERE AND NOWHERE ELSE. `has_phase2` is built during
4247 // layering rather than being a property of the model, so the report twin
4248 // below cannot ask it; every other rule is shared with it.
4249 if (has_phase2)
4250 throw UnsupportedError(
4251 "method='" + mname + "' does not support second-phase activities: the composed "
4252 "entry law has no reply point. Use method='default'.");
4253 // ONE RULE LIST, TWO CALLERS: this throws what `ph_method_refusal`
4254 // returns, in the same order, so the run and a report cannot say
4255 // different things about one model. The squashing rules it also carries
4256 // for `flat.ph` are the ones `ph_flat_server_set` raises, which runs
4257 // before this on that path, so the message a caller sees is unchanged.
4258 const std::string why = ph_method_refusal(mname);
4259 if (!why.empty()) throw UnsupportedError(why);
4260 }
4261
4262 /**
4263 * The same rules as a SENTENCE, so a report can withdraw a method it cannot run.
4264 *
4265 * ONE RULE LIST, TWO CALLERS. `ph_assert_supported` above answers the run
4266 * path by throwing; nothing answered the report, and `list_valid_methods`
4267 * returns the same eight names for every model, so every layered model was
4268 * offered every encoding and `srvn.ph`/`flat.ph` then threw on contact.
4269 *
4270 * PHASE 2 IS DELIBERATELY ABSENT. `has_phase2` is built during layering
4271 * rather than being a property of the model, so a gate cannot ask it without
4272 * doing the layering it is meant to precede; the run path still refuses it.
4273 *
4274 * @param method the concrete method name
4275 * @return empty string when the method can encode this model, else the reason
4276 */
4277 std::string ph_method_refusal(const std::string& method) const {
4278 if (method != "srvn.ph" && method != "flat.ph") return std::string();
4279 // The squashing refusals, `flat.ph` only: each carries PER-LAYER state
4280 // that one submodel cannot hold (ph_flat_server_set).
4281 if (method == "flat.ph") {
4282 for (std::size_t i = 1; i <= NT(); ++i) {
4283 if (lqn.repl[i] > 1.0)
4284 return "method='flat.ph' does not support replicated processors or tasks, "
4285 "whose replicas need a submodel each. Use method='srvn.ph'.";
4286 if (lqn.hassetup[i])
4287 return "method='flat.ph' does not support setup tasks, whose powered-down "
4288 "threads are per-layer state. Use method='srvn.ph'.";
4289 }
4290 }
4291 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx)
4292 if (lqn.calltype[cidx] == CallType::FWD)
4293 return "method='" + method +
4294 "' does not support forwarding calls, whose target is not part of the "
4295 "caller's activity graph. Use method='default'.";
4296 for (std::size_t i = 1; i < lqn.iscache.size(); ++i)
4297 if (lqn.iscache[i])
4298 return "method='" + method +
4299 "' does not support cache tasks. Use method='default'.";
4300 for (std::size_t i = 1; i < lqn.hassetup.size(); ++i) {
4301 if (!lqn.hassetup[i]) continue;
4302 if (lqn.sched[i] == SchedStrategy::INF || !std::isfinite(lqn.mult[i]))
4303 return "method='" + method + "': task '" + lqn.names[i] +
4304 "' declares a setup time on an infinite-server task, which holds no "
4305 "thread to power down; give it a finite multiplicity.";
4306 }
4307 for (std::size_t i = 0; i < lqn.lincon_A.size(); ++i)
4308 if (lqn.lincon_A[i].rows() > 0)
4309 return "method='" + method +
4310 "' does not support admission constraints on a layer station. "
4311 "Use method='default'.";
4312 for (std::size_t i = 1; i < lqn.lldscaling.size(); ++i) {
4313 const char* fname = nullptr;
4314 if (!lqn.lldscaling[i].empty()) fname = "a load dependence";
4315 else if (lqn.cdscaling[i]) fname = "a class dependence";
4316 else if (lqn.jdscaling[i]) fname = "a joint dependence";
4317 else if (!lqn.pools[i].empty()) fname = "server pools";
4318 if (fname != nullptr)
4319 return "method='" + method +
4320 "' does not support queue-dependent service rates on a layer station ('" +
4321 lqn.names[i] + "' declares " + fname + "). Use method='srvn.cs'.";
4322 }
4323 return std::string();
4324 }
4325
4326 /** Build the per-entry workflows and the iteration-invariant processor law. */
4327 void ph_init_laws() {
4328 const std::size_t N = lqn.nidx;
4329 ph_wf.assign(N + 1, workflow::Workflow<T>("empty"));
4330 ph_wfhost.assign(N + 1, workflow::Workflow<T>("empty"));
4331 ph_execs.assign(N + 1, {});
4332 ph_has_wf.assign(N + 1, false);
4333 ph_hostlaw.assign(N + 1, workflow::PhLaw<T>());
4334 ph_entrylaw.assign(N + 1, workflow::PhLaw<T>());
4335 ph_hostmean.assign(N + 1, Tzero());
4336 ph_entrymean.assign(N + 1, Tzero());
4337 ph_entryscv.assign(N + 1, Tone());
4338 ph_share.assign(N + 1, Tzero());
4339 ph_overlap.assign(N + 1, Tone());
4340 ph_setupshare.assign(N + 1, Tzero());
4341 ph_xdemand.assign(N + 1, Tzero());
4342 ph_ncalls = Matrix<T>(N + 1, N + 1, Tzero());
4343 ph_calltime = Matrix<T>(N + 1, N + 1, Tzero());
4344 ph_procresid.assign(N + 1, Tzero());
4345 ph_actthinkt.assign(N + 1, Tzero());
4346 ph_calltotal.assign(N + 1, Tzero());
4347 ph_layers.assign(NT() + 1, PHLayer());
4348 ph_has_layer.assign(NT() + 1, false);
4349
4350 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
4351 const std::size_t eidx = lqn.eshift + e;
4352 const std::size_t tidx = lqn.parent[eidx];
4353 if (ignore[tidx]) continue;
4354 api::lqn::EntryWorkflow<T> ew = api::lqn::entry_workflow(lqn, eidx, true);
4355 ph_wf[eidx] = std::move(ew.wf);
4356 ph_execs[eidx] = ew.execs;
4357 ph_has_wf[eidx] = true;
4358 api::lqn::EntryWorkflow<T> eh = api::lqn::entry_workflow(lqn, eidx, false);
4359 ph_wfhost[eidx] = std::move(eh.wf);
4360 // the processor sees the WORK of concurrent branches, not their
4361 // elapsed time, so the host law serialises an AND fork
4362 ph_hostlaw[eidx] = api::lqn::serial_law(ph_wfhost[eidx]);
4363 ph_hostmean[eidx] =
4364 api::lqn::ph_moments(ph_hostlaw[eidx].alpha, ph_hostlaw[eidx].S).first;
4365 }
4366
4367 // until the first iteration reports throughputs, a task splits its
4368 // requests evenly over its entries
4369 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
4370 const std::size_t tidx = lqn.tshift + t;
4371 const std::size_t n = lqn.entriesof[tidx].size();
4372 if (n == 0) continue;
4373 for (std::size_t eidx : lqn.entriesof[tidx])
4374 ph_share[eidx] = num_traits<T>::from_double(1.0 / double(n));
4375 }
4376 }
4377
4378 /**
4379 * Law of the total time one execution of the issuing activity spends in call
4380 * CIDX: the geometric compound, of mean callproc_mean, of the response law
4381 * of the called entry. The response law is fitted to the response time
4382 * reported by the callee's layer and to the SCV of the callee's own composed
4383 * law, so no extra solver output is needed.
4384 */
4385 Distrib<T> ph_call_burst_law(std::size_t cidx) const {
4386 const double m = dbl(lqn.callproc_mean[cidx]);
4387 const std::size_t eidx = lqn.callpair_dst[cidx];
4389 const T R = T(callservt[cidx] / lqn.callproc_mean[cidx]);
4390 double scvd = dbl(ph_entryscv[eidx]);
4391 if (!std::isfinite(scvd) || scvd <= GlobalConstants::FineTol) scvd = 1.0;
4392 const T Rf = dbl(R) > GlobalConstants::FineTol
4393 ? R : num_traits<T>::from_double(GlobalConstants::FineTol);
4394 const Distrib<T> base = lang::aph_fit_mean_scv(Rf, num_traits<T>::from_double(scvd));
4395 const workflow::PhLaw<T> body = api::lqn::ph_law_of(base);
4396 const workflow::PhLaw<T> loop =
4397 workflow::Workflow<T>::compose_loop_geometric(body, lqn.callproc_mean[cidx]);
4398 return Distrib<T>::phase_type(loop.alpha, loop.S,
4400 }
4401
4402 /**
4403 * Station law of a composed workflow. A geometric loop over a body of two or
4404 * more phases closes a cycle in the phase graph, and a cyclic generator is a
4405 * PH and not an APH: no layer solver declares PH, so such a law is reduced
4406 * to the APH with the SAME first two moments. AMVA and NC read exactly those
4407 * two, so the reduction is lossless for them and is a two-moment fit for the
4408 * phase-aware layer solvers.
4409 */
4410 Distrib<T> ph_station_law(const workflow::PhLaw<T>& law) const {
4412 return Distrib<T>::phase_type(law.alpha, law.S, true);
4413 const std::pair<T, T> mm = api::lqn::ph_moments(law.alpha, law.S);
4414 return lang::aph_fit_mean_scv(mm.first, mm.second);
4415 }
4416
4417 /** The law of a single immediate phase, the empty-composition answer. */
4418 static workflow::PhLaw<T> ph_immediate_law() {
4419 workflow::PhLaw<T> out;
4420 out.alpha.assign(1, Tone());
4421 out.S = Matrix<T>(1, 1, num_traits<T>::from_double(-GlobalConstants::Immediate));
4422 return out;
4423 }
4424
4425 /**
4426 * Recompose the entry service laws from the current fixed-point iterate.
4427 *
4428 * The composed mean is NOT the sum of the leaf means when the graph forks:
4429 * the branches of an AND fork overlap, and the entry finishes with the last
4430 * of them. The ratio of the two, the overlap factor, is what the caller-side
4431 * aggregates are scaled by, so that the pieces of a cycle still add up to
4432 * the cycle.
4433 */
4434 void ph_compose_entry_laws() {
4435 const std::size_t N = lqn.nidx;
4436 std::vector<T> entry_setup_share(N + 1, Tzero());
4437 ph_overlap.assign(N + 1, Tone());
4438
4439 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
4440 const std::size_t eidx = lqn.eshift + e;
4441 if (!ph_has_wf[eidx]) continue;
4442 workflow::Workflow<T>& w = ph_wf[eidx];
4443 const std::unordered_map<std::size_t, T>& ex = ph_execs[eidx];
4444 T entrysum = Tzero(), procsum = Tzero();
4445 for (std::size_t aidx : lqn.actsof[eidx]) {
4446 T m = T(residt[aidx] + act_think_time(aidx));
4447 const T xa = ex.at(aidx);
4448 procsum += T(xa * m);
4449 const double md = dbl(m);
4450 w.set_activity_demand_mean(
4451 lqn.names[aidx],
4453 ? m : num_traits<T>::from_double(GlobalConstants::FineTol));
4454 for (std::size_t cidx : lqn.callsof[aidx]) {
4455 if (lqn.calltype[cidx] != CallType::SYNC) continue;
4456 w.set_activity_demand(lqn.callhashnames[cidx], ph_call_burst_law(cidx));
4457 m = T(m + callservt[cidx]);
4458 }
4459 entrysum += T(xa * m);
4460 }
4461 workflow::PhLaw<T> law = api::lqn::ph_law_of(w.refresh_ph());
4462 std::pair<T, T> mm = api::lqn::ph_moments(law.alpha, law.S);
4463 T m1 = mm.first, scv = mm.second;
4464 // All activities of an entry run on ONE processor, so the branches of
4465 // an AND fork cannot overlap the processor residence they request:
4466 // the composed maximum is a lower bound on the entry service time
4467 // only above that total. Where it falls below, the law is rescaled in
4468 // time to it, which keeps its shape, its SCV and its order.
4469 if (dbl(procsum) > dbl(m1) + GlobalConstants::FineTol) {
4470 const T f = T(m1 / procsum);
4471 for (std::size_t i = 0; i < law.S.rows(); ++i)
4472 for (std::size_t j = 0; j < law.S.cols(); ++j) law.S(i, j) = T(law.S(i, j) * f);
4473 m1 = procsum;
4474 }
4475 // A SetupTask powers a thread down when it goes idle, so a request may
4476 // find it off and pay a cold start before the entry runs at all. The
4477 // setup is not part of the activity graph and never enters the
4478 // series-parallel reduction: it is prefixed to the composed law
4479 // afterwards as the mixture p*(setup THEN entry) + (1-p)*entry, which
4480 // is again phase-type.
4481 const double p = ph_setup_prob(eidx);
4482 if (p > GlobalConstants::FineTol) {
4483 const std::size_t tidx = lqn.parent[eidx];
4484 const double sm = dbl(lqn.setuptime[tidx].mean);
4485 double sscv = dbl(lqn.setuptime[tidx].scv);
4486 if (!std::isfinite(sscv) || sscv <= GlobalConstants::FineTol) sscv = 1.0;
4487 if (std::isfinite(sm) && sm > GlobalConstants::FineTol) {
4488 const workflow::PhLaw<T> sl = api::lqn::ph_law_of(lang::aph_fit_mean_scv(
4489 num_traits<T>::from_double(sm), num_traits<T>::from_double(sscv)));
4490 std::vector<workflow::PhLaw<T>> mix;
4491 mix.push_back(workflow::Workflow<T>::compose_serial(sl, law));
4492 mix.push_back(law);
4493 std::vector<T> probs;
4494 probs.push_back(num_traits<T>::from_double(p));
4495 probs.push_back(num_traits<T>::from_double(1.0 - p));
4497 mm = api::lqn::ph_moments(law.alpha, law.S);
4498 m1 = mm.first;
4499 scv = mm.second;
4500 // The share of the entry law that is cold start and not work.
4501 // The surrogate-delay closure measures a thread's cycle in
4502 // WORK, so it must not read a station utilization this has
4503 // inflated -- see update_think_times_ph.
4504 const double denom = std::max(dbl(m1), GlobalConstants::FineTol);
4505 entry_setup_share[eidx] = num_traits<T>::from_double(p * sm / denom);
4506 }
4507 }
4508 ph_entrylaw[eidx] = law;
4509 ph_entrymean[eidx] = m1;
4510 ph_entryscv[eidx] = scv;
4511 if (dbl(entrysum) > GlobalConstants::FineTol) {
4512 const double r = std::min(1.0, dbl(m1) / dbl(entrysum));
4513 ph_overlap[eidx] = num_traits<T>::from_double(r);
4514 }
4515 }
4516
4517 // Per task, the share-weighted fraction of its station service that is
4518 // cold start rather than work.
4519 ph_setupshare.assign(N + 1, Tzero());
4520 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
4521 const std::size_t tidx = lqn.tshift + t;
4522 if (ignore[tidx]) continue;
4523 for (std::size_t eidx : lqn.entriesof[tidx])
4524 ph_setupshare[tidx] = T(ph_setupshare[tidx] + ph_share[eidx] * entry_setup_share[eidx]);
4525 }
4526
4527 // Expected number of calls per invocation, and the caller-side aggregates
4528 ph_ncalls = Matrix<T>(N + 1, N + 1, Tzero());
4529 ph_calltime = Matrix<T>(N + 1, N + 1, Tzero());
4530 ph_procresid.assign(N + 1, Tzero());
4531 ph_actthinkt.assign(N + 1, Tzero());
4532 ph_calltotal.assign(N + 1, Tzero());
4533 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
4534 const std::size_t tidx = lqn.tshift + t;
4535 if (ignore[tidx]) continue;
4536 for (std::size_t eidx : lqn.entriesof[tidx]) {
4537 if (!ph_has_wf[eidx]) continue;
4538 const T w = ph_share[eidx];
4539 if (!(dbl(w) > 0.0)) continue;
4540 const std::unordered_map<std::size_t, T>& ex = ph_execs[eidx];
4541 const T r = ph_overlap[eidx];
4542 for (std::size_t aidx : lqn.actsof[eidx]) {
4543 const T xa = ex.at(aidx);
4544 ph_procresid[tidx] = T(ph_procresid[tidx] + w * r * xa * residt[aidx]);
4545 ph_actthinkt[tidx] = T(ph_actthinkt[tidx] + w * r * xa * act_think_time(aidx));
4546 for (std::size_t cidx : lqn.callsof[aidx]) {
4547 if (lqn.calltype[cidx] != CallType::SYNC) continue;
4548 const std::size_t tgte = lqn.callpair_dst[cidx];
4549 const std::size_t tgtt = lqn.parent[tgte];
4550 // the COUNT of calls does not change with the overlap,
4551 // only the time the caller is held by them
4552 ph_ncalls(tidx, tgte) =
4553 T(ph_ncalls(tidx, tgte) + w * xa * lqn.callproc_mean[cidx]);
4554 ph_calltime(tidx, tgtt) =
4555 T(ph_calltime(tidx, tgtt) + w * r * xa * callservt[cidx]);
4556 ph_calltotal[tidx] = T(ph_calltotal[tidx] + w * r * xa * callservt[cidx]);
4557 }
4558 }
4559 }
4560 }
4561 }
4562
4563 /** Build the ensemble under method "srvn.ph". */
4564 void build_layers_ph(bool flat = false) {
4565 if (!ph_laws_ready) ph_assert_supported(flat);
4566 // The interlock correction rewrites the populations of the call classes,
4567 // which this method does not create: its callers reach the server in one
4568 // class each
4569 opt.interlocking = false;
4570 interlockMethod = "none";
4571
4572 // A preceding probe has already composed the per-entry workflows; they do
4573 // not depend on the iterate, so they are not rebuilt here.
4574 if (!ph_laws_ready) ph_init_laws();
4575
4576 // Seed the fixed point with the static demands, then compose the laws
4577 const std::size_t N = lqn.nidx;
4578 residt.assign(N + 1, Tzero());
4579 servt.assign(N + 1, Tzero());
4580 callservt.assign(lqn.ncalls + 1, Tzero());
4581 callresidt.assign(lqn.ncalls + 1, Tzero());
4582 for (std::size_t aidx = lqn.ashift + 1; aidx <= lqn.ashift + lqn.nacts; ++aidx)
4583 residt[aidx] = lqn.hostdem[aidx].disabled ? Tzero() : lqn.hostdem[aidx].mean;
4584 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
4585 if (lqn.calltype[cidx] != CallType::SYNC && lqn.calltype[cidx] != CallType::ASYNC)
4586 continue;
4587 const std::size_t eidx = lqn.callpair_dst[cidx];
4588 callservt[cidx] = T(lqn.callproc_mean[cidx] * ph_hostmean[eidx]);
4589 callresidt[cidx] = callservt[cidx];
4590 }
4591 ph_compose_entry_laws();
4592
4593 if (flat) {
4594 // ONE subnetwork holding every processor and every called task
4595 build_ph_flat_layer();
4596 tput.assign(N + 1, Tzero());
4597 util.assign(N + 1, Tzero());
4598 thinkt.assign(N + 1, Tzero());
4599 update_layers_ph(0);
4600 return;
4601 }
4602
4603 std::vector<qn::Layer<T>> raw(NT() + 1);
4604 std::vector<bool> present(NT() + 1, false);
4605
4606 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx) {
4607 if (ignore[hidx]) continue;
4608 std::vector<std::size_t> callers;
4609 for (std::size_t tidx : lqn.tasksof[hidx]) {
4610 if (ignore[tidx]) continue;
4611 if (lqn.isref[tidx]) { callers.push_back(tidx); continue; }
4612 for (std::size_t eidx : lqn.entriesof[tidx])
4613 if (ph_any_caller_of(eidx) || lqn.has_arrival[eidx]) {
4614 callers.push_back(tidx);
4615 break;
4616 }
4617 }
4618 if (callers.empty()) continue;
4619 build_layer_ph(raw[hidx], hidx, callers, true);
4620 present[hidx] = true;
4621 }
4622 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
4623 const std::size_t tidx = lqn.tshift + t;
4624 if (ignore[tidx] || lqn.isref[tidx]) continue;
4625 std::vector<std::size_t> callers;
4626 for (std::size_t ct = 1; ct <= lqn.ntasks; ++ct) {
4627 const std::size_t c = lqn.tshift + ct;
4628 if (c == tidx || ignore[c]) continue;
4629 for (std::size_t e : lqn.entriesof[tidx])
4630 if (lqn.issynccaller.get(c, e)) { callers.push_back(c); break; }
4631 }
4632 if (callers.empty() && ph_async_calls_into(tidx).empty()) continue;
4633 build_layer_ph(raw[tidx], tidx, callers, false);
4634 present[tidx] = true;
4635 }
4636
4637 idxhash.assign(lqn.nidx + 1, -1);
4638 long next = 0;
4639 for (std::size_t i = 1; i <= NT(); ++i)
4640 if (present[i]) {
4641 idxhash[i] = next++;
4642 ensemble.push_back(std::move(raw[i]));
4643 }
4644 layer_init_sol.assign(ensemble.size(), Matrix<T>());
4645 build_fork_views();
4646
4647 // install the initial laws, so that iteration 1 sees the seeded demands
4648 // rather than the placeholders the stations were created with
4649 tput.assign(N + 1, Tzero());
4650 util.assign(N + 1, Tzero());
4651 thinkt.assign(N + 1, Tzero());
4652 update_layers_ph(0);
4653 }
4654
4655 /**
4656 * Build the ONE layer of method "flat.ph": a client delay plus a station for
4657 * every processor and every called task.
4658 *
4659 * A caller task is one closed class, and it visits each server it uses ONCE
4660 * per invocation, carrying there the composed law of the demand it places on
4661 * that server -- the same law method "srvn.ph" installs in the server's own
4662 * layer. What changes is that the servers now contend inside one network
4663 * instead of seeing each other through surrogate delays, so the client delay
4664 * keeps only the think times and whatever of the cycle this model does not
4665 * hold. That is the whole difference between the two encodings of the PH
4666 * composition, and it is why the reconstruction passes are shared verbatim.
4667 */
4668 void build_ph_flat_layer() {
4669 const std::vector<std::size_t> servers = ph_flat_server_set();
4670 const std::size_t nsrv = servers.size();
4671
4672 qn::Layer<T> m;
4673 m.name = "FlatPH";
4674 m.clientIdx = m.add_station(qn::Station<T>{
4675 "Clients", NodeType::Delay, SchedStrategy::INF,
4676 std::numeric_limits<double>::infinity(), false, 0});
4677 const std::size_t clientNode = m.node_of_station(m.clientIdx);
4678 m.server_idx_of.assign(lqn.nidx + 1, 0);
4679
4680 std::vector<std::size_t> station_of(lqn.nidx + 1, 0);
4681 std::vector<std::size_t> serverNode(nsrv, 0);
4682 std::vector<std::size_t> srvStation(nsrv, 0);
4683 for (std::size_t si = 0; si < nsrv; ++si) {
4684 const std::size_t idx = servers[si];
4685 const bool ishost = idx <= lqn.nhosts;
4686 qn::Station<T> st;
4687 st.name = lqn.hashnames[idx];
4688 st.nodetype = lqn.sched[idx] == SchedStrategy::INF ? NodeType::Delay : NodeType::Queue;
4689 st.sched = lqn.sched[idx];
4690 st.nservers = lqn.sched[idx] == SchedStrategy::INF
4691 ? std::numeric_limits<double>::infinity()
4692 : lqn.maxmult[idx];
4693 st.attr_ishost = ishost;
4694 st.attr_idx = idx;
4695 const std::size_t s = m.add_station(st);
4696 srvStation[si] = s;
4697 station_of[idx] = s;
4698 serverNode[si] = m.node_of_station(s);
4699 m.server_idx_of[idx] = s;
4700 if (ishost)
4701 m.host_stations.push_back(s);
4702 else
4703 m.task_stations.push_back(s);
4704 }
4705 // the scalar fallback of the station lookup, which no served element reaches
4706 m.serverIdx = station_of[servers[0]];
4707 m.flat = true;
4708
4709 // Callers of each server, and the union of them, which becomes the class set
4710 std::vector<std::vector<std::size_t>> callers_of(lqn.nidx + 1);
4711 std::vector<std::size_t> all_callers;
4712 for (std::size_t si = 0; si < nsrv; ++si) {
4713 const std::size_t idx = servers[si];
4714 std::vector<std::size_t> cs;
4715 if (idx <= lqn.nhosts) {
4716 for (std::size_t tidx : lqn.tasksof[idx]) {
4717 if (ignore[tidx]) continue;
4718 if (lqn.isref[tidx]) { cs.push_back(tidx); continue; }
4719 for (std::size_t eidx : lqn.entriesof[tidx])
4720 if (ph_any_caller_of(eidx) || lqn.has_arrival[eidx]) {
4721 cs.push_back(tidx);
4722 break;
4723 }
4724 }
4725 } else {
4726 for (std::size_t ct = 1; ct <= lqn.ntasks; ++ct) {
4727 const std::size_t c = lqn.tshift + ct;
4728 if (c == idx || ignore[c]) continue;
4729 for (std::size_t e : lqn.entriesof[idx])
4730 if (lqn.issynccaller.get(c, e)) { cs.push_back(c); break; }
4731 }
4732 }
4733 callers_of[idx] = cs;
4734 for (std::size_t c : cs)
4735 if (std::find(all_callers.begin(), all_callers.end(), c) == all_callers.end())
4736 all_callers.push_back(c);
4737 }
4738 std::sort(all_callers.begin(), all_callers.end());
4739
4740 // One closed class per caller task
4741 std::vector<std::size_t> class_of_caller(lqn.nidx + 1, 0);
4742 double npop = 0.0;
4743 for (std::size_t c : all_callers) {
4744 // ph_flat_server_set has refused every replicated element, so the
4745 // per-replica reduction the srvn builder makes is the identity here
4746 double nj = lqn.maxmult[c];
4747 if (std::isinf(nj)) {
4748 double sacc = 0.0;
4749 for (std::size_t k = 1; k <= NT(); ++k)
4750 if (lqn.taskgraph.get(k, c) != Tzero()) sacc += lqn.maxmult[k];
4751 nj = sacc;
4752 if (std::isinf(nj) || nj == 0.0) {
4753 double s2 = 0.0;
4754 for (std::size_t k = 1; k <= NT(); ++k)
4755 if (std::isfinite(lqn.maxmult[k])) s2 += lqn.maxmult[k];
4756 nj = std::min(s2, 1000.0);
4757 }
4758 }
4759 qn::JobClass jc;
4760 jc.name = lqn.hashnames[c];
4761 jc.type = JobClassType::CLOSED;
4762 jc.population = nj;
4763 jc.refstat = m.clientIdx;
4764 jc.is_ref_class = true;
4765 jc.attr_kind = int(LqnElement::TASK);
4766 jc.attr_idx = c;
4767 const std::size_t k = m.add_class(jc);
4768 class_of_caller[c] = k;
4769 m.attr_tasks.emplace_back(k, c);
4770 npop += nj;
4771 const double zt = dbl(ref_think_time(c));
4772 m.set_service(m.clientIdx, k,
4773 Distrib<T>::exp_mean(num_traits<T>::from_double(
4774 std::max(zt, GlobalConstants::FineTol))));
4775 // A station this caller never reaches must say so with a DISABLED law,
4776 // not with a tiny placeholder. An FCFS station carries ONE service law
4777 // across its classes, so a placeholder is not inert there: it is mixed
4778 // into the multiserver correction and invents waiting where there is
4779 // none. Under "srvn.ph" the question never arises, since every class of
4780 // a layer visits that layer's single server.
4781 for (std::size_t si = 0; si < nsrv; ++si)
4782 m.set_service(srvStation[si], k, Distrib<T>::disabled_dist());
4783 for (std::size_t si = 0; si < nsrv; ++si) {
4784 const std::size_t idx = servers[si];
4785 const std::vector<std::size_t>& cs = callers_of[idx];
4786 if (std::find(cs.begin(), cs.end(), c) == cs.end()) continue;
4787 m.set_service(srvStation[si], k,
4789 num_traits<T>::from_double(GlobalConstants::FineTol)));
4790 njobs(c, idx) = nj;
4791 thinkt_map.push_back({idx, c, m.clientIdx, k});
4792 servt_map.push_back({idx, c, station_of[idx], k});
4793 }
4794 }
4795
4796 // Open classes: entry arrivals on a processor station, async calls on a task one
4797 std::vector<std::vector<std::pair<std::size_t, long>>> open_of(lqn.nidx + 1);
4798 std::size_t sourceStation = 0, sinkNode = 0;
4799 for (std::size_t si = 0; si < nsrv; ++si) {
4800 const std::size_t hidx = servers[si];
4801 if (hidx > lqn.nhosts) continue;
4802 for (std::size_t c : callers_of[hidx]) {
4803 // A task no other task calls has no task station, so the think-time
4804 // closure never gives its caller class a surrogate delay: the class
4805 // cycles against an Immediate one and an open stream on top of it
4806 // doubles the load. The chain is the representation that honours the
4807 // thread pool, so it is kept and closed on the arrival rate instead.
4808 if (ph_open_arrival_only(c)) continue;
4809 for (std::size_t eidx : lqn.entriesof[c]) {
4810 if (!lqn.has_arrival[eidx]) continue;
4811 if (sourceStation == 0) {
4812 sourceStation = m.add_station(qn::Station<T>{
4813 "Source", NodeType::Source, SchedStrategy::EXT,
4814 std::numeric_limits<double>::infinity(), false, 0});
4815 m.sourceIdx = sourceStation;
4816 sinkNode = m.add_node("Sink", NodeType::Sink, false);
4817 m.sinkNode = sinkNode;
4818 }
4819 qn::JobClass oc;
4820 oc.name = lqn.hashnames[eidx] + ".Open";
4821 oc.type = JobClassType::OPEN;
4822 oc.population = std::numeric_limits<double>::infinity();
4823 oc.refstat = sourceStation;
4824 oc.attr_kind = int(LqnElement::ENTRY);
4825 oc.attr_idx = eidx;
4826 const std::size_t k = m.add_class(oc);
4827 m.set_service(sourceStation, k, lqn.arrival[eidx]);
4828 // disabled, not a placeholder, at every station this stream misses
4829 for (std::size_t s2 = 0; s2 < nsrv; ++s2)
4830 m.set_service(srvStation[s2], k, Distrib<T>::disabled_dist());
4831 const double hm = std::max(dbl(ph_hostmean[eidx]), GlobalConstants::FineTol);
4832 m.set_service(srvStation[si], k,
4833 Distrib<T>::exp_mean(num_traits<T>::from_double(hm)));
4834 open_of[hidx].emplace_back(k, long(eidx));
4835 m.attr_entries.emplace_back(k, eidx);
4836 }
4837 }
4838 }
4839 for (std::size_t si = 0; si < nsrv; ++si) {
4840 const std::size_t tidx = servers[si];
4841 if (tidx <= lqn.nhosts) continue;
4842 for (std::size_t cidx : ph_async_calls_into(tidx)) {
4843 if (sourceStation == 0) {
4844 sourceStation = m.add_station(qn::Station<T>{
4845 "Source", NodeType::Source, SchedStrategy::EXT,
4846 std::numeric_limits<double>::infinity(), false, 0});
4847 m.sourceIdx = sourceStation;
4848 sinkNode = m.add_node("Sink", NodeType::Sink, false);
4849 m.sinkNode = sinkNode;
4850 }
4851 const std::size_t eidx = lqn.callpair_dst[cidx];
4852 qn::JobClass oc;
4853 oc.name = lqn.callhashnames[cidx];
4854 oc.type = JobClassType::OPEN;
4855 oc.population = std::numeric_limits<double>::infinity();
4856 oc.refstat = sourceStation;
4857 oc.attr_kind = int(LqnElement::CALL);
4858 oc.attr_idx = cidx;
4859 const std::size_t k = m.add_class(oc);
4860 m.set_service(sourceStation, k, Distrib<T>::immediate());
4861 // disabled, not a placeholder, at every station this stream misses
4862 for (std::size_t s2 = 0; s2 < nsrv; ++s2)
4863 m.set_service(srvStation[s2], k, Distrib<T>::disabled_dist());
4864 const double em = std::max(dbl(ph_entrymean[eidx]), GlobalConstants::FineTol);
4865 m.set_service(srvStation[si], k,
4866 Distrib<T>::exp_mean(num_traits<T>::from_double(em)));
4867 open_of[tidx].emplace_back(k, -long(cidx));
4868 m.attr_calls.push_back({k, cidx, lqn.callpair_src[cidx], eidx});
4869 arv_call_map.push_back({tidx, cidx, sourceStation, k});
4870 call_map.push_back({tidx, cidx, station_of[tidx], k});
4871 }
4872 }
4873
4874 // Routing: one visit per server the caller uses, in server order. The
4875 // number of calls is carried by the service law, not by a visit ratio, so
4876 // no arc ever moves.
4877 for (std::size_t c : all_callers) {
4878 const std::size_t k = class_of_caller[c];
4879 std::size_t prev = clientNode;
4880 bool visited = false;
4881 for (std::size_t si = 0; si < nsrv; ++si) {
4882 const std::vector<std::size_t>& cs = callers_of[servers[si]];
4883 if (std::find(cs.begin(), cs.end(), c) == cs.end()) continue;
4884 m.set_route(k, k, prev, serverNode[si], Tone());
4885 prev = serverNode[si];
4886 visited = true;
4887 }
4888 if (visited) m.set_route(k, k, prev, clientNode, Tone());
4889 }
4890 if (sourceStation != 0) {
4891 const std::size_t srcNode = m.node_of_station(sourceStation);
4892 for (std::size_t si = 0; si < nsrv; ++si)
4893 for (const auto& oa : open_of[servers[si]]) {
4894 m.set_route(oa.first, oa.first, srcNode, serverNode[si], Tone());
4895 m.set_route(oa.first, oa.first, serverNode[si], sinkNode, Tone());
4896 }
4897 }
4898 apply_host_task_priorities(m, servers);
4899 m.refresh_chains();
4900
4901 idxhash.assign(lqn.nidx + 1, -1);
4902 for (std::size_t idx : servers) idxhash[idx] = 0;
4903 ensemble.clear();
4904 ensemble.push_back(std::move(m));
4905 layer_init_sol.assign(ensemble.size(), Matrix<T>());
4906 build_fork_views();
4907
4908 const std::size_t nclasses = ensemble[0].classes.size();
4909 for (std::size_t idx : servers) {
4910 PHLayer L;
4911 L.idx = idx;
4912 L.ishost = idx <= lqn.nhosts;
4913 L.callers = callers_of[idx];
4914 L.class_of_caller = class_of_caller;
4915 L.nreplicas = 1;
4916 L.qstations.assign(1, station_of[idx]);
4917 L.svcmean_by_class.assign(nclasses + 1, Tzero());
4918 L.open_arrivals = open_of[idx];
4919 L.npop = npop < 1.0 ? 1.0 : npop;
4920 ph_layers[idx] = L;
4921 ph_has_layer[idx] = true;
4922 }
4923 }
4924
4925 /**
4926 * Processors and called tasks that become stations of the flat layer.
4927 *
4928 * The set is the elements the srvn builder would have given a layer of their
4929 * own, so "flat.ph" and "srvn.ph" place the SAME stations and differ only in
4930 * how many networks hold them. The refusals are those of flat_server_set,
4931 * since they are properties of the squashing and not of the encoding: each of
4932 * these carries per-layer state that one submodel cannot hold.
4933 */
4934 std::vector<std::size_t> ph_flat_server_set() const {
4935 for (std::size_t i = 1; i <= NT(); ++i) {
4936 if (lqn.repl[i] > 1.0)
4937 throw UnsupportedError(
4938 "method='flat.ph' does not support replicated processors or tasks, whose "
4939 "replicas need a submodel each. Use method='srvn.ph'.");
4940 if (lqn.iscache[i])
4941 throw UnsupportedError(
4942 "method='flat.ph' does not support cache tasks. Use method='default'.");
4943 if (lqn.hassetup[i])
4944 throw UnsupportedError(
4945 "method='flat.ph' does not support setup tasks, whose powered-down threads "
4946 "are per-layer state. Use method='srvn.ph'.");
4947 }
4948 std::vector<std::size_t> servers;
4949 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx) {
4950 if (ignore[hidx] || lqn.tasksof[hidx].empty()) continue;
4951 bool any = false;
4952 for (std::size_t tidx : lqn.tasksof[hidx]) {
4953 if (ignore[tidx]) continue;
4954 if (lqn.isref[tidx]) { any = true; break; }
4955 for (std::size_t eidx : lqn.entriesof[tidx])
4956 if (ph_any_caller_of(eidx) || lqn.has_arrival[eidx]) { any = true; break; }
4957 if (any) break;
4958 }
4959 if (any) servers.push_back(hidx);
4960 }
4961 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
4962 const std::size_t tidx = lqn.tshift + t;
4963 if (ignore[tidx] || lqn.isref[tidx]) continue;
4964 bool has_caller = false;
4965 for (std::size_t ct = 1; ct <= lqn.ntasks && !has_caller; ++ct) {
4966 const std::size_t c = lqn.tshift + ct;
4967 if (c == tidx || ignore[c]) continue;
4968 for (std::size_t e : lqn.entriesof[tidx])
4969 if (lqn.issynccaller.get(c, e)) { has_caller = true; break; }
4970 }
4971 if (!has_caller && ph_async_calls_into(tidx).empty()) continue;
4972 servers.push_back(tidx);
4973 }
4974 if (servers.empty())
4975 throw InputError(
4976 "method='flat.ph' found no server: the model has no processor with tasks.");
4977 return servers;
4978 }
4979
4980 /** Build the two-station layer of server element IDX. */
4981 void build_layer_ph(qn::Layer<T>& m, std::size_t idx,
4982 const std::vector<std::size_t>& callers, bool ishost) {
4983 m.name = lqn.hashnames[idx];
4984
4985 // Replicas of the server station, with the same fan-out reduction as the
4986 // default builder: a caller that reaches every replica sees one
4987 // representative
4988 const double rawrepl = lqn.repl[idx];
4989 std::size_t nreplicas = 1;
4990 if (rawrepl > 1.0 && !callers.empty()) {
4991 bool reduce = true;
4992 if (ishost) {
4993 for (std::size_t c : callers)
4994 if (lqn.repl[c] != rawrepl) reduce = false;
4995 } else {
4996 for (std::size_t c : callers)
4997 if (lqn.fanout_at(c, idx) < rawrepl) reduce = false;
4998 }
4999 nreplicas = reduce ? 1 : static_cast<std::size_t>(std::llround(rawrepl));
5000 if (reduce && !ishost) single_replica_tasks.insert(idx);
5001 }
5002 const bool reduce_fanout = (nreplicas == 1 && rawrepl > 1.0 && !callers.empty());
5003
5004 m.clientIdx = m.add_station(qn::Station<T>{
5005 "Clients", NodeType::Delay, SchedStrategy::INF,
5006 std::numeric_limits<double>::infinity(), false, 0});
5007 const std::size_t clientNode = m.node_of_station(m.clientIdx);
5008 m.serverIdx = m.clientIdx + 1;
5009 PHLayer L;
5010 L.idx = idx;
5011 L.ishost = ishost;
5012 L.callers = callers;
5013 L.nreplicas = nreplicas;
5014 L.class_of_caller.assign(lqn.nidx + 1, 0);
5015 std::vector<std::size_t> serverNode(nreplicas);
5016 for (std::size_t r = 0; r < nreplicas; ++r) {
5017 qn::Station<T> st;
5018 st.name = r == 0 ? lqn.hashnames[idx] : lqn.hashnames[idx] + "." + std::to_string(r + 1);
5019 st.nodetype = lqn.sched[idx] == SchedStrategy::INF ? NodeType::Delay : NodeType::Queue;
5020 st.sched = lqn.sched[idx];
5021 st.nservers = lqn.sched[idx] == SchedStrategy::INF
5022 ? std::numeric_limits<double>::infinity()
5023 : lqn.maxmult[idx];
5024 st.attr_ishost = ishost;
5025 st.attr_idx = idx;
5026 const std::size_t s = m.add_station(st);
5027 L.qstations.push_back(s);
5028 serverNode[r] = m.node_of_station(s);
5029 }
5030
5031 // --- closed class per caller task
5032 for (std::size_t c : callers) {
5033 double nj = njobs(c, idx);
5034 if (nj == 0.0) {
5035 const bool caller_single_replica =
5036 reduce_fanout || single_replica_tasks.count(c) > 0;
5037 nj = caller_single_replica ? lqn.maxmult[c] : lqn.maxmult[c] * lqn.repl[c];
5038 if (std::isinf(nj)) {
5039 double s = 0.0;
5040 for (std::size_t k = 1; k <= NT(); ++k)
5041 if (lqn.taskgraph.get(k, c) != Tzero()) s += lqn.maxmult[k];
5042 nj = s;
5043 if (std::isinf(nj) || nj == 0.0) {
5044 double s2 = 0.0;
5045 for (std::size_t k = 1; k <= NT(); ++k)
5046 if (std::isfinite(lqn.maxmult[k])) s2 += lqn.maxmult[k] * lqn.repl[k];
5047 nj = std::min(s2, 1000.0);
5048 }
5049 }
5050 njobs(c, idx) = nj;
5051 }
5052 qn::JobClass jc;
5053 jc.name = lqn.hashnames[c];
5054 jc.type = JobClassType::CLOSED;
5055 jc.population = nj;
5056 jc.refstat = m.clientIdx;
5057 jc.is_ref_class = true;
5058 jc.attr_kind = int(LqnElement::TASK);
5059 jc.attr_idx = c;
5060 const std::size_t k = m.add_class(jc);
5061 L.class_of_caller[c] = k;
5062 m.attr_tasks.emplace_back(k, c);
5063 const double zt = dbl(ref_think_time(c));
5064 m.set_service(m.clientIdx, k,
5065 Distrib<T>::exp_mean(num_traits<T>::from_double(
5066 std::max(zt, GlobalConstants::FineTol))));
5067 for (std::size_t s : L.qstations)
5068 m.set_service(s, k, Distrib<T>::exp_mean(
5069 num_traits<T>::from_double(GlobalConstants::FineTol)));
5070 // every layer must be refreshed after a law change: post() resets the
5071 // layers named by the think-time map
5072 thinkt_map.push_back({idx, c, m.clientIdx, k});
5073 servt_map.push_back({idx, c, L.qstations[0], k});
5074 }
5075
5076 // --- open classes: entry arrivals on a host layer, async calls on a task layer
5077 std::size_t sourceStation = 0, sinkNode = 0;
5078 if (ishost) {
5079 for (std::size_t c : callers) {
5080 // A task no other task calls has no task layer, so
5081 // update_think_times_ph never gives its caller class a surrogate
5082 // delay: the class cycles against an Immediate one and an open
5083 // stream on top of it doubles the load. The chain is the
5084 // representation that honours the thread pool, so it is kept and
5085 // closed on the arrival rate instead.
5086 if (ph_open_arrival_only(c)) continue;
5087 for (std::size_t eidx : lqn.entriesof[c]) {
5088 if (!lqn.has_arrival[eidx]) continue;
5089 if (sourceStation == 0) {
5090 sourceStation = m.add_station(qn::Station<T>{
5091 "Source", NodeType::Source, SchedStrategy::EXT,
5092 std::numeric_limits<double>::infinity(), false, 0});
5093 m.sourceIdx = sourceStation;
5094 sinkNode = m.add_node("Sink", NodeType::Sink, false);
5095 m.sinkNode = sinkNode;
5096 }
5097 qn::JobClass oc;
5098 oc.name = lqn.hashnames[eidx] + ".Open";
5099 oc.type = JobClassType::OPEN;
5100 oc.population = std::numeric_limits<double>::infinity();
5101 oc.refstat = sourceStation;
5102 oc.attr_kind = int(LqnElement::ENTRY);
5103 oc.attr_idx = eidx;
5104 const std::size_t k = m.add_class(oc);
5105 m.set_service(sourceStation, k, lqn.arrival[eidx]);
5106 for (std::size_t s : L.qstations) {
5107 const double hm = std::max(dbl(ph_hostmean[eidx]), GlobalConstants::FineTol);
5108 m.set_service(s, k, Distrib<T>::exp_mean(num_traits<T>::from_double(hm)));
5109 }
5110 L.open_arrivals.emplace_back(k, long(eidx));
5111 m.attr_entries.emplace_back(k, eidx);
5112 }
5113 }
5114 } else {
5115 for (std::size_t cidx : ph_async_calls_into(idx)) {
5116 if (sourceStation == 0) {
5117 sourceStation = m.add_station(qn::Station<T>{
5118 "Source", NodeType::Source, SchedStrategy::EXT,
5119 std::numeric_limits<double>::infinity(), false, 0});
5120 m.sourceIdx = sourceStation;
5121 sinkNode = m.add_node("Sink", NodeType::Sink, false);
5122 m.sinkNode = sinkNode;
5123 }
5124 const std::size_t eidx = lqn.callpair_dst[cidx];
5125 qn::JobClass oc;
5126 oc.name = lqn.callhashnames[cidx];
5127 oc.type = JobClassType::OPEN;
5128 oc.population = std::numeric_limits<double>::infinity();
5129 oc.refstat = sourceStation;
5130 oc.attr_kind = int(LqnElement::CALL);
5131 oc.attr_idx = cidx;
5132 const std::size_t k = m.add_class(oc);
5133 m.set_service(sourceStation, k, Distrib<T>::immediate());
5134 for (std::size_t s : L.qstations) {
5135 const double em = std::max(dbl(ph_entrymean[eidx]), GlobalConstants::FineTol);
5136 m.set_service(s, k, Distrib<T>::exp_mean(num_traits<T>::from_double(em)));
5137 }
5138 L.open_arrivals.emplace_back(k, -long(cidx));
5139 m.attr_calls.push_back({k, cidx, lqn.callpair_src[cidx], eidx});
5140 arv_call_map.push_back({idx, cidx, sourceStation, k});
5141 call_map.push_back({idx, cidx, L.qstations[0], k});
5142 }
5143 }
5144
5145 // Routing: one visit to the server per client cycle. The number of calls
5146 // is carried by the service law, not by a visit ratio, so no arc changes
5147 const T share = num_traits<T>::from_double(1.0 / double(nreplicas));
5148 for (std::size_t c : callers) {
5149 const std::size_t k = L.class_of_caller[c];
5150 for (std::size_t r = 0; r < nreplicas; ++r) {
5151 m.set_route(k, k, clientNode, serverNode[r], share);
5152 m.set_route(k, k, serverNode[r], clientNode, Tone());
5153 }
5154 }
5155 for (const auto& oa : L.open_arrivals) {
5156 const std::size_t k = oa.first;
5157 const std::size_t srcNode = m.node_of_station(sourceStation);
5158 for (std::size_t r = 0; r < nreplicas; ++r) {
5159 m.set_route(k, k, srcNode, serverNode[r], share);
5160 m.set_route(k, k, serverNode[r], sinkNode, Tone());
5161 }
5162 }
5163 L.svcmean_by_class.assign(m.classes.size() + 1, Tzero());
5164 double np = 0.0;
5165 for (std::size_t c : callers) {
5166 const double v = njobs(c, idx);
5167 if (std::isfinite(v) && v > 0.0) np += v;
5168 }
5169 L.npop = np < 1.0 ? 1.0 : np;
5170 apply_host_task_priorities(m, std::vector<std::size_t>{idx});
5171 m.refresh_chains();
5172 ph_layers[idx] = L;
5173 ph_has_layer[idx] = true;
5174 }
5175
5176 /** Law of the demand caller C places on the server of layer IDX per invocation. */
5177 workflow::PhLaw<T> ph_service_law(std::size_t idx, bool ishost, std::size_t c) const {
5178 if (ishost) {
5179 // mixture over the entries of C, weighted by their share of its requests
5180 std::vector<workflow::PhLaw<T>> laws;
5181 std::vector<T> probs;
5182 double tot = 0.0;
5183 for (std::size_t eidx : lqn.entriesof[c]) {
5184 if (ph_hostlaw[eidx].S.rows() == 0 || !(dbl(ph_share[eidx]) > 0.0)) continue;
5185 laws.push_back(ph_hostlaw[eidx]);
5186 probs.push_back(ph_share[eidx]);
5187 tot += dbl(ph_share[eidx]);
5188 }
5189 if (laws.empty()) return ph_immediate_law();
5190 for (T& p : probs) p = T(p / num_traits<T>::from_double(tot));
5191 return workflow::Workflow<T>::compose_mixture(laws, probs);
5192 }
5193 // task layer: the total demand is the sum, over the entries of the
5194 // server, of a geometric compound of the entry law of mean equal to the
5195 // number of calls
5196 bool started = false;
5197 workflow::PhLaw<T> out;
5198 for (std::size_t eidx : lqn.entriesof[idx]) {
5199 const T n = ph_ncalls(c, eidx);
5200 if (dbl(n) <= GlobalConstants::FineTol || ph_entrylaw[eidx].S.rows() == 0) continue;
5201 const workflow::PhLaw<T> lp =
5203 out = started ? workflow::Workflow<T>::compose_serial(out, lp) : lp;
5204 started = true;
5205 }
5206 if (!started) return ph_immediate_law();
5207 return out;
5208 }
5209
5210 /**
5211 * Mean time a thread of caller C spends away from the server of layer IDX per
5212 * invocation: idle, plus whatever of its cycle the layer does not hold.
5213 */
5214 T ph_delay_mean(std::size_t idx, std::size_t c) const {
5215 // ONE closure for both layerings. Under "srvn.ph" the model holds a single
5216 // server, so a host layer charges the whole call burst to the delay and a
5217 // task layer charges the caller's processor plus every other callee. Under
5218 // "flat.ph" the model holds every server, and only the think times are
5219 // left. Every term is SUMMED in rather than obtained by subtracting from a
5220 // total: that subtraction cancels catastrophically once a call time is
5221 // large, the think time falls below the ULP of the call time, and the layer
5222 // then sees a client delay of zero, saturates, and the fixed point runs
5223 // away.
5224 T z = T(thinkt[c] + ref_think_time(c));
5225 if (!std::isfinite(dbl(z)) || dbl(z) < 0.0) z = Tzero();
5226 z = T(z + ph_actthinkt[c]);
5227 const std::size_t hidx = lqn.parent[c];
5228 if (!ph_served_here(idx, hidx)) z = T(z + ph_procresid[c]);
5229 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5230 const std::size_t tidx = lqn.tshift + t;
5231 if (!ph_served_here(idx, tidx)) z = T(z + ph_calltime(c, tidx));
5232 }
5233 if (!std::isfinite(dbl(z)) || dbl(z) < 0.0)
5234 z = num_traits<T>::from_double(GlobalConstants::FineTol);
5235 return z;
5236 }
5237
5238 /**
5239 * True when LQN element ELEM is a station of the same model that holds server
5240 * IDX. Under "srvn.ph" that is ELEM == IDX, since each server has a layer of
5241 * its own; under "flat.ph" it is every server of the one network.
5242 */
5243 bool ph_served_here(std::size_t idx, std::size_t elem) const {
5244 if (elem < 1 || elem >= idxhash.size() || idx < 1 || idx >= idxhash.size())
5245 return false;
5246 return idxhash[elem] >= 0 && idxhash[elem] == idxhash[idx];
5247 }
5248
5249 /**
5250 * Push the composed laws into the layers.
5251 *
5252 * A layer of this method carries no routing that depends on the iterate: the
5253 * number of calls a caller makes is folded into its service law rather than
5254 * into a visit ratio, so only two laws move per (layer, class) -- the
5255 * phase-type service law at the server and the mean of the surrogate delay
5256 * at the client.
5257 */
5258 void update_layers_ph(int) {
5259 for (std::size_t idx = 1; idx <= NT(); ++idx) {
5260 if (idxhash[idx] < 0 || !ph_has_layer[idx]) continue;
5261 PHLayer& L = ph_layers[idx];
5262 qn::Layer<T>& m = ensemble[std::size_t(idxhash[idx])];
5263 for (std::size_t c : L.callers) {
5264 const std::size_t k = L.class_of_caller[c];
5265 const workflow::PhLaw<T> sl = ph_service_law(idx, L.ishost, c);
5266 L.svcmean_by_class[k] = api::lqn::ph_moments(sl.alpha, sl.S).first;
5267 const Distrib<T> law = ph_station_law(sl);
5268 for (std::size_t s : L.qstations) m.set_service(s, k, law);
5269 const double zd = std::max(dbl(ph_delay_mean(idx, c)),
5271 m.set_service(m.clientIdx, k,
5272 Distrib<T>::exp_mean(num_traits<T>::from_double(zd)));
5273 }
5274 for (const auto& oa : L.open_arrivals) {
5275 const std::size_t k = oa.first;
5276 if (oa.second > 0) {
5277 // entry arrival: the processor demand law of the entry is static
5278 L.svcmean_by_class[k] = ph_hostmean[std::size_t(oa.second)];
5279 continue;
5280 }
5281 const std::size_t cidx = std::size_t(-oa.second);
5282 const std::size_t eidx = lqn.callpair_dst[cidx];
5283 L.svcmean_by_class[k] = ph_entrymean[eidx];
5284 const Distrib<T> law = ph_station_law(ph_entrylaw[eidx]);
5285 for (std::size_t s : L.qstations) m.set_service(s, k, law);
5286 const std::size_t aidx = lqn.callpair_src[cidx];
5287 double rate = dbl(tput[aidx]) * dbl(lqn.callproc_mean[cidx]);
5288 if (!std::isfinite(rate) || rate <= GlobalConstants::FineTol)
5290 m.set_service(m.sourceIdx, k,
5291 Distrib<T>::exp_rate(num_traits<T>::from_double(rate)));
5292 }
5293 m.refresh_rt();
5294 }
5295 }
5296
5297 /**
5298 * Residence time per visit, by Little from the queue length rather than from
5299 * the reported RN. A layer that saturates can come back from AMVA with an RN
5300 * no closed model can produce, and a reconstruction that trusts it feeds the
5301 * impossible value straight back into the call response times.
5302 */
5303 static T ph_residence(const T& Q, const T& X, const T& RN) {
5304 const double q = num_traits<T>::to_double(Q), x = num_traits<T>::to_double(X);
5305 if (std::isfinite(q) && q >= 0.0 && std::isfinite(x) && x > GlobalConstants::FineTol)
5306 return T(Q / X);
5307 return RN;
5308 }
5309
5310 /**
5311 * Ratio of a residence time to the mean of the law it was measured against,
5312 * bounded above by the layer population: a job can wait behind at most every
5313 * other job in a closed layer.
5314 */
5315 static T ph_inflation_of(const T& R, const T& S, double npop) {
5316 double f = 1.0;
5317 const double s = num_traits<T>::to_double(S), r = num_traits<T>::to_double(R);
5318 if (s > GlobalConstants::FineTol && std::isfinite(r) && r > 0.0) f = r / s;
5319 if (!std::isfinite(f) || f < 1.0) f = 1.0;
5320 if (std::isfinite(npop) && npop >= 1.0 && f > npop) f = npop;
5321 return num_traits<T>::from_double(f);
5322 }
5323
5324 /** Total closed population of a layer, i.e. how many jobs a job can queue behind. */
5325 double ph_layer_pop(const PHLayer& L, std::size_t idx) const {
5326 // Under "flat.ph" this is every caller of the single network and not only
5327 // the callers of this one station, so it is taken from the layer record.
5328 if (L.npop >= 1.0) return L.npop;
5329 double n = 0.0;
5330 for (std::size_t c : L.callers) {
5331 const double v = njobs(c, idx);
5332 if (std::isfinite(v) && v > 0.0) n += v;
5333 }
5334 return n < 1.0 ? 1.0 : n;
5335 }
5336
5337 /**
5338 * Reconstruct the LQN metrics.
5339 *
5340 * A layer of this method reports one row per caller task, not one per entry,
5341 * activity and call, so the per-element quantities the rest of SolverLN reads
5342 * -- servt, residt, callservt, callresidt, tput -- are recovered analytically
5343 * from the series-parallel weights of the entry workflows.
5344 *
5345 * The split is conservative by construction. A station reports a residence
5346 * time R per visit against a service law of mean S, so the queueing inflation
5347 * R/S is attributed to every leaf of that visit in proportion to its own
5348 * mean: the pieces sum back to R exactly.
5349 */
5350 void update_metrics_ph(int it) {
5351 const std::size_t N = lqn.nidx;
5352 servt.assign(N + 1, Tzero());
5353 residt.assign(N + 1, Tzero());
5354 callservt.assign(lqn.ncalls + 1, Tzero());
5355 callresidt.assign(lqn.ncalls + 1, Tzero());
5356
5357 std::vector<T> inflNum(N + 1, Tzero()), inflDen(N + 1, Tzero());
5358 std::vector<T> taskTput(N + 1, Tzero()), openTput(N + 1, Tzero());
5359
5360 // Host layers: the queueing inflation of the processor demand
5361 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx) {
5362 if (idxhash[hidx] < 0 || !ph_has_layer[hidx]) continue;
5363 const PHLayer& L = ph_layers[hidx];
5364 const LayerResult<T>& res = results.back()[std::size_t(idxhash[hidx])];
5365 const double npop = ph_layer_pop(L, hidx);
5366 for (std::size_t c : L.callers) {
5367 const std::size_t k = L.class_of_caller[c];
5368 T X = Tzero(), Q = Tzero();
5369 for (std::size_t s : L.qstations) {
5370 X = T(X + res.TN(s - 1, k - 1));
5371 Q = T(Q + res.QN(s - 1, k - 1));
5372 }
5373 const T R = ph_residence(Q, X, res.RN(L.qstations[0] - 1, k - 1));
5374 const T f = ph_inflation_of(R, L.svcmean_by_class[k], npop);
5375 if (!std::isfinite(dbl(X)) || dbl(X) < 0.0) X = Tzero();
5376 // TOTAL over the replicas. The processor layer of a replicated
5377 // element models ONE representative replica, so X is one replica's
5378 // rate and the element's own rate is REPL times it. The matching
5379 // per-replica quantity is ph_xdemand, which the think-time closure
5380 // divides down for the same reason.
5381 taskTput[c] = T(taskTput[c] + num_traits<T>::from_double(lqn.repl[c]) * X);
5382 for (std::size_t eidx : lqn.entriesof[c]) {
5383 const T sh = dbl(ph_share[eidx]) > 0.0 ? ph_share[eidx] : Tzero();
5384 const T w = T(sh * X);
5385 inflNum[eidx] = T(inflNum[eidx] + w * f);
5386 inflDen[eidx] = T(inflDen[eidx] + w);
5387 }
5388 }
5389 for (const auto& oa : L.open_arrivals) {
5390 if (oa.second <= 0) continue; // an async call is served in the task layer
5391 const std::size_t k = oa.first;
5392 const std::size_t eidx = std::size_t(oa.second);
5393 T X = Tzero(), Q = Tzero();
5394 for (std::size_t s : L.qstations) {
5395 X = T(X + res.TN(s - 1, k - 1));
5396 Q = T(Q + res.QN(s - 1, k - 1));
5397 }
5398 if (!std::isfinite(dbl(X)) || dbl(X) <= 0.0) continue;
5399 const T f = ph_inflation_of(ph_residence(Q, X, res.RN(L.qstations[0] - 1, k - 1)),
5400 L.svcmean_by_class[k], npop);
5401 inflNum[eidx] = T(inflNum[eidx] + X * f);
5402 inflDen[eidx] = T(inflDen[eidx] + X);
5403 openTput[eidx] = T(openTput[eidx] + X);
5404 taskTput[lqn.parent[eidx]] = T(taskTput[lqn.parent[eidx]] + X);
5405 }
5406 }
5407
5408 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
5409 const std::size_t eidx = lqn.eshift + e;
5410 double f = 1.0;
5411 if (dbl(inflDen[eidx]) > GlobalConstants::FineTol)
5412 f = dbl(inflNum[eidx]) / dbl(inflDen[eidx]);
5413 if (!std::isfinite(f) || f < 1.0) f = 1.0; // a residence cannot fall below its demand
5414 const T fv = num_traits<T>::from_double(f);
5415 for (std::size_t aidx : lqn.actsof[eidx])
5416 residt[aidx] = T(fv * (lqn.hostdem[aidx].disabled ? Tzero()
5417 : lqn.hostdem[aidx].mean));
5418 }
5419
5420 // Task layers: the response time of every call
5421 std::vector<T> relw(N + 1, Tzero());
5422 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5423 const std::size_t tidx = lqn.tshift + t;
5424 if (idxhash[tidx] < 0 || !ph_has_layer[tidx]) continue;
5425 const PHLayer& L = ph_layers[tidx];
5426 const LayerResult<T>& res = results.back()[std::size_t(idxhash[tidx])];
5427 const double npop = ph_layer_pop(L, tidx);
5428 for (std::size_t c : L.callers) {
5429 const std::size_t k = L.class_of_caller[c];
5430 T X = Tzero(), Q = Tzero();
5431 for (std::size_t s : L.qstations) {
5432 X = T(X + res.TN(s - 1, k - 1));
5433 Q = T(Q + res.QN(s - 1, k - 1));
5434 }
5435 if (!std::isfinite(dbl(X)) || dbl(X) < 0.0) X = Tzero();
5436 const T g = ph_inflation_of(ph_residence(Q, X, res.RN(L.qstations[0] - 1, k - 1)),
5437 L.svcmean_by_class[k], npop);
5438 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
5439 if (lqn.calltype[cidx] != CallType::SYNC) continue;
5440 if (lqn.parent[lqn.callpair_src[cidx]] != c) continue;
5441 if (lqn.parent[lqn.callpair_dst[cidx]] != tidx) continue;
5442 const std::size_t eidx = lqn.callpair_dst[cidx];
5443 callservt[cidx] = T(lqn.callproc_mean[cidx] * g * ph_entrymean[eidx]);
5444 callresidt[cidx] = callservt[cidx];
5445 }
5446 for (std::size_t eidx : lqn.entriesof[tidx])
5447 relw[eidx] = T(relw[eidx] + X * ph_ncalls(c, eidx));
5448 }
5449 for (const auto& oa : L.open_arrivals) {
5450 if (oa.second >= 0) continue;
5451 const std::size_t k = oa.first;
5452 const std::size_t cidx = std::size_t(-oa.second);
5453 const std::size_t eidx = lqn.callpair_dst[cidx];
5454 T X = Tzero();
5455 for (std::size_t s : L.qstations) X = T(X + res.TN(s - 1, k - 1));
5456 const T R = res.RN(L.qstations[0] - 1, k - 1);
5457 if (std::isfinite(dbl(R)) && dbl(R) > 0.0) {
5458 callservt[cidx] = T(R * lqn.callproc_mean[cidx]);
5459 callresidt[cidx] = callservt[cidx];
5460 }
5461 if (std::isfinite(dbl(X)) && dbl(X) > 0.0) relw[eidx] = T(relw[eidx] + X);
5462 }
5463 }
5464
5465 // Entry shares and throughputs
5466 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5467 const std::size_t tidx = lqn.tshift + t;
5468 const std::vector<std::size_t>& entries = lqn.entriesof[tidx];
5469 if (entries.empty()) continue;
5470 // How the requests SPLIT over the entries is a flow-balance question,
5471 // and is answered at the task layer: a caller class reaches that server
5472 // once per invocation of the caller, carrying its whole call burst in
5473 // its service law, so the station rate counts caller cycles and the
5474 // per-entry rate is that rate times the calls the caller makes.
5475 T tot = Tzero();
5476 for (std::size_t eidx : entries) tot = T(tot + relw[eidx] + openTput[eidx]);
5477 if (dbl(tot) > GlobalConstants::FineTol) {
5478 for (std::size_t eidx : entries)
5479 ph_share[eidx] = T((relw[eidx] + openTput[eidx]) / tot);
5480 } else {
5481 for (std::size_t eidx : entries)
5482 ph_share[eidx] = num_traits<T>::from_double(1.0 / double(entries.size()));
5483 }
5484 // HOW MANY requests the task completes is a different question, and the
5485 // flow-balance total does not answer it: that total is what the callers
5486 // DEMAND, not what the task's threads can deliver. A thread cycles
5487 // through its host demand AND then through the task think time, and only
5488 // the processor layer of the task carries both, so the rate is read there.
5489 tput[tidx] = dbl(taskTput[tidx]) > GlobalConstants::FineTol ? taskTput[tidx] : tot;
5490 for (std::size_t eidx : entries) tput[eidx] = T(tput[tidx] * ph_share[eidx]);
5491 // The DEMAND is kept apart because it, and not the rate just reported,
5492 // is what closes the surrogate delay: normalising the think time by a
5493 // rate the same think time produced makes the processor layer
5494 // self-referential. PER REPLICA, because the thread count it is paired
5495 // with there is per replica.
5496 const T nrep = num_traits<T>::from_double(std::max(1.0, lqn.repl[tidx]));
5497 ph_xdemand[tidx] = dbl(tot) > GlobalConstants::FineTol ? T(tot / nrep)
5498 : T(tput[tidx] / nrep);
5499 }
5500
5501 // Recovery, under-relaxation, and the derived per-element quantities
5502 for (std::size_t aidx = lqn.ashift + 1; aidx <= lqn.ashift + lqn.nacts; ++aidx) {
5503 T v = residt[aidx];
5504 if (!std::isfinite(dbl(v)) && it > 1 && !std::isnan(residt_prev[aidx]))
5505 v = residt_prev_v[aidx];
5506 if (relax_omega < 1.0 && it > 1 && !std::isnan(residt_prev[aidx])) {
5507 const T om = num_traits<T>::from_double(relax_omega);
5508 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
5509 v = T(om * v + om1 * residt_prev_v[aidx]);
5510 }
5511 residt[aidx] = v;
5512 residt_prev[aidx] = dbl(v);
5513 residt_prev_v[aidx] = v;
5514 }
5515 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
5516 T v = callservt[cidx];
5517 if (!std::isfinite(dbl(v)))
5518 v = (it > 1 && std::isfinite(callservt_prev[cidx])) ? callservt_prev_v[cidx]
5519 : Tzero();
5520 if (relax_omega < 1.0 && it > 1 && !std::isnan(callservt_prev[cidx])) {
5521 const T om = num_traits<T>::from_double(relax_omega);
5522 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
5523 v = T(om * v + om1 * callservt_prev_v[cidx]);
5524 }
5525 callservt[cidx] = v;
5526 callresidt[cidx] = v;
5527 callservt_prev[cidx] = dbl(v);
5528 callresidt_prev[cidx] = dbl(v);
5529 callservt_prev_v[cidx] = v;
5530 if (dbl(v) > 0.0) callservtproc[cidx] = Distrib<T>::exp_mean(v);
5531 }
5532
5533 // Recompose the entry laws from the iterate just computed. The entry
5534 // service time is then the mean of the COMPOSED law and not the sum of the
5535 // parts: the branches of an AND fork overlap, so an entry that forks
5536 // finishes with the last of its branches and is not charged their sum.
5537 ph_compose_entry_laws();
5538
5539 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
5540 const std::size_t eidx = lqn.eshift + e;
5541 if (!ph_has_wf[eidx]) continue;
5542 const std::unordered_map<std::size_t, T>& ex = ph_execs[eidx];
5543 for (std::size_t aidx : lqn.actsof[eidx]) {
5544 T sa = T(residt[aidx] + act_think_time(aidx));
5545 for (std::size_t cidx : lqn.callsof[aidx])
5546 if (lqn.calltype[cidx] == CallType::SYNC) sa = T(sa + callservt[cidx]);
5547 servt[aidx] = sa;
5548 servt_prev[aidx] = dbl(sa);
5549 servt_prev_v[aidx] = sa;
5550 tput[aidx] = T(tput[eidx] * ex.at(aidx));
5551 tput_prev[aidx] = dbl(tput[aidx]);
5552 tput_prev_v[aidx] = tput[aidx];
5553 // exp_rate admits a null rate, but a never-called activity has no arrivals: disabled says so, as the python twin does.
5554 tputproc[aidx] = dbl(tput[aidx]) > 0.0 ? Distrib<T>::exp_rate(tput[aidx])
5555 : Distrib<T>::disabled_dist();
5556 if (dbl(sa) > 0.0) servtproc[aidx] = Distrib<T>::exp_mean(sa);
5557 }
5558 servt[eidx] = ph_entrymean[eidx];
5559 residt[eidx] = ph_entrymean[eidx];
5560 if (dbl(servt[eidx]) > 0.0) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
5561 }
5562 }
5563
5564 /**
5565 * Surrogate delay of every caller.
5566 *
5567 * Same closure as update_think_times -- a thread of the task is idle for
5568 * whatever of its cycle the task's own station does not hold -- but the rate
5569 * it is normalised by is the INVOCATION rate of the task and not the
5570 * throughput of its station. Under this method a caller class reaches the
5571 * server once per invocation of the caller, carrying its whole call burst in
5572 * its service law, so the station rate counts caller cycles rather than calls
5573 * and the two differ by the mean number of calls.
5574 */
5575 void update_think_times_ph(int it) {
5576 thinktproc.assign(lqn.nidx + 1, Distrib<T>::disabled_dist());
5577 const T floorv = num_traits<T>::from_double(GlobalConstants::Zero);
5578 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5579 const std::size_t tidx = lqn.tshift + t;
5580 if (ignore[tidx]) continue;
5581 const T ztask = ref_think_time(tidx);
5582 if (idxhash[tidx] < 0) {
5583 // A task no other task calls but whose entries carry an arrival
5584 // still has a cycle: its threads are driven by the stream.
5585 // build_layers_ph drops the open class for it precisely so this
5586 // closure can set the rate.
5587 const double arvrate = open_arrival_rate_of(tidx);
5588 if (arvrate > GlobalConstants::FineTol) {
5589 double nja = lqn.maxmult[tidx];
5590 if (!std::isfinite(nja) || nja <= 0.0) {
5591 nja = 0.0;
5592 for (std::size_t c = 1; c <= NT(); ++c) nja = std::max(nja, njobs(tidx, c));
5593 }
5594 T hres = Tzero();
5595 const std::size_t hidx = lqn.parent[tidx];
5596 if (hidx >= 1 && hidx < idxhash.size() && idxhash[hidx] >= 0 &&
5597 ph_has_layer[hidx] && ph_layers[hidx].class_of_caller[tidx] > 0) {
5598 const PHLayer& HL = ph_layers[hidx];
5599 const LayerResult<T>& hr = results.back()[std::size_t(idxhash[hidx])];
5600 const T rr = hr.RN(HL.qstations[0] - 1, HL.class_of_caller[tidx] - 1);
5601 if (!std::isnan(dbl(rr))) hres = rr;
5602 }
5603 T za = T(num_traits<T>::from_double(nja / arvrate) - hres - ztask);
5604 if (za < floorv) za = floorv;
5605 if (relax_omega < 1.0 && it > 1 && !std::isnan(thinkt_prev[tidx])) {
5606 const T om = num_traits<T>::from_double(relax_omega);
5607 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
5608 za = T(om * za + om1 * thinkt_prev_v[tidx]);
5609 }
5610 tput[tidx] = num_traits<T>::from_double(arvrate);
5611 thinkt[tidx] = za;
5612 thinkt_prev[tidx] = dbl(za);
5613 thinkt_prev_v[tidx] = za;
5614 thinktproc[tidx] = Distrib<T>::exp_mean(T(za + ztask));
5615 continue;
5616 }
5617 // a reference task, or one no other task calls: it has no station
5618 // of its own, so its only delay is the think time the user declared
5619 thinkt[tidx] = num_traits<T>::from_double(GlobalConstants::FineTol);
5620 thinktproc[tidx] = Distrib<T>::immediate();
5621 continue;
5622 }
5623 const PHLayer& L = ph_layers[tidx];
5624 const qn::Layer<T>& m = ensemble[std::size_t(idxhash[tidx])];
5625 const LayerResult<T>& r = results.back()[std::size_t(idxhash[tidx])];
5626 T U = Tzero();
5627 for (std::size_t k = 0; k < m.nclasses; ++k) {
5628 const T v = r.UN(L.qstations[0] - 1, k);
5629 if (!std::isnan(dbl(v))) U = T(U + v);
5630 }
5631 util[tidx] = U;
5632 // The closure below measures a thread's cycle in WORK: it is idle for
5633 // whatever of the cycle its station does not hold it working. A
5634 // SetupTask's station service also carries a cold start, which is time
5635 // the thread is unavailable but is not work, so it is taken back out of
5636 // U before the closure reads it. Zero for every task without a setup.
5637 if (dbl(ph_setupshare[tidx]) > 0.0)
5638 U = T(U * (Tone() - ph_setupshare[tidx]));
5639 // the rate the CALLERS ask of the task, not the rate its processor
5640 // layer reported: the latter is itself a function of this think time
5641 T X = ph_xdemand[tidx];
5642 if (!(dbl(X) > GlobalConstants::FineTol)) X = tput[tidx];
5643 // The thread pool of ONE replica, the convention ph_xdemand is kept in
5644 double nj = lqn.maxmult[tidx];
5645 if (!std::isfinite(nj) || nj <= 0.0) {
5646 nj = 0.0;
5647 for (std::size_t c = 1; c <= NT(); ++c) nj = std::max(nj, njobs(tidx, c));
5648 }
5649 T z;
5650 if (dbl(X) > GlobalConstants::FineTol) {
5651 if (lqn.sched[tidx] == SchedStrategy::INF) {
5652 // an infinite server reports a mean number of busy threads
5653 z = T((num_traits<T>::from_double(nj) - U) / X - ztask);
5654 } else {
5655 const T om = U > Tone() ? T(U - Tone()) : T(Tone() - U);
5656 z = T(num_traits<T>::from_double(nj) * om / X - ztask);
5657 }
5658 } else {
5659 z = thinkt[tidx];
5660 }
5661 if (z < floorv) z = floorv;
5662 if (it > 1 && !std::isnan(thinkt_prev[tidx]) && !std::isfinite(dbl(z)))
5663 z = thinkt_prev_v[tidx];
5664 if (relax_omega < 1.0 && it > 1 && !std::isnan(thinkt_prev[tidx])) {
5665 const T om = num_traits<T>::from_double(relax_omega);
5666 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
5667 z = T(om * z + om1 * thinkt_prev_v[tidx]);
5668 }
5669 thinkt[tidx] = z;
5670 thinkt_prev[tidx] = dbl(z);
5671 thinkt_prev_v[tidx] = z;
5672 thinktproc[tidx] = Distrib<T>::exp_mean(T(z + ztask));
5673 }
5674 }
5675
5676 /**
5677 * LQN-level results of method "srvn.ph".
5678 *
5679 * The layers report per caller task, so every entry, activity and call figure
5680 * is rebuilt from the converged fixed point rather than read off a class row,
5681 * in the layout aggregate() returns: QN carries the entry and task
5682 * utilizations, UN the processor utilizations, RN the response times and WN
5683 * the residence times.
5684 */
5685 LnSolution<T> aggregate_ph() {
5686 const std::size_t N = lqn.nidx;
5687 LnSolution<T> out;
5688 out.QN.assign(N + 1, Tzero());
5689 out.UN.assign(N + 1, Tzero());
5690 out.RN.assign(N + 1, Tzero());
5691 out.TN.assign(N + 1, Tzero());
5692 out.AN.assign(N + 1, Tzero());
5693 out.WN.assign(N + 1, Tzero());
5694 out.defined_Q.assign(N + 1, false);
5695 out.defined_U.assign(N + 1, false);
5696 out.defined_R.assign(N + 1, false);
5697 out.defined_T.assign(N + 1, false);
5698 out.defined_A.assign(N + 1, false);
5699 out.defined_W.assign(N + 1, false);
5700 out.iterations = iterations_done;
5701 out.converged = did_converge;
5702
5703 std::vector<T> PN(N + 1, Tzero()), UT(N + 1, Tzero());
5704 std::vector<bool> hasPN(N + 1, false), hasUT(N + 1, false);
5705
5706 for (std::size_t a = 1; a <= lqn.nacts; ++a) {
5707 const std::size_t aidx = lqn.ashift + a;
5708 const std::size_t tidx = lqn.parent[aidx];
5709 if (ignore[tidx]) continue;
5710 const std::size_t hidx = lqn.parent[tidx];
5711 out.TN[aidx] = tput[aidx];
5712 out.defined_T[aidx] = true;
5713 out.RN[aidx] = servt[aidx];
5714 out.defined_R[aidx] = true;
5715 UT[aidx] = T(tput[aidx] * servt[aidx]);
5716 hasUT[aidx] = true;
5717 // LINE scales the utilization of a queueing station into [0,1] whatever
5718 // its multiplicity, and reports a mean number of busy servers at an
5719 // infinite server: the processor share of an activity follows the same
5720 // convention
5721 const T hd = lqn.hostdem[aidx].disabled ? Tzero() : lqn.hostdem[aidx].mean;
5722 PN[aidx] = T(tput[aidx] * hd / num_traits<T>::from_double(ph_host_servers(hidx)));
5723 hasPN[aidx] = true;
5724 PN[hidx] = T(PN[hidx] + PN[aidx]);
5725 hasPN[hidx] = true;
5726 }
5727
5728 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
5729 const std::size_t eidx = lqn.eshift + e;
5730 const std::size_t tidx = lqn.parent[eidx];
5731 if (ignore[tidx]) continue;
5732 out.TN[eidx] = tput[eidx];
5733 out.defined_T[eidx] = true;
5734 out.RN[eidx] = servt[eidx];
5735 out.defined_R[eidx] = true;
5736 UT[eidx] = T(tput[eidx] * servt[eidx]);
5737 hasUT[eidx] = true;
5738 for (std::size_t aidx : lqn.actsof[eidx]) {
5739 PN[eidx] = T(PN[eidx] + PN[aidx]);
5740 hasPN[eidx] = true;
5741 }
5742 // ResidT is reported per visit to the TASK, not per execution of the
5743 // activity: an activity of this entry runs EXECS times per invocation,
5744 // and the entry takes SHARE of the task's invocations. RespT stays per
5745 // execution.
5746 if (ph_has_wf[eidx]) {
5747 const std::unordered_map<std::size_t, T>& ex = ph_execs[eidx];
5748 for (std::size_t aidx : lqn.actsof[eidx]) {
5749 out.WN[aidx] = T(ph_share[eidx] * ex.at(aidx) * residt[aidx]);
5750 out.defined_W[aidx] = true;
5751 }
5752 }
5753 UT[tidx] = T(UT[tidx] + UT[eidx]);
5754 hasUT[tidx] = true;
5755 }
5756
5757 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5758 const std::size_t tidx = lqn.tshift + t;
5759 if (ignore[tidx]) continue;
5760 out.TN[tidx] = tput[tidx];
5761 out.defined_T[tidx] = true;
5762 T w = Tzero();
5763 bool anyw = false;
5764 for (std::size_t aidx : lqn.actsof[tidx]) {
5765 PN[tidx] = T(PN[tidx] + PN[aidx]);
5766 hasPN[tidx] = true;
5767 if (out.defined_W[aidx]) { w = T(w + out.WN[aidx]); anyw = true; }
5768 }
5769 if (anyw) { out.WN[tidx] = w; out.defined_W[tidx] = true; }
5770 }
5771
5772 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx)
5773 out.defined_T[hidx] = false; // kept undefined for consistency with LQNS
5774
5775 for (std::size_t idx = 1; idx <= N; ++idx) {
5776 out.QN[idx] = UT[idx];
5777 out.defined_Q[idx] = hasUT[idx];
5778 out.UN[idx] = PN[idx];
5779 out.defined_U[idx] = hasPN[idx];
5780 // Idle, not undefined -- the same rule aggregate() applies, and for the
5781 // same reason: an unreachable element reports zero for the measures its
5782 // kind HAS and leaves the ones it never has undefined, so that the
5783 // table's NaN mask survives a disconnected component.
5784 if (ignore[idx]) {
5785 out.UN[idx] = Tzero(); // every kind reports a utilization
5786 out.defined_U[idx] = true;
5787 out.defined_A[idx] = false; // nothing reports an arrival rate on an LQN
5788 const bool host = lqn.type[idx] == LqnElement::HOST;
5789 const bool task = lqn.type[idx] == LqnElement::TASK;
5790 const bool entry = lqn.type[idx] == LqnElement::ENTRY;
5791 out.QN[idx] = Tzero();
5792 out.defined_Q[idx] = !host;
5793 out.RN[idx] = Tzero();
5794 out.defined_R[idx] = !host && !task;
5795 out.WN[idx] = Tzero();
5796 out.defined_W[idx] = !host && !entry;
5797 out.TN[idx] = Tzero();
5798 out.defined_T[idx] = !host;
5799 }
5800 }
5801 return out;
5802 }
5803
5804 void update_metrics(int it) {
5805 if (is_ph_encoding()) {
5806 update_metrics_ph(it);
5807 return;
5808 }
5809 if (lnmethod == "moment3") {
5810 update_metrics_moment_based(it);
5811 return;
5812 }
5813 update_metrics_default(it);
5814 }
5815
5816 /**
5817 * Port of ln_layer_refcell: the 1-based class whose throughput at its
5818 * refstat normalises a residence of class K (0-based) in the layer of
5819 * element IDX, 0 when there is none. A chain merged by refpath holds more
5820 * than one caller task and only ONE reference class, so the per-caller rate
5821 * is read through the layer's normclass; everywhere else it is the chain's
5822 * reference class, which is what normclass would have named anyway.
5823 */
5824 std::size_t layer_refclass(std::size_t idx, const qn::Layer<T>& L, std::size_t k) const {
5825 const auto it = layer_normclass.find(idx);
5826 if (it != layer_normclass.end() && k < it->second.size() && it->second[k] > 0)
5827 return it->second[k];
5828 std::size_t c = 0;
5829 for (std::size_t cc = 0; cc < L.nchains; ++cc)
5830 if (L.chains[cc][k]) c = cc;
5831 return L.refclass[c];
5832 }
5833
5834 void update_metrics_default(int it) {
5835 const std::size_t N = lqn.nidx;
5836 servt.assign(N + 1, Tzero());
5837 residt.assign(N + 1, Tzero());
5838 const int iter_min = std::min(30, int(std::ceil(opt.iter_max / 4.0)));
5839 const bool averaging = averagingstart >= 0 && it >= iter_min;
5840 const int wnd = averaging ? int(it - averagingstart + 1) : 1;
5841
5842 for (const UpdRow& row : servt_map) {
5843 const std::size_t e = std::size_t(idxhash[row.idx]);
5844 const qn::Layer<T>& L = ensemble[e];
5845 const std::size_t k = row.cls - 1;
5846 const std::size_t refclass_c = layer_refclass(row.idx, L, k);
5847 const std::size_t refstat_k = L.classes[k].refstat;
5848
5849 T sv = Tzero(), rs = Tzero(), tp = Tzero();
5850 const T wT = T(Tone() / num_traits<T>::from_int(wnd));
5851 for (int w = 0; w < wnd; ++w) {
5852 const LayerResult<T>& r = results[results.size() - 1 - w][e];
5853 sv += r.RN(row.node - 1, k) * wT;
5854 const T TN_ref = (refclass_c > 0 && refstat_k > 0) ? r.TN(refstat_k - 1, refclass_c - 1) : Tzero();
5855 if (dbl(TN_ref) > GlobalConstants::FineTol)
5856 rs += r.QN(row.node - 1, k) / TN_ref * wT;
5857 else
5858 rs += r.WN(row.node - 1, k) * wT;
5859 tp += r.TN(row.node - 1, k) * wT;
5860 }
5861 servt[row.aidx] = sv;
5862 residt[row.aidx] = rs;
5863 tput[row.aidx] = tp;
5864
5865 // an activity think time is in series with the host demand
5866 const Distrib<T>& at = lqn.actthink[row.aidx];
5867 if (!at.disabled && dbl(at.mean) > GlobalConstants::FineTol) {
5868 servt[row.aidx] = T(servt[row.aidx] + at.mean);
5869 residt[row.aidx] = T(residt[row.aidx] + at.mean);
5870 }
5871
5872 // An activity of an async-only entry takes RN, the response per
5873 // visit, and not the visit-weighted residence: entry selection
5874 // routes the task to each of its entries with a share, and there is
5875 // no caller-side visit ratio here to divide that share back out
5876 // (the sync branch below does exactly that). updateMetricsDefault.m:63-77.
5877 if (async_only_activity(row.aidx)) residt[row.aidx] = servt[row.aidx];
5878
5879 if (relax_omega < 1.0 && it > 1) {
5880 const T om = num_traits<T>::from_double(relax_omega);
5881 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
5882 if (!std::isnan(servt_prev[row.aidx]))
5883 servt[row.aidx] = T(om * servt[row.aidx] + om1 * servt_prev_v[row.aidx]);
5884 if (!std::isnan(residt_prev[row.aidx]))
5885 residt[row.aidx] = T(om * residt[row.aidx] + om1 * residt_prev_v[row.aidx]);
5886 if (!std::isnan(tput_prev[row.aidx]))
5887 tput[row.aidx] = T(om * tput[row.aidx] + om1 * tput_prev_v[row.aidx]);
5888 }
5889 servt_prev[row.aidx] = dbl(servt[row.aidx]);
5890 residt_prev[row.aidx] = dbl(residt[row.aidx]);
5891 tput_prev[row.aidx] = dbl(tput[row.aidx]);
5892 servt_prev_v[row.aidx] = servt[row.aidx];
5893 residt_prev_v[row.aidx] = residt[row.aidx];
5894 tput_prev_v[row.aidx] = tput[row.aidx];
5895
5896 if (servt[row.aidx] > Tzero() && dbl(servt[row.aidx]) <= 1e10)
5897 servtproc[row.aidx] = Distrib<T>::exp_mean(servt[row.aidx]);
5898 tputproc[row.aidx] = Distrib<T>::exp_rate(tput[row.aidx]);
5899 }
5900
5901 // The phase split of servt, updateMetricsDefault.m:120-151. It is
5902 // recomputed from scratch each iteration because servt is; the
5903 // overtaking probability it feeds is computed later, once the entry
5904 // throughputs exist.
5905 if (has_phase2) {
5906 servt_ph1.assign(N + 1, Tzero());
5907 servt_ph2.assign(N + 1, Tzero());
5908 for (std::size_t a = 1; a <= lqn.nacts; ++a) {
5909 const std::size_t aidx = lqn.ashift + a;
5910 if (lqn.actphase[a] == 1)
5911 servt_ph1[aidx] = servt[aidx];
5912 else
5913 servt_ph2[aidx] = servt[aidx];
5914 }
5915 // the ENTRY split is taken once the entry service exists, below: summing the
5916 // activities' own servt left out the synchronous calls each phase makes
5917 }
5918
5919 // throughput of the activities that appear only as client-side classes
5920 for (const UpdRow& row : thinkt_map) {
5921 if (!tputproc[row.aidx].disabled) continue;
5922 const std::size_t e = std::size_t(idxhash[row.idx]);
5923 T tp = Tzero();
5924 const T wT = T(Tone() / num_traits<T>::from_int(wnd));
5925 for (int w = 0; w < wnd; ++w)
5926 tp += results[results.size() - 1 - w][e].TN(row.node - 1, row.cls - 1) * wT;
5927 tput[row.aidx] = tp;
5928 tputproc[row.aidx] = Distrib<T>::exp_rate(tp);
5929 }
5930
5931 // call service and residence times
5932 callservt.assign(lqn.ncalls + 1, Tzero());
5933 callresidt.assign(lqn.ncalls + 1, Tzero());
5934 for (const UpdRow& row : call_map) {
5935 if (row.node <= 1) continue; // a client-side call class contributes none
5936 const std::size_t e = std::size_t(idxhash[row.idx]);
5937 const LayerResult<T>& r = results.back()[e];
5938 callservt[row.aidx] =
5939 T(r.RN(row.node - 1, row.cls - 1) * lqn.callproc_mean[row.aidx]);
5940 // Normalise per chain-reference visit, as residt does. WN divides by the
5941 // class's own reference rate when the layer is open (an INF client task),
5942 // which is per-ENTRY visit, and the entry rescaling in
5943 // resolve_entry_service would then charge the call once per entry.
5944 {
5945 const qn::Layer<T>& L = ensemble[e];
5946 const std::size_t k = row.cls - 1;
5947 const std::size_t refclass_c = layer_refclass(row.idx, L, k);
5948 const std::size_t refstat_k = L.classes[k].refstat;
5949 const T TN_ref = (refclass_c > 0 && refstat_k > 0) ? r.TN(refstat_k - 1, refclass_c - 1) : Tzero();
5950 callresidt[row.aidx] = dbl(TN_ref) > GlobalConstants::FineTol
5951 ? T(r.QN(row.node - 1, k) / TN_ref)
5952 : r.WN(row.node - 1, k);
5953 }
5954 const T rw = region_wait(e, row.cls);
5955 if (rw > Tzero()) {
5956 callservt[row.aidx] = T(callservt[row.aidx] + rw);
5957 callresidt[row.aidx] = T(callresidt[row.aidx] + rw);
5958 }
5959 if (relax_omega < 1.0 && it > 1 && !std::isnan(callservt_prev[row.aidx])) {
5960 const T om = num_traits<T>::from_double(relax_omega);
5961 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
5962 callservt[row.aidx] =
5963 T(om * callservt[row.aidx] + om1 * callservt_prev_v[row.aidx]);
5964 }
5965 callservt_prev[row.aidx] = dbl(callservt[row.aidx]);
5966 callservt_prev_v[row.aidx] = callservt[row.aidx];
5967 callresidt_prev[row.aidx] = dbl(callresidt[row.aidx]);
5968 }
5969
5970 resolve_entry_service();
5971
5972 // The overtaking correction, updateMetricsDefault.m:313-351. It runs
5973 // HERE and not with the split above because it needs the entry
5974 // throughput resolve_entry_service has just produced.
5975 //
5976 // servt keeps both phases -- the server IS busy through phase 2, so the
5977 // utilization is unchanged -- while residt becomes the CALLER's view:
5978 // phase 1 in full, phase 2 only when the caller is actually overtaken.
5979 if (has_phase2) {
5980 // Entry phase split: the phase-2 share of the entry service, calls included,
5981 // scaled onto servt as resolve_entry_service scaled the total
5982 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
5983 const std::size_t eidx = lqn.eshift + e;
5984 servt_ph2[eidx] = (eidx < entry_servt_last.size() && entry_servt_last[eidx] > Tzero())
5985 ? T(entry_servt_ph2[eidx] * servt[eidx] / entry_servt_last[eidx])
5986 : Tzero();
5987 servt_ph1[eidx] = T(servt[eidx] - servt_ph2[eidx]);
5988 }
5989 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
5990 const std::size_t eidx = lqn.eshift + e;
5991 const std::size_t tidx = lqn.parent[eidx];
5992 if (!(dbl(servt_ph2[eidx]) > GlobalConstants::FineTol)) continue;
5993 if (lqn.isref[tidx] || !lqn.issynccaller.any_col(eidx)) {
5994 residt[eidx] = servt[eidx];
5995 continue;
5996 }
5997 T entry_tput = Tzero();
5998 if (dbl(tput[eidx]) > GlobalConstants::FineTol)
5999 entry_tput = tput[eidx];
6000 else if (dbl(tput[tidx]) > GlobalConstants::FineTol)
6001 entry_tput = tput[tidx];
6002 prOvertake[e] =
6003 dbl(entry_tput) > GlobalConstants::FineTol
6004 ? lqn_overtake_prob_markov(lqn, servt, callresidt, tput, eidx,
6005 servt_ph2[eidx])
6006 : Tzero();
6007 residt[eidx] = T(servt_ph1[eidx] + prOvertake[e] * servt_ph2[eidx]);
6008 }
6009 }
6010
6011 for (const UpdRow& row : call_map) {
6012 if (row.node <= 1) continue;
6013 const std::size_t eidx = lqn.callpair_dst[row.aidx];
6014 if (servt[eidx] > Tzero()) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
6015 }
6016 for (const UpdRow& row : call_map) {
6017 if (row.node <= 1) continue;
6018 const std::size_t eidx = lqn.callpair_dst[row.aidx];
6019 if (it == 1) {
6020 callservt[row.aidx] = servt[eidx];
6021 callservtproc[row.aidx] = servtproc[eidx];
6022 } else if (callservt[row.aidx] > Tzero()) {
6023 callservtproc[row.aidx] = Distrib<T>::exp_mean(callservt[row.aidx]);
6024 }
6025 }
6026
6027 // What a synchronous CALLER waits for at a phase-2 target is residt,
6028 // not servt (updateMetricsDefault.m:384-400): the loop just above set it
6029 // from the target's full service, which would charge the caller for
6030 // phase 2 it never waits through.
6031 if (has_phase2) {
6032 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
6033 if (lqn.calltype[cidx] != CallType::SYNC) continue;
6034 const std::size_t target = lqn.callpair_dst[cidx];
6035 if (target <= lqn.eshift || target > lqn.eshift + lqn.nentries) continue;
6036 if (!(dbl(servt_ph2[target]) > GlobalConstants::FineTol)) continue;
6037 const T eff = residt[target]; // servt_ph1 + prOvertake * servt_ph2
6038 if (!(eff > Tzero())) continue;
6039 const T w = T(eff * lqn.callproc_mean[cidx]);
6040 callservt[cidx] = w;
6041 callresidt[cidx] = w;
6042 callservtproc[cidx] = Distrib<T>::exp_mean(w);
6043 }
6044 }
6045 }
6046
6047 // -----------------------------------------------------------------------
6048 // updateMetricsMomentBased, the `moment3` method
6049 // -----------------------------------------------------------------------
6050
6051 /**
6052 * The moments of an empirical CDF, MATLAB's `EmpiricalCDF.getMoments`.
6053 *
6054 * READ EXACTLY AS THE REFERENCE READS IT, and the reading is not the
6055 * obvious one: the weight of a bin is the CDF INCREMENT and its abscissa is
6056 * the MIDPOINT of the two t values, so this is a midpoint quadrature of
6057 * integral x^k dF and not a sum over grid points. Reading the grid as a pmf
6058 * at the right endpoint instead inflates every moment; that exact mistake
6059 * cost a wrong entry mean in the Python port (see _kb/06-solver-catalog.md).
6060 */
6061 static void cdf_moments(const fluid::FluidPassage& c, double& m1, double& m2, double& m3) {
6062 m1 = m2 = m3 = 0.0;
6063 for (std::size_t i = 0; i + 1 < c.t.size(); ++i) {
6064 const double x = 0.5 * (c.t[i + 1] + c.t[i]);
6065 const double w = c.cdf[i + 1] - c.cdf[i];
6066 m1 += x * w;
6067 m2 += x * x * w;
6068 m3 += x * x * x * w;
6069 }
6070 }
6071
6072 /** The per-layer response-time CDFs, computed once and cached. */
6073 const std::vector<std::vector<fluid::FluidPassage>>& layer_cdf(std::size_t e) {
6074 if (cdf_repo[e].empty()) {
6075 // THE LAYER HAS NO ROUTING MATRIX UNTIL IT IS ASKED FOR ONE, and the
6076 // fluid drift is built from `sn.rt` alone: without this the ODE has
6077 // no transitions, the state decays to zero, every passage reports the
6078 // degenerate curve at the origin and every entry ends up with an
6079 // empty convolution and a service time of zero. Same reason as in
6080 // solve_layer_fluid, and the failure is silent in both.
6081 ensemble[e].refresh_rt();
6082 try {
6083 cdf_repo[e] = detail::ln_fluid_cdf_respt(ensemble[e], opt.layer_fluid);
6084 } catch (const std::exception&) {
6085 // The reference falls back to the LAYER's own solver when the
6086 // fluid passage fails, and for the MVA-family layers that is
6087 // the base-class exponential law with the layer's mean -- so
6088 // the fallback here is that law over the last solved averages,
6089 // rather than an empty repo that silently collapses every
6090 // entry law to its bare host demand.
6091 cdf_repo[e] = layer_cdf_exp_fallback(e);
6092 }
6093 }
6094 return cdf_repo[e];
6095 }
6096
6097 /** The base-class exponential CDF over layer e's last solved mean response
6098 * times, the reference's fallback route when the fluid passage fails. */
6099 std::vector<std::vector<fluid::FluidPassage>> layer_cdf_exp_fallback(std::size_t e) {
6100 const qn::NetworkStruct<T>& L = ensemble[e];
6101 std::vector<std::vector<fluid::FluidPassage>> out(
6102 L.nstations, std::vector<fluid::FluidPassage>(L.nclasses));
6103 if (results.empty() || e >= results.back().size()) return out;
6104 const Matrix<T>& RN = results.back()[e].RN;
6105 const std::size_t npts = 100;
6106 for (std::size_t i = 0; i < L.nstations; ++i) {
6107 if (L.stations[i].nodetype == qn::NodeType::Source) continue;
6108 for (std::size_t r = 0; r < L.nclasses; ++r) {
6109 if (L.disabled[i][r]) continue;
6110 const double rn = (i < static_cast<std::size_t>(RN.rows()) &&
6111 r < static_cast<std::size_t>(RN.cols()))
6112 ? dbl(RN(i, r))
6113 : 0.0;
6114 if (!(std::isfinite(rn) && rn > 0.0)) continue;
6115 fluid::FluidPassage& cell = out[i][r];
6116 cell.t.reserve(npts);
6117 cell.cdf.reserve(npts);
6118 for (std::size_t j = 0; j < npts; ++j) {
6119 const double q =
6120 0.001 + (0.999 - 0.001) * static_cast<double>(j) / (npts - 1);
6121 cell.cdf.push_back(q);
6122 cell.t.push_back(-std::log(1.0 - q) * rn);
6123 }
6124 }
6125 }
6126 return out;
6127 }
6128
6129 /**
6130 * task_tput / entry_tput at the host layer of `eidx`.
6131 *
6132 * This is what renormalises a residence time from "one visit to the TASK",
6133 * which is how the layer reports it, to "one visit to the ENTRY", which is
6134 * what an entry metric means. It exists as a helper because applying it
6135 * TWICE is a real and silent failure mode: `moment3` once summed per-visit
6136 * response times and then applied this ratio on top, inflating every entry
6137 * service time by the entries-per-task ratio.
6138 *
6139 * `state` is 0 when the entry's layers are ignored (nothing is assigned at
6140 * all), 1 when it has no synchronous caller (no ratio exists) and 2 when
6141 * `ratio` is set.
6142 */
6143 int entry_visit_ratio(std::size_t eidx, T& ratio) const {
6144 const std::size_t tidx = lqn.parent[eidx];
6145 const std::size_t hidx = lqn.parent[tidx];
6146 if (ignore[tidx] || ignore[hidx] || idxhash[hidx] < 0) return 0;
6147 if (!lqn.issynccaller.any_col(eidx)) return 1;
6148 const std::size_t hl = std::size_t(idxhash[hidx]);
6149 const qn::Layer<T>& L = ensemble[hl];
6150 const LayerResult<T>& r = results.back()[hl];
6151 T task_tput = Tzero(), entry_tput = Tzero();
6152 for (const auto& kv : L.attr_tasks)
6153 if (kv.second == tidx) task_tput += r.TN(L.clientIdx - 1, kv.first - 1);
6154 for (const auto& kv : L.attr_entries)
6155 if (kv.second == eidx) entry_tput += r.TN(L.clientIdx - 1, kv.first - 1);
6156 const T floor = num_traits<T>::from_double(GlobalConstants::Zero);
6157 ratio = T(task_tput / (entry_tput > floor ? entry_tput : floor));
6158 return 2;
6159 }
6160
6161 /** (I - servtmatrix)^-1, the reference's `inv(eye - servtmatrix)`. */
6162 Matrix<T> entry_service_resolvent() const {
6163 const std::size_t dim = lqn.nidx + lqn.ncalls;
6164 Matrix<T> A(dim + 1, dim + 1, Tzero());
6165 for (std::size_t i = 0; i <= dim; ++i) {
6166 A(i, i) = Tone();
6167 for (std::size_t j = 0; j <= dim; ++j) A(i, j) = T(A(i, j) - servtmatrix(i, j));
6168 }
6169 return ::line::inverse(A);
6170 }
6171
6172 /**
6173 * Port of @@SolverLN/updateMetricsMomentBased.m, the `moment3` method.
6174 *
6175 * WHAT IT DOES DIFFERENTLY from the default update. The default feeds MEANS
6176 * between the layers. This fits an APH to the response-time CDF of every
6177 * activity and every call a layer reports, convolves those fits along the
6178 * entry's activity sequence, and reads the entry's law off the convolution:
6179 * a mean AND a distribution, which is what `get_cdf_respt` returns.
6180 *
6181 * TWO PASSES, not one. While the ensemble is still moving (`!hasconverged`)
6182 * the update is mean-based -- forming a CDF per layer per iteration would
6183 * cost a fluid integration per layer per iteration and would be fitting
6184 * noise anyway -- and the distribution is formed ONCE, on the converged
6185 * ensemble. The reference splits it exactly here.
6186 *
6187 * THE NORMALISATION TRAP. Both passes build the entry service from
6188 * RESIDENCE times, which are normalised to one visit to the TASK, and then
6189 * apply the task/entry throughput ratio, which converts that to one visit
6190 * to the ENTRY. Summing per-visit RESPONSE times and applying the ratio as
6191 * well applies the normalisation twice and inflates every multi-entry task
6192 * by its entries-per-task ratio. That was a live defect in all three
6193 * reference codebases until 2026-07-31.
6194 */
6195 void update_metrics_moment_based(int it) {
6196 const std::size_t N = lqn.nidx;
6197 servt.assign(N + 1, Tzero());
6198 residt.assign(N + 1, Tzero());
6199 callservt.assign(lqn.ncalls + 1, Tzero());
6200 callresidt.assign(lqn.ncalls + 1, Tzero());
6201
6202 // ---- what every activity's layer reports, common to both passes ----
6203 for (const UpdRow& row : servt_map) {
6204 const std::size_t e = std::size_t(idxhash[row.idx]);
6205 const qn::Layer<T>& L = ensemble[e];
6206 const LayerResult<T>& r = results.back()[e];
6207 const std::size_t k = row.cls - 1;
6208 std::size_t c = 0;
6209 for (std::size_t cc = 0; cc < L.nchains; ++cc)
6210 if (L.chains[cc][k]) c = cc;
6211 const std::size_t refclass_c = L.refclass[c];
6212 const std::size_t refstat_k = L.classes[k].refstat;
6213 const T TN_ref = (refclass_c > 0 && refstat_k > 0) ? r.TN(refstat_k - 1, refclass_c - 1) : Tzero();
6214
6215 tput[row.aidx] = r.TN(row.node - 1, k);
6216 residt[row.aidx] = dbl(TN_ref) > GlobalConstants::FineTol
6217 ? T(r.QN(row.node - 1, k) / TN_ref)
6218 : r.WN(row.node - 1, k);
6219 if (!hasconverged) {
6220 servt[row.aidx] = r.RN(row.node - 1, k);
6221 servtproc[row.aidx] = Distrib<T>::exp_mean(servt[row.aidx]);
6222 const Distrib<T>& at = lqn.actthink[row.aidx];
6223 if (!at.disabled && dbl(at.mean) > GlobalConstants::FineTol) {
6224 servt[row.aidx] = T(servt[row.aidx] + at.mean);
6225 residt[row.aidx] = T(residt[row.aidx] + at.mean);
6226 servtproc[row.aidx] = Distrib<T>::exp_mean(servt[row.aidx]);
6227 }
6228 // An activity of an async-only entry takes the per-visit
6229 // response, for the same reason as in the default update.
6230 if (async_only_activity(row.aidx)) residt[row.aidx] = servt[row.aidx];
6231 } else {
6232 servtcdf[row.aidx] = layer_cdf(e)[row.node - 1][k];
6233 }
6234 }
6235
6236 for (const UpdRow& row : call_map) {
6237 if (row.node <= 1) continue; // a client-side call class contributes none
6238 const std::size_t e = std::size_t(idxhash[row.idx]);
6239 const LayerResult<T>& r = results.back()[e];
6240 callresidt[row.aidx] = r.WN(row.node - 1, row.cls - 1);
6241 if (!hasconverged)
6242 callservt[row.aidx] =
6243 T(r.RN(row.node - 1, row.cls - 1) * lqn.callproc_mean[row.aidx]);
6244 else
6245 callservtcdf[row.aidx] = layer_cdf(e)[row.node - 1][row.cls - 1];
6246 }
6247
6248 if (!hasconverged)
6249 moment3_means_pass(it);
6250 else
6251 moment3_distribution_pass(it);
6252 }
6253
6254 /** The mean-based pass of moment3, run while the ensemble is still moving. */
6255 void moment3_means_pass(int it) {
6256 const std::size_t dim = lqn.nidx + lqn.ncalls;
6257 std::vector<T> x(dim + 1, Tzero());
6258 for (std::size_t i = 1; i <= lqn.nidx; ++i) x[i] = residt[i];
6259 for (std::size_t c = 1; c <= lqn.ncalls; ++c) x[lqn.nidx + c] = callresidt[c];
6260
6261 // entry_servt = (I - servtmatrix) \ [residt; callresidt]
6262 const Matrix<T> Rinv = entry_service_resolvent();
6263 std::vector<T> entry_servt(dim + 1, Tzero());
6264 for (std::size_t i = 1; i <= dim; ++i) {
6265 T s = Tzero();
6266 for (std::size_t j = 1; j <= dim; ++j)
6267 if (Rinv(i, j) != Tzero()) s += Rinv(i, j) * x[j];
6268 entry_servt[i] = s;
6269 }
6270 for (std::size_t i = 1; i <= lqn.eshift; ++i) entry_servt[i] = Tzero();
6271
6272 // NO forwarding propagation here. `lqn_fwd_rendezvous` has already
6273 // reconnected every forwarding chain reachable from a synchronous call to
6274 // the client that issued the rendezvous (Franks 1999, Sec. 3.3.1), so the
6275 // forwarded service is in the caller's chain before this runs; charging it
6276 // again inflated the caller by exactly the forwarded entry's mean. An
6277 // asynchronous call into a chain is left untouched there by design -- a
6278 // send-no-reply does not block -- so it must not accumulate the forwarded
6279 // service either. See BUGS.md BUG-91.
6280
6281 for (std::size_t e = 1; e <= lqn.nentries; ++e)
6282 servt[lqn.eshift + e] = entry_servt[lqn.eshift + e];
6283
6284 // entry_residt = servtmatrix * [residt; callresidt]
6285 std::vector<T> entry_residt(dim + 1, Tzero());
6286 for (std::size_t i = lqn.eshift + 1; i <= lqn.eshift + lqn.nentries; ++i) {
6287 T s = Tzero();
6288 for (std::size_t j = 1; j <= dim; ++j)
6289 if (servtmatrix(i, j) != Tzero()) s += servtmatrix(i, j) * x[j];
6290 entry_residt[i] = s;
6291 }
6292
6293 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
6294 const std::size_t eidx = lqn.eshift + e;
6295 T ratio = Tone();
6296 const int state = entry_visit_ratio(eidx, ratio);
6297 if (state == 0) continue;
6298 if (state == 1) {
6299 residt[eidx] = entry_residt[eidx];
6300 continue;
6301 }
6302 servt[eidx] = T(entry_servt[eidx] * ratio);
6303 residt[eidx] = T(entry_residt[eidx] * ratio);
6304 }
6305
6306 for (const UpdRow& row : call_map) {
6307 if (row.node <= 1) continue;
6308 const std::size_t eidx = lqn.callpair_dst[row.aidx];
6309 if (servt[eidx] > Tzero()) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
6310 }
6311 for (const UpdRow& row : call_map) {
6312 if (row.node <= 1) continue;
6313 const std::size_t eidx = lqn.callpair_dst[row.aidx];
6314 if (it == 1) {
6315 // A response time is per visit, so the number of calls is 1 here.
6316 callservt[row.aidx] = servt[eidx];
6317 callservtproc[row.aidx] = servtproc[eidx];
6318 } else if (callservt[row.aidx] > Tzero()) {
6319 callservtproc[row.aidx] = Distrib<T>::exp_mean(callservt[row.aidx]);
6320 }
6321 }
6322 }
6323
6324 /**
6325 * The distribution pass of moment3, run once on the converged ensemble.
6326 *
6327 * Every term reachable from an entry is fitted to an APH from the first
6328 * three moments of its response-time CDF, repeated as many times as the
6329 * entry-service matrix says it is visited, and the whole sequence is
6330 * convolved. A FRACTIONAL repetition count -- a call made 1.5 times on
6331 * average -- is realised as a branch between the fitted law and a point
6332 * mass at zero, which is the reference's `aph_simplify(..., pattern 3)`.
6333 */
6334 void moment3_distribution_pass(int it) {
6335 if constexpr (!num_traits<T>::has_transcendental) {
6336 throw UnsupportedError(
6337 "SolverLN: the 'moment3' method fits an APH to a response-time CDF, which needs "
6338 "square roots and a matrix exponential; rerun with --arith double or real");
6339 } else {
6340 const std::size_t dim = lqn.nidx + lqn.ncalls;
6341 const Matrix<T> Rinv = entry_service_resolvent();
6342
6343 // The point mass at zero the fractional branch mixes against.
6344 mam::AphPair<T> zero_law;
6345 zero_law.alpha.push_back(Tone());
6346 zero_law.S = Matrix<T>(1, 1, num_traits<T>::from_double(-GlobalConstants::Immediate));
6347
6348 for (std::size_t en = 1; en <= lqn.nentries; ++en) {
6349 const std::size_t eidx = lqn.eshift + en;
6350 std::vector<mam::AphPair<T>> seq;
6351 for (std::size_t fitidx = 1; fitidx <= dim; ++fitidx) {
6352 if (!(Rinv(eidx, fitidx) > Tzero())) continue;
6353 // Entries themselves are dropped: the entry's own service is
6354 // what is being assembled, and a host or task index carries
6355 // no response-time law of its own.
6356 if (fitidx <= lqn.eshift + lqn.nentries) continue;
6357 const bool is_call = fitidx > lqn.nidx;
6358 const fluid::FluidPassage& curve =
6359 is_call ? callservtcdf[fitidx - lqn.nidx] : servtcdf[fitidx];
6360 double m1 = 0.0, m2 = 0.0, m3 = 0.0;
6361 cdf_moments(curve, m1, m2, m3);
6362
6363 // An activity think time is in series with the host demand,
6364 // so its raw moments convolve with the measured ones.
6365 if (!is_call && !lqn.actthink[fitidx].disabled &&
6366 dbl(lqn.actthink[fitidx].mean) > GlobalConstants::FineTol) {
6367 const Distrib<T>& zt = lqn.actthink[fitidx];
6368 const double t1 = dbl(lang::dist_moment(zt, 1));
6369 const double t2 = dbl(lang::dist_moment(zt, 2));
6370 const double t3 = dbl(lang::dist_moment(zt, 3));
6371 m3 = m3 + 3.0 * m2 * t1 + 3.0 * m1 * t2 + t3;
6372 m2 = m2 + 2.0 * m1 * t1 + t2;
6373 m1 = m1 + t1;
6374 }
6375
6376 // CoarseTol and not FineTol: an Immediate activity has a
6377 // near-zero mean whose APH fit has rates of order 1e8, and
6378 // the matrix exponential of the convolution then does not
6379 // terminate. The reference skips those terms outright.
6380 if (!(m1 > GlobalConstants::CoarseTol)) continue;
6381
6382 const mam::AphFitResult<T> fit = mam::aph_fit(
6383 num_traits<T>::from_double(m1), num_traits<T>::from_double(m2),
6384 num_traits<T>::from_double(m3), 10u,
6385 num_traits<T>::from_double(GlobalConstants::FineTol));
6386 const mam::AphPair<T> law = aph_pair_of_map(fit.aph);
6387
6388 double reps = dbl(Rinv(eidx, fitidx));
6389 // servtmatrix carries 1.0 for a call; the mean NUMBER of
6390 // calls is what says how many times its law is convolved.
6391 if (is_call) reps *= dbl(lqn.callproc_mean[fitidx - lqn.nidx]);
6392 const long whole = static_cast<long>(std::floor(reps));
6393 const double frac = reps - static_cast<double>(whole);
6394 for (long q = 0; q < whole; ++q) seq.push_back(law);
6395 if (frac > 0.0)
6396 seq.push_back(mam::aph_simplify(law, zero_law,
6397 num_traits<T>::from_double(frac),
6398 num_traits<T>::from_double(1.0 - frac),
6400
6401 if (is_call) {
6402 const std::size_t cidx = fitidx - lqn.nidx;
6403 callservt[cidx] = num_traits<T>::from_double(m1);
6404 callservtproc[cidx] = Distrib<T>::exp_mean(callservt[cidx]);
6405 } else {
6406 servt[fitidx] = num_traits<T>::from_double(m1);
6407 servtproc[fitidx] = Distrib<T>::exp_mean(servt[fitidx]);
6408 }
6409 }
6410
6411 if (seq.empty()) {
6412 servt[eidx] = Tzero();
6413 continue;
6414 }
6415 const mam::AphPair<T> entry_law = mam::aph_convseq(seq);
6416 entryproc[en] = entry_law;
6417 servt[eidx] = Distrib<T>::ph_moment(entry_law.alpha, entry_law.S, 1);
6418 servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
6419 entrycdfrespt[en] = aph_eval_cdf(entry_law);
6420 }
6421
6422 // NO forwarding propagation here, for the reason given at the
6423 // entry_servt assembly above. See BUGS.md BUG-91.
6424
6425 // entry_residt = servtmatrix * [residt; callresidt], then the same
6426 // task/entry renormalisation the mean pass applies.
6427 std::vector<T> x(dim + 1, Tzero());
6428 for (std::size_t i = 1; i <= lqn.nidx; ++i) x[i] = residt[i];
6429 for (std::size_t c = 1; c <= lqn.ncalls; ++c) x[lqn.nidx + c] = callresidt[c];
6430 for (std::size_t en = 1; en <= lqn.nentries; ++en) {
6431 const std::size_t eidx = lqn.eshift + en;
6432 T s = Tzero();
6433 for (std::size_t j = 1; j <= dim; ++j)
6434 if (servtmatrix(eidx, j) != Tzero()) s += servtmatrix(eidx, j) * x[j];
6435 T ratio = Tone();
6436 const int state = entry_visit_ratio(eidx, ratio);
6437 if (state == 0) continue;
6438 residt[eidx] = state == 2 ? T(s * ratio) : s;
6439 }
6440
6441 if (it == 1)
6442 for (const UpdRow& row : call_map) {
6443 if (row.node <= 1) continue;
6444 const std::size_t eidx = lqn.callpair_dst[row.aidx];
6445 callservt[row.aidx] = servt[eidx];
6446 callservtproc[row.aidx] = Distrib<T>::exp_mean(servt[eidx]);
6447 }
6448
6449 // The entry servt is now the MEAN OF AN APH CONVOLUTION, not a sum
6450 // of residence times, which the interlock rescale in
6451 // `update_populations` has to know. See BUGS.md BUG-97.
6452 moment_pass_done = true;
6453 }
6454 }
6455
6456 /**
6457 * (alpha, S) of an APH handed back by aph_fit as a (D0, D1) pair.
6458 *
6459 * D1(i,j) = (-D0 e)_i alpha_j by construction, so alpha is any row of D1
6460 * divided by that row's exit rate; the first row with a positive exit rate
6461 * is taken. It is not recovered from the stationary phase distribution,
6462 * which is a different vector and would silently refit the law.
6463 */
6464 static mam::AphPair<T> aph_pair_of_map(const mam::Map<T>& m) {
6465 mam::AphPair<T> out;
6466 const std::size_t n = m.D0.rows();
6467 out.S = m.D0;
6468 out.alpha.assign(n, Tzero());
6469 for (std::size_t i = 0; i < n; ++i) {
6470 T rowsum = Tzero();
6471 for (std::size_t j = 0; j < n; ++j) rowsum += m.D1(i, j);
6472 if (!(dbl(rowsum) > GlobalConstants::Zero)) continue;
6473 for (std::size_t j = 0; j < n; ++j) out.alpha[j] = T(m.D1(i, j) / rowsum);
6474 return out;
6475 }
6476 // No phase can absorb: the law is degenerate, so it enters phase one and
6477 // stays there, which is what a zero alpha would NOT say.
6478 out.alpha[0] = Tone();
6479 return out;
6480 }
6481
6482 /**
6483 * F(t) = 1 - alpha exp(S t) e on the reference's grid, `APH.evalCDF` with
6484 * no argument: 500 points over [0, mean + 10 sigma].
6485 */
6486 static LnCdf aph_eval_cdf(const mam::AphPair<T>& law) {
6487 LnCdf out;
6488 const double m1 = dbl(Distrib<T>::ph_moment(law.alpha, law.S, 1));
6489 const double m2 = dbl(Distrib<T>::ph_moment(law.alpha, law.S, 2));
6490 const double var = m2 - m1 * m1;
6491 const double sigma = var > 0.0 ? std::sqrt(var) : 0.0;
6492 const double tmax = m1 + 10.0 * sigma;
6493 const std::size_t P = 500;
6494 out.t.resize(P);
6495 out.cdf.resize(P);
6496 for (std::size_t k = 0; k < P; ++k) {
6497 const double t = tmax * static_cast<double>(k) / static_cast<double>(P - 1);
6498 out.t[k] = t;
6499 Matrix<T> St(law.S.rows(), law.S.cols(), Tzero());
6500 for (std::size_t i = 0; i < law.S.rows(); ++i)
6501 for (std::size_t j = 0; j < law.S.cols(); ++j)
6502 St(i, j) = T(law.S(i, j) * num_traits<T>::from_double(t));
6503 const Matrix<T> E = ::line::expm(St);
6504 T surv = Tzero();
6505 for (std::size_t i = 0; i < E.rows(); ++i)
6506 for (std::size_t j = 0; j < E.cols(); ++j) surv += law.alpha[i] * E(i, j);
6507 out.cdf[k] = 1.0 - dbl(surv);
6508 }
6509 return out;
6510 }
6511
6512 /**
6513 * The AND-join completion times, and how much they undercut the serial sum.
6514 *
6515 * The branches of an AND fork run concurrently, so the time to clear the
6516 * join is the k-th smallest of the branch completion times, k being the
6517 * quorum. The entry-service reachability matrix cannot express that -- it
6518 * charges every activity of every branch to the entry, i.e. it serialises
6519 * them -- so the difference is recorded per join and applied as a
6520 * correction to any entry that reaches it.
6521 *
6522 * Branch times are taken as exponential, so the variance is the square of
6523 * the mean; that is the reference's assumption, not an approximation added
6524 * here.
6525 */
6526 std::vector<T> join_excess() const {
6527 std::vector<T> excess(lqn.nidx + 1, Tzero());
6528 std::vector<std::size_t> joined;
6529 for (std::size_t tail = 1; tail <= lqn.nidx; ++tail) {
6530 if (lqn.actpretype[tail] != PrecedenceType::PRE_AND) continue;
6531 for (std::size_t sx : lqn.graph.succ(tail))
6532 if (sx > lqn.ashift && sx <= lqn.ashift + lqn.nacts) joined.push_back(sx);
6533 }
6534 std::sort(joined.begin(), joined.end());
6535 joined.erase(std::unique(joined.begin(), joined.end()), joined.end());
6536 if (joined.empty()) return excess;
6537
6538 fj::LqnBranchView<T> view;
6539 view.graph = Matrix<T>(lqn.nidx, lqn.nidx, Tzero());
6540 for (std::size_t i = 1; i <= lqn.nidx; ++i)
6541 for (const auto& e : lqn.graph.row[i]) view.graph(i - 1, e.first - 1) = e.second;
6542 view.ashift = lqn.ashift;
6543 view.nacts = lqn.nacts;
6544 view.actposttype.assign(lqn.nidx, 0);
6545 for (std::size_t i = 1; i <= lqn.nidx; ++i)
6546 view.actposttype[i - 1] = static_cast<int>(lqn.actposttype[i]);
6547
6548 for (std::size_t aidx : joined) {
6549 const std::vector<std::vector<std::size_t>> branches =
6550 fj::fj_branch_members(view, aidx);
6551 if (branches.empty()) continue;
6552 std::vector<T> means;
6553 for (const auto& br : branches) {
6554 T s = Tzero();
6555 for (std::size_t a : br) {
6556 s += residt[a];
6557 // A branch activity with an Immediate host demand does all its work
6558 // in a rendezvous, so residt alone leaves this correction inert.
6559 if (a < lqn.callsof.size())
6560 for (std::size_t cidx : lqn.callsof[a])
6561 if (cidx < lqn.calltype.size() && lqn.calltype[cidx] == CallType::SYNC
6562 && cidx < callresidt.size())
6563 s += callresidt[cidx];
6564 }
6565 means.push_back(s);
6566 }
6567 if (means.size() == 1) continue; // a single branch cannot overlap
6568 std::size_t quorum = means.size();
6569 if (lqn.actquorum[aidx] >= 1 && lqn.actquorum[aidx] <= means.size())
6570 quorum = lqn.actquorum[aidx];
6571 std::vector<T> vars;
6572 for (const T& m2 : means) vars.push_back(T(m2 * m2));
6573 T serial = Tzero();
6574 for (const T& m2 : means) serial += m2;
6575 if constexpr (num_traits<T>::has_transcendental) {
6576 const fj::FJQuorumMomentsResult<T> q = fj::fj_quorum_moments(means, vars, quorum);
6577 excess[aidx] = T(q.m - serial);
6578 } else {
6579 throw UnsupportedError(
6580 "SolverLN: the AND-join completion time is a k-th order statistic fitted "
6581 "through a three-point distribution, which needs a square root; use the "
6582 "double or real backend for a model with an AND join");
6583 }
6584 }
6585 return excess;
6586 }
6587
6588 /** The block that turns activity residence times into entry service times. */
6589 void resolve_entry_service() {
6590 const std::size_t dim = lqn.nidx + lqn.ncalls;
6591 std::vector<T> x(dim + 1, Tzero());
6592 for (std::size_t i = 1; i <= lqn.nidx; ++i) x[i] = residt[i];
6593 for (std::size_t c = 1; c <= lqn.ncalls; ++c) x[lqn.nidx + c] = callresidt[c];
6594 std::vector<T> entry_servt(dim + 1, Tzero());
6595 for (std::size_t i = 1; i <= dim; ++i) {
6596 T s = Tzero();
6597 for (std::size_t j = 1; j <= dim; ++j)
6598 if (servtmatrix(i, j) != Tzero()) s += servtmatrix(i, j) * x[j];
6599 entry_servt[i] = s;
6600 }
6601
6602 // replace each reachable join's serialised branch sum by its concurrent
6603 // completion time
6604 const std::vector<T> excess = join_excess();
6605 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
6606 const std::size_t eidx = lqn.eshift + e;
6607 for (std::size_t aidx = 1; aidx <= lqn.nidx; ++aidx) {
6608 if (excess[aidx] == Tzero()) continue;
6609 if (servtmatrix(eidx, aidx) > Tzero())
6610 entry_servt[eidx] = T(entry_servt[eidx] + excess[aidx]);
6611 }
6612 if (entry_servt[eidx] < Tzero()) entry_servt[eidx] = Tzero();
6613 }
6614
6615 // A SetupTask's cold start is charged HERE, to the entry, and with the
6616 // probability that the thread was actually found powered down. It is not
6617 // host demand, so it belongs to no activity's residence -- reporting it
6618 // there put RespT(A2) at 1.29479 against 0.333178 from LDES on lqn_setup,
6619 // the bare demand. The probability is the one 'srvn.ph' uses through
6620 // ph_setup_prob, so the two encodings charge the same thing
6621 // (updateMetricsDefault.m, lqn_setup_charge.m).
6622 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
6623 const std::size_t eidx = lqn.eshift + e;
6624 const double c = setup_charge(lqn.parent[eidx]);
6625 if (c != 0.0)
6626 entry_servt[eidx] = T(entry_servt[eidx] + num_traits<T>::from_double(c));
6627 }
6628
6629 // The phase-2 part of each entry's service: phase-2 activities and the calls they
6630 // make, through the same reachability. The overtaking block splits servt with it.
6631 if (has_phase2) {
6632 std::vector<T> x2(dim + 1, Tzero());
6633 for (std::size_t a = 1; a <= lqn.nacts; ++a)
6634 if (lqn.actphase[a] > 1) x2[lqn.ashift + a] = x[lqn.ashift + a];
6635 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
6636 const std::size_t src = lqn.callpair_src[c];
6637 if (src > lqn.ashift && src <= lqn.ashift + lqn.nacts && lqn.actphase[src - lqn.ashift] > 1)
6638 x2[lqn.nidx + c] = x[lqn.nidx + c];
6639 }
6640 entry_servt_ph2.assign(dim + 1, Tzero());
6641 for (std::size_t i = lqn.eshift + 1; i <= dim; ++i) {
6642 T s2 = Tzero();
6643 for (std::size_t j = 1; j <= dim; ++j)
6644 if (servtmatrix(i, j) != Tzero()) s2 += servtmatrix(i, j) * x2[j];
6645 entry_servt_ph2[i] = s2;
6646 }
6647 entry_servt_last = entry_servt;
6648 }
6649
6650 // ResidT is normalised so the TASK has one visit; the entries need one
6651 // visit each, hence the throughput ratio below
6652 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
6653 const std::size_t eidx = lqn.eshift + e;
6654 const std::size_t tidx = lqn.parent[eidx];
6655 const std::size_t hidx = lqn.parent[tidx];
6656 if (ignore[tidx] || ignore[hidx]) continue;
6657 if (idxhash[hidx] < 0) continue;
6658 const bool has_sync = lqn.issynccaller.any_col(eidx);
6659 if (!has_sync) {
6660 servt[eidx] = entry_servt[eidx];
6661 residt[eidx] = entry_servt[eidx];
6662 continue;
6663 }
6664 const std::size_t hl = std::size_t(idxhash[hidx]);
6665 const qn::Layer<T>& L = ensemble[hl];
6666 const LayerResult<T>& r = results.back()[hl];
6667 T task_tput = Tzero(), entry_tput = Tzero();
6668 for (const auto& kv : L.attr_tasks)
6669 if (kv.second == tidx) task_tput += r.TN(L.clientIdx - 1, kv.first - 1);
6670 for (const auto& kv : L.attr_entries)
6671 if (kv.second == eidx) entry_tput += r.TN(L.clientIdx - 1, kv.first - 1);
6672 if (dbl(entry_tput) > GlobalConstants::Zero) {
6673 servt[eidx] = T(entry_servt[eidx] * task_tput / entry_tput);
6674 residt[eidx] = servt[eidx];
6675 // the same ratio the other way round, the entry's invocations per
6676 // task invocation, which a refpath hop stage converts its call terms by
6677 if (dbl(task_tput) > GlobalConstants::Zero)
6678 entryvisits[eidx] = T(entry_tput / task_tput);
6679 } else {
6680 servt[eidx] = entry_servt[eidx];
6681 residt[eidx] = entry_servt[eidx];
6682 }
6683 }
6684 }
6685
6686 // -----------------------------------------------------------------------
6687 // updatePopulations (interlock correction)
6688 // -----------------------------------------------------------------------
6689 /**
6690 * True when the layer engine applies Eq. (4.7) inside its own MVA. A layer whose MVA path
6691 * has no interlock term would be moved to another algorithm by the matrix alone: exact
6692 * multiserver MVA would become AMVA, the product-form kernels would become the
6693 * load-dependent forward step. That swap is worth far more than the correction it carries,
6694 * and on a layer sitting near a bifurcation it turns the LN iteration into a limit cycle.
6695 * Such a layer keeps the residt scaling instead.
6696 */
6697 bool layer_takes_interlock(std::size_t e) const {
6698 return opt.layer_solver == "mva" && e < ensemble.size() &&
6699 mva::mva_carries_interlock(ensemble[e], opt.layer);
6700 }
6701
6702 /**
6703 * Class-level interlock matrix of one host layer. IL[r][s] is the share of the class-s
6704 * queue that a class-r arrival must not see at the host. It is kept CLASS-indexed, not
6705 * chain-indexed, so that a later chain refresh cannot leave it stale: the analyzer
6706 * aggregates it to chains against the struct it is about to solve. Two classes are
6707 * interlocked only if BOTH their tasks are, which is the 0/1 relation ir_mkj of Eq. (5);
6708 * the diagonal stays zero, since a request always sees its own class in full. The entry
6709 * is the Eq. (5) product Pr(IL_ms)*IR_ms*IR_mr, asymmetric in (r,s) because Pr(IL) is
6710 * taken from the QUEUED class s, so that the layer's ILw(r,s) = 1-IL(r,s) is the
6711 * lower-level adjustment rate r_lower. An empty result keeps the layer on the plain MVA
6712 * path.
6713 */
6714 std::vector<std::vector<double>> build_layer_interlock(std::size_t e,
6715 const std::vector<std::size_t>& hts,
6716 const std::vector<T>& prIL,
6717 const std::vector<T>& PrIL) const {
6718 const qn::Layer<T>& L = ensemble[e];
6719 const std::size_t R = L.nclasses;
6720 std::vector<double> cls_ir(R, 0.0); // IR
6721 std::vector<double> cls_pr(R, 0.0); // Pr(IL)
6722 auto stamp = [&](std::size_t classIdx, std::size_t tidx) {
6723 if (classIdx < 1 || classIdx > R) return;
6724 for (std::size_t i = 0; i < hts.size(); ++i)
6725 if (hts[i] == tidx) {
6726 cls_ir[classIdx - 1] = dbl(prIL[i]);
6727 cls_pr[classIdx - 1] = dbl(PrIL[i]);
6728 }
6729 };
6730 for (const auto& a : L.attr_tasks) stamp(a.first, a.second);
6731 for (const auto& a : L.attr_entries) stamp(a.first, lqn.parent[a.second]);
6732 for (const auto& a : L.attr_activities) stamp(a.first, lqn.parent[a.second]);
6733 for (const auto& a : L.attr_calls) stamp(a[0], lqn.parent[a[2]]);
6734
6735 std::vector<std::vector<double>> IL(R, std::vector<double>(R, 0.0));
6736 bool any = false;
6737 for (std::size_t r = 0; r < R; ++r) {
6738 if (!(cls_ir[r] > GlobalConstants::FineTol)) continue;
6739 for (std::size_t sIl = 0; sIl < R; ++sIl) {
6740 if (sIl == r || !(cls_ir[sIl] > GlobalConstants::FineTol)) continue;
6741 IL[r][sIl] = cls_pr[sIl] * cls_ir[sIl] * cls_ir[r];
6742 if (IL[r][sIl] > GlobalConstants::FineTol) any = true;
6743 }
6744 }
6745 return any ? IL : std::vector<std::vector<double>>();
6746 }
6747
6748 /**
6749 * Interlock probability for one (client, server) pair, as {IR, Pr(IL)} of Li and Franks,
6750 * "An improved interlocking correction for decomposition of layered queueing networks",
6751 * CCECE 2015, Eqs. (3) and (4). isProcessorHost selects the m' rule of lqns
6752 * Interlock::ilrate_pril_flow: at a PROCESSOR the common-source population is doubled
6753 * above 3 customers and squared at or below it, which is what turns m = 4 into the
6754 * pril = 1/8 its trace reports. The two factors are multiplied into the Eq. (5) rate by
6755 * build_layer_interlock, so neither carries the source count on its own -- that lives in
6756 * m'. This replaces the superseded (n_s-1)/n_s discount of Franks (1999), Eq. (4.7).
6757 */
6758 std::pair<T, T> interlock_prob(std::size_t client_tidx, std::size_t server_idx,
6759 bool isProcessorHost) const {
6760 const std::vector<std::size_t>& common = il_common_entries[server_idx];
6761 const double nsrc = il_num_sources[server_idx];
6762 if (nsrc == 0.0 || common.empty()) return std::make_pair(Tzero(), Tzero());
6763 const std::vector<std::size_t>& allsrc = il_src_all[server_idx];
6764 T sum_flow = Tzero();
6765 T sum_pril = Tzero();
6766 for (std::size_t ce : common) {
6767 const std::size_t srcTask = lqn.parent[ce];
6768 const std::size_t cen = ce - lqn.eshift;
6769 // population of this common source, in customer copies
6770 double m_src = lqn.mult[srcTask];
6771 if (!std::isfinite(m_src) || m_src < 1.0) m_src = 1.0;
6772 double m_eff = m_src;
6773 if (isProcessorHost) m_eff = (m_src > 3.0) ? (m_src + m_src) : (m_src * m_src);
6774 for (std::size_t dst : lqn.entriesof[client_tidx]) {
6775 const std::size_t dn = dst - lqn.eshift;
6776 if (dn < 1 || dn > lqn.nentries) continue;
6777 if (!(il_all(cen, dn) > Tzero())) continue;
6778 T ce_tput = tput[ce];
6779 if (!(dbl(ce_tput) > GlobalConstants::FineTol) && !lqn.actsof[ce].empty())
6780 ce_tput = tput[lqn.actsof[ce][0]];
6781 if (!(dbl(ce_tput) > GlobalConstants::FineTol)) ce_tput = tput[srcTask];
6782 if (!(dbl(ce_tput) > GlobalConstants::FineTol)) continue;
6783 // phase-2 entries are refused upstream, so every source is all-phase
6784 if (std::find(allsrc.begin(), allsrc.end(), srcTask) != allsrc.end()) {
6785 const T contrib = T(ce_tput * il_all(cen, dn));
6786 sum_flow += contrib;
6787 sum_pril += T(contrib / num_traits<T>::from_double(m_eff));
6788 }
6789 }
6790 }
6791 T client_tput = tput[client_tidx];
6792 if (!(dbl(client_tput) > GlobalConstants::FineTol)) {
6793 for (std::size_t e : lqn.entriesof[client_tidx]) {
6794 T et = tput[e];
6795 if (!(dbl(et) > GlobalConstants::FineTol) && !lqn.actsof[e].empty())
6796 et = tput[lqn.actsof[e][0]];
6797 client_tput = T(client_tput + et);
6798 }
6799 }
6800 if (!(dbl(client_tput) > GlobalConstants::FineTol))
6801 return std::make_pair(Tzero(), Tzero());
6802 const T flow = sum_flow < client_tput ? sum_flow : client_tput;
6803 T IR = T(flow / client_tput);
6804 if (IR > Tone()) IR = Tone();
6805 if (dbl(IR) < 0.0) IR = Tzero();
6806 T pr = Tzero();
6807 if (dbl(sum_flow) > GlobalConstants::FineTol) pr = T(sum_pril / sum_flow);
6808 if (pr > Tone()) pr = Tone();
6809 if (dbl(pr) < 0.0) pr = Tzero();
6810 return std::make_pair(IR, pr);
6811 }
6812
6813 void update_populations(int) {
6814 const std::vector<T> residt_orig = residt;
6815 const std::vector<T> callresidt_orig = callresidt;
6816 bool adjusted = false;
6817
6818 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
6819 if (lqn.calltype[cidx] != CallType::SYNC) continue;
6820 const std::size_t dst = lqn.callpair_dst[cidx];
6821 const std::size_t server_tidx = lqn.parent[dst];
6822 std::size_t server = 0;
6823 if (server_tidx <= NT() && !il_common_entries[server_tidx].empty()) {
6824 server = server_tidx;
6825 } else if (server_tidx > lqn.tshift) {
6826 const std::size_t h = lqn.parent[server_tidx];
6827 if (h >= 1 && h <= NT() && !il_common_entries[h].empty()) server = h;
6828 }
6829 if (server == 0) continue;
6830 const std::size_t client_tidx = lqn.parent[lqn.callpair_src[cidx]];
6831 // This path serves a TASK, not a processor, so the m' rule of Li/lqns leaves the
6832 // source population alone; the product IR*Pr(IL) reproduces the scalar this
6833 // branch used before.
6834 const std::pair<T, T> ilp = interlock_prob(client_tidx, server, false);
6835 const T prIL = T(ilp.first * ilp.second);
6836 if (!(dbl(prIL) > GlobalConstants::FineTol)) continue;
6837 const T S = servt[dst];
6838 const T cm = lqn.callproc_mean[cidx];
6839 if (!(cm > Tzero()) || !(callservt[cidx] > Tzero())) continue;
6840 const T RN = T(callservt[cidx] / cm);
6841 const T W = RN > S ? T(RN - S) : Tzero();
6842 if (!(dbl(W) > GlobalConstants::FineTol)) continue;
6843 const T RN_adj = T(S + (Tone() - prIL) * W);
6844 const T scale = T(RN_adj / RN);
6845 callservt[cidx] = T(callservt[cidx] * scale);
6846 callresidt[cidx] = T(callresidt[cidx] * scale);
6847 if (callservt[cidx] > Tzero())
6848 callservtproc[cidx] = Distrib<T>::exp_mean(callservt[cidx]);
6849 adjusted = true;
6850 }
6851
6852 // Every layer starts the pass without a matrix, so a host that stops being
6853 // interlocked does not keep the previous iteration's correction alive.
6854 layer_interlock.assign(ensemble.size(), {});
6855 for (std::size_t h = 1; h <= lqn.nhosts; ++h) {
6856 if (il_common_entries[h].empty()) continue;
6857 const std::vector<std::size_t>& hts = lqn.tasksof[h];
6858 std::vector<T> prIL(hts.size(), Tzero()), PrIL(hts.size(), Tzero()),
6859 tutil(hts.size(), Tzero());
6860 for (std::size_t i = 0; i < hts.size(); ++i) {
6861 // The host of a task layer is a PROCESSOR, which is what selects the m' rule.
6862 const std::pair<T, T> ilp = interlock_prob(hts[i], h, true);
6863 prIL[i] = ilp.first;
6864 PrIL[i] = ilp.second;
6865 for (std::size_t e : lqn.entriesof[hts[i]])
6866 for (std::size_t a : lqn.actsof[e]) tutil[i] += tput[a] * lqn.hostdem[a].mean;
6867 }
6868 T Utot = Tzero(), Uil = Tzero();
6869 for (std::size_t i = 0; i < hts.size(); ++i) {
6870 Utot += tutil[i];
6871 if (dbl(prIL[i]) > GlobalConstants::FineTol) Uil += tutil[i];
6872 }
6873 if (!(dbl(Utot) > GlobalConstants::FineTol) || !(dbl(Uil) > GlobalConstants::FineTol))
6874 continue;
6875 const T frac = T(Uil / Utot);
6876
6877 // When the layer engine carries Eq. (4.7) inside its own MVA, the interlock goes
6878 // to the layer as a class-level matrix and the residence times are left
6879 // untouched. Scaling them here as well would remove the same waiting twice, and
6880 // would still leave the layer's own THROUGHPUT uncorrected, which is what breaks
6881 // flow balance across a call: the reported task rate then comes from a cycle time
6882 // the correction has already shortened elsewhere.
6883 if (idxhash[h] >= 0 && layer_takes_interlock(std::size_t(idxhash[h]))) {
6884 const std::size_t e = std::size_t(idxhash[h]);
6885 layer_interlock[e] = build_layer_interlock(e, hts, prIL, PrIL);
6886 continue;
6887 }
6888
6889 for (std::size_t i = 0; i < hts.size(); ++i) {
6890 if (!(dbl(prIL[i]) > GlobalConstants::FineTol)) continue;
6891 // Weight by the share of host utilization that is interlocked. The rate
6892 // is the SAME Eq. (5) product IR*Pr(IL) that pass 1 applies to a call and
6893 // that build_layer_interlock puts in the layer matrix -- IR alone is a
6894 // flow SHARE, ~1 whenever a layer has a single common source, and using
6895 // it here removed the whole processor queueing rather than the
6896 // interlocked part of it, which broke flow balance across a call.
6897 const T eff = T(prIL[i] * PrIL[i] * frac);
6898 for (std::size_t e : lqn.entriesof[hts[i]])
6899 for (std::size_t a : lqn.actsof[e]) {
6900 const T D = lqn.hostdem[a].mean;
6901 if (D > Tzero() && dbl(residt[a] - D) > GlobalConstants::FineTol) {
6902 residt[a] = T(D + (Tone() - eff) * (residt[a] - D));
6903 adjusted = true;
6904 }
6905 }
6906 }
6907 }
6908
6909 if (!adjusted) return;
6910 const std::size_t dim = lqn.nidx + lqn.ncalls;
6911 auto esum = [&](const std::vector<T>& rs, const std::vector<T>& cr, std::size_t i) {
6912 T s = Tzero();
6913 for (std::size_t j = 1; j <= dim; ++j) {
6914 if (servtmatrix(i, j) == Tzero()) continue;
6915 s += servtmatrix(i, j) * (j <= lqn.nidx ? rs[j] : cr[j - lqn.nidx]);
6916 }
6917 return s;
6918 };
6919 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
6920 const std::size_t eidx = lqn.eshift + e;
6921 const T oldv = esum(residt_orig, callresidt_orig, eidx);
6922 if (!(dbl(oldv) > GlobalConstants::FineTol)) continue;
6923 const T ratio = T(esum(residt, callresidt, eidx) / oldv);
6924 // The entry servt is rescaled only when it was itself assembled from
6925 // these residence times, which is the mean-based path. After the
6926 // moment3 distribution pass it is the mean of an APH convolution of
6927 // the activities' own response laws, and a ratio of residence-time
6928 // sums is not a correction to it: applying it multiplied the entry
6929 // law by the entry's visit ratio and reported a service time BELOW
6930 // that of the single activity the entry contains. The residence
6931 // times keep their correction either way. See BUGS.md BUG-97.
6932 if (!moment_pass_done) {
6933 servt[eidx] = T(servt[eidx] * ratio);
6934 if (servt[eidx] > Tzero()) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
6935 }
6936 residt[eidx] = T(residt[eidx] * ratio);
6937 }
6938 }
6939
6940 // -----------------------------------------------------------------------
6941 // Declared think time of a task as it enters the thread cycle: the value for
6942 // a REFERENCE task, zero for any other.
6943 //
6944 // A think time is an attribute of the closed customer population a reference
6945 // task stands for, and it is what separates one request of that population
6946 // from the next. On a served task it has no such meaning, and charging it
6947 // per request throttles the task: lqn_basic's T3 has 25 threads and a
6948 // declared think time of 4, and reading it as a per-request delay caps it at
6949 // 25/(4+0.02) = 6.219 completions per second. Three independent oracles put
6950 // the rate at five calls per caller request instead -- lqsim 66.5, LDES
6951 // 66.955, lqns 75.6. See _kb/06-solver-catalog.md (LN section).
6952 T ref_think_time(std::size_t tidx) const {
6953 if (tidx >= lqn.isref.size() || !lqn.isref[tidx]) return Tzero();
6954 if (lqn.think[tidx].disabled) return Tzero();
6955 return lqn.think[tidx].mean;
6956 }
6957
6958 // updateThinkTimes
6959 // -----------------------------------------------------------------------
6960 /**
6961 * Mean time per request that a thread of task TIDX stays busy after replying, i.e. servt - residt
6962 * averaged over its phase-2 entries by their throughput (phase2Tail in updateThinkTimes.m).
6963 */
6964 T phase2_tail(std::size_t tidx) const {
6965 T tail = Tzero();
6966 if (!has_phase2) return tail;
6967 T wtot = Tzero();
6968 const T fine = num_traits<T>::from_double(GlobalConstants::FineTol);
6969 for (std::size_t eidx : lqn.entriesof[tidx]) {
6970 if (servt_ph2[eidx] <= fine) continue;
6971 T w = tput[eidx];
6972 if (w <= fine) w = Tone();
6973 const T d = T(servt[eidx] - residt[eidx]);
6974 tail += T(w * (d > Tzero() ? d : Tzero()));
6975 wtot += w;
6976 }
6977 if (wtot > fine) tail = T(tail / wtot);
6978 return tail;
6979 }
6980
6981 void update_think_times(int it) {
6982 // Under "srvn.ph" a caller reaches the server once per invocation, so the
6983 // station rate is not the task's invocation rate
6984 if (is_ph_encoding()) {
6985 update_think_times_ph(it);
6986 return;
6987 }
6988 thinktproc.assign(lqn.nidx + 1, Distrib<T>::disabled_dist());
6989 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
6990 const std::size_t tidx = lqn.tshift + t;
6991 // only a REFERENCE task's think time is a per-request delay
6992 const T ztask = ref_think_time(tidx);
6993 if (idxhash[tidx] < 0) {
6994 // A task reached only by an entry arrival still has a cycle: its threads
6995 // are driven by the stream. build_layer drops the open class for it so
6996 // the chain can be closed on the known rate here.
6997 const double arvrate = open_arrival_rate_of(tidx);
6998 if (arvrate > GlobalConstants::FineTol) {
6999 double nja = 0.0;
7000 for (std::size_t c = 1; c <= NT(); ++c) nja = std::max(nja, njobs(tidx, c));
7001 if (!(nja > 0.0)) nja = lqn.maxmult[tidx];
7002 T hres = Tzero();
7003 for (std::size_t eidx : lqn.entriesof[tidx])
7004 for (std::size_t aidx : lqn.actsof[eidx])
7005 if (!std::isnan(dbl(residt[aidx]))) hres += residt[aidx];
7006 const T floora = num_traits<T>::from_double(GlobalConstants::Zero);
7007 T za = T(num_traits<T>::from_double(nja / arvrate) - hres - ztask);
7008 if (za < floora) za = floora;
7009 if (relax_omega < 1.0 && it > 1 && !std::isnan(thinkt_prev[tidx])) {
7010 const T om = num_traits<T>::from_double(relax_omega);
7011 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
7012 za = T(om * za + om1 * thinkt_prev_v[tidx]);
7013 }
7014 tput[tidx] = num_traits<T>::from_double(arvrate);
7015 thinkt[tidx] = za;
7016 thinkt_prev[tidx] = dbl(za);
7017 thinkt_prev_v[tidx] = za;
7018 thinktproc[tidx] = Distrib<T>::exp_mean(T(za + ztask));
7019 continue;
7020 }
7021 thinkt[tidx] = num_traits<T>::from_double(GlobalConstants::FineTol);
7022 thinktproc[tidx] = Distrib<T>::immediate();
7023 continue;
7024 }
7025 const std::size_t e = std::size_t(idxhash[tidx]);
7026 const qn::Layer<T>& L = ensemble[e];
7027 const LayerResult<T>& r = results.back()[e];
7028 double nj = 0.0;
7029 for (std::size_t c = 1; c <= NT(); ++c) nj = std::max(nj, njobs(tidx, c));
7030 // the task's OWN station, which under `flat.cs` is one of many
7031 const std::size_t ts = station_idx_of(L, tidx);
7032 T tp = Tzero(), ut = Tzero();
7033 for (std::size_t k = 0; k < L.nclasses; ++k) {
7034 tp += r.TN(ts - 1, k);
7035 ut += r.UN(ts - 1, k);
7036 }
7037 tput[tidx] = T(num_traits<T>::from_double(lqn.repl[tidx]) * tp);
7038 util[tidx] = ut;
7039 // The closure is a cycle of ONE replica's threads, so both sides are per replica: the rate is the
7040 // total divided back down, and the pool is maxmult, since njobs keeps whichever convention the
7041 // caller's layer used. Inert at repl 1 (updateThinkTimes.m).
7042 const double nrep = std::max(1.0, lqn.repl[tidx]);
7043 const T xcyc = T(tput[tidx] / num_traits<T>::from_double(nrep));
7044 if (nrep > 1.0 && std::isfinite(lqn.maxmult[tidx]) && lqn.maxmult[tidx] > 0.0) nj = lqn.maxmult[tidx];
7045 T raw;
7046 if (lqn.sched[tidx] == SchedStrategy::INF) {
7047 // an infinite server reports utilization as a mean job count
7048 raw = xcyc == Tzero()
7049 ? Tzero()
7050 : T((num_traits<T>::from_double(nj) - util[tidx]) / xcyc - ztask);
7051 } else {
7052 const T om = util[tidx] > Tone() ? T(util[tidx] - Tone()) : T(Tone() - util[tidx]);
7053 raw = xcyc == Tzero()
7054 ? Tzero()
7055 : T(num_traits<T>::from_double(nj) * om / xcyc - ztask);
7056 }
7057 const T floorv = num_traits<T>::from_double(GlobalConstants::Zero);
7058 thinkt[tidx] = raw < floorv ? floorv : raw;
7059
7060 // Phase-2 tail: the entry replies after phase 1, so the station serves the caller for residt while the
7061 // thread stays busy for servt. What is left is busy time, not think time (updateThinkTimes.m).
7062 if (lnmethod != "flat.cs") {
7063 const T lessz = T(thinkt[tidx] - phase2_tail(tidx));
7064 thinkt[tidx] = lessz < floorv ? floorv : lessz;
7065 }
7066
7067 // The cold start goes the OTHER way from the phase-2 tail. A caller
7068 // class cycles as delay plus station service, and the station serves
7069 // only the host demand: the charge is on the ENTRY, not on any
7070 // activity's demand, so the station never sees it and the delay has
7071 // to carry it. Without this the callee layer cycles faster than its
7072 // callers drive it -- 0.529412 against 0.5 on lqn_setup, with the
7073 // caller conserved and the callee not. Zero for a task with no setup
7074 // (updateThinkTimes.m).
7075 {
7076 const double c = setup_charge(tidx);
7077 if (c != 0.0) {
7078 const T withc = T(thinkt[tidx] + num_traits<T>::from_double(c));
7079 thinkt[tidx] = withc < floorv ? floorv : withc;
7080 }
7081 }
7082
7083 if (relax_omega < 1.0 && it > 1 && !std::isnan(thinkt_prev[tidx])) {
7084 const double rawd = dbl(thinkt[tidx]);
7085 if (thinkt_prev[tidx] > 10.0 * rawd && rawd > GlobalConstants::FineTol) {
7086 thinkt_prev[tidx] = rawd;
7087 thinkt_prev_v[tidx] = thinkt[tidx];
7088 }
7089 const T om = num_traits<T>::from_double(relax_omega);
7090 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
7091 thinkt[tidx] = T(om * thinkt[tidx] + om1 * thinkt_prev_v[tidx]);
7092 }
7093 thinkt_prev[tidx] = dbl(thinkt[tidx]);
7094 thinkt_prev_v[tidx] = thinkt[tidx];
7095 thinktproc[tidx] = Distrib<T>::exp_mean(T(thinkt[tidx] + ztask));
7096 }
7097 }
7098
7099 // -----------------------------------------------------------------------
7100 // updateLayers and updateRoutingProbabilities
7101 // -----------------------------------------------------------------------
7102 void update_layers(int it) {
7103 // Under "srvn.ph" the layer classes are one per caller task and their laws
7104 // are composed, not read off the update maps
7105 if (is_ph_encoding()) {
7106 update_layers_ph(it);
7107 return;
7108 }
7109 const bool elevator = (it % 2) == 1;
7110 const std::size_t nt = thinkt_map.size();
7111 for (std::size_t r = 0; r < nt; ++r) {
7112 const UpdRow& row = thinkt_map[elevator ? nt - 1 - r : r];
7113 qn::Layer<T>& L = ensemble[std::size_t(idxhash[row.idx])];
7114 if (row.aidx <= NT() && opt.interlocking &&
7115 L.classes[row.cls - 1].type == JobClassType::CLOSED)
7116 L.classes[row.cls - 1].population = njobs(row.aidx, row.idx);
7117 if (row.node == L.clientIdx) {
7118 if (lqn.type[row.aidx] == LqnElement::TASK) {
7119 if (lqn.sched[row.aidx] != SchedStrategy::REF) {
7120 if (!thinktproc[row.aidx].disabled)
7121 L.set_service(row.node, row.cls, thinktproc[row.aidx]);
7122 } else {
7123 L.set_service(row.node, row.cls, servtproc[row.aidx]);
7124 }
7125 } else {
7126 L.set_service(row.node, row.cls, servtproc[row.aidx]);
7127 }
7128 } else {
7129 L.set_service(row.node, row.cls, servtproc[row.aidx]);
7130 }
7131 }
7132 const std::size_t nc = call_map.size();
7133 for (std::size_t r = 0; r < nc; ++r) {
7134 const UpdRow& row = call_map[elevator ? nc - 1 - r : r];
7135 qn::Layer<T>& L = ensemble[std::size_t(idxhash[row.idx])];
7136 if (row.node == L.clientIdx) {
7137 L.set_service(row.node, row.cls, callservtproc[row.aidx]);
7138 } else {
7139 const std::size_t eidx = lqn.callpair_dst[row.aidx];
7140 // A phase-2 entry replies before phase 2 runs, so the caller is held for residt, not servt; charging
7141 // servt here while the caller's layer charges residt breaks flow conservation across the call.
7142 // Flat layering shares one model between caller and callee and takes no correction (updateLayers.m).
7143 if (has_phase2 && lnmethod != "flat.cs" && dbl(servt_ph2[eidx]) > GlobalConstants::FineTol)
7144 L.set_service(row.node, row.cls, Distrib<T>::exp_mean(residt[eidx]));
7145 else
7146 L.set_service(row.node, row.cls, servtproc[eidx]);
7147 }
7148 }
7149 // Async arrival rate: the caller activity fires at its own throughput,
7150 // and every firing releases callmean jobs into this layer's Source.
7151 // updateLayers.m:63-73 replays the same map; the geometric self-loop in
7152 // build_layer already accounts for callmean, so the rate is the bare
7153 // activity throughput, not scaled by it.
7154 //
7155 // The reference-path stage means go first, after the think times:
7156 // residt, callresidt, util and tput are all fresh by now.
7157 if (interlockMethod == "refpath") update_ref_path_stages(it);
7158 for (const UpdRow& row : arv_call_map) {
7159 qn::Layer<T>& L = ensemble[std::size_t(idxhash[row.idx])];
7160 const Distrib<T>& d = tputproc[lqn.callpair_src[row.aidx]];
7161 if (!d.disabled) L.set_service(row.node, row.cls, d);
7162 }
7163 }
7164
7165 /**
7166 * Port of updateRefPathStages: recompute every reference-path stage mean.
7167 *
7168 * A HOP stage stands for an entry whose task has no activity graph in the
7169 * layer: residt(e), minus callresidt(c)/entryvisits(e) for each descent the
7170 * chain re-expands below it, plus the wait W for one of its task's threads.
7171 * A GATE into a MEMBER charges W(callee task) alone, since everything else
7172 * the member owns is explicit in the layer. W is taken in the POPULATION
7173 * domain, never as the difference of two nearly equal response times.
7174 */
7175 void update_ref_path_stages(int it) {
7176 if (refpath_map.empty()) return;
7177 const std::size_t nkeys = refpath_keys.size();
7178 std::vector<T> stage(nkeys, Tzero());
7179 for (std::size_t r = 0; r < refpath_map.size(); ++r) {
7180 const RefPathRow& row = refpath_map[r];
7181 T term = Tzero();
7182 switch (row.kind) {
7183 case 1: // the entry's own residence, per entry invocation
7184 term = residt[row.elem];
7185 break;
7186 case 2: { // a re-expanded descent, converted to the entry scale
7187 T v = Tone();
7188 if (row.fromentry >= 1 && dbl(entryvisits[row.fromentry]) > GlobalConstants::FineTol)
7189 v = entryvisits[row.fromentry];
7190 term = T(callresidt[row.elem] / v);
7191 break;
7192 }
7193 case 3: // the thread-acquisition wait of the task
7194 term = thread_wait(row.elem);
7195 break;
7196 }
7197 if (!std::isfinite(dbl(term))) term = Tzero();
7198 stage[refpath_group[r]] = T(stage[refpath_group[r]] + term * num_traits<T>::from_double(row.sign));
7199 }
7200 const double omega = relax_omega;
7201 for (std::size_t k = 0; k < nkeys; ++k) {
7202 if (stage[k] < Tzero()) stage[k] = Tzero();
7203 // the think-time schedule, including the escape from a divergent iterate
7204 if (omega < 1.0 && it > 1 && !std::isnan(refpath_stage_prev[k])) {
7205 T prevS = refpath_stage_prev_v[k];
7206 if (dbl(prevS) > 10.0 * dbl(stage[k]) && dbl(stage[k]) > GlobalConstants::FineTol)
7207 prevS = stage[k];
7208 stage[k] = T(num_traits<T>::from_double(omega) * stage[k] +
7209 num_traits<T>::from_double(1.0 - omega) * prevS);
7210 }
7211 refpath_stage_prev[k] = dbl(stage[k]);
7212 refpath_stage_prev_v[k] = stage[k];
7213 qn::Layer<T>& L = ensemble[std::size_t(idxhash[refpath_keys[k][0]])];
7214 // exp_mean(0) is an infinite rate, which the visit solve reads as a
7215 // disabled pair and drops from the chain
7216 if (dbl(stage[k]) > GlobalConstants::FineTol)
7217 L.set_service(refpath_keys[k][1], refpath_keys[k][2], Distrib<T>::exp_mean(stage[k]));
7218 else
7219 L.set_service(refpath_keys[k][1], refpath_keys[k][2], Distrib<T>::immediate());
7220 }
7221 }
7222
7223 /**
7224 * Mean time a request queues for a thread of task TIDX, read from TIDX's own
7225 * layer in the population domain, max(0, Q - m*U)/X. An infinite or
7226 * unbounded pool never queues.
7227 */
7228 T thread_wait(std::size_t tidx) const {
7229 if (tidx < 1 || tidx > lqn.nidx) return Tzero();
7230 if (lqn.sched[tidx] == SchedStrategy::INF || !std::isfinite(lqn.maxmult[tidx])) return Tzero();
7231 if (idxhash[tidx] < 0 || results.empty()) return Tzero();
7232 const std::size_t e = std::size_t(idxhash[tidx]);
7233 const qn::Layer<T>& L = ensemble[e];
7234 const LayerResult<T>& r = results.back()[e];
7235 const std::size_t ks = station_idx_of(L, tidx);
7236 T Qtot = Tzero();
7237 for (std::size_t c = 0; c < L.nclasses; ++c) Qtot += r.QN(ks - 1, c);
7238 const T Btot = T(num_traits<T>::from_double(lqn.maxmult[tidx]) * util[tidx]);
7239 const T Xtot = T(tput[tidx] / num_traits<T>::from_double(std::max(1.0, lqn.repl[tidx])));
7240 if (!(dbl(Xtot) > GlobalConstants::FineTol)) return Tzero();
7241 T d = T(Qtot - Btot);
7242 const T z = num_traits<T>::from_double(GlobalConstants::Zero);
7243 if (d < z) d = z;
7244 return T(d / Xtot);
7245 }
7246
7247 /**
7248 * Time a job of `cls`'s CHAIN spends blocked at this layer's region.
7249 *
7250 * A job held by an admission constraint is at no station at all, so its
7251 * wait is structurally absent from the RN and WN the layer reports back and
7252 * the caller and the callee end up disagreeing on throughput -- the fixed
7253 * point still converges, it just converges to the wrong place.
7254 *
7255 * Recovered by Little over the CHAIN, never per class: a job switches class
7256 * along the activity graph, so a call class carries population 0 and a
7257 * per-class deficit comes out negative and silently does nothing.
7258 * Zero for a layer with no region, which is every layer in a plain model.
7259 */
7260 T region_wait(std::size_t e, std::size_t cls) const {
7261 const qn::Layer<T>& L = ensemble[e];
7262 if (L.regions.empty() || results.empty()) return Tzero();
7263 const LayerResult<T>& r = results.back()[e];
7264 const std::size_t k = cls - 1;
7265 std::size_t c = L.nchains;
7266 for (std::size_t cc = 0; cc < L.nchains; ++cc)
7267 if (L.chains[cc][k]) c = cc;
7268 if (c == L.nchains) return Tzero();
7269
7270 // the station the region is stated over, which under `flat.cs` is one
7271 // among many and under `srvn` is the layer's own server
7272 std::size_t rs = L.serverIdx;
7273 for (std::size_t i = 0; i < L.regions[0].members.size(); ++i)
7274 if (L.regions[0].members[i]) { rs = i + 1; break; }
7275
7276 double pop = 0.0;
7277 T inside = Tzero(), tput_srv = Tzero();
7278 for (std::size_t j = 0; j < L.nclasses; ++j) {
7279 if (!L.chains[c][j]) continue;
7280 const double p = L.classes[j].population;
7281 if (!std::isfinite(p)) return Tzero(); // an open chain has no population to close on
7282 pop += p;
7283 for (std::size_t i = 0; i < L.nstations; ++i) inside += r.QN(i, j);
7284 tput_srv += r.TN(rs - 1, j);
7285 }
7286 const T deficit = T(num_traits<T>::from_double(pop) - inside);
7287 if (!(deficit > Tzero()) || !(tput_srv > Tzero())) return Tzero();
7288 return T(deficit / tput_srv);
7289 }
7290
7291 /** Port of updateRoutingProbabilities: entry selection by throughput ratio. */
7292 void update_routing_probabilities(int) {
7293 for (std::size_t u = 0; u < unique_route_idx.size(); ++u) {
7294 // the reference always takes the reversed order here: its `mod(it,0)`
7295 // guard is NaN, which MATLAB reads as false
7296 const std::size_t idx = unique_route_idx[unique_route_idx.size() - 1 - u];
7297 qn::Layer<T>& L = ensemble[std::size_t(idxhash[idx])];
7298 bool updated = false;
7299 for (const RouteRow& r : route_map) {
7300 if (r.idx != idx) continue;
7301 if (idxhash[r.tidx_caller] < 0) continue;
7302 const std::size_t cl = std::size_t(idxhash[r.tidx_caller]);
7303 const qn::Layer<T>& CL = ensemble[cl];
7304 const LayerResult<T>& cr = results.back()[cl];
7305 // the CALLER's own station, and for a call class the station of
7306 // the entry it targets; the two coincide under `srvn`
7307 const std::size_t cs = station_idx_of(CL, r.tidx_caller);
7308 T Xtot = Tzero();
7309 for (std::size_t k = 0; k < CL.nclasses; ++k) Xtot += cr.TN(cs - 1, k);
7310 if (!(Xtot > Tzero())) continue;
7311 T entry_tput = Tzero();
7312 for (const auto& a : CL.attr_calls)
7313 if (a[3] == r.eidx)
7314 entry_tput += cr.TN(station_idx_of(CL, lqn.parent[a[3]]) - 1, a[0] - 1);
7315 L.set_route(r.cfrom, r.cto, r.nodefrom, r.nodeto, T(entry_tput / Xtot));
7316 updated = true;
7317 }
7318 if (updated) L.refresh_chains();
7319 }
7320 }
7321
7322 // -----------------------------------------------------------------------
7323 // getTranAvg: getTranAvgDecoupled and getTranAvgCoupled
7324 // -----------------------------------------------------------------------
7325
7326 /**
7327 * What a layered transient needs before it can mean anything.
7328 *
7329 * A FLUID ENSEMBLE IS REQUIRED, and not as an implementation shortcut: the
7330 * reference reaches the layer transient through `self.solvers{e}.getTranAvg`,
7331 * which only a transient-capable layer solver has, and the coupled mode
7332 * injects its time-varying demands through the fluid rate schedule
7333 * specifically. An MVA layer has no trajectory to report and no place to
7334 * receive an injection, so an ensemble built on one is refused here rather
7335 * than silently answered with its steady state repeated over a grid.
7336 */
7337 void require_transient_ready() const {
7338 if (opt.layer_solver != "fluid")
7339 throw UnsupportedError(
7340 "SolverLN: the layered transient is the transient OF EACH LAYER, which only the "
7341 "fluid layer solver has; rebuild the ensemble with layer_solver 'fluid'");
7342 }
7343
7344 /** True when the caller named a horizon; otherwise each layer picks its own. */
7345 bool has_finite_horizon() const {
7346 return std::isfinite(opt.timespan_end) && opt.timespan_end > 0.0;
7347 }
7348
7349 /** One layer's trajectory, resampled onto the shared grid. */
7350 struct TranTraj {
7351 std::vector<std::vector<std::vector<double>>> Q, U, Tp, R; ///< [station][class][point]
7352 };
7353
7354 /**
7355 * Run one layer's transient, optionally with an injected rate schedule, and
7356 * return both the reportable block and the trajectory the relaxation reads.
7357 */
7358 void run_layer_transient(std::size_t e, const std::vector<FluidRateSched>& sched,
7359 LnTranLayer& block, TranTraj& traj) {
7360 qn::Layer<T>& L = ensemble[e];
7361 L.refresh_rt();
7362 fluid::FluidOptions fo = opt.layer_fluid;
7363 fo.rate_sched = sched;
7364 // THE WARM START IS REPLAYED HERE, and this is the only place it can be:
7365 // `converged` resets every layer the first time the fixed point settles,
7366 // so a state installed before the solve is gone by now. The steady solve
7367 // ignores an initial state and the transient does not, which is why
7368 // replaying it after the one and before the other loses nothing.
7369 if (e < layer_tran_init.size() && !layer_tran_init[e].empty())
7370 fo.init_sol = layer_tran_init[e];
7371 // No horizon named: this layer picks its own, by the analyzer's own rule
7372 // (thirty mean events of its slowest transition). That is what the
7373 // reference's decoupled path does -- each layer's SolverFluid chooses --
7374 // and it is why an unset timespan is a valid request rather than an error.
7375 const double t_end =
7376 has_finite_horizon() ? opt.timespan_end : fluid::fluid_default_horizon(L, fo);
7377 const std::vector<fluid::FluidTranPoint> pts =
7378 detail::ln_fluid_transient(L, fo, t_end, opt.tran_points, opt.tran_grid);
7379 const std::size_t M = L.nstations, K = L.nclasses, P = pts.size();
7380 block.t.resize(P);
7381 auto alloc = [&](std::vector<std::vector<std::vector<double>>>& A) {
7382 A.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
7383 };
7384 alloc(block.QN);
7385 alloc(block.UN);
7386 alloc(block.TN);
7387 alloc(traj.Q);
7388 alloc(traj.U);
7389 alloc(traj.Tp);
7390 alloc(traj.R);
7391 for (std::size_t p = 0; p < P; ++p) {
7392 block.t[p] = pts[p].t;
7393 for (std::size_t i = 0; i < M; ++i)
7394 for (std::size_t r = 0; r < K; ++r) {
7395 const double q = pts[p].QN(i, r), u = pts[p].UN(i, r), x = pts[p].TN(i, r);
7396 block.QN[i][r][p] = q;
7397 block.UN[i][r][p] = u;
7398 block.TN[i][r][p] = x;
7399 traj.Q[i][r][p] = q;
7400 traj.U[i][r][p] = u;
7401 traj.Tp[i][r][p] = x;
7402 // Residence by Little, which is how the relaxation reads a
7403 // callee's response time off a layer trajectory.
7404 traj.R[i][r][p] = q / std::max(x, GlobalConstants::FineTol);
7405 }
7406 }
7407 }
7408
7409 /**
7410 * Port of getTranAvgDecoupled: freeze the inter-layer demands at the
7411 * converged fixed point and run each layer's transient in isolation.
7412 */
7413 LnTranSolution tran_avg_decoupled() {
7414 require_transient_ready();
7415 if (results.empty()) iterate();
7416 LnTranSolution out;
7417 out.mode = "decoupled";
7418 out.layers.resize(ensemble.size());
7419 for (std::size_t e = 0; e < ensemble.size(); ++e) {
7420 TranTraj tj;
7421 run_layer_transient(e, std::vector<FluidRateSched>(), out.layers[e], tj);
7422 }
7423 return out;
7424 }
7425
7426 /**
7427 * Port of getTranAvgCoupled: reconcile the per-layer transients by waveform
7428 * relaxation, so the layer populations and the inter-layer demands co-evolve
7429 * in model time.
7430 *
7431 * Each layer's fluid transient is driven by TIME-VARYING inter-layer demand
7432 * trajectories taken from the other layers' latest transients, and the loop
7433 * repeats until the trajectories stop moving in sup-norm. Iteration 0 uses
7434 * the frozen equilibrium demands, so it reproduces the decoupled answer
7435 * exactly; at convergence every layer relaxes to its own fixed point, so the
7436 * endpoint equals getAvg.
7437 *
7438 * TWO CHANNELS ARE COUPLED, the task think times (the client delay) and the
7439 * synchronous-call service demands (the caller's client station). Both are
7440 * dominant inter-layer couplings; the intra-layer host service stays at its
7441 * equilibrium value, as in the reference.
7442 */
7443 LnTranSolution tran_avg_coupled() {
7444 require_transient_ready();
7445 // Waveform relaxation reconciles the layers on ONE shared grid, so it
7446 // needs a horizon the layers agree on. With none named there is nothing
7447 // to co-evolve over, and the reference defers to the decoupled path
7448 // rather than inventing one; so does this.
7449 if (!has_finite_horizon()) return tran_avg_decoupled();
7450 if (results.empty()) iterate();
7451 const std::size_t E = ensemble.size();
7452
7453 LnTranSolution out;
7454 out.mode = "coupled";
7455 out.layers.resize(E);
7456 std::vector<TranTraj> traj(E), prev(E);
7457 for (std::size_t e = 0; e < E; ++e)
7458 run_layer_transient(e, std::vector<FluidRateSched>(), out.layers[e], traj[e]);
7459 const std::vector<double> tgrid = out.layers.empty() ? std::vector<double>() : out.layers[0].t;
7460
7461 for (long iter = 1; iter <= opt.ln_transient_iter_max; ++iter) {
7462 prev = traj;
7463 const std::vector<std::vector<FluidRateSched>> sched =
7464 build_rate_sched(recompute_demand(traj, tgrid), tgrid);
7465 for (std::size_t e = 0; e < E; ++e)
7466 run_layer_transient(e, sched[e], out.layers[e], traj[e]);
7467 double gap = 0.0;
7468 for (std::size_t e = 0; e < E; ++e)
7469 for (std::size_t i = 0; i < traj[e].Q.size(); ++i)
7470 for (std::size_t r = 0; r < traj[e].Q[i].size(); ++r)
7471 for (std::size_t p = 0; p < traj[e].Q[i][r].size(); ++p)
7472 gap = std::max(gap, std::fabs(traj[e].Q[i][r][p] - prev[e].Q[i][r][p]));
7473 out.iterations = iter;
7474 out.gap = gap;
7475 if (gap < opt.ln_transient_tol) break;
7476 }
7477 return out;
7478 }
7479
7480 /** The two coupled demand channels, as trajectories on the shared grid. */
7481 struct TranDemand {
7482 std::map<std::size_t, std::vector<double>> thinkt; ///< by task element index
7483 std::map<std::size_t, std::vector<double>> callservt; ///< by call index
7484 };
7485
7486 /**
7487 * Recompute the inter-layer demands pointwise in t, mirroring the SCALAR
7488 * updateThinkTimes and updateMetricsDefault formulas.
7489 *
7490 * They are the same formulas, evaluated at each point of the grid instead of
7491 * at the fixed point: that is what makes the endpoint of the relaxation the
7492 * steady-state answer rather than something near it.
7493 */
7494 TranDemand recompute_demand(const std::vector<TranTraj>& traj,
7495 const std::vector<double>& tgrid) const {
7496 TranDemand out;
7497 const std::size_t ng = tgrid.size();
7498
7499 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
7500 const std::size_t tidx = lqn.tshift + t;
7501 if (idxhash[tidx] < 0 || lqn.isref[tidx]) continue;
7502 const std::size_t e = std::size_t(idxhash[tidx]);
7503 const qn::Layer<T>& L = ensemble[e];
7504 const std::size_t s = station_idx_of(L, tidx) - 1;
7505 double nj = 0.0;
7506 for (std::size_t c = 1; c <= NT(); ++c) nj = std::max(nj, njobs(tidx, c));
7507 // same closure as update_think_times, so the same gate
7508 const double userthink = dbl(ref_think_time(tidx));
7509 std::vector<double> tk(ng, 0.0);
7510 for (std::size_t p = 0; p < ng; ++p) {
7511 double U = 0.0, X = 0.0;
7512 for (std::size_t r = 0; r < L.nclasses; ++r) {
7513 U += traj[e].U[s][r][p];
7514 X += traj[e].Tp[s][r][p];
7515 }
7516 const double Xs = std::max(X, GlobalConstants::FineTol);
7517 double v = lqn.sched[tidx] == SchedStrategy::INF
7518 ? (nj - U) / Xs - userthink
7519 : nj * std::fabs(1.0 - U) / Xs - userthink;
7521 tk[p] = v + userthink; // total mean, user think included
7522 }
7523 out.thinkt[tidx] = tk;
7524 }
7525
7526 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
7527 if (lqn.calltype[cidx] != CallType::SYNC) continue;
7528 const std::size_t eidx = lqn.callpair_dst[cidx];
7529 const std::size_t tidx = lqn.parent[eidx];
7530 if (tidx > NT() || idxhash[tidx] < 0) continue;
7531 const std::size_t e = std::size_t(idxhash[tidx]);
7532 const qn::Layer<T>& L = ensemble[e];
7533 const std::size_t s = station_idx_of(L, tidx) - 1;
7534 std::vector<double> Rc(ng, 0.0);
7535 bool any = false;
7536 for (std::size_t r = 0; r < L.nclasses; ++r) {
7537 if (L.classes[r].attr_kind != int(LqnElement::ENTRY)) continue;
7538 if (L.classes[r].attr_idx != eidx) continue;
7539 any = true;
7540 for (std::size_t p = 0; p < ng; ++p) Rc[p] += traj[e].R[s][r][p];
7541 }
7542 if (!any) {
7543 // No entry class of its own in this layer: the entry's work is
7544 // carried by its activities, so their residence is the answer.
7545 for (std::size_t r = 0; r < L.nclasses; ++r)
7546 for (std::size_t p = 0; p < ng; ++p) Rc[p] += traj[e].R[s][r][p];
7547 }
7548 const double cm = dbl(lqn.callproc_mean[cidx]);
7549 for (std::size_t p = 0; p < ng; ++p) Rc[p] *= cm;
7550 out.callservt[cidx] = Rc;
7551 }
7552 return out;
7553 }
7554
7555 /**
7556 * Map the demand trajectories onto per-layer rate schedules, through the
7557 * SAME update maps that place the scalar setService calls.
7558 *
7559 * The injected schedule MODULATES the layer's equilibrium rate by the ratio
7560 * of the transient demand to its steady-state value: passing rate = 1/d(t)
7561 * with nominal = 1/d(end) makes solver_fluid_ratemult's multiplier
7562 * d(end)/d(t), which is exactly 1 at the horizon end. That is what makes the
7563 * layer relax to its UNMODIFIED fixed point regardless of any small mismatch
7564 * between the fluid residence Q/T and the scalar equilibrium demand.
7565 */
7566 std::vector<std::vector<FluidRateSched>> build_rate_sched(
7567 const TranDemand& demand, const std::vector<double>& tgrid) const {
7568 std::vector<std::vector<FluidRateSched>> out(ensemble.size());
7569 const bool want_think =
7570 opt.ln_transient_channels == "both" || opt.ln_transient_channels == "thinkt";
7571 const bool want_call =
7572 opt.ln_transient_channels == "both" || opt.ln_transient_channels == "callservt";
7573
7574 auto add = [&](std::size_t e, std::size_t station, std::size_t cls,
7575 const std::vector<double>& d) {
7576 if (d.empty()) return;
7577 const double dend = d.back();
7578 if (!(dend > GlobalConstants::FineTol)) return; // degenerate steady state
7579 // Bound the transient demand to a physical band around its
7580 // steady-state value: an early-transient throughput near zero sends
7581 // the reciprocal to infinity and the integrator with it.
7582 const double cap = 20.0;
7584 s.station = station;
7585 s.cls = cls;
7586 s.tgrid = tgrid;
7587 s.rates.resize(d.size());
7588 for (std::size_t p = 0; p < d.size(); ++p) {
7589 const double v = std::min(std::max(d[p], dend / cap), dend * cap);
7590 s.rates[p] = 1.0 / v;
7591 }
7592 s.nominal = 1.0 / dend;
7593 out[e].push_back(s);
7594 };
7595
7596 if (want_think)
7597 for (const UpdRow& row : thinkt_map) {
7598 if (idxhash[row.idx] < 0) continue;
7599 const std::size_t e = std::size_t(idxhash[row.idx]);
7600 if (row.node != ensemble[e].clientIdx) continue;
7601 if (lqn.type[row.aidx] != LqnElement::TASK) continue;
7602 if (lqn.sched[row.aidx] == SchedStrategy::REF) continue;
7603 const auto it = demand.thinkt.find(row.aidx);
7604 if (it == demand.thinkt.end()) continue;
7605 add(e, row.node, row.cls, it->second);
7606 }
7607 if (want_call)
7608 for (const UpdRow& row : call_map) {
7609 if (idxhash[row.idx] < 0) continue;
7610 const std::size_t e = std::size_t(idxhash[row.idx]);
7611 if (row.node != ensemble[e].clientIdx) continue;
7612 const auto it = demand.callservt.find(row.aidx);
7613 if (it == demand.callservt.end()) continue;
7614 add(e, row.node, row.cls, it->second);
7615 }
7616 return out;
7617 }
7618
7619 // -----------------------------------------------------------------------
7620 // getEnsembleAvg
7621 // -----------------------------------------------------------------------
7622 LnSolution<T> aggregate() {
7623 const std::size_t N = lqn.nidx;
7624 LnSolution<T> s;
7625 auto mk = [&](std::vector<T>& v, std::vector<bool>& d) {
7626 v.assign(N + 1, Tzero());
7627 d.assign(N + 1, false);
7628 };
7629 std::vector<T> QN, UN, RN, TN, PN, SN, WN, AN;
7630 std::vector<bool> dQ, dU, dR, dT, dP, dS, dW, dA;
7631 mk(QN, dQ); mk(UN, dU); mk(RN, dR); mk(TN, dT);
7632 mk(PN, dP); mk(SN, dS); mk(WN, dW); mk(AN, dA);
7633 std::vector<bool> wn_done(N + 1, false);
7634
7635 for (std::size_t e = 0; e < ensemble.size(); ++e) {
7636 const qn::Layer<T>& L = ensemble[e];
7637 const LayerResult<T>& r = results.back()[e];
7638 const std::size_t clientIdx = L.clientIdx;
7639 // The processors this layer serves: one under `srvn` (and only when
7640 // it is a host layer), every one of them under `flat.cs`. Each is
7641 // charged with the activities that actually run on it, which is
7642 // vacuous under `srvn` because a host layer holds no others.
7643 const bool has_host_server = !L.host_stations.empty();
7644 for (std::size_t hs : L.host_stations) {
7645 const std::size_t hidx = L.stations[hs - 1].attr_idx;
7646 dT[hidx] = true;
7647 TN[hidx] = Tzero();
7648 dP[hidx] = true;
7649 PN[hidx] = Tzero();
7650 for (std::size_t k = 0; k < L.nclasses; ++k) {
7651 if (L.classes[k].completes) {
7652 T t = clientIdx > 0 ? r.TN(clientIdx - 1, k) : Tzero();
7653 const T ts = r.TN(hs - 1, k);
7654 TN[hidx] = T(TN[hidx] + (t > ts ? t : ts));
7655 }
7656 if (L.classes[k].attr_kind == int(LqnElement::ACTIVITY)) {
7657 // the activity does not run on this processor
7658 if (station_idx_of_class(L, k) != hs) continue;
7659 const std::size_t aidx = L.classes[k].attr_idx;
7660 const std::size_t tidx = lqn.parent[aidx];
7661 dP[aidx] = true;
7662 dP[tidx] = true;
7663 PN[aidx] = T(PN[aidx] + r.UN(hs - 1, k));
7664 PN[tidx] = T(PN[tidx] + r.UN(hs - 1, k));
7665 PN[hidx] = T(PN[hidx] + r.UN(hs - 1, k));
7666 }
7667 }
7668 dT[hidx] = false; // NaN in the reference, for consistency with LQNS
7669 }
7670
7671 for (std::size_t k = 0; k < L.nclasses; ++k) {
7672 const int kind = L.classes[k].attr_kind;
7673 // the station this class is actually served at, which under
7674 // `flat.cs` is the processor of an activity or the called task
7675 // of a call rather than the one server the layer used to have
7676 const std::size_t serverIdx = station_idx_of_class(L, k);
7677 if (kind == int(LqnElement::TASK)) {
7678 const std::size_t tidx = L.classes[k].attr_idx;
7679 if (has_host_server && !dT[tidx]) {
7680 dT[tidx] = true;
7681 TN[tidx] = r.TN(clientIdx - 1, k);
7682 }
7683 } else if (kind == int(LqnElement::ENTRY)) {
7684 const std::size_t eidx = L.classes[k].attr_idx;
7685 dS[eidx] = true;
7686 // getEnsembleAvg.m:84-92: a phase-2 entry reports the
7687 // CALLER's view, which is what residt now holds.
7688 SN[eidx] = (has_phase2 && dbl(servt_ph2[eidx]) > GlobalConstants::FineTol)
7689 ? residt[eidx]
7690 : servt[eidx];
7691 if (has_host_server && !dT[eidx]) {
7692 dT[eidx] = true;
7693 TN[eidx] = r.TN(clientIdx - 1, k);
7694 }
7695 } else if (kind == int(LqnElement::CALL)) {
7696 const std::size_t cidx = L.classes[k].attr_idx;
7697 const std::size_t aidx = lqn.callpair_src[cidx];
7698 if (lqn.calltype[cidx] == CallType::SYNC) {
7699 dS[aidx] = true;
7700 SN[aidx] = T(SN[aidx] + r.RN(serverIdx - 1, k) * lqn.callproc_mean[cidx]);
7701 }
7702 dQ[aidx] = true;
7703 QN[aidx] = T(QN[aidx] + r.QN(serverIdx - 1, k));
7704 } else if (kind == int(LqnElement::ACTIVITY)) {
7705 const std::size_t aidx = L.classes[k].attr_idx;
7706 const std::size_t tidx = lqn.parent[aidx];
7707 dQ[tidx] = true;
7708 QN[tidx] = T(QN[tidx] + r.QN(serverIdx - 1, k));
7709 dT[aidx] = true;
7710 dQ[aidx] = true;
7711 TN[aidx] = T(TN[aidx] + r.TN(serverIdx - 1, k));
7712 dS[aidx] = true;
7713 SN[aidx] = T(SN[aidx] + r.RN(serverIdx - 1, k));
7714 dR[aidx] = true;
7715 RN[aidx] = T(RN[aidx] + r.RN(serverIdx - 1, k));
7716 dW[aidx] = true;
7717 dW[tidx] = true;
7718 WN[aidx] = residt[aidx];
7719 if (!wn_done[aidx]) {
7720 WN[tidx] = T(WN[tidx] + residt[aidx]);
7721 wn_done[aidx] = true;
7722 }
7723 QN[aidx] = T(QN[aidx] + r.QN(serverIdx - 1, k));
7724 }
7725 }
7726 }
7727
7728 // A replicated element is solved as ONE representative replica; report it as the
7729 // TOTAL over its copies (TN and busy time scaled by repl, response times per
7730 // request). Mirrors MATLAB getEnsembleAvg; inert when nothing is replicated.
7731 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
7732 const std::size_t tidx = lqn.tshift + t;
7733 const double nrep = lqn.repl[tidx];
7734 if (!(nrep > 1.0)) continue;
7735 const T scale = num_traits<T>::from_double(nrep);
7736 std::vector<std::size_t> scaleIdx{tidx};
7737 scaleIdx.insert(scaleIdx.end(), lqn.entriesof[tidx].begin(), lqn.entriesof[tidx].end());
7738 scaleIdx.insert(scaleIdx.end(), lqn.actsof[tidx].begin(), lqn.actsof[tidx].end());
7739 for (std::size_t idx : scaleIdx) {
7740 if (dT[idx]) TN[idx] = T(scale * TN[idx]);
7741 if (dP[idx]) PN[idx] = T(scale * PN[idx]);
7742 }
7743 }
7744 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx)
7745 if (lqn.repl[hidx] > 1.0 && dP[hidx]) PN[hidx] = T(num_traits<T>::from_double(lqn.repl[hidx]) * PN[hidx]);
7746
7747 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7748 const std::size_t eidx = lqn.eshift + e;
7749 const std::size_t tidx = lqn.parent[eidx];
7750 dU[tidx] = true;
7751 dU[eidx] = true;
7752 // getEnsembleAvg.m:186-198: the server is busy through BOTH phases,
7753 // so the utilization is the full service time and not SN, which for
7754 // a phase-2 entry has been cut down to the caller's view above.
7755 UN[eidx] = (has_phase2 && dbl(servt_ph2[eidx]) > GlobalConstants::FineTol)
7756 ? T(TN[eidx] * (servt_ph1[eidx] + servt_ph2[eidx]))
7757 : T(TN[eidx] * SN[eidx]);
7758 T ps = Tzero();
7759 bool any = false;
7760 for (std::size_t a : lqn.actsof[eidx])
7761 if (dP[a]) {
7762 ps += PN[a];
7763 any = true;
7764 }
7765 if (!lqn.actsof[eidx].empty()) {
7766 dP[eidx] = true;
7767 PN[eidx] = any ? ps : Tzero();
7768 }
7769 for (std::size_t a : lqn.actsof[tidx]) {
7770 dU[a] = true;
7771 UN[a] = T(TN[a] * SN[a]);
7772 }
7773 UN[tidx] = T(UN[tidx] + UN[eidx]);
7774 }
7775
7776 // AN IGNORED ELEMENT IS IDLE, NOT UNDEFINED, and the two are different cells.
7777 // Its component holds no reference task, so nothing reaches it and every
7778 // measure it HAS is zero -- but the measures its kind never has stay
7779 // undefined, exactly as they do for a reachable element. A flat zero over
7780 // all six columns broke the table's NaN mask (a processor with a queue
7781 // length of 0, an arrival rate reported where no solver reports one), and
7782 // the mask is part of the answer: see _kb/06-solver-catalog.md. The
7783 // relabelling below reports Q from U, U from P and R from S, so QN and RN
7784 // are dead here and are not written.
7785 for (std::size_t i = 1; i <= N; ++i)
7786 if (ignore[i]) {
7787 PN[i] = Tzero(); // every kind reports a utilization
7788 dP[i] = true;
7789 dA[i] = false; // nothing reports an arrival rate on an LQN
7790 const bool host = lqn.type[i] == LqnElement::HOST;
7791 const bool task = lqn.type[i] == LqnElement::TASK;
7792 const bool entry = lqn.type[i] == LqnElement::ENTRY;
7793 UN[i] = Tzero();
7794 dU[i] = !host;
7795 SN[i] = Tzero();
7796 dS[i] = !host && !task;
7797 WN[i] = Tzero();
7798 dW[i] = !host && !entry;
7799 TN[i] = Tzero();
7800 dT[i] = !host;
7801 }
7802
7803 // the reference's final relabelling: Q <- U, U <- P, R <- S
7804 s.QN = UN; s.defined_Q = dU;
7805 s.UN = PN; s.defined_U = dP;
7806 s.RN = SN; s.defined_R = dS;
7807 s.TN = TN; s.defined_T = dT;
7808 s.AN = AN; s.defined_A = dA;
7809 s.WN = WN; s.defined_W = dW;
7810 s.iterations = iterations_done;
7811 s.converged = did_converge;
7812 return s;
7813 }
7814};
7815
7816} // namespace ln
7817} // namespace line
7818
7819// Deliberately at the FOOT of the file: lqn_analyzers.h needs LayerResult and
7820// SolverLN complete, and this file needs its lqn_overtake_prob_markov, so the
7821// two are mutually dependent. Either include order works -- whichever header a
7822// translation unit names first, the other is fully parsed before the templates
7823// above are instantiated -- and the forward declaration near the top of this
7824// file is what makes the call inside update_metrics resolve.
7826
7827#endif // LINE_SOLVERS_LN_SOLVER_LN_H
Convolution of a sequence of matrix-exponential laws.
Minimal-order acyclic phase-type fit of the first three moments (matlab/lib/kpctoolbox/aph/aph_fit....
Composition of two matrix-exponential distributions given in (alpha, T) form.
Malformed or inconsistent input (dimensions, negative populations, ...).
Definition error.h:37
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
UnsupportedError(const std::string &what)
Definition error.h:51
Convergence controller for an ensemble whose layers are solved by a NOISY method (simulation,...
const std::vector< double > & state_refpath_stages() const
Per reference-path stage key, the stage mean the last iteration installed.
Definition solver_ln.h:796
const std::string & state_interlock_method() const
The interlock tracking method this run USES: "ilrate", "refpath" or "none".
Definition solver_ln.h:794
LnSolution< T > box_bounds()
Majumdar-Woodside robust box bounds, reported in the shape of a solution.
Definition solver_ln.h:655
LnLayerBlocks layer_blocks() const
Port of LayeredNetwork.layerBlocks: the block-diagonal layout of the layers in the aggregate (station...
Definition solver_ln.h:699
const std::vector< std::vector< LayerResult< T > > > & iteration_results() const
Diagnostic access to the per-iteration layer results, for the regression.
Definition solver_ln.h:774
LnInterlockChoice interlock_method_for() const
Port of SolverLN.interlockMethodFor: the method a solve would use, asked through the same query build...
Definition solver_ln.h:804
const std::vector< T > & state_util() const
Definition solver_ln.h:784
SolverLN(const LqnStruct< T > &lqn_in, const LnOptions &options)
Definition solver_ln.h:506
LnSensTable< T > get_sensitivity_table(const sens::SensOptions &sopt)
Port of @SolverLN/getSensitivityTable: solve the ensemble, then concatenate each layer solver's own t...
Definition solver_ln.h:610
LnTranSolution get_tran_avg()
Port of @SolverLN/getTranAvg: the block-diagonal aggregate transient.
Definition solver_ln.h:591
const std::vector< T > & state_thinkt() const
Definition solver_ln.h:778
std::size_t nlayers() const
Definition solver_ln.h:688
const std::vector< Distrib< T > > & state_callservtproc() const
Definition solver_ln.h:783
std::vector< LnCdf > get_cdf_respt()
Port of @SolverLN/getCdfRespT: the per-entry response-time distribution.
Definition solver_ln.h:552
const std::vector< T > & state_callresidt() const
Definition solver_ln.h:780
const std::vector< double > & state_maxitererr() const
Per iteration, the layer-summed max queue-length change the convergence test reads (index 0 unused).
Definition solver_ln.h:786
const std::string & state_lnmethod() const
The method the layers were built for: "srvn.ph", "srvn.cs" or "moment3".
Definition solver_ln.h:788
const std::vector< T > & state_tput() const
Definition solver_ln.h:777
void set_tran_grid(const std::vector< double > &g)
Install the explicit output grid of the layered transient; see LnOptions::tran_grid.
Definition solver_ln.h:771
const std::vector< T > & state_servt() const
Definition solver_ln.h:775
static std::vector< std::string > list_valid_methods()
Port of SolverLN.listValidMethods.
Definition solver_ln.h:502
const std::vector< Distrib< T > > & state_servtproc() const
Definition solver_ln.h:781
const std::vector< Distrib< T > > & state_thinktproc() const
Definition solver_ln.h:782
LnSolution< T > get_ensemble_avg()
Port of getEnsembleAvg: run the iteration and aggregate onto LQN elements.
Definition solver_ln.h:528
const std::vector< T > & state_callservt() const
Definition solver_ln.h:779
const std::vector< qn::Layer< T > > & layers() const
Definition solver_ln.h:689
const std::vector< T > & state_residt() const
Definition solver_ln.h:776
void init_from_marginal(const Matrix< double > &n)
Port of LayeredNetwork.initFromMarginal: split an aggregate (M x K) mean queue-length matrix into per...
Definition solver_ln.h:739
A layer network: everything a NetworkStruct holds, plus the LQN back-mapping.
Definition qn_layer.h:52
A network plus its refreshed NetworkStruct.
std::vector< std::vector< bool > > disabled
std::vector< Station< T > > stations
stations[k-1] is the k-th station
static void iter(long k, const char *fmt,...)
Report iteration k of the current loop.
static void step(const char *fmt,...)
Write one progress line.
static bool is_acyclic_generator(const Matrix< T > &S)
True when the phase graph of S has no cycle.
Definition workflow.h:690
static PhLaw< T > compose_loop_geometric(const PhLaw< T > &body, const T &count)
Geometric repetition of a phase-type law, the POST_LOOP semantics.
Definition workflow.h:618
static PhLaw< T > compose_serial(const PhLaw< T > &a, const PhLaw< T > &b)
Serial composition: the second law starts when the first absorbs.
Definition workflow.h:496
static PhLaw< T > compose_mixture(const std::vector< PhLaw< T > > &laws, const std::vector< T > &probs)
Probabilistic mixture: a block-diagonal generator whose initial vector picks branch i with probabilit...
Definition workflow.h:581
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Activities belonging to each branch of an AND-join.
The fork-join fixed point that drives one inner MVA solve.
The fork-join transform SolverMVA applies before solving a layer that contains a Fork.
Mean and variance of a k-of-n (quorum) join completion time, from the mean and variance of each branc...
Response-time distribution by tagged fluid: a port of solver_fluid_passage_time.m,...
The fluid solver's outermost entry point: @@SolverFLD/runAnalyzer.m's method resolution over solver_f...
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Running progress log of a LINE solver run (the "solver console").
The @SolverLN methods that solver_ln.h does not carry.
Majumdar-Woodside robust box bounds on the throughput of a layered network.
Standalone LQN routines that SolverLN needs but does not contain.
Phase-type composition of an LQN activity graph, the machinery behind SolverLN method 'srvn....
Synchronous call DAG carrying reference-task customers into a layer.
LayeredNetworkStruct, the flattened description of a layered queueing network.
Port of solver_mam_analyzer.m: one inner solve, choosing the analyzer that fits the model and the req...
Dense matrix and non-owning view.
Port of @@SolverMVA/mvaDispatch.m: one inner solve, choosing the analyzer that fits the model.
EntryWorkflow< T > entry_workflow(const ::line::lqn::LqnStruct< T > &lqn, std::size_t eidx, bool with_calls)
Activity graph of LQN entry EIDX as a Workflow.
Definition lqn_ph.h:104
PhLaw< T > ph_law_of(const Distrib< T > &d)
The (alpha, S) pair of a phase-type Distrib, the form the composition rules take.
Definition lqn_ph.h:52
PhLaw< T > serial_law(Workflow< T > &wf)
Composed law of a workflow in which the branches of an AND fork are SERIAL rather than concurrent,...
Definition lqn_ph.h:241
std::pair< T, T > ph_moments(const std::vector< T > &alpha, const Matrix< T > &S)
First two moments of a phase-type law without building a Distrib, which is what the layered fixed poi...
Definition lqn_ph.h:258
void sn_fj_nodevisits_mmt(qn::NetworkStruct< T > &sn)
Rewrite sn.nodevisits with the MMT correction.
T sn_compat_scaling(const Matrix< T > &compat, const std::vector< double > &counts, const std::vector< T > &rates, const std::vector< T > &n)
Rate scaling eta(n) a compatibility declaration imposes on its station.
mva::AvgResult< T > solver_ctmc_run_analyzer_any(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Solve on whichever path applies and format, mirroring solver_ctmc_run_analyzer.
std::vector< std::vector< std::size_t > > fj_branch_members(const LqnBranchView< T > &lqn, std::size_t joinaidx)
Branch membership of an AND-join.
FJQuorumMomentsResult< T > fj_quorum_moments(const std::vector< T > &branchMeans, const std::vector< T > &branchVars, std::size_t k)
Mean and variance of a k-of-n (quorum) join completion time, from the mean and variance of each branc...
double fluid_default_horizon(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
The horizon a transient runs to when the caller gives none.
FluidLayout fluid_layout(const qn::NetworkStruct< T > &sn)
Port of the layout half of solver_fluid_odes.m.
Definition fluid_odes.h:282
std::vector< FluidTranPoint > solver_fluid_transient(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, double t_end, std::size_t points=101, const std::vector< double > &out_grid=std::vector< double >())
Port of @@SolverFLD/getTranAvg: the metrics along the trajectory, not just at the fixed point.
FluidSolution solver_fluid(const qn::NetworkStruct< T > &sn_in, const FluidOptions &opt, qn::NetworkStruct< T > *sn_out=nullptr)
Port of solver_fluid_analyzer.m: dispatch on the method, refit the non-exponential FCFS stations the ...
FluidPassage fluid_passage_time(const qn::NetworkStruct< T > &sn, const std::vector< double > &x_steady, std::size_t ist, std::size_t cls, double tol=1e-4, std::size_t points=201, const FluidClosure &closure=FluidClosure())
Response-time CDF at station ist for class cls, both 1-based.
FluidSolution solver_fluid_run_analyzer(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, qn::NetworkStruct< T > *sn_out=nullptr, qn::NetworkStruct< T > *refreshed_out=nullptr, solvers::CacheMetrics< T > *cache_out=nullptr)
Port of @@SolverFLD/runAnalyzer.m: resolve the method, route to the function the reference routes to,...
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:472
LqnElement
LQN element kinds, with the values of MATLAB LayeredNetworkElement.
Definition lang_types.h:466
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:469
Distrib< T > aph_fit_mean_scv(const T &mean, const T &scv)
APH.fitMeanAndSCV(MEAN, SCV), through mam::aph_fit_mean_scv.
JobClassType
Job class kinds, with the values of MATLAB JobClassType.
Definition lang_types.h:369
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:731
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
T dist_moment(const Distrib< T > &d, unsigned k)
The k-th raw moment.
void lqn_fwd_rendezvous(LqnStruct< T > &lqn)
Replace every forwarding chain reachable from a synchronous call by caller-side pseudo rendezvous cal...
Definition lqn_helpers.h:77
T lqn_overtake_prob_markov(const LqnStruct< T > &lqn, const std::vector< T > &servt, const std::vector< T > &callresidt, const std::vector< T > &tput, std::size_t eidx, const T &xj)
Overtaking probability at a server entry, through the LQNS phased-server chain rather than the reduce...
LnInterlockChoice ln_interlock_method(const LqnStruct< T > &lqn, const std::string &requested, bool interlocking, const std::string &lnmethod)
Port of matlab/src/solvers/LN/ln_interlock_method.m, asked as a query.
fluid::FluidOptions::RateSched FluidRateSched
One (station, class) rate trajectory injected into a layer's closing ODE.
Definition solver_ln.h:147
LqnRefRoutes< T > lqn_ref_routes(const LqnStruct< T > &lqn, const std::vector< std::size_t > &callers, double maxpaths=32.0, const std::vector< std::size_t > &server_set={})
Resolve the reference routes into the layer whose callers are CALLERS.
LqnBoxBounds< T > lqn_boxbounds(const LqnStruct< T > &lqn)
Evaluate the box bounds of lqn.
MamSolution< T > mam_dispatch(const qn::NetworkStruct< T > &L, const MamOptions &opt_in)
The ladder.
AphPair< T > aph_convseq(const std::vector< AphPair< T > > &seq)
Convolve the sequence, i.e.
Definition aph_convseq.h:37
AphPair< T > aph_simplify(const AphPair< T > &d1, const AphPair< T > &d2, const T &p1, const T &p2, AphPattern pattern)
Compose two matrix-exponential laws, as aph_simplify.m does.
AphFitResult< T > aph_fit(const T &e1, const T &e2, const T &e3, unsigned nmax, const T &tol)
Fit an APH(n) with n <= nmax to the raw moments e1, e2, e3.
Definition aph_fit.h:176
FjMmt< T > fj_mmt(const qn::NetworkStruct< T > &L)
Build the transformed layer.
Definition fj_mmt.h:444
bool mva_carries_interlock(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
True when the MVA path this model already dispatches to carries a class-level interlock matrix (Frank...
MvaSolution< T > fj_fixed_point(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, std::vector< T > &lam, const MvaOptions &opt, InnerSolve inner)
Drive the fork-join fixed point of a transformed model to convergence.
Definition fj_driver.h:435
DispatchResult< T > mva_dispatch(const qn::NetworkStruct< T > &L, const MvaOptions &opt, const Matrix< T > &init_sol)
The ladder itself.
NcSolution< T > solver_nc_solve(const qn::NetworkStruct< T > &L_in, const NcSolverOptions &opt_in)
The gates, the multiserver conversion and the dispatch of @@SolverNC/runAnalyzer.m,...
SensTable< T > solver_sensitivity_table(qn::NetworkStruct< T > &sn, const SensOptions &opt, bool exact_available, const std::function< mva::MvaSolution< T >()> &solve)
Build the sensitivity table of sn under solve.
SsaSolution solver_ssa(const qn::NetworkStruct< T > &sn, const SsaOptions &opt, std::vector< SsaCacheRatio > *cache=nullptr)
@@SolverSSA/runAnalyzer itself: the engine the method selects, then the result assembly the reference...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Port of solver_nc_analyzer.m, solver_ncld_analyzer.m and @@SolverNC/ncDispatch.m: one inner solve,...
One SolverLN layer: a NetworkStruct plus the LQN annotations that say which element of the layered mo...
Total service rate of a station served by heterogeneous server pools with a class-compatibility graph...
Post-MMT node visits of a fork-join model.
Port of solver_ctmc_fcr_waitq.m: the reachability-built generator of a model whose finite capacity re...
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
SolverMVA over a SolverLN layer.
The SolverNC class surface: @@SolverNC/runAnalyzer.m and the gates around it.
Performance sensitivities with respect to service rates.
The SolverSSA entry surface: a port of @@SolverSSA/runAnalyzer.m's method whitelist,...
Where each (station, class) block sits in the state vector.
Definition fluid_odes.h:86
std::vector< std::vector< std::size_t > > qidx
0-based first index of (i,r)
Definition fluid_odes.h:88
std::size_t nstates
length of the state vector
Definition fluid_odes.h:87
std::vector< std::vector< bool > > enabled
whether (i,r) is served at all
Definition fluid_odes.h:90
options.config.rate_sched: explicit per-(station, class) rate trajectories, the third source solver_f...
Controls, defaulting to SolverOptions('Fluid') in the reference.
double tol
absolute and relative tolerance handed to the integrator
What the analyzer returns, in the same shape as the MVA solver's result.
std::vector< double > XN
std::vector< double > xvec
the converged fluid state
std::vector< double > CN
static Distrib exp_rate(const T &r)
Definition lang_types.h:945
static Distrib phase_type(const std::vector< T > &alpha, const Matrix< T > &A, bool acyclic)
PH / APH given by (alpha, A): D0 = A and D1 = (-A e) alpha.
static Distrib disabled_dist()
Definition lang_types.h:988
static T ph_moment(const std::vector< T > &alpha, const Matrix< T > &A, unsigned k)
The k-th raw moment of a phase-type (alpha, A): k!
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:977
static Distrib exp_mean(const T &m)
Definition lang_types.h:930
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:766
static constexpr double FineTol
Definition lang_types.h:760
static constexpr double Zero
Definition lang_types.h:762
static constexpr double CoarseTol
Definition lang_types.h:761
Per-layer results of one iteration, the [QN,UN,RN,TN,AN,WN] of getAvg.
Definition solver_ln.h:365
A CDF sampled on a grid, the [F, t] pair MATLAB's evalCDF returns.
Definition solver_ln.h:386
bool empty() const
Definition solver_ln.h:388
std::vector< double > t
Definition solver_ln.h:387
std::vector< double > cdf
Definition solver_ln.h:387
The interlock tracking method a run will use, and why it differs from the request.
LayeredNetwork.layerBlocks: where each layer's block sits in the aggregate.
Definition solver_ln.h:419
std::vector< std::size_t > msz
Definition solver_ln.h:420
std::vector< std::size_t > roff
Definition solver_ln.h:420
std::vector< std::size_t > coff
Definition solver_ln.h:420
std::vector< std::size_t > ksz
Definition solver_ln.h:420
Options of SolverLN.
Definition solver_ln.h:266
std::size_t tran_points
Output points per layer trajectory, and the relaxation's own grid size.
Definition solver_ln.h:347
std::vector< double > tran_grid
An EXPLICIT output grid for the layered transient, replacing the uniform tran_points one when it is n...
Definition solver_ln.h:360
std::string relax
Definition solver_ln.h:289
fluid::FluidOptions layer_fluid
Options handed to each layer when layer_solver is fluid.
Definition solver_ln.h:310
std::string layer_solver
Which solver runs each layer: mva, nc, fluid or ssa.
Definition solver_ln.h:308
std::string interlock_method
options.config.interlock_method: how the interlock is TRACKED once interlocking is on.
Definition solver_ln.h:280
mva::MvaOptions layer
Options handed to each layer solver; SolverMVA defaults.
Definition solver_ln.h:292
std::string method
options.method, which selects WHAT is reported and not merely how:
Definition solver_ln.h:326
ssa::SsaOptions layer_ssa
Options handed to each layer when layer_solver is ssa.
Definition solver_ln.h:314
std::string ln_transient_channels
options.config.ln_transient_channels: which inter-layer coupling is injected, both,...
Definition solver_ln.h:343
double interlock_maxpaths
options.config.interlock_maxpaths: refpath refuses a layer carrying more routes into it.
Definition solver_ln.h:282
std::string ln_transient
options.config.ln_transient: how the per-layer transients are coupled.
Definition solver_ln.h:333
nc::NcSolverOptions layer_nc
Options handed to each layer when layer_solver is nc.
Definition solver_ln.h:312
std::string interlock_refpath_scope
options.config.interlock_refpath_scope: merging transforms only the layers where two or more callers ...
Definition solver_ln.h:288
double timespan_end
options.timespan(2): the transient horizon; infinite means none is set.
Definition solver_ln.h:345
long ln_transient_iter_max
options.config.ln_transient_iter_max and ..._tol of the relaxation.
Definition solver_ln.h:335
getSensitivityTable of the ensemble: the layer tables under a Layer column.
Definition solver_ln.h:434
std::vector< std::string > layer_methods
Per layer, the branch that layer took; empty for a layer with no solver.
Definition solver_ln.h:441
std::vector< sens::SensTable< T > > layer_tables
Per layer, the analytic Jacobian where that layer produced one.
Definition solver_ln.h:445
std::vector< Row > rows
Definition solver_ln.h:439
std::string method
The summary label: the common branch, or "mixed" when they differ.
Definition solver_ln.h:443
The LQN-level answer, indexed by element 1..nidx.
Definition solver_ln.h:371
std::vector< T > WN
Definition solver_ln.h:372
std::vector< bool > defined_W
Definition solver_ln.h:373
std::vector< bool > defined_U
Definition solver_ln.h:373
std::vector< T > QN
Definition solver_ln.h:372
std::vector< bool > defined_R
Definition solver_ln.h:373
std::vector< T > RN
Definition solver_ln.h:372
std::vector< T > AN
Definition solver_ln.h:372
std::vector< bool > defined_Q
Definition solver_ln.h:373
bool is_bound
True when the numbers are a BOUND (method = mw.upper / mw.lower) rather than the fixed point.
Definition solver_ln.h:382
std::vector< bool > defined_A
Definition solver_ln.h:373
std::vector< T > UN
Definition solver_ln.h:372
std::vector< bool > defined_T
Definition solver_ln.h:373
std::vector< T > TN
Definition solver_ln.h:372
options.config.stochiter_* of SolverOptions.m, with its defaults.
Definition solver_ln.h:464
double a0
Robbins-Monro step immediately after burn-in.
Definition solver_ln.h:466
long conseq
consecutive sub-tolerance iterations required to stop
Definition solver_ln.h:468
long burnin
Picard iterations before the step decay starts.
Definition solver_ln.h:465
double alpha
step decay exponent, in (0.5, 1]
Definition solver_ln.h:467
double relax_burnin
Relaxation in force during burn-in, i.e.
Definition solver_ln.h:471
One layer's block of the layered transient.
Definition solver_ln.h:399
std::vector< std::vector< std::vector< double > > > QN
[station][class][point]
Definition solver_ln.h:402
std::vector< double > t
the output grid, shared by every series below
Definition solver_ln.h:400
std::vector< std::vector< std::vector< double > > > UN
Definition solver_ln.h:402
std::vector< std::vector< std::vector< double > > > TN
Definition solver_ln.h:402
The layered transient: one block per layer, plus how it was produced.
Definition solver_ln.h:425
std::string mode
"coupled" or "decoupled"
Definition solver_ln.h:427
long iterations
waveform-relaxation sweeps; 0 when decoupled
Definition solver_ln.h:428
double gap
final sup-norm trajectory change, coupled only
Definition solver_ln.h:429
std::vector< LnTranLayer > layers
Definition solver_ln.h:426
std::vector< std::size_t > targets
absolute entry indices, in declaration order
Definition lqn_struct.h:204
std::size_t caller
absolute index of the dispatching activity
Definition lqn_struct.h:202
The options SolverMVA reads.
Definition mva_types.h:31
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
std::vector< T > C
Definition mva_types.h:98
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
The name-value contract of getSensitivityTable.
bool simulation
True when the callback is a simulator, which widens the default step.
One (station, class) row of the table.
What the table carries, plus the branch that produced it.
std::vector< SensRow< T > > rows
std::string method
"exact" or "fd", the branch actually taken
Controls, defaulting to SolverOptions('SSA') in the reference.
Definition ssa_types.h:69