LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
aph2_adjust_opt.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_MAM_APH2_ADJUST_OPT_H
6#define LINE_API_MAM_APH2_ADJUST_OPT_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Optimization-based APH(2) moment adjustment: the 'opt_param' and 'opt_char'
12 * methods of matlab/lib/m3a/m3a/aph2/aph2_adjust.m.
13 *
14 * Both find (M2a, M3a) close to a requested (M2, M3), holding M1 fixed, such
15 * that an APH(2) with those three moments exists. They differ in the space
16 * they search:
17 *
18 * opt_param searches the PARAMETER space. The decision variables are the
19 * second phase mean l2 and the branching probability r1, with
20 * l1 = M1 - l2 r1 chosen so that the first moment is matched
21 * exactly; (M2a, M3a) are then whatever that APH(2) has. Any
22 * point of the feasible box is a valid APH(2), so the answer is
23 * feasible by construction.
24 * opt_char searches the CHARACTERISTIC space. The decision variables are
25 * (M2a, M3a) directly and feasibility is imposed as nonlinear
26 * constraints: the closed-form inversion aph2_fit1 / aph2_fit2
27 * must produce a non-negative discriminant, non-negative phase
28 * means and a probability in [0, 1]. The reference solves the
29 * two inversions as separate problems and keeps whichever gives
30 * the smaller adjustment; so does this port. The closed-form
31 * Telek-Heindl bounds are imposed as three further rows, which
32 * are redundant in exact arithmetic but close a gap in the
33 * inversion form that an optimizer otherwise exploits; see
34 * aph2_moment_bounds_rows for the reproduction.
35 *
36 * ACCEPTANCE CONTRACT (see line/util/auglag.h). MATLAB drives both with
37 * fmincon (active-set) or, in the '_gads' variants, with GlobalSearch. This
38 * port uses the augmented Lagrangian of line/util/auglag.h with the
39 * least-squares inner solver, which is a different algorithm: the iterates do
40 * not match and, on a problem with several local minima, the local minimum
41 * need not be the same one. What is guaranteed and tested is the
42 * specification:
43 * 1. the returned (M1, M2a, M3a) admits an APH(2), i.e. it satisfies the
44 * Telek-Heindl bounds that aph2_adjust's 'simple' method enforces;
45 * 2. when the requested (M2, M3) is already APH(2)-feasible, the returned
46 * pair reproduces it to the stated tolerance (objective ~ 0);
47 * 3. the achieved objective ||(M2a, M3a) - (M2, M3)||, the exact objective
48 * the reference minimizes, is returned in Aph2AdjustOptResult::objective
49 * so it can be compared against any other optimizer's on the same input.
50 *
51 * NOT PORTED: 'opt_param_gads' and 'opt_char_gads'. They are the same two
52 * problems handed to GlobalSearch, whose answer depends on a randomly seeded
53 * multi-start; a deterministic multi-start would be a different algorithm
54 * again and would not reproduce them, so they are deliberately absent rather
55 * than approximated. Since both problems are smooth with a small feasible
56 * region, the single-start solve here reaches the same objective on every
57 * input exercised in tests/test_mam_fit_optim.cpp.
58 *
59 * The 'opt_char' constraints need `tmp0`, the discriminant of the inversion
60 * (c(1) = -tmp0 asks for it to be non-negative). It is computed in the sibling
61 * aph2_fit1 / aph2_fit2, so the constraint must obtain it from them; it is not
62 * in a nested sibling's scope.
63 *
64 * Gated on transcendental arithmetic: the optimizers stop on tolerances, and
65 * the inversion takes a square root.
66 */
67
68#include <cstddef>
69#include <vector>
70
72#include "line/num/number.h"
73#include "line/util/auglag.h"
74#include "line/util/error.h"
75
76namespace line {
77namespace mam {
78
79/** Result of an optimization-based APH(2) moment adjustment. */
80template <class T>
82 T M2a; ///< adjusted second moment
83 T M3a; ///< adjusted third moment
84 T objective; ///< ||(M2a, M3a) - (M2, M3)||, the reference's objective
85 T violation; ///< worst constraint violation at the returned point
86 bool converged; ///< the constrained solve reached feasibility within its caps
87};
88
89namespace fitdetail {
90
91/**
92 * Telek-Heindl APH(2) feasibility of a moment triple, the predicate the
93 * 'simple' method of aph2_adjust enforces by clamping. Used here to state the
94 * acceptance criterion rather than to compute anything.
95 */
96template <class T>
97bool aph2_moments_feasible(const T& M1, const T& M2, const T& M3, const T& tol) {
98 const T one = num_traits<T>::from_int(1);
99 const T two = num_traits<T>::from_int(2);
100 const T three = num_traits<T>::from_int(3);
101 const T six = num_traits<T>::from_int(6);
102 const T half = num_traits<T>::from_rational(1, 2);
103 const T M1sq = M1 * M1;
104 const T scv = (M2 - M1sq) / M1sq;
105 if (scv < half * (one - tol)) return false;
106 if (scv <= one) {
107 const T d = one - scv;
108 const T lb = three * pw(M1, 3) * (three * scv - one + num_sqrt(two) * d * num_sqrt(d));
109 const T ub = six * pw(M1, 3) * scv;
110 return M3 >= lb * (one - tol) && M3 <= ub * (one + tol);
111 }
112 const T lb = three / two * pw(M1, 3) * (one + scv) * (one + scv);
113 return M3 >= lb * (one - tol);
114}
115
116/**
117 * The Telek-Heindl region as three inequality rows c <= 0 in (M2, M3) at a
118 * fixed M1: the SCV floor of 1/2, and the third-moment bounds, whose upper
119 * half is absent above SCV = 1.
120 *
121 * These rows are mathematically REDUNDANT with the inversion constraints of
122 * opt_char -- they describe the same region -- but they are imposed alongside
123 * them because the inversion form does not describe it as a CLOSED set, and an
124 * optimizer will find the gap. Reproduction, for M1 = 1 and the target
125 * (M2, M3) = (1.6, 2.0): the inversion constraints alone are minimized at
126 * (M2a, M3a) = (1.99794, 1.99983) with objective 0.398, well below the 1.4735
127 * of any point of the true region, by driving the branching probability to
128 * p1 = -2.5e-9 while the second phase mean diverges (l2 = 645). Every
129 * inversion constraint is then satisfied to 2.5e-9, which fmincon's default
130 * ConstraintTolerance of 1e-6 would accept outright, yet the returned moment
131 * pair admits no APH(2) at all: at p1 = 0 exactly the distribution is
132 * exponential and M3 must be 6 M1^3, not 2. The bounds below close that gap.
133 */
134template <class T>
135std::vector<T> aph2_moment_bounds_rows(const T& M1, const T& xM2, const T& xM3) {
136 const T zero = num_traits<T>::from_int(0);
137 const T one = num_traits<T>::from_int(1);
138 const T two = num_traits<T>::from_int(2);
139 const T three = num_traits<T>::from_int(3);
140 const T six = num_traits<T>::from_int(6);
141 const T half = num_traits<T>::from_rational(1, 2);
142
143 const T M1sq = M1 * M1;
144 const T scv = (xM2 - M1sq) / M1sq;
145 std::vector<T> c(3, zero);
146 c[0] = half - scv;
147 if (scv <= one) {
148 const T d = one - scv;
149 const T lb = three * pw(M1, 3) * (three * scv - one + num_sqrt(two) * d * num_sqrt(d));
150 const T ub = six * pw(M1, 3) * scv;
151 c[1] = lb - xM3;
152 c[2] = xM3 - ub;
153 } else {
154 const T lb = three / two * pw(M1, 3) * (one + scv) * (one + scv);
155 c[1] = lb - xM3;
156 c[2] = zero;
157 }
158 return c;
159}
160
161/**
162 * The APH(2) inversion of aph2_adjust's nested aph2_fit1 / aph2_fit2:
163 * given (M1, M2, M3), the phase means l1, l2 and the continuation probability
164 * p1. `swapped` selects fit2, which exchanges the two roots.
165 *
166 * Returns the discriminant tmp0 as well, because the constraint set needs it.
167 * The square root is taken of max(tmp0, 0): during the search the optimizer
168 * does visit points with a negative discriminant, where the inversion is not
169 * real; there the constraint -tmp0 <= 0 is what carries the information, and
170 * clamping keeps l1, l2, p1 finite so the other three constraints stay
171 * evaluable. The denominator 6 M2 - 12 M1^2 vanishes at the exponential point
172 * M2 = 2 M1^2; its magnitude is clamped to degen so that the constraint
173 * functions remain bounded there, which no reference does because no
174 * reference evaluates them at all (see the header note).
175 */
176template <class T>
177struct Aph2Inversion {
178 T tmp0;
179 T l1;
180 T l2;
181 T p1;
182};
183
184template <class T>
185Aph2Inversion<T> aph2_invert(const T& M1, const T& xM2, const T& xM3, bool swapped,
186 const T& degen) {
187 const T zero = num_traits<T>::from_int(0);
188 const T two = num_traits<T>::from_int(2);
189 const T three = num_traits<T>::from_int(3);
190 const T six = num_traits<T>::from_int(6);
191 const T eight = num_traits<T>::from_int(8);
192 const T nine = num_traits<T>::from_int(9);
193 const T twelve = num_traits<T>::from_int(12);
194
195 Aph2Inversion<T> r;
196 r.tmp0 = eight * pw(M1, 3) * xM3 / three - three * M1 * M1 * xM2 * xM2 -
197 two * M1 * xM2 * xM3 + two * pw(xM2, 3) + xM3 * xM3 / nine;
198 const T root = r.tmp0 > zero ? num_sqrt(r.tmp0) : zero;
199 const T tmp1 = three * root;
200 const T tmp2 = xM3 - three * M1 * xM2;
201 T tmp3 = six * xM2 - twelve * M1 * M1;
202 if (num_abs(tmp3) < degen) tmp3 = tmp3 < zero ? T(-degen) : degen;
203 if (!swapped) {
204 r.l1 = (tmp2 + tmp1) / tmp3;
205 r.l2 = (tmp2 - tmp1) / tmp3;
206 } else {
207 r.l1 = (tmp2 - tmp1) / tmp3;
208 r.l2 = (tmp2 + tmp1) / tmp3;
209 }
210 if (r.l2 == zero)
211 r.p1 = zero;
212 else
213 r.p1 = (M1 - r.l1) / r.l2;
214 return r;
215}
216
217} // namespace fitdetail
218
219/**
220 * aph2_adjust, method 'opt_param': adjust (M2, M3) by searching the APH(2)
221 * parameter space with M1 matched exactly.
222 *
223 * @param M1,M2,M3 the requested moments
224 * @param feastol the strict-inequality slack of the reference (MATLAB 1e-6),
225 * used as the lower bound on l2 and in the constraint
226 * l2 r1 <= M1 - feastol that keeps l1 positive
227 * @param degentol the slack that keeps r1 away from 0 and 1 (MATLAB 1e-8);
228 * set it to zero to allow the degenerate exponential
229 */
230template <class T>
231Aph2AdjustOptResult<T> aph2_adjust_opt_param(const T& M1, const T& M2, const T& M3,
232 const T& feastol, const T& degentol) {
234 "aph2_adjust_opt_param requires transcendental arithmetic");
235 using std::sqrt;
236 const T zero = num_traits<T>::from_int(0);
237 const T one = num_traits<T>::from_int(1);
238 const T two = num_traits<T>::from_int(2);
239 const T six = num_traits<T>::from_int(6);
240 const T half = num_traits<T>::from_rational(1, 2);
241 if (M1 <= zero) throw InputError("aph2_adjust_opt_param: non-positive first moment");
242
243 // moments of the APH(2) with phase means l1 = M1 - l2 r1 and l2, and
244 // continuation probability r1 (aph2_adjust's fun2)
245 auto moments = [M1, two, six](const std::vector<T>& x, T& xM2, T& xM3) {
246 const T l2 = x[0];
247 const T r1 = x[1];
248 const T l1 = M1 - l2 * r1;
249 xM2 = two * l1 * l1 + two * r1 * l1 * l2 + two * r1 * l2 * l2;
250 xM3 = six * fitdetail::pw(l1, 3) + six * r1 * l1 * l1 * l2 + six * r1 * l1 * l2 * l2 +
251 six * r1 * fitdetail::pw(l2, 3);
252 };
253
254 auto resid = [moments, M2, M3](const std::vector<T>& x) {
255 T xM2 = num_traits<T>::from_int(0);
256 T xM3 = num_traits<T>::from_int(0);
257 moments(x, xM2, xM3);
258 std::vector<T> r(2);
259 r[0] = M2 - xM2;
260 r[1] = M3 - xM3;
261 return r;
262 };
263
264 // box bounds and the reference's nonlcon3, all as inequality rows
265 auto g = [feastol, degentol, one, M1](const std::vector<T>& x) {
266 std::vector<T> gv(4);
267 gv[0] = feastol - x[0]; // l2 >= feastol
268 gv[1] = degentol - x[1]; // r1 >= degentol
269 gv[2] = x[1] - (one - degentol); // r1 <= 1 - degentol
270 gv[3] = x[0] * x[1] - M1 + feastol; // l2 r1 <= M1 - feastol
271 return gv;
272 };
273
274 std::vector<T> x0(2);
275 x0[0] = M1; // the reference's x0 = [M1, 1/2], which fits M1 exactly
276 x0[1] = half;
277
279 opt.ctol = num_traits<T>::from_double(1e-12);
280 const AugLagResult<T> sol = auglag_ls(resid, std::size_t(2), NoConstraints<T>(), g, x0, opt);
281
283 moments(sol.x, r.M2a, r.M3a);
284 const T d2 = M2 - r.M2a;
285 const T d3 = M3 - r.M3a;
286 r.objective = sqrt(T(d2 * d2 + d3 * d3));
287 r.violation = sol.violation;
288 r.converged = sol.violation <= opt.ctol;
289 return r;
290}
291
292/** aph2_adjust_opt_param with the MATLAB defaults feastol = 1e-6, degentol = 1e-8. */
293template <class T>
294Aph2AdjustOptResult<T> aph2_adjust_opt_param(const T& M1, const T& M2, const T& M3) {
295 return aph2_adjust_opt_param(M1, M2, M3, T(num_traits<T>::from_double(1e-6)),
297}
298
299/**
300 * aph2_adjust, method 'opt_char': adjust (M2, M3) by searching the moment
301 * space subject to APH(2) invertibility.
302 *
303 * Two constrained problems are solved, one per branch of the inversion, and
304 * the one with the smaller adjustment is returned, exactly as the reference
305 * intends.
306 *
307 * @param M1,M2,M3 the requested moments
308 * @param postol the strict-positivity slack applied to the phase means
309 * (MATLAB 1e-6)
310 * @param degen magnitude below which the inversion denominator
311 * 6 M2 - 12 M1^2 is clamped; see the note on aph2_invert
312 */
313template <class T>
314Aph2AdjustOptResult<T> aph2_adjust_opt_char(const T& M1, const T& M2, const T& M3, const T& postol,
315 const T& degen) {
317 "aph2_adjust_opt_char requires transcendental arithmetic");
318 using std::sqrt;
319 const T zero = num_traits<T>::from_int(0);
320 if (M1 <= zero) throw InputError("aph2_adjust_opt_char: non-positive first moment");
321
322 auto resid = [M2, M3](const std::vector<T>& x) {
323 std::vector<T> r(2);
324 r[0] = x[0] - M2;
325 r[1] = x[1] - M3;
326 return r;
327 };
328
330 bool have_best = false;
331
332 for (int branch = 0; branch < 2; ++branch) {
333 const bool swapped = branch == 1;
334 auto g = [M1, swapped, postol, degen](const std::vector<T>& x) {
335 const fitdetail::Aph2Inversion<T> inv =
336 fitdetail::aph2_invert(M1, x[0], x[1], swapped, degen);
337 std::vector<T> gv(6);
338 gv[0] = -inv.tmp0; // non-negative discriminant
339 gv[1] = -inv.l1 + postol; // positive first phase mean
340 gv[2] = -inv.l2 + postol; // positive second phase mean
341 gv[3] = -inv.p1; // non-negative branching probability
342 gv[4] = -x[0]; // M2 >= 0, the reference's lb
343 gv[5] = -x[1]; // M3 >= 0, the reference's lb
344 const std::vector<T> th = fitdetail::aph2_moment_bounds_rows(M1, x[0], x[1]);
345 for (std::size_t k = 0; k < th.size(); ++k) gv.push_back(th[k]);
346 return gv;
347 };
348
349 std::vector<T> x0(2);
350 x0[0] = M2;
351 x0[1] = M3;
353 opt.ctol = num_traits<T>::from_double(1e-12);
354 const AugLagResult<T> sol =
355 auglag_ls(resid, std::size_t(2), NoConstraints<T>(), g, x0, opt);
356
358 cand.M2a = sol.x[0];
359 cand.M3a = sol.x[1];
360 const T d2 = cand.M2a - M2;
361 const T d3 = cand.M3a - M3;
362 cand.objective = sqrt(T(d2 * d2 + d3 * d3));
363 cand.violation = sol.violation;
364 cand.converged = sol.violation <= opt.ctol;
365
366 // a branch that could not be made feasible is not a candidate at all
367 if (!cand.converged && have_best) continue;
368 if (!have_best || (cand.converged && !best.converged) ||
369 (cand.converged == best.converged && cand.objective < best.objective)) {
370 best = cand;
371 have_best = true;
372 }
373 }
374 return best;
375}
376
377/** aph2_adjust_opt_char with the MATLAB default postol = 1e-6 and degen = 1e-10. */
378template <class T>
379Aph2AdjustOptResult<T> aph2_adjust_opt_char(const T& M1, const T& M2, const T& M3) {
380 return aph2_adjust_opt_char(M1, M2, M3, T(num_traits<T>::from_double(1e-6)),
382}
383
384} // namespace mam
385} // namespace line
386
387#endif // LINE_API_MAM_APH2_ADJUST_OPT_H
Augmented Lagrangian method for equality- and inequality-constrained minimization,...
InputError(const std::string &what)
Definition error.h:105
The exception types the port throws.
Scalar helpers shared by the MAP/PH moment-matching headers.
Aph2AdjustOptResult< T > aph2_adjust_opt_param(const T &M1, const T &M2, const T &M3, const T &feastol, const T &degentol)
aph2_adjust, method 'opt_param': adjust (M2, M3) by searching the APH(2) parameter space with M1 matc...
Aph2AdjustOptResult< T > aph2_adjust_opt_char(const T &M1, const T &M2, const T &M3, const T &postol, const T &degen)
aph2_adjust, method 'opt_char': adjust (M2, M3) by searching the moment space subject to APH(2) inver...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:184
AugLagOptions< T > auglag_defaults()
Defaults: rho0 = 10, growth 10, feasibility 1e-10, 50 outer iterations.
Definition auglag.h:81
AugLagResult< T > auglag_ls(R r, std::size_t m, H h, G g, const std::vector< T > &x0, const AugLagOptions< T > &opt)
Augmented Lagrangian with a least-squares objective and levmar as the inner solver.
Definition auglag.h:246
Number-type abstraction for the templated API port.
Tuning of the outer multiplier iteration.
Definition auglag.h:68
Outcome of a constrained solve.
Definition auglag.h:96
std::vector< T > x
best point found
Definition auglag.h:97
T violation
max(|h_i|, max(0, g_j)) at x
Definition auglag.h:99
A constraint map that returns no constraints; the default for h or g.
Definition auglag.h:108
Result of an optimization-based APH(2) moment adjustment.
T M2a
adjusted second moment
T violation
worst constraint violation at the returned point
bool converged
the constrained solve reached feasibility within its caps
T objective
||(M2a, M3a) - (M2, M3)||, the reference's objective
T M3a
adjusted third moment