LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_compat_rate.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_SN_SN_COMPAT_RATE_H
6#define LINE_API_SN_SN_COMPAT_RATE_H
7
8/**
9 * @file
10 * @ingroup api_sn
11 * Total service rate of a station served by heterogeneous server pools with a
12 * class-compatibility graph, and the peak that normalizes its utilization.
13 *
14 * Port of matlab/src/api/sn/sn_compat_rate.m. A pool t holds counts(t) identical
15 * servers, each running at rates(t), and may serve operand j when
16 * compat(t, j) is nonzero. The rate the station clears in state n is
17 *
18 * mu(n) = sum_t counts(t) * rates(t) * min(1, sum_{j: compat(t,j) != 0} n(j))
19 *
20 * the ACTIVATED-SERVER law: a pool contributes its full rate as soon as it is
21 * compatible with at least one operand PRESENT. This is the order-independent
22 * reading of a compatibility structure -- at an INTEGER state mu depends on n
23 * only through its SUPPORT, so it is invariant to the arrival order and to any
24 * permutation of the microstate, which is exactly the condition an OI station
25 * has to meet (Dorsman & Gardner, Queueing Systems 107:205-256, 2024, Fig. 1).
26 * It is also what pas_compatibility_5class.m encodes for a flat Network, so the
27 * layered and flat readings of one compatibility matrix agree.
28 *
29 * WHY min(1, .) AND NOT AN INDICATOR. At every integer state the two agree
30 * exactly -- a pool with at least one compatible job present is fully active,
31 * one with none is idle -- so nothing about the OI law on the real state
32 * lattice changes. They part company only at a FRACTIONAL argument, which is
33 * what a mean-value solver hands this function: AMVA evaluates the rate at a
34 * mean population, and under a hard indicator any operand with a mean above
35 * zero, however small, activates every pool it touches. A compatibility
36 * structure would then be invisible to AMVA whenever every operand is a little
37 * bit busy -- which is nearly always. Scaling linearly below one job keeps the
38 * structure visible at the evaluation point while leaving the integer-state law
39 * untouched; it is the ordinary continuous relaxation of a step function, and
40 * the CTMC and simulation paths, which only ever evaluate at integer states,
41 * cannot tell the difference.
42 *
43 * IT IS NOT A MATCHING. A pool of two servers compatible with a class holding
44 * ONE job contributes both servers here, which over-counts against a
45 * non-redundant system where one server serves one job. That is deliberate:
46 * the matching size depends on the counts and not only on the support, so it is
47 * NOT order-independent and would take the station outside the product form the
48 * OI closure is built on. A model that means the matching wants a different
49 * station, not a different reading of this one.
50 *
51 * WHY THE PEAK IS SEPARATE. Utilization at a rate-scaled station is reported as
52 * U = T*S/peak, and the peak is the rate with every pool active, sum_t
53 * counts(t)*rates(t). It is a property of the DECLARATION, not of a state, so
54 * it is computed once and handed to the solver beside the handle rather than
55 * recovered from mu at a guessed state.
56 */
57
58#include <cstddef>
59#include <vector>
60
61#include "line/num/number.h"
62#include "line/util/error.h"
63#include "line/util/matrix.h"
64
65namespace line {
66namespace api {
67
68/**
69 * Rate cleared by the pools when the operands in `n` are present.
70 *
71 * @param compat (npools x noperands), nonzero where the pool may serve
72 * @param counts (npools) servers held by each pool
73 * @param rates (npools) per-server rate of each pool
74 * @param n (noperands) per-operand population, integer or fractional
75 */
76template <class T>
77T sn_compat_rate(const Matrix<T>& compat, const std::vector<double>& counts,
78 const std::vector<T>& rates, const std::vector<T>& n) {
79 const T zero = num_traits<T>::from_int(0);
80 const std::size_t npools = counts.size();
81 if (rates.size() != npools)
82 throw InputError("sn_compat_rate: one rate per pool is required");
83 if (compat.rows() != npools)
84 throw InputError("sn_compat_rate: compat must have one row per pool");
85 if (n.size() != compat.cols())
86 throw InputError("sn_compat_rate: n must have one entry per operand");
87 const T one = num_traits<T>::from_int(1);
88 T mu = zero;
89 for (std::size_t t = 0; t < npools; ++t) {
90 // The pool is activated ONCE by the jobs it can reach, not once per
91 // operand: its weight is the compatible load, capped at one job.
92 T load = zero;
93 for (std::size_t j = 0; j < n.size(); ++j)
94 if (compat(t, j) != zero && n[j] > zero) load = T(load + n[j]);
95 if (load > one) load = one;
96 mu = mu + num_traits<T>::from_double(counts[t]) * rates[t] * load;
97 }
98 return mu;
99}
100
101/** Rate with every pool active: sum_t counts(t)*rates(t). Normalizes U = T*S/peak. */
102template <class T>
103T sn_compat_peak(const std::vector<double>& counts, const std::vector<T>& rates) {
104 const T zero = num_traits<T>::from_int(0);
105 if (rates.size() != counts.size())
106 throw InputError("sn_compat_peak: one rate per pool is required");
107 T peak = zero;
108 for (std::size_t t = 0; t < counts.size(); ++t)
109 peak = peak + num_traits<T>::from_double(counts[t]) * rates[t];
110 return peak;
111}
112
113/**
114 * Rate scaling eta(n) a compatibility declaration imposes on its station.
115 *
116 * This is what SolverLN carries onto the layer station, and it is NOT
117 * `sn_compat_rate / sn_compat_peak`. The denominator is the rate the SAME
118 * population would obtain under FULL compatibility,
119 *
120 * eta(n) = mu(n) / (peak * min(1, sum_j n(j)))
121 *
122 * so eta isolates the effect of the compatibility GRAPH and nothing else. The
123 * denominator DAMPS BY OCCUPANCY RELATIVE TO THE SERVER COUNT, min(1, N/S),
124 * because that is what the solver's own multiserver term contributes: it
125 * applies min(N,S) servers at the average server rate peak/S, so
126 *
127 * min(N,S) * (peak/S) * eta(n) = mu(n)
128 *
129 * and the station clears the activated-server rate exactly, at every state.
130 * Damping by min(1, N) instead -- which this did until 2026-08-28 -- left the
131 * effective law at min(N,S)/S * mu(n) and cancelled the REDUNDANCY SPEED-UP the
132 * activated-server law exists to express: a pool of S servers facing one
133 * compatible job clears S, not 1, because every one works on it and the first
134 * to finish cancels the rest. eta is therefore above one at low occupancy,
135 * which is the speed-up of servers that would otherwise be idle.
136 */
137template <class T>
138T sn_compat_scaling(const Matrix<T>& compat, const std::vector<double>& counts,
139 const std::vector<T>& rates, const std::vector<T>& n) {
140 const T zero = num_traits<T>::from_int(0);
141 const T one = num_traits<T>::from_int(1);
142 T total = zero;
143 for (std::size_t j = 0; j < n.size(); ++j)
144 if (n[j] > zero) total = T(total + n[j]);
145 if (!(total > zero)) return one; // an empty station: nothing to scale
146 T nservers = zero;
147 for (std::size_t t = 0; t < counts.size(); ++t)
148 nservers = T(nservers + num_traits<T>::from_double(counts[t]));
149 if (!(nservers > zero)) return one;
150 const T occ = T(total / nservers);
151 T ref = sn_compat_peak(counts, rates) * (occ > one ? one : occ);
152 return T(sn_compat_rate(compat, counts, rates, n) / ref);
153}
154
155} // namespace api
156} // namespace line
157
158#endif // LINE_API_SN_SN_COMPAT_RATE_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Dense matrix and non-owning view.
T sn_compat_rate(const Matrix< T > &compat, const std::vector< double > &counts, const std::vector< T > &rates, const std::vector< T > &n)
Rate cleared by the pools when the operands in n are present.
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.
T sn_compat_peak(const std::vector< double > &counts, const std::vector< T > &rates)
Rate with every pool active: sum_t counts(t)*rates(t).
Number-type abstraction for the templated API port.