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`, `ssa`, `mam`,
67 * `ctmc`, `ldes` or `jmt`, the C++ spelling of the reference's solver FACTORY. They converge to
68 * DIFFERENT fixed points, because each feeds different demands back into the next outer sweep.
69 * `ssa`, `ldes` and `jmt` are noisy, so the deterministic convergence test is replaced by
70 * LnStochController (Robbins-Monro relaxation, Polyak-Ruppert reporting);
71 * running them 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 <type_traits>
98#include <vector>
99
104#include "line/api/lqn/lqn_mol.h"
107#include "line/api/mam/aph_fit.h"
120#include "line/api/lqn/lqn_ph.h"
134#include "line/util/error.h"
135#include "line/util/expm.h"
136#include "line/util/linalg.h"
137#include "line/util/matrix.h"
139
140namespace line {
141namespace ln {
142
143using lang::CallType;
144using lang::Distrib;
145using lang::GlobalConstants;
147using lang::LqnElement;
148using lang::NodeType;
152using lqn::LqnCallGroup;
153using lqn::LqnStruct;
154
155/** One (station, class) rate trajectory injected into a layer's closing ODE. */
157
158namespace detail {
159
160/**
161 * Run the fluid analyzer on one layer and write its metrics into the shape the
162 * MVA layer path returns.
163 *
164 * Overloaded rather than gated: LSODA is double-only, so the template below is
165 * what any other backend resolves to, and it refuses by name instead of failing
166 * to compile inside a branch that could never run.
167 */
168inline void ln_fluid_solve(const qn::NetworkStruct<double>& L, const fluid::FluidOptions& fo,
170 // THE RUNNER, NOT THE SWITCH. `solver_fluid` is the ungated port of
171 // solver_fluid_analyzer.m and cannot reach `minnormal`, `rmf`, `refined` or
172 // `kp` at all; `solver_fluid_run_analyzer` is the port of runAnalyzer's resolution,
173 // which is what `LN(model, @(m) Fluid(m))` runs in the reference. Calling
174 // the switch here silently ran the FIRST-ORDER `matrix` method in every
175 // layer: on lqn_basic that reported T3's residence as its bare demand
176 // (0.02 against MATLAB's 0.0206615) because the hard min charges no
177 // queueing below the server count, and the LN table then disagreed with
178 // MATLAB by 3.3% on a metric neither codebase flagged.
180 for (std::size_t i = 0; i < L.nstations; ++i)
181 for (std::size_t r = 0; r < L.nclasses; ++r) {
182 out.Q(i, r) = s.QN(i, r);
183 out.U(i, r) = s.UN(i, r);
184 out.R(i, r) = s.RN(i, r);
185 out.Tp(i, r) = s.TN(i, r);
186 }
187 for (std::size_t r = 0; r < L.nclasses && r < s.XN.size(); ++r) {
188 out.X[r] = s.XN[r];
189 out.C[r] = s.CN[r];
190 }
191 out.method = s.method;
192 out.iter = static_cast<int>(s.iters);
193}
194template <class T>
195void ln_fluid_solve(const qn::NetworkStruct<T>&, const fluid::FluidOptions&,
197 throw UnsupportedError(ErrorKind{"Arithmetic"},
198 "SolverLN: a fluid layer integrates its drift with LSODA, whose coefficients assume "
199 "double precision; rerun with --arith double or use layer_solver 'mva'");
200}
201
202/**
203 * The response-time CDF of every (station, class) of one layer, which is what
204 * `SolverFluid(ensemble{e}).getCdfRespT` returns in the reference.
205 *
206 * The layer is integrated once to its fixed point and the converged state is
207 * the marking the passage starts from, so the law reported is the STATIONARY
208 * response time and not the one seen from an arbitrary initial condition. That
209 * is `options.init_sol = odeStateVec` in `@@SolverFLD/getCdfRespT`.
210 *
211 * A pair the class never visits comes back as the degenerate curve at zero,
212 * which `fluid_passage_time` already returns; the moment extraction above reads
213 * it as a mean of zero and the caller drops the term, as MATLAB's
214 * `m1 > CoarseTol` test does.
215 */
216inline std::vector<std::vector<fluid::FluidPassage>> ln_fluid_cdf_respt(
218 fluid::FluidOptions o = fo;
219 // THE RUNNER, as in ln_fluid_solve: @SolverFLD/getCdfRespT takes the fixed
220 // point and its closure variance from the RESOLVED method. The first-order
221 // switch charges no queueing below the server count, so on a single-server
222 // PS host at 0.88 jobs it returned the bare Exp(1) demand as the activity's
223 // law (mean 1.001 against MATLAB's 1.546) and the entry 8.09 against 8.637.
224 // With the runner the randomLQN model of test_ln_methods.cpp reads E:e0
225 // 2.2752 against MATLAB's 2.2750 (once MATLAB's APH.evalCDF stopped hanging).
227 std::vector<std::vector<fluid::FluidPassage>> out(
228 L.nstations, std::vector<fluid::FluidPassage>(L.nclasses));
229 for (std::size_t i = 1; i <= L.nstations; ++i) {
230 if (L.stations[i - 1].nodetype == qn::NodeType::Source) continue;
231 for (std::size_t r = 1; r <= L.nclasses; ++r) {
232 if (L.disabled[i - 1][r - 1]) continue;
233 out[i - 1][r - 1] = fluid::fluid_passage_time(L, s.xvec, i, r, o.tol, 201, s.closure);
234 }
235 }
236 return out;
237}
238template <class T>
239std::vector<std::vector<fluid::FluidPassage>> ln_fluid_cdf_respt(const qn::NetworkStruct<T>&,
240 const fluid::FluidOptions&) {
241 throw UnsupportedError(ErrorKind{"Arithmetic"},
242 "SolverLN: the response-time CDF a layer contributes to the 'moment3' update is a fluid "
243 "passage time, integrated by LSODA in double precision; rerun with --arith double");
244}
245
246/**
247 * One layer's transient trajectory, the `getTranAvg` of a layer.
248 *
249 * `out_grid` REPLACES the uniform grid, and it is not a convenience: SolverENV
250 * sums a layered stage's exit average against the environment's holding-time
251 * CDF, so the trajectory has to be evaluated where that CDF puts its mass
252 * rather than where the horizon happens to spread points. It also makes every
253 * layer of one stage report on the SAME grid, which is what lets the layers be
254 * assembled into one block-diagonal transient with a single time base.
255 */
256inline std::vector<fluid::FluidTranPoint> ln_fluid_transient(
257 const qn::NetworkStruct<double>& L, const fluid::FluidOptions& fo, double t_end,
258 std::size_t points, const std::vector<double>& out_grid = std::vector<double>()) {
259 return fluid::solver_fluid_transient(L, fo, t_end, points, out_grid);
260}
261template <class T>
262std::vector<fluid::FluidTranPoint> ln_fluid_transient(const qn::NetworkStruct<T>&,
263 const fluid::FluidOptions&, double,
264 std::size_t,
265 const std::vector<double>& =
266 std::vector<double>()) {
267 throw UnsupportedError(ErrorKind{"Arithmetic"},
268 "SolverLN: the layered transient integrates each layer's drift with LSODA, which is "
269 "double precision by construction; rerun with --arith double");
270}
271
272} // namespace detail
273
274/** Options of SolverLN. Defaults are SolverOptions('LN'). */
275struct LnOptions {
276 int iter_max = 200;
277 double iter_tol = 5e-3;
278 double tol = 1e-4;
279 bool interlocking = true;
280 /**
281 * `options.config.interlock_method`: how the interlock is TRACKED once
282 * `interlocking` is on. `ilrate` is the post-hoc rate discount of
283 * update_populations (the default and the historical method); `refpath`
284 * merges the callers that are the same reference customers into ONE client
285 * chain running down the reference path, replacing the discount; `none`
286 * applies no correction. A request refpath cannot carry DOWNGRADES to
287 * `ilrate`, see ln_interlock_method.
288 */
289 std::string interlock_method = "ilrate";
290 /** `options.config.interlock_maxpaths`: refpath refuses a layer carrying more routes into it. */
291 double interlock_maxpaths = 32.0;
292 /**
293 * `options.config.interlock_refpath_scope`: `merging` transforms only the
294 * layers where two or more callers descend from one reference task; `all`
295 * also those with a lone caller below a reference task.
296 */
297 std::string interlock_refpath_scope = "merging";
298 std::string relax = "fixed";
299 double relax_factor = 0.5;
300 /** Options handed to each layer solver; SolverMVA defaults. */
302 /**
303 * Which solver runs each layer: `mva`, `nc`, `fluid`, `ssa`, `mam`, `ctmc`,
304 * `ldes` (the native in-process LDES engine) or `jmt` (JSIM, as a subprocess).
305 *
306 * The reference names this by passing a solver FACTORY,
307 * `LN(model, @(m) Fluid(m, opt))`, so the choice is per-ensemble and not
308 * per-layer; this field is the same choice under a name, because a C++
309 * template cannot take a MATLAB function handle.
310 *
311 * THEY DO NOT CONVERGE TO THE SAME PLACE. Each engine feeds different
312 * service demands back into the next outer iteration, so the ensembles
313 * reach different fixed points rather than the same one by different
314 * routes. `ssa` in particular is a NOISY layer solver: the deterministic
315 * convergence test cannot terminate against its standard error, so it is
316 * driven with `LnStochController` (lqn_analyzers.h) instead.
317 */
318 std::string layer_solver = "mva";
319 /** Options handed to each layer when `layer_solver` is `fluid`. */
321 /** Options handed to each layer when `layer_solver` is `nc`. */
323 /** Options handed to each layer when `layer_solver` is `ssa`. */
325 /** Options handed to each layer when `layer_solver` is `mam` (SolverMAM's own default method). */
327 /**
328 * Options handed to each layer when `layer_solver` is `ctmc`, and to a
329 * Region layer, which goes to CTMC whatever the layer solver is.
330 */
332 /**
333 * Options handed to each layer when `layer_solver` is `ldes`. `seed` is the BASE of the
334 * reference's Robbins-Monro schedule: layer e (1-based) of iteration it runs at
335 * `seed + (it-1)*nlayers + e`, as SolverLN.pre does for every stochastic layer.
336 */
338 /** Options handed to each layer when `layer_solver` is `jmt` (JSIM); `seed` as for `layer_ldes`. */
340 /**
341 * `options.method`, which selects WHAT is reported and not merely how:
342 *
343 * `default` the mean-based update (updateMetricsDefault)
344 * `moment3` the APH moment-based update (updateMetricsMomentBased),
345 * which additionally produces a per-entry response-time
346 * distribution, `get_cdf_respt()`
347 * `mw.upper` Majumdar-Woodside robust box bounds INSTEAD of a fixed
348 * `mw.lower` point: no layer is ever solved, and every metric the
349 * bound does not define is reported as undefined
350 */
351 std::string method = "default";
352 /**
353 * `options.config.ln_transient`: how the per-layer transients are coupled.
354 * `decoupled` freezes the inter-layer demands at the converged fixed point;
355 * `coupled` reconciles them by waveform relaxation. Iteration 0 of the
356 * coupled relaxation IS the decoupled answer.
357 */
358 std::string ln_transient = "coupled";
359 /** `options.config.ln_transient_iter_max` and `..._tol` of the relaxation. */
361 double ln_transient_tol = 1e-2;
362 /**
363 * `options.config.ln_transient_channels`: which inter-layer coupling is
364 * injected, `both`, `thinkt` (client delay only) or `callservt`
365 * (synchronous-call service only). Used to isolate each channel's share of
366 * the coupled transient.
367 */
368 std::string ln_transient_channels = "both";
369 /** `options.timespan(2)`: the transient horizon; infinite means none is set. */
370 double timespan_end = std::numeric_limits<double>::infinity();
371 /** Output points per layer trajectory, and the relaxation's own grid size. */
372 std::size_t tran_points = 101;
373 /**
374 * An EXPLICIT output grid for the layered transient, replacing the uniform
375 * `tran_points` one when it is not empty.
376 *
377 * SolverENV is what asks for it: a layered stage's exit metric is a
378 * Riemann-Stieltjes sum against the environment's holding-time CDF, and a
379 * uniform grid over the horizon resolves the horizon rather than the
380 * sojourn. Interpolating afterwards cannot recover resolution the
381 * trajectory never had, so the points are asked for instead --
382 * `SolverEnv::stage_grid` builds them and `set_tran_grid` installs them.
383 * The grid must be increasing and start at zero; LSODA takes it as given.
384 */
385 std::vector<double> tran_grid;
386 /**
387 * `options.config.lld`: a smooth queue-dependent rate multiplier per
388 * station, `{processor or task name: expression in n}`, read by
389 * method='nlp' and by warmstart='nlp' only. See lqn_export_nlp.h.
390 */
391 std::map<std::string, ::line::lqn::NlpLld> lld;
392 /** `options.config.nlp_alpha`: sharpness of the softmin standing in for min(n, m). */
393 double nlp_alpha = 20.0;
394 /** `options.config.nlp_warm`: the warm-start sweeps the program's Newton ladder begins from. */
395 int nlp_warm = 0;
396 /** `options.config.nlp_maxiter`: budget of the descent the root-find falls back to. */
397 int nlp_maxiter = 3000;
398 /** `options.config.nlp_fork`: 'refuse' an AND-fork block, or price it at E[max] of exponentials ('exp'). */
399 std::string nlp_fork = "refuse";
400 /**
401 * `options.config.warmstart`: empty for a cold start, or 'nlp' / 'mol' to
402 * seed a LAYERED method's fixed point with the QD-AMVA program's solution or
403 * the Method of Layers'. Refused with method='nlp', which runs no iteration.
404 */
405 std::string warmstart;
406 /** `options.config.mol_iter_max`, `mol_iter_tol`, `mol_relax_factor`: lqn_mol's own settings for warmstart='mol'. */
407 std::size_t mol_iter_max = 200;
408 double mol_iter_tol = 1e-6;
409 double mol_relax_factor = 0.5;
410};
411
412/** Per-layer results of one iteration, the [QN,UN,RN,TN,AN,WN] of getAvg. */
413template <class T>
416};
417
418/** The LQN-level answer, indexed by element 1..nidx. */
419template <class T>
421 std::vector<T> QN, UN, RN, TN, AN, WN;
423 int iterations = 0;
424 bool converged = false;
425 /**
426 * True when the numbers are a BOUND (`method` = `mw.upper` / `mw.lower`)
427 * rather than the fixed point. A bound defines throughput and processor
428 * utilization and nothing else, so the other four measures come back
429 * undefined; reporting zeros there would be a claim.
430 */
431 bool is_bound = false;
432 /** A warning the analysis raised, verbatim, empty when it raised none (method='nlp' off a root). */
433 std::string warning;
434};
435
436/** A CDF sampled on a grid, the [F, t] pair MATLAB's evalCDF returns. */
437struct LnCdf {
438 std::vector<double> t, cdf;
439 bool empty() const { return t.empty(); }
440};
441
442/**
443 * One layer's block of the layered transient.
444 *
445 * The reference assembles the layers into one BLOCK-DIAGONAL cell array whose
446 * off-diagonal blocks are empty, so the blocks themselves carry the whole
447 * answer; keeping them apart also keeps each layer's own time grid, which the
448 * block-diagonal form has nowhere to put.
449 */
451 std::vector<double> t; ///< the output grid, shared by every series below
452 /// [station][class][point]
453 std::vector<std::vector<std::vector<double>>> QN, UN, TN;
454};
455
456/**
457 * `LayeredNetwork.layerBlocks`: where each layer's block sits in the aggregate.
458 *
459 * An LQN has no stations and classes of its own -- SolverLN builds them, one
460 * network per layer -- so the flat (station, class) view a caller like
461 * SolverENV needs is the BLOCK-DIAGONAL UNION of the layer networks, in layer
462 * order. `roff[e]`/`coff[e]` are layer e's 0-based row/column offset in that
463 * union and `msz[e]`/`ksz[e]` its size; `M`/`K` are the totals.
464 *
465 * The off-diagonal blocks pair a station of one layer with a class of another
466 * and stand for nothing at all. The reference leaves them as EMPTY cells; this
467 * port fills them with zeros, which is what the reference's consumers make of
468 * an empty cell (`tranTimeBase_` returns nothing and the metric stays 0).
469 */
471 std::vector<std::size_t> roff, coff, msz, ksz;
472 std::size_t M = 0, K = 0;
473};
474
475/** The layered transient: one block per layer, plus how it was produced. */
477 std::vector<LnTranLayer> layers;
478 std::string mode; ///< "coupled" or "decoupled"
479 long iterations = 0; ///< waveform-relaxation sweeps; 0 when decoupled
480 double gap = 0.0; ///< final sup-norm trajectory change, coupled only
481};
482
483/** getSensitivityTable of the ensemble: the layer tables under a Layer column. */
484template <class T>
486 struct Row {
487 std::string layer, station, jobclass;
489 };
490 std::vector<Row> rows;
491 /** Per layer, the branch that layer took; empty for a layer with no solver. */
492 std::vector<std::string> layer_methods;
493 /** The summary label: the common branch, or "mixed" when they differ. */
494 std::string method;
495 /** Per layer, the analytic Jacobian where that layer produced one. */
496 std::vector<sens::SensTable<T>> layer_tables;
497};
498
499/**
500 * Overtaking probability at a server entry, defined in lqn_analyzers.h.
501 *
502 * DECLARED, not included: lqn_analyzers.h needs LayerResult and SolverLN
503 * complete (LnStochController holds a vector of the first and two refusals take
504 * the second), so it must be parsed AFTER this class. The definition arrives
505 * through the include at the foot of this file, which is why the declaration
506 * has to stand here -- an unqualified call from a member function would
507 * otherwise find nothing, LqnStruct's associated namespace being line::lqn.
508 */
509template <class T>
510T lqn_overtake_prob_markov(const LqnStruct<T>& lqn, const std::vector<T>& servt,
511 const std::vector<T>& callresidt, const std::vector<T>& tput,
512 std::size_t eidx, const T& xj);
513
514/** `options.config.stochiter_*` of SolverOptions.m, with its defaults. */
516 long burnin = 5; ///< Picard iterations before the step decay starts
517 double a0 = 1.0; ///< Robbins-Monro step immediately after burn-in
518 double alpha = 0.6; ///< step decay exponent, in (0.5, 1]
519 long conseq = 3; ///< consecutive sub-tolerance iterations required to stop
520 double iter_tol = 5e-3;
521 /** Relaxation in force during burn-in, i.e. whatever init left in place. */
522 double relax_burnin = 1.0;
523};
524
525/**
526 * The Robbins-Monro / Polyak-Ruppert controller, defined in lqn_analyzers.h.
527 *
528 * DECLARED HERE, not included: same mutual dependence as above. `iterate` holds
529 * one through a shared_ptr rather than by value so that this class stays
530 * complete without it -- a by-value member would need the definition at the
531 * point the class template is instantiated, and the definition arrives at the
532 * foot of this file.
533 *
534 * ITS CONFIG CANNOT BE DEFERRED THE SAME WAY. `iterate` names LnStochConfig by
535 * value, and that name does not depend on T, so it is looked up and required
536 * COMPLETE when the template is parsed rather than when it is instantiated --
537 * the shared_ptr trick only defers the class template beside it.
538 */
539template <class T>
541
542template <class T>
543class SolverLN {
544public:
545 /**
546 * Port of `SolverLN.listValidMethods`.
547 *
548 * Each name states the LAYERING and the ENCODING; `ln_requested_method`
549 * normalises the alias spellings ("ph", "cs", "srvncs", "flatcs",
550 * "squashed", "squashed.ph") onto these, and they are left out here to keep
551 * the list unambiguous, exactly as the reference does.
552 */
553 static std::vector<std::string> list_valid_methods() {
554 // the names and their order are this solver's row of the method registry (doc/line-methods.json)
555 return ::line::method_registry::methods("LN");
556 }
557
558 SolverLN(const LqnStruct<T>& lqn_in, const LnOptions& options) : lqn(lqn_in), opt(options) {
559 // A LAYER SOLVER NEVER NARRATES, the same invariant the MATLAB, JAR and
560 // python ports stamp on each constructed layer solver. The fixed point
561 // runs every layer once per iteration, so a layer left at the caller's
562 // verbosity would print its own banner nlayers*iter_max times and bury
563 // the layered narration the caller actually asked for. `ssa` is the one
564 // layer engine here carrying a verbosity knob of its own; the level is
565 // forced rather than trusted so a library caller cannot set it.
566 // SolverLN's own reporting is unaffected.
567 opt.layer_ssa.verbose = false;
568 // interlock_method itself is validated by ln_interlock_method, where it is resolved
569 for (char& ch : opt.interlock_refpath_scope)
570 ch = char(std::tolower(static_cast<unsigned char>(ch)));
571 if (opt.interlock_refpath_scope != "merging" && opt.interlock_refpath_scope != "all")
572 throw InputError("Unknown config.interlock_refpath_scope '" +
573 opt.interlock_refpath_scope + "', use 'merging' or 'all'.");
574 if (std::isnan(opt.interlock_maxpaths) || opt.interlock_maxpaths < 0.0)
575 throw InputError("config.interlock_maxpaths must be a non-negative number.");
576 validate_nlp_options();
577 construct();
578 }
579
580 /** True once init() seeded the iterate from config.warmstart; converged() then drops iter_min. */
581 bool warmstarted() const { return warmstarted_; }
582 /** method='nlp': the program's solution, clipped at zero, in the program's variable order. */
583 const std::vector<double>& nlp_solution() const { return nlp_solution_; }
584 /** method='nlp': the program's objective at nlp_solution(). */
585 double nlp_objective() const { return nlp_objective_; }
586
587 /**
588 * The QD-AMVA program of this model under the solver's nlp options, the
589 * program method='nlp' roots and warmstart='nlp' seeds from.
590 */
593 o.lld = opt.lld;
594 o.alpha = opt.nlp_alpha;
595 o.fork = opt.nlp_fork;
596 return ::line::lqn::lqn_nlp_program(lqn, o);
597 }
598
599 /** Port of getEnsembleAvg: run the iteration and aggregate onto LQN elements. */
601 // getAvgTable.m:611-624 answers the two bound requests WITHOUT running
602 // the fixed point at all: a box bound is a statement about the model,
603 // not about an iterate, so solving first and discarding the answer would
604 // only cost time and invite the two to be confused.
605 if (opt.method == "mw.upper" || opt.method == "mw.lower") return box_bounds();
606 // method='nlp' builds no layers and runs no fixed point: the model is one program
607 if (lnmethod == "nlp") return nlp_analyzer();
608 // with every reference task trapped at a zero-capacity element there is nothing to layer
609 if (!zerocap.live) return trapped_overlay(aggregate());
610 iterate();
611 // the layers of "srvn.ph" carry one class per caller task, so the
612 // per-element results are rebuilt analytically -- see aggregate_ph
613 return trapped_overlay(is_ph_encoding() ? aggregate_ph() : aggregate());
614 }
615
616 /**
617 * Nested blocking of the reference tasks trapped at a zero-capacity element
618 * (ln_zero_capacity): every element on a blocked call path holds the requests
619 * stuck below it and never completes one. A processor keeps its undefined
620 * queue length. Port of trappedOverlay in @SolverLN/getEnsembleAvg.m.
621 */
623 if (!zerocap.any_trapped()) return s;
624 const T inf = num_traits<T>::from_double(std::numeric_limits<double>::infinity());
625 for (std::size_t idx = 1; idx <= lqn.nidx; ++idx) {
626 if (!zerocap.onroute[idx]) continue;
627 const T held = num_traits<T>::from_double(zerocap.held[idx]);
628 switch (lqn.type[idx]) {
629 case LqnElement::TASK:
630 // a task the live part also reaches keeps its own requests and rate
631 if (ignore[idx] || !s.defined_Q[idx]) s.QN[idx] = Tzero();
632 s.QN[idx] = T(s.QN[idx] + held);
633 s.defined_Q[idx] = true;
634 if (ignore[idx] && !lqn.isref[idx]) {
635 s.WN[idx] = inf;
636 s.defined_W[idx] = true;
637 }
638 break;
639 case LqnElement::ENTRY:
640 case LqnElement::ACTIVITY:
641 s.QN[idx] = held;
642 s.RN[idx] = inf;
643 s.TN[idx] = Tzero();
644 s.defined_Q[idx] = s.defined_R[idx] = s.defined_T[idx] = true;
645 if (lqn.type[idx] == LqnElement::ACTIVITY) {
646 s.WN[idx] = inf;
647 s.defined_W[idx] = true;
648 }
649 break;
650 default:
651 break;
652 }
653 }
654 return s;
655 }
656
657 /**
658 * Port of @@SolverLN/getCdfRespT: the per-entry response-time distribution.
659 *
660 * ONLY THE `moment3` METHOD PRODUCES ONE. The reference reacts to an empty
661 * table by re-running getAvg under method='moment3' and restoring the
662 * caller's method afterwards, and so does this: the distribution is the
663 * whole point of that method, and the mean-based update has no distribution
664 * to report, not a coarser one.
665 *
666 * Indexed by entry NUMBER 1..nentries, as the reference's
667 * `entrycdfrespt{eidx - (nhosts+ntasks)}` is.
668 */
669 std::vector<LnCdf> get_cdf_respt() {
670 if (lnmethod == "nlp")
671 throw UnsupportedError(ErrorKind{"Analysis"},
672 "method='nlp' solves for means only and builds no layers, so there is no response-time "
673 "distribution to report. Use method='moment3' for getCdfRespT.");
674 if (lqn.nentries == 0) return entrycdfrespt;
675 if (entrycdfrespt.size() <= lqn.nentries || entrycdfrespt[1].empty()) {
676 // The distribution pass reads the ROUTING encoding of the activity
677 // graph, which srvn.ph layers do not carry: re-running the iteration
678 // over them would reconstruct the wrong topology rather than a
679 // coarser answer, and returning the empty table would report no
680 // distribution at all. Refuse by name, as the reference does
681 // (@SolverLN/getCdfRespT).
682 if (is_ph_encoding())
683 throw UnsupportedError(ErrorKind{"Method"},
684 "getCdfRespT needs the routing encoding of the activity graph, which "
685 "method='srvn.ph' does not build. Rebuild the solver with "
686 "method='srvn.cs' or method='moment3'.");
687 // BOTH the option and the RESOLVED method have to move. update_metrics
688 // dispatches on `lnmethod`, which build_layers resolved once, so
689 // flipping `opt.method` alone leaves the mean-based update in place and
690 // the table empty -- which is what this getter did between the alias
691 // landing and 2026-08-11. The routing layers already built serve
692 // moment3 unchanged, so only the update pass changes.
693 const std::string saved = opt.method;
694 const std::string saved_resolved = lnmethod;
695 opt.method = "moment3";
696 lnmethod = "moment3";
697 iterate();
698 aggregate();
699 opt.method = saved;
700 lnmethod = saved_resolved;
701 }
702 return entrycdfrespt;
703 }
704
705 /**
706 * Port of @@SolverLN/getTranAvg: the block-diagonal aggregate transient.
707 *
708 * `opt.ln_transient` selects the coupling; both modes return the same
709 * layout, and iteration 0 of the coupled relaxation IS the decoupled
710 * answer.
711 */
713 if (lnmethod == "nlp")
714 throw UnsupportedError(ErrorKind{"Analysis"},
715 "method='nlp' states the steady state as one program and builds no layers, so there is "
716 "no layer transient to integrate. Use a layered method ('srvn.cs', 'flat.cs') for getTranAvg.");
717 if (opt.ln_transient == "decoupled") return tran_avg_decoupled();
718 if (opt.ln_transient == "coupled") return tran_avg_coupled();
719 throw InputError("SolverLN: unknown ln_transient mode '" + opt.ln_transient +
720 "' (use 'coupled' or 'decoupled')");
721 }
722
723 /**
724 * Port of @@SolverLN/getSensitivityTable: solve the ensemble, then
725 * concatenate each layer solver's own table under a leading Layer column.
726 *
727 * WHAT THESE DERIVATIVES MEAN, and the reference is emphatic about it: each
728 * entry is a partial derivative WITHIN ITS LAYER, taken with the layer
729 * parameters the fixed point produced held fixed. Perturbing a host demand
730 * moves the think times, populations and demands of every other layer
731 * through the fixed-point map, and that indirect term is NOT included. The
732 * table attributes a bottleneck inside a layer; it does not predict the
733 * effect of a parameter change on the solved layered model.
734 */
736 // the derivatives are each layer solver's own, and an empty table would read as "none here"
737 if (lnmethod == "nlp")
738 throw UnsupportedError(ErrorKind{"Analysis"},
739 "method='nlp' states the model as one program and builds no layers, so there are no "
740 "per-layer sensitivities to report. Use a layered method ('srvn', 'srvn.ph', 'flat.cs') "
741 "for getSensitivityTable.");
742 if (results.empty()) iterate();
743 LnSensTable<T> out;
744 out.layer_methods.assign(ensemble.size(), std::string());
745 out.layer_tables.resize(ensemble.size());
746 for (std::size_t e = 0; e < ensemble.size(); ++e) {
747 sens::SensOptions so = sopt;
748 so.simulation = layer_solver_is_stochastic();
749 const bool exact_available =
750 opt.layer_solver == "mva" || opt.layer_solver == "nc";
752 ensemble[e], so, exact_available && !fj_tr[e].active(),
753 [this, e]() { return this->solve_layer(e); });
754 out.layer_methods[e] = t.method;
755 for (const sens::SensRow<T>& r : t.rows) {
756 typename LnSensTable<T>::Row row;
757 row.layer = ensemble[e].name;
758 row.station = r.station;
759 row.jobclass = r.jobclass;
760 row.dTput = r.dTput;
761 row.dRespT = r.dRespT;
762 row.dQLen = r.dQLen;
763 row.dUtil = r.dUtil;
764 out.rows.push_back(row);
765 }
766 out.layer_tables[e] = t;
767 }
768 // One branch label per layer, plus a summary that is `mixed` when the
769 // layers did not all take the same branch.
770 for (const std::string& m : out.layer_methods) {
771 if (m.empty()) continue;
772 if (out.method.empty()) out.method = m;
773 else if (out.method != m) out.method = "mixed";
774 }
775 return out;
776 }
777
778 /**
779 * Majumdar-Woodside robust box bounds, reported in the shape of a solution.
780 *
781 * `mw.upper` reports the upper bound of both throughput and processor
782 * utilization, `mw.lower` the lower bound of both. Everything the bound
783 * does not define -- queue length, response time, residence time, arrival
784 * rate -- is left UNDEFINED rather than zeroed.
785 */
787 const bool upper = opt.method != "mw.lower";
788 // Fully qualified: the member `lqn` shadows the namespace of the same
789 // name inside this class, so an unqualified `lqn::` would not compile.
790 const ::line::lqn::LqnBoxBounds<T> b = ::line::lqn::lqn_boxbounds(lqn);
792 const std::size_t N = lqn.nidx;
793 auto blank = [&](std::vector<T>& v, std::vector<bool>& d) {
794 v.assign(N + 1, Tzero());
795 d.assign(N + 1, false);
796 };
797 blank(s.QN, s.defined_Q);
798 blank(s.UN, s.defined_U);
799 blank(s.RN, s.defined_R);
800 blank(s.TN, s.defined_T);
801 blank(s.AN, s.defined_A);
802 blank(s.WN, s.defined_W);
803 for (std::size_t i = 1; i <= N; ++i) {
804 if (b.defined_T[i]) {
805 s.TN[i] = upper ? b.TN_up[i] : b.TN_lo[i];
806 s.defined_T[i] = true;
807 }
808 if (b.defined_U[i]) {
809 s.UN[i] = upper ? b.UN_up[i] : b.UN_lo[i];
810 s.defined_U[i] = true;
811 }
812 }
813 s.iterations = 0;
814 s.converged = true; // a bound needs no fixed point to have converged to
815 s.is_bound = true;
816 return s;
817 }
818
819 std::size_t nlayers() const { return ensemble.size(); }
820 const std::vector<qn::Layer<T>>& layers() const { return ensemble; }
821
822 /**
823 * Port of `LayeredNetwork.layerBlocks`: the block-diagonal layout of the
824 * layers in the aggregate (station x class) view.
825 *
826 * Available as soon as the solver is constructed -- `build_layers` runs in
827 * the constructor -- which is what lets SolverENV compare the shapes of its
828 * stages before solving any of them.
829 */
832 const std::size_t E = ensemble.size();
833 b.roff.assign(E, 0);
834 b.coff.assign(E, 0);
835 b.msz.assign(E, 0);
836 b.ksz.assign(E, 0);
837 for (std::size_t e = 0; e < E; ++e) {
838 b.roff[e] = b.M;
839 b.coff[e] = b.K;
840 b.msz[e] = ensemble[e].nstations;
841 b.ksz[e] = ensemble[e].nclasses;
842 b.M += b.msz[e];
843 b.K += b.ksz[e];
844 }
845 return b;
846 }
847
848 /**
849 * Port of `getBlockAvg`: the layers' own STEADY averages, in the
850 * block-diagonal layout of `layer_blocks` (each off-block cell 0).
851 *
852 * This is not `get_ensemble_avg`, whose answer is indexed by LQN element and
853 * carries no station or class meaning. It reads the last iterate of the
854 * fixed point and does NOT run it, so `get_ensemble_avg` must have run. A
855 * non-finite cell is an ABSENT station-class pair and is returned as 0.
856 */
857 void get_block_avg(Matrix<T>& QN, Matrix<T>& UN, Matrix<T>& TN) const {
858 if (results.empty() || results.back().size() < ensemble.size())
859 throw InputError("SolverLN::get_block_avg reads the layer averages of the last fixed-point "
860 "iterate; call get_ensemble_avg first");
861 const LnLayerBlocks b = layer_blocks();
862 const T zero = num_traits<T>::from_double(0.0);
863 QN = Matrix<T>(b.M, b.K, zero);
864 UN = Matrix<T>(b.M, b.K, zero);
865 TN = Matrix<T>(b.M, b.K, zero);
866 auto put = [&](Matrix<T>& D, const Matrix<T>& S, std::size_t e) {
867 for (std::size_t i = 0; i < b.msz[e] && i < S.rows(); ++i)
868 for (std::size_t r = 0; r < b.ksz[e] && r < S.cols(); ++r)
869 if (std::isfinite(num_traits<T>::to_double(S(i, r))))
870 D(b.roff[e] + i, b.coff[e] + r) = S(i, r);
871 };
872 for (std::size_t e = 0; e < ensemble.size(); ++e) {
873 const LayerResult<T>& r = results.back()[e];
874 put(QN, r.QN, e);
875 put(UN, r.UN, e);
876 put(TN, r.TN, e);
877 }
878 }
879
880 /**
881 * Port of `LayeredNetwork.initFromMarginal`: split an aggregate (M x K)
882 * mean queue-length matrix into per-layer blocks and warm-start each layer
883 * from its own.
884 *
885 * WHY THIS IS RECORDED AND NOT APPLIED HERE, which is the trap the JAR and
886 * python ports both hit: the layered fixed point RESETS its layers as it
887 * converges, so a warm start installed before the solve does not survive
888 * it. The blocks are therefore kept and replayed by `run_layer_transient`,
889 * i.e. after the fixed point and immediately before each layer's transient
890 * -- the steady solve ignores an initial state, the transient does not.
891 *
892 * THE BLOCK IS NOT ROUNDED. The reference rounds a replayed block onto the
893 * integer lattice for every layer engine EXCEPT the fluid one, and the
894 * layered transient exists only over fluid layers (`require_transient_ready`
895 * refuses the rest), so the branch that would round has no reachable case
896 * here. Rounding a fluid state would quantize the very quantity SolverENV's
897 * fixed point is iterating on.
898 *
899 * An empty matrix clears the warm start, so a caller can put the layers
900 * back on their default initial state without rebuilding the solver.
901 */
903 const std::size_t E = ensemble.size();
904 layer_tran_init.assign(E, std::vector<double>());
905 if (n.empty()) return;
906 const LnLayerBlocks b = layer_blocks();
907 if (n.rows() != b.M || n.cols() != b.K)
908 throw InputError(
909 "SolverLN::init_from_marginal: the marginal is " + std::to_string(n.rows()) + "x" +
910 std::to_string(n.cols()) + " where the block-diagonal union of the layers is " +
911 std::to_string(b.M) + "x" + std::to_string(b.K) +
912 "; the aggregate view is layerBlocks', not the LQN element count");
913 for (std::size_t e = 0; e < E; ++e) {
914 const qn::Layer<T>& L = ensemble[e];
916 std::vector<double> y(lay.nstates, 0.0);
917 bool any = false;
918 for (std::size_t i = 0; i < L.nstations && i < b.msz[e]; ++i)
919 for (std::size_t r = 0; r < L.nclasses && r < b.ksz[e]; ++r) {
920 if (!lay.enabled[i][r]) continue;
921 const double q = std::max(0.0, n(b.roff[e] + i, b.coff[e] + r));
922 y[lay.qidx[i][r]] = q;
923 if (q > 0.0) any = true;
924 }
925 // An all-zero block is NOT a warm start: it is the absence of one,
926 // and installing it would empty a closed layer whose population the
927 // fixed point conserves. The reference reaches the same place by
928 // never calling initFromMarginal before the first stage solve.
929 if (any) layer_tran_init[e] = y;
930 }
931 }
932
933 /** Install the explicit output grid of the layered transient; see `LnOptions::tran_grid`. */
934 void set_tran_grid(const std::vector<double>& g) { opt.tran_grid = g; }
935
936 /** Diagnostic access to the per-iteration layer results, for the regression. */
937 const std::vector<std::vector<LayerResult<T>>>& iteration_results() const { return results; }
938 const std::vector<T>& state_servt() const { return servt; }
939 const std::vector<T>& state_residt() const { return residt; }
940 const std::vector<T>& state_tput() const { return tput; }
941 const std::vector<T>& state_thinkt() const { return thinkt; }
942 const std::vector<T>& state_callservt() const { return callservt; }
943 const std::vector<T>& state_callresidt() const { return callresidt; }
944 const std::vector<Distrib<T>>& state_servtproc() const { return servtproc; }
945 const std::vector<Distrib<T>>& state_thinktproc() const { return thinktproc; }
946 const std::vector<Distrib<T>>& state_callservtproc() const { return callservtproc; }
947 const std::vector<T>& state_util() const { return util; }
948 /** Per iteration, the layer-summed max queue-length change the convergence test reads (index 0 unused). */
949 const std::vector<double>& state_maxitererr() const { return maxitererr; }
950 /** The method the layers were built for: "srvn.ph", "srvn.cs" or "moment3". */
951 const std::string& state_lnmethod() const { return lnmethod; }
952 /**
953 * The interlock tracking method this run USES: "ilrate", "refpath" or
954 * "none". Recorded by build_layers and, when no layer merged a reference
955 * chain, demoted from "refpath" to "ilrate" at the end of construct.
956 */
957 const std::string& state_interlock_method() const { return interlockMethod; }
958 /** Per reference-path stage key, the stage mean the last iteration installed. */
959 const std::vector<double>& state_refpath_stages() const { return refpath_stage_prev; }
960
961 /**
962 * Port of SolverLN.interlockMethodFor: the method a solve would use, asked
963 * through the same query build_layers asks. Like the reference it does not
964 * see the construct-time demotion of a refpath that merged nothing; that
965 * recorded answer is state_interlock_method().
966 */
968 return ln_interlock_method(lqn, opt.interlock_method, opt.interlocking, lnmethod);
969 }
970
971private:
972 // state
975
976 std::vector<qn::Layer<T>> ensemble; ///< compacted, layer e is ensemble[e]
977 std::vector<long> idxhash; ///< (nidx+1) element -> layer index, -1 if none
978 std::vector<bool> ignore; ///< (nidx+1) element in a component with no ref task
979 LnZeroCapacity zerocap; ///< reference tasks trapped at a zero-capacity element
980
981 // update maps, rows of [layerElement, elementOrCall, node, class]
982 struct UpdRow {
983 std::size_t idx, aidx, node, cls;
984 };
985 std::vector<UpdRow> servt_map, thinkt_map, call_map, actthinkt_map;
986 /**
987 * Source arrival of an async call class, `aidx` holding the call index.
988 *
989 * Kept apart from call_map because the two update different things about
990 * the same class: call_map carries the SERVICE at the server replicas,
991 * this one the arrival RATE at the Source, which is the caller activity's
992 * throughput and so moves with the fixed point. Entry open arrivals have
993 * no row here at all -- theirs is exogenous and never reseeded.
994 */
995 std::vector<UpdRow> arv_call_map;
996 /** [idx, tidx_caller, eidx, nodefrom, nodeto, classfrom, classto] */
997 struct RouteRow {
998 std::size_t idx, tidx_caller, eidx, nodefrom, nodeto, cfrom, cto;
999 };
1000 std::vector<RouteRow> route_map;
1001 std::vector<std::size_t> unique_route_idx;
1002 std::vector<std::size_t> route_reset, svc_reset;
1003
1004 /** The interlock tracking method in force, resolved once by build_layers. */
1005 std::string interlockMethod = "ilrate";
1006 /**
1007 * Reference-path stage rows, refpath_classes_updmap of the reference:
1008 * [idx, node, class] name the stage, `kind` 1 = residt(elem), 2 =
1009 * callresidt(elem)/entryvisits(fromentry), 3 = threadWait(elem), each term
1010 * multiplied by `sign`.
1011 */
1012 struct RefPathRow {
1013 std::size_t idx, node, cls;
1014 int kind;
1015 std::size_t elem;
1016 double sign;
1017 std::size_t fromentry;
1018 };
1019 std::vector<RefPathRow> refpath_map;
1020 /** Unique (idx, node, class) of refpath_map, sorted, and each row's key. */
1021 std::vector<std::array<std::size_t, 3>> refpath_keys;
1022 std::vector<std::size_t> refpath_group;
1023 /** Per key, the previous iteration's stage mean; NaN until the first update. */
1024 std::vector<double> refpath_stage_prev;
1025 std::vector<T> refpath_stage_prev_v;
1026 /**
1027 * Per layer ELEMENT, the per-class normalising TASK class (1-based, 0 = use
1028 * the chain's reference class), attribute.normclass of the reference. Only
1029 * the refpath layers fill it; see ln_layer_refcell.
1030 */
1031 std::map<std::size_t, std::vector<std::size_t>> layer_normclass;
1032 /** Invocations of an entry per invocation of its task; starts at 1. */
1033 std::vector<T> entryvisits;
1034
1035 Matrix<double> njobs; ///< (NT+1 x NT+1) population of caller tidx in layer idx
1036
1037 // per-element metric state
1038 std::vector<T> servt, residt, tput, util, thinkt;
1039 std::vector<T> callservt, callresidt;
1040 /**
1041 * Phase-2 state, the reference's hasPhase2 / servt_ph1 / servt_ph2 /
1042 * prOvertake. The split is of `servt` and exists only while `has_phase2`;
1043 * `prOvertake` is indexed by entry NUMBER 1..nentries, as the reference
1044 * indexes it, not by element index.
1045 */
1046 bool has_phase2 = false;
1047 std::vector<T> servt_ph1, servt_ph2, prOvertake;
1048 /** Last entry service and its phase-2 part, resolve_entry_service -> overtaking split. */
1049 std::vector<T> entry_servt_last, entry_servt_ph2;
1050 std::vector<T> join_tail; // straggler time after a quorum join replies, a phase-2 tail
1051 /** Tasks whose own layer was collapsed to one representative replica. */
1052 std::set<std::size_t> single_replica_tasks;
1053 /**
1054 * Replicated tasks whose host layer holds ALL their replicas in one class (the
1055 * host has fewer replicas than the task, or is shared unevenly), mapped to that
1056 * layer's server replicas: the client row read there is already the total over
1057 * the task's replicas, one server station holds 1/nreplicas of it (BUG-110).
1058 */
1059 std::map<std::size_t, std::size_t> host_all_replica_tasks;
1060 std::vector<Distrib<T>> servtproc, thinkproc, thinktproc, tputproc, callservtproc;
1061 /**
1062 * `moment3` state: the response-time CDF of each activity (by element) and
1063 * of each call, the fitted per-entry law and the CDF read off it. All empty
1064 * under the default method, which never forms a distribution at all.
1065 */
1066 std::vector<fluid::FluidPassage> servtcdf, callservtcdf;
1067 std::vector<LnCdf> entrycdfrespt;
1068 std::vector<mam::AphPair<T>> entryproc;
1069 /** Per layer, the response-time CDFs of the converged pass; a lazy cache. */
1070 std::vector<std::vector<std::vector<fluid::FluidPassage>>> cdf_repo;
1071 std::vector<double> servt_prev, residt_prev, tput_prev, thinkt_prev;
1072 std::vector<double> callservt_prev, callresidt_prev;
1073 std::vector<T> servt_prev_v, residt_prev_v, tput_prev_v, thinkt_prev_v, callservt_prev_v;
1074 Matrix<T> servtmatrix; ///< (nidx+ncalls) x (nidx+ncalls) entry reachability
1075
1076 // interlock tables
1077 Matrix<T> il_all, il_ph1;
1078 std::vector<std::vector<std::size_t>> il_common_entries, il_src_all, il_src_ph2;
1079 /**
1080 * Per-layer interlock matrix of Franks (1999), Eq. (4.7), CLASS-indexed. A host layer
1081 * whose engine carries the correction inside its own MVA takes the matrix instead of
1082 * having its residence times scaled after the fact; empty for every other layer.
1083 */
1084 std::vector<std::vector<std::vector<double>>> layer_interlock;
1085 std::vector<double> il_num_sources;
1086
1087 std::vector<std::vector<LayerResult<T>>> results; ///< [iteration][layer]
1088 /** The outer iteration analyze() is running, 1-based; 0 before the first sweep. Seeds stochastic layers. */
1089 long cur_it = 0;
1090 /**
1091 * The newest iterate as the layers returned it, before `converged` folded the moving
1092 * average into it. MATLAB averages a copy (iterate.m keeps its own raw history and
1093 * hands it back to self.results every pass), so the window, the error test and the
1094 * averaged servt in updateMetricsDefault all read RAW past iterates; this puts the raw
1095 * values back once the iterate is no longer the newest.
1096 */
1097 std::vector<LayerResult<T>> results_raw_last;
1098 bool results_raw_valid = false;
1099 std::vector<Matrix<T>> layer_init_sol;
1100 /**
1101 * Per layer, the ODE state `init_from_marginal` recorded, replayed by
1102 * `run_layer_transient`. Empty for a layer with no warm start.
1103 */
1104 std::vector<std::vector<double>> layer_tran_init;
1105 /**
1106 * Per layer, the fork-join transform SolverMVA solves instead of the layer
1107 * itself (inactive for a layer with no fork), and the auxiliary arrival
1108 * rates its fixed point iterates on. See fj_mmt.h.
1109 *
1110 * `fj_lambda` is the `self.fjForkLambda` of the reference and is deliberately
1111 * NOT reset between outer iterations: the transform is rebuilt cold on every
1112 * outer pass but the iterate it converged to last time is a far better
1113 * starting point than FineTol, and the reference warm-starts it the same way
1114 * (options.config.fj_warmstart, true by default).
1115 */
1116 std::vector<mva::FjMmt<T>> fj_tr;
1117 std::vector<std::vector<T>> fj_lambda;
1118 double relax_omega = 1.0;
1119 /** Live only while a NOISY layer engine is in force; null otherwise. */
1120 std::shared_ptr<LnStochController<T>> stoch_ctl;
1121 long averagingstart = -1;
1122 bool hasconverged = false;
1123 /** init() seeded the iterate from config.warmstart, so converged() drops iter_min from its stop test. */
1124 bool warmstarted_ = false;
1125 /** method='nlp': the program's root (clipped at zero) and its objective there. */
1126 std::vector<double> nlp_solution_;
1127 double nlp_objective_ = std::numeric_limits<double>::quiet_NaN();
1128 /** method='moment3': true once the distribution pass has formed the entry laws. */
1129 bool moment_pass_done = false;
1130 std::vector<double> maxitererr;
1131 int iterations_done = 0;
1132 bool did_converge = false;
1133
1134 std::size_t NT() const { return lqn.tshift + lqn.ntasks; }
1135
1136 static T Tzero() { return num_traits<T>::from_int(0); }
1137 static T Tone() { return num_traits<T>::from_int(1); }
1138 static double dbl(const T& x) { return num_traits<T>::to_double(x); }
1139
1140 // -----------------------------------------------------------------------
1141 // construct
1142 // -----------------------------------------------------------------------
1143 void construct() {
1144 // Forwarding rewrite, SolverLN.m:162-164: flatten every forwarding
1145 // chain into caller-side pseudo rendezvous calls BEFORE anything else
1146 // reads lqn, so build_layers/detect_phase2/reject_unsupported all see
1147 // plain rendezvous arcs. The raw FWD calls survive in the struct but
1148 // must not contribute blocking after this point (lqn_helpers.h).
1149 lqn_fwd_rendezvous(lqn);
1150 reject_unsupported();
1151 detect_phase2();
1152
1153 const std::size_t N = lqn.nidx;
1154 ignore.assign(N + 1, false);
1155 // weakly connected components of graph + graph'; a component with no
1156 // reference task is unreachable and its elements are ignored
1157 std::vector<long> comp(N + 1, -1);
1158 long ncomp = 0;
1159 for (std::size_t v = 1; v <= N; ++v) {
1160 if (comp[v] >= 0) continue;
1161 std::vector<std::size_t> stack{v};
1162 comp[v] = ncomp;
1163 while (!stack.empty()) {
1164 const std::size_t u = stack.back();
1165 stack.pop_back();
1166 for (std::size_t w : lqn.graph.succ(u))
1167 if (comp[w] < 0) {
1168 comp[w] = ncomp;
1169 stack.push_back(w);
1170 }
1171 for (std::size_t w : lqn.graph.pred(u))
1172 if (comp[w] < 0) {
1173 comp[w] = ncomp;
1174 stack.push_back(w);
1175 }
1176 }
1177 ++ncomp;
1178 }
1179 if (ncomp > 1) {
1180 std::vector<bool> has_ref(ncomp, false);
1181 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1182 const std::size_t tidx = lqn.tshift + t;
1183 if (lqn.sched[tidx] == SchedStrategy::REF) has_ref[comp[tidx]] = true;
1184 }
1185 for (std::size_t v = 1; v <= N; ++v)
1186 if (!has_ref[comp[v]]) ignore[v] = true;
1187 }
1188 // A reference task trapped at a zero-capacity processor or task is answered
1189 // structurally; the elements only it reaches are dropped from the layers
1190 zerocap = ln_zero_capacity(lqn);
1191 if (!zerocap.reason.empty()) throw UnsupportedError(ErrorKind{"Structure"}, zerocap.reason);
1192 if (zerocap.any_trapped()) {
1193 if (ln_requested_method(opt.method) == "nlp")
1194 throw UnsupportedError(ErrorKind{"Method"},
1195 "method='nlp' cannot solve a model whose reference task is trapped at a "
1196 "zero-capacity element; use a layered method.");
1197 if (num_traits<T>::is_exact)
1198 throw UnsupportedError(ErrorKind{"Structure"},
1199 "A reference task trapped at a zero-capacity element has infinite response "
1200 "times, which exact arithmetic cannot represent; solve it in double precision.");
1201 }
1202 for (std::size_t v = 1; v <= N; ++v)
1203 if (zerocap.ignore[v]) ignore[v] = true;
1204 if (zerocap.any_trapped()) {
1205 // size the thread pools as if the trapped reference tasks were absent
1206 std::vector<double> liveMult = lqn.mult;
1207 for (std::size_t i = 1; i < liveMult.size(); ++i)
1208 if (zerocap.trapped[i]) liveMult[i] = 0.0;
1209 lqn.maxmult = lqn::lqn_sustainable_mult(lqn, liveMult);
1210 }
1211
1212 servtproc.assign(N + 1, Distrib<T>::disabled_dist());
1213 thinkproc.assign(N + 1, Distrib<T>::disabled_dist());
1214 thinktproc.assign(N + 1, Distrib<T>::disabled_dist());
1215 tputproc.assign(N + 1, Distrib<T>::disabled_dist());
1216 for (std::size_t i = 1; i <= N; ++i) {
1217 servtproc[i] = lqn.hostdem[i];
1218 thinkproc[i] = lqn.think[i];
1219 }
1220 callservtproc.assign(lqn.ncalls + 1, Distrib<T>::disabled_dist());
1221 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
1222 callservtproc[c] = lqn.hostdem[lqn.callpair_dst[c]];
1223
1224 // Invocations of an entry per invocation of its task, filled by
1225 // resolve_entry_service; 1 because iteration 1 runs before any measurement
1226 entryvisits.assign(N + 1, Tone());
1227 refpath_map.clear();
1228 layer_normclass.clear();
1229
1230 njobs = Matrix<double>(NT() + 1, NT() + 1, 0.0);
1231 // method 'nlp' solves the model as ONE program and never decomposes it:
1232 // everything above is bookkeeping the analyzer reads (`ignore`), and
1233 // everything below is layer machinery no one would solve. Nor is there
1234 // when every reference task is trapped at a zero-capacity element.
1235 if (ln_requested_method(opt.method) == "nlp" || !zerocap.live) {
1236 if (zerocap.live) lnmethod = "nlp";
1237 return;
1238 }
1239 build_layers();
1240
1241 // layers whose routing or service must be re-derived after an update
1242 std::vector<bool> rr(NT() + 1, false), sr(NT() + 1, false);
1243 for (const RouteRow& r : route_map) rr[r.idx] = true;
1244 for (const UpdRow& r : thinkt_map) sr[r.idx] = true;
1245 for (const UpdRow& r : call_map) sr[r.idx] = true;
1246 // refpath REMOVES the think-time row of every merged caller and the
1247 // client-side call row of every re-expanded descent, so a layer whose only
1248 // per-iteration change was one of those would otherwise never be
1249 // refresh_rates()'d and would freeze at its build-time seed. Its stage
1250 // rows put it back.
1251 for (const RefPathRow& r : refpath_map) sr[r.idx] = true;
1252 // a moved arrival rate reweights the Sink closure, which only
1253 // refresh_chains rebuilds, so these layers need the routing reset
1254 for (const UpdRow& r : arv_call_map) rr[r.idx] = true;
1255 for (std::size_t i = 1; i <= NT(); ++i) {
1256 if (rr[i] && idxhash[i] >= 0) route_reset.push_back(std::size_t(idxhash[i]));
1257 if (sr[i] && idxhash[i] >= 0) svc_reset.push_back(std::size_t(idxhash[i]));
1258 }
1259 std::sort(route_reset.begin(), route_reset.end());
1260 route_reset.erase(std::unique(route_reset.begin(), route_reset.end()), route_reset.end());
1261 std::sort(svc_reset.begin(), svc_reset.end());
1262 svc_reset.erase(std::unique(svc_reset.begin(), svc_reset.end()), svc_reset.end());
1263
1264 // The reference-path stage keys, unique((idx, node, class), 'rows').
1265 refpath_keys.clear();
1266 refpath_group.clear();
1267 for (const RefPathRow& r : refpath_map) refpath_keys.push_back({r.idx, r.node, r.cls});
1268 std::sort(refpath_keys.begin(), refpath_keys.end());
1269 refpath_keys.erase(std::unique(refpath_keys.begin(), refpath_keys.end()),
1270 refpath_keys.end());
1271 for (const RefPathRow& r : refpath_map) {
1272 const std::array<std::size_t, 3> key{r.idx, r.node, r.cls};
1273 refpath_group.push_back(std::size_t(
1274 std::lower_bound(refpath_keys.begin(), refpath_keys.end(), key) -
1275 refpath_keys.begin()));
1276 }
1277 if (refpath_map.empty() && interlockMethod == "refpath") {
1278 // Not one layer merged a reference chain, so the build is the one
1279 // 'none' produces, and so would the solve be: post() suppresses the
1280 // rate discount whenever the method is 'refpath'. Switching a
1281 // correction off and declining to replace it is strictly worse than
1282 // either method alone, so the method is demoted here, before init()
1283 // decides whether to build the interlock tables.
1284 interlockMethod = "ilrate";
1285 }
1286 refpath_stage_prev.assign(refpath_keys.size(), std::numeric_limits<double>::quiet_NaN());
1287 refpath_stage_prev_v.assign(refpath_keys.size(), Tzero());
1288 }
1289
1290 /** Reject, by name, every construct this port does not implement. */
1291 // Phase-2 detection, SolverLN.m:165-173. Deliberately NOT inside
1292 // reject_unsupported: that method is const because it only refuses, and
1293 // detection sets state. Marking the flag mutable would have compiled and
1294 // left a validator that silently mutates the solver.
1295 void detect_phase2() {
1296 has_phase2 = false;
1297 for (std::size_t a = 1; a <= lqn.nacts; ++a)
1298 if (lqn.actphase[a] > 1) has_phase2 = true;
1299 // A quorum join replies after k of its n branches, but the n-k stragglers still run on
1300 // the thread, which stays busy after the reply as in a second phase (ln_has_quorum_tail.m).
1301 // The layer's Join waits for ALL branches; the caller's quorum wait comes back via residt.
1302 for (std::size_t aidx = 1; aidx <= lqn.nidx && aidx < lqn.actquorum.size(); ++aidx) {
1303 if (lqn.actquorum[aidx] < 1) continue;
1304 std::size_t nbranches = 0;
1305 for (std::size_t tail = 1; tail <= lqn.nidx; ++tail) {
1306 if (tail == aidx || lqn.actpretype[tail] != PrecedenceType::PRE_AND) continue;
1307 for (std::size_t sx : lqn.graph.succ(tail))
1308 if (sx == aidx) ++nbranches;
1309 }
1310 if (lqn.actquorum[aidx] < nbranches) has_phase2 = true;
1311 }
1312 }
1313
1314 /**
1315 * True when `eidx` is the destination of a raw forwarding call.
1316 *
1317 * buildLayersRecursive.m:186-192 keeps this test even though
1318 * lqn_fwd_rendezvous has already run, and so must this port:
1319 * the rewrite walks chains out of SYNC calls only, so an entry whose
1320 * forwarder is reached asynchronously gets no pseudo arc and would
1321 * otherwise be dropped from the layer along with all of its work.
1322 */
1323 bool is_fwd_target(std::size_t eidx) const {
1324 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
1325 if (lqn.calltype[c] == CallType::FWD && lqn.callpair_dst[c] == eidx) return true;
1326 return false;
1327 }
1328
1329 /**
1330 * Total exogenous rate into the entries of task `tidx`, zero unless the arrival is
1331 * the ONLY way in.
1332 *
1333 * A task nobody calls has no task layer, so update_think_times never sets its
1334 * surrogate delay and its caller class cycles against an Immediate one. Carrying the
1335 * arrival as an open stream ON TOP of that unthrottled chain loads the host twice:
1336 * lqn_open_arrival read the processor at 0.68 where lqns, lqsim and LDES all give
1337 * 0.32. The chain is the representation that honours the thread pool, so build_layer
1338 * drops the stream for these tasks and update_think_times closes the chain on this
1339 * rate, as the reference does for a forwarding target. With a caller or a forwarding
1340 * source the stream needs a class of its own and this returns 0.
1341 */
1342 double open_arrival_rate_of(std::size_t tidx) const {
1343 if (lqn.isref[tidx]) return 0.0;
1344 for (std::size_t e : lqn.entriesof[tidx])
1345 if (lqn.issynccaller.any_col(e) || lqn.isasynccaller.any_col(e) || is_fwd_target(e))
1346 return 0.0;
1347 double rate = 0.0;
1348 for (std::size_t e : lqn.entriesof[tidx]) {
1349 if (!lqn.has_arrival[e]) continue;
1350 const double m = dbl(lqn.arrival[e].mean);
1351 if (std::isfinite(m) && m > GlobalConstants::FineTol) rate += 1.0 / m;
1352 }
1353 return rate;
1354 }
1355
1356 /**
1357 * Residence time of the synchronous calls the activities `acts` make. A thread of a
1358 * chain closed on a known rate cycles through these calls as well as its host
1359 * demand, so its think time must leave room for them; leaving them out throttled
1360 * the chain below the rate it is closed on: lqns 0.1 against LN 0.0870 for an open
1361 * stream into an entry with one call (BUG-103).
1362 */
1363 T sync_call_residt_of(const std::vector<std::size_t>& acts) const {
1364 T total = Tzero();
1365 for (std::size_t aidx : acts)
1366 for (std::size_t cidx : lqn.callsof[aidx])
1367 if (lqn.calltype[cidx] == CallType::SYNC && cidx < callresidt.size() &&
1368 std::isfinite(dbl(callresidt[cidx])))
1369 total += callresidt[cidx];
1370 return total;
1371 }
1372
1373 /** sync_call_residt_of per arriving request of `tidx`, by each entry's arrival share. */
1374 T open_arrival_call_residt(std::size_t tidx, double arvrate) const {
1375 T total = Tzero();
1376 if (!(arvrate > 0.0)) return total;
1377 for (std::size_t e : lqn.entriesof[tidx]) {
1378 if (!lqn.has_arrival[e]) continue;
1379 const double m = dbl(lqn.arrival[e].mean);
1380 if (std::isfinite(m) && m > GlobalConstants::FineTol)
1381 total += T(num_traits<T>::from_double(1.0 / m / arvrate) *
1382 sync_call_residt_of(lqn.actsof[e]));
1383 }
1384 return total;
1385 }
1386
1387 /** True when layer `e` carries a Cache node. */
1388 bool has_cache_node(std::size_t e) const {
1389 for (std::size_t n = 0; n < ensemble[e].nodes.size(); ++n)
1390 if (ensemble[e].nodes[n].nodetype == NodeType::Cache) return true;
1391 return false;
1392 }
1393
1394 /** True when `aidx` is an activity bound to an entry nobody calls synchronously. */
1395 bool async_only_activity(std::size_t aidx) const {
1396 if (aidx <= lqn.ashift || aidx > lqn.ashift + lqn.nacts) return false;
1397 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
1398 const std::size_t eidx = lqn.eshift + e;
1399 if (lqn.graph.get(eidx, aidx) == Tzero()) continue;
1400 return lqn.isasynccaller.any_col(eidx) && !lqn.issynccaller.any_col(eidx);
1401 }
1402 return false;
1403 }
1404
1405 /**
1406 * Reject, by name, every construct this port does not implement.
1407 *
1408 * NOTHING IS LEFT. Forwarding, asynchronous calls, entry open arrivals,
1409 * admission constraints, cache tasks and setup tasks are each solved.
1410 * The method is kept rather than deleted because it is the place a new
1411 * refusal belongs, and because a layer-build refusal must be raised HERE,
1412 * before construct() has read anything out of the struct, rather than
1413 * halfway through building a layer.
1414 */
1415 void reject_unsupported() const {}
1416
1417 /**
1418 * The interlock tracking method, resolved ONCE for the encoding just chosen,
1419 * for the same reason lnmethod is: what update_populations and init_interlock
1420 * do can never disagree with what was built into the layers.
1421 */
1422 void resolve_interlock_method() {
1423 const LnInterlockChoice ch =
1424 ln_interlock_method(lqn, opt.interlock_method, opt.interlocking, lnmethod);
1425 interlockMethod = ch.method;
1426 if (!ch.why.empty()) std::cerr << "[LINE] Warning: " << ch.why << "\n";
1427 }
1428
1429 // -----------------------------------------------------------------------
1430 // buildLayers
1431 // -----------------------------------------------------------------------
1432 void build_layers() {
1433 // Method resolution. A method name carries both the LAYERING and the
1434 // ENCODING: "srvn.ph" replaces the routing encoding of the activity graph
1435 // by a composed phase-type server law, "srvn" is the alias that takes it
1436 // where it can serve the model and "srvn.cs" otherwise. The choice is
1437 // made ONCE, here, and every later dispatch reads lnmethod.
1438 // See _kb/06-solver-catalog.md (LN section).
1439 const std::string requested = ln_requested_method(opt.method);
1440 assert_call_groups(requested == "flat.cs");
1441 if (requested == "flat.cs") {
1442 lnmethod = "flat.cs";
1443 resolve_interlock_method();
1444 build_flat_layer();
1445 return;
1446 }
1447 if (requested == "flat.ph") {
1448 // the squashed layering with the composed law: ONE submodel holding
1449 // every server, and a caller visiting each of them once per
1450 // invocation. The feature gate is the srvn.ph one plus the refusals a
1451 // single submodel carries -- see ph_flat_server_set.
1452 ph_laws_ready = false;
1453 lnmethod = "flat.ph";
1454 assert_warmstart_encoding();
1455 build_layers_ph(true);
1456 return;
1457 }
1458 if (requested == "srvn.ph" || requested == "srvn") {
1459 ph_laws_ready = false;
1460 // srvn.ph switches the interlock correction off; see has_interlock
1461 if (requested == "srvn.ph" || (!has_interlock() && probe_srvn_ph())) {
1462 lnmethod = "srvn.ph";
1463 assert_warmstart_encoding();
1464 build_layers_ph();
1465 return;
1466 }
1467 }
1468 lnmethod = (requested == "moment3") ? "moment3" : "srvn.cs";
1469 resolve_interlock_method();
1470 std::vector<qn::Layer<T>> raw(NT() + 1);
1471 std::vector<bool> present(NT() + 1, false);
1472
1473 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx) {
1474 if (ignore[hidx]) continue;
1475 std::vector<std::size_t> hcallers; // a task only a trapped reference task reaches is dropped
1476 for (std::size_t c : lqn.tasksof[hidx])
1477 if (!ignore[c]) hcallers.push_back(c);
1478 build_layer(raw[hidx], {hidx}, hcallers, true, false);
1479 present[hidx] = true;
1480 }
1481 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1482 const std::size_t tidx = lqn.tshift + t;
1483 if (ignore[tidx] || lqn.isref[tidx]) continue;
1484 bool any_caller = lqn.iscaller.any_row(tidx) || lqn.iscaller.any_col(tidx);
1485 if (!any_caller) continue;
1486 // tasks that call some entry of tidx
1487 std::vector<std::size_t> callers;
1488 for (std::size_t ct = 1; ct <= lqn.ntasks; ++ct) {
1489 const std::size_t c = lqn.tshift + ct;
1490 if (ignore[c]) continue;
1491 bool calls = false;
1492 for (std::size_t e : lqn.entriesof[tidx])
1493 if (lqn.iscaller.get(c, e)) calls = true;
1494 if (calls) callers.push_back(c);
1495 }
1496 if (callers.empty()) continue;
1497 build_layer(raw[tidx], {tidx}, callers, false, false);
1498 present[tidx] = true;
1499 }
1500
1501 idxhash.assign(lqn.nidx + 1, -1);
1502 long next = 0;
1503 for (std::size_t i = 1; i <= NT(); ++i)
1504 if (present[i]) {
1505 idxhash[i] = next++;
1506 ensemble.push_back(std::move(raw[i]));
1507 }
1508 layer_init_sol.assign(ensemble.size(), Matrix<T>());
1509 build_fork_views();
1510 }
1511
1512 /**
1513 * Reject a routed call group under any layering or layer solver that cannot
1514 * carry it.
1515 *
1516 * Two conditions, and both are refusals rather than degradations. The
1517 * squashed layering is needed because under `srvn` each target lives in a
1518 * submodel of its own and is replaced, in the caller's submodel, by a
1519 * surrogate delay -- no node ever has arcs to more than one of them, so
1520 * there is nothing to dispatch among. A layer solver that resolves the
1521 * strategy from the STATE is needed because `refresh_routing` expands
1522 * RROBIN and JSQ into a uniform probability split for the matrix solvers,
1523 * and returning that split under a round-robin label misreports a
1524 * deterministic policy as a coin. In this port `ssa`, `ldes` and `jmt`
1525 * resolve them, the simulators of the reference's CTMC/SSA/LDES/JMT list.
1526 */
1527 void assert_call_groups(bool flat) const {
1528 if (lqn.callgroups.empty()) return;
1529 if (!flat)
1530 throw UnsupportedError(ErrorKind{"CallGroup"},
1531 "Call groups routed by a routing strategy require the squashed layering; use "
1532 "method='flat'. Under srvn the targets never share a submodel, so the dispatch "
1533 "order cannot be represented.");
1534 if (!layer_solver_is_stochastic())
1535 throw UnsupportedError(ErrorKind{"CallGroup"},
1536 "Routed call groups need a layer solver that resolves the strategy from the "
1537 "state; set layer_solver='ssa', 'ldes' or 'jmt'. MVA, NC and FLD read the routing matrix, into "
1538 "which refresh_routing has expanded the strategy as a uniform split, and would "
1539 "return that split under a round-robin or JSQ label.");
1540 }
1541
1542 /**
1543 * The processors and called tasks that become stations of the flat layer.
1544 *
1545 * Squashing is refused rather than approximated where an element carries
1546 * state that only a submodel of its own can hold: a REPLICATED element
1547 * would need one station per copy inside a layer whose routing addresses it
1548 * once, a CACHE task needs the Cache node in the host layer its reads queue
1549 * at, and a SETUP task's delay-off belongs to the station that powers down.
1550 */
1551 std::vector<std::size_t> flat_server_set() const {
1552 std::vector<std::size_t> servers;
1553 for (std::size_t i = 1; i <= NT(); ++i) {
1554 if (lqn.repl[i] > 1.0)
1555 throw UnsupportedError(ErrorKind{"Replication"},
1556 "Flat layering does not support replicated processors or tasks, use the "
1557 "default 'srvn' layering.");
1558 if (lqn.iscache[i])
1559 throw UnsupportedError(ErrorKind{"CacheTask"},
1560 "Flat layering does not support cache tasks, use the default 'srvn' "
1561 "layering.");
1562 if (lqn.hassetup[i])
1563 throw UnsupportedError(ErrorKind{"SetupDelayOff"},
1564 "Flat layering does not support setup tasks, use the default 'srvn' "
1565 "layering.");
1566 }
1567 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx)
1568 if (!ignore[hidx] && !lqn.tasksof[hidx].empty()) servers.push_back(hidx);
1569 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1570 const std::size_t tidx = lqn.tshift + t;
1571 if (ignore[tidx] || lqn.isref[tidx]) continue;
1572 if (!lqn.iscaller.any_row(tidx) && !lqn.iscaller.any_col(tidx)) continue;
1573 bool has_task_caller = false;
1574 for (std::size_t eidx : lqn.entriesof[tidx])
1575 for (std::size_t c = 1; c <= lqn.ntasks; ++c)
1576 if (lqn.iscaller.get(lqn.tshift + c, eidx)) has_task_caller = true;
1577 if (has_task_caller) servers.push_back(tidx);
1578 }
1579 if (servers.empty())
1580 throw InputError(
1581 "Flat layering found no server: the model has no processor with tasks.");
1582 return servers;
1583 }
1584
1585 /**
1586 * Build the single squashed layer holding every processor and called task.
1587 *
1588 * Every served element resolves to layer 0, which is at once the host layer
1589 * and the task layer, so a consumer that walks `idxhash` finds the same
1590 * network for a processor and for a task and must read the station off
1591 * `server_idx_of` rather than off `serverIdx`.
1592 */
1593 void build_flat_layer() {
1594 const std::vector<std::size_t> flat_servers = flat_server_set();
1595 std::vector<std::size_t> flat_callers;
1596 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1597 const std::size_t tidx = lqn.tshift + t;
1598 if (!ignore[tidx]) flat_callers.push_back(tidx);
1599 }
1600 qn::Layer<T> layer;
1601 build_layer(layer, flat_servers, flat_callers, false, true);
1602 ensemble.clear();
1603 ensemble.push_back(std::move(layer));
1604 idxhash.assign(lqn.nidx + 1, -1);
1605 for (std::size_t s : flat_servers) idxhash[s] = 0;
1606 layer_init_sol.assign(ensemble.size(), Matrix<T>());
1607 build_fork_views();
1608 }
1609
1610 /**
1611 * Build, for every layer that has a fork, the transformed model SolverMVA
1612 * actually solves, and seed its auxiliary arrival rates.
1613 */
1614 void build_fork_views() {
1615 fj_tr.assign(ensemble.size(), mva::FjMmt<T>());
1616 fj_lambda.assign(ensemble.size(), {});
1617 for (std::size_t e = 0; e < ensemble.size(); ++e) {
1618 if (!ensemble[e].has_fork()) continue;
1619 // fj_mmt mints its own Source/Sink pair and would silently overwrite
1620 // sourceIdx/sinkNode, detaching the open stream already routed there.
1621 // NOT a parity gap: the reference does not solve this combination
1622 // either, it fails inside the layer solve with "Arrays have
1623 // incompatible sizes" (checked 2026-07-29 on a fork+async model).
1624 // Refusing by name is the better answer, so this stays.
1625 if (ensemble[e].sourceIdx != 0)
1626 throw UnsupportedError(ErrorKind{"Fork"},
1627 "SolverLN: layer '" + ensemble[e].name +
1628 "' carries both an AND fork and an open stream (an async call or an entry "
1629 "arrival); the fork-join transform needs a Source of its own");
1630 fj_tr[e] = mva::fj_mmt(ensemble[e]);
1631 fj_lambda[e].assign(fj_tr[e].V.classes.size() + 1,
1632 num_traits<T>::from_double(GlobalConstants::FineTol));
1633 }
1634 }
1635
1636 /**
1637 * Port of matlab/src/lang/layered/lqn_dep_layer_handle.m: lift a
1638 * per-operand rate handle onto the classes of a layer station.
1639 *
1640 * COLS[j] lists the layer classes (0-based) through which operand j occupies
1641 * the station; CHAINCOLS is the same list in the CHAIN index space, filled
1642 * once refresh_chains has run. Solvers evaluate the handle in either space
1643 * -- CTMC and the exact recursions pass a per-class vector, the AMVA and NC
1644 * chain recursions a per-chain one -- so the handle reads the LENGTH of what
1645 * it is given to decide, aggregates the operand populations in that space,
1646 * and answers a vector of the same length, since the caller indexes the
1647 * answer with the index it passed in. An index belonging to no operand keeps
1648 * the neutral scaling 1.
1649 */
1650 static lang::CdScaling<T> layer_dep_handle(
1651 const lang::CdScaling<T>& f, const std::vector<std::vector<std::size_t>>& cols,
1652 const std::shared_ptr<std::vector<std::vector<std::size_t>>>& chaincols, std::size_t R) {
1653 return [f, cols, chaincols, R](const std::vector<T>& n) -> std::vector<T> {
1654 const std::size_t L = n.size();
1655 const std::vector<std::vector<std::size_t>>& use =
1656 (L == R || chaincols->empty()) ? cols : *chaincols;
1657 std::vector<T> nop(use.size(), num_traits<T>::from_int(0));
1658 for (std::size_t j = 0; j < use.size(); ++j)
1659 for (std::size_t k = 0; k < use[j].size(); ++k)
1660 if (use[j][k] < L) nop[j] = T(nop[j] + n[use[j][k]]);
1661 const std::vector<T> w = f(nop);
1662 std::vector<T> v(L, num_traits<T>::from_int(1));
1663 if (w.empty()) return v;
1664 for (std::size_t j = 0; j < use.size(); ++j) {
1665 const T wj = w[j < w.size() ? j : w.size() - 1];
1666 for (std::size_t k = 0; k < use[j].size(); ++k)
1667 if (use[j][k] < L) v[use[j][k]] = wj;
1668 }
1669 return v;
1670 };
1671 }
1672
1673 /** Spread a per-operand peak rate onto the layer classes. Twin of layerPeak. */
1674 static std::vector<T> layer_peak(const std::vector<T>& peakPerOperand,
1675 const std::vector<std::vector<std::size_t>>& cols,
1676 std::size_t R) {
1677 std::vector<T> peak(R, num_traits<T>::from_int(1));
1678 if (peakPerOperand.empty()) return peak;
1679 for (std::size_t j = 0; j < cols.size(); ++j) {
1680 const T pj = peakPerOperand[j < peakPerOperand.size() ? j : peakPerOperand.size() - 1];
1681 for (std::size_t k = 0; k < cols[j].size(); ++k)
1682 if (cols[j][k] < R) peak[cols[j][k]] = pj;
1683 }
1684 return peak;
1685 }
1686
1687 /**
1688 * Give every class of a host layer the priority of the task that owns it.
1689 *
1690 * Applies to each processor in idxSet that schedules by priority (HOL, 'pri', ...).
1691 * lqns serves a LARGER task priority first while a class priority of 0 is the highest,
1692 * so the class priority is max(prio on the host) - prio(task). A layer solver with a
1693 * priority arm (MVA, CTMC, ...) then applies it. Without this every class tied at 0
1694 * and a priority processor ran as FCFS. Port of MATLAB ln_host_task_priorities.m.
1695 */
1696 void apply_host_task_priorities(qn::Layer<T>& m, const std::vector<std::size_t>& idxSet) const {
1697 using qn::SchedStrategy;
1698 auto is_prio = [](SchedStrategy s) {
1699 return s == SchedStrategy::HOL || s == SchedStrategy::FCFSPRPRIO ||
1700 s == SchedStrategy::FCFSPIPRIO || s == SchedStrategy::LCFSPRIO ||
1701 s == SchedStrategy::LCFSPRPRIO || s == SchedStrategy::LCFSPIPRIO ||
1702 s == SchedStrategy::PSPRIO || s == SchedStrategy::DPSPRIO ||
1703 s == SchedStrategy::GPSPRIO;
1704 };
1705 const std::size_t R = m.classes.size();
1706 std::vector<std::size_t> owner(R + 1, 0); // 1-based class -> owning task, 0 = none
1707 auto own = [&](std::size_t k, std::size_t t) {
1708 if (k >= 1 && k <= R && owner[k] == 0) owner[k] = t;
1709 };
1710 for (const auto& a : m.attr_tasks) own(a.first, a.second);
1711 for (const auto& a : m.attr_entries) own(a.first, lqn.parent[a.second]);
1712 for (const auto& a : m.attr_activities) own(a.first, lqn.parent[a.second]);
1713 for (const auto& a : m.attr_calls) own(a[0], lqn.parent[a[2]]);
1714 for (std::size_t s : idxSet) {
1715 if (s < 1 || s > lqn.nhosts || !is_prio(lqn.sched[s])) continue;
1716 int pmax = 0;
1717 for (std::size_t t = lqn.tshift + 1; t <= lqn.tshift + lqn.ntasks; ++t)
1718 if (lqn.parent[t] == s) pmax = std::max(pmax, lqn.prio[t]);
1719 for (std::size_t k = 1; k <= R; ++k)
1720 if (owner[k] != 0 && lqn.parent[owner[k]] == s)
1721 m.classes[k - 1].prio = pmax - lqn.prio[owner[k]];
1722 }
1723 }
1724
1725 /**
1726 * Port of buildLayersRecursive.
1727 *
1728 * The layer holds a client Delay carrying the callers' think times and the
1729 * time they spend elsewhere, and `nreplicas` copies of the server station.
1730 * A class is created for every task, entry, activity and call that the
1731 * callers' activity graphs reach, and the graph traversal lays down the
1732 * routing that moves a job between those classes.
1733 */
1734 void build_layer(qn::Layer<T>& m, const std::vector<std::size_t>& idxSet,
1735 const std::vector<std::size_t>& callers, bool ishostlayer, bool flat) {
1736 const T one = Tone();
1737 // The layer key: the model name, the ensemble slot and the column every
1738 // update map is written under. Under `srvn` it is the layer's only
1739 // served element; under `flat` it is the first of them.
1740 const std::size_t idx = idxSet[0];
1741 m.name = flat ? lqn.hashnames[idx] + ".Flat" : lqn.hashnames[idx];
1742 m.flat = flat;
1743
1744 const double rawrepl = lqn.repl[idx];
1745 std::size_t nreplicas = 1;
1746 if (!flat && rawrepl > 1.0 && !callers.empty()) {
1747 // A replicated server is pooled into ONE station instead of one per
1748 // replica when every caller already addresses all of its replicas:
1749 // on a host layer that is the callers replicating in step with it,
1750 // on a task layer it is a declared fan-out covering every replica.
1751 // With no fan-out declared the lookup is 0 < rawrepl and the layer
1752 // materialises the replicas, which is the reference's answer too.
1753 bool reduce = true;
1754 if (ishostlayer) {
1755 for (std::size_t c : callers)
1756 if (lqn.repl[c] != rawrepl) reduce = false;
1757 } else {
1758 for (std::size_t c : callers)
1759 if (lqn.fanout_at(c, idx) < rawrepl) reduce = false;
1760 }
1761 nreplicas = reduce ? 1 : static_cast<std::size_t>(std::llround(rawrepl));
1762 if (reduce && !ishostlayer) single_replica_tasks.insert(idx);
1763 }
1764 const bool reduce_fanout = (nreplicas == 1 && rawrepl > 1.0 && !callers.empty());
1765 const std::vector<double>& mult = lqn.maxmult;
1766
1767 // A client Delay exists unless the layer is a task layer reached only
1768 // by asynchronous callers, which this port refuses upstream.
1769 m.clientIdx = m.add_station(qn::Station<T>{"Clients", NodeType::Delay, SchedStrategy::INF,
1770 std::numeric_limits<double>::infinity(), false, 0});
1771 const std::size_t clientNode = m.node_of_station(m.clientIdx);
1772 m.serverIdx = m.clientIdx + 1;
1773 // One station (times its replicas) per SERVED ELEMENT of this layer.
1774 // Under `srvn` that is one element and `srv[idx] == server`; under
1775 // `flat` every processor and called task of the model is here.
1776 std::vector<std::vector<std::size_t>> srv(lqn.nidx + 1);
1777 std::vector<std::vector<std::size_t>> srvnode(lqn.nidx + 1);
1778 m.server_idx_of.assign(lqn.nidx + 1, 0);
1779 for (std::size_t sidx : idxSet) {
1780 const bool sishost = sidx <= lqn.nhosts;
1781 srv[sidx].resize(nreplicas);
1782 for (std::size_t r = 0; r < nreplicas; ++r) {
1783 qn::Station<T> st;
1784 st.name = r == 0 ? lqn.hashnames[sidx]
1785 : lqn.hashnames[sidx] + "." + std::to_string(r + 1);
1786 // inf-scheduled Queue as Delay node rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
1787 st.nodetype =
1788 lqn.sched[sidx] == SchedStrategy::INF ? NodeType::Delay : NodeType::Queue;
1789 st.sched = lqn.sched[sidx];
1790 // setNumberOfServers no-op rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
1791 st.nservers = lqn.sched[sidx] == SchedStrategy::INF
1792 ? std::numeric_limits<double>::infinity()
1793 : mult[sidx];
1794 st.attr_ishost = flat ? sishost : ishostlayer;
1795 st.attr_idx = sidx;
1796 srv[sidx][r] = m.add_station(st);
1797 srvnode[sidx].push_back(m.node_of_station(srv[sidx][r]));
1798 }
1799 m.server_idx_of[sidx] = srv[sidx][0];
1800 if (sishost)
1801 m.host_stations.push_back(srv[sidx][0]);
1802 else
1803 m.task_stations.push_back(srv[sidx][0]);
1804 }
1805 // the layer's own server, the sole server under `srvn`
1806 const std::vector<std::size_t>& server = srv[idx];
1807 const std::vector<std::size_t>& serverNode = srvnode[idx];
1808
1809 /** Stations of ELEM when it is served in this layer, empty otherwise. */
1810 auto servers_for = [&](std::size_t elem) -> const std::vector<std::size_t>& {
1811 static const std::vector<std::size_t> none;
1812 if (elem >= 1 && elem <= lqn.nidx && !srv[elem].empty()) return srv[elem];
1813 return none;
1814 };
1815 auto server_nodes_for = [&](std::size_t elem) -> const std::vector<std::size_t>& {
1816 static const std::vector<std::size_t> none;
1817 if (elem >= 1 && elem <= lqn.nidx && !srvnode[elem].empty()) return srvnode[elem];
1818 return none;
1819 };
1820 /** True when the processor of task TIDX is a server of this layer. */
1821 auto host_is_server = [&](std::size_t tidx_) {
1822 return !servers_for(lqn.parent[tidx_]).empty();
1823 };
1824 /** True when TIDX calls an entry served by this layer. */
1825 auto is_layer_client = [&](std::size_t tidx_) {
1826 for (std::size_t s : idxSet)
1827 for (std::size_t e : lqn.entriesof[s])
1828 if (lqn.issynccaller.get(tidx_, e)) return true;
1829 return false;
1830 };
1831 /** Declare a class at every server station of this layer. */
1832 auto set_all_servers = [&](std::size_t cl, const Distrib<T>& d) {
1833 for (std::size_t s : idxSet)
1834 for (std::size_t st : srv[s]) m.set_service(st, cl, d);
1835 };
1836
1837 // Routed call groups, resolved from target ENTRIES to the call indices
1838 // that reach them: a group is ONE dispatch with n destinations, so its
1839 // members share a call class and a dispatch class that holds the job at
1840 // the client while the target is picked (callGroupsByCidx in
1841 // buildLayersRecursive.m).
1842 std::vector<std::size_t> group_of_call(lqn.ncalls + 1, 0);
1843 std::vector<std::vector<std::size_t>> group_members(lqn.callgroups.size() + 1);
1844 for (std::size_t g = 0; g < lqn.callgroups.size(); ++g) {
1845 const LqnCallGroup& grp = lqn.callgroups[g];
1846 for (std::size_t tgt : grp.targets)
1847 for (std::size_t cidx : lqn.callsof[grp.caller])
1848 if (lqn.callpair_dst[cidx] == tgt && lqn.calltype[cidx] == CallType::SYNC &&
1849 group_of_call[cidx] == 0) {
1850 group_of_call[cidx] = g + 1;
1851 group_members[g + 1].push_back(cidx);
1852 break;
1853 }
1854 }
1855 // per group: the Router node, the dispatch class and the group class
1856 std::vector<std::size_t> grp_router(lqn.callgroups.size() + 1, 0);
1857 std::vector<std::size_t> grp_dispatch(lqn.callgroups.size() + 1, 0);
1858 std::vector<std::size_t> grp_class(lqn.callgroups.size() + 1, 0);
1859 // (router node, dispatch class, group) triples whose strategy is
1860 // installed once the routing is written
1861 std::vector<std::array<std::size_t, 3>> routed_group_sites;
1862
1863 // Fork/Router/Join construction rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
1864 std::vector<std::size_t> acts_in_caller;
1865 for (std::size_t c : callers)
1866 for (std::size_t a : lqn.actsof[c]) acts_in_caller.push_back(a);
1867 bool hasfork = false, hasjoin = false;
1868 std::size_t maxfanout = 1;
1869 for (std::size_t a : acts_in_caller) {
1870 if (lqn.actposttype[a] == PrecedenceType::POST_AND) hasfork = true;
1871 if (lqn.actpretype[a] == PrecedenceType::PRE_AND) hasjoin = true;
1872 std::size_t nand = 0;
1873 for (std::size_t sx : lqn.graph.succ(a))
1874 if (lqn.actposttype[sx] == PrecedenceType::POST_AND) ++nand;
1875 if (nand > maxfanout) maxfanout = nand;
1876 }
1877 std::size_t forkNode = 0, joinNode = 0, joinStation = 0;
1878 std::vector<std::size_t> forkRouter;
1879 if (hasfork) {
1880 forkNode = m.add_node("Fork_PostAnd", NodeType::Fork, false);
1881 for (std::size_t f = 1; f <= maxfanout; ++f)
1882 forkRouter.push_back(
1883 m.add_node("Fork_PostAnd_" + std::to_string(f), NodeType::Router, true));
1884 }
1885 if (hasjoin) {
1886 qn::Station<T> js;
1887 js.name = "Join_PreAnd";
1888 js.nodetype = NodeType::Join;
1889 js.sched = SchedStrategy::INF;
1890 js.nservers = std::numeric_limits<double>::infinity();
1891 js.attr_ishost = false;
1892 js.attr_idx = 0;
1893 joinStation = m.add_station(js);
1894 joinNode = m.node_of_station(joinStation);
1895 if (forkNode) m.fj.emplace_back(forkNode, joinNode);
1896 }
1897 // A CACHE LAYER is a HOST layer all of whose callers are cache tasks,
1898 // buildLayersRecursive.m:88. The Cache node lives here and not in the
1899 // cache task's own layer: the item lookup is what the task DOES on its
1900 // processor, so the hit and miss branches have to queue at the same
1901 // server the read arrived at.
1902 bool iscachelayer = !flat && ishostlayer && !callers.empty();
1903 for (std::size_t c : callers)
1904 if (!lqn.iscache[c]) iscachelayer = false;
1905 // A SETUP NO LONGER CHANGES HOW A LAYER IS BUILT (buildLayersRecursive.m,
1906 // 2026-08-11). Wiring the setup/delay-off onto the server station routed
1907 // the layer through the open M/G/1-with-setup QBD, which reads the idle
1908 // period off the Poisson rate 1/X and so powers the thread down far more
1909 // often than a CLOSED layer does -- 12.67% below LDES on lqn_setup -- and
1910 // it charged the restart to the ACTIVITY, where it is not host demand.
1911 // The cold start is charged to the ENTRY instead, with the probability
1912 // that the thread was really found down: see setup_charge().
1913 std::size_t cacheNode = 0;
1914 qn::CacheParam<T> cachepar;
1915 if (iscachelayer) {
1916 const std::size_t ct = callers[0];
1917 cachepar.nitems = lqn.nitems[ct];
1918 cachepar.itemcap = lqn.itemcap[ct];
1919 cachepar.replacestrat = lqn.replacestrat[ct];
1920 cacheNode = m.add_node(lqn.hashnames[ct], NodeType::Cache, true);
1921 }
1922
1923 // The open streams this layer carries, decided BEFORE the Source is
1924 // added: m.serverIdx is the client index plus one and replica r is
1925 // m.serverIdx + r, so a station inserted between them would silently
1926 // renumber the servers. Source and Sink therefore come last.
1927 // An async call is declared only in the layer whose server owns the
1928 // called entry, which is serversFor(parent(dst)) in the reference
1929 // (buildLayersRecursive.m:327) and the same test route_sync_call uses.
1930 std::vector<std::size_t> async_here;
1931 for (std::size_t c : callers)
1932 for (std::size_t aidx : lqn.actsof[c])
1933 for (std::size_t cidx : lqn.callsof[aidx])
1934 if (lqn.calltype[cidx] == CallType::ASYNC &&
1935 !servers_for(lqn.parent[lqn.callpair_dst[cidx]]).empty())
1936 async_here.push_back(cidx);
1937 std::vector<std::size_t> open_entries;
1938 for (std::size_t c : callers) {
1939 // An arrival that is the only way into the task is carried by the caller
1940 // CHAIN, not by a stream: that task has no task layer, so nothing ever sets
1941 // its surrogate delay, and a chain cycling against an Immediate one plus a
1942 // stream loads the host twice -- lqn_open_arrival's processor read 0.68
1943 // against 0.32 from lqns, lqsim and LDES. update_think_times closes the
1944 // chain on the known rate instead, as it does for a forwarding target.
1945 if (open_arrival_rate_of(c) > GlobalConstants::FineTol) continue;
1946 for (std::size_t eidx : lqn.entriesof[c])
1947 if (lqn.has_arrival[eidx]) open_entries.push_back(eidx);
1948 }
1949
1950 std::size_t sourceStation = 0, sourceNode = 0, sinkNode = 0;
1951 if (!async_here.empty() || !open_entries.empty()) {
1952 qn::Station<T> src;
1953 src.name = "Source";
1954 src.nodetype = NodeType::Source;
1955 // EXT keeps solver_mva out of both infSET and qSET: the Source is lambda, not a queue
1956 src.sched = SchedStrategy::EXT;
1957 src.nservers = 1.0;
1958 src.attr_ishost = false;
1959 src.attr_idx = 0;
1960 sourceStation = m.add_station(src);
1961 sourceNode = m.node_of_station(sourceStation);
1962 sinkNode = m.add_node("Sink", NodeType::Sink, false);
1963 m.sourceIdx = sourceStation;
1964 m.sinkNode = sinkNode;
1965 }
1966
1967 /** The stack of entry classes at the forks currently open. */
1968 std::vector<std::size_t> forkClassStack;
1969
1970 // class index of each LQN element and each call, 0 when absent
1971 std::vector<std::size_t> cls(lqn.nidx + 1, 0);
1972 std::vector<std::size_t> callcls(lqn.ncalls + 1, 0);
1973 // `<call>.Aux`, present only for a call whose mean count differs from the
1974 // replica count; 0 elsewhere. See the routing in route_sync_call.
1975 std::vector<std::size_t> auxcallcls(lqn.ncalls + 1, 0);
1976
1977 // A caller carries a closed class when its own PROCESSOR is served here
1978 // and something drives it (it is a reference task, is called, is a
1979 // forwarding target or takes an arrival), or when it calls an entry
1980 // served here. Under `srvn` the first disjunct is exactly the host
1981 // layer and the second exactly the task layer, so this reduces to the
1982 // two-branch test it replaces (buildLayersRecursive.m:217).
1983 auto caller_needs_class = [&](std::size_t tidx_caller) {
1984 if (host_is_server(tidx_caller)) {
1985 if (lqn.isref[tidx_caller]) return true;
1986 for (std::size_t e : lqn.entriesof[tidx_caller])
1987 if (lqn.issynccaller.any_col(e) || lqn.isasynccaller.any_col(e) ||
1988 is_fwd_target(e) || lqn.has_arrival[e])
1989 return true;
1990 }
1991 return is_layer_client(tidx_caller);
1992 };
1993 auto caller_acts_visible = [&](std::size_t tidx_caller) {
1994 return host_is_server(tidx_caller) || is_layer_client(tidx_caller);
1995 };
1996 /**
1997 * The population caller TIDX_ contributes to this layer WITHOUT a
1998 * refpath merge, including the infinite-server fallbacks. Shared by the
1999 * first pass, which also records it in njobs, and by resolve_ref_path,
2000 * which compares it against the chain population before any class exists.
2001 */
2002 auto caller_pool = [&](std::size_t tidx_) -> double {
2003 // representative-replica scaling rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
2004 const bool single_replica = reduce_fanout || single_replica_tasks.count(tidx_) > 0;
2005 if (ishostlayer && !single_replica && lqn.repl[tidx_] > 1.0)
2006 host_all_replica_tasks[tidx_] = nreplicas;
2007 if (njobs(tidx_, idx) != 0.0) return njobs(tidx_, idx);
2008 double nj = single_replica ? mult[tidx_] : mult[tidx_] * lqn.repl[tidx_];
2009 if (std::isinf(nj)) {
2010 double s = 0.0;
2011 for (std::size_t c = 1; c <= NT(); ++c)
2012 if (lqn.taskgraph.get(c, tidx_) != Tzero()) s += mult[c];
2013 nj = s;
2014 if (std::isinf(nj)) {
2015 double s2 = 0.0;
2016 for (std::size_t c = 1; c <= NT(); ++c)
2017 if (std::isfinite(mult[c])) s2 += mult[c] * lqn.repl[c];
2018 nj = std::min(s2, 1000.0);
2019 }
2020 }
2021 return nj;
2022 };
2023
2024 // ---- reference-path interlocking (interlock_method = 'refpath') -------
2025 // This layer's client is then not one population per caller but ONE
2026 // chain running from the reference task down through every caller it
2027 // reaches, so two callers that are the same REF customers arriving by
2028 // two routes stop being counted as two. The tables say which callers are
2029 // merged (rp_spliced), which sync calls stop being a surrogate delay and
2030 // become an explicit descent (rp_expand), and which entries above or
2031 // between the callers become hop stages on the client Delay (rp_hop).
2032 // See lqn_ref_routes and _kb/06-solver-catalog.md (LN section).
2033 bool rp_on = false;
2034 std::vector<bool> rp_member(lqn.nidx + 1, false); // task: a caller merged into a chain
2035 std::vector<bool> rp_spliced(lqn.nidx + 1, false); // task: merged AND not the chain head
2036 std::vector<bool> rp_hop(lqn.nidx + 1, false); // entry: a stage of the path
2037 std::vector<bool> rp_expand(lqn.ncalls + 1, false); // call: re-expanded, not a delay
2038 std::vector<std::size_t> rp_child_task(lqn.ncalls + 1, 0); // callee task, when a member
2039 std::vector<std::size_t> rp_child_entry(lqn.ncalls + 1, 0); // callee entry, when a hop
2040 std::vector<T> rp_ret_share(lqn.ncalls + 1, Tzero()); // share of the callee's return
2041 std::vector<T> rp_call_mean(lqn.ncalls + 1, Tzero()); // invocations per execution
2042 std::vector<std::vector<std::size_t>> rp_hop_children(lqn.nidx + 1); // descent order
2043 std::vector<double> rp_hop_seed(lqn.nidx + 1, 0.0); // build-time stage mean
2044 std::vector<std::vector<std::size_t>> rp_member_inbound(lqn.nidx + 1);
2045 std::vector<std::vector<std::size_t>> rp_hop_inbound(lqn.nidx + 1);
2046 std::vector<std::vector<std::size_t>> rp_group_roots;
2047 std::vector<std::size_t> rp_group_reftask;
2048 std::vector<bool> rp_group_head_is_caller;
2049 // classes minted for the path, 0 when absent
2050 std::vector<std::size_t> rp_ref_stage;
2051 std::vector<std::size_t> rp_hop_cls(lqn.nidx + 1, 0), rp_hop_ret(lqn.nidx + 1, 0),
2052 rp_ret_cls(lqn.nidx + 1, 0);
2053 std::vector<std::size_t> rp_gate(lqn.ncalls + 1, 0), rp_aux(lqn.ncalls + 1, 0),
2054 rp_resume(lqn.ncalls + 1, 0);
2055
2056 /** Build-time seed of a hop stage: the entry's own host demand. */
2057 auto host_demand_of_entry = [&](std::size_t eidx_) {
2058 double d = 0.0;
2059 for (std::size_t a_ : lqn.actsof[eidx_]) {
2060 if (lqn.hostdem[a_].disabled) continue;
2061 const double m_ = dbl(lqn.hostdem[a_].mean);
2062 if (std::isfinite(m_)) d += m_;
2063 }
2064 return d;
2065 };
2066 const auto in_idxset = [&](std::size_t t_) {
2067 return std::find(idxSet.begin(), idxSet.end(), t_) != idxSet.end();
2068 };
2069
2070 // Port of resolveRefPath: a pure query over lqn plus the layer's own
2071 // server set, run before any class exists because the first pass needs
2072 // the answer to size the caller populations.
2073 [&]() {
2074 if (interlockMethod != "refpath" || flat || iscachelayer || hasfork || hasjoin) return;
2075 // a Fork flattens every positive alpha in the visit solve; a replicated
2076 // element carries a per-replica njobs convention a shared chain breaks
2077 if (reduce_fanout || nreplicas > 1) return;
2078 for (std::size_t t_ : callers)
2079 if (lqn.repl[t_] > 1.0 || single_replica_tasks.count(t_) > 0) return;
2080 const lqn::LqnRefRoutes<T> R =
2081 lqn::lqn_ref_routes(lqn, callers, opt.interlock_maxpaths, idxSet);
2082 if (!R.why.empty()) {
2083 // named, because the discontinuity is otherwise invisible
2084 std::cerr << "[LINE] Warning: layer '" << lqn.hashnames[idx]
2085 << "' falls back to the 'ilrate' interlock method: " << R.why
2086 << ".\n";
2087 return;
2088 }
2089 std::vector<std::size_t> keep;
2090 for (std::size_t g_ = 0; g_ < R.groups.size(); ++g_) {
2091 const lqn::LqnRefGroup<T>& G = R.groups[g_];
2092 bool wanted = G.members.size() >= 2;
2093 // a lone caller below a reference task: no merge, but the path
2094 // above it becomes explicit instead of a fitted idle time
2095 if (!wanted && opt.interlock_refpath_scope == "all" && !G.head_is_caller &&
2096 !G.members.empty())
2097 wanted = true;
2098 if (!wanted) continue;
2099 // The merge only pays when the chain population it installs is
2100 // STRICTLY below the caller pools it replaces; otherwise the
2101 // group stays on 'ilrate' (17-external-interlock).
2102 double pool = 0.0;
2103 for (std::size_t t_ : G.members) pool += caller_pool(t_);
2104 const double chainPop = mult[G.reftask] * lqn.repl[G.reftask];
2105 if (!(pool > chainPop)) continue;
2106 keep.push_back(g_);
2107 }
2108 if (keep.empty()) return;
2109 // Stage the whole decision, then commit: a refusal found on the
2110 // second group must not leave the first one half-applied.
2111 std::vector<bool> sMember(lqn.nidx + 1, false), sSpliced(lqn.nidx + 1, false),
2112 sHop(lqn.nidx + 1, false), sExpand(lqn.ncalls + 1, false);
2113 std::vector<std::size_t> sChildTask(lqn.ncalls + 1, 0),
2114 sChildEntry(lqn.ncalls + 1, 0);
2115 std::vector<T> sRetShare(lqn.ncalls + 1, Tzero()), sCallMean(lqn.ncalls + 1, Tzero());
2116 std::vector<std::vector<std::size_t>> sHopChildren(lqn.nidx + 1),
2117 sMemberInbound(lqn.nidx + 1), sHopInbound(lqn.nidx + 1);
2118 std::vector<double> sHopSeed(lqn.nidx + 1, 0.0);
2119 std::vector<std::vector<std::size_t>> sRoots;
2120 std::vector<std::size_t> sRefTask;
2121 std::vector<bool> sHeadIsCaller;
2122 for (std::size_t g_ : keep) {
2123 const lqn::LqnRefGroup<T>& G = R.groups[g_];
2124 const std::size_t nE = G.entries.size();
2125 // does this DAG node lead to a caller? reverse topological order
2126 std::vector<bool> leads(G.ismember.begin(), G.ismember.end());
2127 for (std::size_t i_ = nE; i_-- > 0;)
2128 for (const lqn::LqnRefCall<T>& c_ : G.calls)
2129 if (c_.from == i_ && leads[c_.to]) leads[i_] = true;
2130 for (std::size_t t_ : G.members)
2131 if (!caller_needs_class(t_)) return; // no client class to enter
2132 if (G.head_is_caller && !caller_needs_class(G.reftask)) return;
2133 // an infinite reference pool has no chain population
2134 if (!G.head_is_caller && !std::isfinite(lqn.maxmult[G.reftask])) return;
2135 for (std::size_t i_ = 0; i_ < nE; ++i_) {
2136 if (!leads[i_] || G.ismember[i_]) continue;
2137 // a hop served by this layer's own station would be in two places
2138 if (in_idxset(G.etask[i_]) || lqn.repl[G.etask[i_]] > 1.0) return;
2139 }
2140 for (const lqn::LqnRefCall<T>& c_ : G.calls) {
2141 if (!leads[c_.to]) continue;
2142 if (in_idxset(G.etask[c_.to])) return; // callee served by this layer
2143 }
2144 // commit this group into the staging tables
2145 const std::size_t head = G.reftask;
2146 for (std::size_t t_ : G.members) {
2147 sMember[t_] = true;
2148 sSpliced[t_] = !(G.head_is_caller && t_ == head);
2149 }
2150 std::vector<T> inTotMember(lqn.nidx + 1, Tzero()), inTotHop(lqn.nidx + 1, Tzero());
2151 for (const lqn::LqnRefCall<T>& c_ : G.calls) {
2152 if (!leads[c_.to]) continue;
2153 const std::size_t cidx_ = c_.cidx;
2154 const std::size_t fromEidx = G.entries[c_.from];
2155 sExpand[cidx_] = true;
2156 sCallMean[cidx_] = lqn.callproc_mean[cidx_];
2157 sHopChildren[fromEidx].push_back(cidx_);
2158 if (G.ismember[c_.to]) {
2159 const std::size_t tv = G.etask[c_.to];
2160 sChildTask[cidx_] = tv;
2161 sMemberInbound[tv].push_back(cidx_);
2162 inTotMember[tv] = T(inTotMember[tv] + c_.vcall);
2163 } else {
2164 const std::size_t ev = G.entries[c_.to];
2165 sChildEntry[cidx_] = ev;
2166 sHopInbound[ev].push_back(cidx_);
2167 inTotHop[ev] = T(inTotHop[ev] + c_.vcall);
2168 }
2169 sRetShare[cidx_] = c_.vcall; // normalised below
2170 }
2171 for (const lqn::LqnRefCall<T>& c_ : G.calls) {
2172 const std::size_t cidx_ = c_.cidx;
2173 if (!sExpand[cidx_]) continue;
2174 const T tot = sChildTask[cidx_] > 0 ? inTotMember[sChildTask[cidx_]]
2175 : inTotHop[sChildEntry[cidx_]];
2176 sRetShare[cidx_] = dbl(tot) > GlobalConstants::FineTol
2177 ? T(sRetShare[cidx_] / tot)
2178 : Tzero();
2179 }
2180 for (std::size_t i_ = 0; i_ < nE; ++i_)
2181 if (leads[i_] && !G.ismember[i_]) {
2182 const std::size_t ev = G.entries[i_];
2183 sHop[ev] = true;
2184 sHopSeed[ev] = host_demand_of_entry(ev);
2185 }
2186 if (!G.head_is_caller) {
2187 // EVERY entry of the reference task becomes a stage, not only
2188 // those reaching this layer, or the 1/nentries split would
2189 // shrink its denominator and over-drive the chain
2190 for (std::size_t ev : lqn.entriesof[G.reftask])
2191 if (!sHop[ev]) {
2192 sHop[ev] = true;
2193 sHopSeed[ev] = host_demand_of_entry(ev);
2194 }
2195 }
2196 sRoots.push_back(lqn.entriesof[head]);
2197 sRefTask.push_back(head);
2198 sHeadIsCaller.push_back(G.head_is_caller);
2199 }
2200 rp_member = sMember;
2201 rp_spliced = sSpliced;
2202 rp_hop = sHop;
2203 rp_expand = sExpand;
2204 rp_child_task = sChildTask;
2205 rp_child_entry = sChildEntry;
2206 rp_ret_share = sRetShare;
2207 rp_call_mean = sCallMean;
2208 rp_hop_children = sHopChildren;
2209 rp_hop_seed = sHopSeed;
2210 rp_member_inbound = sMemberInbound;
2211 rp_hop_inbound = sHopInbound;
2212 rp_group_roots = sRoots;
2213 rp_group_reftask = sRefTask;
2214 rp_group_head_is_caller = sHeadIsCaller;
2215 rp_on = true;
2216 }();
2217
2218 // ---- first pass: create the classes --------------------------------
2219 for (std::size_t tidx_caller : callers) {
2220 if (caller_needs_class(tidx_caller)) {
2221 double nj = njobs(tidx_caller, idx);
2222 if (nj == 0.0) {
2223 nj = caller_pool(tidx_caller);
2224 njobs(tidx_caller, idx) = nj;
2225 }
2226 // A caller merged into a reference chain brings NO population of
2227 // its own: the customers are the reference task's. Only the CLASS
2228 // is zeroed; njobs still records the pool for update_think_times.
2229 const bool rp_spliced_here = rp_on && rp_spliced[tidx_caller];
2230 qn::JobClass jc;
2231 jc.name = lqn.hashnames[tidx_caller];
2232 jc.type = JobClassType::CLOSED;
2233 jc.population = rp_spliced_here ? 0.0 : nj;
2234 jc.refstat = m.clientIdx;
2235 jc.completes = false;
2236 // ONE reference class per chain: a merged caller yields it to the
2237 // chain head and is normalised through layer_normclass instead
2238 jc.is_ref_class = !rp_spliced_here;
2239 jc.attr_kind = int(LqnElement::TASK);
2240 jc.attr_idx = tidx_caller;
2241 cls[tidx_caller] = m.add_class(jc);
2242 m.attr_tasks.emplace_back(cls[tidx_caller], tidx_caller);
2243 if (lqn.isref[tidx_caller]) {
2244 m.set_service(m.clientIdx, cls[tidx_caller], thinkproc[tidx_caller]);
2245 } else if (rp_spliced_here) {
2246 // the path above this caller is explicit now, so there is no
2247 // idle time left to fit and no update-map row to refresh it
2248 m.set_service(m.clientIdx, cls[tidx_caller], Distrib<T>::immediate());
2249 } else {
2250 // a served task's declared think time is not a per-request
2251 // delay, so the seed carries none either; update_think_times
2252 // replaces this from the first iteration on
2253 m.set_service(m.clientIdx, cls[tidx_caller], Distrib<T>::immediate());
2254 thinkt_map.push_back({idx, tidx_caller, m.clientIdx, cls[tidx_caller]});
2255 }
2256
2257 for (std::size_t eidx : lqn.entriesof[tidx_caller]) {
2258 qn::JobClass ec;
2259 ec.name = lqn.hashnames[eidx];
2260 ec.type = JobClassType::CLOSED;
2261 ec.population = 0.0;
2262 ec.refstat = m.clientIdx;
2263 ec.completes = false;
2264 ec.attr_kind = int(LqnElement::ENTRY);
2265 ec.attr_idx = eidx;
2266 cls[eidx] = m.add_class(ec);
2267 m.attr_entries.emplace_back(cls[eidx], eidx);
2268 m.set_service(m.clientIdx, cls[eidx], Distrib<T>::immediate());
2269 }
2270 }
2271
2272 for (std::size_t aidx : lqn.actsof[tidx_caller]) {
2273 if (caller_acts_visible(tidx_caller)) {
2274 qn::JobClass ac;
2275 ac.name = lqn.hashnames[aidx];
2276 ac.type = JobClassType::CLOSED;
2277 ac.population = 0.0;
2278 ac.refstat = m.clientIdx;
2279 ac.completes = false;
2280 ac.attr_kind = int(LqnElement::ACTIVITY);
2281 ac.attr_idx = aidx;
2282 cls[aidx] = m.add_class(ac);
2283 m.attr_activities.emplace_back(cls[aidx], aidx);
2284 // The host demand is served at the processor's own station
2285 // when this layer holds it; everywhere else the activity is
2286 // a surrogate delay at the client.
2287 const std::size_t hidx = lqn.parent[lqn.parent[aidx]];
2288 if (servers_for(hidx).empty())
2289 m.set_service(m.clientIdx, cls[aidx], servtproc[aidx]);
2290 }
2291 for (std::size_t cidx : lqn.callsof[aidx]) {
2292 if (lqn.calltype[cidx] == CallType::ASYNC) {
2293 // An async call is a stream, not a visit: the caller does
2294 // not block, so it carries no closed class and instead
2295 // drives an open chain of its own out of the Source.
2296 const std::size_t adst = lqn.parent[lqn.callpair_dst[cidx]];
2297 if (servers_for(adst).empty()) continue;
2298 qn::JobClass oc;
2299 oc.name = lqn.callhashnames[cidx];
2300 oc.type = JobClassType::OPEN;
2301 oc.population = std::numeric_limits<double>::infinity();
2302 oc.refstat = sourceStation;
2303 oc.completes = false;
2304 oc.is_ref_class = false;
2305 oc.attr_kind = int(LqnElement::CALL);
2306 oc.attr_idx = cidx;
2307 callcls[cidx] = m.add_class(oc);
2308 m.attr_calls.push_back({callcls[cidx], cidx, lqn.callpair_src[cidx],
2309 lqn.callpair_dst[cidx]});
2310 // A NEGLIGIBLE seed, not the reference's Immediate. The
2311 // rate is replaced from the caller's throughput on the
2312 // first post(), but apply_sink_closure weights the
2313 // Sink->Source arcs by it in between, and Immediate is
2314 // rate 1/0: the closed classes sharing the closure then
2315 // get NaN visits. fj_mmt seeds its own open classes the
2316 // same way, for the same reason.
2317 m.set_service(sourceStation, callcls[cidx],
2319 num_traits<T>::from_double(GlobalConstants::FineTol)));
2320 T minRespTA = Tzero();
2321 for (std::size_t ta : lqn.actsof[adst]) minRespTA += lqn.hostdem[ta].mean;
2322 for (std::size_t st : servers_for(adst)) {
2323 m.set_service(st, callcls[cidx], Distrib<T>::exp_mean(minRespTA));
2324 call_map.push_back({idx, cidx, st, callcls[cidx]});
2325 }
2326 arv_call_map.push_back({idx, cidx, sourceStation, callcls[cidx]});
2327 continue;
2328 }
2329 if (lqn.calltype[cidx] != CallType::SYNC) continue;
2330 const std::size_t gid = group_of_call[cidx];
2331 if (gid != 0) {
2332 // ONE dispatch with n destinations. The members SHARE the
2333 // dispatch class, which is both the class the strategy
2334 // routes and the class that visits the targets: the hop
2335 // must not switch class, because a state-dependent
2336 // routing function is evaluated at zero off the class
2337 // diagonal. The class switch goes on the return arc
2338 // instead, into a group class the job continues in.
2339 if (grp_dispatch[gid] == 0) {
2340 // The strategy is a property of a NODE and routes
2341 // over that node's links, not over one class's arcs,
2342 // so a dedicated Router whose only links are the
2343 // group's targets is the only place where the choice
2344 // is exactly the group's.
2345 const std::string tag =
2346 lqn.hashnames[aidx] + ".Dispatch" + std::to_string(gid);
2347 grp_router[gid] = m.add_node(tag + ".Router", NodeType::Router, true);
2348 qn::JobClass dc;
2349 dc.name = tag;
2350 dc.type = JobClassType::CLOSED;
2351 dc.population = 0.0;
2352 dc.refstat = m.clientIdx;
2353 dc.completes = false;
2354 dc.attr_kind = int(LqnElement::CALL);
2355 dc.attr_idx = cidx;
2356 grp_dispatch[gid] = m.add_class(dc);
2357 m.set_service(m.clientIdx, grp_dispatch[gid], Distrib<T>::immediate());
2358 qn::JobClass gc;
2359 gc.name = lqn.callhashnames[cidx] + ".Group" + std::to_string(gid);
2360 gc.type = JobClassType::CLOSED;
2361 gc.population = 0.0;
2362 gc.refstat = m.clientIdx;
2363 gc.completes = false;
2364 gc.attr_kind = int(LqnElement::CALL);
2365 gc.attr_idx = cidx;
2366 grp_class[gid] = m.add_class(gc);
2367 m.set_service(m.clientIdx, grp_class[gid], Distrib<T>::immediate());
2368 routed_group_sites.push_back(
2369 {grp_router[gid], grp_dispatch[gid], gid});
2370 }
2371 callcls[cidx] = grp_dispatch[gid];
2372 m.attr_calls.push_back({callcls[cidx], cidx, lqn.callpair_src[cidx],
2373 lqn.callpair_dst[cidx]});
2374 for (std::size_t st2 : servers_for(lqn.parent[lqn.callpair_dst[cidx]]))
2375 m.set_service(st2, callcls[cidx], callservtproc[cidx]);
2376 continue;
2377 }
2378 qn::JobClass cc;
2379 cc.name = lqn.callhashnames[cidx];
2380 cc.type = JobClassType::CLOSED;
2381 cc.population = 0.0;
2382 cc.refstat = m.clientIdx;
2383 cc.completes = false;
2384 cc.attr_kind = int(LqnElement::CALL);
2385 cc.attr_idx = cidx;
2386 callcls[cidx] = m.add_class(cc);
2387 m.attr_calls.push_back({callcls[cidx], cidx, lqn.callpair_src[cidx],
2388 lqn.callpair_dst[cidx]});
2389 // An upper bound on the server's response, replaced at the
2390 // first iteration by the measured one. The station seeded is
2391 // the CALLEE's under `flat` and the layer's own under
2392 // `srvn`, where the latter also seeds calls that leave the
2393 // layer -- a phantom rate on a class with no visits here,
2394 // kept because removing it moves the fixed point (see
2395 // seedCallService in buildLayersRecursive.m).
2396 const std::size_t seedidx = flat ? lqn.parent[lqn.callpair_dst[cidx]] : idx;
2397 T minRespT = Tzero();
2398 for (std::size_t ta : lqn.actsof[seedidx]) minRespT += lqn.hostdem[ta].mean;
2399 for (std::size_t st : servers_for(seedidx))
2400 m.set_service(st, callcls[cidx], Distrib<T>::exp_mean(minRespT));
2401 // .Aux class rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
2402 if (lqn.callproc_mean[cidx] !=
2403 num_traits<T>::from_double(double(nreplicas))) {
2404 qn::JobClass xc;
2405 xc.name = lqn.callhashnames[cidx] + ".Aux";
2406 xc.type = JobClassType::CLOSED;
2407 xc.population = 0.0;
2408 xc.refstat = m.clientIdx;
2409 xc.completes = false;
2410 xc.attr_kind = int(LqnElement::CALL);
2411 xc.attr_idx = cidx;
2412 auxcallcls[cidx] = m.add_class(xc);
2413 m.set_service(m.clientIdx, auxcallcls[cidx], Distrib<T>::immediate());
2414 }
2415 }
2416 }
2417 }
2418
2419 // ---- reference-path classes (buildRefPathClasses) ------------------
2420 // Minted with the caller classes. A descent issued from a caller's own
2421 // activity graph REUSES the call classes that already exist as its gate
2422 // and its geometric auxiliary: fresh ones would leave those two with no
2423 // inbound arc at all.
2424 auto rp_class = [&](const std::string& nm, double pop, std::size_t attr) {
2425 qn::JobClass rc;
2426 rc.name = nm;
2427 rc.type = JobClassType::CLOSED;
2428 rc.population = pop;
2429 rc.refstat = m.clientIdx;
2430 rc.completes = false;
2431 rc.is_ref_class = false;
2432 rc.attr_kind = -1;
2433 rc.attr_idx = attr;
2434 const std::size_t k_ = m.add_class(rc);
2435 m.set_service(m.clientIdx, k_, Distrib<T>::immediate());
2436 return k_;
2437 };
2438 if (rp_on) {
2439 for (std::size_t g_ = 0; g_ < rp_group_reftask.size(); ++g_) {
2440 if (rp_group_head_is_caller[g_]) {
2441 rp_ref_stage.push_back(0); // the caller class IS the head
2442 continue;
2443 }
2444 const std::size_t rt = rp_group_reftask[g_];
2445 const std::size_t k_ =
2446 rp_class(lqn.hashnames[rt] + ".RefPath", mult[rt] * lqn.repl[rt], rt);
2447 m.classes[k_ - 1].is_ref_class = true;
2448 m.set_service(m.clientIdx, k_, thinkproc[rt]);
2449 rp_ref_stage.push_back(k_);
2450 }
2451 for (std::size_t ev = 1; ev <= lqn.nidx; ++ev) {
2452 if (!rp_hop[ev]) continue;
2453 const std::size_t k_ = rp_class(lqn.hashnames[ev] + ".RefHop", 0.0, ev);
2454 if (rp_hop_seed[ev] > GlobalConstants::FineTol)
2455 m.set_service(m.clientIdx, k_,
2456 Distrib<T>::exp_mean(num_traits<T>::from_double(rp_hop_seed[ev])));
2457 rp_hop_cls[ev] = k_;
2458 // the entry's own residence, MINUS the descents re-expanded below
2459 // it, PLUS the wait its task's threads queue for
2460 refpath_map.push_back({idx, m.clientIdx, k_, 1, ev, 1.0, 0});
2461 for (std::size_t c_ : rp_hop_children[ev])
2462 refpath_map.push_back({idx, m.clientIdx, k_, 2, c_, -1.0, ev});
2463 refpath_map.push_back({idx, m.clientIdx, k_, 3, lqn.parent[ev], 1.0, 0});
2464 rp_hop_ret[ev] = rp_class(lqn.hashnames[ev] + ".RefHopRet", 0.0, ev);
2465 }
2466 for (std::size_t tv = 1; tv <= lqn.nidx; ++tv)
2467 if (rp_spliced[tv]) rp_ret_cls[tv] = rp_class(lqn.hashnames[tv] + ".RefRet", 0.0, tv);
2468 for (std::size_t cidx_ = 1; cidx_ <= lqn.ncalls; ++cidx_) {
2469 if (!rp_expand[cidx_]) continue;
2470 if (callcls[cidx_] != 0) {
2471 rp_gate[cidx_] = callcls[cidx_];
2472 m.set_service(m.clientIdx, rp_gate[cidx_], Distrib<T>::immediate());
2473 if (auxcallcls[cidx_] != 0) rp_aux[cidx_] = auxcallcls[cidx_];
2474 } else {
2475 rp_gate[cidx_] = rp_class(lqn.callhashnames[cidx_] + ".RefGate", 0.0, cidx_);
2476 if (rp_call_mean[cidx_] != Tone())
2477 rp_aux[cidx_] =
2478 rp_class(lqn.callhashnames[cidx_] + ".RefGate.Aux", 0.0, cidx_);
2479 }
2480 // A MEMBER entered through this gate has its subgraph explicit
2481 // here; the one cost it does not show is the wait for one of its
2482 // own threads, which lives in its own task layer.
2483 if (rp_child_task[cidx_] > 0)
2484 refpath_map.push_back(
2485 {idx, m.clientIdx, rp_gate[cidx_], 3, rp_child_task[cidx_], 1.0, 0});
2486 rp_resume[cidx_] = rp_class(lqn.callhashnames[cidx_] + ".RefResume", 0.0, cidx_);
2487 }
2488 }
2489
2490 // ---- second pass: routing out of the entries -------------------------
2491 struct Ctx {
2492 std::size_t curclass;
2493 int jobpos; // 1 = at the client, 2 = at a server replica, 3 = at the Cache
2494 // The nodes the job stands at when jobpos is atServer. Under `srvn`
2495 // these are always the layer's own server replicas; under `flat`
2496 // they are whichever element's stations the last hop landed on, so
2497 // the position has to be carried rather than assumed.
2498 std::vector<std::size_t> curnodes;
2499 };
2500 const int atClient = 1, atServer = 2, atCache = 3;
2501 std::vector<int> jobposkey(lqn.nidx + 1, atClient);
2502 std::vector<std::size_t> curclasskey(lqn.nidx + 1, 0);
2503 std::vector<std::vector<std::size_t>> curnodeskey(lqn.nidx + 1);
2504
2505 // Port of routeRefPathDescent: one explicit descent into the callee of
2506 // CIDX_, replacing the surrogate delay that would charge its whole
2507 // residence at the client. The multiplicity is a geometric loop per
2508 // edge, never one flat loop of the product, and the m < 1 branch is not
2509 // optional: 1 - 1/m goes negative below one.
2510 auto rp_descent = [&](std::size_t cidx_, Ctx st) -> Ctx {
2511 const std::size_t gate = rp_gate[cidx_], res = rp_resume[cidx_], aux = rp_aux[cidx_];
2512 const T m_ = rp_call_mean[cidx_];
2513 const std::size_t enter = rp_child_task[cidx_] > 0 ? cls[rp_child_task[cidx_]]
2514 : rp_hop_cls[rp_child_entry[cidx_]];
2515 if (st.jobpos == atClient)
2516 m.set_route(st.curclass, gate, clientNode, clientNode, Tone());
2517 else
2518 for (std::size_t nd : st.curnodes) m.set_route(st.curclass, gate, nd, clientNode, Tone());
2519 std::size_t cont;
2520 if (m_ == Tone()) {
2521 m.set_route(gate, enter, clientNode, clientNode, Tone());
2522 cont = res;
2523 } else if (m_ > Tone()) {
2524 m.set_route(gate, enter, clientNode, clientNode, Tone());
2525 m.set_route(res, enter, clientNode, clientNode, T(Tone() - Tone() / m_));
2526 m.set_route(res, aux, clientNode, clientNode, T(Tone() / m_));
2527 cont = aux;
2528 } else {
2529 m.set_route(gate, enter, clientNode, clientNode, m_);
2530 m.set_route(gate, aux, clientNode, clientNode, T(Tone() - m_));
2531 m.set_route(res, aux, clientNode, clientNode, Tone());
2532 cont = aux;
2533 }
2534 st.jobpos = atClient;
2535 st.curnodes.clear();
2536 st.curclass = cont;
2537 return st;
2538 };
2539
2540 std::function<Ctx(std::size_t, std::size_t, Ctx)> recur =
2541 [&](std::size_t tidx_caller, std::size_t aidx, Ctx st) -> Ctx {
2542 jobposkey[aidx] = st.jobpos;
2543 curclasskey[aidx] = st.curclass;
2544 curnodeskey[aidx] = st.curnodes;
2545 const std::vector<std::size_t> nexts = lqn.graph.succ(aidx);
2546 std::size_t lastEntryClass = st.curclass;
2547 // fork pre-state save/restore rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
2548 bool next_is_fork = false;
2549 for (std::size_t sx : nexts)
2550 if (lqn.actposttype[sx] == PrecedenceType::POST_AND) next_is_fork = true;
2551 const Ctx preFork = st;
2552 std::vector<std::size_t> andSuccs;
2553 for (std::size_t sx : nexts)
2554 if (lqn.actposttype[sx] == PrecedenceType::POST_AND) andSuccs.push_back(sx);
2555
2556 for (std::size_t k = 0; k < nexts.size(); ++k) {
2557 const std::size_t nextaidx = nexts[k];
2558 if (next_is_fork) st = preFork;
2559 bool isLoop = lqn.graph.get(aidx, nextaidx) != lqn.dag.get(aidx, nextaidx);
2560 if (lqn.parent[aidx] != lqn.parent[nextaidx]) {
2561 // a call to an entry of another task
2562 std::size_t cidx = 0;
2563 for (std::size_t c : lqn.callsof[aidx])
2564 if (lqn.callpair_dst[c] == nextaidx) cidx = c;
2565 if (cidx == 0) continue;
2566 // FWD lays down no routing: its work is already carried by the pseudo SYNC arc
2567 if (lqn.calltype[cidx] != CallType::SYNC) continue;
2568 const std::size_t gid = group_of_call[cidx];
2569 if (gid != 0) {
2570 // The whole group is routed once, at its FIRST member;
2571 // the others are the same dispatch and lay down nothing.
2572 if (group_members[gid].empty() || group_members[gid][0] != cidx) continue;
2573 // The job is switched into the dispatch class while still
2574 // at the client, so (client, dispatch) carries exactly
2575 // the n arcs the strategy chooses among. The 1/n split
2576 // laid down here is the probabilistic reading a solver
2577 // without state-dependent routing would see; the
2578 // declared strategy replaces it below.
2579 std::vector<std::size_t> tnode2, tstat2, tcall2;
2580 for (std::size_t mc : group_members[gid]) {
2581 const std::size_t tt = lqn.parent[lqn.callpair_dst[mc]];
2582 if (servers_for(tt).empty()) continue;
2583 tnode2.push_back(server_nodes_for(tt)[0]);
2584 tstat2.push_back(servers_for(tt)[0]);
2585 tcall2.push_back(mc);
2586 }
2587 if (tnode2.size() < 2) continue; // not enough of it is here
2588 const std::size_t fromNode2 =
2589 st.jobpos == atClient ? clientNode : st.curnodes[0];
2590 const std::size_t dc = grp_dispatch[gid], gc = grp_class[gid];
2591 m.set_route(st.curclass, dc, fromNode2, grp_router[gid], Tone());
2592 const T share2 =
2593 T(Tone() / num_traits<T>::from_int(int(tnode2.size())));
2594 for (std::size_t d = 0; d < tnode2.size(); ++d) {
2595 m.set_route(dc, dc, grp_router[gid], tnode2[d], share2);
2596 m.set_route(dc, gc, tnode2[d], clientNode, Tone());
2597 m.set_service(tstat2[d], dc, callservtproc[tcall2[d]]);
2598 call_map.push_back({idx, tcall2[d], tstat2[d], dc});
2599 }
2600 st.curclass = gc;
2601 st.jobpos = atClient;
2602 st.curnodes.clear();
2603 continue;
2604 }
2605 if (rp_on && rp_expand[cidx]) {
2606 // the callee is on the reference path into this layer: the
2607 // customer descends into it rather than waiting a delay out
2608 st = rp_descent(cidx, st);
2609 continue;
2610 }
2611 const std::size_t ctgt = lqn.parent[lqn.callpair_dst[cidx]];
2612 st = route_sync_call(m, idx, cidx, st, server_nodes_for(ctgt),
2613 servers_for(ctgt), callcls, auxcallcls, clientNode,
2614 atClient, atServer, flat);
2615 continue;
2616 }
2617 // a successor inside the same task
2618 bool any_entry_succ = false;
2619 for (std::size_t sx : nexts)
2620 if (sx > lqn.eshift && sx <= lqn.eshift + lqn.nentries) any_entry_succ = true;
2621 if (!any_entry_succ) {
2622 st.jobpos = jobposkey[aidx];
2623 st.curclass = curclasskey[aidx];
2624 st.curnodes = curnodeskey[aidx];
2625 } else {
2626 if (k > 0 && nexts[k - 1] > lqn.eshift && nexts[k - 1] <= lqn.eshift + lqn.nentries)
2627 lastEntryClass = st.curclass;
2628 st.jobpos = atClient;
2629 st.curclass = lastEntryClass;
2630 st.curnodes.clear();
2631 }
2632 const T w = lqn.graph.get(aidx, nextaidx);
2633
2634 // THE CACHE READ, buildLayersRecursive.m:743-765. `aidx` is the
2635 // item entry and `nextaidx` the activity bound to it: the job
2636 // goes from the client to the Cache node, which decides the hit
2637 // or the miss and switches the class accordingly, so the read
2638 // class itself is never served anywhere and the branch classes
2639 // pick the work up at the server.
2640 if (iscachelayer && lqn.nitems[aidx] > 0 && cls[nextaidx] != 0) {
2641 const std::size_t readcls = cls[nextaidx];
2642 m.set_route(st.curclass, readcls, clientNode, cacheNode, w);
2643 if (cachepar.pread.size() < m.classes.size())
2644 cachepar.pread.resize(m.classes.size());
2645 cachepar.pread[readcls - 1] = lqn.itemproc[aidx];
2646 const std::vector<std::size_t> hm = lqn.graph.succ(nextaidx);
2647 if (hm.size() != 2)
2648 throw InputError("SolverLN: the cache read '" + lqn.names[nextaidx] +
2649 "' needs exactly one hit and one miss successor");
2650 if (cachepar.hitclass.size() < m.classes.size()) {
2651 cachepar.hitclass.resize(m.classes.size(), 0);
2652 cachepar.missclass.resize(m.classes.size(), 0);
2653 }
2654 cachepar.hitclass[readcls - 1] = cls[hm[0]];
2655 cachepar.missclass[readcls - 1] = cls[hm[1]];
2656 st.jobpos = atCache;
2657 st.curclass = readcls;
2658 st.curnodes.clear();
2659 st = recur(tidx_caller, nextaidx, st);
2660 continue;
2661 }
2662
2663 const bool is_and_join_tail = lqn.actpretype[aidx] == PrecedenceType::PRE_AND;
2664 // the branch index of this successor among the fork's outputs
2665 std::size_t fbranch = 0;
2666 if (next_is_fork)
2667 for (std::size_t q = 0; q < andSuccs.size(); ++q)
2668 if (andSuccs[q] == nextaidx) fbranch = q + 1;
2669
2670 // Where the successor's HOST DEMAND is served: at the station of
2671 // its own processor when this layer holds it, at the client
2672 // otherwise. Under `srvn` that is the host layer's own server
2673 // and nothing else, so this reduces to the `ishostlayer` test
2674 // it replaces.
2675 const std::size_t hidxOf = lqn.parent[lqn.parent[nextaidx]];
2676 const std::vector<std::size_t>& actStations = servers_for(hidxOf);
2677 const std::vector<std::size_t>& actNodes = server_nodes_for(hidxOf);
2678 const bool actAtServer = !actStations.empty();
2679 const std::size_t from = st.jobpos == atClient ? clientNode
2680 : st.jobpos == atCache ? cacheNode
2681 : st.curnodes[0];
2682 // the node a job continues to after this successor is served
2683 // Routes INTO the replica stations share the probability among them,
2684 // as the reference's routing does (BUG-110); a route from replica r
2685 // to replica r is not split.
2686 const T split = actAtServer ? T(Tone() / num_traits<T>::from_double(double(nreplicas)))
2687 : Tone();
2688 const bool oneToOne = st.jobpos != atClient && st.jobpos != atCache &&
2689 st.curnodes.size() == nreplicas;
2690 for (std::size_t r = 0; r < nreplicas; ++r) {
2691 const std::size_t fromNode =
2692 st.jobpos == atClient ? clientNode
2693 : st.jobpos == atCache ? cacheNode
2694 : st.curnodes[std::min(r, st.curnodes.size() - 1)];
2695 const std::size_t toNode = actAtServer ? actNodes[r] : clientNode;
2696 if (next_is_fork && fbranch > 0) {
2697 m.set_route(st.curclass, st.curclass, fromNode, forkNode, Tone());
2698 if (r == 0) forkClassStack.push_back(st.curclass);
2699 m.set_route(st.curclass, st.curclass, forkNode, forkRouter[fbranch - 1],
2700 Tone());
2701 m.set_route(st.curclass, cls[nextaidx], forkRouter[fbranch - 1], toNode,
2702 T(Tone() * split));
2703 } else if (is_and_join_tail) {
2704 // rejoin the class the branch was forked from, then
2705 // leave the Join in the successor's class
2706 if (forkClassStack.empty())
2707 throw InputError("SolverLN: an AND join has no matching fork in '" +
2708 lqn.names[aidx] + "'");
2709 const std::size_t forkClass = forkClassStack.back();
2710 if (r + 1 == nreplicas) forkClassStack.pop_back();
2711 m.set_route(st.curclass, forkClass, fromNode, joinNode, Tone());
2712 m.set_route(forkClass, cls[nextaidx], joinNode, toNode, T(Tone() * split));
2713 } else {
2714 // replica r stays on replica r; a job arriving from one node
2715 // spreads over the replicas (BUG-110)
2716 m.set_route(st.curclass, cls[nextaidx], fromNode, toNode,
2717 oneToOne ? w : T(w * split));
2718 }
2719 if (actAtServer)
2720 m.set_service(actStations[r], cls[nextaidx], lqn.hostdem[nextaidx]);
2721 }
2722 (void)from;
2723 if (actAtServer) {
2724 st.jobpos = atServer;
2725 st.curclass = cls[nextaidx];
2726 st.curnodes = actNodes;
2727 servt_map.push_back({idx, nextaidx, actStations[0], cls[nextaidx]});
2728 } else {
2729 st.jobpos = atClient;
2730 st.curclass = cls[nextaidx];
2731 st.curnodes.clear();
2732 m.set_service(m.clientIdx, cls[nextaidx], servtproc[nextaidx]);
2733 thinkt_map.push_back({idx, nextaidx, m.clientIdx, cls[nextaidx]});
2734 }
2735 if (aidx != nextaidx && !isLoop) {
2736 st = recur(tidx_caller, nextaidx, st);
2737 // close the branch with a reply back to the caller's class; a
2738 // merged caller replies to its OWN return class instead, which
2739 // hands the customer back to whichever descent invoked it
2740 const std::size_t reply =
2741 (rp_on && rp_spliced[tidx_caller]) ? rp_ret_cls[tidx_caller]
2742 : cls[tidx_caller];
2743 if (st.jobpos == atClient) {
2744 m.set_route(st.curclass, reply, clientNode, clientNode, Tone());
2745 } else {
2746 for (std::size_t nd : st.curnodes)
2747 m.set_route(st.curclass, reply, nd, clientNode, Tone());
2748 }
2749 // .Aux completion-guard rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_ln.h)
2750 if (!is_aux_class(m.classes[st.curclass - 1].name))
2751 m.classes[st.curclass - 1].completes = true;
2752 }
2753 }
2754 return st;
2755 };
2756
2757 for (std::size_t tidx_caller : callers) {
2758 if (!caller_needs_class(tidx_caller)) continue;
2759 const std::vector<std::size_t>& ents = lqn.entriesof[tidx_caller];
2760 const T share = T(Tone() / num_traits<T>::from_int(int(ents.size())));
2761 for (std::size_t eidx : ents) {
2762 m.set_route(cls[tidx_caller], cls[eidx], clientNode, clientNode, share);
2763 if (ents.size() > 1)
2764 route_map.push_back({idx, tidx_caller, eidx, m.clientIdx, m.clientIdx,
2765 cls[tidx_caller], cls[eidx]});
2766 Ctx st{cls[eidx], atClient, {}};
2767 recur(tidx_caller, eidx, st);
2768 }
2769 }
2770
2771 // ---- reference-path arcs no activity graph of this layer writes ------
2772 // routeRefPath: the reference stage, the hop stages and every callee's
2773 // return split.
2774 if (rp_on) {
2775 for (std::size_t g_ = 0; g_ < rp_group_reftask.size(); ++g_) {
2776 if (rp_group_head_is_caller[g_]) continue; // its own graph carries the descent
2777 std::vector<std::size_t> roots;
2778 for (std::size_t r_ : rp_group_roots[g_])
2779 if (rp_hop[r_]) roots.push_back(r_);
2780 if (roots.empty()) continue;
2781 // the REF task picks among its entries: a PROBABILISTIC split
2782 const T share = T(Tone() / num_traits<T>::from_int(int(roots.size())));
2783 for (std::size_t r_ : roots) {
2784 m.set_route(rp_ref_stage[g_], rp_hop_cls[r_], clientNode, clientNode, share);
2785 m.set_route(rp_hop_ret[r_], rp_ref_stage[g_], clientNode, clientNode, Tone());
2786 }
2787 }
2788 // the descents below a hop are a SERIES: one cycle traverses all of them
2789 for (std::size_t ev = 1; ev <= lqn.nidx; ++ev) {
2790 if (!rp_hop[ev]) continue;
2791 Ctx cur{rp_hop_cls[ev], atClient, {}};
2792 for (std::size_t c_ : rp_hop_children[ev]) cur = rp_descent(c_, cur);
2793 m.set_route(cur.curclass, rp_hop_ret[ev], clientNode, clientNode, Tone());
2794 }
2795 // Return arcs, split by the share of the callee's invocations each
2796 // descent contributes: v(p_i)*m_i, exact rather than a heuristic.
2797 for (std::size_t tv = 1; tv <= lqn.nidx; ++tv) {
2798 if (!rp_spliced[tv]) continue;
2799 for (std::size_t c_ : rp_member_inbound[tv])
2800 if (rp_ret_share[c_] > Tzero())
2801 m.set_route(rp_ret_cls[tv], rp_resume[c_], clientNode, clientNode,
2802 rp_ret_share[c_]);
2803 }
2804 for (std::size_t ev = 1; ev <= lqn.nidx; ++ev) {
2805 if (!rp_hop[ev]) continue;
2806 for (std::size_t c_ : rp_hop_inbound[ev])
2807 if (rp_ret_share[c_] > Tzero())
2808 m.set_route(rp_hop_ret[ev], rp_resume[c_], clientNode, clientNode,
2809 rp_ret_share[c_]);
2810 }
2811 }
2812
2813 // ---- open streams, laid down AFTER the activity-graph walk ----------
2814 // buildLayersRecursive.m:473 puts the entry-arrival routing here for the
2815 // same reason: the walk above rewrites whole rows of P and would erase
2816 // an arc written before it. Both kinds route Source -> server -> Sink
2817 // within one class, so they never join a closed class in a chain --
2818 // refresh_chains would reject that, the two carrying different refstats.
2819 if (sourceStation != 0) {
2820 const T nrep = num_traits<T>::from_int(int(nreplicas));
2821 for (std::size_t cidx : async_here) {
2822 const std::size_t oc = callcls[cidx];
2823 if (oc == 0) continue;
2824 // the stream enters the CALLEE's station, which under `srvn` is
2825 // this layer's own server and under `flat` one among many
2826 const std::vector<std::size_t>& anode =
2827 server_nodes_for(lqn.parent[lqn.callpair_dst[cidx]]);
2828 const T callmean = lqn.callproc_mean[cidx];
2829 if (callmean < Tone()) {
2830 // fewer than one call per firing: a single Bernoulli pass
2831 m.set_route(oc, oc, sourceNode, sinkNode, T(Tone() - callmean));
2832 for (std::size_t r = 0; r < anode.size(); ++r) {
2833 m.set_route(oc, oc, sourceNode, anode[r], T(callmean / nrep));
2834 m.set_route(oc, oc, anode[r], sinkNode, Tone());
2835 }
2836 } else {
2837 // callmean visits in expectation, as a geometric self-loop
2838 const T p = T(Tone() / callmean);
2839 for (std::size_t r = 0; r < anode.size(); ++r) {
2840 m.set_route(oc, oc, sourceNode, anode[r], T(Tone() / nrep));
2841 for (std::size_t q = 0; q < anode.size(); ++q)
2842 m.set_route(oc, oc, anode[r], anode[q], T((Tone() - p) / nrep));
2843 m.set_route(oc, oc, anode[r], sinkNode, p);
2844 }
2845 }
2846 }
2847 for (std::size_t eidx : open_entries) {
2848 qn::JobClass eo;
2849 eo.name = lqn.hashnames[eidx] + "_Open";
2850 eo.type = JobClassType::OPEN;
2851 eo.population = std::numeric_limits<double>::infinity();
2852 eo.refstat = sourceStation;
2853 eo.completes = false;
2854 eo.is_ref_class = false;
2855 eo.attr_kind = int(LqnElement::ENTRY);
2856 eo.attr_idx = eidx;
2857 const std::size_t ec = m.add_class(eo);
2858 // the arrival is exogenous and fixed, so it is never reseeded
2859 m.set_service(sourceStation, ec, lqn.arrival[eidx]);
2860 // entries are Immediate; the work is the activity bound to them
2861 std::size_t bound = 0;
2862 for (std::size_t sx : lqn.graph.succ(eidx))
2863 if (bound == 0 && sx > lqn.ashift) bound = sx;
2864 const Distrib<T>& svc = bound != 0 ? servtproc[bound] : servtproc[eidx];
2865 // the arrival enters the processor of the entry's task under
2866 // host layering, and the task's own station under `flat`
2867 const std::vector<std::size_t>& ostat =
2868 flat ? servers_for(lqn.parent[eidx]) : server;
2869 const std::vector<std::size_t>& onode =
2870 flat ? server_nodes_for(lqn.parent[eidx]) : serverNode;
2871 for (std::size_t r = 0; r < ostat.size(); ++r) {
2872 m.set_service(ostat[r], ec, svc);
2873 m.set_route(ec, ec, sourceNode, onode[r], T(Tone() / nrep));
2874 m.set_route(ec, ec, onode[r], sinkNode, Tone());
2875 }
2876 }
2877 }
2878
2879 // ---- admission constraint on the server station ---------------------
2880 // buildLayersRecursive.m:596-625. The constraint is stated over LQN
2881 // elements and has to be re-expressed in the layer's own classes: a
2882 // task occupies its host through the classes of its ACTIVITIES, an
2883 // entry is occupied by the classes of the CALLS that target it. The
2884 // columns are summed into the class, never assigned, because several
2885 // classes can stand for one column.
2886 // Rows of the admission constraint, in the layer's own classes: the
2887 // declared lincon first, then the refpath pool rows.
2888 std::vector<std::vector<T>> Arows;
2889 std::vector<T> brows;
2890 if (idx < lqn.lincon_A.size() && lqn.lincon_A[idx].rows() > 0) {
2891 const Matrix<T>& Aelem = lqn.lincon_A[idx];
2892 Matrix<T> Alayer(Aelem.rows(), m.classes.size(), Tzero());
2893 const std::vector<std::size_t>& constrained =
2894 ishostlayer ? lqn.tasksof[idx] : lqn.entriesof[idx];
2895 for (std::size_t j = 0; j < constrained.size() && j < Aelem.cols(); ++j) {
2896 std::vector<std::size_t> layerClasses;
2897 if (ishostlayer) {
2898 for (std::size_t a : lqn.actsof[constrained[j]])
2899 if (cls[a] != 0) layerClasses.push_back(cls[a]);
2900 } else {
2901 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
2902 if (lqn.callpair_dst[c] == constrained[j] && callcls[c] != 0)
2903 layerClasses.push_back(callcls[c]);
2904 }
2905 for (std::size_t k = 0; k < layerClasses.size(); ++k)
2906 for (std::size_t rr = 0; rr < Aelem.rows(); ++rr)
2907 Alayer(rr, layerClasses[k] - 1) =
2908 T(Alayer(rr, layerClasses[k] - 1) + Aelem(rr, j));
2909 }
2910 for (std::size_t rr = 0; rr < Alayer.rows(); ++rr) {
2911 std::vector<T> row(m.classes.size(), Tzero());
2912 for (std::size_t k = 0; k < m.classes.size(); ++k) row[k] = Alayer(rr, k);
2913 Arows.push_back(row);
2914 brows.push_back(lqn.lincon_b[idx][rr]);
2915 }
2916 }
2917 // A merged caller no longer carries its thread pool as a population, so
2918 // the pool is declared as an admission bound on the server instead, BUT
2919 // ONLY WHEN THE LAYER SOLVER CAN HONOUR ONE. Under the default MVA layer
2920 // solver the pool's other carrier, the thread-acquisition wait charged
2921 // at every descent gate and hop stage, carries it alone; emitting the
2922 // region there anyway would change which engine solves the layer.
2923 if (rp_on && layer_solver_declares_region()) {
2924 std::vector<std::size_t> srvEntries;
2925 for (std::size_t sidx : idxSet)
2926 for (std::size_t e : lqn.entriesof[sidx]) srvEntries.push_back(e);
2927 for (std::size_t tv = 1; tv <= lqn.nidx; ++tv) {
2928 if (!rp_spliced[tv]) continue;
2929 const double cap = lqn.maxmult[tv];
2930 if (!std::isfinite(cap) || cap <= 0.0) continue;
2931 std::vector<T> row(m.classes.size(), Tzero());
2932 bool anyc = false;
2933 if (ishostlayer) {
2934 // a task occupies the host through the classes of its activities
2935 for (std::size_t a_ : lqn.actsof[tv])
2936 if (cls[a_] != 0) {
2937 row[cls[a_] - 1] = Tone();
2938 anyc = true;
2939 }
2940 } else {
2941 // a task occupies the server through the calls it makes into it
2942 for (std::size_t a_ : lqn.actsof[tv])
2943 for (std::size_t c_ : lqn.callsof[a_])
2944 if (std::find(srvEntries.begin(), srvEntries.end(),
2945 lqn.callpair_dst[c_]) != srvEntries.end() &&
2946 callcls[c_] != 0) {
2947 row[callcls[c_] - 1] = Tone();
2948 anyc = true;
2949 }
2950 }
2951 if (anyc) {
2952 Arows.push_back(row);
2953 brows.push_back(num_traits<T>::from_double(cap));
2954 }
2955 }
2956 }
2957 {
2958 bool any = false;
2959 for (const std::vector<T>& row : Arows)
2960 for (const T& v : row)
2961 if (v != Tzero()) any = true;
2962 if (any) {
2963 Matrix<T> Alayer(Arows.size(), m.classes.size(), Tzero());
2964 for (std::size_t rr = 0; rr < Arows.size(); ++rr)
2965 for (std::size_t k = 0; k < m.classes.size(); ++k) Alayer(rr, k) = Arows[rr][k];
2966 // ONE region over every replica, not one each: the constraint
2967 // models a passive resource of the server as a whole (a
2968 // semaphore, a connection pool), so the replicas share tokens.
2969 typename qn::NetworkStruct<T>::Region rg;
2970 const std::size_t M = m.stations.size(), K = m.classes.size();
2971 rg.cap.assign(M, std::vector<double>(K + 1, -1.0));
2972 rg.maxmem.assign(M, -1.0);
2973 rg.members.assign(M, false);
2974 rg.rule.assign(K, lang::DropStrategy::WAITQ);
2975 rg.weight.assign(K, Tone());
2976 rg.size.assign(K, Tone());
2977 for (std::size_t r = 0; r < nreplicas; ++r) rg.members[server[r] - 1] = true;
2978 rg.lincon_A = Alayer;
2979 rg.lincon_b = brows;
2980 m.regions.push_back(rg);
2981 }
2982 }
2983
2984 // The Cache node's parameters are only complete once the walk has seen
2985 // every read: pread, hitclass and missclass are all per READ CLASS, and
2986 // the classes are created as the activity graph is traversed.
2987 if (cacheNode != 0) {
2988 cachepar.pread.resize(m.classes.size());
2989 cachepar.hitclass.resize(m.classes.size(), 0);
2990 cachepar.missclass.resize(m.classes.size(), 0);
2991 m.nodeparam[cacheNode] = cachepar;
2992 }
2993
2994 // The declared dispatch replaces the probabilistic split on the router,
2995 // and ONLY on the (router, dispatch class) pair whose arcs are exactly
2996 // the group's targets. The split stays in P underneath, which is what
2997 // `refresh_routing` re-expands for anything that reads a matrix; the SSA
2998 // engine walks the arcs itself and honours the strategy.
2999 for (const std::array<std::size_t, 3>& site : routed_group_sites) {
3000 qn::NodeDef& nd = m.nodes[site[0] - 1];
3001 if (nd.routing.size() < m.classes.size())
3002 nd.routing.resize(m.classes.size(), RoutingStrategy::PROB);
3003 nd.routing[site[1] - 1] = lqn.callgroups[site[2] - 1].strategy;
3004 }
3005
3006 // ---- queue-dependent service rates on the server station ------------
3007 // buildLayersRecursive.m:700-762. Declared over the server's OPERANDS
3008 // (the tasks of a host, the entries of a task) and re-expressed in the
3009 // layer's own classes, on the same mapping the admission constraint
3010 // uses above. The handles are evaluated in two index spaces -- CTMC and
3011 // the exact recursions pass a per-class vector, the AMVA and NC chain
3012 // recursions a per-chain one -- so each carries both column lists and
3013 // picks by the length of what it is handed. The chain list is only known
3014 // after refresh_chains, hence the shared slot filled just below.
3015 std::vector<std::pair<std::shared_ptr<std::vector<std::vector<std::size_t>>>,
3016 std::vector<std::vector<std::size_t>>>> deferred_chaincols;
3017 for (std::size_t sidx : idxSet) {
3018 const bool hasld = sidx < lqn.lldscaling.size() && !lqn.lldscaling[sidx].empty();
3019 const bool hascd = sidx < lqn.cdscaling.size() && bool(lqn.cdscaling[sidx]);
3020 const bool hasjd = sidx < lqn.jdscaling.size() && bool(lqn.jdscaling[sidx]);
3021 const bool haspools = sidx < lqn.pools.size() && !lqn.pools[sidx].empty();
3022 if (!(hasld || hascd || hasjd || haspools)) continue;
3023 const bool sishost = sidx <= lqn.nhosts;
3024 const std::vector<std::size_t>& operandIdx =
3025 sishost ? lqn.tasksof[sidx] : lqn.entriesof[sidx];
3026 std::vector<std::vector<std::size_t>> cols(operandIdx.size());
3027 for (std::size_t j = 0; j < operandIdx.size(); ++j) {
3028 if (sishost) {
3029 for (std::size_t a : lqn.actsof[operandIdx[j]])
3030 if (cls[a] != 0) cols[j].push_back(cls[a] - 1);
3031 } else {
3032 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
3033 if (lqn.callpair_dst[c] == operandIdx[j] && callcls[c] != 0)
3034 cols[j].push_back(callcls[c] - 1);
3035 }
3036 }
3037 const std::size_t R = m.classes.size();
3038 std::shared_ptr<std::vector<std::vector<std::size_t>>> chaincols =
3039 std::make_shared<std::vector<std::vector<std::size_t>>>();
3040 deferred_chaincols.push_back(std::make_pair(chaincols, cols));
3041 for (std::size_t r = 0; r < nreplicas; ++r) {
3042 // The scalings are written straight onto the Station, as the
3043 // region block above writes m.regions: a Layer is a
3044 // NetworkStruct, not the NetworkBuilder that carries the
3045 // set_*_dependence helpers.
3046 qn::Station<T>& stn = m.stations[srv[sidx][r] - 1];
3047 if (hasld) stn.lldscaling = lqn.lldscaling[sidx];
3048 if (hascd) {
3049 // beta_{i,r} is product form only while an operand maps to a
3050 // single class; where it aggregates several, the same
3051 // scaling is emitted as a joint dependence, numerically
3052 // identical but no longer exact.
3053 bool one_class_each = true;
3054 for (std::size_t j = 0; j < cols.size(); ++j)
3055 if (cols[j].size() > 1) one_class_each = false;
3056 const lang::CdScaling<T> h =
3057 layer_dep_handle(lqn.cdscaling[sidx], cols, chaincols, R);
3058 const std::vector<T> pk = layer_peak(lqn.cdscalingpeak[sidx], cols, R);
3059 if (one_class_each) {
3060 stn.cdscaling = h;
3061 stn.cdscalingpeak = pk;
3062 } else {
3063 stn.jdscaling = h;
3064 stn.jdscalingpeak = pk;
3065 }
3066 }
3067 if (hasjd) {
3068 stn.jdscaling = layer_dep_handle(lqn.jdscaling[sidx], cols, chaincols, R);
3069 stn.jdscalingpeak = layer_peak(lqn.jdscalingpeak[sidx], cols, R);
3070 }
3071 if (haspools) {
3072 // A compatibility declaration IS a rate law: the pools clear
3073 // the activated-server rate of api::sn_compat_rate, order
3074 // independent at every integer state. sn_compat_scaling
3075 // normalises it against the rate the SAME population would
3076 // get under full compatibility, so eta isolates the
3077 // compatibility GRAPH and a fully-compatible pool is the
3078 // neutral eta == 1; the low-occupancy loss stays with the
3079 // solver's own multiserver term.
3080 const lqn::ServerPools<T>& pl = lqn.pools[sidx];
3081 const Matrix<T> compat = pl.compat;
3082 const std::vector<double> counts = pl.counts;
3083 const std::vector<T> rates = pl.rates;
3084 lang::CdScaling<T> etaPool = [compat, counts,
3085 rates](const std::vector<T>& nop) {
3086 return std::vector<T>(
3087 1, api::sn_compat_scaling(compat, counts, rates, nop));
3088 };
3089 stn.jdscaling = layer_dep_handle(etaPool, cols, chaincols, R);
3090 stn.jdscalingpeak = layer_peak(
3091 std::vector<T>(cols.size(), num_traits<T>::from_int(1)), cols, R);
3092 }
3093 }
3094 }
3095
3096 if (rp_on) {
3097 // Per-class normalisation task, read by layer_refclass. A merged chain
3098 // holds more than one caller and only ONE reference class, so without
3099 // this every merged caller's residence would come out scaled by the
3100 // product of the call multiplicities along the path.
3101 std::vector<std::size_t> nc(m.classes.size(), 0);
3102 for (std::size_t tv = 1; tv <= lqn.nidx; ++tv) {
3103 if (!rp_member[tv] || cls[tv] == 0) continue;
3104 const std::size_t own = cls[tv];
3105 std::vector<std::size_t> owned{cls[tv]};
3106 for (std::size_t e_ : lqn.entriesof[tv]) owned.push_back(cls[e_]);
3107 for (std::size_t a_ : lqn.actsof[tv]) {
3108 owned.push_back(cls[a_]);
3109 for (std::size_t c_ : lqn.callsof[a_]) {
3110 owned.push_back(callcls[c_]);
3111 owned.push_back(auxcallcls[c_]);
3112 }
3113 }
3114 // an open class (an async call) has no chain reference rate of its own
3115 for (std::size_t q_ : owned)
3116 if (q_ != 0 && m.classes[q_ - 1].type == JobClassType::CLOSED) nc[q_ - 1] = own;
3117 }
3118 layer_normclass[idx] = nc;
3119 ref_path_audit(m, idx, callers, caller_needs_class, rp_spliced, rp_member, rp_hop,
3120 rp_expand, rp_group_head_is_caller, rp_ref_stage, rp_hop_cls,
3121 rp_hop_ret, rp_ret_cls, rp_gate, rp_aux, rp_resume, cls);
3122 }
3123
3124 // a priority-scheduling processor orders its callers by task priority
3125 apply_host_task_priorities(m, idxSet);
3126 m.refresh_chains();
3127 // The chain columns of every handle emitted above, now that the chains
3128 // exist. The slots are shared with the handles, so filling them here
3129 // reaches every replica without rebuilding a single lambda.
3130 for (std::size_t d = 0; d < deferred_chaincols.size(); ++d) {
3131 const std::vector<std::vector<std::size_t>>& cols = deferred_chaincols[d].second;
3132 std::vector<std::vector<std::size_t>>& out = *deferred_chaincols[d].first;
3133 out.assign(cols.size(), std::vector<std::size_t>());
3134 for (std::size_t j = 0; j < cols.size(); ++j) {
3135 for (std::size_t k = 0; k < cols[j].size(); ++k)
3136 for (std::size_t cc = 0; cc < m.chains.size(); ++cc)
3137 if (cols[j][k] < m.chains[cc].size() && m.chains[cc][cols[j][k]]) {
3138 bool seen = false;
3139 for (std::size_t q = 0; q < out[j].size(); ++q)
3140 if (out[j][q] == cc) seen = true;
3141 if (!seen) out[j].push_back(cc);
3142 }
3143 }
3144 }
3145 // A layer holding a POST_AND fork is a fork-join model like any other,
3146 // and the reference builds it as a `Network`, so buildLayers's
3147 // `getStruct()` runs the MMT node-visit pass on it: layer P:P2 of
3148 // lqn_workflows reports a Join visit of 4.3333, not the 1 the routing
3149 // solve leaves. THE LATER `refresh_chains()` CALLS DO NOT REPEAT IT, and
3150 // that is the reference's behaviour rather than an oversight here:
3151 // SolverLN.m re-runs `refreshChains()` alone after a routing reset,
3152 // which recomputes the visits WITHOUT the pass.
3154 }
3155
3156 /**
3157 * Port of layerSolverDeclaresRegion: true when the layer solver's feature
3158 * set covers an admission constraint. Asked while a layer is being built, so
3159 * it reads the solver CLASS's feature set, as the reference does: NC, FLD
3160 * and SSA declare Region, MVA does not.
3161 */
3162 bool layer_solver_declares_region() const {
3163 // SolverCTMC, SolverLDES and SolverJMT declare Region in the reference's feature set; SolverMAM does not.
3164 return opt.layer_solver == "nc" || opt.layer_solver == "fluid" || opt.layer_solver == "ssa" ||
3165 opt.layer_solver == "ctmc" || opt.layer_solver == "ldes" || opt.layer_solver == "jmt";
3166 }
3167
3168 /** True when the layer engine is a simulator, i.e. `isStochastic()` of the reference's layer solver. */
3169 bool layer_solver_is_stochastic() const {
3170 return opt.layer_solver == "ssa" || opt.layer_solver == "ldes" || opt.layer_solver == "jmt";
3171 }
3172
3173 /**
3174 * The seed SolverLN.pre gives stochastic layer e (0-based here) in Robbins-Monro mode:
3175 * `seed + (it-1)*nlayers + e`, with e 1-based there. `cur_it` is the outer iteration
3176 * analyze() is running; a solve outside the sweep (sensitivity, transient) reuses the last.
3177 */
3178 long layer_seed(long base, std::size_t e) const {
3179 const long it = cur_it > 0 ? cur_it : 1;
3180 return base + (it - 1) * static_cast<long>(ensemble.size()) + static_cast<long>(e) + 1;
3181 }
3182
3183 /**
3184 * Port of refPathAudit. Only the classes refpath wrote are checked: every
3185 * row they leave a node by must sum to one with no negative entry, and the
3186 * layer must carry exactly one reference class per chain it built, which is
3187 * what refresh_chains requires. Warnings, as in the reference.
3188 */
3189 template <class NeedsClass>
3190 void ref_path_audit(const qn::Layer<T>& m, std::size_t idx,
3191 const std::vector<std::size_t>& callers, const NeedsClass& needs_class,
3192 const std::vector<bool>& spliced, const std::vector<bool>& member,
3193 const std::vector<bool>& hop, const std::vector<bool>& expand,
3194 const std::vector<bool>& head_is_caller,
3195 const std::vector<std::size_t>& ref_stage,
3196 const std::vector<std::size_t>& hop_cls,
3197 const std::vector<std::size_t>& hop_ret,
3198 const std::vector<std::size_t>& ret_cls,
3199 const std::vector<std::size_t>& gate, const std::vector<std::size_t>& aux,
3200 const std::vector<std::size_t>& resume,
3201 const std::vector<std::size_t>& cls) const {
3202 std::set<std::size_t> watch;
3203 for (std::size_t k : ref_stage)
3204 if (k) watch.insert(k);
3205 for (std::size_t e = 1; e < hop.size(); ++e)
3206 if (hop[e]) {
3207 if (hop_cls[e]) watch.insert(hop_cls[e]);
3208 if (hop_ret[e]) watch.insert(hop_ret[e]);
3209 }
3210 for (std::size_t t = 1; t < member.size(); ++t)
3211 if (member[t]) {
3212 if (ret_cls[t]) watch.insert(ret_cls[t]);
3213 if (cls[t]) watch.insert(cls[t]);
3214 }
3215 for (std::size_t c = 1; c < expand.size(); ++c)
3216 if (expand[c]) {
3217 if (gate[c]) watch.insert(gate[c]);
3218 if (aux[c]) watch.insert(aux[c]);
3219 if (resume[c]) watch.insert(resume[c]);
3220 }
3221 const std::size_t I = m.nodes.size();
3222 for (std::size_t r : watch) {
3223 for (std::size_t n = 1; n <= I; ++n) {
3224 double tot = 0.0;
3225 bool neg = false;
3226 for (const auto& kv : m.P) {
3227 if (kv.first.first != r || n > kv.second.rows()) continue;
3228 for (std::size_t j = 0; j < kv.second.cols(); ++j) {
3229 const double v = dbl(kv.second(n - 1, j));
3230 if (v < -GlobalConstants::FineTol) neg = true;
3231 tot += v;
3232 }
3233 }
3234 if (neg)
3235 std::cerr << "[LINE] Warning: refpath layer '" << lqn.hashnames[idx]
3236 << "': class '" << m.classes[r - 1].name << "' routes out of node "
3237 << n << " with a negative probability.\n";
3238 if (tot > GlobalConstants::FineTol && std::fabs(tot - 1.0) > 1e-6)
3239 std::cerr << "[LINE] Warning: refpath layer '" << lqn.hashnames[idx]
3240 << "': class '" << m.classes[r - 1].name << "' leaves node " << n
3241 << " with total probability " << tot << ".\n";
3242 }
3243 }
3244 std::size_t nref = 0;
3245 for (const qn::JobClass& jc : m.classes)
3246 if (jc.is_ref_class) ++nref;
3247 std::size_t expect = 0;
3248 for (bool h : head_is_caller)
3249 if (!h) ++expect;
3250 for (std::size_t t : callers)
3251 if (!spliced[t] && needs_class(t)) ++expect;
3252 if (nref != expect)
3253 std::cerr << "[LINE] Warning: refpath layer '" << lqn.hashnames[idx] << "' declares "
3254 << nref << " reference classes where " << expect
3255 << " were built; a chain holding two makes refresh_chains throw.\n";
3256 }
3257
3258 /**
3259 * Port of routeSynchCall (buildLayersRecursive.m).
3260 *
3261 * A synchronous call is made `callmean` times per visit to the calling
3262 * activity, against `nreplicas` server replicas. The branch is chosen by
3263 * callmean ALONE, against 1 and never against nreplicas: a Bernoulli pass
3264 * carries at most one call, more than one needs the geometric loop through
3265 * the `<call>.Aux` class, and the replicas only SPLIT each probability by
3266 * ntgt -- they never relax that bound (buildLayersRecursive.m:1028-1051).
3267 * Comparing against nreplicas instead agrees with the reference only at
3268 * nreplicas == 1; on lqn_sockshop's replicated P2_1 it put a callmean of 1
3269 * on the < branch and doubled T1's throughput and P2_1's utilization.
3270 *
3271 * A second class is needed because a self-loop on the call class would also
3272 * re-enter its service. Which of the two carries the call time depends on
3273 * the direction:
3274 *
3275 * callmean < 1 the mean count is folded into the DEMAND
3276 * (callservt = callmean * W), so the call class must
3277 * be visited exactly once or the time is discounted
3278 * twice; .Aux absorbs the remaining branch.
3279 * callmean > 1 the call class is re-entered, and .Aux is the
3280 * return path that closes the loop with probability
3281 * 1/callmean, giving a mean of callmean visits.
3282 *
3283 * The four cases below are (job at client / at server) x (call targets an
3284 * entry of THIS layer's server / of some other task).
3285 */
3286 struct CtxPair {
3287 std::size_t curclass;
3288 int jobpos;
3289 };
3290
3291 /** The `<call>.Aux` suffix, which is how the reference identifies them too. */
3292 static bool is_aux_class(const std::string& name) {
3293 return name.size() >= 4 && name.compare(name.size() - 4, 4, ".Aux") == 0;
3294 }
3295 template <class Ctx>
3296 Ctx route_sync_call(qn::Layer<T>& m, std::size_t idx, std::size_t cidx, Ctx st,
3297 const std::vector<std::size_t>& tnode,
3298 const std::vector<std::size_t>& tstat,
3299 const std::vector<std::size_t>& callcls,
3300 const std::vector<std::size_t>& auxcallcls, std::size_t clientNode,
3301 int atClient, int atServer, bool flat) {
3302 const T one = Tone();
3303 // The callee's stations in THIS layer, empty when it is served
3304 // elsewhere: under `srvn` that is the layer's own server or nothing,
3305 // under `flat` it is whichever of the many servers the call targets.
3306 const std::size_t ntgt = tnode.size();
3307 const bool to_this_server = ntgt > 0;
3308 const T nrep = num_traits<T>::from_int(int(ntgt > 0 ? ntgt : 1));
3309 const T share = T(one / nrep);
3310 const T callmean = lqn.callproc_mean[cidx];
3311 const std::size_t cc = callcls[cidx];
3312 const std::size_t ax = auxcallcls[cidx];
3313 const bool below = callmean < one;
3314 const bool above = callmean > one;
3315 if (st.jobpos == atClient) {
3316 if (to_this_server) {
3317 if (below) {
3318 m.set_route(st.curclass, ax, clientNode, clientNode, T(one - callmean));
3319 for (std::size_t r = 0; r < ntgt; ++r) {
3320 m.set_route(st.curclass, cc, clientNode, tnode[r], T(callmean / nrep));
3321 m.set_route(cc, cc, tnode[r], clientNode, one);
3322 }
3323 // keeps .Aux attached to the routing graph; carries no time
3324 m.set_route(ax, cc, clientNode, clientNode, one);
3325 } else if (above) {
3326 for (std::size_t r = 0; r < ntgt; ++r) {
3327 m.set_route(st.curclass, cc, clientNode, tnode[r], share);
3328 m.set_route(cc, ax, tnode[r], clientNode, one);
3329 m.set_route(ax, cc, clientNode, tnode[r], T((one - one / callmean) / nrep));
3330 }
3331 m.set_route(ax, cc, clientNode, clientNode, T(one / callmean));
3332 } else {
3333 for (std::size_t r = 0; r < ntgt; ++r) {
3334 m.set_route(st.curclass, cc, clientNode, tnode[r], share);
3335 m.set_route(cc, cc, tnode[r], clientNode, one);
3336 }
3337 }
3338 for (std::size_t r = 0; r < ntgt; ++r) {
3339 m.set_service(tstat[r], cc, callservtproc[cidx]);
3340 call_map.push_back({idx, cidx, tstat[r], cc});
3341 }
3342 m.set_service(m.clientIdx, cc, Distrib<T>::immediate());
3343 st.jobpos = atClient;
3344 st.curnodes.clear();
3345 st.curclass = cc;
3346 } else {
3347 m.set_route(st.curclass, cc, clientNode, clientNode, one);
3348 if (below || above) {
3349 m.set_route(cc, ax, clientNode, clientNode, one);
3350 st.curclass = ax;
3351 } else {
3352 st.curclass = cc;
3353 }
3354 st.jobpos = atClient;
3355 st.curnodes.clear();
3356 m.set_service(m.clientIdx, cc, callservtproc[cidx]);
3357 call_map.push_back({idx, cidx, m.clientIdx, cc});
3358 }
3359 } else {
3360 // the node the job stands at, one per replica of wherever it landed
3361 auto fromNode = [&](std::size_t r) {
3362 return st.curnodes[std::min(r, st.curnodes.size() - 1)];
3363 };
3364 if (to_this_server) {
3365 if (below) {
3366 // The skip and the call merge back in the CALL class at the
3367 // client, exactly as in the atClient branch above. Routing
3368 // the skip into the call class instead and leaving in .Aux
3369 // gives .Aux no inbound arc at all, so its chain has no
3370 // reference class (buildLayersRecursive.m:1100-1118). This
3371 // port did that until 2026-08-11 and the arm is the same
3372 // under both layerings, as it is in the reference.
3373 for (std::size_t r = 0; r < ntgt; ++r) {
3374 m.set_route(st.curclass, ax, fromNode(r), clientNode, T(one - callmean));
3375 m.set_route(st.curclass, cc, fromNode(r), tnode[r], T(callmean / nrep));
3376 m.set_route(cc, cc, tnode[r], clientNode, one);
3377 }
3378 m.set_route(ax, cc, clientNode, clientNode, one);
3379 m.set_service(m.clientIdx, cc, Distrib<T>::immediate());
3380 st.jobpos = atClient;
3381 st.curnodes.clear();
3382 st.curclass = cc;
3383 } else if (above) {
3384 if (flat) {
3385 // the geometric repeat transits the client between
3386 // visits; a self-loop would merge them into one
3387 for (std::size_t r = 0; r < ntgt; ++r) {
3388 m.set_route(st.curclass, cc, fromNode(r), tnode[r], one);
3389 m.set_route(cc, ax, tnode[r], clientNode, one);
3390 m.set_route(ax, cc, clientNode, tnode[r],
3391 T((one - one / callmean) / nrep));
3392 }
3393 m.set_route(ax, cc, clientNode, clientNode, T(one / callmean));
3394 m.set_service(m.clientIdx, cc, Distrib<T>::immediate());
3395 st.curclass = cc;
3396 } else {
3397 for (std::size_t r = 0; r < ntgt; ++r) {
3398 m.set_route(st.curclass, cc, fromNode(r), tnode[r], one);
3399 m.set_route(cc, cc, tnode[r], tnode[r], T(one - one / callmean));
3400 m.set_route(cc, ax, tnode[r], clientNode, T(one / callmean));
3401 }
3402 st.curclass = ax;
3403 }
3404 st.jobpos = atClient;
3405 st.curnodes.clear();
3406 } else {
3407 for (std::size_t r = 0; r < ntgt; ++r)
3408 m.set_route(st.curclass, cc, fromNode(r), tnode[r], one);
3409 if (flat) {
3410 // the reply returns the job to the client, which is
3411 // where the successor restoration expects it
3412 for (std::size_t r = 0; r < ntgt; ++r)
3413 m.set_route(cc, cc, tnode[r], clientNode, one);
3414 m.set_service(m.clientIdx, cc, Distrib<T>::immediate());
3415 st.jobpos = atClient;
3416 st.curnodes.clear();
3417 } else {
3418 st.jobpos = atServer;
3419 st.curnodes = tnode;
3420 }
3421 st.curclass = cc;
3422 }
3423 for (std::size_t r = 0; r < ntgt; ++r) {
3424 m.set_service(tstat[r], cc, callservtproc[cidx]);
3425 call_map.push_back({idx, cidx, tstat[r], cc});
3426 }
3427 } else {
3428 for (std::size_t nd : st.curnodes)
3429 m.set_route(st.curclass, cc, nd, clientNode, one);
3430 if (below || above) {
3431 m.set_route(cc, ax, clientNode, clientNode, one);
3432 st.curclass = ax;
3433 } else {
3434 st.curclass = cc;
3435 }
3436 st.jobpos = atClient;
3437 st.curnodes.clear();
3438 m.set_service(m.clientIdx, cc, callservtproc[cidx]);
3439 call_map.push_back({idx, cidx, m.clientIdx, cc});
3440 }
3441 }
3442 return st;
3443 }
3444
3445 // -----------------------------------------------------------------------
3446 // getEntryServiceMatrix
3447 // -----------------------------------------------------------------------
3448
3449 /**
3450 * Reachability of activities and calls from each entry, as a 0/1 matrix over
3451 * the combined element-and-call index space. Multiplying it by
3452 * [residt; callresidt] sums an entry's whole service into one number.
3453 */
3454 void build_entry_service_matrix() {
3455 const std::size_t dim = lqn.nidx + lqn.ncalls;
3456 servtmatrix = Matrix<T>(dim + 1, dim + 1, Tzero());
3457 std::function<void(std::size_t, std::size_t)> rec = [&](std::size_t aidx, std::size_t eidx) {
3458 for (std::size_t nextaidx : lqn.graph.succ(aidx)) {
3459 const bool isLoop = lqn.graph.get(aidx, nextaidx) != lqn.dag.get(aidx, nextaidx);
3460 if (lqn.parent[aidx] != lqn.parent[nextaidx]) {
3461 for (std::size_t cidx : lqn.callsof[aidx])
3462 if (lqn.calltype[cidx] == CallType::SYNC)
3463 servtmatrix(eidx, lqn.nidx + cidx) = Tone();
3464 } else if (nextaidx != aidx && !isLoop) {
3465 servtmatrix(eidx, nextaidx) = Tone();
3466 rec(nextaidx, eidx);
3467 }
3468 }
3469 };
3470 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
3471 const std::size_t eidx = lqn.eshift + e;
3472 rec(eidx, eidx);
3473 }
3474 }
3475
3476 // -----------------------------------------------------------------------
3477 // initInterlock
3478 // -----------------------------------------------------------------------
3479
3480 /** Port of initInterlock: the LQNS V5 static interlock analysis. */
3481 /**
3482 * True when the interlock correction would act on this model: some server has
3483 * interlocked sources (il_num_sources > 0 with common parent entries). The tables
3484 * only read lqn, so they are built here when absent. Read by the 'srvn' alias:
3485 * srvn.ph builds no call classes and switches the correction off, which reported the
3486 * uncorrected answer on an interlocked model (10-interlock 0.2236 against lqns's
3487 * 0.25), so the alias takes srvn.cs there. Port of SolverLN.hasInterlock.
3488 */
3489 bool has_interlock() {
3490 if (!opt.interlocking) return false;
3491 if (il_num_sources.empty()) init_interlock();
3492 for (std::size_t s = 0; s < il_num_sources.size(); ++s)
3493 if (il_num_sources[s] > 0.0 && s < il_common_entries.size() &&
3494 !il_common_entries[s].empty())
3495 return true;
3496 return false;
3497 }
3498
3499 void init_interlock() {
3500 const std::size_t NE = lqn.nentries;
3501 il_all = Matrix<T>(NE + 1, NE + 1, Tzero());
3502 il_ph1 = Matrix<T>(NE + 1, NE + 1, Tzero());
3503
3504 std::function<void(std::size_t, std::size_t, T, T, std::vector<bool>&, int)> trace =
3505 [&](std::size_t eidx, std::size_t root_e, T pall, T pph1, std::vector<bool>& visited,
3506 int depth) {
3507 if (eidx <= lqn.eshift || eidx > lqn.eshift + NE) return;
3508 const std::size_t e = eidx - lqn.eshift;
3509 if (visited[e]) return;
3510 visited[e] = true;
3511 il_all(root_e, e) = T(il_all(root_e, e) + pall);
3512 il_ph1(root_e, e) = T(il_ph1(root_e, e) + pph1);
3513 for (std::size_t aidx : lqn.actsof[eidx]) {
3514 if (aidx <= lqn.ashift || aidx > lqn.ashift + lqn.nacts) continue;
3515 const std::size_t a = aidx - lqn.ashift;
3516 if (depth > 0 && lqn.actphase[a] > 1) continue;
3517 const bool is_ph1 = lqn.actphase[a] <= 1;
3518 for (std::size_t cidx : lqn.callsof[aidx]) {
3519 if (lqn.calltype[cidx] != CallType::SYNC) continue;
3520 if (!(lqn.callproc_mean[cidx] > Tzero())) continue;
3521 const std::size_t dst = lqn.callpair_dst[cidx];
3522 if (dst <= lqn.eshift || dst > lqn.eshift + NE) continue;
3523 trace(dst, root_e, T(pall * lqn.callproc_mean[cidx]),
3524 is_ph1 ? T(pph1 * lqn.callproc_mean[cidx]) : Tzero(), visited,
3525 depth + 1);
3526 }
3527 }
3528 visited[e] = false;
3529 };
3530 for (std::size_t e = 1; e <= NE; ++e) {
3531 std::vector<bool> visited(NE + 1, false);
3532 trace(lqn.eshift + e, e, Tone(), Tone(), visited, 0);
3533 }
3534
3535 il_common_entries.assign(NT() + 1, {});
3536 il_src_all.assign(NT() + 1, {});
3537 il_src_ph2.assign(NT() + 1, {});
3538 il_num_sources.assign(NT() + 1, 0.0);
3539
3540 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
3541 const std::size_t tidx = lqn.tshift + t;
3542 if (lqn.isref[tidx] || lqn.sched[tidx] == SchedStrategy::INF) continue;
3543 interlock_for_server(tidx);
3544 }
3545 for (std::size_t h = 1; h <= lqn.nhosts; ++h) {
3546 if (lqn.sched[h] == SchedStrategy::INF) continue;
3547 interlock_for_server(h);
3548 }
3549 }
3550
3551 std::vector<std::size_t> server_entry_nums(std::size_t serverIdx) const {
3552 std::vector<std::size_t> out;
3553 if (serverIdx <= lqn.nhosts) {
3554 for (std::size_t tidx : lqn.tasksof[serverIdx])
3555 for (std::size_t se : lqn.entriesof[tidx]) out.push_back(se - lqn.eshift);
3556 } else {
3557 for (std::size_t se : lqn.entriesof[serverIdx]) out.push_back(se - lqn.eshift);
3558 }
3559 return out;
3560 }
3561
3562 std::vector<std::size_t> client_tasks(std::size_t serverIdx) const {
3563 std::vector<std::size_t> out;
3564 if (serverIdx <= lqn.nhosts) return lqn.tasksof[serverIdx];
3565 for (std::size_t se : lqn.entriesof[serverIdx])
3566 for (std::size_t ci : lqn.iscaller.col(se))
3567 if (ci > lqn.tshift && ci <= NT()) out.push_back(ci);
3568 std::sort(out.begin(), out.end());
3569 out.erase(std::unique(out.begin(), out.end()), out.end());
3570 return out;
3571 }
3572
3573 std::vector<std::size_t> call_dst_tasks(std::size_t src_eidx, std::size_t target_e) const {
3574 std::vector<std::size_t> out;
3575 for (std::size_t aidx : lqn.actsof[src_eidx]) {
3576 if (aidx <= lqn.ashift || aidx > lqn.ashift + lqn.nacts) continue;
3577 for (std::size_t cidx : lqn.callsof[aidx]) {
3578 if (lqn.calltype[cidx] != CallType::SYNC) continue;
3579 const std::size_t dst = lqn.callpair_dst[cidx];
3580 const std::size_t de = dst - lqn.eshift;
3581 if (de >= 1 && de <= lqn.nentries && il_all(de, target_e) > Tzero())
3582 out.push_back(lqn.parent[dst]);
3583 }
3584 }
3585 std::sort(out.begin(), out.end());
3586 out.erase(std::unique(out.begin(), out.end()), out.end());
3587 return out;
3588 }
3589
3590 bool is_branch_point(std::size_t srcX, std::size_t entryA, std::size_t srcY,
3591 std::size_t entryB) const {
3592 const std::size_t taskA = lqn.parent[entryA], taskB = lqn.parent[entryB];
3593 const std::size_t taskX = lqn.parent[srcX];
3594 if (taskX == taskA && taskX == taskB) return false;
3595 if (srcX == entryA || srcY == entryB) return true;
3596 const std::vector<std::size_t> dx = call_dst_tasks(srcX, entryA - lqn.eshift);
3597 const std::vector<std::size_t> dy = call_dst_tasks(srcY, entryB - lqn.eshift);
3598 for (std::size_t a : dx)
3599 for (std::size_t b : dy)
3600 if (a != b) return true;
3601 return false;
3602 }
3603
3604 void trace_to_server(std::size_t eidx, std::size_t serverIdx, std::vector<bool>& visited,
3605 std::vector<std::size_t>& itasks, bool isHead) const {
3606 if (eidx <= lqn.eshift || eidx > lqn.eshift + lqn.nentries) return;
3607 const std::size_t e = eidx - lqn.eshift;
3608 if (visited[e]) return;
3609 const std::size_t owner = lqn.parent[eidx];
3610 if (owner == serverIdx) return;
3611 if (serverIdx <= lqn.nhosts && lqn.parent[owner] == serverIdx) return;
3612 visited[e] = true;
3613 bool found = false;
3614 const std::vector<std::size_t> sen = server_entry_nums(serverIdx);
3615 for (std::size_t aidx : lqn.actsof[eidx]) {
3616 if (aidx <= lqn.ashift || aidx > lqn.ashift + lqn.nacts) continue;
3617 for (std::size_t cidx : lqn.callsof[aidx]) {
3618 if (lqn.calltype[cidx] != CallType::SYNC) continue;
3619 const std::size_t dst = lqn.callpair_dst[cidx];
3620 const std::size_t dtask = lqn.parent[dst];
3621 bool reaches = dtask == serverIdx ||
3622 (serverIdx <= lqn.nhosts && lqn.parent[dtask] == serverIdx);
3623 if (!reaches) {
3624 const std::size_t de = dst - lqn.eshift;
3625 for (std::size_t sn : sen)
3626 if (il_all(de, sn) > Tzero()) reaches = true;
3627 }
3628 if (reaches) {
3629 trace_to_server(dst, serverIdx, visited, itasks, false);
3630 found = true;
3631 }
3632 }
3633 }
3634 if (found && !isHead) {
3635 itasks.push_back(owner);
3636 std::sort(itasks.begin(), itasks.end());
3637 itasks.erase(std::unique(itasks.begin(), itasks.end()), itasks.end());
3638 }
3639 visited[e] = false;
3640 }
3641
3642 void interlock_for_server(std::size_t serverIdx) {
3643 const std::vector<std::size_t> sen = server_entry_nums(serverIdx);
3644 if (sen.empty()) return;
3645 const std::vector<std::size_t> cts = client_tasks(serverIdx);
3646 if (cts.empty()) return;
3647
3648 std::vector<std::pair<std::size_t, std::size_t>> pairs; // (task, entry number)
3649 for (std::size_t ct : cts)
3650 for (std::size_t ce : lqn.entriesof[ct]) {
3651 const std::size_t cen = ce - lqn.eshift;
3652 if (cen < 1 || cen > lqn.nentries) continue;
3653 for (std::size_t se : sen)
3654 if (il_all(cen, se) > Tzero()) {
3655 pairs.emplace_back(ct, cen);
3656 break;
3657 }
3658 }
3659 if (pairs.size() < 2) return;
3660
3661 // A client entry that reaches the server only from its own phase 2 has REPLIED by then, so a
3662 // parent above it runs concurrently with that request and can meet it at the server: that branch
3663 // is not interlocked. The tracer in init_interlock cuts a nested phase 2 for the same reason.
3664 std::vector<bool> reach_ph1(lqn.nentries + 1, false);
3665 for (const auto& p : pairs)
3666 for (std::size_t se : sen)
3667 if (il_ph1(p.second, se) > Tzero()) {
3668 reach_ph1[p.second] = true;
3669 break;
3670 }
3671
3672 std::vector<std::size_t> common;
3673 for (std::size_t i = 0; i < pairs.size(); ++i)
3674 for (std::size_t j = i + 1; j < pairs.size(); ++j) {
3675 if (pairs[i].first == pairs[j].first) continue;
3676 const std::size_t eA = pairs[i].second, eC = pairs[j].second;
3677 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
3678 const std::size_t tidx = lqn.tshift + t;
3679 for (std::size_t ex : lqn.entriesof[tidx])
3680 for (std::size_t ey : lqn.entriesof[tidx]) {
3681 const std::size_t xn = ex - lqn.eshift, yn = ey - lqn.eshift;
3682 if (xn < 1 || yn < 1 || xn > lqn.nentries || yn > lqn.nentries) continue;
3683 if (il_all(xn, eA) > Tzero() && il_all(yn, eC) > Tzero() &&
3684 (reach_ph1[eA] || xn == eA) && (reach_ph1[eC] || yn == eC) &&
3685 is_branch_point(ex, eA + lqn.eshift, ey, eC + lqn.eshift))
3686 common.push_back(ex);
3687 }
3688 }
3689 }
3690 std::sort(common.begin(), common.end());
3691 common.erase(std::unique(common.begin(), common.end()), common.end());
3692 if (common.empty()) return;
3693
3694 std::vector<std::size_t> interlocked;
3695 for (std::size_t ce : common) {
3696 std::vector<bool> visited(lqn.nentries + 1, false);
3697 std::vector<std::size_t> it;
3698 trace_to_server(ce, serverIdx, visited, it, true);
3699 for (std::size_t x : it) interlocked.push_back(x);
3700 }
3701 std::sort(interlocked.begin(), interlocked.end());
3702 interlocked.erase(std::unique(interlocked.begin(), interlocked.end()), interlocked.end());
3703
3704 std::vector<std::size_t> src_all;
3705 for (std::size_t ce : common) src_all.push_back(lqn.parent[ce]);
3706 std::sort(src_all.begin(), src_all.end());
3707 src_all.erase(std::unique(src_all.begin(), src_all.end()), src_all.end());
3708 {
3709 std::vector<std::size_t> diff;
3710 for (std::size_t x : src_all)
3711 if (!std::binary_search(interlocked.begin(), interlocked.end(), x)) diff.push_back(x);
3712 src_all = diff;
3713 }
3714
3715 // Left empty on purpose, and NOT because phase 2 is unsupported: the
3716 // reference computes ph2SrcTasks (initInterlock.m:230-262) and never
3717 // reads it, and its phase-2 source count is behind `if false`
3718 // (initInterlock.m:288-304). Filling it would change no number.
3719 std::vector<std::size_t> src_ph2;
3720 for (std::size_t it : interlocked)
3721 for (std::size_t ie : lqn.entriesof[it])
3722 for (std::size_t ci : lqn.iscaller.col(ie))
3723 if (ci > lqn.tshift && ci <= NT() &&
3724 !std::binary_search(interlocked.begin(), interlocked.end(), ci))
3725 src_all.push_back(ci);
3726 std::sort(src_all.begin(), src_all.end());
3727 src_all.erase(std::unique(src_all.begin(), src_all.end()), src_all.end());
3728
3729 double nsrc = 0.0;
3730 for (std::size_t st : src_all) nsrc += lqn.mult[st];
3731
3732 il_common_entries[serverIdx] = common;
3733 il_src_all[serverIdx] = src_all;
3734 il_src_ph2[serverIdx] = src_ph2;
3735 il_num_sources[serverIdx] = nsrc;
3736 }
3737
3738 // -----------------------------------------------------------------------
3739 // init and the outer iteration
3740 // -----------------------------------------------------------------------
3741 void init() {
3742 const std::size_t N = lqn.nidx;
3743 tput.assign(N + 1, Tzero());
3744 util.assign(N + 1, Tzero());
3745 servt.assign(N + 1, Tzero());
3746 residt.assign(N + 1, Tzero());
3747 thinkt.assign(N + 1, Tzero());
3748 callservt.assign(lqn.ncalls + 1, Tzero());
3749 callresidt.assign(lqn.ncalls + 1, Tzero());
3750 tputproc.assign(N + 1, Distrib<T>::disabled_dist());
3751 servt_ph1.assign(N + 1, Tzero());
3752 servt_ph2.assign(N + 1, Tzero());
3753 prOvertake.assign(lqn.nentries + 1, Tzero());
3754 build_entry_service_matrix();
3755
3756 relax_omega = (opt.relax == "fixed" || opt.relax == "adaptive") ? opt.relax_factor : 1.0;
3757
3758 servt_prev.assign(N + 1, std::numeric_limits<double>::quiet_NaN());
3759 residt_prev.assign(N + 1, std::numeric_limits<double>::quiet_NaN());
3760 tput_prev.assign(N + 1, std::numeric_limits<double>::quiet_NaN());
3761 thinkt_prev.assign(N + 1, std::numeric_limits<double>::quiet_NaN());
3762 callservt_prev.assign(lqn.ncalls + 1, std::numeric_limits<double>::quiet_NaN());
3763 callresidt_prev.assign(lqn.ncalls + 1, std::numeric_limits<double>::quiet_NaN());
3764 servt_prev_v.assign(N + 1, Tzero());
3765 residt_prev_v.assign(N + 1, Tzero());
3766 tput_prev_v.assign(N + 1, Tzero());
3767 thinkt_prev_v.assign(N + 1, Tzero());
3768 callservt_prev_v.assign(lqn.ncalls + 1, Tzero());
3769
3770 unique_route_idx.clear();
3771 for (const RouteRow& r : route_map) unique_route_idx.push_back(r.idx);
3772 std::sort(unique_route_idx.begin(), unique_route_idx.end());
3773 unique_route_idx.erase(std::unique(unique_route_idx.begin(), unique_route_idx.end()),
3774 unique_route_idx.end());
3775
3776 // only 'ilrate' reads the interlock tables, and their static analysis is costly
3777 if (interlockMethod == "ilrate") init_interlock();
3778
3779 maxitererr.assign(opt.iter_max + 2, 0.0);
3780 results_raw_last.clear();
3781 results_raw_valid = false;
3782 averagingstart = -1;
3783 hasconverged = false;
3784 moment_pass_done = false;
3785 results.clear();
3786
3787 servtcdf.assign(N + 1, fluid::FluidPassage());
3788 callservtcdf.assign(lqn.ncalls + 1, fluid::FluidPassage());
3789 entrycdfrespt.assign(lqn.nentries + 1, LnCdf());
3790 entryproc.assign(lqn.nentries + 1, mam::AphPair<T>());
3791 cdf_repo.assign(ensemble.size(), std::vector<std::vector<fluid::FluidPassage>>());
3792
3793 // warmstart replaces the starting point, and comes LAST so it wins over
3794 // everything above that writes the iterate
3795 warmstarted_ = false;
3796 if (opt.warmstart == "nlp") {
3797 nlp_warm_start();
3798 } else if (opt.warmstart == "mol") {
3799 mol_warm_start();
3800 }
3801 }
3802
3803 /**
3804 * The Picard iteration, with the stochastic controller in place of the
3805 * deterministic test when the layer engine is a simulator.
3806 *
3807 * The two tests are NOT interchangeable and cannot both run: `converged`
3808 * folds a moving average into `results` in place, which is exactly what the
3809 * Polyak-Ruppert average would then be taken of a second time. Whichever
3810 * test is in force therefore owns the results.
3811 */
3812 void iterate() {
3813 init();
3814 const bool stoch = layer_solver_is_stochastic();
3815 if (stoch) {
3816 LnStochConfig cfg;
3817 cfg.iter_tol = opt.iter_tol;
3818 cfg.relax_burnin = relax_omega;
3819 stoch_ctl = std::make_shared<LnStochController<T>>(cfg);
3820 }
3821 int it = 0;
3822 // converged(0), converged(1), ..., converged(n), as iterate.m calls it: the last
3823 // iterate is tested (and averaged) even when the loop stops on iter_max
3824 bool done = !stoch && converged(it);
3825 while (!done && it < opt.iter_max) {
3826 if (results_raw_valid) {
3827 results.back() = results_raw_last;
3828 results_raw_valid = false;
3829 }
3830 ++it;
3831 results.emplace_back(ensemble.size());
3832 for (std::size_t e = 0; e < ensemble.size(); ++e) analyze(it, e);
3833 post(it);
3834 if (stoch) {
3835 std::vector<double> jobs(ensemble.size(), 0.0);
3836 for (std::size_t e = 0; e < ensemble.size(); ++e)
3837 jobs[e] = ensemble[e].total_jobs();
3838 const bool stop = stoch_ctl->update(it, results.back(), jobs, servt, residt);
3839 // The step this sets is the one the NEXT updateMetrics applies,
3840 // which is why it is read back here and not before the sweep.
3841 relax_omega = stoch_ctl->relax_omega();
3842 if (stop) {
3843 did_converge = true;
3844 hasconverged = true;
3845 break;
3846 }
3847 } else {
3848 done = converged(it);
3849 }
3850 }
3851 iterations_done = it;
3852 // finish(): in Robbins-Monro mode report the averaged iterate, not the
3853 // last (noisy) one.
3854 if (stoch && stoch_ctl && stoch_ctl->averaging_count() > 0) {
3855 const std::vector<LayerResult<T>>& avg = stoch_ctl->averaged_results();
3856 for (std::size_t e = 0; e < results.back().size() && e < avg.size(); ++e)
3857 results.back()[e] = avg[e];
3858 servt = stoch_ctl->averaged_servt();
3859 residt = stoch_ctl->averaged_residt();
3860 }
3861 }
3862
3863 /**
3864 * Port of the filterMetric helper inside @@NetworkSolver/getAvg.m.
3865 *
3866 * This is not cosmetic post-processing: it is where a raw solver matrix
3867 * becomes the result the rest of LINE consumes, and three of its rules
3868 * change numbers that SolverLN then iterates on.
3869 *
3870 * 1. a (station, class) pair the class never visits is zeroed, whether
3871 * because its service is disabled or because its visit ratio is zero;
3872 * 2. anything below FineTol is snapped to zero;
3873 * 3. the caller's zeroMask is applied -- and for the queue length and the
3874 * utilization that mask is `RN < 10*FineTol`, which zeroes both
3875 * wherever the response time is immediate.
3876 *
3877 * Rule 3 is the one that matters most here. Every LQN entry, task and call
3878 * class is served by an Immediate distribution somewhere, whose response
3879 * time is 1e-8, so without it a layer reports the Immediate classes' share
3880 * of the population as real queue length and the LN think-time update reads
3881 * a task utilization that MATLAB reports as zero.
3882 */
3883 Matrix<T> filter_metric(const qn::Layer<T>& L, const Matrix<T>& metric,
3884 const std::vector<std::vector<bool>>* zero_mask) const {
3885 const std::size_t M = L.nstations, K = L.nclasses;
3886 Matrix<T> out(M, K, Tzero());
3887 for (std::size_t i = 0; i < M; ++i)
3888 for (std::size_t k = 0; k < K; ++k)
3889 if (!L.disabled[i][k]) out(i, k) = metric(i, k);
3890 if (zero_mask)
3891 for (std::size_t i = 0; i < M; ++i)
3892 for (std::size_t k = 0; k < K; ++k)
3893 if ((*zero_mask)[i][k]) out(i, k) = Tzero();
3894 for (std::size_t i = 0; i < M; ++i)
3895 for (std::size_t k = 0; k < K; ++k)
3896 if (dbl(out(i, k)) < GlobalConstants::FineTol) out(i, k) = Tzero();
3897 for (std::size_t k = 0; k < K; ++k) {
3898 std::size_t c = L.nchains;
3899 for (std::size_t cc = 0; cc < L.nchains; ++cc)
3900 if (L.chains[cc][k]) c = cc;
3901 if (c == L.nchains) continue;
3902 for (std::size_t i = 0; i < M; ++i)
3903 if (L.visits[c](L.stateful_of_station(i + 1) - 1, k) == Tzero())
3904 out(i, k) = Tzero();
3905 }
3906 return out;
3907 }
3908
3909 /**
3910 * Solve one layer with CTMC and report it in the shape the MVA path returns:
3911 * the reference's `LN(model, @(m) CTMC(m))`, and the engine a layer carrying
3912 * a Region is forced onto whatever the layer solver is.
3913 *
3914 * `qn::Layer<T>` derives from NetworkStruct, so the layer goes to the
3915 * analyzer as it stands; the generator needs the routing matrix, which the
3916 * layer does not build until asked (see solve_layer_fluid). A layer is a
3917 * closed network of the LQN's own small populations, so its state space is
3918 * finite without a cutoff; how large it grows is this engine's real limit.
3919 * A fork layer is refused, as under NC and SSA.
3920 */
3921 mva::MvaSolution<T> solve_layer_ctmc(std::size_t e) {
3922 refuse_fork_layer(e);
3923 qn::Layer<T>& L = ensemble[e];
3924 L.refresh_rt();
3925 // The capacities the WAITQ generator reads to bound each station's
3926 // marginal are filled by solve_layer, for every engine; the routing
3927 // matrix is not, because only this path and the fluid one need it.
3928 const ctmc::CtmcOptions& co = opt.layer_ctmc;
3929 // _run_any, not _run: the plain generator implements DROP only, and a
3930 // region built from an LQN admission constraint carries the WAITQ
3931 // default, whose per-region token FIFO lives in the waitq generator.
3932 const mva::AvgResult<T> a = ctmc::solver_ctmc_run_analyzer_any(L, co);
3933 mva::MvaSolution<T> out;
3934 out.method = "ctmc";
3935 out.iter = 1;
3936 out.Q = a.QN;
3937 out.U = a.UN;
3938 out.R = a.RN;
3939 out.Tp = a.TN;
3940 out.C = a.CN;
3941 out.X = a.XN;
3942 return out;
3943 }
3944
3945 /**
3946 * Solve one layer with SolverMAM, the reference's `LN(model, @(m) MAM(m))`.
3947 *
3948 * The layer goes through the full `mam_dispatch` ladder under
3949 * `opt.layer_mam`, whose method is SolverMAM's own `default` unless the
3950 * caller set one, exactly as the factory builds a default SolverMAM. On a
3951 * closed layer that resolves to the `dec.source` decomposition; MAM is a
3952 * deterministic engine, so the ordinary convergence test stays in force. A
3953 * fork layer is refused, as under NC and SSA.
3954 */
3955 mva::MvaSolution<T> solve_layer_mam(std::size_t e) {
3956 refuse_fork_layer(e);
3957 qn::Layer<T>& L = ensemble[e];
3958 L.refresh_rt();
3959 L.refresh_capacity();
3960 mva::MvaSolution<T> out = mam::mam_dispatch(L, opt.layer_mam).sol;
3961 out.method = "mam";
3962 return out;
3963 }
3964
3965 /** A fork layer's fixed point is driven by MVA in this port; any other engine refuses it by name. */
3966 void refuse_fork_layer(std::size_t e) const {
3967 if (fj_tr[e].active())
3968 throw UnsupportedError(ErrorKind{"Fork"},
3969 "SolverLN: layer '" + ensemble[e].name +
3970 "' carries a fork, whose fixed point is driven by MVA in this port; solve this "
3971 "model with layer_solver 'mva'");
3972 }
3973
3974 /**
3975 * Solve one layer with SolverNC, the reference's `LN(model, @(m) NC(m))`.
3976 *
3977 * This is the layer engine the Java CLI's bare `ln` token selects, against
3978 * `ln.mva` for the MVA one. NC evaluates the normalizing constant rather
3979 * than the MVA recursion, so on a product-form layer it is the SAME answer
3980 * reached exactly instead of through the AMVA approximation -- and on a
3981 * layer that is not product form the two differ, which is the whole reason
3982 * the token exists.
3983 */
3984 mva::MvaSolution<T> solve_layer_nc(std::size_t e) {
3985 if (fj_tr[e].active())
3986 throw UnsupportedError(ErrorKind{"Fork"},
3987 "SolverLN: layer '" + ensemble[e].name +
3988 "' carries a fork, whose fixed point is driven by MVA in this port; solve this "
3989 "model with layer_solver 'mva'");
3990 qn::Layer<T>& L = ensemble[e];
3991 L.refresh_rt();
3992 // `solver_nc_solve`, NOT `nc_dispatch`: the latter is the INNER network
3993 // solve, the one the caching-queueing decomposition itself calls as its
3994 // `netfun`, and it carries no cache branch at all. The three cache gates
3995 // live in `solver_nc_solve` (solver_nc_runner.h), which is the true
3996 // counterpart of `mva_dispatch` on the branch below. Routed here, a
3997 // cache layer never reached `solver_nc_cacheqn_analyzer` and its Cache
3998 // node was solved as ordinary routing, so the hit/miss split came back
3999 // as the probability `link()` offered -- an even 1/2, independent of
4000 // capacity, item count and replacement strategy, with no warning.
4001 // `.sol` alone is taken, exactly as the MVA branch takes it: the layer
4002 // reads the split off its own station throughputs, and the auxiliary
4003 // `refreshed_struct` both dispatchers also return is unused on either.
4004 mva::MvaSolution<T> out = nc::solver_nc_solve(L, opt.layer_nc).sol;
4005 out.method = "nc";
4006 return out;
4007 }
4008
4009 /**
4010 * Solve one layer by simulation, the reference's `LN(model, @(m) SSA(m))`.
4011 *
4012 * THE LAYER RESULTS ARE THEN NOISY, and the deterministic convergence test
4013 * cannot terminate against noise: its successive-difference error is
4014 * bounded below by the standard error of the estimates. `iterate` therefore
4015 * switches to `LnStochController` whenever this engine is selected, so the
4016 * relaxation decays as Robbins-Monro and the reported iterate is the
4017 * Polyak-Ruppert average. Selecting this engine and keeping the
4018 * deterministic test would simply run to iter_max.
4019 */
4020 mva::MvaSolution<T> solve_layer_ssa(std::size_t e) {
4021 if (fj_tr[e].active())
4022 throw UnsupportedError(ErrorKind{"Fork"},
4023 "SolverLN: layer '" + ensemble[e].name +
4024 "' carries a fork, whose fixed point is driven by MVA in this port; solve this "
4025 "model with layer_solver 'mva'");
4026 qn::Layer<T>& L = ensemble[e];
4027 L.refresh_rt();
4028 const ssa::SsaSolution s = ssa::solver_ssa(L, opt.layer_ssa);
4029 mva::MvaSolution<T> out;
4030 out.method = "ssa";
4031 out.iter = 1;
4032 out.Q = Matrix<T>(L.nstations, L.nclasses, Tzero());
4033 out.U = Matrix<T>(L.nstations, L.nclasses, Tzero());
4034 out.R = Matrix<T>(L.nstations, L.nclasses, Tzero());
4035 out.Tp = Matrix<T>(L.nstations, L.nclasses, Tzero());
4036 for (std::size_t i = 0; i < L.nstations; ++i)
4037 for (std::size_t r = 0; r < L.nclasses; ++r) {
4038 out.Q(i, r) = num_traits<T>::from_double(s.QN(i, r));
4039 out.U(i, r) = num_traits<T>::from_double(s.UN(i, r));
4040 out.R(i, r) = num_traits<T>::from_double(s.RN(i, r));
4041 out.Tp(i, r) = num_traits<T>::from_double(s.TN(i, r));
4042 }
4043 out.C.assign(L.nclasses, Tzero());
4044 out.X.assign(L.nclasses, Tzero());
4045 for (std::size_t r = 0; r < L.nclasses && r < s.XN.size(); ++r) {
4046 out.X[r] = num_traits<T>::from_double(s.XN[r]);
4047 out.C[r] = num_traits<T>::from_double(s.CN[r]);
4048 }
4049 return out;
4050 }
4051
4052 /**
4053 * Solve one layer with the native LDES engine, the reference's `LN(model, @(m) LDES(m))`.
4054 *
4055 * IN PROCESS, not through `solver_ldes`'s subprocess client: the layer is a
4056 * NetworkStruct this port built itself, so there is no model.json whose
4057 * bytes would have to be forwarded losslessly, and one process per layer per
4058 * iteration would dominate the run. The engine is the one `common/ldes`
4059 * runs (ldes_engine.h). Noisy like `ssa`, so `iterate` runs the stochastic
4060 * controller; each solve gets the Robbins-Monro seed of SolverLN.pre. A
4061 * fork layer is refused, as under NC and SSA.
4062 */
4063 mva::MvaSolution<T> solve_layer_ldes(std::size_t e) {
4064 refuse_fork_layer(e);
4065 qn::Layer<T>& L = ensemble[e];
4066 // The engine samples in double precision (its fork and clock state are
4067 // double), so a higher arithmetic has nothing to carry: refuse it.
4068 if constexpr (!std::is_same<T, double>::value) {
4069 throw UnsupportedError(ErrorKind{"Arithmetic"},
4070 "SolverLN: the ldes layer engine simulates in double precision; "
4071 "rerun with --arith double");
4072 } else {
4073 L.refresh_rt();
4074 ldes::LdesOptions lo = opt.layer_ldes;
4075 lo.seed = layer_seed(opt.layer_ldes.seed, e);
4076 const ldes::LdesResult s = ldes::ldes_engine_solve(L, lo);
4077 mva::MvaSolution<T> out = sim_layer_solution(L, s.QN, s.UN, s.RN, s.TN, "ldes");
4078 for (std::size_t r = 0; r < L.nclasses && r < s.XN.cols(); ++r) {
4079 out.X[r] = s.XN(0, r);
4080 out.C[r] = s.CN(0, r);
4081 }
4082 return out;
4083 }
4084 }
4085
4086 /**
4087 * Solve one layer with JSIM, the reference's `LN(model, @(m) JMT(m))`.
4088 *
4089 * One JMT subprocess per layer per iteration, exactly as the reference's
4090 * factory runs one. A layer carrying immediate feedback is refused up front
4091 * with the reference's own sentence (assertLayerSolverSupportsModel):
4092 * JSIM re-queues a job the layer means to keep in service. Seeded and
4093 * driven as `ldes` is; a fork layer is refused, as under NC and SSA.
4094 */
4095 mva::MvaSolution<T> solve_layer_jmt(std::size_t e) {
4096 refuse_fork_layer(e);
4097 qn::Layer<T>& L = ensemble[e];
4098 for (std::size_t i = 0; i < L.immfeed.size(); ++i)
4099 for (std::size_t r = 0; r < L.immfeed[i].size(); ++r)
4100 if (L.immfeed[i][r])
4101 throw UnsupportedError(ErrorKind{"Structure"},
4102 "SolverJMT cannot solve LN layer " + std::to_string(e + 1) +
4103 ": the layer carries immediate feedback (sn.immfeed), which SolverJMT does "
4104 "not support, so LN would fail at the first iteration. Use the default layer "
4105 "factory (MVA/NC) or another layer solver that supports immediate feedback.");
4106 L.refresh_rt();
4107 jmt::JmtOptions jo = opt.layer_jmt;
4108 jo.seed = layer_seed(opt.layer_jmt.seed, e);
4109 jo.verbose = false;
4110 const jmt::JmtResult<T> s = jmt::solver_jmt_run_analyzer(L, jo);
4111 Matrix<double> Q(L.nstations, L.nclasses, 0.0), U = Q, R = Q, X = Q;
4112 for (std::size_t i = 0; i < L.nstations; ++i)
4113 for (std::size_t r = 0; r < L.nclasses; ++r) {
4114 Q(i, r) = num_traits<T>::to_double(s.avg.QN(i, r));
4115 U(i, r) = num_traits<T>::to_double(s.avg.UN(i, r));
4116 R(i, r) = num_traits<T>::to_double(s.avg.RN(i, r));
4117 X(i, r) = num_traits<T>::to_double(s.avg.TN(i, r));
4118 }
4119 mva::MvaSolution<T> out = sim_layer_solution(L, Q, U, R, X, "jmt");
4120 for (std::size_t r = 0; r < L.nclasses; ++r) {
4121 if (r < s.avg.XN.size()) out.X[r] = s.avg.XN[r];
4122 if (r < s.avg.CN.size()) out.C[r] = s.avg.CN[r];
4123 }
4124 return out;
4125 }
4126
4127 /** A simulator's station tables (the first nstations rows) in the shape the MVA path returns. */
4128 static mva::MvaSolution<T> sim_layer_solution(const qn::Layer<T>& L, const Matrix<double>& Q,
4129 const Matrix<double>& U, const Matrix<double>& R,
4130 const Matrix<double>& X, const char* method) {
4131 mva::MvaSolution<T> out;
4132 out.method = method;
4133 out.iter = 1;
4134 out.Q = Matrix<T>(L.nstations, L.nclasses, Tzero());
4135 out.U = Matrix<T>(L.nstations, L.nclasses, Tzero());
4136 out.R = Matrix<T>(L.nstations, L.nclasses, Tzero());
4137 out.Tp = Matrix<T>(L.nstations, L.nclasses, Tzero());
4138 for (std::size_t i = 0; i < L.nstations; ++i)
4139 for (std::size_t r = 0; r < L.nclasses; ++r) {
4140 out.Q(i, r) = num_traits<T>::from_double(Q(i, r));
4141 out.U(i, r) = num_traits<T>::from_double(U(i, r));
4142 out.R(i, r) = num_traits<T>::from_double(R(i, r));
4143 out.Tp(i, r) = num_traits<T>::from_double(X(i, r));
4144 }
4145 out.C.assign(L.nclasses, Tzero());
4146 out.X.assign(L.nclasses, Tzero());
4147 return out;
4148 }
4149
4150 /**
4151 * Run one layer through the fluid analyzer instead of MVA.
4152 *
4153 * WHAT MAKES THIS SOUND AT ALL. The layer handed to a solver is an ordinary
4154 * closed queueing network -- the layering has already replaced every call
4155 * by a class with a service demand -- so any NetworkSolver can solve it,
4156 * and the reference says exactly that by taking a solver factory. The outer
4157 * Picard iteration only ever reads [QN, UN, RN, TN] back, so the fluid
4158 * result maps onto the same shape MVA returns.
4159 *
4160 * WHAT CHANGES, and it is not nothing: the fluid limit is exact only as the
4161 * populations grow, so on a layer of a few jobs it is a genuinely different
4162 * approximation, not a slower route to the same fixed point. It also feeds
4163 * DIFFERENT service demands back into the next outer iteration, so the two
4164 * ensembles converge to different fixed points rather than to the same one
4165 * by different paths.
4166 *
4167 * REFUSED BY NAME: a non-double backend (LSODA is double-only, see
4168 * solver_fluid.h) and a fork layer (`fj_fixed_point` supplies MVA as its
4169 * inner solve, and the auxiliary near-zero-rate open classes it introduces
4170 * are not something the fluid drift represents).
4171 */
4172 mva::MvaSolution<T> solve_layer_fluid(std::size_t e) {
4173 if (fj_tr[e].active())
4174 throw UnsupportedError(ErrorKind{"Fork"},
4175 "SolverLN: layer '" + std::to_string(e) +
4176 "' carries a fork, whose fixed point is driven by MVA in this port; solve this "
4177 "model with layer_solver 'mva'");
4178 qn::Layer<T>& L = ensemble[e];
4179 // THE LAYER HAS NO ROUTING MATRIX UNTIL IT IS ASKED FOR ONE. `buildLayers`
4180 // records the wiring with set_route and then calls refresh_chains, which
4181 // reads P directly and computes the VISITS; MVA needs nothing else, so
4182 // `rt` stays empty. Both fluid methods route through `sn.rt` instead, and
4183 // an empty one is not an error anywhere -- it is read as "no transition
4184 // exists", the drift decays to zero and every metric comes back 0. It is
4185 // rebuilt on every solve because update_routing_probabilities can change
4186 // the wiring between outer iterations.
4187 L.refresh_rt();
4188 mva::MvaSolution<T> out;
4189 out.method = "fluid";
4190 out.iter = 1;
4191 out.Q = Matrix<T>(L.nstations, L.nclasses, Tzero());
4192 out.U = Matrix<T>(L.nstations, L.nclasses, Tzero());
4193 out.R = Matrix<T>(L.nstations, L.nclasses, Tzero());
4194 out.Tp = Matrix<T>(L.nstations, L.nclasses, Tzero());
4195 out.C.assign(L.nclasses, Tzero());
4196 out.X.assign(L.nclasses, Tzero());
4197 detail::ln_fluid_solve(L, opt.layer_fluid, out);
4198 return out;
4199 }
4200
4201 /**
4202 * Solve one layer, running the fork-join fixed point when it has a fork.
4203 *
4204 * The driver itself is shared (fj_driver.h); the layer path supplies
4205 * solver_mva_analyzer as the inner solve. The auxiliary arrival rates
4206 * fj_lambda are warm-started across outer iterations, per the reference.
4207 *
4208 * `default` becomes `amva` on a fork layer, which is what mvaDispatch.m does
4209 * for any model whose BASE has a fork: the transformed layer is mixed with
4210 * auxiliary near-zero-rate open classes, and the exact mixed MVA the default
4211 * ladder would otherwise pick degenerates on them.
4212 */
4213 mva::MvaSolution<T> solve_layer(std::size_t e) {
4214 // BEFORE ANY ENGINE, AND FOR EVERY ONE OF THEM. `build_layer` stops at
4215 // refresh_chains, so a layer reaches this point with `cap` and
4216 // `classcap` still EMPTY, while the reference hands SolverMVA a struct
4217 // that refreshCapacity has already filled. Every consumer of Kendall's
4218 // K -- `buffer_size`, and through it `has_blocking` and the BCMP gate
4219 // `has_product_form` -- indexes those vectors by station, so an empty
4220 // one is an out-of-range read and not a permissive default. It is
4221 // recomputed on each solve because the layer's class populations move
4222 // between outer iterations, and the buffers are derived from them.
4223 ensemble[e].refresh_capacity();
4224 // A layer carrying an admission constraint goes to CTMC whatever the
4225 // layer solver is, as adaptiveSolverFactory does (SolverLN.m:1047-1089):
4226 // MVA and the fluid path both refuse a Region by name, so leaving the
4227 // layer on them would refuse a model the reference solves. The
4228 // reference then falls back to LDES and SSA; neither is reachable here
4229 // (there is no cpp LDES, and cpp SSA refuses regions), so CTMC is the
4230 // only path and its state-space bound is this port's real limit.
4231 // An explicit LDES or JMT factory keeps the layer: both declare Region,
4232 // so the reference's assertLayerSolverSupportsModel admits them as given.
4233 if (!ensemble[e].regions.empty() && opt.layer_solver != "ldes" && opt.layer_solver != "jmt")
4234 return solve_layer_ctmc(e);
4235 // A setup no longer forces the MAM decomposition on the layer
4236 // (SolverLN.m, 2026-08-11): the cold start is charged to the entry by
4237 // setup_charge(), not wired into the station, so the layer is an
4238 // ordinary one and the user's own layer solver serves it.
4239 // A cache layer is dispatched to the integrated caching-queueing
4240 // analyzer, which reads the NODE routing to find what lies downstream
4241 // of the cache. The layer carries none until asked, and it has to be
4242 // rebuilt every pass because entry selection can move it. EVERY layer
4243 // engine below must reach that analyzer, not just the MVA one: this
4244 // refresh runs for all of them, and a branch that then hands the
4245 // rebuilt routing to a solver with no cache gate reports the offered
4246 // hit/miss split instead of the converged one.
4247 if (has_cache_node(e)) ensemble[e].refresh_rt();
4248 if (opt.layer_solver == "fluid") return solve_layer_fluid(e);
4249 if (opt.layer_solver == "nc") return solve_layer_nc(e);
4250 if (opt.layer_solver == "ssa") return solve_layer_ssa(e);
4251 if (opt.layer_solver == "mam") return solve_layer_mam(e);
4252 if (opt.layer_solver == "ctmc") return solve_layer_ctmc(e);
4253 if (opt.layer_solver == "ldes") return solve_layer_ldes(e);
4254 if (opt.layer_solver == "jmt") return solve_layer_jmt(e);
4255 if (opt.layer_solver != "mva")
4256 throw UnsupportedError(ErrorKind{"Option"}, "SolverLN: layer_solver '" + opt.layer_solver +
4257 "' is not available; use 'mva', 'nc', 'fluid', 'ssa', 'mam', 'ctmc', "
4258 "'ldes' or 'jmt'");
4259 // Route each layer through the full mva_dispatch ladder, as MATLAB's
4260 // SolverLN does by handing every layer to SolverMVA.runAnalyzer: a plain
4261 // closed layer still lands on branch 13 (solver_mva_analyzer), but a
4262 // layer carrying load- or class-dependent scaling now reaches
4263 // solver_mvald_analyzer, matching the reference rather than silently
4264 // running the flat AMVA.
4265 mva::MvaOptions eopt = opt.layer;
4266 if (e < layer_interlock.size()) eopt.interlock = layer_interlock[e];
4267 if (!fj_tr[e].active())
4268 return mva::mva_dispatch(ensemble[e], eopt, layer_init_sol[e]).sol;
4269 mva::MvaOptions lopt = eopt;
4270 if (lopt.method == "default") lopt.method = "amva";
4271 return mva::fj_fixed_point(
4272 ensemble[e], fj_tr[e], fj_lambda[e], eopt,
4273 [&lopt](qn::NetworkStruct<T>& V) {
4274 return mva::mva_dispatch(V, lopt, Matrix<T>()).sol;
4275 });
4276 }
4277
4278 void analyze(int it, std::size_t e) {
4279 qn::Layer<T>& L = ensemble[e];
4280 cur_it = it;
4281 const mva::MvaSolution<T> s = solve_layer(e);
4282 LayerResult<T>& r = results[it - 1][e];
4283 r.RN = filter_metric(L, s.R, nullptr);
4284 std::vector<std::vector<bool>> zmask(L.nstations, std::vector<bool>(L.nclasses, false));
4285 for (std::size_t i = 0; i < L.nstations; ++i)
4286 for (std::size_t k = 0; k < L.nclasses; ++k)
4287 zmask[i][k] = dbl(r.RN(i, k)) < 10.0 * GlobalConstants::FineTol;
4288 r.QN = filter_metric(L, s.Q, &zmask);
4289 r.UN = filter_metric(L, s.U, &zmask);
4290 r.TN = filter_metric(L, s.Tp, nullptr);
4291 r.WN = residence_from_response(L, r.RN);
4292 // warm start the next solve of this layer from the chain-aggregated
4293 // queue lengths, as the reference does for SolverMVA layers
4294 // A fork layer is exempt: the reference guards this on the result having
4295 // the LAYER's station count, and the transformed model reports one more
4296 // row (its Source), so the hint is never installed there. A fluid layer
4297 // is exempt too, and for the reference's own reason: `analyze` guards
4298 // the warm start on `strcmp(self.solvers{e}.name, 'SolverMVA')`, because
4299 // init_sol means a queue-length vector to AMVA and a full ODE state
4300 // vector to the fluid solver -- the two are not interchangeable.
4301 if (!fj_tr[e].active() && opt.layer_solver == "mva") {
4302 Matrix<T> Qch(L.nstations, L.nchains, Tzero());
4303 for (std::size_t c = 0; c < L.nchains; ++c)
4304 for (std::size_t i = 0; i < L.nstations; ++i) {
4305 T s2 = Tzero();
4306 for (std::size_t k : L.inchain[c]) s2 += s.Q(i, k - 1);
4307 Qch(i, c) = s2;
4308 }
4309 layer_init_sol[e] = Qch;
4310 }
4311 }
4312
4313 /** Port of sn_get_residt_from_respt: response time scaled by the visit ratio. */
4314 Matrix<T> residence_from_response(const qn::Layer<T>& L, const Matrix<T>& RN) const {
4315 Matrix<T> V(L.nstations, L.nclasses, Tzero());
4316 for (std::size_t c = 0; c < L.nchains; ++c)
4317 for (std::size_t i = 0; i < L.nstations; ++i) {
4318 const std::size_t sf = L.stateful_of_station(i + 1) - 1;
4319 for (std::size_t k = 0; k < L.nclasses; ++k)
4320 V(i, k) = T(V(i, k) + L.visits[c](sf, k));
4321 }
4322 Matrix<T> WN(L.nstations, L.nclasses, Tzero());
4323 for (std::size_t i = 0; i < L.nstations; ++i)
4324 for (std::size_t k = 0; k < L.nclasses; ++k) {
4325 if (L.disabled[i][k]) continue;
4326 if (!(RN(i, k) > Tzero())) continue;
4327 if (dbl(RN(i, k)) < GlobalConstants::FineTol) {
4328 WN(i, k) = RN(i, k);
4329 continue;
4330 }
4331 std::size_t c = 0;
4332 for (std::size_t cc = 0; cc < L.nchains; ++cc)
4333 if (L.chains[cc][k]) c = cc;
4334 const std::size_t rstat = L.classes[k].refstat;
4335 T den = Tzero();
4336 if (L.refclass[c] > 0) {
4337 den = V(rstat - 1, L.refclass[c] - 1);
4338 } else {
4339 for (std::size_t kk : L.inchain[c]) den += V(rstat - 1, kk - 1);
4340 }
4341 if (den == Tzero()) continue;
4342 WN(i, k) = T(RN(i, k) * V(i, k) / den);
4343 }
4344 for (std::size_t i = 0; i < L.nstations; ++i)
4345 for (std::size_t k = 0; k < L.nclasses; ++k)
4346 if (dbl(WN(i, k)) < 10.0 * GlobalConstants::FineTol) WN(i, k) = Tzero();
4347 return WN;
4348 }
4349
4350 void post(int it) {
4351 update_metrics(it);
4352 // Under 'refpath' the merged client chain performs the interlocking
4353 // structurally, and the discount as well would deflate the same wait twice
4354 if (interlockMethod == "ilrate") update_populations(it);
4355 update_think_times(it);
4356 update_layers(it);
4357 update_routing_probabilities(it);
4358 for (std::size_t e : route_reset) {
4359 ensemble[e].refresh_chains();
4360 layer_init_sol[e] = Matrix<T>();
4361 }
4362 // moment3 needs no refreshProcesses here: this port keeps the service
4363 // DISTRIBUTION on the layer and refresh_rates re-reads it, so there is no
4364 // second representation to fall out of step, unlike sn.proc in MATLAB.
4365 for (std::size_t e : svc_reset) ensemble[e].refresh_rates();
4366 }
4367
4368 /** Port of converged.m: moving average of the layer results plus the test. */
4369 bool converged(int it) {
4370 const std::size_t E = ensemble.size();
4371 const int iter_min = std::max<int>(2 * int(E), int(std::ceil(opt.iter_max / 4.0)));
4372 const int wnd_size = std::max(5, int(std::ceil(iter_min / 5.0)));
4373
4374 if (it >= iter_min && int(results.size()) >= wnd_size) {
4375 results_raw_last = results.back();
4376 results_raw_valid = true;
4377 const T w = T(Tone() / num_traits<T>::from_int(wnd_size));
4378 for (std::size_t e = 0; e < E; ++e) {
4379 LayerResult<T>& cur = results[results.size() - 1][e];
4380 auto scale = [&](Matrix<T>& A) {
4381 for (std::size_t i = 0; i < A.rows(); ++i)
4382 for (std::size_t j = 0; j < A.cols(); ++j) A(i, j) = T(A(i, j) * w);
4383 };
4384 Matrix<T> Q = cur.QN, U = cur.UN, R = cur.RN, Tp = cur.TN, W = cur.WN;
4385 scale(Q); scale(U); scale(R); scale(Tp); scale(W);
4386 for (int k = 1; k < wnd_size; ++k) {
4387 const LayerResult<T>& old = results[results.size() - 1 - k][e];
4388 auto add = [&](Matrix<T>& A, const Matrix<T>& B) {
4389 for (std::size_t i = 0; i < A.rows(); ++i)
4390 for (std::size_t j = 0; j < A.cols(); ++j) A(i, j) = T(A(i, j) + B(i, j) * w);
4391 };
4392 add(Q, old.QN); add(U, old.UN); add(R, old.RN); add(Tp, old.TN); add(W, old.WN);
4393 }
4394 cur.QN = Q; cur.UN = U; cur.RN = R; cur.TN = Tp; cur.WN = W;
4395 }
4396 }
4397
4398 if (it > 1) {
4399 double err = 0.0;
4400 for (std::size_t e = 0; e < E; ++e) {
4401 const Matrix<T>& Q = results[results.size() - 1][e].QN;
4402 const Matrix<T>& Q1 = results[results.size() - 2][e].QN;
4403 const double Njobs = ensemble[e].total_jobs();
4404 if (!(Njobs > 0.0)) continue;
4405 double mx = 0.0;
4406 for (std::size_t i = 0; i < Q.rows(); ++i)
4407 for (std::size_t j = 0; j < Q.cols(); ++j)
4408 mx = std::max(mx, std::fabs(dbl(Q(i, j)) - dbl(Q1(i, j))));
4409 err += mx / Njobs;
4410 }
4411 maxitererr[it] = err;
4413 static_cast<long>(it),
4414 "layer iteration %zu: max queue-length change %.3e (tolerance %.3e)",
4415 static_cast<std::size_t>(it), err, opt.iter_tol);
4416 if (it == iter_min) {
4417 line::util::LineConsole::step("started averaging the iterates to aid convergence");
4418 averagingstart = it;
4419 }
4420 }
4421
4422 // ITER_MIN DOES NOT FLOOR THE STOP TEST OF A WARM-STARTED RUN: the floor keeps a cold iterate from
4423 // reading its early plateau as convergence, and a seeded one starts past it. It still sizes the
4424 // averaging window above, so both arms iterate the same map. Three computed errors are still needed.
4425 const int stop_floor = warmstarted_ ? 3 : iter_min;
4426 if (it > stop_floor && maxitererr[it] < opt.iter_tol && maxitererr[it - 1] < opt.iter_tol &&
4427 maxitererr[it - 2] < opt.iter_tol) {
4428 if (!hasconverged) {
4429 hasconverged = true;
4430 } else {
4431 did_converge = true;
4432 return true;
4433 }
4434 } else {
4435 hasconverged = false;
4436 }
4437 return false;
4438 }
4439
4440 // -----------------------------------------------------------------------
4441 // updateMetrics
4442 // -----------------------------------------------------------------------
4443 /** Port of updateMetrics.m: the method selects which update runs. */
4444 // -----------------------------------------------------------------------
4445 // Method "srvn.ph": the activity graph of an entry as a phase-type server law
4446 //
4447 // Each layer is a two-station cycle, a client Delay plus the server, with
4448 // one closed class per caller task. The sequencing the default method
4449 // encodes as routing -- a class per entry, per activity and per call, plus
4450 // Fork, Join, Router and ClassSwitch nodes -- is composed instead into a
4451 // single phase-type service law per (layer, caller), by the exact
4452 // series-parallel reduction of Workflow. The layer therefore carries only
4453 // the client/server back-and-forth, and the activity graph survives as a
4454 // distribution.
4455 //
4456 // Port of the MATLAB @SolverLN/buildLayersPH.m, updateLayersPH.m,
4457 // updateMetricsPH.m, updateThinkTimesPH.m, phComposeEntryLaws.m and
4458 // getEnsembleAvgPH.m, of the JAR jline.solvers.ln.SolverLNPH and of the
4459 // Python line_solver.solvers.solver_ln.solver_ln_ph. See
4460 // _kb/06-solver-catalog.md (LN section) for the layering taxonomy.
4461 // -----------------------------------------------------------------------
4462
4463 /**
4464 * The method the layers were actually BUILT for: "srvn.ph", "srvn.cs",
4465 * "flat.cs", "flat.ph" or "moment3". Resolved once in build_layers, because
4466 * the alias "srvn" may fall back; every dispatch reads this and not
4467 * opt.method, so a reconstruction can never disagree with the layers it is
4468 * reading.
4469 */
4470 std::string lnmethod;
4471 /** True once ph_init_laws has composed the per-entry workflows. */
4472 bool ph_laws_ready = false;
4473
4474 /**
4475 * Normalise a method name onto one the solver dispatches on. A method name
4476 * carries TWO decisions: the LAYERING, which fixes what a submodel is, and
4477 * the ENCODING, which fixes how an activity graph is written into it.
4478 *
4479 * "srvn.cs" encodes the activity graph as ROUTING, "srvn.ph" as a composed
4480 * phase-type server law, "srvn" is the alias that takes "srvn.ph" where it
4481 * can serve the model and "srvn.cs" otherwise, "flat.cs" squashes every
4482 * server into one submodel with the routing encoding, "flat.ph" squashes
4483 * them with the composed one ("flat" is the alias of "flat.cs" and resolves
4484 * unconditionally rather than probing "flat.ph", because a model is squashed
4485 * in order to express what only the routing encoding carries), and "moment3"
4486 * is the three-moment distribution pass over the routing layers. "default"
4487 * is the srvn alias; an unrecognised method name takes "srvn.cs".
4488 */
4489 static std::string ln_requested_method(const std::string& method) {
4490 std::string m;
4491 for (char c : method) m += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
4492 if (m.empty() || m == "srvn" || m == "default" || m == "auto") return "srvn";
4493 if (m == "srvn.ph" || m == "ph") return "srvn.ph";
4494 if (m == "srvn.cs" || m == "srvncs" || m == "cs") return "srvn.cs";
4495 if (m == "flat.cs" || m == "flatcs" || m == "flat" || m == "squashed") return "flat.cs";
4496 if (m == "flat.ph" || m == "flatph" || m == "squashed.ph") return "flat.ph";
4497 if (m == "moment3") return "moment3";
4498 // 'nlp' names NO layering: the whole model is one program over the QD-AMVA laws
4499 if (m == "nlp" || m == "qdamva" || m == "qd-amva") return "nlp";
4500 // An unrecognised token takes the routing encoding, which is what every
4501 // name other than "moment3" resolved to before the alias existed.
4502 return "srvn.cs";
4503 }
4504
4505 /**
4506 * Refuse a warm start the composed phase-type layers cannot take: both seeds
4507 * solve for means, so there is no phase distribution in either to start them from.
4508 */
4509 void assert_warmstart_encoding() const {
4510 if (!is_ph_encoding() || opt.warmstart.empty()) return;
4511 throw InputError("config['warmstart']='" + opt.warmstart + "' cannot seed method='" + lnmethod +
4512 "': its layers carry a COMPOSED PHASE-TYPE service law and the seed solves for "
4513 "means, so there is no phase distribution to start it from. Use method='srvn.cs' "
4514 "for the same layering under the routing encoding, which those means do "
4515 "parameterize.");
4516 }
4517
4518 /**
4519 * config.warmstart and the nlp keys, validated before anything is built.
4520 * The program and lqn_mol are double precision by construction, so another
4521 * arithmetic is refused rather than handed a double answer under its name.
4522 */
4523 void validate_nlp_options() const {
4524 const bool nlp = ln_requested_method(opt.method) == "nlp";
4525 if (!opt.warmstart.empty()) {
4526 if (opt.warmstart != "nlp" && opt.warmstart != "mol")
4527 throw InputError("config['warmstart'] takes 'nlp', 'mol' or None, got '" + opt.warmstart +
4528 "'. 'nlp' seeds the layered fixed point with the QD-AMVA program's solution "
4529 "and 'mol' with the Method of Layers'; None starts it from the model's own "
4530 "host demands.");
4531 if (nlp)
4532 throw InputError("config['warmstart']='" + opt.warmstart +
4533 "' is a starting point for a LAYERED method, and method='nlp' runs no "
4534 "iteration to start. Drop one of the two.");
4535 }
4536 if ((nlp || !opt.warmstart.empty()) && !std::is_same<T, double>::value)
4537 throw UnsupportedError(ErrorKind{"Arithmetic"},
4538 std::string(nlp ? "SolverLN: method='nlp'" : "SolverLN: config.warmstart") +
4539 " roots a double-precision program, so it has no exact answer to give; rerun with "
4540 "--arith double");
4541 }
4542
4543 /** The double value of v as T. */
4544 static T fromd(double v) { return num_traits<T>::from_double(v); }
4545
4546 /** The program's root under the solver's nlp options, and the program itself; python ns['solve']. */
4547 std::vector<double> nlp_root(::line::lqn::NlpProgram& P) const {
4548 P = nlp_program();
4549 std::vector<double> x = P.solve(opt.nlp_warm, opt.nlp_maxiter).x;
4550 for (double& v : x) v = std::max(v, 0.0);
4551 return x;
4552 }
4553
4554 /** The share of its host an activity holds: busy servers at a delay, a busy fraction otherwise. */
4555 static std::vector<double> nlp_act_util(const ::line::lqn::NlpProgram& P, const std::vector<double>& Xa) {
4556 std::vector<double> u(P.NA, 0.0);
4557 for (std::size_t a = 0; a < P.NA; ++a) {
4558 const std::size_t h = P.task_host[P.act_task[a]];
4559 const double busy = P.act_demand[a] * Xa[a];
4560 u[a] = P.host_delay[h] ? busy : busy / P.host_maxmult[h];
4561 }
4562 return u;
4563 }
4564
4565 /**
4566 * method='nlp', python solver_ln_nlp_analyzer: root the program and place its
4567 * blocks on the LQN index space. A processor reports Util only, a task Tput,
4568 * QLen, Util and ResidT, an entry Tput, RespT, QLen and Util, an activity all
4569 * but ArvR; what stays undefined is what the layered path leaves undefined.
4570 */
4571 LnSolution<T> nlp_analyzer() {
4572 using P_ = ::line::lqn::NlpProgram;
4573 P_ P;
4574 const std::vector<double> x = nlp_root(P);
4575 const double obj = P.objective(x);
4576 nlp_solution_ = x;
4577 nlp_objective_ = obj;
4578 hasconverged = P.solved(x);
4579 did_converge = hasconverged;
4580
4581 LnSolution<T> s;
4582 const std::size_t N = lqn.nidx;
4583 auto blank = [&](std::vector<T>& v, std::vector<bool>& d) {
4584 v.assign(N + 1, Tzero());
4585 d.assign(N + 1, false);
4586 };
4587 blank(s.QN, s.defined_Q);
4588 blank(s.UN, s.defined_U);
4589 blank(s.RN, s.defined_R);
4590 blank(s.TN, s.defined_T);
4591 blank(s.AN, s.defined_A);
4592 blank(s.WN, s.defined_W);
4593 auto put = [&](std::vector<T>& v, std::vector<bool>& d, std::size_t i, double val) {
4594 v[i] = fromd(val);
4595 d[i] = true;
4596 };
4597 const std::vector<double> Xe = P.block(x, P_::XE), Xa = P.block(x, P_::XA), W = P.block(x, P_::W),
4598 S = P.block(x, P_::S), R = P.block(x, P_::R);
4599 const std::vector<double> util_a = nlp_act_util(P, Xa);
4600 for (std::size_t a = 0; a < P.NA; ++a) {
4601 double resp = W[a];
4602 for (std::size_t c : P.calls_of_act[a]) resp += P.call_mean[c] * R[c];
4603 const std::size_t aidx = lqn.ashift + a + 1;
4604 put(s.TN, s.defined_T, aidx, Xa[a]);
4605 put(s.RN, s.defined_R, aidx, resp);
4606 put(s.QN, s.defined_Q, aidx, Xa[a] * resp);
4607 put(s.UN, s.defined_U, aidx, util_a[a]);
4608 put(s.WN, s.defined_W, aidx, P.act_visits[a] * W[a]);
4609 }
4610 for (std::size_t e = 0; e < P.NE; ++e) {
4611 const std::size_t eidx = lqn.eshift + e + 1;
4612 double u = 0.0;
4613 for (std::size_t a = 0; a < P.NA; ++a)
4614 if (P.act_entry[a] == e) u += util_a[a];
4615 put(s.TN, s.defined_T, eidx, Xe[e]);
4616 put(s.RN, s.defined_R, eidx, S[e]);
4617 put(s.QN, s.defined_Q, eidx, Xe[e] * S[e]);
4618 put(s.UN, s.defined_U, eidx, u);
4619 }
4620 for (std::size_t t = 0; t < P.NT; ++t) {
4621 double tn = 0.0, qn = 0.0, un = 0.0, wn = 0.0;
4622 for (std::size_t e = 0; e < P.NE; ++e)
4623 if (P.entry_task[e] == t) {
4624 tn += Xe[e];
4625 qn += Xe[e] * S[e];
4626 }
4627 for (std::size_t a = 0; a < P.NA; ++a)
4628 if (P.act_task[a] == t) {
4629 un += util_a[a];
4630 wn += P.act_visits[a] * W[a];
4631 }
4632 const std::size_t tidx = lqn.tshift + t + 1;
4633 put(s.TN, s.defined_T, tidx, tn);
4634 put(s.QN, s.defined_Q, tidx, qn);
4635 put(s.UN, s.defined_U, tidx, un);
4636 put(s.WN, s.defined_W, tidx, wn);
4637 }
4638 for (std::size_t h = 0; h < P.NH; ++h) {
4639 double un = 0.0;
4640 for (std::size_t a : P.acts_of_host[h]) un += util_a[a];
4641 put(s.UN, s.defined_U, lqn.hshift + h + 1, un);
4642 }
4643 for (std::size_t i = 1; i <= N; ++i)
4644 if (ignore[i]) {
4645 put(s.QN, s.defined_Q, i, 0.0);
4646 put(s.UN, s.defined_U, i, 0.0);
4647 put(s.RN, s.defined_R, i, 0.0);
4648 put(s.TN, s.defined_T, i, 0.0);
4649 put(s.AN, s.defined_A, i, 0.0);
4650 put(s.WN, s.defined_W, i, 0.0);
4651 }
4652 s.iterations = 0;
4653 s.converged = hasconverged;
4654 if (!hasconverged)
4655 s.warning = "method='nlp' did not reach a root of the QD-AMVA laws (objective " +
4656 ::line::lqn::nlp_detail::fmt("%.3e", obj) + " over " + std::to_string(P.NRES) +
4657 " residuals); the reported means satisfy the linear relations but not every congestion "
4658 "law. solve() already escalated the warm start to 100 sweeps, so raise "
4659 "config['nlp_warm'] past that to start it further along.";
4660 return s;
4661 }
4662
4663 /** The surrogate client delay law of update_think_times, at a given busy-thread count. */
4664 void seed_think_time(std::size_t tidx, double X, double busy, double z, bool inf, double mult) {
4665 double nj = 0.0;
4666 for (std::size_t c = 1; c <= NT(); ++c) nj = std::max(nj, njobs(tidx, c));
4667 const double val = inf ? (nj - busy) / X - z : nj * std::fabs(1.0 - busy / mult) / X - z;
4668 const double th = std::max(GlobalConstants::Zero, std::max(0.0, val) + setup_charge(tidx));
4669 thinkt[tidx] = fromd(th);
4670 if (th + z > 0) thinktproc[tidx] = Distrib<T>::exp_mean(fromd(th + z));
4671 }
4672
4673 /** Fit the seeded means as exponential laws, as both python seeds do. */
4674 void seed_processes() {
4675 for (std::size_t i = 1; i <= lqn.nidx; ++i)
4676 if (dbl(servt[i]) > 0) servtproc[i] = Distrib<T>::exp_mean(servt[i]);
4677 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
4678 if (dbl(callservt[c]) > 0) callservtproc[c] = Distrib<T>::exp_mean(callservt[c]);
4679 }
4680
4681 /** Carry the seeded laws into the layers, the tail of post() without an iteration's results. */
4682 void seed_layers() {
4683 update_layers(0);
4684 for (std::size_t e : svc_reset) ensemble[e].refresh_rates();
4685 warmstarted_ = true;
4686 }
4687
4688 /**
4689 * warmstart='nlp', python ln_nlp_warm_start: seed the LAYERED iterate with
4690 * the program's service, residence, throughput and surrogate delays. The
4691 * iteration then runs normally and converges to its own fixed point.
4692 */
4693 void nlp_warm_start() {
4694 using P_ = ::line::lqn::NlpProgram;
4695 P_ P;
4696 const std::vector<double> x = nlp_root(P);
4697 const std::vector<double> Xe = P.block(x, P_::XE), Xa = P.block(x, P_::XA), W = P.block(x, P_::W),
4698 S = P.block(x, P_::S), R = P.block(x, P_::R), BC = P.block(x, P_::BC),
4699 BR = P.block(x, P_::BR);
4700 const std::vector<double> util_a = nlp_act_util(P, Xa);
4701 for (std::size_t a = 0; a < P.NA; ++a) {
4702 const std::size_t idx = lqn.ashift + a + 1;
4703 double st = W[a];
4704 for (std::size_t c : P.calls_of_act[a]) st += P.call_mean[c] * R[c];
4705 servt[idx] = fromd(st);
4706 residt[idx] = fromd(P.act_visits[a] * W[a]);
4707 tput[idx] = fromd(Xa[a]);
4708 util[idx] = fromd(util_a[a]);
4709 }
4710 for (std::size_t e = 0; e < P.NE; ++e) {
4711 const std::size_t idx = lqn.eshift + e + 1;
4712 servt[idx] = residt[idx] = fromd(S[e]);
4713 tput[idx] = fromd(Xe[e]);
4714 }
4715 for (std::size_t t = 0; t < P.NT; ++t) {
4716 double x_t = 0.0;
4717 for (std::size_t e = 0; e < P.NE; ++e)
4718 if (P.entry_task[e] == t) x_t += Xe[e];
4719 tput[lqn.tshift + t + 1] = fromd(x_t);
4720 }
4721 for (std::size_t c = 0; c < P.NC; ++c) {
4722 callservt[c + 1] = fromd(R[c]);
4723 callresidt[c + 1] = fromd(P.call_mean[c] * R[c]);
4724 }
4725 seed_processes();
4726 // the surrogate delays: BC(c) is call c's hold on its callee, BR(r) the threads of reference task r held
4727 for (std::size_t t = 0; t < P.NT; ++t) {
4728 const std::size_t idx = lqn.tshift + t + 1;
4729 const double X = dbl(tput[idx]);
4730 if (X <= GlobalConstants::Zero) continue;
4731 const auto r = std::find(P.ref_tasks.begin(), P.ref_tasks.end(), t);
4732 double busy = 0.0;
4733 if (r != P.ref_tasks.end()) {
4734 busy = BR[std::size_t(r - P.ref_tasks.begin())];
4735 } else {
4736 for (std::size_t c : P.calls_into_task[t]) busy += BC[c];
4737 }
4738 const double mult = P.task_mult[t];
4739 seed_think_time(idx, X, busy, P.task_think[t],
4740 lqn.sched[idx] == SchedStrategy::INF || std::isinf(mult), mult);
4741 }
4742 seed_layers();
4743 }
4744
4745 /**
4746 * warmstart='mol', python ln_mol_warm_start: seed the LAYERED iterate with
4747 * lqn_mol's converged state. lqn_mol serves entry-only models, so each
4748 * entry's single activity takes the entry's whole execution.
4749 */
4750 void mol_warm_start() {
4751 if constexpr (num_traits<T>::has_transcendental) {
4752 ::line::lqn::LqnMolOptions mo;
4753 mo.iter_max = opt.mol_iter_max;
4754 mo.iter_tol = opt.mol_iter_tol;
4755 mo.relax_factor = opt.mol_relax_factor;
4756 const ::line::lqn::LqnMolResult<T> m = ::line::lqn::lqn_mol(lqn, mo);
4757 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
4758 const std::size_t eidx = lqn.eshift + e;
4759 const std::size_t aidx = lqn.actsof[eidx].at(0);
4760 servt[eidx] = residt[eidx] = m.info.servt[eidx];
4761 tput[eidx] = m.TN[eidx];
4762 servt[aidx] = m.info.servt[eidx];
4763 residt[aidx] = m.info.residt[eidx];
4764 tput[aidx] = m.TN[eidx];
4765 util[aidx] = m.UN[eidx];
4766 }
4767 for (std::size_t t = 1; t <= lqn.ntasks; ++t) tput[lqn.tshift + t] = m.TN[lqn.tshift + t];
4768 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
4769 double y = dbl(lqn.callproc_mean[c]);
4770 if (std::isnan(y)) y = 0.0;
4771 callservt[c] = m.info.callservt[c];
4772 callresidt[c] = T(fromd(y) * m.info.callservt[c]);
4773 }
4774 seed_processes();
4775 // lqn_mol's own thinkt is a submodel's Z, so SolverLN's law is applied to its busy threads instead
4776 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
4777 const std::size_t tidx = lqn.tshift + t;
4778 const double X = dbl(m.TN[tidx]);
4779 if (X <= GlobalConstants::Zero) continue;
4780 double z = 0.0;
4781 if (tidx < lqn.think.size() && !lqn.think[tidx].disabled) {
4782 z = dbl(lqn.think[tidx].mean);
4783 if (!std::isfinite(z) || z < 0.0) z = 0.0;
4784 }
4785 double mult = tidx < lqn.mult.size() ? lqn.mult[tidx] : 1.0;
4786 if (mult == 0.0) mult = 1.0;
4787 seed_think_time(tidx, X, dbl(m.QN[tidx]), z,
4788 lqn.sched[tidx] == SchedStrategy::INF || !std::isfinite(mult),
4789 std::max(1.0, mult));
4790 }
4791 seed_layers();
4792 } else {
4793 // validate_nlp_options refuses this arithmetic before anything is built
4794 throw UnsupportedError(ErrorKind{"Arithmetic"},
4795 "SolverLN: config.warmstart='mol' needs transcendental arithmetic; rerun with --arith double");
4796 }
4797 }
4798
4799 /** True when the layers are the collapsed phase-type ones of "srvn.ph". */
4800 bool is_srvn_ph() const { return lnmethod == "srvn.ph"; }
4801
4802 /**
4803 * True when the layers carry the COMPOSED phase-type server law rather than
4804 * the routing encoding of the activity graph, under either layering. The
4805 * encoding, not the layering, decides which update and reconstruction passes
4806 * run, so every such dispatch asks this and not for one method name.
4807 */
4808 bool is_ph_encoding() const { return lnmethod == "srvn.ph" || lnmethod == "flat.ph"; }
4809
4810 /** True under the flat (squashed) layering, whichever encoding it carries. */
4811 bool is_flat_layering() const { return lnmethod == "flat.cs" || lnmethod == "flat.ph"; }
4812
4813 /**
4814 * Station of layer L that stands for LQN element ELEM, falling back to the
4815 * layer's own server when ELEM is not a server there. Under "srvn" the
4816 * fallback is the answer for every element; under "flat.cs" it is the map
4817 * that tells the many servers of one layer apart.
4818 */
4819 static std::size_t station_idx_of(const qn::Layer<T>& L, std::size_t elem) {
4820 if (elem >= 1 && elem < L.server_idx_of.size() && L.server_idx_of[elem] != 0)
4821 return L.server_idx_of[elem];
4822 return L.serverIdx;
4823 }
4824
4825 /**
4826 * Station of layer L that class K (0-based) is served at: the PROCESSOR of
4827 * an activity, the CALLED TASK of a call, the layer's own server otherwise.
4828 */
4829 std::size_t station_idx_of_class(const qn::Layer<T>& L, std::size_t k) const {
4830 const int kind = L.classes[k].attr_kind;
4831 const std::size_t a = L.classes[k].attr_idx;
4832 if (kind == int(LqnElement::ACTIVITY))
4833 return station_idx_of(L, lqn.parent[lqn.parent[a]]);
4834 if (kind == int(LqnElement::CALL))
4835 return station_idx_of(L, lqn.parent[lqn.callpair_dst[a]]);
4836 return L.serverIdx;
4837 }
4838
4839 /**
4840 * Answer whether "srvn.ph" can serve this model, without disturbing the
4841 * solver.
4842 *
4843 * Both the feature gate and the series-parallel reduction can refuse, and
4844 * the second only finds out by composing the per-entry workflows -- work the
4845 * build then reuses, since those laws do not depend on the iterate.
4846 */
4847 bool probe_srvn_ph() {
4848 try {
4849 ph_assert_supported();
4850 ph_init_laws();
4851 ph_laws_ready = true;
4852 return true;
4853 } catch (const std::exception&) {
4854 ph_laws_ready = false;
4855 return false;
4856 }
4857 }
4858
4859 /** One two-station layer, and the caller classes that cycle through it. */
4860 struct PHLayer {
4861 std::size_t idx = 0;
4862 bool ishost = false;
4863 std::vector<std::size_t> callers;
4864 /** 1-based class index of each caller task, 0 when absent. */
4865 std::vector<std::size_t> class_of_caller;
4866 std::size_t nreplicas = 1;
4867 /** 1-based station indices of the server replicas. */
4868 std::vector<std::size_t> qstations;
4869 /** Mean of the law each class is currently served with, by class index. */
4870 std::vector<T> svcmean_by_class;
4871 /** (class index, entry index) or (class index, -call index). */
4872 std::vector<std::pair<std::size_t, long>> open_arrivals;
4873 /**
4874 * Closed population of the MODEL this server sits in. Under "flat.ph"
4875 * that is every caller of the single network, not only the callers of
4876 * this one station, so it is recorded here rather than recomputed.
4877 */
4878 double npop = 0.0;
4879 };
4880
4881 // per-entry workflows and their composed laws
4882 std::vector<workflow::Workflow<T>> ph_wf, ph_wfhost;
4883 std::vector<std::unordered_map<std::size_t, T>> ph_execs;
4884 std::vector<bool> ph_has_wf;
4885 std::vector<workflow::PhLaw<T>> ph_hostlaw, ph_entrylaw;
4886 std::vector<T> ph_hostmean, ph_entrymean, ph_entryscv;
4887 std::vector<T> ph_share, ph_overlap, ph_setupshare, ph_xdemand;
4888 Matrix<T> ph_ncalls, ph_calltime;
4889 std::vector<T> ph_procresid, ph_actthinkt, ph_calltotal;
4890 std::vector<T> ph_procsave; ///< overlap an AND fork saves on the processor
4891 Matrix<T> ph_callsave; ///< [caller task, called task] overlap saved on a callee
4892 std::vector<PHLayer> ph_layers; ///< by element index, empty where absent
4893 std::vector<bool> ph_has_layer;
4894
4895 /** Mean of an activity's own think time, 0 when it declares none. */
4896 T act_think_time(std::size_t aidx) const {
4897 if (aidx >= lqn.actthink.size() || lqn.actthink[aidx].disabled) return Tzero();
4898 const double m = dbl(lqn.actthink[aidx].mean);
4899 if (!std::isfinite(m) || m <= GlobalConstants::FineTol) return Tzero();
4900 return lqn.actthink[aidx].mean;
4901 }
4902
4903 /** SetupTask cold-start charge per task, as last computed by setup_charge(tidx, S). */
4904 std::unordered_map<std::size_t, double> setupcharge;
4905
4906 /**
4907 * Mean extra time one request of task TIDX spends because threads power
4908 * down, 0 when it declares no setup.
4909 *
4910 * A SetupTask powers a thread down when its delay-off countdown expires while
4911 * it is idle, and a request that finds no warm thread waits for a setup. The
4912 * task's thread pool is therefore an M/M/k queue with setup and delayed-off
4913 * servers (Gandhi, Harchol-Balter and Adan, Performance Evaluation 67(11),
4914 * 2010), with k the multiplicity and the admission rules of the LDES engine,
4915 * and it is solved EXACTLY by mam::qbd_setupdelayoff_mmk, at the callers'
4916 * population N and complementary delay Z = (N - Q)/X of the layer where TIDX
4917 * is the server (or at its arrival rate X when a visiting class is open), and
4918 * at the base service time S of a request, i.e. the entry service before any
4919 * charge. The layer serves the pool as a plain M/M/k, so the charge is the
4920 * EQUIVALENT extra service,
4921 *
4922 * charge = S' - S, with R_M/M/k(S') = QN/X (setup, delay-off),
4923 *
4924 * S' found by bisection. Subtracting the two response times instead counted
4925 * the queueing behind a setup twice, once in the chain and again in the
4926 * layer, and ran the one-thread model of test_ln_setup 5.6% below LDES. The
4927 * result is continuous in the load and exact at any thread count; it
4928 * replaces a closure that assumed max(1, b) hot threads at offered load b,
4929 * which dropped the charge to zero above one busy thread and oscillated at
4930 * the kink (BUG-78).
4931 *
4932 * Called with S, it computes the charge from the current layer solution and
4933 * stores it, as the metric update does once per iteration; called with NaN,
4934 * it returns the stored value, so every consumer of one iteration charges
4935 * the same cold start. Before anything has been computed the thread is down
4936 * when the first request arrives, and the charge is the setup mean. Twin of
4937 * MATLAB lqn_setup_charge.m, the JAR SolverLN.setupCharge and the Python
4938 * SolverLN._setup_charge.
4939 */
4940 double setup_charge(std::size_t tidx, double S = std::numeric_limits<double>::quiet_NaN()) {
4941 if (tidx >= lqn.hassetup.size() || !lqn.hassetup[tidx]) return 0.0;
4942 const double s = tidx < lqn.setuptime.size() && !lqn.setuptime[tidx].disabled
4943 ? dbl(lqn.setuptime[tidx].mean) : 0.0;
4944 const double d = tidx < lqn.delayofftime.size() && !lqn.delayofftime[tidx].disabled
4945 ? dbl(lqn.delayofftime[tidx].mean) : 0.0;
4946 if (!(s > GlobalConstants::FineTol) || !(d > GlobalConstants::FineTol)) return 0.0;
4947 const double mult = lqn.mult[tidx];
4948 if (!std::isfinite(mult) || mult <= 0.0) return 0.0;
4949 if (std::isnan(S)) {
4950 const auto it = setupcharge.find(tidx);
4951 return it == setupcharge.end() ? s : it->second;
4952 }
4953 const double c = setup_charge_exact(tidx, static_cast<int>(std::llround(mult)), S, s, d);
4954 setupcharge[tidx] = c;
4955 return c;
4956 }
4957
4958 /** The charge of setup_charge from the layer where TIDX is the server. */
4959 double setup_charge_exact(std::size_t tidx, int k, double S, double s, double d) const {
4960 // nothing has arrived yet, so the thread is down when the first does
4961 if (!(S > GlobalConstants::FineTol) || tidx >= idxhash.size() || idxhash[tidx] < 0
4962 || results.empty() || std::size_t(idxhash[tidx]) >= results.back().size())
4963 return s;
4964 const std::size_t e = std::size_t(idxhash[tidx]);
4965 const qn::Layer<T>& L = ensemble[e];
4966 const LayerResult<T>& r = results.back()[e];
4967 if (r.TN.rows() == 0 || r.QN.rows() == 0) return s;
4968 const std::size_t ts = station_idx_of(L, tidx) - 1;
4969 // per replica: the layer's station is one representative replica
4970 double X = 0.0, Q = 0.0;
4971 for (std::size_t c = 0; c < L.nclasses && c < r.TN.cols(); ++c) {
4972 const double tn = dbl(r.TN(ts, c)), qn = dbl(r.QN(ts, c));
4973 if (!std::isnan(tn)) X += tn;
4974 if (!std::isnan(qn)) Q += qn;
4975 }
4976 if (!std::isfinite(X) || X <= GlobalConstants::FineTol) return s;
4977 // N is the population of every CHAIN that visits the station: under
4978 // 'srvn.cs' a caller's jobs sit in a class of the chain other than the
4979 // one served here
4980 double N = 0.0;
4981 bool isopen = false;
4982 for (std::size_t ch = 0; ch < L.inchain.size(); ++ch) {
4983 bool visits = false;
4984 double pop = 0.0;
4985 for (std::size_t c1 : L.inchain[ch]) {
4986 if (c1 < 1 || c1 > L.nclasses || c1 > r.TN.cols()) continue;
4987 if (dbl(r.TN(ts, c1 - 1)) > 0.0) visits = true;
4988 pop += L.classes[c1 - 1].population;
4989 }
4990 if (!visits) continue;
4991 if (std::isfinite(pop)) N += pop;
4992 else isopen = true;
4993 }
4994 const double mu = 1.0 / S;
4995 auto scv_of = [](const std::vector<Distrib<T>>& procs, std::size_t i) {
4996 if (i >= procs.size() || procs[i].disabled) return 1.0;
4997 const double v = dbl(procs[i].scv);
4998 return (std::isfinite(v) && v > 0.0) ? v : 1.0;
4999 };
5000 const double ascv = scv_of(lqn.setuptime, tidx);
5001 const double bscv = scv_of(lqn.delayofftime, tidx);
5002 // the layer serves the pool as a plain M/M/k, so the charge is what that
5003 // station needs on top of S to answer in the setup pool's response time
5004 if (isopen || N <= 0.0) {
5005 if (X >= k * mu) return 0.0; // a saturated pool never idles, so no thread powers down
5006 const mam::SetupDelayoffMMk<double> res = mam::qbd_setupdelayoff_mmk<double>(
5007 k, std::numeric_limits<double>::infinity(), X, mu, 1.0 / s, ascv, 1.0 / d, bscv);
5008 if (!(res.XN > GlobalConstants::FineTol)) return s;
5009 return mmk_equiv_service([&](double Sp) { return mmk_open_resp(k, X, 1.0 / Sp); }, S,
5010 res.QN / res.XN, k / X) - S;
5011 }
5012 const int pop = static_cast<int>(std::llround(N));
5013 const double Z = std::max((pop - std::min(Q, double(pop))) / X, GlobalConstants::FineTol);
5014 const mam::SetupDelayoffMMk<double> res =
5015 mam::qbd_setupdelayoff_mmk<double>(k, pop, Z, mu, 1.0 / s, ascv, 1.0 / d, bscv);
5016 if (!(res.XN > GlobalConstants::FineTol)) return s;
5017 return mmk_equiv_service([&](double Sp) { return mmk_closed_resp(k, pop, Z, 1.0 / Sp); }, S,
5018 res.QN / res.XN, res.QN / res.XN) - S;
5019 }
5020
5021 /**
5022 * Service time S' >= S at which the M/M/k response time RESP(S') equals Rs,
5023 * found by bisection on [S, hi]; S itself when the pool already answers in Rs.
5024 */
5025 template <class F>
5026 static double mmk_equiv_service(const F& resp, double S, double Rs, double hi) {
5027 double lo = S;
5028 if (resp(lo) >= Rs) return S;
5029 for (int it = 0; it < 200; ++it) {
5030 const double mid = 0.5 * (lo + hi);
5031 if (resp(mid) < Rs) lo = mid;
5032 else hi = mid;
5033 if (hi - lo <= 1e-12 * hi) break;
5034 }
5035 return 0.5 * (lo + hi);
5036 }
5037
5038 /**
5039 * Mean response time of an M/M/k station visited by N customers that spend
5040 * a mean Z elsewhere: the birth-death chain of the machine-repairman model.
5041 */
5042 static double mmk_closed_resp(int k, int N, double Z, double mu) {
5043 std::vector<double> p(std::size_t(N) + 1, 0.0);
5044 p[0] = 1.0;
5045 for (int n = 1; n <= N; ++n) {
5046 p[n] = p[n - 1] * ((N - n + 1) / Z) / (std::min(n, k) * mu);
5047 if (p[n] > 1e250)
5048 for (int i = 0; i <= n; ++i) p[i] /= p[n];
5049 }
5050 double tot = 0.0, Qs = 0.0, Xs = 0.0;
5051 for (int n = 0; n <= N; ++n) tot += p[n];
5052 for (int n = 0; n <= N; ++n) {
5053 Qs += n * p[n] / tot;
5054 Xs += (N - n) / Z * p[n] / tot;
5055 }
5056 return Qs / Xs;
5057 }
5058
5059 /** Mean response time of an open M/M/k queue (Erlang C). */
5060 static double mmk_open_resp(int k, double lam, double mu) {
5061 const double a = lam / mu, rho = a / k;
5062 double term = 1.0, acc = 1.0;
5063 for (int i = 1; i < k; ++i) {
5064 term *= a / i;
5065 acc += term;
5066 }
5067 const double tk = term * a / k;
5068 const double C = tk / (1.0 - rho) / (acc + tk / (1.0 - rho));
5069 return 1.0 / mu + C / (k * mu - lam);
5070 }
5071
5072 /**
5073 * Probability that a request for entry EIDX finds its task's thread off.
5074 * ONE closure for both methods: setup_charge returns p*s, so p is that over
5075 * s, evaluated at M1, the mean of the composed entry law before any setup.
5076 * It also answers p = 1 during construction, before the first layer solve.
5077 */
5078 double ph_setup_prob(std::size_t eidx, double m1) {
5079 const std::size_t tidx = lqn.parent[eidx];
5080 if (tidx >= lqn.hassetup.size() || !lqn.hassetup[tidx]) return 0.0;
5081 const double s = tidx < lqn.setuptime.size() && !lqn.setuptime[tidx].disabled
5082 ? dbl(lqn.setuptime[tidx].mean) : 0.0;
5083 const double d = tidx < lqn.delayofftime.size() && !lqn.delayofftime[tidx].disabled
5084 ? dbl(lqn.delayofftime[tidx].mean) : 0.0;
5085 if (!(s > GlobalConstants::FineTol) || !(d > GlobalConstants::FineTol)) return 0.0;
5086 return std::min(1.0, std::max(0.0, setup_charge(tidx, m1) / s));
5087 }
5088
5089 /** Divisor that scales a processor utilization into [0,1]. */
5090 double ph_host_servers(std::size_t hidx) const {
5091 if (lqn.sched[hidx] == SchedStrategy::INF) return 1.0;
5092 const double m = lqn.maxmult[hidx];
5093 return (std::isfinite(m) && m > 0.0) ? m : 1.0;
5094 }
5095
5096 /** True when any task calls entry EIDX, synchronously or not. */
5097 bool ph_any_caller_of(std::size_t eidx) const {
5098 return lqn.issynccaller.any_col(eidx) || lqn.isasynccaller.any_col(eidx);
5099 }
5100
5101 /**
5102 * True when an entry arrival is the ONLY way requests reach task TIDX.
5103 * "srvn.ph" refuses forwarding calls outright, so sync/async callers are the
5104 * whole test.
5105 */
5106 bool ph_open_arrival_only(std::size_t tidx) const {
5107 if (lqn.isref[tidx]) return false;
5108 for (std::size_t eidx : lqn.entriesof[tidx])
5109 if (ph_any_caller_of(eidx)) return false;
5110 for (std::size_t eidx : lqn.entriesof[tidx])
5111 if (lqn.has_arrival[eidx]) return true;
5112 return false;
5113 }
5114
5115 /** Asynchronous calls whose target entry belongs to TIDX. */
5116 std::vector<std::size_t> ph_async_calls_into(std::size_t tidx) const {
5117 std::vector<std::size_t> out;
5118 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
5119 if (lqn.calltype[cidx] != CallType::ASYNC) continue;
5120 for (std::size_t e : lqn.entriesof[tidx])
5121 if (lqn.callpair_dst[cidx] == e) { out.push_back(cidx); break; }
5122 }
5123 return out;
5124 }
5125
5126 /**
5127 * Features the collapsed layer cannot represent are refused by name rather
5128 * than silently degraded.
5129 */
5130 void ph_assert_supported(bool flat = false) const {
5131 // The list is a property of the ENCODING, so it is the same under either
5132 // layering; what the squashing adds on top is refused in ph_flat_server_set.
5133 const std::string mname = flat ? "flat.ph" : "srvn.ph";
5134 // PHASE 2 IS ASKED HERE AND NOWHERE ELSE. `has_phase2` is built during
5135 // layering rather than being a property of the model, so the report twin
5136 // below cannot ask it; every other rule is shared with it.
5137 if (has_phase2)
5138 throw UnsupportedError(ErrorKind{"Activity"},
5139 "method='" + mname + "' does not support second-phase activities: the composed "
5140 "entry law has no reply point. Use method='default'.");
5141 // ONE RULE LIST, TWO CALLERS: this throws what `ph_method_refusal`
5142 // returns, in the same order, so the run and a report cannot say
5143 // different things about one model. The squashing rules it also carries
5144 // for `flat.ph` are the ones `ph_flat_server_set` raises, which runs
5145 // before this on that path, so the message a caller sees is unchanged.
5146 const std::string why = ph_method_refusal(mname);
5147 if (!why.empty()) throw UnsupportedError(ErrorKind{"Model"}, why);
5148 }
5149
5150 /**
5151 * The same rules as a SENTENCE, so a report can withdraw a method it cannot run.
5152 *
5153 * ONE RULE LIST, TWO CALLERS. `ph_assert_supported` above answers the run
5154 * path by throwing; nothing answered the report, and `list_valid_methods`
5155 * returns the same eight names for every model, so every layered model was
5156 * offered every encoding and `srvn.ph`/`flat.ph` then threw on contact.
5157 *
5158 * PHASE 2 IS DELIBERATELY ABSENT. `has_phase2` is built during layering
5159 * rather than being a property of the model, so a gate cannot ask it without
5160 * doing the layering it is meant to precede; the run path still refuses it.
5161 *
5162 * @param method the concrete method name
5163 * @return empty string when the method can encode this model, else the reason
5164 */
5165 std::string ph_method_refusal(const std::string& method) const {
5166 if (method != "srvn.ph" && method != "flat.ph") return std::string();
5167 // The squashing refusals, `flat.ph` only: each carries PER-LAYER state
5168 // that one submodel cannot hold (ph_flat_server_set).
5169 if (method == "flat.ph") {
5170 for (std::size_t i = 1; i <= NT(); ++i) {
5171 if (lqn.repl[i] > 1.0)
5172 return "method='flat.ph' does not support replicated processors or tasks, "
5173 "whose replicas need a submodel each. Use method='srvn.ph'.";
5174 if (lqn.hassetup[i])
5175 return "method='flat.ph' does not support setup tasks, whose powered-down "
5176 "threads are per-layer state. Use method='srvn.ph'.";
5177 }
5178 }
5179 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx)
5180 if (lqn.calltype[cidx] == CallType::FWD)
5181 return "method='" + method +
5182 "' does not support forwarding calls, whose target is not part of the "
5183 "caller's activity graph. Use method='default'.";
5184 for (std::size_t i = 1; i < lqn.iscache.size(); ++i)
5185 if (lqn.iscache[i])
5186 return "method='" + method +
5187 "' does not support cache tasks. Use method='default'.";
5188 for (std::size_t i = 1; i < lqn.hassetup.size(); ++i) {
5189 if (!lqn.hassetup[i]) continue;
5190 if (lqn.sched[i] == SchedStrategy::INF || !std::isfinite(lqn.mult[i]))
5191 return "method='" + method + "': task '" + lqn.names[i] +
5192 "' declares a setup time on an infinite-server task, which holds no "
5193 "thread to power down; give it a finite multiplicity.";
5194 }
5195 for (std::size_t i = 0; i < lqn.lincon_A.size(); ++i)
5196 if (lqn.lincon_A[i].rows() > 0)
5197 return "method='" + method +
5198 "' does not support admission constraints on a layer station. "
5199 "Use method='default'.";
5200 for (std::size_t i = 1; i < lqn.lldscaling.size(); ++i) {
5201 const char* fname = nullptr;
5202 if (!lqn.lldscaling[i].empty()) fname = "a load dependence";
5203 else if (lqn.cdscaling[i]) fname = "a class dependence";
5204 else if (lqn.jdscaling[i]) fname = "a joint dependence";
5205 else if (!lqn.pools[i].empty()) fname = "server pools";
5206 if (fname != nullptr)
5207 return "method='" + method +
5208 "' does not support queue-dependent service rates on a layer station ('" +
5209 lqn.names[i] + "' declares " + fname + "). Use method='srvn.cs'.";
5210 }
5211 return std::string();
5212 }
5213
5214 /** Build the per-entry workflows and the iteration-invariant processor law. */
5215 void ph_init_laws() {
5216 const std::size_t N = lqn.nidx;
5217 ph_wf.assign(N + 1, workflow::Workflow<T>("empty"));
5218 ph_wfhost.assign(N + 1, workflow::Workflow<T>("empty"));
5219 ph_execs.assign(N + 1, {});
5220 ph_has_wf.assign(N + 1, false);
5221 ph_hostlaw.assign(N + 1, workflow::PhLaw<T>());
5222 ph_entrylaw.assign(N + 1, workflow::PhLaw<T>());
5223 ph_hostmean.assign(N + 1, Tzero());
5224 ph_entrymean.assign(N + 1, Tzero());
5225 ph_entryscv.assign(N + 1, Tone());
5226 ph_share.assign(N + 1, Tzero());
5227 ph_overlap.assign(N + 1, Tone());
5228 ph_setupshare.assign(N + 1, Tzero());
5229 ph_xdemand.assign(N + 1, Tzero());
5230 ph_ncalls = Matrix<T>(N + 1, N + 1, Tzero());
5231 ph_calltime = Matrix<T>(N + 1, N + 1, Tzero());
5232 ph_procresid.assign(N + 1, Tzero());
5233 ph_actthinkt.assign(N + 1, Tzero());
5234 ph_calltotal.assign(N + 1, Tzero());
5235 ph_procsave.assign(N + 1, Tzero());
5236 ph_callsave = Matrix<T>(N + 1, N + 1, Tzero());
5237 ph_layers.assign(NT() + 1, PHLayer());
5238 ph_has_layer.assign(NT() + 1, false);
5239
5240 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
5241 const std::size_t eidx = lqn.eshift + e;
5242 const std::size_t tidx = lqn.parent[eidx];
5243 if (ignore[tidx]) continue;
5244 api::lqn::EntryWorkflow<T> ew = api::lqn::entry_workflow(lqn, eidx, true);
5245 ph_wf[eidx] = std::move(ew.wf);
5246 ph_execs[eidx] = ew.execs;
5247 ph_has_wf[eidx] = true;
5248 api::lqn::EntryWorkflow<T> eh = api::lqn::entry_workflow(lqn, eidx, false);
5249 ph_wfhost[eidx] = std::move(eh.wf);
5250 // the processor sees the WORK of concurrent branches, not their
5251 // elapsed time, so the host law serialises an AND fork
5252 ph_hostlaw[eidx] = api::lqn::serial_law(ph_wfhost[eidx]);
5253 ph_hostmean[eidx] =
5254 api::lqn::ph_moments(ph_hostlaw[eidx].alpha, ph_hostlaw[eidx].S).first;
5255 }
5256
5257 // until the first iteration reports throughputs, a task splits its
5258 // requests evenly over its entries
5259 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5260 const std::size_t tidx = lqn.tshift + t;
5261 const std::size_t n = lqn.entriesof[tidx].size();
5262 if (n == 0) continue;
5263 for (std::size_t eidx : lqn.entriesof[tidx])
5264 ph_share[eidx] = num_traits<T>::from_double(1.0 / double(n));
5265 }
5266 }
5267
5268 /**
5269 * Law of the total time one execution of the issuing activity spends in call
5270 * CIDX: the geometric compound, of mean callproc_mean, of the response law
5271 * of the called entry. The response law is fitted to the response time
5272 * reported by the callee's layer and to the SCV of the callee's own composed
5273 * law, so no extra solver output is needed.
5274 */
5275 Distrib<T> ph_call_burst_law(std::size_t cidx) const {
5276 const double m = dbl(lqn.callproc_mean[cidx]);
5277 const std::size_t eidx = lqn.callpair_dst[cidx];
5279 const T R = T(callservt[cidx] / lqn.callproc_mean[cidx]);
5280 double scvd = dbl(ph_entryscv[eidx]);
5281 if (!std::isfinite(scvd) || scvd <= GlobalConstants::FineTol) scvd = 1.0;
5282 const T Rf = dbl(R) > GlobalConstants::FineTol
5283 ? R : num_traits<T>::from_double(GlobalConstants::FineTol);
5284 const Distrib<T> base = lang::aph_fit_mean_scv(Rf, num_traits<T>::from_double(scvd));
5285 const workflow::PhLaw<T> body = api::lqn::ph_law_of(base);
5286 const workflow::PhLaw<T> loop =
5287 workflow::Workflow<T>::compose_loop_geometric(body, lqn.callproc_mean[cidx]);
5288 return Distrib<T>::phase_type(loop.alpha, loop.S,
5290 }
5291
5292 /**
5293 * Station law of a composed workflow. A geometric loop over a body of two or
5294 * more phases closes a cycle in the phase graph, and a cyclic generator is a
5295 * PH and not an APH: no layer solver declares PH, so such a law is reduced
5296 * to the APH with the SAME first two moments. AMVA and NC read exactly those
5297 * two, so the reduction is lossless for them and is a two-moment fit for the
5298 * phase-aware layer solvers.
5299 */
5300 Distrib<T> ph_station_law(const workflow::PhLaw<T>& law) const {
5302 return Distrib<T>::phase_type(law.alpha, law.S, true);
5303 const std::pair<T, T> mm = api::lqn::ph_moments(law.alpha, law.S);
5304 return lang::aph_fit_mean_scv(mm.first, mm.second);
5305 }
5306
5307 /** The law of a single immediate phase, the empty-composition answer. */
5308 static workflow::PhLaw<T> ph_immediate_law() {
5309 workflow::PhLaw<T> out;
5310 out.alpha.assign(1, Tone());
5311 out.S = Matrix<T>(1, 1, num_traits<T>::from_double(-GlobalConstants::Immediate));
5312 return out;
5313 }
5314
5315 /** True when an activity of entry EIDX opens an AND fork, the only place the branches can overlap. */
5316 bool ph_has_and_fork(std::size_t eidx) const {
5317 for (std::size_t aidx : lqn.actsof[eidx])
5318 if (aidx < lqn.actposttype.size() && lqn.actposttype[aidx] == PrecedenceType::POST_AND) return true;
5319 return false;
5320 }
5321
5322 /**
5323 * True when the processor of entry EIDX's task serves one job at a time, so the
5324 * branches of an AND fork cannot run on it concurrently. Declared multiplicity,
5325 * not maxmult: maxmult caps a host by its tasks' threads, and a fork adds threads.
5326 */
5327 bool ph_single_server_host(std::size_t eidx) const {
5328 const std::size_t hidx = lqn.parent[lqn.parent[eidx]];
5329 if (lqn.sched[hidx] == SchedStrategy::INF) return false;
5330 const double m = lqn.mult[hidx];
5331 return std::isfinite(m) && m <= 1.0;
5332 }
5333
5334 /**
5335 * Recompose the entry service laws from the current fixed-point iterate.
5336 *
5337 * The composed mean is NOT the sum of the leaf means when the graph forks:
5338 * the branches of an AND fork overlap, and the entry finishes with the last
5339 * of them. The ratio of the two, the overlap factor, is what the caller-side
5340 * aggregates are scaled by, so that the pieces of a cycle still add up to
5341 * the cycle.
5342 */
5343 void ph_compose_entry_laws() {
5344 const std::size_t N = lqn.nidx;
5345 std::vector<T> entry_setup_share(N + 1, Tzero());
5346 ph_overlap.assign(N + 1, Tone());
5347 std::vector<bool> hostserial(N + 1, false);
5348
5349 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
5350 const std::size_t eidx = lqn.eshift + e;
5351 if (!ph_has_wf[eidx]) continue;
5352 workflow::Workflow<T>& w = ph_wf[eidx];
5353 const std::unordered_map<std::size_t, T>& ex = ph_execs[eidx];
5354 T entrysum = Tzero(), procsum = Tzero(), pres = Tzero();
5355 for (std::size_t aidx : lqn.actsof[eidx]) {
5356 T m = T(residt[aidx] + act_think_time(aidx));
5357 const T xa = ex.at(aidx);
5358 procsum += T(xa * m);
5359 pres += T(xa * residt[aidx]);
5360 const double md = dbl(m);
5361 w.set_activity_demand_mean(
5362 lqn.names[aidx],
5364 ? m : num_traits<T>::from_double(GlobalConstants::FineTol));
5365 for (std::size_t cidx : lqn.callsof[aidx]) {
5366 if (lqn.calltype[cidx] != CallType::SYNC) continue;
5367 w.set_activity_demand(lqn.callhashnames[cidx], ph_call_burst_law(cidx));
5368 m = T(m + callservt[cidx]);
5369 }
5370 entrysum += T(xa * m);
5371 }
5372 workflow::PhLaw<T> law = api::lqn::ph_law_of(w.refresh_ph());
5373 std::pair<T, T> mm = api::lqn::ph_moments(law.alpha, law.S);
5374 T m1 = mm.first, scv = mm.second;
5375 // All activities of an entry run on ONE processor, so the branches of
5376 // an AND fork cannot overlap the processor residence they request:
5377 // the composed maximum is a lower bound on the entry service time
5378 // only above that total. Where it falls below, the law is rescaled in
5379 // time to it, which keeps its shape, its SCV and its order.
5380 if (dbl(procsum) > dbl(m1) + GlobalConstants::FineTol) {
5381 const T f = T(m1 / procsum);
5382 for (std::size_t i = 0; i < law.S.rows(); ++i)
5383 for (std::size_t j = 0; j < law.S.cols(); ++j) law.S(i, j) = T(law.S(i, j) * f);
5384 m1 = procsum;
5385 }
5386 // On a SINGLE-SERVER host that bound is not enough: the branches queue for
5387 // the one server, so their host demand is not overlapped at all and only the
5388 // rest of the entry is. The law is stretched in time by the overlap the host
5389 // lost, and every layer then charges the host serially (see the aggregates).
5390 if (ph_has_and_fork(eidx) && ph_single_server_host(eidx)) {
5391 hostserial[eidx] = true;
5392 if (dbl(entrysum) > GlobalConstants::FineTol) {
5393 const T add = T((Tone() - num_traits<T>::from_double(
5394 std::min(1.0, dbl(m1) / dbl(entrysum)))) * pres);
5395 if (dbl(add) > GlobalConstants::FineTol) {
5396 const T f = T(m1 / (m1 + add));
5397 for (std::size_t i = 0; i < law.S.rows(); ++i)
5398 for (std::size_t j = 0; j < law.S.cols(); ++j) law.S(i, j) = T(law.S(i, j) * f);
5399 m1 = T(m1 + add);
5400 }
5401 }
5402 }
5403 // A SetupTask powers a thread down when it goes idle, so a request may
5404 // find it off and pay a cold start before the entry runs at all. The
5405 // setup is not part of the activity graph and never enters the
5406 // series-parallel reduction: it is prefixed to the composed law
5407 // afterwards as the mixture p*(setup THEN entry) + (1-p)*entry, which
5408 // is again phase-type.
5409 const double p = ph_setup_prob(eidx, dbl(m1));
5410 if (p > GlobalConstants::FineTol) {
5411 const std::size_t tidx = lqn.parent[eidx];
5412 const double sm = dbl(lqn.setuptime[tidx].mean);
5413 double sscv = dbl(lqn.setuptime[tidx].scv);
5414 if (!std::isfinite(sscv) || sscv <= GlobalConstants::FineTol) sscv = 1.0;
5415 if (std::isfinite(sm) && sm > GlobalConstants::FineTol) {
5416 const workflow::PhLaw<T> sl = api::lqn::ph_law_of(lang::aph_fit_mean_scv(
5417 num_traits<T>::from_double(sm), num_traits<T>::from_double(sscv)));
5418 std::vector<workflow::PhLaw<T>> mix;
5419 mix.push_back(workflow::Workflow<T>::compose_serial(sl, law));
5420 mix.push_back(law);
5421 std::vector<T> probs;
5422 probs.push_back(num_traits<T>::from_double(p));
5423 probs.push_back(num_traits<T>::from_double(1.0 - p));
5425 mm = api::lqn::ph_moments(law.alpha, law.S);
5426 m1 = mm.first;
5427 scv = mm.second;
5428 // The share of the entry law that is cold start and not work.
5429 // The surrogate-delay closure measures a thread's cycle in
5430 // WORK, so it must not read a station utilization this has
5431 // inflated -- see update_think_times_ph.
5432 const double denom = std::max(dbl(m1), GlobalConstants::FineTol);
5433 entry_setup_share[eidx] = num_traits<T>::from_double(p * sm / denom);
5434 }
5435 }
5436 ph_entrylaw[eidx] = law;
5437 ph_entrymean[eidx] = m1;
5438 ph_entryscv[eidx] = scv;
5439 if (hostserial[eidx]) {
5440 // only the non-host part of the entry overlaps: pres + r*(entrysum-pres) = m1
5441 const double rest = dbl(entrysum) - dbl(pres);
5442 if (rest > GlobalConstants::FineTol) {
5443 const double r = std::min(1.0, std::max(0.0, (dbl(m1) - dbl(pres)) / rest));
5444 ph_overlap[eidx] = num_traits<T>::from_double(r);
5445 }
5446 } else if (dbl(entrysum) > GlobalConstants::FineTol) {
5447 const double r = std::min(1.0, dbl(m1) / dbl(entrysum));
5448 ph_overlap[eidx] = num_traits<T>::from_double(r);
5449 }
5450 }
5451
5452 // Per task, the share-weighted fraction of its station service that is
5453 // cold start rather than work.
5454 ph_setupshare.assign(N + 1, Tzero());
5455 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5456 const std::size_t tidx = lqn.tshift + t;
5457 if (ignore[tidx]) continue;
5458 for (std::size_t eidx : lqn.entriesof[tidx])
5459 ph_setupshare[tidx] = T(ph_setupshare[tidx] + ph_share[eidx] * entry_setup_share[eidx]);
5460 }
5461
5462 // Expected number of calls per invocation, and the caller-side aggregates
5463 ph_ncalls = Matrix<T>(N + 1, N + 1, Tzero());
5464 ph_calltime = Matrix<T>(N + 1, N + 1, Tzero());
5465 ph_procresid.assign(N + 1, Tzero());
5466 ph_actthinkt.assign(N + 1, Tzero());
5467 ph_calltotal.assign(N + 1, Tzero());
5468 // The overlap SAVED on the processor and on each callee: a station charges its
5469 // demand in full, serial, so the layer holding it must take the saving back out
5470 // of the delay, or that layer's cycle is the serial sum and not the composed law.
5471 // Only an entry with an AND fork can save anything, so only those carry it: a
5472 // ratio of 1-eps elsewhere would subtract eps*calltime and cancel, see ph_delay_mean.
5473 ph_procsave.assign(N + 1, Tzero());
5474 ph_callsave = Matrix<T>(N + 1, N + 1, Tzero());
5475 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5476 const std::size_t tidx = lqn.tshift + t;
5477 if (ignore[tidx]) continue;
5478 for (std::size_t eidx : lqn.entriesof[tidx]) {
5479 if (!ph_has_wf[eidx]) continue;
5480 const T w = ph_share[eidx];
5481 if (!(dbl(w) > 0.0)) continue;
5482 const std::unordered_map<std::size_t, T>& ex = ph_execs[eidx];
5483 const T r = ph_overlap[eidx];
5484 const T sv = ph_has_and_fork(eidx) ? T(Tone() - r) : Tzero();
5485 // a single-server host is charged serially, so it neither overlaps nor saves
5486 const T rp = hostserial[eidx] ? Tone() : r;
5487 const T svp = hostserial[eidx] ? Tzero() : sv;
5488 for (std::size_t aidx : lqn.actsof[eidx]) {
5489 const T xa = ex.at(aidx);
5490 ph_procresid[tidx] = T(ph_procresid[tidx] + w * rp * xa * residt[aidx]);
5491 ph_procsave[tidx] = T(ph_procsave[tidx] + w * svp * xa * residt[aidx]);
5492 ph_actthinkt[tidx] = T(ph_actthinkt[tidx] + w * r * xa * act_think_time(aidx));
5493 for (std::size_t cidx : lqn.callsof[aidx]) {
5494 if (lqn.calltype[cidx] != CallType::SYNC) continue;
5495 const std::size_t tgte = lqn.callpair_dst[cidx];
5496 const std::size_t tgtt = lqn.parent[tgte];
5497 // the COUNT of calls does not change with the overlap,
5498 // only the time the caller is held by them
5499 ph_ncalls(tidx, tgte) =
5500 T(ph_ncalls(tidx, tgte) + w * xa * lqn.callproc_mean[cidx]);
5501 ph_calltime(tidx, tgtt) =
5502 T(ph_calltime(tidx, tgtt) + w * r * xa * callservt[cidx]);
5503 ph_calltotal[tidx] = T(ph_calltotal[tidx] + w * r * xa * callservt[cidx]);
5504 ph_callsave(tidx, tgtt) =
5505 T(ph_callsave(tidx, tgtt) + w * sv * xa * callservt[cidx]);
5506 }
5507 }
5508 }
5509 }
5510 }
5511
5512 /** Build the ensemble under method "srvn.ph". */
5513 void build_layers_ph(bool flat = false) {
5514 if (!ph_laws_ready) ph_assert_supported(flat);
5515 // The interlock correction rewrites the populations of the call classes,
5516 // which this method does not create: its callers reach the server in one
5517 // class each
5518 opt.interlocking = false;
5519 interlockMethod = "none";
5520
5521 // A preceding probe has already composed the per-entry workflows; they do
5522 // not depend on the iterate, so they are not rebuilt here.
5523 if (!ph_laws_ready) ph_init_laws();
5524
5525 // Seed the fixed point with the static demands, then compose the laws
5526 const std::size_t N = lqn.nidx;
5527 residt.assign(N + 1, Tzero());
5528 servt.assign(N + 1, Tzero());
5529 callservt.assign(lqn.ncalls + 1, Tzero());
5530 callresidt.assign(lqn.ncalls + 1, Tzero());
5531 for (std::size_t aidx = lqn.ashift + 1; aidx <= lqn.ashift + lqn.nacts; ++aidx)
5532 residt[aidx] = lqn.hostdem[aidx].disabled ? Tzero() : lqn.hostdem[aidx].mean;
5533 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
5534 if (lqn.calltype[cidx] != CallType::SYNC && lqn.calltype[cidx] != CallType::ASYNC)
5535 continue;
5536 const std::size_t eidx = lqn.callpair_dst[cidx];
5537 callservt[cidx] = T(lqn.callproc_mean[cidx] * ph_hostmean[eidx]);
5538 callresidt[cidx] = callservt[cidx];
5539 }
5540 ph_compose_entry_laws();
5541
5542 if (flat) {
5543 // ONE subnetwork holding every processor and every called task
5544 build_ph_flat_layer();
5545 tput.assign(N + 1, Tzero());
5546 util.assign(N + 1, Tzero());
5547 thinkt.assign(N + 1, Tzero());
5548 update_layers_ph(0);
5549 for (qn::Layer<T>& e : ensemble) e.refresh_chains();
5550 return;
5551 }
5552
5553 std::vector<qn::Layer<T>> raw(NT() + 1);
5554 std::vector<bool> present(NT() + 1, false);
5555
5556 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx) {
5557 if (ignore[hidx]) continue;
5558 std::vector<std::size_t> callers;
5559 for (std::size_t tidx : lqn.tasksof[hidx]) {
5560 if (ignore[tidx]) continue;
5561 if (lqn.isref[tidx]) { callers.push_back(tidx); continue; }
5562 for (std::size_t eidx : lqn.entriesof[tidx])
5563 if (ph_any_caller_of(eidx) || lqn.has_arrival[eidx]) {
5564 callers.push_back(tidx);
5565 break;
5566 }
5567 }
5568 if (callers.empty()) continue;
5569 build_layer_ph(raw[hidx], hidx, callers, true);
5570 present[hidx] = true;
5571 }
5572 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5573 const std::size_t tidx = lqn.tshift + t;
5574 if (ignore[tidx] || lqn.isref[tidx]) continue;
5575 std::vector<std::size_t> callers;
5576 for (std::size_t ct = 1; ct <= lqn.ntasks; ++ct) {
5577 const std::size_t c = lqn.tshift + ct;
5578 if (c == tidx || ignore[c]) continue;
5579 for (std::size_t e : lqn.entriesof[tidx])
5580 if (lqn.issynccaller.get(c, e)) { callers.push_back(c); break; }
5581 }
5582 if (callers.empty() && ph_async_calls_into(tidx).empty()) continue;
5583 build_layer_ph(raw[tidx], tidx, callers, false);
5584 present[tidx] = true;
5585 }
5586
5587 idxhash.assign(lqn.nidx + 1, -1);
5588 long next = 0;
5589 for (std::size_t i = 1; i <= NT(); ++i)
5590 if (present[i]) {
5591 idxhash[i] = next++;
5592 ensemble.push_back(std::move(raw[i]));
5593 }
5594 layer_init_sol.assign(ensemble.size(), Matrix<T>());
5595 build_fork_views();
5596
5597 // install the initial laws, so that iteration 1 sees the seeded demands
5598 // rather than the placeholders the stations were created with
5599 tput.assign(N + 1, Tzero());
5600 util.assign(N + 1, Tzero());
5601 thinkt.assign(N + 1, Tzero());
5602 update_layers_ph(0);
5603 // update_layers_ph only rebuilds rt; the cached rates still hold the build-time
5604 // placeholders (an async Source seeded Immediate, rate 1e8, saturates iteration 1)
5605 // until post() refreshes them, so refresh here as MATLAB's setService invalidation does
5606 for (qn::Layer<T>& e : ensemble) e.refresh_chains();
5607 }
5608
5609 /**
5610 * Build the ONE layer of method "flat.ph": a client delay plus a station for
5611 * every processor and every called task.
5612 *
5613 * A caller task is one closed class, and it visits each server it uses ONCE
5614 * per invocation, carrying there the composed law of the demand it places on
5615 * that server -- the same law method "srvn.ph" installs in the server's own
5616 * layer. What changes is that the servers now contend inside one network
5617 * instead of seeing each other through surrogate delays, so the client delay
5618 * keeps only the think times and whatever of the cycle this model does not
5619 * hold. That is the whole difference between the two encodings of the PH
5620 * composition, and it is why the reconstruction passes are shared verbatim.
5621 */
5622 void build_ph_flat_layer() {
5623 const std::vector<std::size_t> servers = ph_flat_server_set();
5624 const std::size_t nsrv = servers.size();
5625
5626 qn::Layer<T> m;
5627 m.name = "FlatPH";
5628 m.clientIdx = m.add_station(qn::Station<T>{
5629 "Clients", NodeType::Delay, SchedStrategy::INF,
5630 std::numeric_limits<double>::infinity(), false, 0});
5631 const std::size_t clientNode = m.node_of_station(m.clientIdx);
5632 m.server_idx_of.assign(lqn.nidx + 1, 0);
5633
5634 std::vector<std::size_t> station_of(lqn.nidx + 1, 0);
5635 std::vector<std::size_t> serverNode(nsrv, 0);
5636 std::vector<std::size_t> srvStation(nsrv, 0);
5637 for (std::size_t si = 0; si < nsrv; ++si) {
5638 const std::size_t idx = servers[si];
5639 const bool ishost = idx <= lqn.nhosts;
5640 qn::Station<T> st;
5641 st.name = lqn.hashnames[idx];
5642 st.nodetype = lqn.sched[idx] == SchedStrategy::INF ? NodeType::Delay : NodeType::Queue;
5643 st.sched = lqn.sched[idx];
5644 st.nservers = lqn.sched[idx] == SchedStrategy::INF
5645 ? std::numeric_limits<double>::infinity()
5646 : lqn.maxmult[idx];
5647 st.attr_ishost = ishost;
5648 st.attr_idx = idx;
5649 const std::size_t s = m.add_station(st);
5650 srvStation[si] = s;
5651 station_of[idx] = s;
5652 serverNode[si] = m.node_of_station(s);
5653 m.server_idx_of[idx] = s;
5654 if (ishost)
5655 m.host_stations.push_back(s);
5656 else
5657 m.task_stations.push_back(s);
5658 }
5659 // the scalar fallback of the station lookup, which no served element reaches
5660 m.serverIdx = station_of[servers[0]];
5661 m.flat = true;
5662
5663 // Callers of each server, and the union of them, which becomes the class set
5664 std::vector<std::vector<std::size_t>> callers_of(lqn.nidx + 1);
5665 std::vector<std::size_t> all_callers;
5666 for (std::size_t si = 0; si < nsrv; ++si) {
5667 const std::size_t idx = servers[si];
5668 std::vector<std::size_t> cs;
5669 if (idx <= lqn.nhosts) {
5670 for (std::size_t tidx : lqn.tasksof[idx]) {
5671 if (ignore[tidx]) continue;
5672 if (lqn.isref[tidx]) { cs.push_back(tidx); continue; }
5673 for (std::size_t eidx : lqn.entriesof[tidx])
5674 if (ph_any_caller_of(eidx) || lqn.has_arrival[eidx]) {
5675 cs.push_back(tidx);
5676 break;
5677 }
5678 }
5679 } else {
5680 for (std::size_t ct = 1; ct <= lqn.ntasks; ++ct) {
5681 const std::size_t c = lqn.tshift + ct;
5682 if (c == idx || ignore[c]) continue;
5683 for (std::size_t e : lqn.entriesof[idx])
5684 if (lqn.issynccaller.get(c, e)) { cs.push_back(c); break; }
5685 }
5686 }
5687 callers_of[idx] = cs;
5688 for (std::size_t c : cs)
5689 if (std::find(all_callers.begin(), all_callers.end(), c) == all_callers.end())
5690 all_callers.push_back(c);
5691 }
5692 std::sort(all_callers.begin(), all_callers.end());
5693
5694 // One closed class per caller task
5695 std::vector<std::size_t> class_of_caller(lqn.nidx + 1, 0);
5696 double npop = 0.0;
5697 for (std::size_t c : all_callers) {
5698 // ph_flat_server_set has refused every replicated element, so the
5699 // per-replica reduction the srvn builder makes is the identity here
5700 double nj = lqn.maxmult[c];
5701 if (std::isinf(nj)) {
5702 double sacc = 0.0;
5703 for (std::size_t k = 1; k <= NT(); ++k)
5704 if (lqn.taskgraph.get(k, c) != Tzero()) sacc += lqn.maxmult[k];
5705 nj = sacc;
5706 if (std::isinf(nj) || nj == 0.0) {
5707 double s2 = 0.0;
5708 for (std::size_t k = 1; k <= NT(); ++k)
5709 if (std::isfinite(lqn.maxmult[k])) s2 += lqn.maxmult[k];
5710 nj = std::min(s2, 1000.0);
5711 }
5712 }
5713 qn::JobClass jc;
5714 jc.name = lqn.hashnames[c];
5715 jc.type = JobClassType::CLOSED;
5716 jc.population = nj;
5717 jc.refstat = m.clientIdx;
5718 jc.is_ref_class = true;
5719 jc.attr_kind = int(LqnElement::TASK);
5720 jc.attr_idx = c;
5721 const std::size_t k = m.add_class(jc);
5722 class_of_caller[c] = k;
5723 m.attr_tasks.emplace_back(k, c);
5724 npop += nj;
5725 const double zt = dbl(ref_think_time(c));
5726 m.set_service(m.clientIdx, k,
5727 Distrib<T>::exp_mean(num_traits<T>::from_double(
5728 std::max(zt, GlobalConstants::FineTol))));
5729 // A station this caller never reaches must say so with a DISABLED law,
5730 // not with a tiny placeholder. An FCFS station carries ONE service law
5731 // across its classes, so a placeholder is not inert there: it is mixed
5732 // into the multiserver correction and invents waiting where there is
5733 // none. Under "srvn.ph" the question never arises, since every class of
5734 // a layer visits that layer's single server.
5735 for (std::size_t si = 0; si < nsrv; ++si)
5736 m.set_service(srvStation[si], k, Distrib<T>::disabled_dist());
5737 for (std::size_t si = 0; si < nsrv; ++si) {
5738 const std::size_t idx = servers[si];
5739 const std::vector<std::size_t>& cs = callers_of[idx];
5740 if (std::find(cs.begin(), cs.end(), c) == cs.end()) continue;
5741 m.set_service(srvStation[si], k,
5743 num_traits<T>::from_double(GlobalConstants::FineTol)));
5744 njobs(c, idx) = nj;
5745 thinkt_map.push_back({idx, c, m.clientIdx, k});
5746 servt_map.push_back({idx, c, station_of[idx], k});
5747 }
5748 }
5749
5750 // Open classes: entry arrivals on a processor station, async calls on a task one
5751 std::vector<std::vector<std::pair<std::size_t, long>>> open_of(lqn.nidx + 1);
5752 std::size_t sourceStation = 0, sinkNode = 0;
5753 for (std::size_t si = 0; si < nsrv; ++si) {
5754 const std::size_t hidx = servers[si];
5755 if (hidx > lqn.nhosts) continue;
5756 for (std::size_t c : callers_of[hidx]) {
5757 // A task no other task calls has no task station, so the think-time
5758 // closure never gives its caller class a surrogate delay: the class
5759 // cycles against an Immediate one and an open stream on top of it
5760 // doubles the load. The chain is the representation that honours the
5761 // thread pool, so it is kept and closed on the arrival rate instead.
5762 if (ph_open_arrival_only(c)) continue;
5763 for (std::size_t eidx : lqn.entriesof[c]) {
5764 if (!lqn.has_arrival[eidx]) continue;
5765 if (sourceStation == 0) {
5766 sourceStation = m.add_station(qn::Station<T>{
5767 "Source", NodeType::Source, SchedStrategy::EXT,
5768 std::numeric_limits<double>::infinity(), false, 0});
5769 m.sourceIdx = sourceStation;
5770 sinkNode = m.add_node("Sink", NodeType::Sink, false);
5771 m.sinkNode = sinkNode;
5772 }
5773 qn::JobClass oc;
5774 oc.name = lqn.hashnames[eidx] + ".Open";
5775 oc.type = JobClassType::OPEN;
5776 oc.population = std::numeric_limits<double>::infinity();
5777 oc.refstat = sourceStation;
5778 oc.attr_kind = int(LqnElement::ENTRY);
5779 oc.attr_idx = eidx;
5780 const std::size_t k = m.add_class(oc);
5781 m.set_service(sourceStation, k, lqn.arrival[eidx]);
5782 // disabled, not a placeholder, at every station this stream misses
5783 for (std::size_t s2 = 0; s2 < nsrv; ++s2)
5784 m.set_service(srvStation[s2], k, Distrib<T>::disabled_dist());
5785 const double hm = std::max(dbl(ph_hostmean[eidx]), GlobalConstants::FineTol);
5786 m.set_service(srvStation[si], k,
5787 Distrib<T>::exp_mean(num_traits<T>::from_double(hm)));
5788 open_of[hidx].emplace_back(k, long(eidx));
5789 m.attr_entries.emplace_back(k, eidx);
5790 }
5791 }
5792 }
5793 for (std::size_t si = 0; si < nsrv; ++si) {
5794 const std::size_t tidx = servers[si];
5795 if (tidx <= lqn.nhosts) continue;
5796 for (std::size_t cidx : ph_async_calls_into(tidx)) {
5797 if (sourceStation == 0) {
5798 sourceStation = m.add_station(qn::Station<T>{
5799 "Source", NodeType::Source, SchedStrategy::EXT,
5800 std::numeric_limits<double>::infinity(), false, 0});
5801 m.sourceIdx = sourceStation;
5802 sinkNode = m.add_node("Sink", NodeType::Sink, false);
5803 m.sinkNode = sinkNode;
5804 }
5805 const std::size_t eidx = lqn.callpair_dst[cidx];
5806 qn::JobClass oc;
5807 oc.name = lqn.callhashnames[cidx];
5808 oc.type = JobClassType::OPEN;
5809 oc.population = std::numeric_limits<double>::infinity();
5810 oc.refstat = sourceStation;
5811 oc.attr_kind = int(LqnElement::CALL);
5812 oc.attr_idx = cidx;
5813 const std::size_t k = m.add_class(oc);
5814 m.set_service(sourceStation, k, Distrib<T>::immediate());
5815 // disabled, not a placeholder, at every station this stream misses
5816 for (std::size_t s2 = 0; s2 < nsrv; ++s2)
5817 m.set_service(srvStation[s2], k, Distrib<T>::disabled_dist());
5818 const double em = std::max(dbl(ph_entrymean[eidx]), GlobalConstants::FineTol);
5819 m.set_service(srvStation[si], k,
5820 Distrib<T>::exp_mean(num_traits<T>::from_double(em)));
5821 open_of[tidx].emplace_back(k, -long(cidx));
5822 m.attr_calls.push_back({k, cidx, lqn.callpair_src[cidx], eidx});
5823 arv_call_map.push_back({tidx, cidx, sourceStation, k});
5824 call_map.push_back({tidx, cidx, station_of[tidx], k});
5825 }
5826 }
5827
5828 // Routing: one visit per server the caller uses, in server order. The
5829 // number of calls is carried by the service law, not by a visit ratio, so
5830 // no arc ever moves.
5831 for (std::size_t c : all_callers) {
5832 const std::size_t k = class_of_caller[c];
5833 std::size_t prev = clientNode;
5834 bool visited = false;
5835 for (std::size_t si = 0; si < nsrv; ++si) {
5836 const std::vector<std::size_t>& cs = callers_of[servers[si]];
5837 if (std::find(cs.begin(), cs.end(), c) == cs.end()) continue;
5838 m.set_route(k, k, prev, serverNode[si], Tone());
5839 prev = serverNode[si];
5840 visited = true;
5841 }
5842 if (visited) m.set_route(k, k, prev, clientNode, Tone());
5843 }
5844 if (sourceStation != 0) {
5845 const std::size_t srcNode = m.node_of_station(sourceStation);
5846 for (std::size_t si = 0; si < nsrv; ++si)
5847 for (const auto& oa : open_of[servers[si]]) {
5848 m.set_route(oa.first, oa.first, srcNode, serverNode[si], Tone());
5849 m.set_route(oa.first, oa.first, serverNode[si], sinkNode, Tone());
5850 }
5851 }
5852 apply_host_task_priorities(m, servers);
5853 m.refresh_chains();
5854
5855 idxhash.assign(lqn.nidx + 1, -1);
5856 for (std::size_t idx : servers) idxhash[idx] = 0;
5857 ensemble.clear();
5858 ensemble.push_back(std::move(m));
5859 layer_init_sol.assign(ensemble.size(), Matrix<T>());
5860 build_fork_views();
5861
5862 const std::size_t nclasses = ensemble[0].classes.size();
5863 for (std::size_t idx : servers) {
5864 PHLayer L;
5865 L.idx = idx;
5866 L.ishost = idx <= lqn.nhosts;
5867 L.callers = callers_of[idx];
5868 L.class_of_caller = class_of_caller;
5869 L.nreplicas = 1;
5870 L.qstations.assign(1, station_of[idx]);
5871 L.svcmean_by_class.assign(nclasses + 1, Tzero());
5872 L.open_arrivals = open_of[idx];
5873 L.npop = npop < 1.0 ? 1.0 : npop;
5874 ph_layers[idx] = L;
5875 ph_has_layer[idx] = true;
5876 }
5877 }
5878
5879 /**
5880 * Processors and called tasks that become stations of the flat layer.
5881 *
5882 * The set is the elements the srvn builder would have given a layer of their
5883 * own, so "flat.ph" and "srvn.ph" place the SAME stations and differ only in
5884 * how many networks hold them. The refusals are those of flat_server_set,
5885 * since they are properties of the squashing and not of the encoding: each of
5886 * these carries per-layer state that one submodel cannot hold.
5887 */
5888 std::vector<std::size_t> ph_flat_server_set() const {
5889 for (std::size_t i = 1; i <= NT(); ++i) {
5890 if (lqn.repl[i] > 1.0)
5891 throw UnsupportedError(ErrorKind{"Replication"},
5892 "method='flat.ph' does not support replicated processors or tasks, whose "
5893 "replicas need a submodel each. Use method='srvn.ph'.");
5894 if (lqn.iscache[i])
5895 throw UnsupportedError(ErrorKind{"CacheTask"},
5896 "method='flat.ph' does not support cache tasks. Use method='default'.");
5897 if (lqn.hassetup[i])
5898 throw UnsupportedError(ErrorKind{"SetupDelayOff"},
5899 "method='flat.ph' does not support setup tasks, whose powered-down threads "
5900 "are per-layer state. Use method='srvn.ph'.");
5901 }
5902 std::vector<std::size_t> servers;
5903 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx) {
5904 if (ignore[hidx] || lqn.tasksof[hidx].empty()) continue;
5905 bool any = false;
5906 for (std::size_t tidx : lqn.tasksof[hidx]) {
5907 if (ignore[tidx]) continue;
5908 if (lqn.isref[tidx]) { any = true; break; }
5909 for (std::size_t eidx : lqn.entriesof[tidx])
5910 if (ph_any_caller_of(eidx) || lqn.has_arrival[eidx]) { any = true; break; }
5911 if (any) break;
5912 }
5913 if (any) servers.push_back(hidx);
5914 }
5915 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
5916 const std::size_t tidx = lqn.tshift + t;
5917 if (ignore[tidx] || lqn.isref[tidx]) continue;
5918 bool has_caller = false;
5919 for (std::size_t ct = 1; ct <= lqn.ntasks && !has_caller; ++ct) {
5920 const std::size_t c = lqn.tshift + ct;
5921 if (c == tidx || ignore[c]) continue;
5922 for (std::size_t e : lqn.entriesof[tidx])
5923 if (lqn.issynccaller.get(c, e)) { has_caller = true; break; }
5924 }
5925 if (!has_caller && ph_async_calls_into(tidx).empty()) continue;
5926 servers.push_back(tidx);
5927 }
5928 if (servers.empty())
5929 throw InputError(
5930 "method='flat.ph' found no server: the model has no processor with tasks.");
5931 return servers;
5932 }
5933
5934 /** Build the two-station layer of server element IDX. */
5935 void build_layer_ph(qn::Layer<T>& m, std::size_t idx,
5936 const std::vector<std::size_t>& callers, bool ishost) {
5937 m.name = lqn.hashnames[idx];
5938
5939 // Replicas of the server station, with the same fan-out reduction as the
5940 // default builder: a caller that reaches every replica sees one
5941 // representative
5942 const double rawrepl = lqn.repl[idx];
5943 std::size_t nreplicas = 1;
5944 if (rawrepl > 1.0 && !callers.empty()) {
5945 bool reduce = true;
5946 if (ishost) {
5947 for (std::size_t c : callers)
5948 if (lqn.repl[c] != rawrepl) reduce = false;
5949 } else {
5950 for (std::size_t c : callers)
5951 if (lqn.fanout_at(c, idx) < rawrepl) reduce = false;
5952 }
5953 nreplicas = reduce ? 1 : static_cast<std::size_t>(std::llround(rawrepl));
5954 if (reduce && !ishost) single_replica_tasks.insert(idx);
5955 }
5956 const bool reduce_fanout = (nreplicas == 1 && rawrepl > 1.0 && !callers.empty());
5957
5958 m.clientIdx = m.add_station(qn::Station<T>{
5959 "Clients", NodeType::Delay, SchedStrategy::INF,
5960 std::numeric_limits<double>::infinity(), false, 0});
5961 const std::size_t clientNode = m.node_of_station(m.clientIdx);
5962 m.serverIdx = m.clientIdx + 1;
5963 PHLayer L;
5964 L.idx = idx;
5965 L.ishost = ishost;
5966 L.callers = callers;
5967 L.nreplicas = nreplicas;
5968 L.class_of_caller.assign(lqn.nidx + 1, 0);
5969 std::vector<std::size_t> serverNode(nreplicas);
5970 for (std::size_t r = 0; r < nreplicas; ++r) {
5971 qn::Station<T> st;
5972 st.name = r == 0 ? lqn.hashnames[idx] : lqn.hashnames[idx] + "." + std::to_string(r + 1);
5973 st.nodetype = lqn.sched[idx] == SchedStrategy::INF ? NodeType::Delay : NodeType::Queue;
5974 st.sched = lqn.sched[idx];
5975 st.nservers = lqn.sched[idx] == SchedStrategy::INF
5976 ? std::numeric_limits<double>::infinity()
5977 : lqn.maxmult[idx];
5978 st.attr_ishost = ishost;
5979 st.attr_idx = idx;
5980 const std::size_t s = m.add_station(st);
5981 L.qstations.push_back(s);
5982 serverNode[r] = m.node_of_station(s);
5983 }
5984
5985 // --- closed class per caller task
5986 for (std::size_t c : callers) {
5987 double nj = njobs(c, idx);
5988 if (nj == 0.0) {
5989 const bool caller_single_replica =
5990 reduce_fanout || single_replica_tasks.count(c) > 0;
5991 nj = caller_single_replica ? lqn.maxmult[c] : lqn.maxmult[c] * lqn.repl[c];
5992 if (std::isinf(nj)) {
5993 double s = 0.0;
5994 for (std::size_t k = 1; k <= NT(); ++k)
5995 if (lqn.taskgraph.get(k, c) != Tzero()) s += lqn.maxmult[k];
5996 nj = s;
5997 if (std::isinf(nj) || nj == 0.0) {
5998 double s2 = 0.0;
5999 for (std::size_t k = 1; k <= NT(); ++k)
6000 if (std::isfinite(lqn.maxmult[k])) s2 += lqn.maxmult[k] * lqn.repl[k];
6001 nj = std::min(s2, 1000.0);
6002 }
6003 }
6004 njobs(c, idx) = nj;
6005 }
6006 qn::JobClass jc;
6007 jc.name = lqn.hashnames[c];
6008 jc.type = JobClassType::CLOSED;
6009 jc.population = nj;
6010 jc.refstat = m.clientIdx;
6011 jc.is_ref_class = true;
6012 jc.attr_kind = int(LqnElement::TASK);
6013 jc.attr_idx = c;
6014 const std::size_t k = m.add_class(jc);
6015 L.class_of_caller[c] = k;
6016 m.attr_tasks.emplace_back(k, c);
6017 const double zt = dbl(ref_think_time(c));
6018 m.set_service(m.clientIdx, k,
6019 Distrib<T>::exp_mean(num_traits<T>::from_double(
6020 std::max(zt, GlobalConstants::FineTol))));
6021 for (std::size_t s : L.qstations)
6022 m.set_service(s, k, Distrib<T>::exp_mean(
6023 num_traits<T>::from_double(GlobalConstants::FineTol)));
6024 // every layer must be refreshed after a law change: post() resets the
6025 // layers named by the think-time map
6026 thinkt_map.push_back({idx, c, m.clientIdx, k});
6027 servt_map.push_back({idx, c, L.qstations[0], k});
6028 }
6029
6030 // --- open classes: entry arrivals on a host layer, async calls on a task layer
6031 std::size_t sourceStation = 0, sinkNode = 0;
6032 if (ishost) {
6033 for (std::size_t c : callers) {
6034 // A task no other task calls has no task layer, so
6035 // update_think_times_ph never gives its caller class a surrogate
6036 // delay: the class cycles against an Immediate one and an open
6037 // stream on top of it doubles the load. The chain is the
6038 // representation that honours the thread pool, so it is kept and
6039 // closed on the arrival rate instead.
6040 if (ph_open_arrival_only(c)) continue;
6041 for (std::size_t eidx : lqn.entriesof[c]) {
6042 if (!lqn.has_arrival[eidx]) continue;
6043 if (sourceStation == 0) {
6044 sourceStation = m.add_station(qn::Station<T>{
6045 "Source", NodeType::Source, SchedStrategy::EXT,
6046 std::numeric_limits<double>::infinity(), false, 0});
6047 m.sourceIdx = sourceStation;
6048 sinkNode = m.add_node("Sink", NodeType::Sink, false);
6049 m.sinkNode = sinkNode;
6050 }
6051 qn::JobClass oc;
6052 oc.name = lqn.hashnames[eidx] + ".Open";
6053 oc.type = JobClassType::OPEN;
6054 oc.population = std::numeric_limits<double>::infinity();
6055 oc.refstat = sourceStation;
6056 oc.attr_kind = int(LqnElement::ENTRY);
6057 oc.attr_idx = eidx;
6058 const std::size_t k = m.add_class(oc);
6059 m.set_service(sourceStation, k, lqn.arrival[eidx]);
6060 for (std::size_t s : L.qstations) {
6061 const double hm = std::max(dbl(ph_hostmean[eidx]), GlobalConstants::FineTol);
6062 m.set_service(s, k, Distrib<T>::exp_mean(num_traits<T>::from_double(hm)));
6063 }
6064 L.open_arrivals.emplace_back(k, long(eidx));
6065 m.attr_entries.emplace_back(k, eidx);
6066 }
6067 }
6068 } else {
6069 for (std::size_t cidx : ph_async_calls_into(idx)) {
6070 if (sourceStation == 0) {
6071 sourceStation = m.add_station(qn::Station<T>{
6072 "Source", NodeType::Source, SchedStrategy::EXT,
6073 std::numeric_limits<double>::infinity(), false, 0});
6074 m.sourceIdx = sourceStation;
6075 sinkNode = m.add_node("Sink", NodeType::Sink, false);
6076 m.sinkNode = sinkNode;
6077 }
6078 const std::size_t eidx = lqn.callpair_dst[cidx];
6079 qn::JobClass oc;
6080 oc.name = lqn.callhashnames[cidx];
6081 oc.type = JobClassType::OPEN;
6082 oc.population = std::numeric_limits<double>::infinity();
6083 oc.refstat = sourceStation;
6084 oc.attr_kind = int(LqnElement::CALL);
6085 oc.attr_idx = cidx;
6086 const std::size_t k = m.add_class(oc);
6087 m.set_service(sourceStation, k, Distrib<T>::immediate());
6088 for (std::size_t s : L.qstations) {
6089 const double em = std::max(dbl(ph_entrymean[eidx]), GlobalConstants::FineTol);
6090 m.set_service(s, k, Distrib<T>::exp_mean(num_traits<T>::from_double(em)));
6091 }
6092 L.open_arrivals.emplace_back(k, -long(cidx));
6093 m.attr_calls.push_back({k, cidx, lqn.callpair_src[cidx], eidx});
6094 arv_call_map.push_back({idx, cidx, sourceStation, k});
6095 call_map.push_back({idx, cidx, L.qstations[0], k});
6096 }
6097 }
6098
6099 // Routing: one visit to the server per client cycle. The number of calls
6100 // is carried by the service law, not by a visit ratio, so no arc changes
6101 const T share = num_traits<T>::from_double(1.0 / double(nreplicas));
6102 for (std::size_t c : callers) {
6103 const std::size_t k = L.class_of_caller[c];
6104 for (std::size_t r = 0; r < nreplicas; ++r) {
6105 m.set_route(k, k, clientNode, serverNode[r], share);
6106 m.set_route(k, k, serverNode[r], clientNode, Tone());
6107 }
6108 }
6109 for (const auto& oa : L.open_arrivals) {
6110 const std::size_t k = oa.first;
6111 const std::size_t srcNode = m.node_of_station(sourceStation);
6112 for (std::size_t r = 0; r < nreplicas; ++r) {
6113 m.set_route(k, k, srcNode, serverNode[r], share);
6114 m.set_route(k, k, serverNode[r], sinkNode, Tone());
6115 }
6116 }
6117 L.svcmean_by_class.assign(m.classes.size() + 1, Tzero());
6118 double np = 0.0;
6119 for (std::size_t c : callers) {
6120 const double v = njobs(c, idx);
6121 if (std::isfinite(v) && v > 0.0) np += v;
6122 }
6123 L.npop = np < 1.0 ? 1.0 : np;
6124 apply_host_task_priorities(m, std::vector<std::size_t>{idx});
6125 m.refresh_chains();
6126 ph_layers[idx] = L;
6127 ph_has_layer[idx] = true;
6128 }
6129
6130 /** Law of the demand caller C places on the server of layer IDX per invocation. */
6131 workflow::PhLaw<T> ph_service_law(std::size_t idx, bool ishost, std::size_t c) const {
6132 if (ishost) {
6133 // mixture over the entries of C, weighted by their share of its requests
6134 std::vector<workflow::PhLaw<T>> laws;
6135 std::vector<T> probs;
6136 double tot = 0.0;
6137 for (std::size_t eidx : lqn.entriesof[c]) {
6138 if (ph_hostlaw[eidx].S.rows() == 0 || !(dbl(ph_share[eidx]) > 0.0)) continue;
6139 laws.push_back(ph_hostlaw[eidx]);
6140 probs.push_back(ph_share[eidx]);
6141 tot += dbl(ph_share[eidx]);
6142 }
6143 if (laws.empty()) return ph_immediate_law();
6144 for (T& p : probs) p = T(p / num_traits<T>::from_double(tot));
6145 return workflow::Workflow<T>::compose_mixture(laws, probs);
6146 }
6147 // task layer: the total demand is the sum, over the entries of the
6148 // server, of a geometric compound of the entry law of mean equal to the
6149 // number of calls
6150 bool started = false;
6151 workflow::PhLaw<T> out;
6152 for (std::size_t eidx : lqn.entriesof[idx]) {
6153 const T n = ph_ncalls(c, eidx);
6154 if (dbl(n) <= GlobalConstants::FineTol || ph_entrylaw[eidx].S.rows() == 0) continue;
6155 const workflow::PhLaw<T> lp =
6157 out = started ? workflow::Workflow<T>::compose_serial(out, lp) : lp;
6158 started = true;
6159 }
6160 if (!started) return ph_immediate_law();
6161 return out;
6162 }
6163
6164 /**
6165 * Mean time a thread of caller C spends away from the server of layer IDX per
6166 * invocation: idle, plus whatever of its cycle the layer does not hold.
6167 */
6168 T ph_delay_mean(std::size_t idx, std::size_t c) const {
6169 // ONE closure for both layerings. Under "srvn.ph" the model holds a single
6170 // server, so a host layer charges the whole call burst to the delay and a
6171 // task layer charges the caller's processor plus every other callee. Under
6172 // "flat.ph" the model holds every server, and only the think times are
6173 // left. Every term is SUMMED in rather than obtained by subtracting from a
6174 // total: that subtraction cancels catastrophically once a call time is
6175 // large, the think time falls below the ULP of the call time, and the layer
6176 // then sees a client delay of zero, saturates, and the fixed point runs
6177 // away.
6178 T z = T(thinkt[c] + ref_think_time(c));
6179 if (!std::isfinite(dbl(z)) || dbl(z) < 0.0) z = Tzero();
6180 z = T(z + ph_actthinkt[c]);
6181 const std::size_t hidx = lqn.parent[c];
6182 // A station held here charges its demand SERIAL, while the cycle holds it only
6183 // at the overlap ratio of the AND forks it sits in, so the saving comes back out
6184 // of the delay. Every layer then cycles in z + the composed entry mean, and flow
6185 // balances across the call (lqn_workflows, _kb/06-solver-catalog.md).
6186 if (!ph_served_here(idx, hidx)) z = T(z + ph_procresid[c]);
6187 else z = T(z - ph_procsave[c]);
6188 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
6189 const std::size_t tidx = lqn.tshift + t;
6190 if (!ph_served_here(idx, tidx)) z = T(z + ph_calltime(c, tidx));
6191 else z = T(z - ph_callsave(c, tidx));
6192 }
6193 if (!std::isfinite(dbl(z)) || dbl(z) < 0.0)
6194 z = num_traits<T>::from_double(GlobalConstants::FineTol);
6195 return z;
6196 }
6197
6198 /**
6199 * True when LQN element ELEM is a station of the same model that holds server
6200 * IDX. Under "srvn.ph" that is ELEM == IDX, since each server has a layer of
6201 * its own; under "flat.ph" it is every server of the one network.
6202 */
6203 bool ph_served_here(std::size_t idx, std::size_t elem) const {
6204 if (elem < 1 || elem >= idxhash.size() || idx < 1 || idx >= idxhash.size())
6205 return false;
6206 return idxhash[elem] >= 0 && idxhash[elem] == idxhash[idx];
6207 }
6208
6209 /**
6210 * Push the composed laws into the layers.
6211 *
6212 * A layer of this method carries no routing that depends on the iterate: the
6213 * number of calls a caller makes is folded into its service law rather than
6214 * into a visit ratio, so only two laws move per (layer, class) -- the
6215 * phase-type service law at the server and the mean of the surrogate delay
6216 * at the client.
6217 */
6218 void update_layers_ph(int) {
6219 for (std::size_t idx = 1; idx <= NT(); ++idx) {
6220 if (idxhash[idx] < 0 || !ph_has_layer[idx]) continue;
6221 PHLayer& L = ph_layers[idx];
6222 qn::Layer<T>& m = ensemble[std::size_t(idxhash[idx])];
6223 for (std::size_t c : L.callers) {
6224 const std::size_t k = L.class_of_caller[c];
6225 const workflow::PhLaw<T> sl = ph_service_law(idx, L.ishost, c);
6226 L.svcmean_by_class[k] = api::lqn::ph_moments(sl.alpha, sl.S).first;
6227 const Distrib<T> law = ph_station_law(sl);
6228 for (std::size_t s : L.qstations) m.set_service(s, k, law);
6229 const double zd = std::max(dbl(ph_delay_mean(idx, c)),
6231 m.set_service(m.clientIdx, k,
6232 Distrib<T>::exp_mean(num_traits<T>::from_double(zd)));
6233 }
6234 for (const auto& oa : L.open_arrivals) {
6235 const std::size_t k = oa.first;
6236 if (oa.second > 0) {
6237 // entry arrival: the processor demand law of the entry is static
6238 L.svcmean_by_class[k] = ph_hostmean[std::size_t(oa.second)];
6239 continue;
6240 }
6241 const std::size_t cidx = std::size_t(-oa.second);
6242 const std::size_t eidx = lqn.callpair_dst[cidx];
6243 L.svcmean_by_class[k] = ph_entrymean[eidx];
6244 const Distrib<T> law = ph_station_law(ph_entrylaw[eidx]);
6245 for (std::size_t s : L.qstations) m.set_service(s, k, law);
6246 const std::size_t aidx = lqn.callpair_src[cidx];
6247 double rate = dbl(tput[aidx]) * dbl(lqn.callproc_mean[cidx]);
6248 if (!std::isfinite(rate) || rate <= GlobalConstants::FineTol)
6250 m.set_service(m.sourceIdx, k,
6251 Distrib<T>::exp_rate(num_traits<T>::from_double(rate)));
6252 }
6253 m.refresh_rt();
6254 }
6255 }
6256
6257 /**
6258 * Residence time per visit, by Little from the queue length rather than from
6259 * the reported RN. A layer that saturates can come back from AMVA with an RN
6260 * no closed model can produce, and a reconstruction that trusts it feeds the
6261 * impossible value straight back into the call response times.
6262 */
6263 static T ph_residence(const T& Q, const T& X, const T& RN) {
6264 const double q = num_traits<T>::to_double(Q), x = num_traits<T>::to_double(X);
6265 if (std::isfinite(q) && q >= 0.0 && std::isfinite(x) && x > GlobalConstants::FineTol)
6266 return T(Q / X);
6267 return RN;
6268 }
6269
6270 /**
6271 * Ratio of a residence time to the mean of the law it was measured against,
6272 * bounded above by the layer population: a job can wait behind at most every
6273 * other job in a closed layer.
6274 */
6275 static T ph_inflation_of(const T& R, const T& S, double npop) {
6276 double f = 1.0;
6277 const double s = num_traits<T>::to_double(S), r = num_traits<T>::to_double(R);
6278 if (s > GlobalConstants::FineTol && std::isfinite(r) && r > 0.0) f = r / s;
6279 if (!std::isfinite(f) || f < 1.0) f = 1.0;
6280 if (std::isfinite(npop) && npop >= 1.0 && f > npop) f = npop;
6281 return num_traits<T>::from_double(f);
6282 }
6283
6284 /** Total closed population of a layer, i.e. how many jobs a job can queue behind. */
6285 double ph_layer_pop(const PHLayer& L, std::size_t idx) const {
6286 // Under "flat.ph" this is every caller of the single network and not only
6287 // the callers of this one station, so it is taken from the layer record.
6288 if (L.npop >= 1.0) return L.npop;
6289 double n = 0.0;
6290 for (std::size_t c : L.callers) {
6291 const double v = njobs(c, idx);
6292 if (std::isfinite(v) && v > 0.0) n += v;
6293 }
6294 return n < 1.0 ? 1.0 : n;
6295 }
6296
6297 /**
6298 * Reconstruct the LQN metrics.
6299 *
6300 * A layer of this method reports one row per caller task, not one per entry,
6301 * activity and call, so the per-element quantities the rest of SolverLN reads
6302 * -- servt, residt, callservt, callresidt, tput -- are recovered analytically
6303 * from the series-parallel weights of the entry workflows.
6304 *
6305 * The split is conservative by construction. A station reports a residence
6306 * time R per visit against a service law of mean S, so the queueing inflation
6307 * R/S is attributed to every leaf of that visit in proportion to its own
6308 * mean: the pieces sum back to R exactly.
6309 */
6310 void update_metrics_ph(int it) {
6311 const std::size_t N = lqn.nidx;
6312 servt.assign(N + 1, Tzero());
6313 residt.assign(N + 1, Tzero());
6314 callservt.assign(lqn.ncalls + 1, Tzero());
6315 callresidt.assign(lqn.ncalls + 1, Tzero());
6316
6317 std::vector<T> inflNum(N + 1, Tzero()), inflDen(N + 1, Tzero());
6318 std::vector<T> taskTput(N + 1, Tzero()), openTput(N + 1, Tzero());
6319
6320 // Host layers: the queueing inflation of the processor demand
6321 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx) {
6322 if (idxhash[hidx] < 0 || !ph_has_layer[hidx]) continue;
6323 const PHLayer& L = ph_layers[hidx];
6324 const LayerResult<T>& res = results.back()[std::size_t(idxhash[hidx])];
6325 const double npop = ph_layer_pop(L, hidx);
6326 for (std::size_t c : L.callers) {
6327 const std::size_t k = L.class_of_caller[c];
6328 T X = Tzero(), Q = Tzero();
6329 for (std::size_t s : L.qstations) {
6330 X = T(X + res.TN(s - 1, k - 1));
6331 Q = T(Q + res.QN(s - 1, k - 1));
6332 }
6333 const T R = ph_residence(Q, X, res.RN(L.qstations[0] - 1, k - 1));
6334 const T f = ph_inflation_of(R, L.svcmean_by_class[k], npop);
6335 if (!std::isfinite(dbl(X)) || dbl(X) < 0.0) X = Tzero();
6336 // TOTAL over the replicas. The processor layer of a replicated
6337 // element models ONE representative replica, so X is one replica's
6338 // rate and the element's own rate is REPL times it. The matching
6339 // per-replica quantity is ph_xdemand, which the think-time closure
6340 // divides down for the same reason.
6341 // A host with fewer replicas than C holds ALL of C's threads in the
6342 // one class, so X is already the total (BUG-110).
6343 double rc = lqn.repl[c];
6344 if (rc > 1.0 && njobs(c, hidx) >= lqn.maxmult[c] * rc) rc = 1.0;
6345 taskTput[c] = T(taskTput[c] + num_traits<T>::from_double(rc) * X);
6346 for (std::size_t eidx : lqn.entriesof[c]) {
6347 const T sh = dbl(ph_share[eidx]) > 0.0 ? ph_share[eidx] : Tzero();
6348 const T w = T(sh * X);
6349 inflNum[eidx] = T(inflNum[eidx] + w * f);
6350 inflDen[eidx] = T(inflDen[eidx] + w);
6351 }
6352 }
6353 for (const auto& oa : L.open_arrivals) {
6354 if (oa.second <= 0) continue; // an async call is served in the task layer
6355 const std::size_t k = oa.first;
6356 const std::size_t eidx = std::size_t(oa.second);
6357 T X = Tzero(), Q = Tzero();
6358 for (std::size_t s : L.qstations) {
6359 X = T(X + res.TN(s - 1, k - 1));
6360 Q = T(Q + res.QN(s - 1, k - 1));
6361 }
6362 if (!std::isfinite(dbl(X)) || dbl(X) <= 0.0) continue;
6363 const T f = ph_inflation_of(ph_residence(Q, X, res.RN(L.qstations[0] - 1, k - 1)),
6364 L.svcmean_by_class[k], npop);
6365 inflNum[eidx] = T(inflNum[eidx] + X * f);
6366 inflDen[eidx] = T(inflDen[eidx] + X);
6367 openTput[eidx] = T(openTput[eidx] + X);
6368 taskTput[lqn.parent[eidx]] = T(taskTput[lqn.parent[eidx]] + X);
6369 }
6370 }
6371
6372 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
6373 const std::size_t eidx = lqn.eshift + e;
6374 double f = 1.0;
6375 if (dbl(inflDen[eidx]) > GlobalConstants::FineTol)
6376 f = dbl(inflNum[eidx]) / dbl(inflDen[eidx]);
6377 if (!std::isfinite(f) || f < 1.0) f = 1.0; // a residence cannot fall below its demand
6378 const T fv = num_traits<T>::from_double(f);
6379 for (std::size_t aidx : lqn.actsof[eidx])
6380 residt[aidx] = T(fv * (lqn.hostdem[aidx].disabled ? Tzero()
6381 : lqn.hostdem[aidx].mean));
6382 }
6383
6384 // Task layers: the response time of every call
6385 std::vector<T> relw(N + 1, Tzero());
6386 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
6387 const std::size_t tidx = lqn.tshift + t;
6388 if (idxhash[tidx] < 0 || !ph_has_layer[tidx]) continue;
6389 const PHLayer& L = ph_layers[tidx];
6390 const LayerResult<T>& res = results.back()[std::size_t(idxhash[tidx])];
6391 const double npop = ph_layer_pop(L, tidx);
6392 for (std::size_t c : L.callers) {
6393 const std::size_t k = L.class_of_caller[c];
6394 T X = Tzero(), Q = Tzero();
6395 for (std::size_t s : L.qstations) {
6396 X = T(X + res.TN(s - 1, k - 1));
6397 Q = T(Q + res.QN(s - 1, k - 1));
6398 }
6399 if (!std::isfinite(dbl(X)) || dbl(X) < 0.0) X = Tzero();
6400 const T g = ph_inflation_of(ph_residence(Q, X, res.RN(L.qstations[0] - 1, k - 1)),
6401 L.svcmean_by_class[k], npop);
6402 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
6403 if (lqn.calltype[cidx] != CallType::SYNC) continue;
6404 if (lqn.parent[lqn.callpair_src[cidx]] != c) continue;
6405 if (lqn.parent[lqn.callpair_dst[cidx]] != tidx) continue;
6406 const std::size_t eidx = lqn.callpair_dst[cidx];
6407 callservt[cidx] = T(lqn.callproc_mean[cidx] * g * ph_entrymean[eidx]);
6408 callresidt[cidx] = callservt[cidx];
6409 }
6410 for (std::size_t eidx : lqn.entriesof[tidx])
6411 relw[eidx] = T(relw[eidx] + X * ph_ncalls(c, eidx));
6412 }
6413 for (const auto& oa : L.open_arrivals) {
6414 if (oa.second >= 0) continue;
6415 const std::size_t k = oa.first;
6416 const std::size_t cidx = std::size_t(-oa.second);
6417 const std::size_t eidx = lqn.callpair_dst[cidx];
6418 T X = Tzero();
6419 for (std::size_t s : L.qstations) X = T(X + res.TN(s - 1, k - 1));
6420 const T R = res.RN(L.qstations[0] - 1, k - 1);
6421 if (std::isfinite(dbl(R)) && dbl(R) > 0.0) {
6422 callservt[cidx] = T(R * lqn.callproc_mean[cidx]);
6423 callresidt[cidx] = callservt[cidx];
6424 }
6425 if (std::isfinite(dbl(X)) && dbl(X) > 0.0) relw[eidx] = T(relw[eidx] + X);
6426 }
6427 }
6428
6429 // Entry shares and throughputs
6430 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
6431 const std::size_t tidx = lqn.tshift + t;
6432 const std::vector<std::size_t>& entries = lqn.entriesof[tidx];
6433 if (entries.empty()) continue;
6434 // How the requests SPLIT over the entries is a flow-balance question,
6435 // and is answered at the task layer: a caller class reaches that server
6436 // once per invocation of the caller, carrying its whole call burst in
6437 // its service law, so the station rate counts caller cycles and the
6438 // per-entry rate is that rate times the calls the caller makes.
6439 T tot = Tzero();
6440 for (std::size_t eidx : entries) tot = T(tot + relw[eidx] + openTput[eidx]);
6441 if (dbl(tot) > GlobalConstants::FineTol) {
6442 for (std::size_t eidx : entries)
6443 ph_share[eidx] = T((relw[eidx] + openTput[eidx]) / tot);
6444 } else {
6445 for (std::size_t eidx : entries)
6446 ph_share[eidx] = num_traits<T>::from_double(1.0 / double(entries.size()));
6447 }
6448 // HOW MANY requests the task completes is a different question, and the
6449 // flow-balance total does not answer it: that total is what the callers
6450 // DEMAND, not what the task's threads can deliver. A thread cycles
6451 // through its host demand AND then through the task think time, and only
6452 // the processor layer of the task carries both, so the rate is read there.
6453 tput[tidx] = dbl(taskTput[tidx]) > GlobalConstants::FineTol ? taskTput[tidx] : tot;
6454 for (std::size_t eidx : entries) tput[eidx] = T(tput[tidx] * ph_share[eidx]);
6455 // The DEMAND is kept apart because it, and not the rate just reported,
6456 // is what closes the surrogate delay: normalising the think time by a
6457 // rate the same think time produced makes the processor layer
6458 // self-referential. PER REPLICA, because the thread count it is paired
6459 // with there is per replica.
6460 const T nrep = num_traits<T>::from_double(std::max(1.0, lqn.repl[tidx]));
6461 ph_xdemand[tidx] = dbl(tot) > GlobalConstants::FineTol ? T(tot / nrep)
6462 : T(tput[tidx] / nrep);
6463 }
6464
6465 // Recovery, under-relaxation, and the derived per-element quantities
6466 for (std::size_t aidx = lqn.ashift + 1; aidx <= lqn.ashift + lqn.nacts; ++aidx) {
6467 T v = residt[aidx];
6468 if (!std::isfinite(dbl(v)) && it > 1 && !std::isnan(residt_prev[aidx]))
6469 v = residt_prev_v[aidx];
6470 if (relax_omega < 1.0 && it > 1 && !std::isnan(residt_prev[aidx])) {
6471 const T om = num_traits<T>::from_double(relax_omega);
6472 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
6473 v = T(om * v + om1 * residt_prev_v[aidx]);
6474 }
6475 residt[aidx] = v;
6476 residt_prev[aidx] = dbl(v);
6477 residt_prev_v[aidx] = v;
6478 }
6479 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
6480 T v = callservt[cidx];
6481 if (!std::isfinite(dbl(v)))
6482 v = (it > 1 && std::isfinite(callservt_prev[cidx])) ? callservt_prev_v[cidx]
6483 : Tzero();
6484 if (relax_omega < 1.0 && it > 1 && !std::isnan(callservt_prev[cidx])) {
6485 const T om = num_traits<T>::from_double(relax_omega);
6486 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
6487 v = T(om * v + om1 * callservt_prev_v[cidx]);
6488 }
6489 callservt[cidx] = v;
6490 callresidt[cidx] = v;
6491 callservt_prev[cidx] = dbl(v);
6492 callresidt_prev[cidx] = dbl(v);
6493 callservt_prev_v[cidx] = v;
6494 if (dbl(v) > 0.0) callservtproc[cidx] = Distrib<T>::exp_mean(v);
6495 }
6496
6497 // Recompose the entry laws from the iterate just computed. The entry
6498 // service time is then the mean of the COMPOSED law and not the sum of the
6499 // parts: the branches of an AND fork overlap, so an entry that forks
6500 // finishes with the last of its branches and is not charged their sum.
6501 ph_compose_entry_laws();
6502
6503 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
6504 const std::size_t eidx = lqn.eshift + e;
6505 if (!ph_has_wf[eidx]) continue;
6506 const std::unordered_map<std::size_t, T>& ex = ph_execs[eidx];
6507 for (std::size_t aidx : lqn.actsof[eidx]) {
6508 T sa = T(residt[aidx] + act_think_time(aidx));
6509 for (std::size_t cidx : lqn.callsof[aidx])
6510 if (lqn.calltype[cidx] == CallType::SYNC) sa = T(sa + callservt[cidx]);
6511 servt[aidx] = sa;
6512 servt_prev[aidx] = dbl(sa);
6513 servt_prev_v[aidx] = sa;
6514 tput[aidx] = T(tput[eidx] * ex.at(aidx));
6515 tput_prev[aidx] = dbl(tput[aidx]);
6516 tput_prev_v[aidx] = tput[aidx];
6517 // exp_rate admits a null rate, but a never-called activity has no arrivals: disabled says so, as the python twin does.
6518 tputproc[aidx] = dbl(tput[aidx]) > 0.0 ? Distrib<T>::exp_rate(tput[aidx])
6519 : Distrib<T>::disabled_dist();
6520 if (dbl(sa) > 0.0) servtproc[aidx] = Distrib<T>::exp_mean(sa);
6521 }
6522 servt[eidx] = ph_entrymean[eidx];
6523 residt[eidx] = ph_entrymean[eidx];
6524 if (dbl(servt[eidx]) > 0.0) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
6525 }
6526 }
6527
6528 /**
6529 * Busy fraction of a task's threads, read from its own layer (ln_thread_busy_fraction.m).
6530 *
6531 * The think-time closure Z = njobs*(1-U)/X conserves throughput across a call only when
6532 * njobs*U is the mean number of HELD threads, sum_r T_r*S_r over c servers with S_r the
6533 * nominal service mean at the task station. Without a rate dependence that is the reported
6534 * Util. A load-, class- or joint-dependent station (server pools lower to the joint form)
6535 * reports Util normalized by its declared peak instead, so a 5-thread task with
6536 * class-dependence peak [2 1] read U = 1.35 and flow balance broke (T0 1.43412 against
6537 * E1/E2 1.94888). For such a station the fraction is computed from the definition; an
6538 * infinite server, or one with no dependence, returns the plain row sum; `skipnan` keeps
6539 * the srvn.ph closure's habit of dropping NaN entries from that sum.
6540 */
6541 T thread_busy_fraction(const qn::Layer<T>& L, std::size_t ts, const LayerResult<T>& r,
6542 bool skipnan) const {
6543 const qn::Station<T>& st = L.stations[ts - 1];
6544 const double c = st.nservers;
6545 const bool hasdep = !st.lldscaling.empty() || bool(st.cdscaling) || bool(st.jdscaling);
6546 T u = Tzero();
6547 if (!hasdep || std::isinf(c) || c <= 0.0) {
6548 for (std::size_t k = 0; k < L.nclasses; ++k) {
6549 const T v = r.UN(ts - 1, k);
6550 if (!skipnan || !std::isnan(dbl(v))) u = T(u + v);
6551 }
6552 return u;
6553 }
6554 for (std::size_t k = 0; k < L.nclasses; ++k) {
6555 const T x = r.TN(ts - 1, k);
6556 if (std::isnan(dbl(x)) || !(dbl(x) > 0.0)) continue;
6557 const Distrib<T>& d = L.service[ts - 1][k];
6558 if (d.disabled) continue;
6559 if (std::isfinite(dbl(d.mean))) u = T(u + x * d.mean);
6560 }
6561 return T(u / num_traits<T>::from_double(c));
6562 }
6563
6564 /**
6565 * Surrogate delay of every caller.
6566 *
6567 * Same closure as update_think_times -- a thread of the task is idle for
6568 * whatever of its cycle the task's own station does not hold -- but the rate
6569 * it is normalised by is the INVOCATION rate of the task and not the
6570 * throughput of its station. Under this method a caller class reaches the
6571 * server once per invocation of the caller, carrying its whole call burst in
6572 * its service law, so the station rate counts caller cycles rather than calls
6573 * and the two differ by the mean number of calls.
6574 */
6575 void update_think_times_ph(int it) {
6576 thinktproc.assign(lqn.nidx + 1, Distrib<T>::disabled_dist());
6577 const T floorv = num_traits<T>::from_double(GlobalConstants::Zero);
6578 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
6579 const std::size_t tidx = lqn.tshift + t;
6580 if (ignore[tidx]) continue;
6581 const T ztask = ref_think_time(tidx);
6582 if (idxhash[tidx] < 0) {
6583 // A task no other task calls but whose entries carry an arrival
6584 // still has a cycle: its threads are driven by the stream.
6585 // build_layers_ph drops the open class for it precisely so this
6586 // closure can set the rate.
6587 const double arvrate = open_arrival_rate_of(tidx);
6588 if (arvrate > GlobalConstants::FineTol) {
6589 double nja = lqn.maxmult[tidx];
6590 if (!std::isfinite(nja) || nja <= 0.0) {
6591 nja = 0.0;
6592 for (std::size_t c = 1; c <= NT(); ++c) nja = std::max(nja, njobs(tidx, c));
6593 }
6594 T hres = Tzero();
6595 const std::size_t hidx = lqn.parent[tidx];
6596 if (hidx >= 1 && hidx < idxhash.size() && idxhash[hidx] >= 0 &&
6597 ph_has_layer[hidx] && ph_layers[hidx].class_of_caller[tidx] > 0) {
6598 const PHLayer& HL = ph_layers[hidx];
6599 const LayerResult<T>& hr = results.back()[std::size_t(idxhash[hidx])];
6600 const T rr = hr.RN(HL.qstations[0] - 1, HL.class_of_caller[tidx] - 1);
6601 if (!std::isnan(dbl(rr))) hres = rr;
6602 }
6603 hres += open_arrival_call_residt(tidx, arvrate);
6604 T za = T(num_traits<T>::from_double(nja / arvrate) - hres - ztask);
6605 if (za < floorv) za = floorv;
6606 if (relax_omega < 1.0 && it > 1 && !std::isnan(thinkt_prev[tidx])) {
6607 const T om = num_traits<T>::from_double(relax_omega);
6608 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
6609 za = T(om * za + om1 * thinkt_prev_v[tidx]);
6610 }
6611 tput[tidx] = num_traits<T>::from_double(arvrate);
6612 thinkt[tidx] = za;
6613 thinkt_prev[tidx] = dbl(za);
6614 thinkt_prev_v[tidx] = za;
6615 thinktproc[tidx] = Distrib<T>::exp_mean(T(za + ztask));
6616 continue;
6617 }
6618 // a reference task, or one no other task calls: it has no station
6619 // of its own, so its only delay is the think time the user declared
6620 thinkt[tidx] = num_traits<T>::from_double(GlobalConstants::FineTol);
6621 thinktproc[tidx] = Distrib<T>::immediate();
6622 continue;
6623 }
6624 const PHLayer& L = ph_layers[tidx];
6625 const qn::Layer<T>& m = ensemble[std::size_t(idxhash[tidx])];
6626 const LayerResult<T>& r = results.back()[std::size_t(idxhash[tidx])];
6627 // the thread busy fraction, not a dependent station's peak-normalized Util
6628 T U = thread_busy_fraction(m, L.qstations[0], r, true);
6629 util[tidx] = U;
6630 // The closure below measures a thread's cycle in WORK: it is idle for
6631 // whatever of the cycle its station does not hold it working. A
6632 // SetupTask's station service also carries a cold start, which is time
6633 // the thread is unavailable but is not work, so it is taken back out of
6634 // U before the closure reads it. Zero for every task without a setup.
6635 if (dbl(ph_setupshare[tidx]) > 0.0)
6636 U = T(U * (Tone() - ph_setupshare[tidx]));
6637 // the rate the CALLERS ask of the task, not the rate its processor
6638 // layer reported: the latter is itself a function of this think time
6639 T X = ph_xdemand[tidx];
6640 if (!(dbl(X) > GlobalConstants::FineTol)) X = tput[tidx];
6641 // The thread pool of ONE replica, the convention ph_xdemand is kept in
6642 double nj = lqn.maxmult[tidx];
6643 if (!std::isfinite(nj) || nj <= 0.0) {
6644 nj = 0.0;
6645 for (std::size_t c = 1; c <= NT(); ++c) nj = std::max(nj, njobs(tidx, c));
6646 }
6647 T z;
6648 if (dbl(X) > GlobalConstants::FineTol) {
6649 if (lqn.sched[tidx] == SchedStrategy::INF) {
6650 // an infinite server reports a mean number of busy threads
6651 z = T((num_traits<T>::from_double(nj) - U) / X - ztask);
6652 } else {
6653 const T om = U > Tone() ? T(U - Tone()) : T(Tone() - U);
6654 z = T(num_traits<T>::from_double(nj) * om / X - ztask);
6655 }
6656 } else {
6657 z = thinkt[tidx];
6658 }
6659 if (z < floorv) z = floorv;
6660 if (it > 1 && !std::isnan(thinkt_prev[tidx]) && !std::isfinite(dbl(z)))
6661 z = thinkt_prev_v[tidx];
6662 if (relax_omega < 1.0 && it > 1 && !std::isnan(thinkt_prev[tidx])) {
6663 const T om = num_traits<T>::from_double(relax_omega);
6664 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
6665 z = T(om * z + om1 * thinkt_prev_v[tidx]);
6666 }
6667 thinkt[tidx] = z;
6668 thinkt_prev[tidx] = dbl(z);
6669 thinkt_prev_v[tidx] = z;
6670 thinktproc[tidx] = Distrib<T>::exp_mean(T(z + ztask));
6671 }
6672 }
6673
6674 /**
6675 * LQN-level results of method "srvn.ph".
6676 *
6677 * The layers report per caller task, so every entry, activity and call figure
6678 * is rebuilt from the converged fixed point rather than read off a class row,
6679 * in the layout aggregate() returns: QN carries the entry and task
6680 * utilizations, UN the processor utilizations, RN the response times and WN
6681 * the residence times.
6682 */
6683 LnSolution<T> aggregate_ph() {
6684 const std::size_t N = lqn.nidx;
6685 LnSolution<T> out;
6686 out.QN.assign(N + 1, Tzero());
6687 out.UN.assign(N + 1, Tzero());
6688 out.RN.assign(N + 1, Tzero());
6689 out.TN.assign(N + 1, Tzero());
6690 out.AN.assign(N + 1, Tzero());
6691 out.WN.assign(N + 1, Tzero());
6692 out.defined_Q.assign(N + 1, false);
6693 out.defined_U.assign(N + 1, false);
6694 out.defined_R.assign(N + 1, false);
6695 out.defined_T.assign(N + 1, false);
6696 out.defined_A.assign(N + 1, false);
6697 out.defined_W.assign(N + 1, false);
6698 out.iterations = iterations_done;
6699 out.converged = did_converge;
6700
6701 std::vector<T> PN(N + 1, Tzero()), UT(N + 1, Tzero());
6702 std::vector<bool> hasPN(N + 1, false), hasUT(N + 1, false);
6703
6704 for (std::size_t a = 1; a <= lqn.nacts; ++a) {
6705 const std::size_t aidx = lqn.ashift + a;
6706 const std::size_t tidx = lqn.parent[aidx];
6707 if (ignore[tidx]) continue;
6708 const std::size_t hidx = lqn.parent[tidx];
6709 out.TN[aidx] = tput[aidx];
6710 out.defined_T[aidx] = true;
6711 out.RN[aidx] = servt[aidx];
6712 out.defined_R[aidx] = true;
6713 UT[aidx] = T(tput[aidx] * servt[aidx]);
6714 hasUT[aidx] = true;
6715 // LINE scales the utilization of a queueing station into [0,1] whatever
6716 // its multiplicity, and reports a mean number of busy servers at an
6717 // infinite server: the processor share of an activity follows the same
6718 // convention
6719 const T hd = lqn.hostdem[aidx].disabled ? Tzero() : lqn.hostdem[aidx].mean;
6720 PN[aidx] = T(tput[aidx] * hd / num_traits<T>::from_double(ph_host_servers(hidx)));
6721 hasPN[aidx] = true;
6722 PN[hidx] = T(PN[hidx] + PN[aidx]);
6723 hasPN[hidx] = true;
6724 }
6725
6726 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
6727 const std::size_t eidx = lqn.eshift + e;
6728 const std::size_t tidx = lqn.parent[eidx];
6729 if (ignore[tidx]) continue;
6730 out.TN[eidx] = tput[eidx];
6731 out.defined_T[eidx] = true;
6732 out.RN[eidx] = servt[eidx];
6733 out.defined_R[eidx] = true;
6734 UT[eidx] = T(tput[eidx] * servt[eidx]);
6735 hasUT[eidx] = true;
6736 for (std::size_t aidx : lqn.actsof[eidx]) {
6737 PN[eidx] = T(PN[eidx] + PN[aidx]);
6738 hasPN[eidx] = true;
6739 }
6740 // ResidT is reported per visit to the TASK, not per execution of the
6741 // activity: an activity of this entry runs EXECS times per invocation,
6742 // and the entry takes SHARE of the task's invocations. RespT stays per
6743 // execution.
6744 if (ph_has_wf[eidx]) {
6745 const std::unordered_map<std::size_t, T>& ex = ph_execs[eidx];
6746 for (std::size_t aidx : lqn.actsof[eidx]) {
6747 out.WN[aidx] = T(ph_share[eidx] * ex.at(aidx) * residt[aidx]);
6748 out.defined_W[aidx] = true;
6749 }
6750 }
6751 UT[tidx] = T(UT[tidx] + UT[eidx]);
6752 hasUT[tidx] = true;
6753 }
6754
6755 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
6756 const std::size_t tidx = lqn.tshift + t;
6757 if (ignore[tidx]) continue;
6758 out.TN[tidx] = tput[tidx];
6759 out.defined_T[tidx] = true;
6760 T w = Tzero();
6761 bool anyw = false;
6762 for (std::size_t aidx : lqn.actsof[tidx]) {
6763 PN[tidx] = T(PN[tidx] + PN[aidx]);
6764 hasPN[tidx] = true;
6765 if (out.defined_W[aidx]) { w = T(w + out.WN[aidx]); anyw = true; }
6766 }
6767 if (anyw) { out.WN[tidx] = w; out.defined_W[tidx] = true; }
6768 }
6769
6770 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx)
6771 out.defined_T[hidx] = false; // kept undefined for consistency with LQNS
6772
6773 for (std::size_t idx = 1; idx <= N; ++idx) {
6774 out.QN[idx] = UT[idx];
6775 out.defined_Q[idx] = hasUT[idx];
6776 out.UN[idx] = PN[idx];
6777 out.defined_U[idx] = hasPN[idx];
6778 // Idle, not undefined -- the same rule aggregate() applies, and for the
6779 // same reason: an unreachable element reports zero for the measures its
6780 // kind HAS and leaves the ones it never has undefined, so that the
6781 // table's NaN mask survives a disconnected component.
6782 if (ignore[idx]) {
6783 out.UN[idx] = Tzero(); // every kind reports a utilization
6784 out.defined_U[idx] = true;
6785 out.defined_A[idx] = false; // nothing reports an arrival rate on an LQN
6786 const bool host = lqn.type[idx] == LqnElement::HOST;
6787 const bool task = lqn.type[idx] == LqnElement::TASK;
6788 const bool entry = lqn.type[idx] == LqnElement::ENTRY;
6789 out.QN[idx] = Tzero();
6790 out.defined_Q[idx] = !host;
6791 out.RN[idx] = Tzero();
6792 out.defined_R[idx] = !host && !task;
6793 out.WN[idx] = Tzero();
6794 out.defined_W[idx] = !host && !entry;
6795 out.TN[idx] = Tzero();
6796 out.defined_T[idx] = !host;
6797 }
6798 }
6799 return out;
6800 }
6801
6802 void update_metrics(int it) {
6803 if (is_ph_encoding()) {
6804 update_metrics_ph(it);
6805 return;
6806 }
6807 if (lnmethod == "moment3") {
6808 update_metrics_moment_based(it);
6809 return;
6810 }
6811 update_metrics_default(it);
6812 }
6813
6814 /**
6815 * Port of ln_layer_refcell: the 1-based class whose throughput at its
6816 * refstat normalises a residence of class K (0-based) in the layer of
6817 * element IDX, 0 when there is none. A chain merged by refpath holds more
6818 * than one caller task and only ONE reference class, so the per-caller rate
6819 * is read through the layer's normclass; everywhere else it is the chain's
6820 * reference class, which is what normclass would have named anyway.
6821 */
6822 std::size_t layer_refclass(std::size_t idx, const qn::Layer<T>& L, std::size_t k) const {
6823 const auto it = layer_normclass.find(idx);
6824 if (it != layer_normclass.end() && k < it->second.size() && it->second[k] > 0)
6825 return it->second[k];
6826 std::size_t c = 0;
6827 for (std::size_t cc = 0; cc < L.nchains; ++cc)
6828 if (L.chains[cc][k]) c = cc;
6829 return L.refclass[c];
6830 }
6831
6832 void update_metrics_default(int it) {
6833 const std::size_t N = lqn.nidx;
6834 servt.assign(N + 1, Tzero());
6835 residt.assign(N + 1, Tzero());
6836 const int iter_min = std::min(30, int(std::ceil(opt.iter_max / 4.0)));
6837 const bool averaging = averagingstart >= 0 && it >= iter_min;
6838 const int wnd = averaging ? int(it - averagingstart + 1) : 1;
6839
6840 for (const UpdRow& row : servt_map) {
6841 const std::size_t e = std::size_t(idxhash[row.idx]);
6842 const qn::Layer<T>& L = ensemble[e];
6843 const std::size_t k = row.cls - 1;
6844 const std::size_t refclass_c = layer_refclass(row.idx, L, k);
6845 const std::size_t refstat_k = L.classes[k].refstat;
6846
6847 T sv = Tzero(), rs = Tzero(), tp = Tzero();
6848 const T wT = T(Tone() / num_traits<T>::from_int(wnd));
6849 for (int w = 0; w < wnd; ++w) {
6850 const LayerResult<T>& r = results[results.size() - 1 - w][e];
6851 sv += r.RN(row.node - 1, k) * wT;
6852 const T TN_ref = (refclass_c > 0 && refstat_k > 0) ? r.TN(refstat_k - 1, refclass_c - 1) : Tzero();
6853 if (dbl(TN_ref) > GlobalConstants::FineTol)
6854 rs += r.QN(row.node - 1, k) / TN_ref * wT;
6855 else
6856 rs += r.WN(row.node - 1, k) * wT;
6857 tp += r.TN(row.node - 1, k) * wT;
6858 }
6859 servt[row.aidx] = sv;
6860 residt[row.aidx] = rs;
6861 tput[row.aidx] = tp;
6862
6863 // A host layer holding every replica of the task spreads its class evenly
6864 // over the host's identical replica stations, and what was read is one of
6865 // them: the residence and rate are the sum over all (BUG-110)
6866 if (lqn.parent[lqn.parent[row.aidx]] == row.idx) {
6867 const auto hr = host_all_replica_tasks.find(lqn.parent[row.aidx]);
6868 if (hr != host_all_replica_tasks.end() && hr->second > 1) {
6869 const T h = num_traits<T>::from_double(double(hr->second));
6870 residt[row.aidx] = T(h * residt[row.aidx]);
6871 tput[row.aidx] = T(h * tput[row.aidx]);
6872 }
6873 }
6874
6875 // an activity think time is in series with the host demand
6876 const Distrib<T>& at = lqn.actthink[row.aidx];
6877 if (!at.disabled && dbl(at.mean) > GlobalConstants::FineTol) {
6878 servt[row.aidx] = T(servt[row.aidx] + at.mean);
6879 residt[row.aidx] = T(residt[row.aidx] + at.mean);
6880 }
6881
6882 // An activity of an async-only entry takes RN, the response per
6883 // visit, and not the visit-weighted residence: entry selection
6884 // routes the task to each of its entries with a share, and there is
6885 // no caller-side visit ratio here to divide that share back out
6886 // (the sync branch below does exactly that). updateMetricsDefault.m:63-77.
6887 if (async_only_activity(row.aidx)) residt[row.aidx] = servt[row.aidx];
6888
6889 if (relax_omega < 1.0 && it > 1) {
6890 const T om = num_traits<T>::from_double(relax_omega);
6891 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
6892 if (!std::isnan(servt_prev[row.aidx]))
6893 servt[row.aidx] = T(om * servt[row.aidx] + om1 * servt_prev_v[row.aidx]);
6894 if (!std::isnan(residt_prev[row.aidx]))
6895 residt[row.aidx] = T(om * residt[row.aidx] + om1 * residt_prev_v[row.aidx]);
6896 if (!std::isnan(tput_prev[row.aidx]))
6897 tput[row.aidx] = T(om * tput[row.aidx] + om1 * tput_prev_v[row.aidx]);
6898 }
6899 servt_prev[row.aidx] = dbl(servt[row.aidx]);
6900 residt_prev[row.aidx] = dbl(residt[row.aidx]);
6901 tput_prev[row.aidx] = dbl(tput[row.aidx]);
6902 servt_prev_v[row.aidx] = servt[row.aidx];
6903 residt_prev_v[row.aidx] = residt[row.aidx];
6904 tput_prev_v[row.aidx] = tput[row.aidx];
6905
6906 if (servt[row.aidx] > Tzero() && dbl(servt[row.aidx]) <= 1e10)
6907 servtproc[row.aidx] = Distrib<T>::exp_mean(servt[row.aidx]);
6908 tputproc[row.aidx] = Distrib<T>::exp_rate(tput[row.aidx]);
6909 }
6910
6911 // The phase split of servt, updateMetricsDefault.m:120-151. It is
6912 // recomputed from scratch each iteration because servt is; the
6913 // overtaking probability it feeds is computed later, once the entry
6914 // throughputs exist.
6915 if (has_phase2) {
6916 servt_ph1.assign(N + 1, Tzero());
6917 servt_ph2.assign(N + 1, Tzero());
6918 for (std::size_t a = 1; a <= lqn.nacts; ++a) {
6919 const std::size_t aidx = lqn.ashift + a;
6920 if (lqn.actphase[a] == 1)
6921 servt_ph1[aidx] = servt[aidx];
6922 else
6923 servt_ph2[aidx] = servt[aidx];
6924 }
6925 // the ENTRY split is taken once the entry service exists, below: summing the
6926 // activities' own servt left out the synchronous calls each phase makes
6927 }
6928
6929 // throughput of the activities that appear only as client-side classes
6930 for (const UpdRow& row : thinkt_map) {
6931 if (!tputproc[row.aidx].disabled) continue;
6932 const std::size_t e = std::size_t(idxhash[row.idx]);
6933 T tp = Tzero();
6934 const T wT = T(Tone() / num_traits<T>::from_int(wnd));
6935 for (int w = 0; w < wnd; ++w)
6936 tp += results[results.size() - 1 - w][e].TN(row.node - 1, row.cls - 1) * wT;
6937 tput[row.aidx] = tp;
6938 tputproc[row.aidx] = Distrib<T>::exp_rate(tp);
6939 }
6940
6941 // call service and residence times
6942 callservt.assign(lqn.ncalls + 1, Tzero());
6943 callresidt.assign(lqn.ncalls + 1, Tzero());
6944 // A call into a replicated callee registers ONE ROW PER REPLICA STATION, each
6945 // rewriting the same entry, so a Little residence read off one station keeps one
6946 // replica's share. The replicas are symmetric, so the whole call is that share
6947 // times the number of DISTINCT stations (rows would also count a call reached from
6948 // several graph positions). Without it 51-replication's B1 read a response time of
6949 // 44.2 against the reference's 99.9 (BUG-103; updateMetricsDefault.m callRowsPerLayer).
6950 std::vector<std::size_t> call_rows(call_map.size(), 1);
6951 for (std::size_t q = 0; q < call_map.size(); ++q) {
6952 std::vector<std::size_t> st;
6953 for (const UpdRow& o : call_map)
6954 if (o.idx == call_map[q].idx && o.aidx == call_map[q].aidx && o.node > 1 &&
6955 std::find(st.begin(), st.end(), o.node) == st.end())
6956 st.push_back(o.node);
6957 call_rows[q] = std::max<std::size_t>(1, st.size());
6958 }
6959 for (std::size_t q = 0; q < call_map.size(); ++q) {
6960 const UpdRow& row = call_map[q];
6961 if (row.node <= 1) continue; // a client-side call class contributes none
6962 const std::size_t e = std::size_t(idxhash[row.idx]);
6963 const LayerResult<T>& r = results.back()[e];
6964 callservt[row.aidx] =
6965 T(r.RN(row.node - 1, row.cls - 1) * lqn.callproc_mean[row.aidx]);
6966 // Normalise per chain-reference visit, as residt does. WN divides by the
6967 // class's own reference rate when the layer is open (an INF client task),
6968 // which is per-ENTRY visit, and the entry rescaling in
6969 // resolve_entry_service would then charge the call once per entry.
6970 {
6971 const qn::Layer<T>& L = ensemble[e];
6972 const std::size_t k = row.cls - 1;
6973 const std::size_t refclass_c = layer_refclass(row.idx, L, k);
6974 const std::size_t refstat_k = L.classes[k].refstat;
6975 const T TN_ref = (refclass_c > 0 && refstat_k > 0) ? r.TN(refstat_k - 1, refclass_c - 1) : Tzero();
6976 callresidt[row.aidx] = dbl(TN_ref) > GlobalConstants::FineTol
6977 ? T(num_traits<T>::from_int(int(call_rows[q])) *
6978 r.QN(row.node - 1, k) / TN_ref)
6979 : r.WN(row.node - 1, k);
6980 }
6981 const T rw = region_wait(e, row.cls);
6982 if (rw > Tzero()) {
6983 callservt[row.aidx] = T(callservt[row.aidx] + rw);
6984 callresidt[row.aidx] = T(callresidt[row.aidx] + rw);
6985 }
6986 if (relax_omega < 1.0 && it > 1 && !std::isnan(callservt_prev[row.aidx])) {
6987 const T om = num_traits<T>::from_double(relax_omega);
6988 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
6989 callservt[row.aidx] =
6990 T(om * callservt[row.aidx] + om1 * callservt_prev_v[row.aidx]);
6991 }
6992 callservt_prev[row.aidx] = dbl(callservt[row.aidx]);
6993 callservt_prev_v[row.aidx] = callservt[row.aidx];
6994 callresidt_prev[row.aidx] = dbl(callresidt[row.aidx]);
6995 }
6996
6997 resolve_entry_service();
6998
6999 // The overtaking correction, updateMetricsDefault.m:313-351. It runs
7000 // HERE and not with the split above because it needs the entry
7001 // throughput resolve_entry_service has just produced.
7002 //
7003 // servt keeps both phases -- the server IS busy through phase 2, so the
7004 // utilization is unchanged -- while residt becomes the CALLER's view:
7005 // phase 1 in full, phase 2 only when the caller is actually overtaken.
7006 if (has_phase2) {
7007 // Entry phase split: the phase-2 share of the entry service, calls included,
7008 // scaled onto servt as resolve_entry_service scaled the total
7009 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7010 const std::size_t eidx = lqn.eshift + e;
7011 servt_ph2[eidx] = (eidx < entry_servt_last.size() && entry_servt_last[eidx] > Tzero())
7012 ? T(entry_servt_ph2[eidx] * servt[eidx] / entry_servt_last[eidx])
7013 : Tzero();
7014 servt_ph1[eidx] = T(servt[eidx] - servt_ph2[eidx]);
7015 }
7016 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7017 const std::size_t eidx = lqn.eshift + e;
7018 const std::size_t tidx = lqn.parent[eidx];
7019 if (!(dbl(servt_ph2[eidx]) > GlobalConstants::FineTol)) continue;
7020 if (lqn.isref[tidx] || !lqn.issynccaller.any_col(eidx)) {
7021 residt[eidx] = servt[eidx];
7022 continue;
7023 }
7024 T entry_tput = Tzero();
7025 if (dbl(tput[eidx]) > GlobalConstants::FineTol)
7026 entry_tput = tput[eidx];
7027 else if (dbl(tput[tidx]) > GlobalConstants::FineTol)
7028 entry_tput = tput[tidx];
7029 prOvertake[e] =
7030 dbl(entry_tput) > GlobalConstants::FineTol
7031 ? lqn_overtake_prob_markov(lqn, servt, callresidt, tput, eidx,
7032 servt_ph2[eidx])
7033 : Tzero();
7034 residt[eidx] = T(servt_ph1[eidx] + prOvertake[e] * servt_ph2[eidx]);
7035 }
7036 }
7037
7038 for (const UpdRow& row : call_map) {
7039 if (row.node <= 1) continue;
7040 const std::size_t eidx = lqn.callpair_dst[row.aidx];
7041 if (servt[eidx] > Tzero()) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
7042 }
7043 for (const UpdRow& row : call_map) {
7044 if (row.node <= 1) continue;
7045 const std::size_t eidx = lqn.callpair_dst[row.aidx];
7046 if (it == 1) {
7047 callservt[row.aidx] = servt[eidx];
7048 callservtproc[row.aidx] = servtproc[eidx];
7049 } else if (callservt[row.aidx] > Tzero()) {
7050 callservtproc[row.aidx] = Distrib<T>::exp_mean(callservt[row.aidx]);
7051 }
7052 }
7053
7054 // What a synchronous CALLER waits for at a phase-2 target is residt,
7055 // not servt (updateMetricsDefault.m:384-400): the loop just above set it
7056 // from the target's full service, which would charge the caller for
7057 // phase 2 it never waits through. Under srvn layering the callee's layer
7058 // already serves residt (update_layers), so the response read from it is
7059 // residt plus the callee's queueing and must stand after the first pass:
7060 // overriding it with a residt computed before update_populations charges
7061 // the caller a value the callee no longer serves (BUGS.md BUG-103).
7062 if (has_phase2 && (is_flat_layering() || it == 1)) {
7063 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
7064 if (lqn.calltype[cidx] != CallType::SYNC) continue;
7065 const std::size_t target = lqn.callpair_dst[cidx];
7066 if (target <= lqn.eshift || target > lqn.eshift + lqn.nentries) continue;
7067 if (!(dbl(servt_ph2[target]) > GlobalConstants::FineTol)) continue;
7068 const T eff = residt[target]; // servt_ph1 + prOvertake * servt_ph2
7069 if (!(eff > Tzero())) continue;
7070 const T w = T(eff * lqn.callproc_mean[cidx]);
7071 callservt[cidx] = w;
7072 callresidt[cidx] = w;
7073 callservtproc[cidx] = Distrib<T>::exp_mean(w);
7074 }
7075 }
7076 }
7077
7078 // -----------------------------------------------------------------------
7079 // updateMetricsMomentBased, the `moment3` method
7080 // -----------------------------------------------------------------------
7081
7082 /**
7083 * The moments of an empirical CDF, MATLAB's `EmpiricalCDF.getMoments`.
7084 *
7085 * READ EXACTLY AS THE REFERENCE READS IT, and the reading is not the
7086 * obvious one: the weight of a bin is the CDF INCREMENT and its abscissa is
7087 * the MIDPOINT of the two t values, so this is a midpoint quadrature of
7088 * integral x^k dF and not a sum over grid points. Reading the grid as a pmf
7089 * at the right endpoint instead inflates every moment; that exact mistake
7090 * cost a wrong entry mean in the Python port (see _kb/06-solver-catalog.md).
7091 */
7092 static void cdf_moments(const fluid::FluidPassage& c, double& m1, double& m2, double& m3) {
7093 m1 = m2 = m3 = 0.0;
7094 for (std::size_t i = 0; i + 1 < c.t.size(); ++i) {
7095 const double x = 0.5 * (c.t[i + 1] + c.t[i]);
7096 const double w = c.cdf[i + 1] - c.cdf[i];
7097 m1 += x * w;
7098 m2 += x * x * w;
7099 m3 += x * x * x * w;
7100 }
7101 }
7102
7103 /** The per-layer response-time CDFs, computed once and cached. */
7104 const std::vector<std::vector<fluid::FluidPassage>>& layer_cdf(std::size_t e) {
7105 if (cdf_repo[e].empty()) {
7106 // THE LAYER HAS NO ROUTING MATRIX UNTIL IT IS ASKED FOR ONE, and the
7107 // fluid drift is built from `sn.rt` alone: without this the ODE has
7108 // no transitions, the state decays to zero, every passage reports the
7109 // degenerate curve at the origin and every entry ends up with an
7110 // empty convolution and a service time of zero. Same reason as in
7111 // solve_layer_fluid, and the failure is silent in both.
7112 ensemble[e].refresh_rt();
7113 try {
7114 cdf_repo[e] = detail::ln_fluid_cdf_respt(ensemble[e], opt.layer_fluid);
7115 } catch (const std::exception&) {
7116 // The reference falls back to the LAYER's own solver when the
7117 // fluid passage fails, and for the MVA-family layers that is
7118 // the base-class exponential law with the layer's mean -- so
7119 // the fallback here is that law over the last solved averages,
7120 // rather than an empty repo that silently collapses every
7121 // entry law to its bare host demand.
7122 cdf_repo[e] = layer_cdf_exp_fallback(e);
7123 }
7124 }
7125 return cdf_repo[e];
7126 }
7127
7128 /** The base-class exponential CDF over layer e's last solved mean response
7129 * times, the reference's fallback route when the fluid passage fails. */
7130 std::vector<std::vector<fluid::FluidPassage>> layer_cdf_exp_fallback(std::size_t e) {
7131 const qn::NetworkStruct<T>& L = ensemble[e];
7132 std::vector<std::vector<fluid::FluidPassage>> out(
7133 L.nstations, std::vector<fluid::FluidPassage>(L.nclasses));
7134 if (results.empty() || e >= results.back().size()) return out;
7135 const Matrix<T>& RN = results.back()[e].RN;
7136 const std::size_t npts = 100;
7137 for (std::size_t i = 0; i < L.nstations; ++i) {
7138 if (L.stations[i].nodetype == qn::NodeType::Source) continue;
7139 for (std::size_t r = 0; r < L.nclasses; ++r) {
7140 if (L.disabled[i][r]) continue;
7141 const double rn = (i < static_cast<std::size_t>(RN.rows()) &&
7142 r < static_cast<std::size_t>(RN.cols()))
7143 ? dbl(RN(i, r))
7144 : 0.0;
7145 if (!(std::isfinite(rn) && rn > 0.0)) continue;
7146 fluid::FluidPassage& cell = out[i][r];
7147 cell.t.reserve(npts);
7148 cell.cdf.reserve(npts);
7149 for (std::size_t j = 0; j < npts; ++j) {
7150 const double q =
7151 0.001 + (0.999 - 0.001) * static_cast<double>(j) / (npts - 1);
7152 cell.cdf.push_back(q);
7153 cell.t.push_back(-std::log(1.0 - q) * rn);
7154 }
7155 }
7156 }
7157 return out;
7158 }
7159
7160 /**
7161 * task_tput / entry_tput at the host layer of `eidx`.
7162 *
7163 * This is what renormalises a residence time from "one visit to the TASK",
7164 * which is how the layer reports it, to "one visit to the ENTRY", which is
7165 * what an entry metric means. It exists as a helper because applying it
7166 * TWICE is a real and silent failure mode: `moment3` once summed per-visit
7167 * response times and then applied this ratio on top, inflating every entry
7168 * service time by the entries-per-task ratio.
7169 *
7170 * `state` is 0 when the entry's layers are ignored (nothing is assigned at
7171 * all), 1 when it has no synchronous caller (no ratio exists) and 2 when
7172 * `ratio` is set.
7173 */
7174 int entry_visit_ratio(std::size_t eidx, T& ratio) const {
7175 const std::size_t tidx = lqn.parent[eidx];
7176 const std::size_t hidx = lqn.parent[tidx];
7177 if (ignore[tidx] || ignore[hidx] || idxhash[hidx] < 0) return 0;
7178 if (!lqn.issynccaller.any_col(eidx)) return 1;
7179 const std::size_t hl = std::size_t(idxhash[hidx]);
7180 const qn::Layer<T>& L = ensemble[hl];
7181 const LayerResult<T>& r = results.back()[hl];
7182 T task_tput = Tzero(), entry_tput = Tzero();
7183 for (const auto& kv : L.attr_tasks)
7184 if (kv.second == tidx) task_tput += r.TN(L.clientIdx - 1, kv.first - 1);
7185 for (const auto& kv : L.attr_entries)
7186 if (kv.second == eidx) entry_tput += r.TN(L.clientIdx - 1, kv.first - 1);
7187 const T floor = num_traits<T>::from_double(GlobalConstants::Zero);
7188 ratio = T(task_tput / (entry_tput > floor ? entry_tput : floor));
7189 return 2;
7190 }
7191
7192 /** (I - servtmatrix)^-1, the reference's `inv(eye - servtmatrix)`. */
7193 Matrix<T> entry_service_resolvent() const {
7194 const std::size_t dim = lqn.nidx + lqn.ncalls;
7195 Matrix<T> A(dim + 1, dim + 1, Tzero());
7196 for (std::size_t i = 0; i <= dim; ++i) {
7197 A(i, i) = Tone();
7198 for (std::size_t j = 0; j <= dim; ++j) A(i, j) = T(A(i, j) - servtmatrix(i, j));
7199 }
7200 return ::line::inverse(A);
7201 }
7202
7203 /**
7204 * Port of @@SolverLN/updateMetricsMomentBased.m, the `moment3` method.
7205 *
7206 * WHAT IT DOES DIFFERENTLY from the default update. The default feeds MEANS
7207 * between the layers. This fits an APH to the response-time CDF of every
7208 * activity and every call a layer reports, convolves those fits along the
7209 * entry's activity sequence, and reads the entry's law off the convolution:
7210 * a mean AND a distribution, which is what `get_cdf_respt` returns.
7211 *
7212 * TWO PASSES, not one. While the ensemble is still moving (`!hasconverged`)
7213 * the update is mean-based -- forming a CDF per layer per iteration would
7214 * cost a fluid integration per layer per iteration and would be fitting
7215 * noise anyway -- and the distribution is formed ONCE, on the converged
7216 * ensemble. The reference splits it exactly here.
7217 *
7218 * THE NORMALISATION TRAP. Both passes build the entry service from
7219 * RESIDENCE times, which are normalised to one visit to the TASK, and then
7220 * apply the task/entry throughput ratio, which converts that to one visit
7221 * to the ENTRY. Summing per-visit RESPONSE times and applying the ratio as
7222 * well applies the normalisation twice and inflates every multi-entry task
7223 * by its entries-per-task ratio. That was a live defect in all three
7224 * reference codebases until 2026-07-31.
7225 */
7226 void update_metrics_moment_based(int it) {
7227 const std::size_t N = lqn.nidx;
7228 servt.assign(N + 1, Tzero());
7229 residt.assign(N + 1, Tzero());
7230 callservt.assign(lqn.ncalls + 1, Tzero());
7231 callresidt.assign(lqn.ncalls + 1, Tzero());
7232
7233 // ---- what every activity's layer reports, common to both passes ----
7234 for (const UpdRow& row : servt_map) {
7235 const std::size_t e = std::size_t(idxhash[row.idx]);
7236 const qn::Layer<T>& L = ensemble[e];
7237 const LayerResult<T>& r = results.back()[e];
7238 const std::size_t k = row.cls - 1;
7239 std::size_t c = 0;
7240 for (std::size_t cc = 0; cc < L.nchains; ++cc)
7241 if (L.chains[cc][k]) c = cc;
7242 const std::size_t refclass_c = L.refclass[c];
7243 const std::size_t refstat_k = L.classes[k].refstat;
7244 const T TN_ref = (refclass_c > 0 && refstat_k > 0) ? r.TN(refstat_k - 1, refclass_c - 1) : Tzero();
7245
7246 tput[row.aidx] = r.TN(row.node - 1, k);
7247 residt[row.aidx] = dbl(TN_ref) > GlobalConstants::FineTol
7248 ? T(r.QN(row.node - 1, k) / TN_ref)
7249 : r.WN(row.node - 1, k);
7250 if (!hasconverged) {
7251 servt[row.aidx] = r.RN(row.node - 1, k);
7252 servtproc[row.aidx] = Distrib<T>::exp_mean(servt[row.aidx]);
7253 const Distrib<T>& at = lqn.actthink[row.aidx];
7254 if (!at.disabled && dbl(at.mean) > GlobalConstants::FineTol) {
7255 servt[row.aidx] = T(servt[row.aidx] + at.mean);
7256 residt[row.aidx] = T(residt[row.aidx] + at.mean);
7257 servtproc[row.aidx] = Distrib<T>::exp_mean(servt[row.aidx]);
7258 }
7259 // An activity of an async-only entry takes the per-visit
7260 // response, for the same reason as in the default update.
7261 if (async_only_activity(row.aidx)) residt[row.aidx] = servt[row.aidx];
7262 } else {
7263 servtcdf[row.aidx] = layer_cdf(e)[row.node - 1][k];
7264 }
7265 }
7266
7267 for (const UpdRow& row : call_map) {
7268 if (row.node <= 1) continue; // a client-side call class contributes none
7269 const std::size_t e = std::size_t(idxhash[row.idx]);
7270 const LayerResult<T>& r = results.back()[e];
7271 callresidt[row.aidx] = r.WN(row.node - 1, row.cls - 1);
7272 if (!hasconverged)
7273 callservt[row.aidx] =
7274 T(r.RN(row.node - 1, row.cls - 1) * lqn.callproc_mean[row.aidx]);
7275 else
7276 callservtcdf[row.aidx] = layer_cdf(e)[row.node - 1][row.cls - 1];
7277 }
7278
7279 if (!hasconverged)
7280 moment3_means_pass(it);
7281 else
7282 moment3_distribution_pass(it);
7283 }
7284
7285 /** The mean-based pass of moment3, run while the ensemble is still moving. */
7286 void moment3_means_pass(int it) {
7287 const std::size_t dim = lqn.nidx + lqn.ncalls;
7288 std::vector<T> x(dim + 1, Tzero());
7289 for (std::size_t i = 1; i <= lqn.nidx; ++i) x[i] = residt[i];
7290 for (std::size_t c = 1; c <= lqn.ncalls; ++c) x[lqn.nidx + c] = callresidt[c];
7291
7292 // entry_servt = (I - servtmatrix) \ [residt; callresidt]
7293 const Matrix<T> Rinv = entry_service_resolvent();
7294 std::vector<T> entry_servt(dim + 1, Tzero());
7295 for (std::size_t i = 1; i <= dim; ++i) {
7296 T s = Tzero();
7297 for (std::size_t j = 1; j <= dim; ++j)
7298 if (Rinv(i, j) != Tzero()) s += Rinv(i, j) * x[j];
7299 entry_servt[i] = s;
7300 }
7301 for (std::size_t i = 1; i <= lqn.eshift; ++i) entry_servt[i] = Tzero();
7302
7303 // NO forwarding propagation here. `lqn_fwd_rendezvous` has already
7304 // reconnected every forwarding chain reachable from a synchronous call to
7305 // the client that issued the rendezvous (Franks 1999, Sec. 3.3.1), so the
7306 // forwarded service is in the caller's chain before this runs; charging it
7307 // again inflated the caller by exactly the forwarded entry's mean. An
7308 // asynchronous call into a chain is left untouched there by design -- a
7309 // send-no-reply does not block -- so it must not accumulate the forwarded
7310 // service either. See BUGS.md BUG-91.
7311
7312 for (std::size_t e = 1; e <= lqn.nentries; ++e)
7313 servt[lqn.eshift + e] = entry_servt[lqn.eshift + e];
7314
7315 // entry_residt = servtmatrix * [residt; callresidt]
7316 std::vector<T> entry_residt(dim + 1, Tzero());
7317 for (std::size_t i = lqn.eshift + 1; i <= lqn.eshift + lqn.nentries; ++i) {
7318 T s = Tzero();
7319 for (std::size_t j = 1; j <= dim; ++j)
7320 if (servtmatrix(i, j) != Tzero()) s += servtmatrix(i, j) * x[j];
7321 entry_residt[i] = s;
7322 }
7323
7324 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7325 const std::size_t eidx = lqn.eshift + e;
7326 T ratio = Tone();
7327 const int state = entry_visit_ratio(eidx, ratio);
7328 if (state == 0) continue;
7329 if (state == 1) {
7330 residt[eidx] = entry_residt[eidx];
7331 continue;
7332 }
7333 servt[eidx] = T(entry_servt[eidx] * ratio);
7334 residt[eidx] = T(entry_residt[eidx] * ratio);
7335 }
7336
7337 for (const UpdRow& row : call_map) {
7338 if (row.node <= 1) continue;
7339 const std::size_t eidx = lqn.callpair_dst[row.aidx];
7340 if (servt[eidx] > Tzero()) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
7341 }
7342 for (const UpdRow& row : call_map) {
7343 if (row.node <= 1) continue;
7344 const std::size_t eidx = lqn.callpair_dst[row.aidx];
7345 if (it == 1) {
7346 // A response time is per visit, so the number of calls is 1 here.
7347 callservt[row.aidx] = servt[eidx];
7348 callservtproc[row.aidx] = servtproc[eidx];
7349 } else if (callservt[row.aidx] > Tzero()) {
7350 callservtproc[row.aidx] = Distrib<T>::exp_mean(callservt[row.aidx]);
7351 }
7352 }
7353 }
7354
7355 /**
7356 * The distribution pass of moment3, run once on the converged ensemble.
7357 *
7358 * Every term reachable from an entry is fitted to an APH from the first
7359 * three moments of its response-time CDF, repeated as many times as the
7360 * entry-service matrix says it is visited, and the whole sequence is
7361 * convolved. A FRACTIONAL repetition count -- a call made 1.5 times on
7362 * average -- is realised as a branch between the fitted law and a point
7363 * mass at zero, which is the reference's `aph_simplify(..., pattern 3)`.
7364 */
7365 void moment3_distribution_pass(int it) {
7366 if constexpr (!num_traits<T>::has_transcendental) {
7367 throw UnsupportedError(ErrorKind{"Arithmetic"},
7368 "SolverLN: the 'moment3' method fits an APH to a response-time CDF, which needs "
7369 "square roots and a matrix exponential; rerun with --arith double or real");
7370 } else {
7371 const std::size_t dim = lqn.nidx + lqn.ncalls;
7372 const Matrix<T> Rinv = entry_service_resolvent();
7373
7374 // The point mass at zero the fractional branch mixes against.
7375 mam::AphPair<T> zero_law;
7376 zero_law.alpha.push_back(Tone());
7377 zero_law.S = Matrix<T>(1, 1, num_traits<T>::from_double(-GlobalConstants::Immediate));
7378
7379 for (std::size_t en = 1; en <= lqn.nentries; ++en) {
7380 const std::size_t eidx = lqn.eshift + en;
7381 std::vector<mam::AphPair<T>> seq;
7382 for (std::size_t fitidx = 1; fitidx <= dim; ++fitidx) {
7383 if (!(Rinv(eidx, fitidx) > Tzero())) continue;
7384 // Entries themselves are dropped: the entry's own service is
7385 // what is being assembled, and a host or task index carries
7386 // no response-time law of its own.
7387 if (fitidx <= lqn.eshift + lqn.nentries) continue;
7388 const bool is_call = fitidx > lqn.nidx;
7389 const fluid::FluidPassage& curve =
7390 is_call ? callservtcdf[fitidx - lqn.nidx] : servtcdf[fitidx];
7391 double m1 = 0.0, m2 = 0.0, m3 = 0.0;
7392 cdf_moments(curve, m1, m2, m3);
7393
7394 // An activity think time is in series with the host demand,
7395 // so its raw moments convolve with the measured ones.
7396 if (!is_call && !lqn.actthink[fitidx].disabled &&
7397 dbl(lqn.actthink[fitidx].mean) > GlobalConstants::FineTol) {
7398 const Distrib<T>& zt = lqn.actthink[fitidx];
7399 const double t1 = dbl(lang::dist_moment(zt, 1));
7400 const double t2 = dbl(lang::dist_moment(zt, 2));
7401 const double t3 = dbl(lang::dist_moment(zt, 3));
7402 m3 = m3 + 3.0 * m2 * t1 + 3.0 * m1 * t2 + t3;
7403 m2 = m2 + 2.0 * m1 * t1 + t2;
7404 m1 = m1 + t1;
7405 }
7406
7407 // CoarseTol and not FineTol: an Immediate activity has a
7408 // near-zero mean whose APH fit has rates of order 1e8, and
7409 // the matrix exponential of the convolution then does not
7410 // terminate. The reference skips those terms outright.
7411 if (!(m1 > GlobalConstants::CoarseTol)) continue;
7412
7413 const mam::AphFitResult<T> fit = mam::aph_fit(
7414 num_traits<T>::from_double(m1), num_traits<T>::from_double(m2),
7415 num_traits<T>::from_double(m3), 10u,
7416 num_traits<T>::from_double(GlobalConstants::FineTol));
7417 const mam::AphPair<T> law = aph_pair_of_map(fit.aph);
7418
7419 double reps = dbl(Rinv(eidx, fitidx));
7420 // servtmatrix carries 1.0 for a call; the mean NUMBER of
7421 // calls is what says how many times its law is convolved.
7422 if (is_call) reps *= dbl(lqn.callproc_mean[fitidx - lqn.nidx]);
7423 const long whole = static_cast<long>(std::floor(reps));
7424 const double frac = reps - static_cast<double>(whole);
7425 for (long q = 0; q < whole; ++q) seq.push_back(law);
7426 if (frac > 0.0)
7427 seq.push_back(mam::aph_simplify(law, zero_law,
7428 num_traits<T>::from_double(frac),
7429 num_traits<T>::from_double(1.0 - frac),
7431
7432 if (is_call) {
7433 const std::size_t cidx = fitidx - lqn.nidx;
7434 callservt[cidx] = num_traits<T>::from_double(m1);
7435 callservtproc[cidx] = Distrib<T>::exp_mean(callservt[cidx]);
7436 } else {
7437 servt[fitidx] = num_traits<T>::from_double(m1);
7438 servtproc[fitidx] = Distrib<T>::exp_mean(servt[fitidx]);
7439 }
7440 }
7441
7442 if (seq.empty()) {
7443 servt[eidx] = Tzero();
7444 continue;
7445 }
7446 const mam::AphPair<T> entry_law = mam::aph_convseq(seq);
7447 entryproc[en] = entry_law;
7448 servt[eidx] = Distrib<T>::ph_moment(entry_law.alpha, entry_law.S, 1);
7449 servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
7450 entrycdfrespt[en] = aph_eval_cdf(entry_law);
7451 }
7452
7453 // NO forwarding propagation here, for the reason given at the
7454 // entry_servt assembly above. See BUGS.md BUG-91.
7455
7456 // entry_residt = servtmatrix * [residt; callresidt], then the same
7457 // task/entry renormalisation the mean pass applies.
7458 std::vector<T> x(dim + 1, Tzero());
7459 for (std::size_t i = 1; i <= lqn.nidx; ++i) x[i] = residt[i];
7460 for (std::size_t c = 1; c <= lqn.ncalls; ++c) x[lqn.nidx + c] = callresidt[c];
7461 for (std::size_t en = 1; en <= lqn.nentries; ++en) {
7462 const std::size_t eidx = lqn.eshift + en;
7463 T s = Tzero();
7464 for (std::size_t j = 1; j <= dim; ++j)
7465 if (servtmatrix(eidx, j) != Tzero()) s += servtmatrix(eidx, j) * x[j];
7466 T ratio = Tone();
7467 const int state = entry_visit_ratio(eidx, ratio);
7468 if (state == 0) continue;
7469 residt[eidx] = state == 2 ? T(s * ratio) : s;
7470 }
7471
7472 if (it == 1)
7473 for (const UpdRow& row : call_map) {
7474 if (row.node <= 1) continue;
7475 const std::size_t eidx = lqn.callpair_dst[row.aidx];
7476 callservt[row.aidx] = servt[eidx];
7477 callservtproc[row.aidx] = Distrib<T>::exp_mean(servt[eidx]);
7478 }
7479
7480 // The entry servt is now the MEAN OF AN APH CONVOLUTION, not a sum
7481 // of residence times, which the interlock rescale in
7482 // `update_populations` has to know. See BUGS.md BUG-97.
7483 moment_pass_done = true;
7484 }
7485 }
7486
7487 /**
7488 * (alpha, S) of an APH handed back by aph_fit as a (D0, D1) pair.
7489 *
7490 * D1(i,j) = (-D0 e)_i alpha_j by construction, so alpha is any row of D1
7491 * divided by that row's exit rate; the first row with a positive exit rate
7492 * is taken. It is not recovered from the stationary phase distribution,
7493 * which is a different vector and would silently refit the law.
7494 */
7495 static mam::AphPair<T> aph_pair_of_map(const mam::Map<T>& m) {
7496 mam::AphPair<T> out;
7497 const std::size_t n = m.D0.rows();
7498 out.S = m.D0;
7499 out.alpha.assign(n, Tzero());
7500 for (std::size_t i = 0; i < n; ++i) {
7501 T rowsum = Tzero();
7502 for (std::size_t j = 0; j < n; ++j) rowsum += m.D1(i, j);
7503 if (!(dbl(rowsum) > GlobalConstants::Zero)) continue;
7504 for (std::size_t j = 0; j < n; ++j) out.alpha[j] = T(m.D1(i, j) / rowsum);
7505 return out;
7506 }
7507 // No phase can absorb: the law is degenerate, so it enters phase one and
7508 // stays there, which is what a zero alpha would NOT say.
7509 out.alpha[0] = Tone();
7510 return out;
7511 }
7512
7513 /**
7514 * F(t) = 1 - alpha exp(S t) e on the reference's grid, `APH.evalCDF` with
7515 * no argument: 500 points over [0, mean + 10 sigma].
7516 */
7517 static LnCdf aph_eval_cdf(const mam::AphPair<T>& law) {
7518 LnCdf out;
7519 const double m1 = dbl(Distrib<T>::ph_moment(law.alpha, law.S, 1));
7520 const double m2 = dbl(Distrib<T>::ph_moment(law.alpha, law.S, 2));
7521 const double var = m2 - m1 * m1;
7522 const double sigma = var > 0.0 ? std::sqrt(var) : 0.0;
7523 const double tmax = m1 + 10.0 * sigma;
7524 const std::size_t P = 500;
7525 out.t.resize(P);
7526 out.cdf.resize(P);
7527 for (std::size_t k = 0; k < P; ++k) {
7528 const double t = tmax * static_cast<double>(k) / static_cast<double>(P - 1);
7529 out.t[k] = t;
7530 Matrix<T> St(law.S.rows(), law.S.cols(), Tzero());
7531 for (std::size_t i = 0; i < law.S.rows(); ++i)
7532 for (std::size_t j = 0; j < law.S.cols(); ++j)
7533 St(i, j) = T(law.S(i, j) * num_traits<T>::from_double(t));
7534 const Matrix<T> E = ::line::expm(St);
7535 T surv = Tzero();
7536 for (std::size_t i = 0; i < E.rows(); ++i)
7537 for (std::size_t j = 0; j < E.cols(); ++j) surv += law.alpha[i] * E(i, j);
7538 out.cdf[k] = 1.0 - dbl(surv);
7539 }
7540 return out;
7541 }
7542
7543 /**
7544 * The AND-join completion times, and how much they undercut the serial sum.
7545 *
7546 * The branches of an AND fork run concurrently, so the time to clear the
7547 * join is the k-th smallest of the branch completion times, k being the
7548 * quorum. The entry-service reachability matrix cannot express that -- it
7549 * charges every activity of every branch to the entry, i.e. it serialises
7550 * them -- so the difference is recorded per join and applied as a
7551 * correction to any entry that reaches it.
7552 *
7553 * Branch times are taken as exponential, so the variance is the square of
7554 * the mean; that is the reference's assumption, not an approximation added
7555 * here.
7556 */
7557 std::vector<T> join_excess() {
7558 std::vector<T> excess(lqn.nidx + 1, Tzero());
7559 join_tail.assign(lqn.nidx + 1, Tzero());
7560 std::vector<std::size_t> joined;
7561 for (std::size_t tail = 1; tail <= lqn.nidx; ++tail) {
7562 if (lqn.actpretype[tail] != PrecedenceType::PRE_AND) continue;
7563 for (std::size_t sx : lqn.graph.succ(tail))
7564 if (sx > lqn.ashift && sx <= lqn.ashift + lqn.nacts) joined.push_back(sx);
7565 }
7566 std::sort(joined.begin(), joined.end());
7567 joined.erase(std::unique(joined.begin(), joined.end()), joined.end());
7568 if (joined.empty()) return excess;
7569
7570 fj::LqnBranchView<T> view;
7571 view.graph = Matrix<T>(lqn.nidx, lqn.nidx, Tzero());
7572 for (std::size_t i = 1; i <= lqn.nidx; ++i)
7573 for (const auto& e : lqn.graph.row[i]) view.graph(i - 1, e.first - 1) = e.second;
7574 view.ashift = lqn.ashift;
7575 view.nacts = lqn.nacts;
7576 view.actposttype.assign(lqn.nidx, 0);
7577 for (std::size_t i = 1; i <= lqn.nidx; ++i)
7578 view.actposttype[i - 1] = static_cast<int>(lqn.actposttype[i]);
7579
7580 for (std::size_t aidx : joined) {
7581 const std::vector<std::vector<std::size_t>> branches =
7582 fj::fj_branch_members(view, aidx);
7583 if (branches.empty()) continue;
7584 std::vector<T> means;
7585 for (const auto& br : branches) {
7586 T s = Tzero();
7587 for (std::size_t a : br) {
7588 s += residt[a];
7589 // A branch activity with an Immediate host demand does all its work
7590 // in a rendezvous, so residt alone leaves this correction inert.
7591 if (a < lqn.callsof.size())
7592 for (std::size_t cidx : lqn.callsof[a])
7593 if (cidx < lqn.calltype.size() && lqn.calltype[cidx] == CallType::SYNC
7594 && cidx < callresidt.size())
7595 s += callresidt[cidx];
7596 }
7597 means.push_back(s);
7598 }
7599 if (means.size() == 1) continue; // a single branch cannot overlap
7600 std::size_t quorum = means.size();
7601 if (lqn.actquorum[aidx] >= 1 && lqn.actquorum[aidx] <= means.size())
7602 quorum = lqn.actquorum[aidx];
7603 std::vector<T> vars;
7604 for (const T& m2 : means) vars.push_back(T(m2 * m2));
7605 T serial = Tzero();
7606 for (const T& m2 : means) serial += m2;
7607 if constexpr (num_traits<T>::has_transcendental) {
7608 const fj::FJQuorumMomentsResult<T> q = fj::fj_quorum_moments(means, vars, quorum);
7609 excess[aidx] = T(q.m - serial);
7610 if (quorum < means.size()) {
7611 // The n-k stragglers still hold the thread until the LAST branch ends.
7612 const fj::FJQuorumMomentsResult<T> all = fj::fj_quorum_moments(means, vars, means.size());
7613 if (all.m > q.m) join_tail[aidx] = T(all.m - q.m);
7614 }
7615 } else {
7616 throw UnsupportedError(ErrorKind{"Arithmetic"},
7617 "SolverLN: the AND-join completion time is a k-th order statistic fitted "
7618 "through a three-point distribution, which needs a square root; use the "
7619 "double or real backend for a model with an AND join");
7620 }
7621 }
7622 return excess;
7623 }
7624
7625 /** The block that turns activity residence times into entry service times. */
7626 void resolve_entry_service() {
7627 const std::size_t dim = lqn.nidx + lqn.ncalls;
7628 std::vector<T> x(dim + 1, Tzero());
7629 for (std::size_t i = 1; i <= lqn.nidx; ++i) x[i] = residt[i];
7630 for (std::size_t c = 1; c <= lqn.ncalls; ++c) x[lqn.nidx + c] = callresidt[c];
7631 std::vector<T> entry_servt(dim + 1, Tzero());
7632 for (std::size_t i = 1; i <= dim; ++i) {
7633 T s = Tzero();
7634 for (std::size_t j = 1; j <= dim; ++j)
7635 if (servtmatrix(i, j) != Tzero()) s += servtmatrix(i, j) * x[j];
7636 entry_servt[i] = s;
7637 }
7638
7639 // replace each reachable join's serialised branch sum by its concurrent
7640 // completion time
7641 const std::vector<T> excess = join_excess();
7642 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7643 const std::size_t eidx = lqn.eshift + e;
7644 for (std::size_t aidx = 1; aidx <= lqn.nidx; ++aidx) {
7645 if (excess[aidx] == Tzero()) continue;
7646 if (servtmatrix(eidx, aidx) > Tzero())
7647 entry_servt[eidx] = T(entry_servt[eidx] + excess[aidx]);
7648 }
7649 if (entry_servt[eidx] < Tzero()) entry_servt[eidx] = Tzero();
7650 // The quorum tail lengthens the thread's holding time but not the caller's wait
7651 for (std::size_t aidx = 1; aidx <= lqn.nidx; ++aidx)
7652 if (join_tail[aidx] > Tzero() && servtmatrix(eidx, aidx) > Tzero())
7653 entry_servt[eidx] = T(entry_servt[eidx] + join_tail[aidx]);
7654 }
7655
7656 // A SetupTask's cold start is charged HERE, to the entry. It is not host
7657 // demand, so it belongs to no activity's residence -- reporting it there
7658 // put RespT(A2) at 1.29479 against 0.333178 from LDES on lqn_setup, the
7659 // bare demand. The charge is computed ONCE per task from the base service
7660 // of a request, the sum of its entries' services before any charge, and
7661 // ph_setup_prob reads the same closure (updateMetricsDefault.m,
7662 // lqn_setup_charge.m).
7663 std::unordered_map<std::size_t, double> charge_su;
7664 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7665 const std::size_t tidx = lqn.parent[lqn.eshift + e];
7666 if (charge_su.count(tidx)) continue;
7667 double S = 0.0;
7668 for (std::size_t e2 = 1; e2 <= lqn.nentries; ++e2)
7669 if (lqn.parent[lqn.eshift + e2] == tidx) S += dbl(entry_servt[lqn.eshift + e2]);
7670 charge_su[tidx] = setup_charge(tidx, S);
7671 }
7672 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7673 const std::size_t eidx = lqn.eshift + e;
7674 const double c = charge_su[lqn.parent[eidx]];
7675 if (c != 0.0)
7676 entry_servt[eidx] = T(entry_servt[eidx] + num_traits<T>::from_double(c));
7677 }
7678
7679 // The phase-2 part of each entry's service: phase-2 activities and the calls they
7680 // make, through the same reachability. The overtaking block splits servt with it.
7681 if (has_phase2) {
7682 std::vector<T> x2(dim + 1, Tzero());
7683 for (std::size_t a = 1; a <= lqn.nacts; ++a)
7684 if (lqn.actphase[a] > 1) x2[lqn.ashift + a] = x[lqn.ashift + a];
7685 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
7686 const std::size_t src = lqn.callpair_src[c];
7687 if (src > lqn.ashift && src <= lqn.ashift + lqn.nacts && lqn.actphase[src - lqn.ashift] > 1)
7688 x2[lqn.nidx + c] = x[lqn.nidx + c];
7689 }
7690 entry_servt_ph2.assign(dim + 1, Tzero());
7691 for (std::size_t i = lqn.eshift + 1; i <= dim; ++i) {
7692 T s2 = Tzero();
7693 for (std::size_t j = 1; j <= dim; ++j)
7694 if (servtmatrix(i, j) != Tzero()) s2 += servtmatrix(i, j) * x2[j];
7695 entry_servt_ph2[i] = s2;
7696 }
7697 // and the quorum tail is all phase 2, see join_excess
7698 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7699 const std::size_t eidx = lqn.eshift + e;
7700 for (std::size_t aidx = 1; aidx <= lqn.nidx; ++aidx)
7701 if (join_tail[aidx] > Tzero() && servtmatrix(eidx, aidx) > Tzero())
7702 entry_servt_ph2[eidx] = T(entry_servt_ph2[eidx] + join_tail[aidx]);
7703 }
7704 entry_servt_last = entry_servt;
7705 }
7706
7707 // ResidT is normalised so the TASK has one visit; the entries need one
7708 // visit each, hence the throughput ratio below
7709 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
7710 const std::size_t eidx = lqn.eshift + e;
7711 const std::size_t tidx = lqn.parent[eidx];
7712 const std::size_t hidx = lqn.parent[tidx];
7713 if (ignore[tidx] || ignore[hidx]) continue;
7714 if (idxhash[hidx] < 0) continue;
7715 const bool has_sync = lqn.issynccaller.any_col(eidx);
7716 if (!has_sync) {
7717 servt[eidx] = entry_servt[eidx];
7718 residt[eidx] = entry_servt[eidx];
7719 continue;
7720 }
7721 const std::size_t hl = std::size_t(idxhash[hidx]);
7722 const qn::Layer<T>& L = ensemble[hl];
7723 const LayerResult<T>& r = results.back()[hl];
7724 T task_tput = Tzero(), entry_tput = Tzero();
7725 for (const auto& kv : L.attr_tasks)
7726 if (kv.second == tidx) task_tput += r.TN(L.clientIdx - 1, kv.first - 1);
7727 for (const auto& kv : L.attr_entries)
7728 if (kv.second == eidx) entry_tput += r.TN(L.clientIdx - 1, kv.first - 1);
7729 if (dbl(entry_tput) > GlobalConstants::Zero) {
7730 servt[eidx] = T(entry_servt[eidx] * task_tput / entry_tput);
7731 residt[eidx] = servt[eidx];
7732 // the same ratio the other way round, the entry's invocations per
7733 // task invocation, which a refpath hop stage converts its call terms by
7734 if (dbl(task_tput) > GlobalConstants::Zero)
7735 entryvisits[eidx] = T(entry_tput / task_tput);
7736 } else {
7737 servt[eidx] = entry_servt[eidx];
7738 residt[eidx] = entry_servt[eidx];
7739 }
7740 }
7741 }
7742
7743 // -----------------------------------------------------------------------
7744 // updatePopulations (interlock correction)
7745 // -----------------------------------------------------------------------
7746 /**
7747 * True when the layer engine applies Eq. (4.7) inside its own MVA. A layer whose MVA path
7748 * has no interlock term would be moved to another algorithm by the matrix alone: exact
7749 * multiserver MVA would become AMVA, the product-form kernels would become the
7750 * load-dependent forward step. That swap is worth far more than the correction it carries,
7751 * and on a layer sitting near a bifurcation it turns the LN iteration into a limit cycle.
7752 * Such a layer keeps the residt scaling instead.
7753 */
7754 bool layer_takes_interlock(std::size_t e) const {
7755 return opt.layer_solver == "mva" && e < ensemble.size() &&
7756 mva::mva_carries_interlock(ensemble[e], opt.layer);
7757 }
7758
7759 /**
7760 * Class-level interlock matrix of one host layer. IL[r][s] is the share of the class-s
7761 * queue that a class-r arrival must not see at the host. It is kept CLASS-indexed, not
7762 * chain-indexed, so that a later chain refresh cannot leave it stale: the analyzer
7763 * aggregates it to chains against the struct it is about to solve. Two classes are
7764 * interlocked only if BOTH their tasks are, which is the 0/1 relation ir_mkj of Eq. (5);
7765 * the diagonal stays zero, since a request always sees its own class in full. The entry
7766 * is the Eq. (5) product Pr(IL_ms)*IR_ms*IR_mr, asymmetric in (r,s) because Pr(IL) is
7767 * taken from the QUEUED class s, so that the layer's ILw(r,s) = 1-IL(r,s) is the
7768 * lower-level adjustment rate r_lower. An empty result keeps the layer on the plain MVA
7769 * path.
7770 */
7771 std::vector<std::vector<double>> build_layer_interlock(std::size_t e,
7772 const std::vector<std::size_t>& hts,
7773 const std::vector<T>& prIL,
7774 const std::vector<T>& PrIL) const {
7775 const qn::Layer<T>& L = ensemble[e];
7776 const std::size_t R = L.nclasses;
7777 std::vector<double> cls_ir(R, 0.0); // IR
7778 std::vector<double> cls_pr(R, 0.0); // Pr(IL)
7779 auto stamp = [&](std::size_t classIdx, std::size_t tidx) {
7780 if (classIdx < 1 || classIdx > R) return;
7781 for (std::size_t i = 0; i < hts.size(); ++i)
7782 if (hts[i] == tidx) {
7783 cls_ir[classIdx - 1] = dbl(prIL[i]);
7784 cls_pr[classIdx - 1] = dbl(PrIL[i]);
7785 }
7786 };
7787 for (const auto& a : L.attr_tasks) stamp(a.first, a.second);
7788 for (const auto& a : L.attr_entries) stamp(a.first, lqn.parent[a.second]);
7789 for (const auto& a : L.attr_activities) stamp(a.first, lqn.parent[a.second]);
7790 for (const auto& a : L.attr_calls) stamp(a[0], lqn.parent[a[2]]);
7791
7792 std::vector<std::vector<double>> IL(R, std::vector<double>(R, 0.0));
7793 bool any = false;
7794 for (std::size_t r = 0; r < R; ++r) {
7795 if (!(cls_ir[r] > GlobalConstants::FineTol)) continue;
7796 for (std::size_t sIl = 0; sIl < R; ++sIl) {
7797 if (sIl == r || !(cls_ir[sIl] > GlobalConstants::FineTol)) continue;
7798 IL[r][sIl] = cls_pr[sIl] * cls_ir[sIl] * cls_ir[r];
7799 if (IL[r][sIl] > GlobalConstants::FineTol) any = true;
7800 }
7801 }
7802 return any ? IL : std::vector<std::vector<double>>();
7803 }
7804
7805 /**
7806 * Interlock probability for one (client, server) pair, as {IR, Pr(IL)} of Li and Franks,
7807 * "An improved interlocking correction for decomposition of layered queueing networks",
7808 * CCECE 2015, Eqs. (3) and (4). isProcessorHost selects the m' rule of lqns
7809 * Interlock::ilrate_pril_flow: at a PROCESSOR the common-source population is doubled
7810 * above 3 customers and squared at or below it, which is what turns m = 4 into the
7811 * pril = 1/8 its trace reports. The two factors are multiplied into the Eq. (5) rate by
7812 * build_layer_interlock, so neither carries the source count on its own -- that lives in
7813 * m'. This replaces the superseded (n_s-1)/n_s discount of Franks (1999), Eq. (4.7).
7814 */
7815 std::pair<T, T> interlock_prob(std::size_t client_tidx, std::size_t server_idx,
7816 bool isProcessorHost) const {
7817 const std::vector<std::size_t>& common = il_common_entries[server_idx];
7818 const double nsrc = il_num_sources[server_idx];
7819 if (nsrc == 0.0 || common.empty()) return std::make_pair(Tzero(), Tzero());
7820 const std::vector<std::size_t>& allsrc = il_src_all[server_idx];
7821 T sum_flow = Tzero();
7822 T sum_pril = Tzero();
7823 for (std::size_t ce : common) {
7824 const std::size_t srcTask = lqn.parent[ce];
7825 const std::size_t cen = ce - lqn.eshift;
7826 // population of this common source, in customer copies
7827 double m_src = lqn.mult[srcTask];
7828 if (!std::isfinite(m_src) || m_src < 1.0) m_src = 1.0;
7829 double m_eff = m_src;
7830 if (isProcessorHost) m_eff = (m_src > 3.0) ? (m_src + m_src) : (m_src * m_src);
7831 for (std::size_t dst : lqn.entriesof[client_tidx]) {
7832 const std::size_t dn = dst - lqn.eshift;
7833 if (dn < 1 || dn > lqn.nentries) continue;
7834 if (!(il_all(cen, dn) > Tzero())) continue;
7835 T ce_tput = tput[ce];
7836 if (!(dbl(ce_tput) > GlobalConstants::FineTol) && !lqn.actsof[ce].empty())
7837 ce_tput = tput[lqn.actsof[ce][0]];
7838 if (!(dbl(ce_tput) > GlobalConstants::FineTol)) ce_tput = tput[srcTask];
7839 if (!(dbl(ce_tput) > GlobalConstants::FineTol)) continue;
7840 // phase-2 entries are refused upstream, so every source is all-phase
7841 if (std::find(allsrc.begin(), allsrc.end(), srcTask) != allsrc.end()) {
7842 const T contrib = T(ce_tput * il_all(cen, dn));
7843 sum_flow += contrib;
7844 sum_pril += T(contrib / num_traits<T>::from_double(m_eff));
7845 }
7846 }
7847 }
7848 T client_tput = tput[client_tidx];
7849 if (!(dbl(client_tput) > GlobalConstants::FineTol)) {
7850 for (std::size_t e : lqn.entriesof[client_tidx]) {
7851 T et = tput[e];
7852 if (!(dbl(et) > GlobalConstants::FineTol) && !lqn.actsof[e].empty())
7853 et = tput[lqn.actsof[e][0]];
7854 client_tput = T(client_tput + et);
7855 }
7856 }
7857 if (!(dbl(client_tput) > GlobalConstants::FineTol))
7858 return std::make_pair(Tzero(), Tzero());
7859 const T flow = sum_flow < client_tput ? sum_flow : client_tput;
7860 T IR = T(flow / client_tput);
7861 if (IR > Tone()) IR = Tone();
7862 if (dbl(IR) < 0.0) IR = Tzero();
7863 T pr = Tzero();
7864 if (dbl(sum_flow) > GlobalConstants::FineTol) pr = T(sum_pril / sum_flow);
7865 if (pr > Tone()) pr = Tone();
7866 if (dbl(pr) < 0.0) pr = Tzero();
7867 return std::make_pair(IR, pr);
7868 }
7869
7870 void update_populations(int) {
7871 const std::vector<T> residt_orig = residt;
7872 const std::vector<T> callresidt_orig = callresidt;
7873 bool adjusted = false;
7874
7875 // Every layer starts the pass without a matrix, so a server that stops being
7876 // interlocked does not keep the previous iteration's correction alive.
7877 layer_interlock.assign(ensemble.size(), {});
7878
7879 // Pass 0: a TASK layer whose MVA carries Eq. (4.7) takes the interlock as a class
7880 // matrix, as a host layer does below. Correcting its calls after the solve (pass 1)
7881 // shortens the callers' residence while the layer's own throughput keeps the full
7882 // waiting, so the server's throughput stops matching the sum of its callers'
7883 // (BUGS.md BUG-103).
7884 std::vector<bool> il_in_layer(lqn.nidx + 1, false);
7885 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
7886 const std::size_t server_tidx = lqn.tshift + t;
7887 if (server_tidx >= il_common_entries.size() || il_common_entries[server_tidx].empty()) continue;
7888 if (!(idxhash[server_tidx] >= 0) || !layer_takes_interlock(std::size_t(idxhash[server_tidx])))
7889 continue;
7890 const std::size_t e = std::size_t(idxhash[server_tidx]);
7891 std::vector<std::size_t> cts;
7892 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
7893 if (lqn.calltype[cidx] != CallType::SYNC) continue;
7894 if (lqn.parent[lqn.callpair_dst[cidx]] != server_tidx) continue;
7895 const std::size_t ctidx = lqn.parent[lqn.callpair_src[cidx]];
7896 if (std::find(cts.begin(), cts.end(), ctidx) == cts.end()) cts.push_back(ctidx);
7897 }
7898 std::vector<T> prIL(cts.size(), Tzero()), PrIL(cts.size(), Tzero());
7899 for (std::size_t i = 0; i < cts.size(); ++i) {
7900 const std::pair<T, T> ilp = interlock_prob(cts[i], server_tidx, false);
7901 prIL[i] = ilp.first;
7902 PrIL[i] = ilp.second;
7903 }
7904 il_in_layer[server_tidx] = true;
7905 layer_interlock[e] = build_layer_interlock(e, cts, prIL, PrIL);
7906 }
7907
7908 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
7909 if (lqn.calltype[cidx] != CallType::SYNC) continue;
7910 const std::size_t dst = lqn.callpair_dst[cidx];
7911 const std::size_t server_tidx = lqn.parent[dst];
7912 std::size_t server = 0;
7913 if (server_tidx <= NT() && !il_common_entries[server_tidx].empty()) {
7914 server = server_tidx;
7915 } else if (server_tidx > lqn.tshift) {
7916 const std::size_t h = lqn.parent[server_tidx];
7917 if (h >= 1 && h <= NT() && !il_common_entries[h].empty()) server = h;
7918 }
7919 if (server == 0 || il_in_layer[server]) continue;
7920 // A HOST whose layer carries Eq. (4.7) in its own MVA (Pass 2 hands it the matrix)
7921 // has already discounted the processor queueing this call's residence reads, so
7922 // scaling the call again removes the same waiting twice and shortens the caller's
7923 // cycle while the callee's layer keeps the full wait: flow stopped balancing across
7924 // the call, above the demand bound lqsim sits on (BUGS.md BUG-107).
7925 if (server <= lqn.nhosts && idxhash[server] >= 0 &&
7926 layer_takes_interlock(std::size_t(idxhash[server])))
7927 continue;
7928 const std::size_t client_tidx = lqn.parent[lqn.callpair_src[cidx]];
7929 // This path serves a TASK, not a processor, so the m' rule of Li/lqns leaves the
7930 // source population alone; the product IR*Pr(IL) reproduces the scalar this
7931 // branch used before.
7932 const std::pair<T, T> ilp = interlock_prob(client_tidx, server, false);
7933 const T prIL = T(ilp.first * ilp.second);
7934 if (!(dbl(prIL) > GlobalConstants::FineTol)) continue;
7935 const T S = servt[dst];
7936 const T cm = lqn.callproc_mean[cidx];
7937 if (!(cm > Tzero()) || !(callservt[cidx] > Tzero())) continue;
7938 const T RN = T(callservt[cidx] / cm);
7939 const T W = RN > S ? T(RN - S) : Tzero();
7940 if (!(dbl(W) > GlobalConstants::FineTol)) continue;
7941 const T RN_adj = T(S + (Tone() - prIL) * W);
7942 const T scale = T(RN_adj / RN);
7943 callservt[cidx] = T(callservt[cidx] * scale);
7944 callresidt[cidx] = T(callresidt[cidx] * scale);
7945 if (callservt[cidx] > Tzero())
7946 callservtproc[cidx] = Distrib<T>::exp_mean(callservt[cidx]);
7947 adjusted = true;
7948 }
7949
7950 for (std::size_t h = 1; h <= lqn.nhosts; ++h) {
7951 if (il_common_entries[h].empty()) continue;
7952 const std::vector<std::size_t>& hts = lqn.tasksof[h];
7953 std::vector<T> prIL(hts.size(), Tzero()), PrIL(hts.size(), Tzero()),
7954 tutil(hts.size(), Tzero());
7955 for (std::size_t i = 0; i < hts.size(); ++i) {
7956 // The host of a task layer is a PROCESSOR, which is what selects the m' rule.
7957 const std::pair<T, T> ilp = interlock_prob(hts[i], h, true);
7958 prIL[i] = ilp.first;
7959 PrIL[i] = ilp.second;
7960 for (std::size_t e : lqn.entriesof[hts[i]])
7961 for (std::size_t a : lqn.actsof[e]) tutil[i] += tput[a] * lqn.hostdem[a].mean;
7962 }
7963 T Utot = Tzero(), Uil = Tzero();
7964 for (std::size_t i = 0; i < hts.size(); ++i) {
7965 Utot += tutil[i];
7966 if (dbl(prIL[i]) > GlobalConstants::FineTol) Uil += tutil[i];
7967 }
7968 if (!(dbl(Utot) > GlobalConstants::FineTol) || !(dbl(Uil) > GlobalConstants::FineTol))
7969 continue;
7970 const T frac = T(Uil / Utot);
7971
7972 // When the layer engine carries Eq. (4.7) inside its own MVA, the interlock goes
7973 // to the layer as a class-level matrix and the residence times are left
7974 // untouched. Scaling them here as well would remove the same waiting twice, and
7975 // would still leave the layer's own THROUGHPUT uncorrected, which is what breaks
7976 // flow balance across a call: the reported task rate then comes from a cycle time
7977 // the correction has already shortened elsewhere.
7978 if (idxhash[h] >= 0 && layer_takes_interlock(std::size_t(idxhash[h]))) {
7979 const std::size_t e = std::size_t(idxhash[h]);
7980 layer_interlock[e] = build_layer_interlock(e, hts, prIL, PrIL);
7981 continue;
7982 }
7983
7984 for (std::size_t i = 0; i < hts.size(); ++i) {
7985 if (!(dbl(prIL[i]) > GlobalConstants::FineTol)) continue;
7986 // Weight by the share of host utilization that is interlocked. The rate
7987 // is the SAME Eq. (5) product IR*Pr(IL) that pass 1 applies to a call and
7988 // that build_layer_interlock puts in the layer matrix -- IR alone is a
7989 // flow SHARE, ~1 whenever a layer has a single common source, and using
7990 // it here removed the whole processor queueing rather than the
7991 // interlocked part of it, which broke flow balance across a call.
7992 const T eff = T(prIL[i] * PrIL[i] * frac);
7993 for (std::size_t e : lqn.entriesof[hts[i]])
7994 for (std::size_t a : lqn.actsof[e]) {
7995 // D is per VISIT and residt per CYCLE (visits times response): take the
7996 // waiting from the per-visit servt and apply the cut as a ratio, as pass 1
7997 // does for a call. Subtracting D from residt dropped a loop's visit count.
7998 const T D = lqn.hostdem[a].mean;
7999 const T R = servt[a];
8000 if (D > Tzero() && dbl(R - D) > GlobalConstants::FineTol) {
8001 residt[a] = T(residt[a] * (D + (Tone() - eff) * (R - D)) / R);
8002 adjusted = true;
8003 }
8004 }
8005 }
8006 }
8007
8008 if (!adjusted) return;
8009 const std::size_t dim = lqn.nidx + lqn.ncalls;
8010 auto esum = [&](const std::vector<T>& rs, const std::vector<T>& cr, std::size_t i) {
8011 T s = Tzero();
8012 for (std::size_t j = 1; j <= dim; ++j) {
8013 if (servtmatrix(i, j) == Tzero()) continue;
8014 s += servtmatrix(i, j) * (j <= lqn.nidx ? rs[j] : cr[j - lqn.nidx]);
8015 }
8016 return s;
8017 };
8018 // The split update_metrics uses: phase-2 activities and the calls they make. A
8019 // correction landing in phase 2 must not shrink phase 1, and the caller-visible
8020 // residt is rebuilt from the two parts rather than scaled with the whole entry
8021 // (BUGS.md BUG-103).
8022 std::vector<bool> isph2(dim + 1, false);
8023 if (has_phase2) {
8024 for (std::size_t a = 1; a <= lqn.nacts; ++a)
8025 if (lqn.actphase[a] > 1) isph2[lqn.ashift + a] = true;
8026 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
8027 const std::size_t src = lqn.callpair_src[c];
8028 if (src > lqn.ashift && src <= lqn.ashift + lqn.nacts && lqn.actphase[src - lqn.ashift] > 1)
8029 isph2[lqn.nidx + c] = true;
8030 }
8031 }
8032 auto esum2 = [&](const std::vector<T>& rs, const std::vector<T>& cr, std::size_t i) {
8033 T s = Tzero();
8034 for (std::size_t j = 1; j <= dim; ++j) {
8035 if (!isph2[j] || servtmatrix(i, j) == Tzero()) continue;
8036 s += servtmatrix(i, j) * (j <= lqn.nidx ? rs[j] : cr[j - lqn.nidx]);
8037 }
8038 return s;
8039 };
8040 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
8041 const std::size_t eidx = lqn.eshift + e;
8042 const T oldv = esum(residt_orig, callresidt_orig, eidx);
8043 if (!(dbl(oldv) > GlobalConstants::FineTol)) continue;
8044 const T ratio = T(esum(residt, callresidt, eidx) / oldv);
8045 if (has_phase2 && !moment_pass_done && dbl(servt_ph2[eidx]) > GlobalConstants::FineTol) {
8046 const T old2 = esum2(residt_orig, callresidt_orig, eidx);
8047 const T new2 = esum2(residt, callresidt, eidx);
8048 const T old1 = T(oldv - old2);
8049 const T new1 = T(esum(residt, callresidt, eidx) - new2);
8050 const T r1 = dbl(old1) > GlobalConstants::FineTol ? T(new1 / old1) : Tone();
8051 const T r2 = dbl(old2) > GlobalConstants::FineTol ? T(new2 / old2) : Tone();
8052 servt_ph1[eidx] = T(servt_ph1[eidx] * r1);
8053 servt_ph2[eidx] = T(servt_ph2[eidx] * r2);
8054 servt[eidx] = T(servt_ph1[eidx] + servt_ph2[eidx]);
8055 if (servt[eidx] > Tzero()) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
8056 // residt as update_metrics builds it: the full servt for a reference task or an
8057 // entry with no synchronous caller, else phase 1 plus the overtaken share of phase 2
8058 const std::size_t tidx = lqn.parent[eidx];
8059 if (lqn.isref[tidx] || !lqn.issynccaller.any_col(eidx))
8060 residt[eidx] = servt[eidx];
8061 else
8062 residt[eidx] = T(servt_ph1[eidx] + prOvertake[e] * servt_ph2[eidx]);
8063 continue;
8064 }
8065 // The entry servt is rescaled only when it was itself assembled from
8066 // these residence times, which is the mean-based path. After the
8067 // moment3 distribution pass it is the mean of an APH convolution of
8068 // the activities' own response laws, and a ratio of residence-time
8069 // sums is not a correction to it: applying it multiplied the entry
8070 // law by the entry's visit ratio and reported a service time BELOW
8071 // that of the single activity the entry contains. The residence
8072 // times keep their correction either way. See BUGS.md BUG-97.
8073 if (!moment_pass_done) {
8074 servt[eidx] = T(servt[eidx] * ratio);
8075 if (servt[eidx] > Tzero()) servtproc[eidx] = Distrib<T>::exp_mean(servt[eidx]);
8076 }
8077 residt[eidx] = T(residt[eidx] * ratio);
8078 }
8079 }
8080
8081 // -----------------------------------------------------------------------
8082 // Declared think time of a task as it enters the thread cycle: the value for
8083 // a REFERENCE task, zero for any other.
8084 //
8085 // A think time is an attribute of the closed customer population a reference
8086 // task stands for, and it is what separates one request of that population
8087 // from the next. On a served task it has no such meaning, and charging it
8088 // per request throttles the task: lqn_basic's T3 has 25 threads and a
8089 // declared think time of 4, and reading it as a per-request delay caps it at
8090 // 25/(4+0.02) = 6.219 completions per second. Three independent oracles put
8091 // the rate at five calls per caller request instead -- lqsim 66.5, LDES
8092 // 66.955, lqns 75.6. See _kb/06-solver-catalog.md (LN section).
8093 T ref_think_time(std::size_t tidx) const {
8094 if (tidx >= lqn.isref.size() || !lqn.isref[tidx]) return Tzero();
8095 if (lqn.think[tidx].disabled) return Tzero();
8096 return lqn.think[tidx].mean;
8097 }
8098
8099 // updateThinkTimes
8100 // -----------------------------------------------------------------------
8101 /**
8102 * Mean time per request that a thread of task TIDX stays busy after replying, i.e. servt - residt
8103 * averaged over its phase-2 entries by their throughput (phase2Tail in updateThinkTimes.m).
8104 */
8105 T phase2_tail(std::size_t tidx) const {
8106 T tail = Tzero();
8107 if (!has_phase2) return tail;
8108 T wtot = Tzero();
8109 const T fine = num_traits<T>::from_double(GlobalConstants::FineTol);
8110 for (std::size_t eidx : lqn.entriesof[tidx]) {
8111 if (servt_ph2[eidx] <= fine) continue;
8112 T w = tput[eidx];
8113 if (w <= fine) w = Tone();
8114 const T d = T(servt[eidx] - residt[eidx]);
8115 tail += T(w * (d > Tzero() ? d : Tzero()));
8116 wtot += w;
8117 }
8118 if (wtot > fine) tail = T(tail / wtot);
8119 return tail;
8120 }
8121
8122 void update_think_times(int it) {
8123 // Under "srvn.ph" a caller reaches the server once per invocation, so the
8124 // station rate is not the task's invocation rate
8125 if (is_ph_encoding()) {
8126 update_think_times_ph(it);
8127 return;
8128 }
8129 thinktproc.assign(lqn.nidx + 1, Distrib<T>::disabled_dist());
8130 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
8131 const std::size_t tidx = lqn.tshift + t;
8132 // only a REFERENCE task's think time is a per-request delay
8133 const T ztask = ref_think_time(tidx);
8134 if (idxhash[tidx] < 0) {
8135 // A task reached only by an entry arrival still has a cycle: its threads
8136 // are driven by the stream. build_layer drops the open class for it so
8137 // the chain can be closed on the known rate here.
8138 const double arvrate = open_arrival_rate_of(tidx);
8139 if (arvrate > GlobalConstants::FineTol) {
8140 double nja = 0.0;
8141 for (std::size_t c = 1; c <= NT(); ++c) nja = std::max(nja, njobs(tidx, c));
8142 if (!(nja > 0.0)) nja = lqn.maxmult[tidx];
8143 T hres = Tzero();
8144 for (std::size_t eidx : lqn.entriesof[tidx]) {
8145 for (std::size_t aidx : lqn.actsof[eidx])
8146 if (!std::isnan(dbl(residt[aidx]))) hres += residt[aidx];
8147 hres += sync_call_residt_of(lqn.actsof[eidx]);
8148 }
8149 const T floora = num_traits<T>::from_double(GlobalConstants::Zero);
8150 T za = T(num_traits<T>::from_double(nja / arvrate) - hres - ztask);
8151 if (za < floora) za = floora;
8152 if (relax_omega < 1.0 && it > 1 && !std::isnan(thinkt_prev[tidx])) {
8153 const T om = num_traits<T>::from_double(relax_omega);
8154 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
8155 za = T(om * za + om1 * thinkt_prev_v[tidx]);
8156 }
8157 tput[tidx] = num_traits<T>::from_double(arvrate);
8158 thinkt[tidx] = za;
8159 thinkt_prev[tidx] = dbl(za);
8160 thinkt_prev_v[tidx] = za;
8161 thinktproc[tidx] = Distrib<T>::exp_mean(T(za + ztask));
8162 continue;
8163 }
8164 thinkt[tidx] = num_traits<T>::from_double(GlobalConstants::FineTol);
8165 thinktproc[tidx] = Distrib<T>::immediate();
8166 continue;
8167 }
8168 const std::size_t e = std::size_t(idxhash[tidx]);
8169 const qn::Layer<T>& L = ensemble[e];
8170 const LayerResult<T>& r = results.back()[e];
8171 double nj = 0.0;
8172 for (std::size_t c = 1; c <= NT(); ++c) nj = std::max(nj, njobs(tidx, c));
8173 // the task's OWN station, which under `flat.cs` is one of many
8174 const std::size_t ts = station_idx_of(L, tidx);
8175 T tp = Tzero();
8176 for (std::size_t k = 0; k < L.nclasses; ++k) tp += r.TN(ts - 1, k);
8177 tput[tidx] = T(num_traits<T>::from_double(lqn.repl[tidx]) * tp);
8178 // the thread busy fraction, not a dependent station's peak-normalized Util
8179 util[tidx] = thread_busy_fraction(L, ts, r, false);
8180 // The closure is a cycle of ONE replica's threads, so both sides are per replica: the rate is the
8181 // total divided back down, and the pool is maxmult, since njobs keeps whichever convention the
8182 // caller's layer used. Inert at repl 1 (updateThinkTimes.m).
8183 const double nrep = std::max(1.0, lqn.repl[tidx]);
8184 const T xcyc = T(tput[tidx] / num_traits<T>::from_double(nrep));
8185 if (nrep > 1.0 && std::isfinite(lqn.maxmult[tidx]) && lqn.maxmult[tidx] > 0.0) nj = lqn.maxmult[tidx];
8186 T raw;
8187 if (lqn.sched[tidx] == SchedStrategy::INF) {
8188 // an infinite server reports utilization as a mean job count
8189 raw = xcyc == Tzero()
8190 ? Tzero()
8191 : T((num_traits<T>::from_double(nj) - util[tidx]) / xcyc - ztask);
8192 } else {
8193 const T om = util[tidx] > Tone() ? T(util[tidx] - Tone()) : T(Tone() - util[tidx]);
8194 raw = xcyc == Tzero()
8195 ? Tzero()
8196 : T(num_traits<T>::from_double(nj) * om / xcyc - ztask);
8197 }
8198 const T floorv = num_traits<T>::from_double(GlobalConstants::Zero);
8199 thinkt[tidx] = raw < floorv ? floorv : raw;
8200
8201 // Phase-2 tail: the entry replies after phase 1, so the station serves the caller for residt while the
8202 // thread stays busy for servt. What is left is busy time, not think time (updateThinkTimes.m).
8203 if (lnmethod != "flat.cs") {
8204 const T lessz = T(thinkt[tidx] - phase2_tail(tidx));
8205 thinkt[tidx] = lessz < floorv ? floorv : lessz;
8206 }
8207
8208 // The cold start goes the OTHER way from the phase-2 tail. A caller
8209 // class cycles as delay plus station service, and the station serves
8210 // only the host demand: the charge is on the ENTRY, not on any
8211 // activity's demand, so the station never sees it and the delay has
8212 // to carry it. Without this the callee layer cycles faster than its
8213 // callers drive it -- 0.529412 against 0.5 on lqn_setup, with the
8214 // caller conserved and the callee not. Zero for a task with no setup
8215 // (updateThinkTimes.m).
8216 {
8217 const double c = setup_charge(tidx);
8218 if (c != 0.0) {
8219 const T withc = T(thinkt[tidx] + num_traits<T>::from_double(c));
8220 thinkt[tidx] = withc < floorv ? floorv : withc;
8221 }
8222 }
8223
8224 if (relax_omega < 1.0 && it > 1 && !std::isnan(thinkt_prev[tidx])) {
8225 const double rawd = dbl(thinkt[tidx]);
8226 if (thinkt_prev[tidx] > 10.0 * rawd && rawd > GlobalConstants::FineTol) {
8227 thinkt_prev[tidx] = rawd;
8228 thinkt_prev_v[tidx] = thinkt[tidx];
8229 }
8230 const T om = num_traits<T>::from_double(relax_omega);
8231 const T om1 = num_traits<T>::from_double(1.0 - relax_omega);
8232 thinkt[tidx] = T(om * thinkt[tidx] + om1 * thinkt_prev_v[tidx]);
8233 }
8234 thinkt_prev[tidx] = dbl(thinkt[tidx]);
8235 thinkt_prev_v[tidx] = thinkt[tidx];
8236 thinktproc[tidx] = Distrib<T>::exp_mean(T(thinkt[tidx] + ztask));
8237 }
8238 }
8239
8240 // -----------------------------------------------------------------------
8241 // updateLayers and updateRoutingProbabilities
8242 // -----------------------------------------------------------------------
8243 void update_layers(int it) {
8244 // Under "srvn.ph" the layer classes are one per caller task and their laws
8245 // are composed, not read off the update maps
8246 if (is_ph_encoding()) {
8247 update_layers_ph(it);
8248 return;
8249 }
8250 const bool elevator = (it % 2) == 1;
8251 const std::size_t nt = thinkt_map.size();
8252 for (std::size_t r = 0; r < nt; ++r) {
8253 const UpdRow& row = thinkt_map[elevator ? nt - 1 - r : r];
8254 qn::Layer<T>& L = ensemble[std::size_t(idxhash[row.idx])];
8255 if (row.aidx <= NT() && opt.interlocking &&
8256 L.classes[row.cls - 1].type == JobClassType::CLOSED)
8257 L.classes[row.cls - 1].population = njobs(row.aidx, row.idx);
8258 if (row.node == L.clientIdx) {
8259 if (lqn.type[row.aidx] == LqnElement::TASK) {
8260 if (lqn.sched[row.aidx] != SchedStrategy::REF) {
8261 if (!thinktproc[row.aidx].disabled)
8262 L.set_service(row.node, row.cls, thinktproc[row.aidx]);
8263 } else {
8264 L.set_service(row.node, row.cls, servtproc[row.aidx]);
8265 }
8266 } else {
8267 L.set_service(row.node, row.cls, servtproc[row.aidx]);
8268 }
8269 } else {
8270 L.set_service(row.node, row.cls, servtproc[row.aidx]);
8271 }
8272 }
8273 const std::size_t nc = call_map.size();
8274 for (std::size_t r = 0; r < nc; ++r) {
8275 const UpdRow& row = call_map[elevator ? nc - 1 - r : r];
8276 qn::Layer<T>& L = ensemble[std::size_t(idxhash[row.idx])];
8277 if (row.node == L.clientIdx) {
8278 L.set_service(row.node, row.cls, callservtproc[row.aidx]);
8279 } else {
8280 const std::size_t eidx = lqn.callpair_dst[row.aidx];
8281 // A phase-2 entry replies before phase 2 runs, so the caller is held for residt, not servt; charging
8282 // servt here while the caller's layer charges residt breaks flow conservation across the call.
8283 // Flat layering shares one model between caller and callee and takes no correction (updateLayers.m).
8284 if (has_phase2 && lnmethod != "flat.cs" && dbl(servt_ph2[eidx]) > GlobalConstants::FineTol)
8285 L.set_service(row.node, row.cls, Distrib<T>::exp_mean(residt[eidx]));
8286 else
8287 L.set_service(row.node, row.cls, servtproc[eidx]);
8288 }
8289 }
8290 // Async arrival rate: the caller activity fires at its own throughput,
8291 // and every firing releases callmean jobs into this layer's Source.
8292 // updateLayers.m:63-73 replays the same map; the geometric self-loop in
8293 // build_layer already accounts for callmean, so the rate is the bare
8294 // activity throughput, not scaled by it.
8295 //
8296 // The reference-path stage means go first, after the think times:
8297 // residt, callresidt, util and tput are all fresh by now.
8298 if (interlockMethod == "refpath") update_ref_path_stages(it);
8299 for (const UpdRow& row : arv_call_map) {
8300 qn::Layer<T>& L = ensemble[std::size_t(idxhash[row.idx])];
8301 const Distrib<T>& d = tputproc[lqn.callpair_src[row.aidx]];
8302 if (!d.disabled) L.set_service(row.node, row.cls, d);
8303 }
8304 }
8305
8306 /**
8307 * Port of updateRefPathStages: recompute every reference-path stage mean.
8308 *
8309 * A HOP stage stands for an entry whose task has no activity graph in the
8310 * layer: residt(e), minus callresidt(c)/entryvisits(e) for each descent the
8311 * chain re-expands below it, plus the wait W for one of its task's threads.
8312 * A GATE into a MEMBER charges W(callee task) alone, since everything else
8313 * the member owns is explicit in the layer. W is taken in the POPULATION
8314 * domain, never as the difference of two nearly equal response times.
8315 */
8316 void update_ref_path_stages(int it) {
8317 if (refpath_map.empty()) return;
8318 const std::size_t nkeys = refpath_keys.size();
8319 std::vector<T> stage(nkeys, Tzero());
8320 for (std::size_t r = 0; r < refpath_map.size(); ++r) {
8321 const RefPathRow& row = refpath_map[r];
8322 T term = Tzero();
8323 switch (row.kind) {
8324 case 1: // the entry's own residence, per entry invocation
8325 term = residt[row.elem];
8326 break;
8327 case 2: { // a re-expanded descent, converted to the entry scale
8328 T v = Tone();
8329 if (row.fromentry >= 1 && dbl(entryvisits[row.fromentry]) > GlobalConstants::FineTol)
8330 v = entryvisits[row.fromentry];
8331 term = T(callresidt[row.elem] / v);
8332 break;
8333 }
8334 case 3: // the thread-acquisition wait of the task
8335 term = thread_wait(row.elem);
8336 break;
8337 }
8338 if (!std::isfinite(dbl(term))) term = Tzero();
8339 stage[refpath_group[r]] = T(stage[refpath_group[r]] + term * num_traits<T>::from_double(row.sign));
8340 }
8341 const double omega = relax_omega;
8342 for (std::size_t k = 0; k < nkeys; ++k) {
8343 if (stage[k] < Tzero()) stage[k] = Tzero();
8344 // the think-time schedule, including the escape from a divergent iterate
8345 if (omega < 1.0 && it > 1 && !std::isnan(refpath_stage_prev[k])) {
8346 T prevS = refpath_stage_prev_v[k];
8347 if (dbl(prevS) > 10.0 * dbl(stage[k]) && dbl(stage[k]) > GlobalConstants::FineTol)
8348 prevS = stage[k];
8349 stage[k] = T(num_traits<T>::from_double(omega) * stage[k] +
8350 num_traits<T>::from_double(1.0 - omega) * prevS);
8351 }
8352 refpath_stage_prev[k] = dbl(stage[k]);
8353 refpath_stage_prev_v[k] = stage[k];
8354 qn::Layer<T>& L = ensemble[std::size_t(idxhash[refpath_keys[k][0]])];
8355 // exp_mean(0) is an infinite rate, which the visit solve reads as a
8356 // disabled pair and drops from the chain
8357 if (dbl(stage[k]) > GlobalConstants::FineTol)
8358 L.set_service(refpath_keys[k][1], refpath_keys[k][2], Distrib<T>::exp_mean(stage[k]));
8359 else
8360 L.set_service(refpath_keys[k][1], refpath_keys[k][2], Distrib<T>::immediate());
8361 }
8362 }
8363
8364 /**
8365 * Mean time a request queues for a thread of task TIDX, read from TIDX's own
8366 * layer in the population domain, max(0, Q - m*U)/X. An infinite or
8367 * unbounded pool never queues.
8368 */
8369 T thread_wait(std::size_t tidx) const {
8370 if (tidx < 1 || tidx > lqn.nidx) return Tzero();
8371 if (lqn.sched[tidx] == SchedStrategy::INF || !std::isfinite(lqn.maxmult[tidx])) return Tzero();
8372 if (idxhash[tidx] < 0 || results.empty()) return Tzero();
8373 const std::size_t e = std::size_t(idxhash[tidx]);
8374 const qn::Layer<T>& L = ensemble[e];
8375 const LayerResult<T>& r = results.back()[e];
8376 const std::size_t ks = station_idx_of(L, tidx);
8377 T Qtot = Tzero();
8378 for (std::size_t c = 0; c < L.nclasses; ++c) Qtot += r.QN(ks - 1, c);
8379 const T Btot = T(num_traits<T>::from_double(lqn.maxmult[tidx]) * util[tidx]);
8380 const T Xtot = T(tput[tidx] / num_traits<T>::from_double(std::max(1.0, lqn.repl[tidx])));
8381 if (!(dbl(Xtot) > GlobalConstants::FineTol)) return Tzero();
8382 T d = T(Qtot - Btot);
8383 const T z = num_traits<T>::from_double(GlobalConstants::Zero);
8384 if (d < z) d = z;
8385 return T(d / Xtot);
8386 }
8387
8388 /**
8389 * Time a job of `cls`'s CHAIN spends blocked at this layer's region.
8390 *
8391 * A job held by an admission constraint is at no station at all, so its
8392 * wait is structurally absent from the RN and WN the layer reports back and
8393 * the caller and the callee end up disagreeing on throughput -- the fixed
8394 * point still converges, it just converges to the wrong place.
8395 *
8396 * Recovered by Little over the CHAIN, never per class: a job switches class
8397 * along the activity graph, so a call class carries population 0 and a
8398 * per-class deficit comes out negative and silently does nothing.
8399 * Zero for a layer with no region, which is every layer in a plain model.
8400 */
8401 T region_wait(std::size_t e, std::size_t cls) const {
8402 const qn::Layer<T>& L = ensemble[e];
8403 if (L.regions.empty() || results.empty()) return Tzero();
8404 const LayerResult<T>& r = results.back()[e];
8405 const std::size_t k = cls - 1;
8406 std::size_t c = L.nchains;
8407 for (std::size_t cc = 0; cc < L.nchains; ++cc)
8408 if (L.chains[cc][k]) c = cc;
8409 if (c == L.nchains) return Tzero();
8410
8411 // the station the region is stated over, which under `flat.cs` is one
8412 // among many and under `srvn` is the layer's own server
8413 std::size_t rs = L.serverIdx;
8414 for (std::size_t i = 0; i < L.regions[0].members.size(); ++i)
8415 if (L.regions[0].members[i]) { rs = i + 1; break; }
8416
8417 double pop = 0.0;
8418 T inside = Tzero(), tput_srv = Tzero();
8419 for (std::size_t j = 0; j < L.nclasses; ++j) {
8420 if (!L.chains[c][j]) continue;
8421 const double p = L.classes[j].population;
8422 if (!std::isfinite(p)) return Tzero(); // an open chain has no population to close on
8423 pop += p;
8424 for (std::size_t i = 0; i < L.nstations; ++i) inside += r.QN(i, j);
8425 tput_srv += r.TN(rs - 1, j);
8426 }
8427 const T deficit = T(num_traits<T>::from_double(pop) - inside);
8428 if (!(deficit > Tzero()) || !(tput_srv > Tzero())) return Tzero();
8429 return T(deficit / tput_srv);
8430 }
8431
8432 /** Port of updateRoutingProbabilities: entry selection by throughput ratio. */
8433 void update_routing_probabilities(int) {
8434 for (std::size_t u = 0; u < unique_route_idx.size(); ++u) {
8435 // the reference always takes the reversed order here: its `mod(it,0)`
8436 // guard is NaN, which MATLAB reads as false
8437 const std::size_t idx = unique_route_idx[unique_route_idx.size() - 1 - u];
8438 qn::Layer<T>& L = ensemble[std::size_t(idxhash[idx])];
8439 bool updated = false;
8440 for (const RouteRow& r : route_map) {
8441 if (r.idx != idx) continue;
8442 if (idxhash[r.tidx_caller] < 0) continue;
8443 const std::size_t cl = std::size_t(idxhash[r.tidx_caller]);
8444 const qn::Layer<T>& CL = ensemble[cl];
8445 const LayerResult<T>& cr = results.back()[cl];
8446 // the CALLER's own station, and for a call class the station of
8447 // the entry it targets; the two coincide under `srvn`
8448 const std::size_t cs = station_idx_of(CL, r.tidx_caller);
8449 T Xtot = Tzero();
8450 for (std::size_t k = 0; k < CL.nclasses; ++k) Xtot += cr.TN(cs - 1, k);
8451 if (!(Xtot > Tzero())) continue;
8452 T entry_tput = Tzero();
8453 for (const auto& a : CL.attr_calls)
8454 if (a[3] == r.eidx)
8455 entry_tput += cr.TN(station_idx_of(CL, lqn.parent[a[3]]) - 1, a[0] - 1);
8456 L.set_route(r.cfrom, r.cto, r.nodefrom, r.nodeto, T(entry_tput / Xtot));
8457 updated = true;
8458 }
8459 if (updated) L.refresh_chains();
8460 }
8461 }
8462
8463 // -----------------------------------------------------------------------
8464 // getTranAvg: getTranAvgDecoupled and getTranAvgCoupled
8465 // -----------------------------------------------------------------------
8466
8467 /**
8468 * What a layered transient needs before it can mean anything.
8469 *
8470 * A FLUID ENSEMBLE IS REQUIRED, and not as an implementation shortcut: the
8471 * reference reaches the layer transient through `self.solvers{e}.getTranAvg`,
8472 * which only a transient-capable layer solver has, and the coupled mode
8473 * injects its time-varying demands through the fluid rate schedule
8474 * specifically. An MVA layer has no trajectory to report and no place to
8475 * receive an injection, so an ensemble built on one is refused here rather
8476 * than silently answered with its steady state repeated over a grid.
8477 */
8478 void require_transient_ready() const {
8479 if (opt.layer_solver != "fluid")
8480 throw UnsupportedError(ErrorKind{"Analysis"},
8481 "SolverLN: the layered transient is the transient OF EACH LAYER, which only the "
8482 "fluid layer solver has; rebuild the ensemble with layer_solver 'fluid'");
8483 }
8484
8485 /** True when the caller named a horizon; otherwise each layer picks its own. */
8486 bool has_finite_horizon() const {
8487 return std::isfinite(opt.timespan_end) && opt.timespan_end > 0.0;
8488 }
8489
8490 /** One layer's trajectory, resampled onto the shared grid. */
8491 struct TranTraj {
8492 std::vector<std::vector<std::vector<double>>> Q, U, Tp, R; ///< [station][class][point]
8493 };
8494
8495 /**
8496 * Run one layer's transient, optionally with an injected rate schedule, and
8497 * return both the reportable block and the trajectory the relaxation reads.
8498 */
8499 void run_layer_transient(std::size_t e, const std::vector<FluidRateSched>& sched,
8500 LnTranLayer& block, TranTraj& traj) {
8501 qn::Layer<T>& L = ensemble[e];
8502 L.refresh_rt();
8503 fluid::FluidOptions fo = opt.layer_fluid;
8504 fo.rate_sched = sched;
8505 // THE WARM START IS REPLAYED HERE, and this is the only place it can be:
8506 // `converged` resets every layer the first time the fixed point settles,
8507 // so a state installed before the solve is gone by now. The steady solve
8508 // ignores an initial state and the transient does not, which is why
8509 // replaying it after the one and before the other loses nothing.
8510 if (e < layer_tran_init.size() && !layer_tran_init[e].empty())
8511 fo.init_sol = layer_tran_init[e];
8512 // No horizon named: this layer picks its own, by the analyzer's own rule
8513 // (thirty mean events of its slowest transition). That is what the
8514 // reference's decoupled path does -- each layer's SolverFluid chooses --
8515 // and it is why an unset timespan is a valid request rather than an error.
8516 const double t_end =
8517 has_finite_horizon() ? opt.timespan_end : fluid::fluid_default_horizon(L, fo);
8518 const std::vector<fluid::FluidTranPoint> pts =
8519 detail::ln_fluid_transient(L, fo, t_end, opt.tran_points, opt.tran_grid);
8520 const std::size_t M = L.nstations, K = L.nclasses, P = pts.size();
8521 block.t.resize(P);
8522 auto alloc = [&](std::vector<std::vector<std::vector<double>>>& A) {
8523 A.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
8524 };
8525 alloc(block.QN);
8526 alloc(block.UN);
8527 alloc(block.TN);
8528 alloc(traj.Q);
8529 alloc(traj.U);
8530 alloc(traj.Tp);
8531 alloc(traj.R);
8532 for (std::size_t p = 0; p < P; ++p) {
8533 block.t[p] = pts[p].t;
8534 for (std::size_t i = 0; i < M; ++i)
8535 for (std::size_t r = 0; r < K; ++r) {
8536 const double q = pts[p].QN(i, r), u = pts[p].UN(i, r), x = pts[p].TN(i, r);
8537 block.QN[i][r][p] = q;
8538 block.UN[i][r][p] = u;
8539 block.TN[i][r][p] = x;
8540 traj.Q[i][r][p] = q;
8541 traj.U[i][r][p] = u;
8542 traj.Tp[i][r][p] = x;
8543 // Residence by Little, which is how the relaxation reads a
8544 // callee's response time off a layer trajectory.
8545 traj.R[i][r][p] = q / std::max(x, GlobalConstants::FineTol);
8546 }
8547 }
8548 }
8549
8550 /**
8551 * Port of getTranAvgDecoupled: freeze the inter-layer demands at the
8552 * converged fixed point and run each layer's transient in isolation.
8553 */
8554 LnTranSolution tran_avg_decoupled() {
8555 require_transient_ready();
8556 if (results.empty()) iterate();
8557 LnTranSolution out;
8558 out.mode = "decoupled";
8559 out.layers.resize(ensemble.size());
8560 for (std::size_t e = 0; e < ensemble.size(); ++e) {
8561 TranTraj tj;
8562 run_layer_transient(e, std::vector<FluidRateSched>(), out.layers[e], tj);
8563 }
8564 return out;
8565 }
8566
8567 /**
8568 * Port of getTranAvgCoupled: reconcile the per-layer transients by waveform
8569 * relaxation, so the layer populations and the inter-layer demands co-evolve
8570 * in model time.
8571 *
8572 * Each layer's fluid transient is driven by TIME-VARYING inter-layer demand
8573 * trajectories taken from the other layers' latest transients, and the loop
8574 * repeats until the trajectories stop moving in sup-norm. Iteration 0 uses
8575 * the frozen equilibrium demands, so it reproduces the decoupled answer
8576 * exactly; at convergence every layer relaxes to its own fixed point, so the
8577 * endpoint equals getAvg.
8578 *
8579 * TWO CHANNELS ARE COUPLED, the task think times (the client delay) and the
8580 * synchronous-call service demands (the caller's client station). Both are
8581 * dominant inter-layer couplings; the intra-layer host service stays at its
8582 * equilibrium value, as in the reference.
8583 */
8584 LnTranSolution tran_avg_coupled() {
8585 require_transient_ready();
8586 // Waveform relaxation reconciles the layers on ONE shared grid, so it
8587 // needs a horizon the layers agree on. With none named there is nothing
8588 // to co-evolve over, and the reference defers to the decoupled path
8589 // rather than inventing one; so does this.
8590 if (!has_finite_horizon()) return tran_avg_decoupled();
8591 if (results.empty()) iterate();
8592 const std::size_t E = ensemble.size();
8593
8594 LnTranSolution out;
8595 out.mode = "coupled";
8596 out.layers.resize(E);
8597 std::vector<TranTraj> traj(E), prev(E);
8598 for (std::size_t e = 0; e < E; ++e)
8599 run_layer_transient(e, std::vector<FluidRateSched>(), out.layers[e], traj[e]);
8600 const std::vector<double> tgrid = out.layers.empty() ? std::vector<double>() : out.layers[0].t;
8601
8602 for (long iter = 1; iter <= opt.ln_transient_iter_max; ++iter) {
8603 prev = traj;
8604 const std::vector<std::vector<FluidRateSched>> sched =
8605 build_rate_sched(recompute_demand(traj, tgrid), tgrid);
8606 for (std::size_t e = 0; e < E; ++e)
8607 run_layer_transient(e, sched[e], out.layers[e], traj[e]);
8608 double gap = 0.0;
8609 for (std::size_t e = 0; e < E; ++e)
8610 for (std::size_t i = 0; i < traj[e].Q.size(); ++i)
8611 for (std::size_t r = 0; r < traj[e].Q[i].size(); ++r)
8612 for (std::size_t p = 0; p < traj[e].Q[i][r].size(); ++p)
8613 gap = std::max(gap, std::fabs(traj[e].Q[i][r][p] - prev[e].Q[i][r][p]));
8614 out.iterations = iter;
8615 out.gap = gap;
8616 if (gap < opt.ln_transient_tol) break;
8617 }
8618 return out;
8619 }
8620
8621 /** The two coupled demand channels, as trajectories on the shared grid. */
8622 struct TranDemand {
8623 std::map<std::size_t, std::vector<double>> thinkt; ///< by task element index
8624 std::map<std::size_t, std::vector<double>> callservt; ///< by call index
8625 };
8626
8627 /**
8628 * Recompute the inter-layer demands pointwise in t, mirroring the SCALAR
8629 * updateThinkTimes and updateMetricsDefault formulas.
8630 *
8631 * They are the same formulas, evaluated at each point of the grid instead of
8632 * at the fixed point: that is what makes the endpoint of the relaxation the
8633 * steady-state answer rather than something near it.
8634 */
8635 TranDemand recompute_demand(const std::vector<TranTraj>& traj,
8636 const std::vector<double>& tgrid) const {
8637 TranDemand out;
8638 const std::size_t ng = tgrid.size();
8639
8640 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
8641 const std::size_t tidx = lqn.tshift + t;
8642 if (idxhash[tidx] < 0 || lqn.isref[tidx]) continue;
8643 const std::size_t e = std::size_t(idxhash[tidx]);
8644 const qn::Layer<T>& L = ensemble[e];
8645 const std::size_t s = station_idx_of(L, tidx) - 1;
8646 double nj = 0.0;
8647 for (std::size_t c = 1; c <= NT(); ++c) nj = std::max(nj, njobs(tidx, c));
8648 // same closure as update_think_times, so the same gate
8649 const double userthink = dbl(ref_think_time(tidx));
8650 std::vector<double> tk(ng, 0.0);
8651 for (std::size_t p = 0; p < ng; ++p) {
8652 double U = 0.0, X = 0.0;
8653 for (std::size_t r = 0; r < L.nclasses; ++r) {
8654 U += traj[e].U[s][r][p];
8655 X += traj[e].Tp[s][r][p];
8656 }
8657 const double Xs = std::max(X, GlobalConstants::FineTol);
8658 double v = lqn.sched[tidx] == SchedStrategy::INF
8659 ? (nj - U) / Xs - userthink
8660 : nj * std::fabs(1.0 - U) / Xs - userthink;
8662 tk[p] = v + userthink; // total mean, user think included
8663 }
8664 out.thinkt[tidx] = tk;
8665 }
8666
8667 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
8668 if (lqn.calltype[cidx] != CallType::SYNC) continue;
8669 const std::size_t eidx = lqn.callpair_dst[cidx];
8670 const std::size_t tidx = lqn.parent[eidx];
8671 if (tidx > NT() || idxhash[tidx] < 0) continue;
8672 const std::size_t e = std::size_t(idxhash[tidx]);
8673 const qn::Layer<T>& L = ensemble[e];
8674 const std::size_t s = station_idx_of(L, tidx) - 1;
8675 std::vector<double> Rc(ng, 0.0);
8676 bool any = false;
8677 for (std::size_t r = 0; r < L.nclasses; ++r) {
8678 if (L.classes[r].attr_kind != int(LqnElement::ENTRY)) continue;
8679 if (L.classes[r].attr_idx != eidx) continue;
8680 any = true;
8681 for (std::size_t p = 0; p < ng; ++p) Rc[p] += traj[e].R[s][r][p];
8682 }
8683 if (!any) {
8684 // No entry class of its own in this layer: the entry's work is
8685 // carried by its activities, so their residence is the answer.
8686 for (std::size_t r = 0; r < L.nclasses; ++r)
8687 for (std::size_t p = 0; p < ng; ++p) Rc[p] += traj[e].R[s][r][p];
8688 }
8689 const double cm = dbl(lqn.callproc_mean[cidx]);
8690 for (std::size_t p = 0; p < ng; ++p) Rc[p] *= cm;
8691 out.callservt[cidx] = Rc;
8692 }
8693 return out;
8694 }
8695
8696 /**
8697 * Map the demand trajectories onto per-layer rate schedules, through the
8698 * SAME update maps that place the scalar setService calls.
8699 *
8700 * The injected schedule MODULATES the layer's equilibrium rate by the ratio
8701 * of the transient demand to its steady-state value: passing rate = 1/d(t)
8702 * with nominal = 1/d(end) makes solver_fluid_ratemult's multiplier
8703 * d(end)/d(t), which is exactly 1 at the horizon end. That is what makes the
8704 * layer relax to its UNMODIFIED fixed point regardless of any small mismatch
8705 * between the fluid residence Q/T and the scalar equilibrium demand.
8706 */
8707 std::vector<std::vector<FluidRateSched>> build_rate_sched(
8708 const TranDemand& demand, const std::vector<double>& tgrid) const {
8709 std::vector<std::vector<FluidRateSched>> out(ensemble.size());
8710 const bool want_think =
8711 opt.ln_transient_channels == "both" || opt.ln_transient_channels == "thinkt";
8712 const bool want_call =
8713 opt.ln_transient_channels == "both" || opt.ln_transient_channels == "callservt";
8714
8715 auto add = [&](std::size_t e, std::size_t station, std::size_t cls,
8716 const std::vector<double>& d) {
8717 if (d.empty()) return;
8718 const double dend = d.back();
8719 if (!(dend > GlobalConstants::FineTol)) return; // degenerate steady state
8720 // Bound the transient demand to a physical band around its
8721 // steady-state value: an early-transient throughput near zero sends
8722 // the reciprocal to infinity and the integrator with it.
8723 const double cap = 20.0;
8725 s.station = station;
8726 s.cls = cls;
8727 s.tgrid = tgrid;
8728 s.rates.resize(d.size());
8729 for (std::size_t p = 0; p < d.size(); ++p) {
8730 const double v = std::min(std::max(d[p], dend / cap), dend * cap);
8731 s.rates[p] = 1.0 / v;
8732 }
8733 s.nominal = 1.0 / dend;
8734 out[e].push_back(s);
8735 };
8736
8737 if (want_think)
8738 for (const UpdRow& row : thinkt_map) {
8739 if (idxhash[row.idx] < 0) continue;
8740 const std::size_t e = std::size_t(idxhash[row.idx]);
8741 if (row.node != ensemble[e].clientIdx) continue;
8742 if (lqn.type[row.aidx] != LqnElement::TASK) continue;
8743 if (lqn.sched[row.aidx] == SchedStrategy::REF) continue;
8744 const auto it = demand.thinkt.find(row.aidx);
8745 if (it == demand.thinkt.end()) continue;
8746 add(e, row.node, row.cls, it->second);
8747 }
8748 if (want_call)
8749 for (const UpdRow& row : call_map) {
8750 if (idxhash[row.idx] < 0) continue;
8751 const std::size_t e = std::size_t(idxhash[row.idx]);
8752 if (row.node != ensemble[e].clientIdx) continue;
8753 const auto it = demand.callservt.find(row.aidx);
8754 if (it == demand.callservt.end()) continue;
8755 add(e, row.node, row.cls, it->second);
8756 }
8757 return out;
8758 }
8759
8760 /// First activity bound to entry EIDX, or 0 when it has none.
8761 std::size_t bound_activity_of(std::size_t eidx) const {
8762 for (std::size_t sx : lqn.graph.succ(eidx))
8763 if (sx > lqn.ashift) return sx;
8764 return 0;
8765 }
8766
8767 // -----------------------------------------------------------------------
8768 // getEnsembleAvg
8769 // -----------------------------------------------------------------------
8770 LnSolution<T> aggregate() {
8771 const std::size_t N = lqn.nidx;
8772 LnSolution<T> s;
8773 auto mk = [&](std::vector<T>& v, std::vector<bool>& d) {
8774 v.assign(N + 1, Tzero());
8775 d.assign(N + 1, false);
8776 };
8777 std::vector<T> QN, UN, RN, TN, PN, SN, WN, AN;
8778 std::vector<bool> dQ, dU, dR, dT, dP, dS, dW, dA;
8779 mk(QN, dQ); mk(UN, dU); mk(RN, dR); mk(TN, dT);
8780 mk(PN, dP); mk(SN, dS); mk(WN, dW); mk(AN, dA);
8781 std::vector<bool> wn_done(N + 1, false);
8782
8783 for (std::size_t e = 0; e < ensemble.size(); ++e) {
8784 const qn::Layer<T>& L = ensemble[e];
8785 const LayerResult<T>& r = results.back()[e];
8786 const std::size_t clientIdx = L.clientIdx;
8787 // The processors this layer serves: one under `srvn` (and only when
8788 // it is a host layer), every one of them under `flat.cs`. Each is
8789 // charged with the activities that actually run on it, which is
8790 // vacuous under `srvn` because a host layer holds no others.
8791 const bool has_host_server = !L.host_stations.empty();
8792 for (std::size_t hs : L.host_stations) {
8793 const std::size_t hidx = L.stations[hs - 1].attr_idx;
8794 dT[hidx] = true;
8795 TN[hidx] = Tzero();
8796 dP[hidx] = true;
8797 PN[hidx] = Tzero();
8798 for (std::size_t k = 0; k < L.nclasses; ++k) {
8799 if (L.classes[k].completes) {
8800 T t = clientIdx > 0 ? r.TN(clientIdx - 1, k) : Tzero();
8801 const T ts = r.TN(hs - 1, k);
8802 TN[hidx] = T(TN[hidx] + (t > ts ? t : ts));
8803 }
8804 if (L.classes[k].attr_kind == int(LqnElement::ACTIVITY)) {
8805 // the activity does not run on this processor
8806 if (station_idx_of_class(L, k) != hs) continue;
8807 const std::size_t aidx = L.classes[k].attr_idx;
8808 const std::size_t tidx = lqn.parent[aidx];
8809 dP[aidx] = true;
8810 dP[tidx] = true;
8811 PN[aidx] = T(PN[aidx] + r.UN(hs - 1, k));
8812 PN[tidx] = T(PN[tidx] + r.UN(hs - 1, k));
8813 PN[hidx] = T(PN[hidx] + r.UN(hs - 1, k));
8814 } else if (L.classes[k].attr_kind == int(LqnElement::ENTRY)
8815 && L.classes[k].type == JobClassType::OPEN) {
8816 // an entry's own open stream is served here for its bound
8817 // activity's demand, so its busy time belongs to that activity,
8818 // its task and this processor like any other
8819 if (station_idx_of_class(L, k) != hs) continue;
8820 const std::size_t eidx = L.classes[k].attr_idx;
8821 const std::size_t tidx = lqn.parent[eidx];
8822 const std::size_t aidx = bound_activity_of(eidx);
8823 const T uo = r.UN(hs - 1, k);
8824 if (aidx != 0) {
8825 dP[aidx] = true;
8826 PN[aidx] = T(PN[aidx] + uo);
8827 }
8828 dP[tidx] = true;
8829 PN[tidx] = T(PN[tidx] + uo);
8830 PN[hidx] = T(PN[hidx] + uo);
8831 }
8832 }
8833 dT[hidx] = false; // NaN in the reference, for consistency with LQNS
8834 }
8835
8836 for (std::size_t k = 0; k < L.nclasses; ++k) {
8837 const int kind = L.classes[k].attr_kind;
8838 // the station this class is actually served at, which under
8839 // `flat.cs` is the processor of an activity or the called task
8840 // of a call rather than the one server the layer used to have
8841 const std::size_t serverIdx = station_idx_of_class(L, k);
8842 if (kind == int(LqnElement::TASK)) {
8843 const std::size_t tidx = L.classes[k].attr_idx;
8844 if (has_host_server && !dT[tidx]) {
8845 dT[tidx] = true;
8846 TN[tidx] = r.TN(clientIdx - 1, k);
8847 }
8848 } else if (kind == int(LqnElement::ENTRY) && L.classes[k].type == JobClassType::OPEN) {
8849 // the entry's own open stream, on top of what its callers send: it
8850 // rides a class of its own in the host layer, so its rate adds to the
8851 // entry, its task and its bound activity
8852 const std::size_t eidx = L.classes[k].attr_idx;
8853 const T to = r.TN(L.classes[k].refstat - 1, k);
8854 for (std::size_t idx : {eidx, lqn.parent[eidx], bound_activity_of(eidx)}) {
8855 if (idx == 0) continue;
8856 dT[idx] = true;
8857 TN[idx] = T(TN[idx] + to);
8858 }
8859 } else if (kind == int(LqnElement::ENTRY)) {
8860 const std::size_t eidx = L.classes[k].attr_idx;
8861 dS[eidx] = true;
8862 // getEnsembleAvg.m:84-92: a phase-2 entry reports the
8863 // CALLER's view, which is what residt now holds.
8864 SN[eidx] = (has_phase2 && dbl(servt_ph2[eidx]) > GlobalConstants::FineTol)
8865 ? residt[eidx]
8866 : servt[eidx];
8867 if (has_host_server && !dT[eidx]) {
8868 dT[eidx] = true;
8869 TN[eidx] = r.TN(clientIdx - 1, k);
8870 }
8871 } else if (kind == int(LqnElement::CALL)) {
8872 const std::size_t cidx = L.classes[k].attr_idx;
8873 const std::size_t aidx = lqn.callpair_src[cidx];
8874 if (lqn.calltype[cidx] == CallType::SYNC) {
8875 dS[aidx] = true;
8876 SN[aidx] = T(SN[aidx] + r.RN(serverIdx - 1, k) * lqn.callproc_mean[cidx]);
8877 }
8878 dQ[aidx] = true;
8879 QN[aidx] = T(QN[aidx] + r.QN(serverIdx - 1, k));
8880 } else if (kind == int(LqnElement::ACTIVITY)) {
8881 const std::size_t aidx = L.classes[k].attr_idx;
8882 const std::size_t tidx = lqn.parent[aidx];
8883 dQ[tidx] = true;
8884 QN[tidx] = T(QN[tidx] + r.QN(serverIdx - 1, k));
8885 dT[aidx] = true;
8886 dQ[aidx] = true;
8887 TN[aidx] = T(TN[aidx] + r.TN(serverIdx - 1, k));
8888 dS[aidx] = true;
8889 SN[aidx] = T(SN[aidx] + r.RN(serverIdx - 1, k));
8890 dR[aidx] = true;
8891 RN[aidx] = T(RN[aidx] + r.RN(serverIdx - 1, k));
8892 dW[aidx] = true;
8893 dW[tidx] = true;
8894 WN[aidx] = residt[aidx];
8895 if (!wn_done[aidx]) {
8896 WN[tidx] = T(WN[tidx] + residt[aidx]);
8897 wn_done[aidx] = true;
8898 }
8899 QN[aidx] = T(QN[aidx] + r.QN(serverIdx - 1, k));
8900 }
8901 }
8902 }
8903
8904 // A replicated element is solved as ONE representative replica; report it as the
8905 // TOTAL over its copies (TN and busy time scaled by repl, response times per
8906 // request). Mirrors MATLAB getEnsembleAvg; inert when nothing is replicated.
8907 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
8908 const std::size_t tidx = lqn.tshift + t;
8909 const double nrep = lqn.repl[tidx];
8910 if (!(nrep > 1.0)) continue;
8911 std::vector<std::size_t> scaleIdx{tidx};
8912 scaleIdx.insert(scaleIdx.end(), lqn.entriesof[tidx].begin(), lqn.entriesof[tidx].end());
8913 scaleIdx.insert(scaleIdx.end(), lqn.actsof[tidx].begin(), lqn.actsof[tidx].end());
8914 // read from a host layer that held every replica (BUG-110): the client row
8915 // is already the total, a server station holds one host replica's share
8916 const auto hr = host_all_replica_tasks.find(tidx);
8917 const bool hall = hr != host_all_replica_tasks.end();
8918 const T pscale = num_traits<T>::from_double(hall ? double(hr->second) : nrep);
8919 for (std::size_t idx : scaleIdx) {
8920 const bool isact = std::find(lqn.actsof[tidx].begin(), lqn.actsof[tidx].end(), idx) !=
8921 lqn.actsof[tidx].end();
8922 const T tscale = !hall ? num_traits<T>::from_double(nrep)
8923 : isact ? pscale : Tone();
8924 if (dT[idx]) TN[idx] = T(tscale * TN[idx]);
8925 if (dP[idx]) PN[idx] = T(pscale * PN[idx]);
8926 }
8927 }
8928 for (std::size_t hidx = 1; hidx <= lqn.nhosts; ++hidx)
8929 if (lqn.repl[hidx] > 1.0 && dP[hidx]) PN[hidx] = T(num_traits<T>::from_double(lqn.repl[hidx]) * PN[hidx]);
8930
8931 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
8932 const std::size_t eidx = lqn.eshift + e;
8933 const std::size_t tidx = lqn.parent[eidx];
8934 dU[tidx] = true;
8935 dU[eidx] = true;
8936 // getEnsembleAvg.m:186-198: the server is busy through BOTH phases,
8937 // so the utilization is the full service time and not SN, which for
8938 // a phase-2 entry has been cut down to the caller's view above.
8939 UN[eidx] = (has_phase2 && dbl(servt_ph2[eidx]) > GlobalConstants::FineTol)
8940 ? T(TN[eidx] * (servt_ph1[eidx] + servt_ph2[eidx]))
8941 : T(TN[eidx] * SN[eidx]);
8942 T ps = Tzero();
8943 bool any = false;
8944 for (std::size_t a : lqn.actsof[eidx])
8945 if (dP[a]) {
8946 ps += PN[a];
8947 any = true;
8948 }
8949 if (!lqn.actsof[eidx].empty()) {
8950 dP[eidx] = true;
8951 PN[eidx] = any ? ps : Tzero();
8952 }
8953 for (std::size_t a : lqn.actsof[tidx]) {
8954 dU[a] = true;
8955 UN[a] = T(TN[a] * SN[a]);
8956 }
8957 UN[tidx] = T(UN[tidx] + UN[eidx]);
8958 }
8959
8960 // AN IGNORED ELEMENT IS IDLE, NOT UNDEFINED, and the two are different cells.
8961 // Its component holds no reference task, so nothing reaches it and every
8962 // measure it HAS is zero -- but the measures its kind never has stay
8963 // undefined, exactly as they do for a reachable element. A flat zero over
8964 // all six columns broke the table's NaN mask (a processor with a queue
8965 // length of 0, an arrival rate reported where no solver reports one), and
8966 // the mask is part of the answer: see _kb/06-solver-catalog.md. The
8967 // relabelling below reports Q from U, U from P and R from S, so QN and RN
8968 // are dead here and are not written.
8969 for (std::size_t i = 1; i <= N; ++i)
8970 if (ignore[i]) {
8971 PN[i] = Tzero(); // every kind reports a utilization
8972 dP[i] = true;
8973 dA[i] = false; // nothing reports an arrival rate on an LQN
8974 const bool host = lqn.type[i] == LqnElement::HOST;
8975 const bool task = lqn.type[i] == LqnElement::TASK;
8976 const bool entry = lqn.type[i] == LqnElement::ENTRY;
8977 UN[i] = Tzero();
8978 dU[i] = !host;
8979 SN[i] = Tzero();
8980 dS[i] = !host && !task;
8981 WN[i] = Tzero();
8982 dW[i] = !host && !entry;
8983 TN[i] = Tzero();
8984 dT[i] = !host;
8985 }
8986
8987 // the reference's final relabelling: Q <- U, U <- P, R <- S
8988 s.QN = UN; s.defined_Q = dU;
8989 s.UN = PN; s.defined_U = dP;
8990 s.RN = SN; s.defined_R = dS;
8991 s.TN = TN; s.defined_T = dT;
8992 s.AN = AN; s.defined_A = dA;
8993 s.WN = WN; s.defined_W = dW;
8994 s.iterations = iterations_done;
8995 s.converged = did_converge;
8996 return s;
8997 }
8998};
8999
9000} // namespace ln
9001} // namespace line
9002
9003// Deliberately at the FOOT of the file: lqn_analyzers.h needs LayerResult and
9004// SolverLN complete, and this file needs its lqn_overtake_prob_markov, so the
9005// two are mutually dependent. Either include order works -- whichever header a
9006// translation unit names first, the other is fully parsed before the templates
9007// above are instantiated -- and the forward declaration near the top of this
9008// file is what makes the call inside update_metrics resolve.
9010
9011#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, ...): LINE:input:<kind>.
Definition error.h:103
InputError(const std::string &what)
Definition error.h:105
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:119
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:959
const std::string & state_interlock_method() const
The interlock tracking method this run USES: "ilrate", "refpath" or "none".
Definition solver_ln.h:957
LnSolution< T > box_bounds()
Majumdar-Woodside robust box bounds, reported in the shape of a solution.
Definition solver_ln.h:786
LnLayerBlocks layer_blocks() const
Port of LayeredNetwork.layerBlocks: the block-diagonal layout of the layers in the aggregate (station...
Definition solver_ln.h:830
LnSolution< T > trapped_overlay(LnSolution< T > s) const
Nested blocking of the reference tasks trapped at a zero-capacity element (ln_zero_capacity): every e...
Definition solver_ln.h:622
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:937
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:967
const std::vector< T > & state_util() const
Definition solver_ln.h:947
SolverLN(const LqnStruct< T > &lqn_in, const LnOptions &options)
Definition solver_ln.h:558
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:735
LnTranSolution get_tran_avg()
Port of @SolverLN/getTranAvg: the block-diagonal aggregate transient.
Definition solver_ln.h:712
const std::vector< T > & state_thinkt() const
Definition solver_ln.h:941
double nlp_objective() const
method='nlp': the program's objective at nlp_solution().
Definition solver_ln.h:585
std::size_t nlayers() const
Definition solver_ln.h:819
void get_block_avg(Matrix< T > &QN, Matrix< T > &UN, Matrix< T > &TN) const
Port of getBlockAvg: the layers' own STEADY averages, in the block-diagonal layout of layer_blocks (e...
Definition solver_ln.h:857
const std::vector< Distrib< T > > & state_callservtproc() const
Definition solver_ln.h:946
std::vector< LnCdf > get_cdf_respt()
Port of @SolverLN/getCdfRespT: the per-entry response-time distribution.
Definition solver_ln.h:669
const std::vector< T > & state_callresidt() const
Definition solver_ln.h:943
::line::lqn::NlpProgram nlp_program() const
The QD-AMVA program of this model under the solver's nlp options, the program method='nlp' roots and ...
Definition solver_ln.h:591
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:949
const std::string & state_lnmethod() const
The method the layers were built for: "srvn.ph", "srvn.cs" or "moment3".
Definition solver_ln.h:951
bool warmstarted() const
True once init() seeded the iterate from config.warmstart; converged() then drops iter_min.
Definition solver_ln.h:581
const std::vector< T > & state_tput() const
Definition solver_ln.h:940
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:934
const std::vector< T > & state_servt() const
Definition solver_ln.h:938
static std::vector< std::string > list_valid_methods()
Port of SolverLN.listValidMethods.
Definition solver_ln.h:553
const std::vector< Distrib< T > > & state_servtproc() const
Definition solver_ln.h:944
const std::vector< Distrib< T > > & state_thinktproc() const
Definition solver_ln.h:945
LnSolution< T > get_ensemble_avg()
Port of getEnsembleAvg: run the iteration and aggregate onto LQN elements.
Definition solver_ln.h:600
const std::vector< double > & nlp_solution() const
method='nlp': the program's solution, clipped at zero, in the program's variable order.
Definition solver_ln.h:583
const std::vector< T > & state_callservt() const
Definition solver_ln.h:942
const std::vector< qn::Layer< T > > & layers() const
Definition solver_ln.h:820
const std::vector< T > & state_residt() const
Definition solver_ln.h:939
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:902
The program of one layered model.
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...
The NATIVE LDES discrete-event engine.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Running progress log of a LINE solver run (the "solver console").
Reference tasks trapped at a zero-capacity element of a layered network.
The @SolverLN methods that solver_ln.h does not carry.
Majumdar-Woodside robust box bounds on the throughput of a layered network.
A layered queueing network as ONE nonlinear program: linear constraints over the flows,...
Standalone LQN routines that SolverLN needs but does not contain.
Method of Layers on the SRVN decomposition of a layered queueing network whose entries carry no activ...
Phase-type composition of an LQN activity graph, the machinery behind SolverLN method 'srvn....
.lqnx -> LqnStruct, a port of matlab/src/lang/layered/@LayeredNetwork/parseXML.m followed by ....
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.
The method names of each solver, in listing order.
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.
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,...
JmtResult< T > solver_jmt_run_analyzer(const qn::NetworkStruct< T > &sn, const JmtOptions &opt_in)
Port of @@SolverJMT/runAnalyzer.m, the jsim and jmva arms.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:477
LqnElement
LQN element kinds, with the values of MATLAB LayeredNetworkElement.
Definition lang_types.h:471
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:474
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:374
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:736
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
T dist_moment(const Distrib< T > &d, unsigned k)
The k-th raw moment.
LdesResult ldes_engine_solve(const qn::NetworkStruct< T > &sn, const LdesOptions &o)
Simulate sn, over independent REPLICATIONS when options.replications > 1.
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
LnZeroCapacity ln_zero_capacity(const lqn::LqnStruct< T > &lqn)
Port of ln_zero_capacity.m; see the file header.
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:156
std::string elem(const LqnStruct< T > &lqn, std::size_t idx)
double tot(const std::vector< double > &pops)
std::string fmt(const char *f, double v)
std::string attr(const char *name, const Val &v)
InputError err(const std::string &msg, int line=-1)
Definition srvn_reader.h:97
std::string name(const Element *el, const std::string &key="name")
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.
LqnMolResult< T > lqn_mol(const LqnStruct< T > &lsn, const LqnMolOptions &options=LqnMolOptions())
Method of Layers on the SRVN decomposition of a layered queueing network whose entries carry no activ...
Definition lqn_mol.h:209
LqnBoxBounds< T > lqn_boxbounds(const LqnStruct< T > &lqn)
Evaluate the box bounds of lqn.
std::vector< double > lqn_sustainable_mult(const LqnStruct< T > &l, const std::vector< double > &mult)
Sustainable multiplicity (lsn_max_multiplicity) of every host and task of l under the declared multip...
Definition lqn_reader.h:451
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.
SetupDelayoffMMk< T > qbd_setupdelayoff_mmk(int k, double N, const T &arv, const T &mu, const T &alpharate, const T &alphascv, const T &betarate, const T &betascv)
Mean number present and throughput of a k-SERVER queue whose servers each pay a setup when woken from...
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
MvaSolution< T > fj_fixed_point(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, std::vector< T > &lam, const MvaOptions &opt, InnerSolve inner, std::string *warning=nullptr)
Drive the fork-join fixed point of a transformed model to convergence.
Definition fj_driver.h:512
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...
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.
void put(Matrix< double > &A, std::size_t r0, std::size_t c0, const Matrix< double > &S)
A(r0:, c0:) = S.
Definition mg1.h:927
SsaSolution solver_ssa(const qn::NetworkStruct< T > &sn, const SsaOptions &opt, std::vector< SsaCacheRatio > *cache=nullptr)
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,...
Multi-server queue with setup and delayed-off servers.
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....
Port of SolverJMT, the Java Modelling Tools client.
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,...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The SolverCTMC knobs this port honours.
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
The options of one JMT solve, SolverOptions('JMT') restricted to what is read.
Definition solver_jmt.h:121
The knobs of one LDES run.
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
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:997
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 point mass at zero.
Definition lang_types.h:986
static Distrib exp_mean(const T &m)
Definition lang_types.h:940
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:771
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double Zero
Definition lang_types.h:767
static constexpr double CoarseTol
Definition lang_types.h:766
Per-layer results of one iteration, the [QN,UN,RN,TN,AN,WN] of getAvg.
Definition solver_ln.h:414
A CDF sampled on a grid, the [F, t] pair MATLAB's evalCDF returns.
Definition solver_ln.h:437
bool empty() const
Definition solver_ln.h:439
std::vector< double > t
Definition solver_ln.h:438
std::vector< double > cdf
Definition solver_ln.h:438
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:470
std::vector< std::size_t > msz
Definition solver_ln.h:471
std::vector< std::size_t > roff
Definition solver_ln.h:471
std::vector< std::size_t > coff
Definition solver_ln.h:471
std::vector< std::size_t > ksz
Definition solver_ln.h:471
Options of SolverLN.
Definition solver_ln.h:275
ldes::LdesOptions layer_ldes
Options handed to each layer when layer_solver is ldes.
Definition solver_ln.h:337
std::size_t tran_points
Output points per layer trajectory, and the relaxation's own grid size.
Definition solver_ln.h:372
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:385
std::string relax
Definition solver_ln.h:298
std::string nlp_fork
options.config.nlp_fork: 'refuse' an AND-fork block, or price it at E[max] of exponentials ('exp').
Definition solver_ln.h:399
fluid::FluidOptions layer_fluid
Options handed to each layer when layer_solver is fluid.
Definition solver_ln.h:320
std::string layer_solver
Which solver runs each layer: mva, nc, fluid, ssa, mam, ctmc, ldes (the native in-process LDES engine...
Definition solver_ln.h:318
std::string interlock_method
options.config.interlock_method: how the interlock is TRACKED once interlocking is on.
Definition solver_ln.h:289
int nlp_warm
options.config.nlp_warm: the warm-start sweeps the program's Newton ladder begins from.
Definition solver_ln.h:395
std::size_t mol_iter_max
options.config.mol_iter_max, mol_iter_tol, mol_relax_factor: lqn_mol's own settings for warmstart='mo...
Definition solver_ln.h:407
mva::MvaOptions layer
Options handed to each layer solver; SolverMVA defaults.
Definition solver_ln.h:301
std::string method
options.method, which selects WHAT is reported and not merely how:
Definition solver_ln.h:351
ssa::SsaOptions layer_ssa
Options handed to each layer when layer_solver is ssa.
Definition solver_ln.h:324
std::string ln_transient_channels
options.config.ln_transient_channels: which inter-layer coupling is injected, both,...
Definition solver_ln.h:368
mam::MamOptions layer_mam
Options handed to each layer when layer_solver is mam (SolverMAM's own default method).
Definition solver_ln.h:326
double interlock_maxpaths
options.config.interlock_maxpaths: refpath refuses a layer carrying more routes into it.
Definition solver_ln.h:291
std::string ln_transient
options.config.ln_transient: how the per-layer transients are coupled.
Definition solver_ln.h:358
int nlp_maxiter
options.config.nlp_maxiter: budget of the descent the root-find falls back to.
Definition solver_ln.h:397
nc::NcSolverOptions layer_nc
Options handed to each layer when layer_solver is nc.
Definition solver_ln.h:322
std::string warmstart
options.config.warmstart: empty for a cold start, or 'nlp' / 'mol' to seed a LAYERED method's fixed p...
Definition solver_ln.h:405
std::string interlock_refpath_scope
options.config.interlock_refpath_scope: merging transforms only the layers where two or more callers ...
Definition solver_ln.h:297
ctmc::CtmcOptions layer_ctmc
Options handed to each layer when layer_solver is ctmc, and to a Region layer, which goes to CTMC wha...
Definition solver_ln.h:331
double timespan_end
options.timespan(2): the transient horizon; infinite means none is set.
Definition solver_ln.h:370
long ln_transient_iter_max
options.config.ln_transient_iter_max and ..._tol of the relaxation.
Definition solver_ln.h:360
jmt::JmtOptions layer_jmt
Options handed to each layer when layer_solver is jmt (JSIM); seed as for layer_ldes.
Definition solver_ln.h:339
std::map< std::string, ::line::lqn::NlpLld > lld
options.config.lld: a smooth queue-dependent rate multiplier per station, {processor or task name: ex...
Definition solver_ln.h:391
double nlp_alpha
options.config.nlp_alpha: sharpness of the softmin standing in for min(n, m).
Definition solver_ln.h:393
getSensitivityTable of the ensemble: the layer tables under a Layer column.
Definition solver_ln.h:485
std::vector< std::string > layer_methods
Per layer, the branch that layer took; empty for a layer with no solver.
Definition solver_ln.h:492
std::vector< sens::SensTable< T > > layer_tables
Per layer, the analytic Jacobian where that layer produced one.
Definition solver_ln.h:496
std::vector< Row > rows
Definition solver_ln.h:490
std::string method
The summary label: the common branch, or "mixed" when they differ.
Definition solver_ln.h:494
The LQN-level answer, indexed by element 1..nidx.
Definition solver_ln.h:420
std::vector< T > WN
Definition solver_ln.h:421
std::vector< bool > defined_W
Definition solver_ln.h:422
std::vector< bool > defined_U
Definition solver_ln.h:422
std::string warning
A warning the analysis raised, verbatim, empty when it raised none (method='nlp' off a root).
Definition solver_ln.h:433
std::vector< T > QN
Definition solver_ln.h:421
std::vector< bool > defined_R
Definition solver_ln.h:422
std::vector< T > RN
Definition solver_ln.h:421
std::vector< T > AN
Definition solver_ln.h:421
std::vector< bool > defined_Q
Definition solver_ln.h:422
bool is_bound
True when the numbers are a BOUND (method = mw.upper / mw.lower) rather than the fixed point.
Definition solver_ln.h:431
std::vector< bool > defined_A
Definition solver_ln.h:422
std::vector< T > UN
Definition solver_ln.h:421
std::vector< bool > defined_T
Definition solver_ln.h:422
std::vector< T > TN
Definition solver_ln.h:421
options.config.stochiter_* of SolverOptions.m, with its defaults.
Definition solver_ln.h:515
double a0
Robbins-Monro step immediately after burn-in.
Definition solver_ln.h:517
long conseq
consecutive sub-tolerance iterations required to stop
Definition solver_ln.h:519
long burnin
Picard iterations before the step decay starts.
Definition solver_ln.h:516
double alpha
step decay exponent, in (0.5, 1]
Definition solver_ln.h:518
double relax_burnin
Relaxation in force during burn-in, i.e.
Definition solver_ln.h:522
One layer's block of the layered transient.
Definition solver_ln.h:450
std::vector< std::vector< std::vector< double > > > QN
[station][class][point]
Definition solver_ln.h:453
std::vector< double > t
the output grid, shared by every series below
Definition solver_ln.h:451
std::vector< std::vector< std::vector< double > > > UN
Definition solver_ln.h:453
std::vector< std::vector< std::vector< double > > > TN
Definition solver_ln.h:453
The layered transient: one block per layer, plus how it was produced.
Definition solver_ln.h:476
std::string mode
"coupled" or "decoupled"
Definition solver_ln.h:478
long iterations
waveform-relaxation sweeps; 0 when decoupled
Definition solver_ln.h:479
double gap
final sup-norm trajectory change, coupled only
Definition solver_ln.h:480
std::vector< LnTranLayer > layers
Definition solver_ln.h:477
The structural answer of ln_zero_capacity; see the file header.
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
std::size_t iter_max
Definition lqn_mol.h:92
Options of the program: python export_nlp(lld=, alpha=, fork=).
double alpha
softmin sharpness
std::string fork
'refuse' or 'exp'
std::map< std::string, NlpLld > lld
station name -> multiplier
The options SolverMAM reads.
Definition mam_types.h:30
The options SolverMVA reads.
Definition mva_types.h:32
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:97
std::vector< T > X
Definition mva_types.h:99
std::vector< T > C
Definition mva_types.h:99
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:71