LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_quadrature.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_QSYS_QUADRATURE_H
6#define LINE_API_QSYS_QUADRATURE_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * Adaptive quadrature for the qsys functions whose MATLAB originals call
12 * integral(), and the cumulative trapezoid rule for the one that calls
13 * cumtrapz().
14 *
15 * Three MATLAB files in matlab/src/api/qsys reduce a per-class mean response
16 * time to a definite integral of a non-elementary integrand -- qsys_mg1_fb,
17 * qsys_mg1_psjf and qsys_mg1k_loss -- and one, qsys_mg1_srpt, integrates on a
18 * fixed uniform grid with cumtrapz/trapz. This header carries the two rules
19 * they need so that each ported function stays a 1:1 image of its MATLAB file.
20 * It is the numerical counterpart of qsys_types.h, which carries the shared
21 * return type; nothing here corresponds to a MATLAB file of its own.
22 *
23 * The adaptive rule is the Gauss-Kronrod 7/15 pair on recursively bisected
24 * subintervals, with the local error estimated as |K - G| and the tolerance
25 * split evenly between halves. That is the same rule MATLAB's integral() uses
26 * (MATLAB applies it to a transformed interval and controls the error
27 * globally), so the two agree to the requested relative tolerance on the
28 * smooth integrands here; the ported functions therefore claim agreement at
29 * the tolerance MATLAB was asked for, not beyond it.
30 *
31 * ARITHMETIC. Both rules are inherently inexact -- the Kronrod nodes are
32 * irrational and the trapezoid rule has a discretization error -- so both are
33 * gated on num_traits<T>::has_transcendental. The node and weight constants
34 * are the QUADPACK values, but they enter through num_traits<T>::from_double
35 * and are therefore carried at double precision: a Real<D> instantiation gains
36 * exact accumulation and no cancellation in the sums, but the quadrature error
37 * floor stays near 1e-16 relative because the nodes themselves do. Any qsys
38 * function that routes through this header inherits that floor, and its tests
39 * assert at the tolerance MATLAB's integral() was asked for, never below it.
40 */
41
42#include <cstddef>
43#include <vector>
44
45#include "line/num/number.h"
46#include "line/util/error.h"
47
48namespace line {
49namespace qsys {
50namespace detail {
51
52/** Kronrod 15-point abscissae on [-1,1], positive half plus the origin. */
53template <class T>
54const std::vector<T>& gk15_nodes() {
55 static const std::vector<T> x = {
56 num_traits<T>::from_double(0.991455371120812639206854697526329),
57 num_traits<T>::from_double(0.949107912342758524526189684047851),
58 num_traits<T>::from_double(0.864864423359769072789712788640926),
59 num_traits<T>::from_double(0.741531185599394439863864773280788),
60 num_traits<T>::from_double(0.586087235467691130294144838258730),
61 num_traits<T>::from_double(0.405845151377397166906606412076961),
62 num_traits<T>::from_double(0.207784955007898467600689403773245),
63 num_traits<T>::from_double(0.0)};
64 return x;
65}
66
67/** Kronrod weights matched to gk15_nodes. */
68template <class T>
69const std::vector<T>& gk15_weights() {
70 static const std::vector<T> w = {
71 num_traits<T>::from_double(0.022935322010529224963732008058970),
72 num_traits<T>::from_double(0.063092092629978553290700663189204),
73 num_traits<T>::from_double(0.104790010322250183839876322541518),
74 num_traits<T>::from_double(0.140653259715525918745189590510238),
75 num_traits<T>::from_double(0.169004726639267902826583426598550),
76 num_traits<T>::from_double(0.190350578064785409913256402421014),
77 num_traits<T>::from_double(0.204432940075298892414161999234649),
78 num_traits<T>::from_double(0.209482141084727828012999174891714)};
79 return w;
80}
81
82/** Gauss 7-point weights, applied at the odd-indexed Kronrod nodes. */
83template <class T>
84const std::vector<T>& g7_weights() {
85 static const std::vector<T> w = {
86 num_traits<T>::from_double(0.129484966168869693270611432679082),
87 num_traits<T>::from_double(0.279705391489276667901467771423780),
88 num_traits<T>::from_double(0.381830050505118944950369775488975),
89 num_traits<T>::from_double(0.417959183673469387755102040816327)};
90 return w;
91}
92
93/**
94 * One Gauss-Kronrod 7/15 panel on [a,b]. Returns the Kronrod estimate and
95 * writes |K - G| into err.
96 */
97template <class T, class F>
98T gk15_panel(F&& f, const T& a, const T& b, T& err) {
99 const T two = num_traits<T>::from_int(2);
100 const T c = (a + b) / two;
101 const T h = (b - a) / two;
102 const std::vector<T>& x = gk15_nodes<T>();
103 const std::vector<T>& wk = gk15_weights<T>();
104 const std::vector<T>& wg = g7_weights<T>();
105
106 T K = num_traits<T>::from_int(0);
107 T G = num_traits<T>::from_int(0);
108 for (std::size_t i = 0; i < 7; ++i) {
109 const T d = h * x[i];
110 const T fsum = f(T(c - d)) + f(T(c + d));
111 K += wk[i] * fsum;
112 if (i % 2 == 1) G += wg[i / 2] * fsum;
113 }
114 const T f0 = f(c);
115 K += wk[7] * f0;
116 G += wg[3] * f0;
117 K *= h;
118 G *= h;
119 err = num_abs(T(K - G));
120 return K;
121}
122
123/**
124 * One adaptively bisected panel. min_depth forces the first few bisections
125 * unconditionally: on a long interval where the integrand is supported on a
126 * small part of it -- qsys_mg1k_loss integrates over [0, 1e4/lambda] a density
127 * that has died out by t = O(1/mu) -- a single Kronrod panel can see fifteen
128 * near-zero samples, estimate a near-zero error and accept a wrong answer.
129 * MATLAB's integral() avoids this by subdividing the transformed interval
130 * before it adapts; forcing a few levels here is the same guard.
131 */
132template <class T, class F>
133T gk15_adapt(F&& f, const T& a, const T& b, const T& reltol, const T& abstol, unsigned depth,
134 unsigned min_depth) {
135 T err = num_traits<T>::from_int(0);
136 const T q = gk15_panel(f, a, b, err);
137 const T target = reltol * num_abs(q);
138 if (min_depth == 0 && (depth == 0 || err <= (target > abstol ? target : abstol))) return q;
139 if (depth == 0) return q;
140 const T m = (a + b) / num_traits<T>::from_int(2);
141 const T half = abstol / num_traits<T>::from_int(2);
142 const unsigned md = min_depth == 0 ? 0u : min_depth - 1;
143 return gk15_adapt(f, a, m, reltol, half, depth - 1, md) +
144 gk15_adapt(f, m, b, reltol, half, depth - 1, md);
145}
146
147/**
148 * Adaptive Gauss-Kronrod integration of f over [a,b].
149 *
150 * The interval is first split into init_panels equal pieces, each of which is
151 * then bisected adaptively with at least min_depth forced levels.
152 *
153 * @param reltol relative tolerance, MATLAB's 'RelTol'
154 * @param abstol absolute tolerance, MATLAB's 'AbsTol'
155 * @param depth bisection budget per panel; exhausting it returns the best
156 * estimate reached, exactly as MATLAB warns and returns rather
157 * than failing
158 * @param f the integrand
159 * @param a lower limit
160 * @param b upper limit
161 * @param init_panels number of panels of the first pass
162 * @param min_depth refinement levels performed before the tolerance test
163 */
164template <class T, class F>
165T num_integral(F&& f, const T& a, const T& b, const T& reltol, const T& abstol,
166 unsigned depth = 50u, unsigned init_panels = 8u, unsigned min_depth = 3u) {
167 static_assert(num_traits<T>::has_transcendental,
168 "num_integral requires transcendental arithmetic");
169 if (b == a) return num_traits<T>::from_int(0);
170 if (init_panels == 0) init_panels = 1;
171 const T np = num_traits<T>::from_int(static_cast<long>(init_panels));
172 const T panel_abstol = abstol / np;
173 T total = num_traits<T>::from_int(0);
174 for (unsigned i = 0; i < init_panels; ++i) {
175 const T lo = a + (b - a) * num_traits<T>::from_int(static_cast<long>(i)) / np;
176 const T hi = a + (b - a) * num_traits<T>::from_int(static_cast<long>(i + 1)) / np;
177 total += gk15_adapt(f, lo, hi, reltol, panel_abstol, depth, min_depth);
178 }
179 return total;
180}
181
182/**
183 * Trapezoid rule on the sample pairs (x, y), MATLAB's trapz(x, y).
184 */
185template <class T>
186T num_trapz(const std::vector<T>& x, const std::vector<T>& y) {
187 static_assert(num_traits<T>::has_transcendental,
188 "num_trapz requires transcendental arithmetic");
189 if (x.size() != y.size()) throw InputError("num_trapz: x and y have different lengths");
190 const T two = num_traits<T>::from_int(2);
191 T s = num_traits<T>::from_int(0);
192 for (std::size_t i = 1; i < x.size(); ++i) s += (x[i] - x[i - 1]) * (y[i] + y[i - 1]) / two;
193 return s;
194}
195
196/**
197 * Cumulative trapezoid rule, MATLAB's cumtrapz(x, y): element i is the
198 * integral of y from x[0] to x[i], so the first element is zero.
199 */
200template <class T>
201std::vector<T> num_cumtrapz(const std::vector<T>& x, const std::vector<T>& y) {
202 static_assert(num_traits<T>::has_transcendental,
203 "num_cumtrapz requires transcendental arithmetic");
204 if (x.size() != y.size()) throw InputError("num_cumtrapz: x and y have different lengths");
205 const T two = num_traits<T>::from_int(2);
206 std::vector<T> c(x.size(), num_traits<T>::from_int(0));
207 for (std::size_t i = 1; i < x.size(); ++i)
208 c[i] = c[i - 1] + (x[i] - x[i - 1]) * (y[i] + y[i - 1]) / two;
209 return c;
210}
211
212} // namespace detail
213} // namespace qsys
214} // namespace line
215
216#endif // LINE_API_QSYS_QUADRATURE_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.