LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_export_nlp.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_LQN_EXPORT_NLP_H
6#define LINE_API_LQN_EXPORT_NLP_H
7
8/**
9 * @file
10 * @ingroup api_lqn
11 * A layered queueing network as ONE nonlinear program: linear constraints over
12 * the flows, the service-time composition and the closures, plus a nonlinear
13 * objective that is the sum of squares of the QD-AMVA congestion and Little laws
14 * (Casale, Perez and Wang, PERFORMANCE 2015).
15 *
16 * Twin of python line_solver.api.lqn.lqn_export_nlp. The python exporter EMITS
17 * a self-contained script; that emission is python-only by design. What is
18 * ported is the PROGRAM the script builds, constructed in memory: the same
19 * element numbering, the same variable layout
20 *
21 * Xe(NE) Xa(NA) Xc(NC) W(NA) S(NE) R(NC) Q(NA) Qt(NC) BC(NC) BR(NR) GH(NGH) GT(NGT) SA(NE if forking)
22 *
23 * the same row order of AEQ/AUB, the same residual order, the same refusals
24 * (`LINE:unsupported:NlpExport`) and the same solve: Newton on the square
25 * system with a minimum-norm least-squares step, escalating the warm-start
26 * sweeps 0, 5, 20, 100 before falling back to a constrained descent.
27 *
28 * THE FALLBACK DIFFERS FROM PYTHON. Python falls back to scipy's trust-constr;
29 * this port runs a projected Levenberg-Marquardt on the merit
30 * ||r||^2 + ||AEQ x - BEQ||^2 + ||max(0, AUB x - BUB)||^2 over x >= 0, identical
31 * in MATLAB, the JAR and C++. Every model the reference suite solves is rooted
32 * by the Newton ladder, which reaches the same root in every codebase.
33 *
34 * Rate functions r_i(n) = softmin(n, m_i) [* lld_i(n)] and the AND-fork closure
35 * are differentiated by COMPLEX STEP, as in python. A declared lld multiplier is
36 * an expression in `n` over a small grammar shared by the four codebases:
37 *
38 * expr := term (('+'|'-') term)* term := unary (('*'|'/') unary)*
39 * unary := ('+'|'-') unary | power power := atom ('**' unary)?
40 * atom := NUMBER | 'n' | FUNC '(' expr {',' expr} ')' | '(' expr ')'
41 * FUNC := exp | log | sqrt | sin | cos | tanh | abs | min | max, optionally np. or math.
42 *
43 * `abs` is the real modulus and `min`/`max` reject a complex argument, so both
44 * are caught by the analyticity check exactly as python catches them.
45 */
46
47#include <algorithm>
48#include <cmath>
49#include <complex>
50#include <cstdio>
51#include <cstdlib>
52#include <limits>
53#include <map>
54#include <memory>
55#include <set>
56#include <stdexcept>
57#include <string>
58#include <utility>
59#include <vector>
60
63#include "line/num/number.h"
64#include "line/util/error.h"
65#include "line/util/lu.h"
66#include "line/util/matrix.h"
67
68namespace line {
69namespace lqn {
70
71/**
72 * A declared queue-dependent rate multiplier of one station: an expression in
73 * `n`, or a tabulated lldscaling row, which is refused because its interpolant
74 * has a corner at every knot.
75 */
76struct NlpLld {
77 std::string expr;
78 std::vector<double> table;
79 bool tabulated = false;
80 NlpLld() {}
81 NlpLld(const std::string& e) : expr(e) {} // NOLINT(runtime/explicit): a string IS an lld
82 NlpLld(const char* e) : expr(e) {} // NOLINT(runtime/explicit)
83 static NlpLld tabulated_row(const std::vector<double>& v) {
84 NlpLld l;
85 l.table = v;
86 l.tabulated = true;
87 return l;
88 }
89};
90
91/** Options of the program: python export_nlp(lld=, alpha=, fork=). */
92struct NlpOptions {
93 std::map<std::string, NlpLld> lld; ///< station name -> multiplier
94 double alpha = 20.0; ///< softmin sharpness
95 std::string fork = "refuse"; ///< 'refuse' or 'exp'
96};
97
98namespace nlp_detail {
99
100using cplx = std::complex<double>;
101
102inline std::string fmt(const char* f, double v) {
103 char b[64];
104 std::snprintf(b, sizeof(b), f, v);
105 return b;
106}
107
108/** Python's repr of a float: the shortest round-tripping digits, in python's layout. */
109inline std::string py_repr(double v) {
110 if (std::isnan(v)) return "nan";
111 if (std::isinf(v)) return v > 0 ? "inf" : "-inf";
112 if (v == 0.0) return std::signbit(v) ? "-0.0" : "0.0";
113 char b[64];
114 int p = 1;
115 for (; p <= 17; ++p) {
116 std::snprintf(b, sizeof(b), "%.*e", p - 1, v);
117 if (std::strtod(b, nullptr) == v) break;
118 }
119 const std::string s(b);
120 const std::size_t epos = s.find('e');
121 const int ex = std::atoi(s.c_str() + epos + 1);
122 std::string mant = s.substr(0, epos);
123 const bool neg = mant[0] == '-';
124 if (neg) mant = mant.substr(1);
125 std::string digits;
126 for (char c : mant)
127 if (c != '.') digits += c;
128 while (digits.size() > 1 && digits.back() == '0') digits.pop_back();
129 std::string out;
130 if (ex >= -4 && ex < 16) {
131 if (ex >= 0) {
132 if (int(digits.size()) <= ex + 1) {
133 out = digits + std::string(std::size_t(ex + 1 - int(digits.size())), '0') + ".0";
134 } else {
135 out = digits.substr(0, std::size_t(ex + 1)) + "." + digits.substr(std::size_t(ex + 1));
136 }
137 } else {
138 out = "0." + std::string(std::size_t(-ex - 1), '0') + digits;
139 }
140 } else {
141 out = digits.substr(0, 1);
142 if (digits.size() > 1) out += "." + digits.substr(1);
143 char e[16];
144 std::snprintf(e, sizeof(e), "e%c%02d", ex < 0 ? '-' : '+', ex < 0 ? -ex : ex);
145 out += e;
146 }
147 return neg ? "-" + out : out;
148}
149
150/** Python's repr of a value that may be complex. */
151inline std::string py_repr(const cplx& z) {
152 if (z.imag() == 0.0) return py_repr(z.real());
153 std::string im = py_repr(z.imag());
154 if (im.size() > 2 && im.compare(im.size() - 2, 2, ".0") == 0) im = im.substr(0, im.size() - 2);
155 std::string re = py_repr(z.real());
156 if (re.size() > 2 && re.compare(re.size() - 2, 2, ".0") == 0) re = re.substr(0, re.size() - 2);
157 return "(" + re + (z.imag() < 0 || std::signbit(z.imag()) ? "" : "+") + im + "j)";
158}
159
160/** An evaluation failure, carrying the python exception name the message reports. */
161struct LldEvalError : std::runtime_error {
162 std::string kind;
163 LldEvalError(const std::string& k, const std::string& m) : std::runtime_error(m), kind(k) {}
164};
165
166/** A parse failure: the text is not an expression of the grammar. */
167struct LldSyntaxError : std::runtime_error {
168 explicit LldSyntaxError(const std::string& m) : std::runtime_error(m) {}
169};
170
171/**
172 * An lld expression, parsed once and evaluated over std::complex<double>.
173 *
174 * Each operation takes REAL semantics when its operands are real, as python
175 * does on a float (log and sqrt of a negative number are nan, math.* raises),
176 * and complex semantics otherwise, which is what the complex step reads.
177 */
178class LldExpr {
179public:
180 explicit LldExpr(const std::string& src) : src_(src) {
181 pos_ = 0;
182 next();
183 root_ = parse_expr();
184 if (tok_.kind != Tok::END) throw LldSyntaxError("invalid syntax");
185 }
186
187 cplx eval(const cplx& n) const { return eval_node(root_, n); }
188
189private:
190 struct Tok {
191 enum Kind { END, NUM, ID, OP, LP, RP, COMMA } kind = END;
192 std::string text;
193 double num = 0.0;
194 };
195 struct Node {
196 enum Kind { NUM, VAR, ID, NEG, POS, ADD, SUB, MUL, DIV, POW, CALL } kind = NUM;
197 double num = 0.0;
198 std::string name;
199 std::vector<int> kids;
200 };
201
202 std::string src_;
203 std::size_t pos_ = 0;
204 Tok tok_;
205 std::vector<Node> nodes_;
206 int root_ = -1;
207
208 static bool idstart(char c) { return std::isalpha(static_cast<unsigned char>(c)) || c == '_'; }
209 static bool idchar(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
210 static bool digit(char c) { return c >= '0' && c <= '9'; }
211
212 void next() {
213 while (pos_ < src_.size() && std::isspace(static_cast<unsigned char>(src_[pos_]))) ++pos_;
214 tok_ = Tok();
215 if (pos_ >= src_.size()) return;
216 const char c = src_[pos_];
217 if (digit(c) || (c == '.' && pos_ + 1 < src_.size() && digit(src_[pos_ + 1]))) {
218 const std::size_t b = pos_;
219 while (pos_ < src_.size() && digit(src_[pos_])) ++pos_;
220 if (pos_ < src_.size() && src_[pos_] == '.') {
221 ++pos_;
222 while (pos_ < src_.size() && digit(src_[pos_])) ++pos_;
223 }
224 if (pos_ < src_.size() && (src_[pos_] == 'e' || src_[pos_] == 'E')) {
225 std::size_t q = pos_ + 1;
226 if (q < src_.size() && (src_[q] == '+' || src_[q] == '-')) ++q;
227 if (q >= src_.size() || !digit(src_[q])) throw LldSyntaxError("invalid decimal literal");
228 pos_ = q;
229 while (pos_ < src_.size() && digit(src_[pos_])) ++pos_;
230 }
231 if (pos_ < src_.size() && idchar(src_[pos_])) throw LldSyntaxError("invalid decimal literal");
232 tok_.kind = Tok::NUM;
233 tok_.text = src_.substr(b, pos_ - b);
234 tok_.num = std::strtod(tok_.text.c_str(), nullptr);
235 return;
236 }
237 if (idstart(c)) {
238 const std::size_t b = pos_;
239 while (pos_ < src_.size() && idchar(src_[pos_])) ++pos_;
240 while (pos_ + 1 < src_.size() && src_[pos_] == '.' && idstart(src_[pos_ + 1])) {
241 ++pos_;
242 while (pos_ < src_.size() && idchar(src_[pos_])) ++pos_;
243 }
244 tok_.kind = Tok::ID;
245 tok_.text = src_.substr(b, pos_ - b);
246 return;
247 }
248 ++pos_;
249 if (c == '*' && pos_ < src_.size() && src_[pos_] == '*') {
250 ++pos_;
251 tok_.kind = Tok::OP;
252 tok_.text = "**";
253 return;
254 }
255 if (c == '+' || c == '-' || c == '*' || c == '/') {
256 tok_.kind = Tok::OP;
257 tok_.text = std::string(1, c);
258 return;
259 }
260 if (c == '(') { tok_.kind = Tok::LP; return; }
261 if (c == ')') { tok_.kind = Tok::RP; return; }
262 if (c == ',') { tok_.kind = Tok::COMMA; return; }
263 throw LldSyntaxError("invalid syntax");
264 }
265
266 int add(Node::Kind k, std::vector<int> kids, double num = 0.0, const std::string& name = "") {
267 Node nd;
268 nd.kind = k;
269 nd.kids = std::move(kids);
270 nd.num = num;
271 nd.name = name;
272 nodes_.push_back(nd);
273 return int(nodes_.size()) - 1;
274 }
275
276 bool is_op(const char* s) const { return tok_.kind == Tok::OP && tok_.text == s; }
277
278 int parse_expr() {
279 int l = parse_term();
280 while (is_op("+") || is_op("-")) {
281 const bool plus = is_op("+");
282 next();
283 const int r = parse_term();
284 l = add(plus ? Node::ADD : Node::SUB, {l, r});
285 }
286 return l;
287 }
288 int parse_term() {
289 int l = parse_unary();
290 while (is_op("*") || is_op("/")) {
291 const bool mul = is_op("*");
292 next();
293 const int r = parse_unary();
294 l = add(mul ? Node::MUL : Node::DIV, {l, r});
295 }
296 return l;
297 }
298 int parse_unary() {
299 if (is_op("+") || is_op("-")) {
300 const bool neg = is_op("-");
301 next();
302 const int u = parse_unary();
303 return add(neg ? Node::NEG : Node::POS, {u});
304 }
305 return parse_power();
306 }
307 int parse_power() {
308 const int a = parse_atom();
309 if (is_op("**")) {
310 next();
311 const int e = parse_unary();
312 return add(Node::POW, {a, e});
313 }
314 return a;
315 }
316 int parse_atom() {
317 if (tok_.kind == Tok::NUM) {
318 const double v = tok_.num;
319 next();
320 return add(Node::NUM, {}, v);
321 }
322 if (tok_.kind == Tok::ID) {
323 const std::string name = tok_.text;
324 next();
325 if (tok_.kind == Tok::LP) {
326 next();
327 std::vector<int> args;
328 if (tok_.kind != Tok::RP) {
329 args.push_back(parse_expr());
330 while (tok_.kind == Tok::COMMA) {
331 next();
332 args.push_back(parse_expr());
333 }
334 }
335 if (tok_.kind != Tok::RP) throw LldSyntaxError("'(' was never closed");
336 next();
337 return add(Node::CALL, args, 0.0, name);
338 }
339 if (name == "n") return add(Node::VAR, {});
340 if (name == "np.pi" || name == "math.pi") return add(Node::NUM, {}, 3.141592653589793);
341 if (name == "np.e" || name == "math.e") return add(Node::NUM, {}, 2.718281828459045);
342 return add(Node::ID, {}, 0.0, name);
343 }
344 if (tok_.kind == Tok::LP) {
345 next();
346 const int e = parse_expr();
347 if (tok_.kind != Tok::RP) throw LldSyntaxError("'(' was never closed");
348 next();
349 return e;
350 }
351 throw LldSyntaxError("invalid syntax");
352 }
353
354 static bool real(const cplx& z) { return z.imag() == 0.0; }
355
356 static cplx ipow(cplx b, long e) {
357 // CPython's c_powi: repeated squaring for a small integral exponent
358 const bool inv = e < 0;
359 unsigned long m = static_cast<unsigned long>(inv ? -e : e);
360 cplx r(1.0, 0.0);
361 while (m) {
362 if (m & 1UL) r *= b;
363 b *= b;
364 m >>= 1;
365 }
366 return inv ? cplx(1.0, 0.0) / r : r;
367 }
368
369 static cplx do_pow(const cplx& a, const cplx& b) {
370 if (real(a) && real(b)) {
371 const double x = a.real(), y = b.real();
372 if (x == 0.0 && y < 0.0)
373 throw LldEvalError("ZeroDivisionError", "0.0 cannot be raised to a negative power");
374 if (x >= 0.0 || y == std::floor(y)) return cplx(std::pow(x, y), 0.0);
375 return std::pow(a, b); // a negative base to a fractional power is complex in python
376 }
377 if (a == cplx(0.0, 0.0)) {
378 if (b.imag() != 0.0 || b.real() < 0.0)
379 throw LldEvalError("ZeroDivisionError", "0.0 to a negative or complex power");
380 return b.real() == 0.0 ? cplx(1.0, 0.0) : cplx(0.0, 0.0);
381 }
382 if (b.imag() == 0.0 && b.real() == std::floor(b.real()) && std::fabs(b.real()) <= 100.0)
383 return ipow(a, static_cast<long>(b.real()));
384 return std::pow(a, b);
385 }
386
387 static cplx call(const std::string& full, const std::vector<cplx>& v) {
388 std::string prefix, base = full;
389 const std::size_t dot = full.rfind('.');
390 if (dot != std::string::npos) {
391 prefix = full.substr(0, dot);
392 base = full.substr(dot + 1);
393 }
394 if (!prefix.empty() && prefix != "np" && prefix != "math")
395 throw LldEvalError("NameError", "name '" + prefix.substr(0, prefix.find('.')) +
396 "' is not defined");
397 const bool math = prefix == "math";
398 static const char* unary[] = {"exp", "log", "sqrt", "sin", "cos", "tanh", "abs"};
399 bool isunary = false;
400 for (const char* u : unary)
401 if (base == u) isunary = true;
402 if (isunary) {
403 if (v.size() != 1)
404 throw LldEvalError("TypeError", base + "() takes exactly one argument (" +
405 std::to_string(v.size()) + " given)");
406 const cplx z = v[0];
407 if (base == "abs") return cplx(std::abs(z), 0.0);
408 if (real(z)) {
409 const double x = z.real();
410 if (base == "exp") return cplx(std::exp(x), 0.0);
411 if (base == "sin") return cplx(std::sin(x), 0.0);
412 if (base == "cos") return cplx(std::cos(x), 0.0);
413 if (base == "tanh") return cplx(std::tanh(x), 0.0);
414 if (base == "log") {
415 if (math && x <= 0.0) throw LldEvalError("ValueError", "math domain error");
416 if (x < 0.0) return cplx(std::numeric_limits<double>::quiet_NaN(), 0.0);
417 return cplx(std::log(x), 0.0);
418 }
419 // sqrt
420 if (math && x < 0.0) throw LldEvalError("ValueError", "math domain error");
421 if (x < 0.0) return cplx(std::numeric_limits<double>::quiet_NaN(), 0.0);
422 return cplx(std::sqrt(x), 0.0);
423 }
424 if (math && base != "abs")
425 throw LldEvalError("TypeError", "must be real number, not complex");
426 if (base == "exp") return std::exp(z);
427 if (base == "sin") return std::sin(z);
428 if (base == "cos") return std::cos(z);
429 if (base == "tanh") return std::tanh(z);
430 if (base == "log") return std::log(z);
431 return std::sqrt(z);
432 }
433 if (base == "min" || base == "max") {
434 if (v.size() < 2)
435 throw LldEvalError("TypeError", base + " expected at least 2 arguments, got " +
436 std::to_string(v.size()));
437 for (const cplx& z : v)
438 if (!real(z))
439 throw LldEvalError("TypeError", std::string("'") + (base == "max" ? ">" : "<") +
440 "' not supported between instances of "
441 "'complex' and 'float'");
442 double r = v[0].real();
443 for (std::size_t i = 1; i < v.size(); ++i)
444 r = base == "max" ? (v[i].real() > r ? v[i].real() : r)
445 : (v[i].real() < r ? v[i].real() : r);
446 return cplx(r, 0.0);
447 }
448 if (prefix == "np") throw LldEvalError("AttributeError", "module 'numpy' has no attribute '" + base + "'");
449 if (prefix == "math") throw LldEvalError("AttributeError", "module 'math' has no attribute '" + base + "'");
450 throw LldEvalError("NameError", "name '" + base + "' is not defined");
451 }
452
453 cplx eval_node(int i, const cplx& n) const {
454 const Node& nd = nodes_[std::size_t(i)];
455 switch (nd.kind) {
456 case Node::NUM: return cplx(nd.num, 0.0);
457 case Node::VAR: return n;
458 case Node::ID: throw LldEvalError("NameError", "name '" + nd.name + "' is not defined");
459 case Node::NEG: return -eval_node(nd.kids[0], n);
460 case Node::POS: return eval_node(nd.kids[0], n);
461 case Node::ADD: return eval_node(nd.kids[0], n) + eval_node(nd.kids[1], n);
462 case Node::SUB: return eval_node(nd.kids[0], n) - eval_node(nd.kids[1], n);
463 case Node::MUL: {
464 const cplx a = eval_node(nd.kids[0], n), b = eval_node(nd.kids[1], n);
465 if (real(a) && real(b)) return cplx(a.real() * b.real(), 0.0);
466 return a * b;
467 }
468 case Node::DIV: {
469 const cplx a = eval_node(nd.kids[0], n), b = eval_node(nd.kids[1], n);
470 if (b == cplx(0.0, 0.0))
471 throw LldEvalError("ZeroDivisionError", real(a) ? "float division by zero"
472 : "complex division by zero");
473 if (real(a) && real(b)) return cplx(a.real() / b.real(), 0.0);
474 return a / b;
475 }
476 case Node::POW: return do_pow(eval_node(nd.kids[0], n), eval_node(nd.kids[1], n));
477 case Node::CALL: {
478 std::vector<cplx> args;
479 for (int k : nd.kids) args.push_back(eval_node(k, n));
480 return call(nd.name, args);
481 }
482 }
483 return cplx(0.0, 0.0);
484 }
485};
486
487inline double re(double x) { return x; }
488inline double re(const cplx& z) { return z.real(); }
489
490/** Smooth stand-in for min(a, b), verbatim python _SOFTMIN_SRC; real or complex. */
491template <class S>
492S softmin(const S& a, const S& b, double alpha) {
493 S lo = a, hi = b;
494 if (!(re(a) <= re(b))) {
495 lo = b;
496 hi = a;
497 }
498 const S gap = hi - lo;
499 if (re(gap) * alpha > 700.0) return lo;
500 using std::exp;
501 const S w = exp(-alpha * gap);
502 return lo + gap * w / (1.0 + w);
503}
504
505/**
506 * Why an lld expression cannot be used, or empty: python _check_lld verbatim.
507 * Real and positive at n = 1, 2.5, 10, and analytic, which a complex step
508 * compared against a central difference at n = 2.5 verifies.
509 */
510inline std::string check_lld(const std::string& src, std::shared_ptr<const LldExpr>& out) {
511 std::shared_ptr<const LldExpr> e;
512 try {
513 e = std::make_shared<const LldExpr>(src);
514 } catch (const LldSyntaxError& ex) {
515 return std::string("not a Python expression (") + ex.what() + ")";
516 }
517 const double pts[3] = {1.0, 2.5, 10.0};
518 for (double n : pts) {
519 cplx v;
520 try {
521 v = e->eval(cplx(n, 0.0));
522 } catch (const LldEvalError& ex) {
523 return "fails at n=" + fmt("%g", n) + " (" + ex.kind + ": " + ex.what() + ")";
524 }
525 if (v.imag() != 0.0 || !std::isfinite(v.real()) || v.real() <= 0.0)
526 return "must be real and positive, is " + py_repr(v) + " at n=" + fmt("%g", n);
527 }
528 const double n0 = 2.5, step = 1e-6;
529 double dcs = 0.0;
530 try {
531 dcs = e->eval(cplx(n0, 1e-20)).imag() / 1e-20;
532 } catch (const LldEvalError& ex) {
533 return std::string("rejects a complex argument, so it cannot be differentiated by ") +
534 "complex step (" + ex.kind + ": " + ex.what() + ")";
535 }
536 const double dfd = (e->eval(cplx(n0 + step, 0.0)).real() - e->eval(cplx(n0 - step, 0.0)).real()) /
537 (2.0 * step);
538 if (std::fabs(dcs - dfd) > 1e-4 * (1.0 + std::fabs(dfd)))
539 return "is not analytic at n=" + fmt("%g", n0) + ": its complex step gives " + fmt("%g", dcs) +
540 " where a finite difference gives " + fmt("%g", dfd) +
541 ". abs, min, max, a comparison or an interpolation will do this.";
542 out = e;
543 return "";
544}
545
546/** One-sided Jacobi SVD of A (m x n), returning the min-norm least-squares solution of A x = b. */
547inline std::vector<double> lstsq_min_norm(const Matrix<double>& A, const std::vector<double>& b) {
548 const std::size_t m = A.rows(), n = A.cols();
549 std::vector<double> x(n, 0.0);
550 if (m == 0 || n == 0) return x;
551 // column-major working copy of A, rotated until its columns are orthogonal
552 std::vector<std::vector<double>> W(n, std::vector<double>(m)), V(n, std::vector<double>(n, 0.0));
553 for (std::size_t j = 0; j < n; ++j) {
554 for (std::size_t i = 0; i < m; ++i) W[j][i] = A(i, j);
555 V[j][j] = 1.0;
556 }
557 const double eps = std::numeric_limits<double>::epsilon();
558 for (int sweep = 0; sweep < 100; ++sweep) {
559 bool rotated = false;
560 for (std::size_t p = 0; p + 1 < n; ++p) {
561 for (std::size_t q = p + 1; q < n; ++q) {
562 double alpha = 0.0, beta = 0.0, gamma = 0.0;
563 for (std::size_t i = 0; i < m; ++i) {
564 alpha += W[p][i] * W[p][i];
565 beta += W[q][i] * W[q][i];
566 gamma += W[p][i] * W[q][i];
567 }
568 if (gamma == 0.0 || std::fabs(gamma) <= eps * std::sqrt(alpha * beta)) continue;
569 rotated = true;
570 const double zeta = (beta - alpha) / (2.0 * gamma);
571 const double t = (zeta >= 0.0 ? 1.0 : -1.0) / (std::fabs(zeta) + std::sqrt(1.0 + zeta * zeta));
572 const double c = 1.0 / std::sqrt(1.0 + t * t), s = c * t;
573 for (std::size_t i = 0; i < m; ++i) {
574 const double wp = W[p][i], wq = W[q][i];
575 W[p][i] = c * wp - s * wq;
576 W[q][i] = s * wp + c * wq;
577 }
578 for (std::size_t i = 0; i < n; ++i) {
579 const double vp = V[p][i], vq = V[q][i];
580 V[p][i] = c * vp - s * vq;
581 V[q][i] = s * vp + c * vq;
582 }
583 }
584 }
585 if (!rotated) break;
586 }
587 std::vector<double> sig(n, 0.0);
588 double smax = 0.0;
589 for (std::size_t j = 0; j < n; ++j) {
590 double s2 = 0.0;
591 for (std::size_t i = 0; i < m; ++i) s2 += W[j][i] * W[j][i];
592 sig[j] = std::sqrt(s2);
593 smax = std::max(smax, sig[j]);
594 }
595 // numpy lstsq(rcond=None): singular values at or below eps*max(m,n)*smax count as zero
596 const double cut = eps * double(std::max(m, n)) * smax;
597 for (std::size_t j = 0; j < n; ++j) {
598 if (!(sig[j] > cut)) continue;
599 double ub = 0.0; // (w_j . b) / sigma_j^2 = (u_j . b) / sigma_j
600 for (std::size_t i = 0; i < m; ++i) ub += W[j][i] * b[i];
601 const double f = ub / (sig[j] * sig[j]);
602 for (std::size_t i = 0; i < n; ++i) x[i] += f * V[j][i];
603 }
604 return x;
605}
606
607inline double norm2(const std::vector<double>& v) {
608 double s = 0.0;
609 for (double x : v) s += x * x;
610 return std::sqrt(s);
611}
612
613} // namespace nlp_detail
614
615/**
616 * E[max] of K independent EXPONENTIAL branch completion times of means m, with
617 * the reciprocals cleared so a branch of zero mean contributes zero:
618 * sum over nonempty S of (-1)^(|S|+1) prod_{i in S} m_i / sum_{i in S} prod_{j in S, j != i} m_j.
619 */
620template <class S>
621S fj_max_exp(const std::vector<S>& m) {
622 const std::size_t k = m.size();
623 S tot = S(0.0);
624 for (std::size_t mask = 1; mask < (std::size_t(1) << k); ++mask) {
625 S num = S(1.0), den = S(0.0);
626 std::size_t card = 0;
627 for (std::size_t i = 0; i < k; ++i) {
628 if (!((mask >> i) & 1U)) continue;
629 ++card;
630 num = num * m[i];
631 S q = S(1.0);
632 for (std::size_t j = 0; j < k; ++j)
633 if (j != i && ((mask >> j) & 1U)) q = q * m[j];
634 den = den + q;
635 }
636 tot = tot + (card % 2 ? 1.0 : -1.0) * num / den;
637 }
638 return tot;
639}
640
641/** Gradient of fj_max_exp by complex step, one coordinate at a time. */
642inline std::vector<double> fj_max_exp_grad(const std::vector<double>& u) {
643 std::vector<double> g(u.size(), 0.0);
644 for (std::size_t j = 0; j < u.size(); ++j) {
645 std::vector<nlp_detail::cplx> z(u.begin(), u.end());
646 z[j] += nlp_detail::cplx(0.0, 1e-20);
647 g[j] = fj_max_exp(z).imag() / 1e-20;
648 }
649 return g;
650}
651
652/** The queue-dependent rate of one station, r(n) = softmin(n, m) [* lld(n)], or n at a delay. */
653struct NlpRate {
654 bool delay = false;
655 double mult = 1.0;
656 double alpha = 20.0;
657 std::shared_ptr<const nlp_detail::LldExpr> lld;
658
659 double operator()(double n) const {
660 if (delay) return n;
661 double s = nlp_detail::softmin<double>(n, mult, alpha);
662 if (lld) s = s * lld->eval(nlp_detail::cplx(n, 0.0)).real();
663 return s;
664 }
666 if (delay) return n;
668 if (lld) s = s * lld->eval(n);
669 return s;
670 }
671 /** Derivative by complex step: imag(f(n + 1e-20 i)) / 1e-20. */
672 double deriv(double n) const { return (*this)(nlp_detail::cplx(n, 1e-20)).imag() / 1e-20; }
673};
674
675/** What NlpProgram::solve returns: python's Result. */
677 std::vector<double> x;
678 std::string message;
679 int niter = 0; ///< fallback iterations; 0 when the Newton ladder rooted it
680 bool newton = true; ///< the Newton ladder reached a feasible root
681};
682
683/**
684 * The program of one layered model. Every member is named after the python
685 * script's global of the same meaning (lower case); indices are 0-based in
686 * declaration order, so element i of a block is the LQN's absolute index
687 * shift + i + 1.
688 */
690public:
691 enum Block { XE = 0, XA, XC, W, S, R, Q, QT, BC, BR, GH, GT, SA, NBLOCKS };
692
693 // ---- model parameters ------------------------------------------------
694 std::string name;
695 std::vector<std::string> hosts, tasks, entries, acts, calls;
696 std::vector<double> host_mult, host_maxmult;
697 std::vector<bool> host_inf;
698 std::vector<std::size_t> task_host;
699 std::vector<double> task_mult, task_think;
700 std::vector<bool> task_inf, task_isref;
701 std::vector<std::size_t> entry_task, entry_order;
702 std::vector<std::size_t> act_task, act_entry;
703 std::vector<double> act_demand, act_visits;
704 std::vector<std::size_t> call_act, call_entry, call_caller_task;
705 std::vector<double> call_mean;
706 std::vector<std::size_t> fork_entry;
707 std::vector<std::vector<std::vector<std::size_t>>> fork_branches;
708 std::vector<std::string> fork_label;
709 double alpha = 20.0;
710 std::vector<std::string> host_lld, task_lld;
711 std::vector<bool> host_rate_capped;
712 std::vector<NlpRate> host_rate, task_rate;
713
714 // ---- derived index sets ----------------------------------------------
715 std::size_t NH = 0, NT = 0, NE = 0, NA = 0, NC = 0, NFORK = 0, NR = 0, NGH = 0, NGT = 0;
716 std::vector<std::vector<std::size_t>> acts_of_host, acts_of_entry, calls_of_act,
719 std::set<std::size_t> fork_acts;
720 std::vector<std::size_t> ref_tasks, ref_entry, qd_hosts, qd_tasks;
721 std::map<std::size_t, std::size_t> gh_of, gt_of;
722 std::vector<bool> task_delay, host_delay;
724
725 // ---- layout ----------------------------------------------------------
726 std::size_t offset[NBLOCKS] = {0};
727 std::size_t blocksize[NBLOCKS] = {0};
728 std::size_t NVAR = 0, NRES = 0;
729
730 // ---- linear block ----------------------------------------------------
732 std::vector<double> BEQ, BUB;
733
734 // ---- residuals: const + lin.x + bilinear + rate terms + fork terms ----
735 struct Bil { std::size_t k, i, j; double c; };
736 struct Nlt { std::size_t k; const NlpRate* f; double s, c0; std::vector<double> a; };
737 struct Mnl { std::size_t k; double s; std::vector<double> c; Matrix<double> a; };
739 std::vector<double> res_const;
740 std::vector<Bil> bil;
741 std::vector<Nlt> nlt;
742 std::vector<Mnl> mnl;
743
745 NlpProgram(const NlpProgram& o) { *this = o; }
747 if (this == &o) return *this;
748 copy_fields(o);
749 // the rate terms point into this program's own rate tables
750 rebind_rates(o);
751 return *this;
752 }
753
754 std::size_t col(Block b, std::size_t i) const { return offset[b] + i; }
755 std::size_t sent(std::size_t e) const { return NFORK ? col(SA, e) : col(S, e); }
756
757 /** The block's slice of an iterate. */
758 std::vector<double> block(const std::vector<double>& x, Block b) const {
759 return std::vector<double>(x.begin() + long(offset[b]), x.begin() + long(offset[b] + blocksize[b]));
760 }
761
762 std::vector<std::string> var_names() const {
763 static const char* tag[NBLOCKS] = {"Xe", "Xa", "Xc", "W", "S", "R", "Q", "Qt", "BC", "BR", "GH", "GT", "SA"};
764 std::vector<std::string> out;
765 for (int b = 0; b < NBLOCKS; ++b) {
766 for (std::size_t i = 0; i < blocksize[b]; ++i) {
767 std::string lab;
768 switch (Block(b)) {
769 case XE: case S: case SA: lab = entries[i]; break;
770 case XA: case W: case Q: lab = acts[i]; break;
771 case XC: case R: case QT: case BC: lab = calls[i]; break;
772 case BR: lab = tasks[ref_tasks[i]]; break;
773 case GH: lab = hosts[qd_hosts[i]]; break;
774 case GT: lab = tasks[qd_tasks[i]]; break;
775 default: break;
776 }
777 out.push_back(std::string(tag[b]) + "[" + lab + "]");
778 }
779 }
780 return out;
781 }
782
783 // ---- QD-AMVA arrival-instant estimate --------------------------------
784 std::vector<std::pair<std::size_t, double>> qbar_host_terms(std::size_t h, std::size_t a, double g) const {
785 std::vector<std::pair<std::size_t, double>> out;
786 for (std::size_t ap : acts_of_host[h]) out.push_back({col(Q, ap), g});
787 const double pop = task_pop[act_task[a]];
788 if (!std::isinf(pop) && pop > 0)
789 for (std::size_t ap : sibling_acts[a]) out.push_back({col(Q, ap), -g / pop});
790 return out;
791 }
792 std::vector<std::pair<std::size_t, double>> qbar_task_terms(std::size_t t, std::size_t c, double g) const {
793 std::vector<std::pair<std::size_t, double>> out;
794 for (std::size_t cp : calls_into_task[t]) out.push_back({col(QT, cp), g});
795 const double pop = task_pop[call_caller_task[c]];
796 if (!std::isinf(pop) && pop > 0)
797 for (std::size_t cp : sibling_calls[c]) out.push_back({col(QT, cp), -g / pop});
798 return out;
799 }
800 double qd_pop_host(const std::vector<double>& Qv, std::size_t h) const {
801 double s = 0.0;
802 for (std::size_t a : acts_of_host[h]) s += Qv[a];
803 return 1.0 + host_delta[h] * s;
804 }
805 double qd_pop_task(const std::vector<double>& Qtv, std::size_t t) const {
806 double s = 0.0;
807 for (std::size_t c : calls_into_task[t]) s += Qtv[c];
808 return 1.0 + task_delta[t] * s;
809 }
810 double qbar_host(const std::vector<double>& Qv, std::size_t h, std::size_t a) const {
811 const double pop = task_pop[act_task[a]];
812 double tot = 0.0, own = 0.0;
813 for (std::size_t ap : acts_of_host[h]) tot += Qv[ap];
814 for (std::size_t ap : sibling_acts[a]) own += Qv[ap];
815 return tot - ((!std::isinf(pop) && pop > 0) ? own / pop : 0.0);
816 }
817 double qbar_task(const std::vector<double>& Qtv, std::size_t t, std::size_t c) const {
818 const double pop = task_pop[call_caller_task[c]];
819 double tot = 0.0, own = 0.0;
820 for (std::size_t cp : calls_into_task[t]) tot += Qtv[cp];
821 for (std::size_t cp : sibling_calls[c]) own += Qtv[cp];
822 return tot - ((!std::isinf(pop) && pop > 0) ? own / pop : 0.0);
823 }
824
825 // ---- evaluation ------------------------------------------------------
826 std::vector<double> residuals(const std::vector<double>& x) const {
827 std::vector<double> r(NRES, 0.0);
828 for (std::size_t k = 0; k < NRES; ++k) {
829 double s = res_const[k];
830 for (std::size_t j = 0; j < NVAR; ++j) s += res_lin(k, j) * x[j];
831 r[k] = s;
832 }
833 for (const Bil& b : bil) r[b.k] += b.c * x[b.i] * x[b.j];
834 for (const Nlt& t : nlt) r[t.k] += t.s * (*t.f)(t.c0 + dot(t.a, x));
835 for (const Mnl& t : mnl) r[t.k] += t.s * fj_max_exp(mnl_args(t, x));
836 return r;
837 }
838
839 Matrix<double> jacobian(const std::vector<double>& x) const {
841 for (const Bil& b : bil) {
842 J(b.k, b.i) += b.c * x[b.j];
843 J(b.k, b.j) += b.c * x[b.i];
844 }
845 for (const Nlt& t : nlt) {
846 const double d = t.s * t.f->deriv(t.c0 + dot(t.a, x));
847 for (std::size_t j = 0; j < NVAR; ++j) J(t.k, j) += d * t.a[j];
848 }
849 for (const Mnl& t : mnl) {
850 const std::vector<double> g = fj_max_exp_grad(mnl_args(t, x));
851 for (std::size_t j = 0; j < NVAR; ++j) {
852 double s = 0.0;
853 for (std::size_t i = 0; i < g.size(); ++i) s += g[i] * t.a(i, j);
854 J(t.k, j) += t.s * s;
855 }
856 }
857 return J;
858 }
859
860 double objective(const std::vector<double>& x) const {
861 const std::vector<double> r = residuals(x);
862 double s = 0.0;
863 for (double v : r) s += v * v;
864 return s;
865 }
866
867 std::vector<double> aeq_resid(const std::vector<double>& x) const { return affine(AEQ, BEQ, x); }
868 std::vector<double> aub_resid(const std::vector<double>& x) const { return affine(AUB, BUB, x); }
869
870 // ---- starting point --------------------------------------------------
871 double compose(const std::vector<std::size_t>& as, const std::vector<double>& Wv,
872 const std::vector<double>& Rv) const {
873 double s = 0.0;
874 for (std::size_t a : as) {
875 s += act_visits[a] * Wv[a];
876 for (std::size_t c : calls_of_act[a]) s += act_visits[a] * call_mean[c] * Rv[c];
877 }
878 return s;
879 }
880
881 struct Sweep { std::vector<double> W, S, SA, R, Xe, Xa, Xc; };
882
883 /** One pass of the relations at the given queue lengths: python _sweep. */
884 Sweep sweep(const std::vector<double>& Qv, const std::vector<double>& Qtv) const {
885 Sweep o;
886 o.W.assign(NA, 0.0);
887 for (std::size_t h = 0; h < NH; ++h)
888 for (std::size_t a : acts_of_host[h])
889 o.W[a] = host_delay[h] ? act_demand[a]
890 : act_demand[a] * (1.0 + qbar_host(Qv, h, a)) / host_rate[h](qd_pop_host(Qv, h));
891 o.S.assign(NE, 0.0);
892 o.SA.assign(NE, 0.0);
893 o.R.assign(NC, 0.0);
894 for (std::size_t e : entry_order) {
895 o.SA[e] = compose(acts_of_entry[e], o.W, o.R);
896 if (!blocks_of_entry[e].empty()) {
897 double s = compose(seq_acts_of_entry[e], o.W, o.R);
898 for (std::size_t b : blocks_of_entry[e]) {
899 std::vector<double> m;
900 for (const std::vector<std::size_t>& br : fork_branches[b]) m.push_back(compose(br, o.W, o.R));
901 s += fj_max_exp(m);
902 }
903 o.S[e] = s;
904 } else {
905 o.S[e] = o.SA[e];
906 }
907 const std::size_t t = entry_task[e];
908 for (std::size_t c : calls_into_entry[e])
909 o.R[c] = task_delay[t] ? o.S[e]
910 : o.S[e] * (1.0 + qbar_task(Qtv, t, c)) / task_rate[t](qd_pop_task(Qtv, t));
911 }
912 o.Xe.assign(NE, 0.0);
913 o.Xa.assign(NA, 0.0);
914 o.Xc.assign(NC, 0.0);
915 for (std::size_t ri = 0; ri < NR; ++ri) {
916 const std::size_t t = ref_tasks[ri], e = ref_entry[ri];
917 const double cyc = task_think[t] + o.S[e];
918 o.Xe[e] = cyc > 0 ? task_mult[t] / cyc : 0.0;
919 }
920 for (std::size_t q = entry_order.size(); q-- > 0;) {
921 const std::size_t e = entry_order[q];
922 if (!task_isref[entry_task[e]]) {
923 double s = 0.0;
924 for (std::size_t c : calls_into_entry[e]) s += o.Xc[c];
925 o.Xe[e] = s;
926 }
927 for (std::size_t a : acts_of_entry[e]) {
928 o.Xa[a] = act_visits[a] * o.Xe[e];
929 for (std::size_t c : calls_of_act[a]) o.Xc[c] = call_mean[c] * o.Xa[a];
930 }
931 }
932 return o;
933 }
934
935 /** A point satisfying every linear relation, after WARM fixed-point sweeps. */
936 std::vector<double> initial_point(int warm = 0) const {
937 std::vector<double> Qv(NA, 0.0), Qtv(NC, 0.0);
938 for (int it = 0; it < std::max(0, warm) + 1; ++it) {
939 const Sweep s = sweep(Qv, Qtv);
940 for (std::size_t a = 0; a < NA; ++a) Qv[a] = s.Xa[a] * s.W[a];
941 for (std::size_t c = 0; c < NC; ++c) Qtv[c] = s.Xc[c] * s.R[c];
942 }
943 const Sweep s = sweep(Qv, Qtv);
944 std::vector<double> x(NVAR, 0.0);
945 put(x, XE, s.Xe);
946 put(x, XA, s.Xa);
947 put(x, XC, s.Xc);
948 put(x, W, s.W);
949 put(x, S, s.S);
950 if (NFORK) put(x, SA, s.SA);
951 put(x, R, s.R);
952 if (warm) {
953 put(x, Q, Qv);
954 put(x, QT, Qtv);
955 }
956 for (std::size_t c = 0; c < NC; ++c) x[col(BC, c)] = s.Xc[c] * s.SA[call_entry[c]];
957 for (std::size_t ri = 0; ri < NR; ++ri) {
958 const std::size_t e = ref_entry[ri];
959 x[col(BR, ri)] = s.Xe[e] * s.S[e];
960 }
961 const std::vector<double> Qs = block(x, Q), Qts = block(x, QT);
962 for (std::size_t h : qd_hosts) x[col(GH, gh_of.at(h))] = host_rate[h](qd_pop_host(Qs, h));
963 for (std::size_t t : qd_tasks) x[col(GT, gt_of.at(t))] = task_rate[t](qd_pop_task(Qts, t));
964 return x;
965 }
966
967 // ---- solve -----------------------------------------------------------
968 /** Newton on the square system [residuals; AEQ x - BEQ], min-norm step: python polish. */
969 std::vector<double> polish(std::vector<double> x, int iters = 50) const {
970 std::vector<double> best = x;
971 double bobj = objective(x);
972 const double scale = 1.0 + maxabs(x);
973 for (int it = 0; it < iters; ++it) {
974 const std::vector<double> r = residuals(x), e = aeq_resid(x);
975 const Matrix<double> J = jacobian(x);
976 Matrix<double> JF(NRES + AEQ.rows(), NVAR, 0.0);
977 std::vector<double> F(NRES + AEQ.rows(), 0.0);
978 for (std::size_t k = 0; k < NRES; ++k) {
979 F[k] = -r[k];
980 for (std::size_t j = 0; j < NVAR; ++j) JF(k, j) = J(k, j);
981 }
982 for (std::size_t k = 0; k < AEQ.rows(); ++k) {
983 F[NRES + k] = -e[k];
984 for (std::size_t j = 0; j < NVAR; ++j) JF(NRES + k, j) = AEQ(k, j);
985 }
986 const std::vector<double> dx = nlp_detail::lstsq_min_norm(JF, F);
987 for (std::size_t j = 0; j < NVAR; ++j) x[j] += dx[j];
988 if (minval(x) < -1e-6 * scale) break;
989 if (AUB.rows() && maxval(aub_resid(x)) > 1e-6 * scale) break;
990 const double obj = objective(x);
991 if (obj < bobj) {
992 best = x;
993 bobj = obj;
994 }
995 if (nlp_detail::norm2(dx) <= 1e-14 * (1.0 + nlp_detail::norm2(x))) break;
996 }
997 for (double& v : best) v = std::max(v, 0.0);
998 return best;
999 }
1000
1001 /** A root of the square system, feasible to the bounds and the inequalities. */
1002 bool solved(const std::vector<double>& x) const {
1003 const double scale = 1.0 + (x.empty() ? 0.0 : maxabs(x));
1004 if (!x.empty() && minval(x) < -1e-9 * scale) return false;
1005 if (AUB.rows() && maxval(aub_resid(x)) > 1e-7 * scale) return false;
1006 if (AEQ.rows()) {
1007 const std::vector<double> e = aeq_resid(x);
1008 if (maxabs(e) > 1e-9 * scale) return false;
1009 }
1010 return NRES ? maxabs(residuals(x)) <= 1e-10 * scale : true;
1011 }
1012
1013 /** The merit the fallback descends: ||r||^2 + ||AEQ x - BEQ||^2 + ||max(0, AUB x - BUB)||^2. */
1014 double merit(const std::vector<double>& x) const {
1015 double s = objective(x);
1016 for (double v : aeq_resid(x)) s += v * v;
1017 for (double v : aub_resid(x))
1018 if (v > 0) s += v * v;
1019 return s;
1020 }
1021
1022 /**
1023 * The projected Levenberg-Marquardt fallback, shared by MATLAB, the JAR and
1024 * C++ in place of python's trust-constr. `merits`, when given, receives the
1025 * merit after each ACCEPTED step, which is how a test sees it descend.
1026 */
1027 std::vector<double> lm_fallback(std::vector<double> x, int maxiter, int* iters = nullptr,
1028 std::vector<double>* merits = nullptr) const {
1029 for (double& v : x) v = std::max(v, 0.0);
1030 double phi = merit(x);
1031 double mu = -1.0;
1032 int it = 0;
1033 while (it < maxiter && phi > 1e-30) {
1034 const std::vector<double> r = residuals(x), e = aeq_resid(x), u = aub_resid(x);
1035 const Matrix<double> J = jacobian(x);
1036 std::vector<std::vector<double>> rows;
1037 std::vector<double> G;
1038 for (std::size_t k = 0; k < NRES; ++k) {
1039 rows.push_back(std::vector<double>(&J(k, 0), &J(k, 0) + NVAR));
1040 G.push_back(r[k]);
1041 }
1042 for (std::size_t k = 0; k < AEQ.rows(); ++k) {
1043 rows.push_back(std::vector<double>(&AEQ(k, 0), &AEQ(k, 0) + NVAR));
1044 G.push_back(e[k]);
1045 }
1046 for (std::size_t k = 0; k < AUB.rows(); ++k)
1047 if (u[k] > 0) {
1048 rows.push_back(std::vector<double>(&AUB(k, 0), &AUB(k, 0) + NVAR));
1049 G.push_back(u[k]);
1050 }
1051 Matrix<double> JtJ(NVAR, NVAR, 0.0);
1052 std::vector<double> JtG(NVAR, 0.0);
1053 for (std::size_t q = 0; q < rows.size(); ++q)
1054 for (std::size_t i = 0; i < NVAR; ++i) {
1055 if (rows[q][i] == 0.0) continue;
1056 JtG[i] += rows[q][i] * G[q];
1057 for (std::size_t j = 0; j < NVAR; ++j) JtJ(i, j) += rows[q][i] * rows[q][j];
1058 }
1059 if (mu < 0) {
1060 double dmax = 0.0;
1061 for (std::size_t i = 0; i < NVAR; ++i) dmax = std::max(dmax, JtJ(i, i));
1062 mu = 1e-3 * dmax;
1063 }
1064 bool stop = false;
1065 while (it < maxiter) {
1066 ++it;
1067 Matrix<double> M = JtJ;
1068 for (std::size_t i = 0; i < NVAR; ++i) M(i, i) += mu;
1069 std::vector<double> rhs(NVAR);
1070 for (std::size_t i = 0; i < NVAR; ++i) rhs[i] = -JtG[i];
1071 const std::vector<double> d = line::solve(M, rhs);
1072 std::vector<double> xn(NVAR);
1073 for (std::size_t i = 0; i < NVAR; ++i) xn[i] = std::max(x[i] + d[i], 0.0);
1074 const double pn = merit(xn);
1075 if (nlp_detail::norm2(d) <= 1e-14 * (1.0 + nlp_detail::norm2(x))) stop = true;
1076 if (pn < phi) {
1077 x = xn;
1078 phi = pn;
1079 mu /= 3.0;
1080 if (merits) merits->push_back(phi);
1081 break;
1082 }
1083 mu *= 2.0;
1084 if (stop) break;
1085 }
1086 if (stop) break;
1087 }
1088 if (iters) *iters = it;
1089 return x;
1090 }
1091
1092 /**
1093 * Root the program: the Newton ladder over warm starts max(0,warm), 5, 20,
1094 * 100, then the fallback from the ladder start of smallest objective.
1095 */
1096 NlpSolveResult solve(int warm = 0, int maxiter = 3000) const {
1097 std::vector<int> ladder{std::max(0, warm)};
1098 for (int w : {5, 20, 100})
1099 if (w > warm) ladder.push_back(w);
1100 std::vector<double> best_x0;
1101 double best_obj = std::numeric_limits<double>::infinity();
1102 for (int w : ladder) {
1103 const std::vector<double> x0 = initial_point(w);
1104 const std::vector<double> x = polish(x0);
1105 if (solved(x)) {
1106 NlpSolveResult res;
1107 res.x = x;
1108 res.message = "Newton polish reached a feasible root from " + std::to_string(w) +
1109 " warm sweep" + (w == 1 ? "" : "s") + ".";
1110 return res;
1111 }
1112 const double obj = objective(x0);
1113 if (obj < best_obj) {
1114 best_x0 = x0;
1115 best_obj = obj;
1116 }
1117 }
1118 if (best_x0.empty()) best_x0 = initial_point(ladder.front());
1119 NlpSolveResult res;
1120 res.newton = false;
1121 res.x = polish(lm_fallback(best_x0, maxiter, &res.niter));
1122 res.message = "projected Levenberg-Marquardt fallback, " + std::to_string(res.niter) +
1123 " iterations, then Newton polish; merit " + nlp_detail::fmt("%.3e", merit(res.x)) + ".";
1124 return res;
1125 }
1126
1127 // ---- helpers ---------------------------------------------------------
1128 static double dot(const std::vector<double>& a, const std::vector<double>& x) {
1129 double s = 0.0;
1130 for (std::size_t j = 0; j < a.size(); ++j) s += a[j] * x[j];
1131 return s;
1132 }
1133 static double maxabs(const std::vector<double>& v) {
1134 double m = 0.0;
1135 for (double x : v) m = std::max(m, std::fabs(x));
1136 return m;
1137 }
1138 static double minval(const std::vector<double>& v) {
1139 double m = std::numeric_limits<double>::infinity();
1140 for (double x : v) m = std::min(m, x);
1141 return m;
1142 }
1143 static double maxval(const std::vector<double>& v) {
1144 double m = -std::numeric_limits<double>::infinity();
1145 for (double x : v) m = std::max(m, x);
1146 return m;
1147 }
1148
1149private:
1150 std::vector<double> mnl_args(const Mnl& t, const std::vector<double>& x) const {
1151 std::vector<double> u(t.c);
1152 for (std::size_t i = 0; i < u.size(); ++i)
1153 for (std::size_t j = 0; j < NVAR; ++j) u[i] += t.a(i, j) * x[j];
1154 return u;
1155 }
1156 std::vector<double> affine(const Matrix<double>& A, const std::vector<double>& b,
1157 const std::vector<double>& x) const {
1158 std::vector<double> out(A.rows(), 0.0);
1159 for (std::size_t k = 0; k < A.rows(); ++k) {
1160 double s = 0.0;
1161 for (std::size_t j = 0; j < NVAR; ++j) s += A(k, j) * x[j];
1162 out[k] = s - b[k];
1163 }
1164 return out;
1165 }
1166 void put(std::vector<double>& x, Block b, const std::vector<double>& v) const {
1167 for (std::size_t i = 0; i < blocksize[b]; ++i) x[offset[b] + i] = v[i];
1168 }
1169 void copy_fields(const NlpProgram& o);
1170 void rebind_rates(const NlpProgram& o) {
1171 for (Nlt& t : nlt) {
1172 for (std::size_t h = 0; h < o.host_rate.size(); ++h)
1173 if (t.f == &o.host_rate[h]) t.f = &host_rate[h];
1174 for (std::size_t q = 0; q < o.task_rate.size(); ++q)
1175 if (t.f == &o.task_rate[q]) t.f = &task_rate[q];
1176 }
1177 }
1178};
1179
1180inline void NlpProgram::copy_fields(const NlpProgram& o) {
1181 name = o.name; hosts = o.hosts; tasks = o.tasks; entries = o.entries; acts = o.acts; calls = o.calls;
1182 host_mult = o.host_mult; host_maxmult = o.host_maxmult; host_inf = o.host_inf;
1183 task_host = o.task_host; task_mult = o.task_mult; task_think = o.task_think;
1184 task_inf = o.task_inf; task_isref = o.task_isref; entry_task = o.entry_task;
1185 entry_order = o.entry_order; act_task = o.act_task; act_entry = o.act_entry;
1186 act_demand = o.act_demand; act_visits = o.act_visits; call_act = o.call_act;
1187 call_entry = o.call_entry; call_caller_task = o.call_caller_task; call_mean = o.call_mean;
1188 fork_entry = o.fork_entry; fork_branches = o.fork_branches; fork_label = o.fork_label;
1189 alpha = o.alpha; host_lld = o.host_lld; task_lld = o.task_lld;
1190 host_rate_capped = o.host_rate_capped; host_rate = o.host_rate; task_rate = o.task_rate;
1191 NH = o.NH; NT = o.NT; NE = o.NE; NA = o.NA; NC = o.NC; NFORK = o.NFORK; NR = o.NR;
1192 NGH = o.NGH; NGT = o.NGT;
1193 acts_of_host = o.acts_of_host; acts_of_entry = o.acts_of_entry; calls_of_act = o.calls_of_act;
1194 calls_into_entry = o.calls_into_entry; calls_into_task = o.calls_into_task;
1195 entries_of_task = o.entries_of_task; blocks_of_entry = o.blocks_of_entry;
1196 seq_acts_of_entry = o.seq_acts_of_entry; sibling_acts = o.sibling_acts;
1197 sibling_calls = o.sibling_calls; fork_acts = o.fork_acts; ref_tasks = o.ref_tasks;
1198 ref_entry = o.ref_entry; qd_hosts = o.qd_hosts; qd_tasks = o.qd_tasks; gh_of = o.gh_of;
1199 gt_of = o.gt_of; task_delay = o.task_delay; host_delay = o.host_delay; task_pop = o.task_pop;
1200 host_ntot = o.host_ntot; task_ntot = o.task_ntot; host_delta = o.host_delta;
1201 task_delta = o.task_delta;
1202 for (int b = 0; b < NBLOCKS; ++b) {
1203 offset[b] = o.offset[b];
1204 blocksize[b] = o.blocksize[b];
1205 }
1206 NVAR = o.NVAR; NRES = o.NRES; AEQ = o.AEQ; AUB = o.AUB; BEQ = o.BEQ; BUB = o.BUB;
1207 res_lin = o.res_lin; res_const = o.res_const; bil = o.bil; nlt = o.nlt; mnl = o.mnl;
1208}
1209
1210namespace nlp_detail {
1211
1212inline UnsupportedError nlp_error(const std::string& msg) {
1213 return UnsupportedError(ErrorKind{"NlpExport"}, msg);
1214}
1215
1216template <class T>
1217std::string elem(const LqnStruct<T>& lqn, std::size_t idx) {
1218 if (idx < lqn.names.size() && !lqn.names[idx].empty()) return lqn.names[idx];
1219 return "#" + std::to_string(idx);
1220}
1221
1222template <class T>
1223std::string callnm(const LqnStruct<T>& lqn, std::size_t c) {
1224 if (c < lqn.callnames.size() && !lqn.callnames[c].empty()) return lqn.callnames[c];
1225 return "C" + std::to_string(c);
1226}
1227
1228template <class T>
1229double hostdem_mean(const LqnStruct<T>& lqn, std::size_t a) {
1230 if (a >= lqn.hostdem.size() || lqn.hostdem[a].disabled) return 0.0;
1231 const double d = num_traits<T>::to_double(lqn.hostdem[a].mean);
1232 return std::isnan(d) ? 0.0 : d;
1233}
1234
1235/** One AND-fork/join block, in absolute LQN indices. */
1237 std::size_t entry = 0, join = 0;
1238 std::vector<std::size_t> tails;
1239 std::vector<std::vector<std::size_t>> branches;
1240 std::string label;
1241};
1242
1243template <class T>
1244bool is_post_and(const LqnStruct<T>& lqn, std::size_t a) {
1245 return a < lqn.actposttype.size() && lqn.actposttype[a] == lang::PrecedenceType::POST_AND;
1246}
1247template <class T>
1248bool is_pre_and(const LqnStruct<T>& lqn, std::size_t a) {
1249 return a < lqn.actpretype.size() && lqn.actpretype[a] == lang::PrecedenceType::PRE_AND;
1250}
1251template <class T>
1252std::size_t quorum_of(const LqnStruct<T>& lqn, std::size_t a) {
1253 return a < lqn.actquorum.size() ? lqn.actquorum[a] : 0;
1254}
1255
1256/** python _read_block: one candidate block, or the reason it is not one. */
1257template <class T>
1258std::string read_block(const LqnStruct<T>& lqn, const std::vector<std::size_t>& A,
1259 const std::vector<std::size_t>& H, const std::vector<std::size_t>& srcs,
1260 const std::map<std::size_t, std::vector<std::size_t>>& succ,
1261 const std::map<std::size_t, std::vector<std::size_t>>& prd, ForkBlock& blk) {
1262 auto nm = [&](std::size_t a) { return elem(lqn, a); };
1263 auto reach = [&](std::size_t start) {
1264 std::set<std::size_t> seen;
1265 std::vector<std::size_t> stack{start};
1266 while (!stack.empty()) {
1267 const std::size_t x = stack.back();
1268 stack.pop_back();
1269 if (seen.count(x)) continue;
1270 seen.insert(x);
1271 for (std::size_t y : succ.at(x)) stack.push_back(y);
1272 }
1273 return seen;
1274 };
1275 std::vector<std::size_t> Hs = H;
1276 std::sort(Hs.begin(), Hs.end());
1277 std::string lbl;
1278 for (std::size_t i = 0; i < Hs.size(); ++i) lbl += (i ? "+" : "") + nm(Hs[i]);
1279 if (H.size() < 2) return "AND-fork at " + lbl + " has one branch";
1280
1281 std::vector<std::set<std::size_t>> Rs;
1282 for (std::size_t h : H) Rs.push_back(reach(h));
1283 std::set<std::size_t> common = Rs[0];
1284 for (std::size_t i = 1; i < Rs.size(); ++i) {
1285 std::set<std::size_t> c;
1286 for (std::size_t x : common)
1287 if (Rs[i].count(x)) c.insert(x);
1288 common = c;
1289 }
1290 if (common.empty())
1291 return "the AND-fork branches at " + lbl + " never reconverge, so the entry has no single completion";
1292 std::vector<std::size_t> cands;
1293 for (std::size_t x : common) {
1294 if (quorum_of(lqn, x) == 0) continue;
1295 const std::set<std::size_t> rx = reach(x);
1296 bool sub = true;
1297 for (std::size_t y : common)
1298 if (!rx.count(y)) sub = false;
1299 if (sub) cands.push_back(x);
1300 }
1301 if (cands.size() != 1) return "the AND-fork branches at " + lbl + " do not meet at one join target";
1302 const std::size_t j = cands[0];
1303
1304 std::vector<std::size_t> tails;
1305 const std::vector<std::size_t>& pj = prd.at(j);
1306 for (std::size_t a : A)
1307 if (is_pre_and(lqn, a) && std::find(pj.begin(), pj.end(), a) != pj.end()) tails.push_back(a);
1308 if (tails.size() != H.size())
1309 return "the join " + nm(j) + " takes " + std::to_string(tails.size()) +
1310 " AND predecessors where the fork at " + lbl + " opens " + std::to_string(H.size()) +
1311 " branches";
1312 const std::size_t q = quorum_of(lqn, j);
1313 if (q != H.size())
1314 return "quorum join at " + nm(j) + " fires on " + std::to_string(q) + " of " +
1315 std::to_string(H.size()) + " branches; the k-th order statistic is a different closure";
1316
1317 std::vector<std::vector<std::size_t>> branches;
1318 for (const std::set<std::size_t>& r : Rs) {
1319 std::vector<std::size_t> br;
1320 for (std::size_t x : r)
1321 if (!common.count(x)) br.push_back(x);
1322 branches.push_back(br);
1323 }
1324 std::set<std::size_t> seen;
1325 for (std::size_t i = 0; i < branches.size(); ++i) {
1326 if (branches[i].empty()) return "branch " + nm(H[i]) + " of the AND-fork at " + lbl + " is empty";
1327 for (std::size_t a : branches[i])
1328 if (seen.count(a)) return "the AND-fork branches at " + lbl + " overlap, so they are not independent";
1329 for (std::size_t a : branches[i]) seen.insert(a);
1330 }
1331 for (std::size_t i = 0; i < branches.size(); ++i) {
1332 const std::vector<std::size_t>& br = branches[i];
1333 for (std::size_t a : br) {
1334 for (std::size_t b : prd.at(a))
1335 if (std::find(br.begin(), br.end(), b) == br.end() &&
1336 std::find(srcs.begin(), srcs.end(), b) == srcs.end())
1337 return "activity " + nm(a) + " is inside a branch of the AND-fork at " + lbl +
1338 " but is also entered from " + nm(b) + ", so the branch is not a block";
1339 if (is_post_and(lqn, a) && a != H[i])
1340 return "AND-fork at " + nm(a) + " nested inside a branch of the one at " + lbl;
1341 if (is_pre_and(lqn, a) && std::find(tails.begin(), tails.end(), a) == tails.end())
1342 return "AND-join at " + nm(a) + " nested inside a branch of the fork at " + lbl;
1343 }
1344 bool work = false;
1345 for (std::size_t a : br) {
1346 if (hostdem_mean(lqn, a) > 0) work = true;
1347 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
1348 if (lqn.callpair_src[c] == a) work = true;
1349 }
1350 if (!work)
1351 return "branch " + nm(H[i]) + " of the AND-fork at " + lbl +
1352 " has no host demand and no call, so its completion time is identically zero";
1353 }
1354 blk.join = j;
1355 blk.tails = tails;
1356 blk.branches = branches;
1357 return "";
1358}
1359
1360/** python _fork_blocks: the well-formed blocks, and every AND marking outside one. */
1361template <class T>
1362std::vector<ForkBlock> fork_blocks(const LqnStruct<T>& lqn, const std::string& fork,
1363 std::vector<std::string>& bad) {
1364 std::vector<ForkBlock> blocks;
1365 const std::size_t a0 = lqn.ashift + 1, a1 = lqn.ashift + lqn.nacts;
1366 std::vector<std::size_t> marked;
1367 for (std::size_t a = a0; a <= a1; ++a)
1368 if (is_post_and(lqn, a) || is_pre_and(lqn, a)) marked.push_back(a);
1369 if (marked.empty()) return blocks;
1370 if (fork != "exp") {
1371 for (std::size_t a : marked)
1372 bad.push_back("AND-fork or AND-join at activity " + elem(lqn, a) +
1373 " (pass fork='exp' to carry it under the exponential-branch closure)");
1374 return blocks;
1375 }
1376 const auto G = [&](std::size_t i, std::size_t j) {
1377 return i <= lqn.graph.n && j <= lqn.graph.n && num_traits<T>::to_double(lqn.graph.get(i, j)) != 0.0;
1378 };
1379 std::set<std::size_t> placed;
1380 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1381 const std::size_t tidx = lqn.tshift + t;
1382 std::vector<std::size_t> ents = tidx < lqn.entriesof.size() ? lqn.entriesof[tidx] : std::vector<std::size_t>();
1383 std::sort(ents.begin(), ents.end());
1384 for (std::size_t eidx : ents) {
1385 std::set<std::size_t> As;
1386 if (eidx < lqn.actsof.size())
1387 for (std::size_t a : lqn.actsof[eidx])
1388 if (a >= a0 && a <= a1) As.insert(a);
1389 const std::vector<std::size_t> A(As.begin(), As.end());
1390 if (A.empty()) continue;
1391 std::map<std::size_t, std::vector<std::size_t>> succ, prd;
1392 for (std::size_t a : A) {
1393 succ[a];
1394 prd[a];
1395 for (std::size_t b : A) {
1396 if (G(a, b)) succ[a].push_back(b);
1397 if (G(b, a)) prd[a].push_back(b);
1398 }
1399 }
1400 std::map<std::vector<std::size_t>, std::vector<std::size_t>> groups;
1401 for (std::size_t a : A)
1402 if (is_post_and(lqn, a)) {
1403 std::vector<std::size_t> key = prd[a];
1404 std::sort(key.begin(), key.end());
1405 groups[key].push_back(a);
1406 }
1407 for (const auto& g : groups) {
1408 ForkBlock blk;
1409 const std::string why = read_block(lqn, A, g.second, g.first, succ, prd, blk);
1410 if (!why.empty()) {
1411 bad.push_back(why);
1412 continue;
1413 }
1414 blk.entry = eidx;
1415 std::string src;
1416 for (std::size_t i = 0; i < g.first.size(); ++i) src += (i ? "+" : "") + elem(lqn, g.first[i]);
1417 if (src.empty()) src = elem(lqn, eidx);
1418 blk.label = src + " -> " + elem(lqn, blk.join);
1419 blocks.push_back(blk);
1420 for (std::size_t h : g.second) placed.insert(h);
1421 for (std::size_t tl : blk.tails) placed.insert(tl);
1422 }
1423 }
1424 }
1425 for (std::size_t a : marked)
1426 if (!placed.count(a))
1427 bad.push_back("AND marking at activity " + elem(lqn, a) + " outside any well-formed fork/join block");
1428 return blocks;
1429}
1430
1431/** python _scope_check: the blocks, or LINE:unsupported:NlpExport naming every offender. */
1432template <class T>
1433std::vector<ForkBlock> scope_check(const LqnStruct<T>& lqn, const std::string& fork) {
1434 std::vector<std::string> bad;
1435 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
1436 const CallType ct = c < lqn.calltype.size() ? lqn.calltype[c] : CallType::SYNC;
1437 if (ct != CallType::SYNC)
1438 bad.push_back(std::string(ct == CallType::ASYNC ? "asynchronous" : "forwarding") +
1439 " call: " + callnm(lqn, c));
1440 }
1441 for (std::size_t i = 1; i <= lqn.nidx; ++i)
1442 if (i < lqn.has_arrival.size() && lqn.has_arrival[i])
1443 bad.push_back("open arrival at entry " + elem(lqn, i));
1444 for (std::size_t i = 1; i <= lqn.nidx; ++i) {
1445 const double r = i < lqn.repl.size() && !std::isnan(lqn.repl[i]) ? lqn.repl[i] : 1.0;
1446 if (r > 1) bad.push_back("replication of " + elem(lqn, i) + " (repl=" + fmt("%g", r) + ")");
1447 }
1448 for (std::size_t a = 1; a <= lqn.nacts; ++a)
1449 if (a < lqn.actphase.size() && lqn.actphase[a] > 1)
1450 bad.push_back("phase-2 activity " + elem(lqn, lqn.ashift + a));
1451 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1452 const std::size_t tidx = lqn.tshift + t;
1453 if (tidx < lqn.hassetup.size() && lqn.hassetup[tidx]) bad.push_back("setup time on task " + elem(lqn, tidx));
1454 }
1455 for (std::size_t t = 1; t <= lqn.ntasks; ++t) {
1456 const std::size_t tidx = lqn.tshift + t;
1457 if (tidx < lqn.iscache.size() && lqn.iscache[tidx]) bad.push_back("cache task " + elem(lqn, tidx));
1458 }
1459 const std::vector<ForkBlock> blocks = fork_blocks(lqn, fork, bad);
1460 for (std::size_t i = 1; i < lqn.lincon_A.size(); ++i)
1461 if (lqn.lincon_A[i].rows() > 0) bad.push_back("admission region on " + elem(lqn, i));
1462 for (std::size_t a = 1; a <= lqn.nacts; ++a) {
1463 const std::size_t aidx = lqn.ashift + a;
1464 const std::size_t tidx = lqn.parent[aidx];
1465 std::vector<std::size_t> owners;
1466 if (tidx < lqn.entriesof.size())
1467 for (std::size_t e : lqn.entriesof[tidx])
1468 if (e < lqn.actsof.size() &&
1469 std::find(lqn.actsof[e].begin(), lqn.actsof[e].end(), aidx) != lqn.actsof[e].end())
1470 owners.push_back(e);
1471 if (owners.size() > 1) {
1472 std::string s;
1473 for (std::size_t i = 0; i < owners.size(); ++i) s += (i ? ", " : "") + elem(lqn, owners[i]);
1474 bad.push_back("activity " + elem(lqn, aidx) + " shared by entries " + s);
1475 } else if (owners.empty()) {
1476 bad.push_back("activity " + elem(lqn, aidx) + " bound to no entry");
1477 }
1478 }
1479 std::vector<std::size_t> refs;
1480 for (std::size_t t = 1; t <= lqn.ntasks; ++t)
1481 if (lqn.tshift + t < lqn.isref.size() && lqn.isref[lqn.tshift + t]) refs.push_back(lqn.tshift + t);
1482 if (refs.empty()) bad.push_back("no reference task: nothing drives the model");
1483 for (std::size_t tidx : refs) {
1484 const std::size_t ne = tidx < lqn.entriesof.size() ? lqn.entriesof[tidx].size() : 0;
1485 if (ne != 1)
1486 bad.push_back("reference task " + elem(lqn, tidx) + " has " + std::to_string(ne) +
1487 " entries, expected 1");
1488 }
1489 if (!bad.empty()) {
1490 std::string msg = "cannot express this model as linear constraints plus a nonlinear objective:";
1491 for (const std::string& b : bad) msg += "\n " + b;
1492 throw nlp_error(msg);
1493 }
1494 return blocks;
1495}
1496
1497inline double tot(const std::vector<double>& pops) {
1498 double s = 0.0;
1499 for (double n : pops) {
1500 if (std::isinf(n)) return std::numeric_limits<double>::infinity();
1501 s += n;
1502 }
1503 return s;
1504}
1505
1506inline double delta(double t) {
1507 if (std::isinf(t)) return 1.0;
1508 return t > 0 ? (t - 1.0) / t : 0.0;
1509}
1510
1511inline std::string strip(const std::string& s) {
1512 const std::size_t b = s.find_first_not_of(" \t\n\r\f\v");
1513 if (b == std::string::npos) return "";
1514 const std::size_t e = s.find_last_not_of(" \t\n\r\f\v");
1515 return s.substr(b, e - b + 1);
1516}
1517
1518} // namespace nlp_detail
1519
1520/**
1521 * Build the nonlinear program of a layered model: python export_nlp without the
1522 * emission, i.e. _collect + _attach_lld + the script's index sets, layout,
1523 * build_linear and build_residuals.
1524 *
1525 * @throws UnsupportedError LINE:unsupported:NlpExport naming every feature the
1526 * form cannot express, a bad fork mode or alpha, or an unusable lld
1527 */
1528template <class T>
1530 const std::string& name = "") {
1531 using namespace nlp_detail;
1532 if (opt.fork != "refuse" && opt.fork != "exp")
1533 throw nlp_error("fork takes 'refuse' or 'exp', got '" + opt.fork + "'");
1534 const std::vector<ForkBlock> blocks = scope_check(lqn, opt.fork);
1535
1536 NlpProgram p;
1537 p.name = name.empty() ? "lqn" : name;
1538 const double inf = std::numeric_limits<double>::infinity();
1539 auto todbl = [](const T& v) { return num_traits<T>::to_double(v); };
1540 auto mult = [&](std::size_t i) { return i < lqn.mult.size() ? lqn.mult[i] : std::nan(""); };
1541 auto isinf_elem = [&](std::size_t i) {
1542 return std::isinf(mult(i)) || (i < lqn.sched.size() && lqn.sched[i] == SchedStrategy::INF);
1543 };
1544 const std::size_t hs = lqn.hshift, ts = lqn.tshift, es = lqn.eshift, as = lqn.ashift;
1545 p.NH = lqn.nhosts;
1546 p.NT = lqn.ntasks;
1547 p.NE = lqn.nentries;
1548 p.NA = lqn.nacts;
1549 p.NC = lqn.ncalls;
1550 const std::vector<T> visits = detail::lqn_act_visits<T>(lqn);
1551
1552 for (std::size_t h = 0; h < p.NH; ++h) {
1553 const std::size_t i = hs + h + 1;
1554 p.hosts.push_back(elem(lqn, i));
1555 p.host_mult.push_back(mult(i));
1556 const double mm = i < lqn.maxmult.size() ? lqn.maxmult[i] : std::nan("");
1557 p.host_maxmult.push_back((std::isnan(mm) || mm <= 0) ? mult(i) : mm);
1558 p.host_inf.push_back(isinf_elem(i));
1559 }
1560 for (std::size_t t = 0; t < p.NT; ++t) {
1561 const std::size_t i = ts + t + 1;
1562 p.tasks.push_back(elem(lqn, i));
1563 p.task_host.push_back(lqn.parent[i] - hs - 1);
1564 p.task_mult.push_back(mult(i));
1565 p.task_inf.push_back(isinf_elem(i));
1566 p.task_isref.push_back(i < lqn.isref.size() && lqn.isref[i]);
1567 p.task_think.push_back(todbl(detail::lqn_ref_thinktime(lqn, i)));
1568 }
1569 for (std::size_t e = 0; e < p.NE; ++e) {
1570 const std::size_t i = es + e + 1;
1571 p.entries.push_back(elem(lqn, i));
1572 p.entry_task.push_back(lqn.parent[i] - ts - 1);
1573 }
1574 for (std::size_t a = 0; a < p.NA; ++a) {
1575 const std::size_t i = as + a + 1;
1576 p.acts.push_back(elem(lqn, i));
1577 p.act_task.push_back(lqn.parent[i] - ts - 1);
1578 p.act_entry.push_back(detail::lqn_entry_of_activity(lqn, i) - es - 1);
1579 p.act_demand.push_back(hostdem_mean(lqn, i));
1580 p.act_visits.push_back(todbl(visits[i]));
1581 }
1582 for (std::size_t c = 0; c < p.NC; ++c) {
1583 p.calls.push_back(callnm(lqn, c + 1));
1584 p.call_act.push_back(lqn.callpair_src[c + 1] - as - 1);
1585 p.call_entry.push_back(lqn.callpair_dst[c + 1] - es - 1);
1586 const double y = c + 1 < lqn.callproc_mean.size() ? todbl(lqn.callproc_mean[c + 1]) : 1.0;
1587 if (!std::isfinite(y) || y < 0)
1588 throw nlp_error("call " + callnm(lqn, c + 1) + " has a non-finite mean count " + py_repr(y));
1589 p.call_mean.push_back(y);
1590 p.call_caller_task.push_back(p.act_task[p.call_act[c]]);
1591 }
1592 for (const ForkBlock& b : blocks) {
1593 p.fork_entry.push_back(b.entry - es - 1);
1594 std::vector<std::vector<std::size_t>> brs;
1595 for (const std::vector<std::size_t>& br : b.branches) {
1596 std::vector<std::size_t> v;
1597 for (std::size_t a : br) v.push_back(a - as - 1);
1598 brs.push_back(v);
1599 }
1600 p.fork_branches.push_back(brs);
1601 p.fork_label.push_back(b.label);
1602 }
1603
1604 // python _entry_order: callees before callers; a cycle is a call-path deadlock
1605 {
1606 std::vector<std::set<std::size_t>> dep(p.NE);
1607 for (std::size_t c = 0; c < p.NC; ++c) dep[p.act_entry[p.call_act[c]]].insert(p.call_entry[c]);
1608 std::vector<bool> done(p.NE, false);
1609 while (p.entry_order.size() < p.NE) {
1610 bool progressed = false;
1611 for (std::size_t e = 0; e < p.NE; ++e) {
1612 if (done[e]) continue;
1613 bool ok = true;
1614 for (std::size_t d : dep[e])
1615 if (!done[d]) ok = false;
1616 if (ok) {
1617 done[e] = true;
1618 p.entry_order.push_back(e);
1619 progressed = true;
1620 }
1621 }
1622 if (!progressed) {
1623 std::string stuck;
1624 for (std::size_t e = 0; e < p.NE; ++e)
1625 if (!done[e]) stuck += (stuck.empty() ? "" : ", ") + p.entries[e];
1626 throw nlp_error("cyclic call graph over entries: " + stuck);
1627 }
1628 }
1629 }
1630
1631 // python _attach_lld
1632 if (!std::isfinite(opt.alpha) || opt.alpha <= 0)
1633 throw nlp_error("the softmin sharpness alpha must be finite and positive, got " + py_repr(opt.alpha));
1634 p.alpha = opt.alpha;
1635 p.host_lld.assign(p.NH, "1.0");
1636 p.task_lld.assign(p.NT, "1.0");
1637 std::vector<std::shared_ptr<const LldExpr>> host_expr(p.NH), task_expr(p.NT);
1638 {
1639 std::vector<std::string> bad;
1640 for (const auto& kv : opt.lld) {
1641 const std::string& key = kv.first;
1642 if (kv.second.tabulated) {
1643 bad.push_back(key + ": a tabulated lldscaling row is not smooth -- its interpolant has a "
1644 "corner at every knot. Pass an expression in n instead.");
1645 continue;
1646 }
1647 std::vector<std::pair<int, std::size_t>> where;
1648 for (std::size_t i = 0; i < p.NH; ++i)
1649 if (p.hosts[i] == key) where.push_back({0, i});
1650 for (std::size_t i = 0; i < p.NT; ++i)
1651 if (p.tasks[i] == key) where.push_back({1, i});
1652 if (where.empty()) {
1653 bad.push_back(key + ": no processor or task by that name");
1654 continue;
1655 }
1656 if (where.size() > 1) {
1657 bad.push_back(key + ": names both a processor and a task");
1658 continue;
1659 }
1660 std::shared_ptr<const LldExpr> ex;
1661 const std::string why = check_lld(kv.second.expr, ex);
1662 if (!why.empty()) {
1663 bad.push_back(key + ": " + why);
1664 continue;
1665 }
1666 if (where[0].first == 0) {
1667 p.host_lld[where[0].second] = kv.second.expr;
1668 host_expr[where[0].second] = ex;
1669 } else {
1670 p.task_lld[where[0].second] = kv.second.expr;
1671 task_expr[where[0].second] = ex;
1672 }
1673 }
1674 if (!bad.empty()) {
1675 std::string msg = "cannot use these queue-dependent rates:";
1676 for (const std::string& b : bad) msg += "\n " + b;
1677 throw nlp_error(msg);
1678 }
1679 }
1680 for (std::size_t h = 0; h < p.NH; ++h) {
1681 const bool capped = strip(p.host_lld[h]) == "1.0";
1682 p.host_rate_capped.push_back(capped);
1683 NlpRate r;
1684 r.delay = p.host_inf[h] || std::isinf(p.host_mult[h]);
1685 r.mult = p.host_mult[h];
1686 r.alpha = p.alpha;
1687 if (!capped) r.lld = host_expr[h];
1688 p.host_rate.push_back(r);
1689 }
1690 for (std::size_t t = 0; t < p.NT; ++t) {
1691 NlpRate r;
1692 r.delay = p.task_inf[t] || std::isinf(p.task_mult[t]);
1693 r.mult = p.task_mult[t];
1694 r.alpha = p.alpha;
1695 if (strip(p.task_lld[t]) != "1.0") r.lld = task_expr[t];
1696 p.task_rate.push_back(r);
1697 }
1698
1699 // ---- derived index sets ----------------------------------------------
1700 const std::size_t NH = p.NH, NT = p.NT, NE = p.NE, NA = p.NA, NC = p.NC;
1701 p.acts_of_host.assign(NH, {});
1702 for (std::size_t h = 0; h < NH; ++h)
1703 for (std::size_t a = 0; a < NA; ++a)
1704 if (p.task_host[p.act_task[a]] == h) p.acts_of_host[h].push_back(a);
1705 p.acts_of_entry.assign(NE, {});
1706 for (std::size_t e = 0; e < NE; ++e)
1707 for (std::size_t a = 0; a < NA; ++a)
1708 if (p.act_entry[a] == e) p.acts_of_entry[e].push_back(a);
1709 p.calls_of_act.assign(NA, {});
1710 for (std::size_t a = 0; a < NA; ++a)
1711 for (std::size_t c = 0; c < NC; ++c)
1712 if (p.call_act[c] == a) p.calls_of_act[a].push_back(c);
1713 p.calls_into_entry.assign(NE, {});
1714 for (std::size_t e = 0; e < NE; ++e)
1715 for (std::size_t c = 0; c < NC; ++c)
1716 if (p.call_entry[c] == e) p.calls_into_entry[e].push_back(c);
1717 p.calls_into_task.assign(NT, {});
1718 for (std::size_t t = 0; t < NT; ++t)
1719 for (std::size_t c = 0; c < NC; ++c)
1720 if (p.entry_task[p.call_entry[c]] == t) p.calls_into_task[t].push_back(c);
1721 p.entries_of_task.assign(NT, {});
1722 for (std::size_t t = 0; t < NT; ++t)
1723 for (std::size_t e = 0; e < NE; ++e)
1724 if (p.entry_task[e] == t) p.entries_of_task[t].push_back(e);
1725
1726 p.NFORK = p.fork_entry.size();
1727 for (const auto& blk : p.fork_branches)
1728 for (const auto& br : blk)
1729 for (std::size_t a : br) p.fork_acts.insert(a);
1730 p.blocks_of_entry.assign(NE, {});
1731 for (std::size_t e = 0; e < NE; ++e)
1732 for (std::size_t b = 0; b < p.NFORK; ++b)
1733 if (p.fork_entry[b] == e) p.blocks_of_entry[e].push_back(b);
1734 p.seq_acts_of_entry.assign(NE, {});
1735 for (std::size_t e = 0; e < NE; ++e)
1736 for (std::size_t a : p.acts_of_entry[e])
1737 if (!p.fork_acts.count(a)) p.seq_acts_of_entry[e].push_back(a);
1738
1739 for (std::size_t t = 0; t < NT; ++t)
1740 if (p.task_isref[t]) p.ref_tasks.push_back(t);
1741 p.NR = p.ref_tasks.size();
1742 for (std::size_t t : p.ref_tasks) p.ref_entry.push_back(p.entries_of_task[t][0]);
1743
1744 for (std::size_t t = 0; t < NT; ++t) p.task_delay.push_back(p.task_inf[t] || std::isinf(p.task_mult[t]));
1745 for (std::size_t h = 0; h < NH; ++h) p.host_delay.push_back(p.host_inf[h] || std::isinf(p.host_mult[h]));
1746 p.task_pop = p.task_mult;
1747
1748 p.sibling_acts.assign(NA, {});
1749 for (std::size_t a = 0; a < NA; ++a)
1750 for (std::size_t ap : p.acts_of_host[p.task_host[p.act_task[a]]])
1751 if (p.act_task[ap] == p.act_task[a]) p.sibling_acts[a].push_back(ap);
1752 p.sibling_calls.assign(NC, {});
1753 for (std::size_t c = 0; c < NC; ++c)
1754 for (std::size_t cp : p.calls_into_task[p.entry_task[p.call_entry[c]]])
1755 if (p.call_caller_task[cp] == p.call_caller_task[c]) p.sibling_calls[c].push_back(cp);
1756
1757 for (std::size_t h = 0; h < NH; ++h)
1758 if (!p.host_delay[h] && !p.acts_of_host[h].empty()) p.qd_hosts.push_back(h);
1759 for (std::size_t t = 0; t < NT; ++t)
1760 if (!p.task_delay[t] && !p.calls_into_task[t].empty()) p.qd_tasks.push_back(t);
1761 for (std::size_t i = 0; i < p.qd_hosts.size(); ++i) p.gh_of[p.qd_hosts[i]] = i;
1762 for (std::size_t i = 0; i < p.qd_tasks.size(); ++i) p.gt_of[p.qd_tasks[i]] = i;
1763 p.NGH = p.qd_hosts.size();
1764 p.NGT = p.qd_tasks.size();
1765
1766 for (std::size_t h = 0; h < NH; ++h) {
1767 std::set<std::size_t> ts2;
1768 for (std::size_t a : p.acts_of_host[h]) ts2.insert(p.act_task[a]);
1769 std::vector<double> pops;
1770 for (std::size_t t : ts2) pops.push_back(p.task_pop[t]);
1771 p.host_ntot.push_back(tot(pops));
1772 p.host_delta.push_back(delta(p.host_ntot.back()));
1773 }
1774 for (std::size_t t = 0; t < NT; ++t) {
1775 std::set<std::size_t> ts2;
1776 for (std::size_t c : p.calls_into_task[t]) ts2.insert(p.call_caller_task[c]);
1777 std::vector<double> pops;
1778 for (std::size_t q : ts2) pops.push_back(p.task_pop[q]);
1779 p.task_ntot.push_back(tot(pops));
1780 p.task_delta.push_back(delta(p.task_ntot.back()));
1781 }
1782
1783 // ---- layout ----------------------------------------------------------
1784 const std::size_t sizes[NlpProgram::NBLOCKS] = {NE, NA, NC, NA, NE, NC, NA, NC, NC, p.NR, p.NGH, p.NGT,
1785 p.NFORK ? NE : 0};
1786 std::size_t off = 0;
1787 for (int b = 0; b < NlpProgram::NBLOCKS; ++b) {
1788 p.offset[b] = off;
1789 p.blocksize[b] = sizes[b];
1790 off += sizes[b];
1791 }
1792 p.NVAR = off;
1793 const std::size_t NV = p.NVAR;
1794 using B = NlpProgram;
1795
1796 // ---- linear constraints: python build_linear, row for row -------------
1797 std::vector<std::vector<double>> aeq, aub;
1798 std::vector<double> beq, bub;
1799 auto row = [&]() { return std::vector<double>(NV, 0.0); };
1800 for (std::size_t c = 0; c < NC; ++c) {
1801 auto r = row();
1802 r[p.col(B::XC, c)] = 1.0;
1803 r[p.col(B::XA, p.call_act[c])] -= p.call_mean[c];
1804 aeq.push_back(r);
1805 beq.push_back(0.0);
1806 }
1807 for (std::size_t e = 0; e < NE; ++e) {
1808 if (p.task_isref[p.entry_task[e]]) continue;
1809 auto r = row();
1810 r[p.col(B::XE, e)] = 1.0;
1811 for (std::size_t c : p.calls_into_entry[e]) r[p.col(B::XC, c)] -= 1.0;
1812 aeq.push_back(r);
1813 beq.push_back(0.0);
1814 }
1815 for (std::size_t a = 0; a < NA; ++a) {
1816 auto r = row();
1817 r[p.col(B::XA, a)] = 1.0;
1818 r[p.col(B::XE, p.act_entry[a])] -= p.act_visits[a];
1819 aeq.push_back(r);
1820 beq.push_back(0.0);
1821 }
1822 for (std::size_t h = 0; h < NH; ++h) {
1823 if (!p.host_delay[h]) continue;
1824 for (std::size_t a : p.acts_of_host[h]) {
1825 auto r = row();
1826 r[p.col(B::W, a)] = 1.0;
1827 aeq.push_back(r);
1828 beq.push_back(p.act_demand[a]);
1829 }
1830 }
1831 auto composition = [&](std::vector<double>& r, std::size_t e) {
1832 for (std::size_t a : p.acts_of_entry[e]) {
1833 r[p.col(B::W, a)] -= p.act_visits[a];
1834 for (std::size_t c : p.calls_of_act[a]) r[p.col(B::R, c)] -= p.act_visits[a] * p.call_mean[c];
1835 }
1836 };
1837 for (std::size_t e = 0; e < NE; ++e) {
1838 if (!p.blocks_of_entry[e].empty()) continue;
1839 auto r = row();
1840 r[p.col(B::S, e)] = 1.0;
1841 composition(r, e);
1842 aeq.push_back(r);
1843 beq.push_back(0.0);
1844 }
1845 if (p.NFORK)
1846 for (std::size_t e = 0; e < NE; ++e) {
1847 auto r = row();
1848 r[p.col(B::SA, e)] = 1.0;
1849 composition(r, e);
1850 aeq.push_back(r);
1851 beq.push_back(0.0);
1852 }
1853 for (std::size_t c = 0; c < NC; ++c)
1854 if (p.task_delay[p.entry_task[p.call_entry[c]]]) {
1855 auto r = row();
1856 r[p.col(B::R, c)] = 1.0;
1857 r[p.col(B::S, p.call_entry[c])] -= 1.0;
1858 aeq.push_back(r);
1859 beq.push_back(0.0);
1860 }
1861 for (std::size_t ri = 0; ri < p.NR; ++ri) {
1862 const std::size_t t = p.ref_tasks[ri];
1863 auto r = row();
1864 r[p.col(B::XE, p.ref_entry[ri])] = p.task_think[t];
1865 r[p.col(B::BR, ri)] = 1.0;
1866 aeq.push_back(r);
1867 beq.push_back(p.task_mult[t]);
1868 }
1869 for (std::size_t h = 0; h < NH; ++h) {
1870 if (p.host_delay[h] || !p.host_rate_capped[h]) continue;
1871 auto r = row();
1872 for (std::size_t a : p.acts_of_host[h]) r[p.col(B::XA, a)] += p.act_demand[a];
1873 aub.push_back(r);
1874 bub.push_back(p.host_mult[h]);
1875 }
1876 for (std::size_t t = 0; t < NT; ++t) {
1877 if (p.task_isref[t] || p.task_delay[t] || p.calls_into_task[t].empty()) continue;
1878 auto r = row();
1879 for (std::size_t c : p.calls_into_task[t]) r[p.col(B::BC, c)] = 1.0;
1880 aub.push_back(r);
1881 bub.push_back(p.task_mult[t]);
1882 }
1883 for (std::size_t h = 0; h < NH; ++h) {
1884 if (p.host_delay[h] || std::isinf(p.host_ntot[h]) || p.acts_of_host[h].empty()) continue;
1885 auto r = row();
1886 for (std::size_t a : p.acts_of_host[h]) r[p.col(B::Q, a)] = 1.0;
1887 aub.push_back(r);
1888 bub.push_back(p.host_ntot[h]);
1889 }
1890 for (std::size_t t = 0; t < NT; ++t) {
1891 if (p.task_delay[t] || std::isinf(p.task_ntot[t]) || p.calls_into_task[t].empty()) continue;
1892 auto r = row();
1893 for (std::size_t c : p.calls_into_task[t]) r[p.col(B::QT, c)] = 1.0;
1894 aub.push_back(r);
1895 bub.push_back(p.task_ntot[t]);
1896 }
1897 auto tomat = [&](const std::vector<std::vector<double>>& rows) {
1898 Matrix<double> M(rows.size(), NV, 0.0);
1899 for (std::size_t i = 0; i < rows.size(); ++i)
1900 for (std::size_t j = 0; j < NV; ++j) M(i, j) = rows[i][j];
1901 return M;
1902 };
1903 p.AEQ = tomat(aeq);
1904 p.AUB = tomat(aub);
1905 p.BEQ = beq;
1906 p.BUB = bub;
1907
1908 // ---- residuals: python build_residuals, in its order ------------------
1909 std::vector<std::vector<double>> lin;
1910 std::size_t k = 0;
1911 for (std::size_t a = 0; a < NA; ++a) {
1912 auto r = row();
1913 r[p.col(B::Q, a)] = 1.0;
1914 lin.push_back(r);
1915 p.res_const.push_back(0.0);
1916 p.bil.push_back({k++, p.col(B::XA, a), p.col(B::W, a), -1.0});
1917 }
1918 for (std::size_t c = 0; c < NC; ++c) {
1919 auto r = row();
1920 r[p.col(B::QT, c)] = 1.0;
1921 lin.push_back(r);
1922 p.res_const.push_back(0.0);
1923 p.bil.push_back({k++, p.col(B::XC, c), p.col(B::R, c), -1.0});
1924 }
1925 for (std::size_t c = 0; c < NC; ++c) {
1926 auto r = row();
1927 r[p.col(B::BC, c)] = 1.0;
1928 lin.push_back(r);
1929 p.res_const.push_back(0.0);
1930 p.bil.push_back({k++, p.col(B::XC, c), p.sent(p.call_entry[c]), -1.0});
1931 }
1932 for (std::size_t ri = 0; ri < p.NR; ++ri) {
1933 const std::size_t e = p.ref_entry[ri];
1934 auto r = row();
1935 r[p.col(B::BR, ri)] = 1.0;
1936 lin.push_back(r);
1937 p.res_const.push_back(0.0);
1938 p.bil.push_back({k++, p.col(B::XE, e), p.col(B::S, e), -1.0});
1939 }
1940 for (std::size_t e = 0; e < NE; ++e) {
1941 if (p.blocks_of_entry[e].empty()) continue;
1942 auto r = row();
1943 r[p.col(B::S, e)] = 1.0;
1944 for (std::size_t a : p.seq_acts_of_entry[e]) {
1945 r[p.col(B::W, a)] -= p.act_visits[a];
1946 for (std::size_t c : p.calls_of_act[a]) r[p.col(B::R, c)] -= p.act_visits[a] * p.call_mean[c];
1947 }
1948 lin.push_back(r);
1949 p.res_const.push_back(0.0);
1950 for (std::size_t b : p.blocks_of_entry[e]) {
1952 t.k = k;
1953 t.s = -1.0;
1954 t.c.assign(p.fork_branches[b].size(), 0.0);
1955 t.a = Matrix<double>(p.fork_branches[b].size(), NV, 0.0);
1956 for (std::size_t i = 0; i < p.fork_branches[b].size(); ++i)
1957 for (std::size_t a : p.fork_branches[b][i]) {
1958 t.a(i, p.col(B::W, a)) += p.act_visits[a];
1959 for (std::size_t c : p.calls_of_act[a]) t.a(i, p.col(B::R, c)) += p.act_visits[a] * p.call_mean[c];
1960 }
1961 p.mnl.push_back(t);
1962 }
1963 ++k;
1964 }
1965 for (std::size_t h = 0; h < NH; ++h) {
1966 if (p.host_delay[h]) continue;
1967 const std::size_t g = p.col(B::GH, p.gh_of.at(h));
1968 for (std::size_t a : p.acts_of_host[h]) {
1969 auto r = row();
1970 for (const auto& jw : p.qbar_host_terms(h, a, -p.act_demand[a])) r[jw.first] += jw.second;
1971 lin.push_back(r);
1972 p.res_const.push_back(-p.act_demand[a]);
1973 p.bil.push_back({k, p.col(B::W, a), g, 1.0});
1974 ++k;
1975 }
1976 }
1977 for (std::size_t c = 0; c < NC; ++c) {
1978 const std::size_t e = p.call_entry[c], t = p.entry_task[e];
1979 if (p.task_delay[t]) continue;
1980 auto r = row();
1981 r[p.col(B::S, e)] = -1.0;
1982 lin.push_back(r);
1983 p.res_const.push_back(0.0);
1984 p.bil.push_back({k, p.col(B::R, c), p.col(B::GT, p.gt_of.at(t)), 1.0});
1985 for (const auto& jw : p.qbar_task_terms(t, c, -1.0)) p.bil.push_back({k, p.col(B::S, e), jw.first, jw.second});
1986 ++k;
1987 }
1988 for (std::size_t h : p.qd_hosts) {
1989 auto r = row();
1990 r[p.col(B::GH, p.gh_of.at(h))] = 1.0;
1991 lin.push_back(r);
1992 p.res_const.push_back(0.0);
1994 t.k = k++;
1995 t.f = &p.host_rate[h];
1996 t.s = -1.0;
1997 t.c0 = 1.0;
1998 t.a.assign(NV, 0.0);
1999 for (std::size_t a : p.acts_of_host[h]) t.a[p.col(B::Q, a)] += p.host_delta[h];
2000 p.nlt.push_back(t);
2001 }
2002 for (std::size_t tq : p.qd_tasks) {
2003 auto r = row();
2004 r[p.col(B::GT, p.gt_of.at(tq))] = 1.0;
2005 lin.push_back(r);
2006 p.res_const.push_back(0.0);
2008 t.k = k++;
2009 t.f = &p.task_rate[tq];
2010 t.s = -1.0;
2011 t.c0 = 1.0;
2012 t.a.assign(NV, 0.0);
2013 for (std::size_t c : p.calls_into_task[tq]) t.a[p.col(B::QT, c)] += p.task_delta[tq];
2014 p.nlt.push_back(t);
2015 }
2016 p.res_lin = tomat(lin);
2017 p.NRES = lin.size();
2018 return p;
2019}
2020
2021} // namespace lqn
2022} // namespace line
2023
2024#endif // LINE_API_LQN_EXPORT_NLP_H
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
A node of the model: the index it was given, and the model that owns it.
Definition nodes.h:59
A refusal: the feature, method or analysis is not covered: LINE:unsupported:<kind>.
Definition error.h:117
UnsupportedError(const std::string &what)
Definition error.h:119
The program of one layered model.
std::vector< std::string > tasks
static double maxval(const std::vector< double > &v)
double qbar_task(const std::vector< double > &Qtv, std::size_t t, std::size_t c) const
std::vector< Bil > bil
std::vector< std::string > calls
std::vector< double > BEQ
Matrix< double > AUB
std::vector< std::vector< std::size_t > > acts_of_host
std::vector< double > act_demand
std::vector< std::size_t > call_caller_task
std::vector< std::vector< std::size_t > > entries_of_task
std::vector< bool > task_isref
double qd_pop_task(const std::vector< double > &Qtv, std::size_t t) const
std::vector< std::size_t > act_task
std::vector< double > task_mult
Matrix< double > res_lin
Matrix< double > AEQ
std::vector< Nlt > nlt
std::vector< Mnl > mnl
std::vector< std::size_t > entry_task
std::vector< NlpRate > task_rate
std::vector< std::string > entries
std::vector< std::size_t > qd_hosts
std::vector< std::string > var_names() const
std::vector< std::vector< std::size_t > > sibling_calls
std::vector< std::string > task_lld
std::vector< double > host_mult
std::size_t col(Block b, std::size_t i) const
std::size_t blocksize[NBLOCKS]
std::vector< double > call_mean
std::vector< std::vector< std::vector< std::size_t > > > fork_branches
Sweep sweep(const std::vector< double > &Qv, const std::vector< double > &Qtv) const
One pass of the relations at the given queue lengths: python _sweep.
std::vector< std::string > host_lld
std::vector< double > aeq_resid(const std::vector< double > &x) const
std::vector< std::vector< std::size_t > > blocks_of_entry
std::vector< double > task_delta
NlpProgram & operator=(const NlpProgram &o)
std::vector< std::size_t > entry_order
std::vector< bool > host_rate_capped
std::set< std::size_t > fork_acts
std::vector< double > polish(std::vector< double > x, int iters=50) const
Newton on the square system [residuals; AEQ x - BEQ], min-norm step: python polish.
std::vector< std::size_t > call_entry
std::vector< double > task_pop
std::vector< double > host_ntot
std::vector< std::string > fork_label
std::vector< std::pair< std::size_t, double > > qbar_task_terms(std::size_t t, std::size_t c, double g) const
std::vector< double > task_ntot
std::vector< std::size_t > qd_tasks
Matrix< double > jacobian(const std::vector< double > &x) const
NlpProgram(const NlpProgram &o)
std::vector< double > initial_point(int warm=0) const
A point satisfying every linear relation, after WARM fixed-point sweeps.
double merit(const std::vector< double > &x) const
The merit the fallback descends: ||r||^2 + ||AEQ x - BEQ||^2 + ||max(0, AUB x - BUB)||^2.
std::vector< std::vector< std::size_t > > seq_acts_of_entry
std::vector< bool > task_inf
std::vector< double > BUB
bool solved(const std::vector< double > &x) const
A root of the square system, feasible to the bounds and the inequalities.
std::vector< double > res_const
std::vector< std::size_t > ref_tasks
std::vector< double > host_delta
double objective(const std::vector< double > &x) const
std::vector< bool > host_delay
double qd_pop_host(const std::vector< double > &Qv, std::size_t h) const
std::vector< std::string > hosts
std::vector< bool > host_inf
std::vector< double > lm_fallback(std::vector< double > x, int maxiter, int *iters=nullptr, std::vector< double > *merits=nullptr) const
The projected Levenberg-Marquardt fallback, shared by MATLAB, the JAR and C++ in place of python's tr...
std::vector< std::string > acts
std::size_t offset[NBLOCKS]
double qbar_host(const std::vector< double > &Qv, std::size_t h, std::size_t a) const
std::size_t sent(std::size_t e) const
static double minval(const std::vector< double > &v)
std::vector< std::vector< std::size_t > > acts_of_entry
std::vector< std::vector< std::size_t > > calls_of_act
std::vector< bool > task_delay
std::vector< std::size_t > fork_entry
std::vector< std::vector< std::size_t > > calls_into_entry
std::vector< double > host_maxmult
std::vector< double > aub_resid(const std::vector< double > &x) const
std::vector< std::vector< std::size_t > > calls_into_task
std::map< std::size_t, std::size_t > gh_of
std::vector< std::size_t > act_entry
double compose(const std::vector< std::size_t > &as, const std::vector< double > &Wv, const std::vector< double > &Rv) const
std::vector< std::pair< std::size_t, double > > qbar_host_terms(std::size_t h, std::size_t a, double g) const
static double dot(const std::vector< double > &a, const std::vector< double > &x)
static double maxabs(const std::vector< double > &v)
NlpSolveResult solve(int warm=0, int maxiter=3000) const
Root the program: the Newton ladder over warm starts max(0,warm), 5, 20, 100, then the fallback from ...
std::vector< std::size_t > ref_entry
std::vector< std::size_t > task_host
std::map< std::size_t, std::size_t > gt_of
std::vector< NlpRate > host_rate
std::vector< std::size_t > call_act
std::vector< double > task_think
std::vector< std::vector< std::size_t > > sibling_acts
std::vector< double > block(const std::vector< double > &x, Block b) const
The block's slice of an iterate.
std::vector< double > act_visits
std::vector< double > residuals(const std::vector< double > &x) const
cplx eval(const cplx &n) const
LldExpr(const std::string &src)
The exception types the port throws.
LayeredNetworkStruct, the flattened description of a layered queueing network.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:474
std::string strip(const std::string &s)
std::string elem(const LqnStruct< T > &lqn, std::size_t idx)
S softmin(const S &a, const S &b, double alpha)
Smooth stand-in for min(a, b), verbatim python _SOFTMIN_SRC; real or complex.
UnsupportedError nlp_error(const std::string &msg)
double tot(const std::vector< double > &pops)
bool is_pre_and(const LqnStruct< T > &lqn, std::size_t a)
std::string fmt(const char *f, double v)
std::size_t quorum_of(const LqnStruct< T > &lqn, std::size_t a)
std::complex< double > cplx
double re(double x)
std::vector< ForkBlock > fork_blocks(const LqnStruct< T > &lqn, const std::string &fork, std::vector< std::string > &bad)
python _fork_blocks: the well-formed blocks, and every AND marking outside one.
double delta(double t)
std::vector< ForkBlock > scope_check(const LqnStruct< T > &lqn, const std::string &fork)
python _scope_check: the blocks, or LINE:unsupported:NlpExport naming every offender.
bool is_post_and(const LqnStruct< T > &lqn, std::size_t a)
std::string callnm(const LqnStruct< T > &lqn, std::size_t c)
double hostdem_mean(const LqnStruct< T > &lqn, std::size_t a)
std::string check_lld(const std::string &src, std::shared_ptr< const LldExpr > &out)
Why an lld expression cannot be used, or empty: python _check_lld verbatim.
double norm2(const std::vector< double > &v)
std::vector< double > lstsq_min_norm(const Matrix< double > &A, const std::vector< double > &b)
One-sided Jacobi SVD of A (m x n), returning the min-norm least-squares solution of A x = b.
std::string read_block(const LqnStruct< T > &lqn, const std::vector< std::size_t > &A, const std::vector< std::size_t > &H, const std::vector< std::size_t > &srcs, const std::map< std::size_t, std::vector< std::size_t > > &succ, const std::map< std::size_t, std::vector< std::size_t > > &prd, ForkBlock &blk)
python _read_block: one candidate block, or the reason it is not one.
std::string py_repr(double v)
Python's repr of a float: the shortest round-tripping digits, in python's layout.
std::string name(const Element *el, const std::string &key="name")
std::vector< double > fj_max_exp_grad(const std::vector< double > &u)
Gradient of fj_max_exp by complex step, one coordinate at a time.
NlpProgram lqn_nlp_program(const LqnStruct< T > &lqn, const NlpOptions &opt=NlpOptions(), const std::string &name="")
Build the nonlinear program of a layered model: python export_nlp without the emission,...
S fj_max_exp(const std::vector< S > &m)
E[max] of K independent EXPONENTIAL branch completion times of means m, with the reciprocals cleared ...
double dot(const std::vector< double > &a, const std::vector< double > &b)
The inner product of a row vector with a column held as a vector.
Definition mg1.h:240
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
SolverNC NC
Definition solver.h:547
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
std::vector< double > table
NlpLld(const char *e)
static NlpLld tabulated_row(const std::vector< double > &v)
NlpLld(const std::string &e)
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
std::vector< double > c
std::vector< double > a
std::vector< double > S
std::vector< double > W
std::vector< double > Xc
std::vector< double > SA
std::vector< double > Xe
std::vector< double > R
std::vector< double > Xa
The queue-dependent rate of one station, r(n) = softmin(n, m) [* lld(n)], or n at a delay.
nlp_detail::cplx operator()(const nlp_detail::cplx &n) const
std::shared_ptr< const nlp_detail::LldExpr > lld
double operator()(double n) const
double deriv(double n) const
Derivative by complex step: imag(f(n + 1e-20 i)) / 1e-20.
What NlpProgram::solve returns: python's Result.
bool newton
the Newton ladder reached a feasible root
std::vector< double > x
int niter
fallback iterations; 0 when the Newton ladder rooted it
One AND-fork/join block, in absolute LQN indices.
std::vector< std::vector< std::size_t > > branches
std::vector< std::size_t > tails
An evaluation failure, carrying the python exception name the message reports.
LldEvalError(const std::string &k, const std::string &m)
A parse failure: the text is not an expression of the grammar.