LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
perm_approx.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_PERM_PERM_APPROX_H
6#define LINE_API_PERM_PERM_APPROX_H
7
8/**
9 * @file
10 * @ingroup api_perm
11 * APPROXIMATE permanents: the Sinkhorn heuristic, the Bethe estimate and the
12 * saddle-point expansion.
13 *
14 * Port of python/line_solver/api/perm/approx.py, itself a twin of MATLAB's
15 * `perm_heur.m` and of `jline.lib.perm.BethePermanent`.
16 *
17 * WHY APPROXIMATE AT ALL. The exact algorithms in `permanent.h` cost 2^n or
18 * n!, and the multiplicity method only escapes that when columns repeat. On a
19 * dense matrix with distinct columns neither is usable past about thirty, and
20 * these two are what remain.
21 *
22 * THE THREE ARE NOT INTERCHANGEABLE, and a caller has to know which guarantee it
23 * is getting:
24 *
25 * - `perm_heur` is a HEURISTIC with no error bound in either direction. It
26 * Sinkhorn-scales the matrix toward double stochasticity, averages a
27 * mean-field van der Waerden estimate with a Gurvits capacity bound on the
28 * scaled matrix, and undoes the scaling. The average of a lower bound and a
29 * mean-field estimate is neither.
30 * - `perm_bethe` is a LOWER BOUND on a STRICTLY POSITIVE matrix, which is a
31 * real guarantee and the reason to prefer it when one is needed. It runs
32 * sum-product message passing on the square root of the matrix to a fixed
33 * point and exponentiates the Bethe free energy there.
34 * - `perm_spm` is the saddle-point (Laplace) expansion of the coefficient
35 * integral, the HOMOGENEOUS variant of `cache_spm`. It is exact in the limit
36 * of large column multiplicities and, at unit multiplicities, overestimates
37 * by a factor near (e/sqrt(2 pi))^n with a tight spread across matrices. No
38 * bound in either direction, but far closer than the bare capacity it
39 * corrects, and it is the one of the three that takes repeated columns.
40 *
41 * ALL REQUIRE A STRICTLY POSITIVE MATRIX and refuse otherwise, by name and
42 * with the offending position. Two separate reasons:
43 *
44 * - a NEGATIVE entry: Sinkhorn scaling diverges rather than failing, and the
45 * Bethe bound is simply not a bound off the nonnegative orthant;
46 * - a ZERO entry: the matrix has no full support. Both routines used to floor
47 * a zero to a small eps first, and that substitution is NOT INVERTIBLE.
48 * Every permutation takes one entry per row, so the floored matrix has
49 * permanent n! eps times the permanent of the rest against a true permanent
50 * that may be 0; n! outruns eps by n = 18, and the order of the replicated
51 * demand matrix in pfqn_jointmarg is sum(N). The floored answers were also
52 * simply wrong: on a 3x3 of ones with two zeroed entries, whose permanent is
53 * 3, perm_bethe returned 2748880111.1018 -- the lower-bound property gone by
54 * nine orders of magnitude -- and perm_heur returned the van der Waerden
55 * bound of the Sinkhorn limit of the FLOORED matrix.
56 *
57 * Positivity is sufficient but not necessary. The sharp precondition of the
58 * Sinkhorn scaling is TOTAL SUPPORT: every positive entry lies on a positive
59 * permutation. A matrix with a strictly positive permanent can still fail it --
60 * [[J3, 0], [J3, J3]] has permanent 36 and no total support, and the scaling
61 * then stalls on its tolerance instead of converging. Positivity is the test
62 * used because it is O(n^2). Use the exact engine for a matrix with zeros.
63 *
64 * ARITHMETIC: double. All are iterative floating-point schemes.
65 */
66
67#include <algorithm>
68#include <cmath>
69#include <cstddef>
70#include <limits>
71#include <string>
72#include <vector>
73
74#include "line/util/error.h"
75#include "line/util/matrix.h"
76
77namespace line {
78namespace perm {
79
80namespace approxdetail {
81
82/**
83 * n! in double.
84 *
85 * Exact to n = 170, the largest factorial a double holds; past that Stirling
86 * avoids the overflow, which is the reference's own rule.
87 */
88inline double factorial_d(std::size_t n) {
89 if (n <= 1) return 1.0;
90 if (n <= 170) {
91 double v = 1.0;
92 for (std::size_t i = 2; i <= n; ++i) v *= static_cast<double>(i);
93 return v;
94 }
95 const double d = static_cast<double>(n);
96 return std::sqrt(2.0 * M_PI * d) * std::pow(d / std::exp(1.0), d);
97}
98
99/** Refuse a matrix that is not square and nonnegative, naming the entry. */
100inline void require_nonnegative_square(const Matrix<double>& m, const char* who) {
101 const std::size_t n = m.rows();
102 if (n == 0 || m.cols() != n)
103 throw InputError(std::string(who) + ": the matrix must be square and non-empty");
104 for (std::size_t i = 0; i < n; ++i)
105 for (std::size_t j = 0; j < n; ++j)
106 if (m(i, j) < 0.0)
107 throw InputError(std::string(who) + ": the matrix must be non-negative; entry (" +
108 std::to_string(i) + ", " + std::to_string(j) + ") is " +
109 std::to_string(m(i, j)));
110}
111
112/**
113 * Log determinant of a symmetric positive definite k x k matrix, by Cholesky.
114 *
115 * A failed factorisation is a degenerate saddle point, not a rounding accident,
116 * so it is reported rather than nudged.
117 */
118inline double log_det_cholesky(const std::vector<double>& h, std::size_t k, const char* who) {
119 std::vector<double> l(k * k, 0.0);
120 double logdet = 0.0;
121 for (std::size_t i = 0; i < k; ++i) {
122 for (std::size_t j = 0; j <= i; ++j) {
123 double acc = h[i * k + j];
124 for (std::size_t t = 0; t < j; ++t) acc -= l[i * k + t] * l[j * k + t];
125 if (i == j) {
126 if (!(acc > 0.0))
127 throw InputError(std::string(who) + ": the reduced Hessian is not positive"
128 " definite (leading minor " + std::to_string(i + 1) +
129 "), so the saddle point is degenerate and the Gaussian"
130 " factor does not exist");
131 l[i * k + i] = std::sqrt(acc);
132 logdet += 2.0 * std::log(l[i * k + i]);
133 } else {
134 l[i * k + j] = acc / l[j * k + j];
135 }
136 }
137 }
138 return logdet;
139}
140
141} // namespace approxdetail
142
143/**
144 * Sinkhorn scaling toward double stochasticity.
145 *
146 * Returns the scaled matrix and the two diagonal scalings, since undoing them
147 * is what turns an estimate on the scaled matrix back into one on the original.
148 */
149/**
150 * Refuse a matrix the permanent approximations cannot take.
151 *
152 * The approximations rest, directly or through the Sinkhorn scaling they
153 * share, on a strictly positive matrix. A zero used to be floored to a small
154 * eps first, and that substitution is not invertible: a matrix with an
155 * identically zero row has permanent 0 while the floored matrix has permanent
156 * n! eps times the permanent of the rest, which is O(1) by n = 18. Positivity
157 * is sufficient but not necessary -- the sharp precondition is TOTAL SUPPORT,
158 * which a matrix with a positive permanent can still fail -- but it is O(n^2)
159 * and is the contract this header already states.
160 */
161inline void require_full_support(const Matrix<double>& m, const char* who) {
162 for (std::size_t i = 0; i < m.rows(); ++i)
163 for (std::size_t j = 0; j < m.cols(); ++j)
164 if (!(m(i, j) > 0.0))
165 throw InputError(std::string(who) +
166 ": requires a strictly positive matrix, but entry (" +
167 std::to_string(i + 1) + "," + std::to_string(j + 1) +
168 ") is " + std::to_string(m(i, j)) +
169 ", so the matrix has no full support. Flooring it would change"
170 " the permanent by n!*eps, which is O(1) by n=18. Use the exact"
171 " engine.");
172}
173
174inline void sinkhorn_scaling(const Matrix<double>& in, Matrix<double>* B, std::vector<double>* r,
175 std::vector<double>* c, double tolerance = 1e-10,
176 std::size_t max_iterations = 1000) {
177 const std::size_t n = in.rows();
178 r->assign(n, 1.0);
179 c->assign(n, 1.0);
180 bool converged = false;
181 double last_error = std::numeric_limits<double>::infinity();
182 for (std::size_t it = 0; it < max_iterations; ++it) {
183 for (std::size_t i = 0; i < n; ++i) {
184 double s = 0.0;
185 for (std::size_t j = 0; j < n; ++j) s += in(i, j) * (*c)[j];
186 (*r)[i] = (s != 0.0) ? 1.0 / s : 0.0;
187 }
188 for (std::size_t j = 0; j < n; ++j) {
189 double s = 0.0;
190 for (std::size_t i = 0; i < n; ++i) s += in(i, j) * (*r)[i];
191 (*c)[j] = (s != 0.0) ? 1.0 / s : 0.0;
192 }
193 double worst = 0.0;
194 for (std::size_t i = 0; i < n; ++i) {
195 double s = 0.0;
196 for (std::size_t j = 0; j < n; ++j) s += in(i, j) * (*c)[j];
197 worst = std::max(worst, std::fabs((*r)[i] * s - 1.0));
198 }
199 if (worst < tolerance) {
200 converged = true;
201 break;
202 }
203 last_error = worst;
204 }
205 if (!converged)
206 throw InputError("sinkhorn_scaling: did not converge to a doubly stochastic matrix in " +
207 std::to_string(max_iterations) + " sweeps (margin error " +
208 std::to_string(last_error) + " against a tolerance of " +
209 std::to_string(tolerance) +
210 "). The usual cause is a matrix without total support.");
211 *B = Matrix<double>(n, n, 0.0);
212 for (std::size_t i = 0; i < n; ++i)
213 for (std::size_t j = 0; j < n; ++j) (*B)(i, j) = (*r)[i] * in(i, j) * (*c)[j];
214}
215
216/**
217 * The Sinkhorn heuristic. NO error bound in either direction -- see the header.
218 *
219 * A zero entry used to be nudged to 1e-15 before scaling. That is not
220 * invertible -- it changes the permanent by n!*eps -- so a non-positive entry
221 * is now REFUSED by `require_full_support` instead.
222 */
223inline double perm_heur(const Matrix<double>& m, double tolerance = 1e-10,
224 std::size_t max_iterations = 1000) {
225 approxdetail::require_nonnegative_square(m, "perm_heur");
226 const std::size_t n = m.rows();
227 if (n > 0) require_full_support(m, "perm_heur");
228
229 Matrix<double> work = m;
230
232 std::vector<double> r, c;
233 sinkhorn_scaling(work, &B, &r, &c, tolerance, max_iterations);
234
235 std::vector<double> rowsum(n, 0.0);
236 for (std::size_t i = 0; i < n; ++i)
237 for (std::size_t j = 0; j < n; ++j) rowsum[i] += B(i, j);
238
239 const double nd = static_cast<double>(n);
240 double rowprod = 1.0, logsum = 0.0;
241 for (std::size_t i = 0; i < n; ++i) {
242 rowprod *= rowsum[i];
243 logsum += std::log(rowsum[i]);
244 }
245 const double fact = approxdetail::factorial_d(n);
246 const double p_meanfield = fact * (rowprod / std::pow(nd, nd));
247 const double cap = std::exp(logsum / nd);
248 const double p_gurvits = fact * std::pow(cap / nd, nd);
249 const double p_est = 0.5 * (p_meanfield + p_gurvits);
250
251 double scale = 1.0;
252 for (std::size_t i = 0; i < n; ++i) scale *= 1.0 / r[i];
253 for (std::size_t j = 0; j < n; ++j) scale *= 1.0 / c[j];
254 return p_est * scale;
255}
256
257/**
258 * The Bethe permanent, by sum-product message passing.
259 *
260 * A LOWER BOUND of the permanent for a nonnegative matrix. Two message
261 * families -- right-going `r` and left-going `l` -- are iterated to a fixed
262 * point on the SQUARE ROOT of the matrix, and the Bethe free energy there is
263 * exponentiated.
264 *
265 * THE DENOMINATOR EXCLUDES THE DIAGONAL, NOT THE ENTRY ITSELF. A textbook
266 * sum-product message from (i,j) omits column j of row i; this scheme omits
267 * the DIAGONAL element of the row instead, so one denominator serves the whole
268 * row and the numerator carries `s(i,j)`. The two agree on a symmetric matrix
269 * and disagree otherwise -- measured on a 4x4 dense instance, 386.3 for the
270 * textbook form against the reference's 325.7, both below the exact 1092 and
271 * so both bounds, but only one of them the reference's. Transcribed as written.
272 *
273 * @param epsilon squared message change below which the iteration stops
274 * @param max_iteration cap on the message passing
275 */
276inline double perm_bethe(const Matrix<double>& m, double epsilon = 0.001,
277 std::size_t max_iteration = 200000) {
278 approxdetail::require_nonnegative_square(m, "perm_bethe");
279 const std::size_t n = m.rows();
280 if (n > 0) require_full_support(m, "perm_bethe");
281 // kMin guards the LOGARITHMS of message products below against underflow.
282 // It is deliberately NOT applied to the input: flooring the input is what
283 // fabricates a permanent of n!*eps where the truth is zero.
284 const double kMin = 2.220446049250314e-16;
285 (void)kMin;
286
287 Matrix<double> s(n, n, 0.0);
288 for (std::size_t i = 0; i < n; ++i)
289 for (std::size_t j = 0; j < n; ++j) s(i, j) = std::sqrt(m(i, j));
290
291 // One update: right-going from the current left-going, then left-going
292 // from the right-going just produced. The second half reads the FRESH r,
293 // which is why the two cannot be swapped.
294 auto update = [&s, n](const Matrix<double>& l, Matrix<double>* r1, Matrix<double>* l1) {
295 *r1 = Matrix<double>(n, n, 0.0);
296 *l1 = Matrix<double>(n, n, 0.0);
297 for (std::size_t i = 0; i < n; ++i) {
298 double d = 0.0;
299 for (std::size_t j = 0; j < n; ++j) d += s(i, j) * l(i, j);
300 d -= s(i, i) * l(i, i); // the DIAGONAL, not the (i,j) term
301 for (std::size_t j = 0; j < n; ++j) (*r1)(i, j) = (d != 0.0) ? s(i, j) / d : 0.0;
302 }
303 for (std::size_t j = 0; j < n; ++j) {
304 double d = 0.0;
305 for (std::size_t i = 0; i < n; ++i) d += s(i, j) * (*r1)(i, j);
306 d -= s(j, j) * (*r1)(j, j);
307 for (std::size_t i = 0; i < n; ++i) (*l1)(i, j) = (d != 0.0) ? s(i, j) / d : 0.0;
308 }
309 };
310
311 Matrix<double> r_past(n, n, 1.0), l_past(n, n, 1.0), r, l;
312 update(l_past, &r, &l);
313 for (std::size_t it = 0; it < max_iteration; ++it) {
314 double change = 0.0;
315 for (std::size_t i = 0; i < n; ++i)
316 for (std::size_t j = 0; j < n; ++j) {
317 const double a = r_past(i, j) - r(i, j), b = l_past(i, j) - l(i, j);
318 change += a * a + b * b;
319 }
320 if (change <= epsilon) break;
321 r_past = r;
322 l_past = l;
323 update(l_past, &r, &l);
324 }
325
326 // The Bethe free energy at the fixed point, in logs so the products do not
327 // overflow: row sums of s*l, column sums of s*r, less the edge term r*l+1.
328 double logv = 0.0;
329 for (std::size_t i = 0; i < n; ++i) {
330 double t = 0.0;
331 for (std::size_t j = 0; j < n; ++j) t += s(i, j) * l(i, j);
332 logv += std::log(std::max(t, kMin));
333 }
334 for (std::size_t j = 0; j < n; ++j) {
335 double t = 0.0;
336 for (std::size_t i = 0; i < n; ++i) t += s(i, j) * r(i, j);
337 logv += std::log(std::max(t, kMin));
338 }
339 for (std::size_t i = 0; i < n; ++i)
340 for (std::size_t j = 0; j < n; ++j)
341 logv -= std::log(std::max(r(i, j) * l(i, j) + 1.0, kMin));
342
343 const double out = std::exp(logv);
344 // A non-finite free energy is not a bound; the reference reports zero.
345 return std::isfinite(out) ? out : 0.0;
346}
347
348/** Outcome of the saddle-point expansion: the estimate and what produced it. */
350 double value = 1.0; ///< the approximate permanent
351 double log_value = 0.0; ///< its logarithm, correct even when `value` overflows
352 double log_capacity = 0.0; ///< log Gurvits capacity, an upper bound on the log permanent
353 std::vector<double> xi; ///< saddle point, unit geometric mean, 0 on a dropped column
354};
355
356/**
357 * Saddle-point (SPM) approximation of the permanent. THE HOMOGENEOUS CACHE_SPM.
358 *
359 * Approximates the permanent of the matrix built from the n x h matrix `a` by
360 * repeating column l exactly `mult[l]` times, so sum(mult) must equal n; an
361 * empty `mult` means all ones, which requires a square matrix.
362 *
363 * cache_spm and this routine evaluate the SAME Cauchy integral by Laplace's
364 * method and differ only in the generating function whose coefficient they
365 * extract:
366 *
367 * cache_spm E(m) = prod_l m_l! [prod_l z_l^m_l] prod_k (1 + sum_l g_kl z_l)
368 * perm_spm P = prod_l m_l! [prod_l z_l^m_l] prod_k ( sum_l a_kl z_l)
369 *
370 * The cache factor carries a "+1" because an item may stay out of the cache, so
371 * what it extracts is a RECTANGULAR permanent over n items and sum(m) < n
372 * slots. Dropping the "+1" forces every row to be matched, which is exactly the
373 * permanent and requires sum(m) == n -- the one case cache_spm cannot serve,
374 * since at n == sum(m) its multipliers diverge and it falls back on cache_erec.
375 * Here the integrand is homogeneous and the saddle point is interior in the
376 * h-1 directions that survive.
377 *
378 * METHOD. With z_l = xi_l exp(i th_l) the saddle point in xi solves
379 *
380 * sum_k a_kl xi_l / (sum_j a_kj xi_j) = m_l, l = 1..h,
381 *
382 * so p_kl = a_kl xi_l / s_k with s = a*xi is the diagonal scaling of `a` to row
383 * sums 1 and column sums m (Sinkhorn; doubly stochastic when m is all ones).
384 * There phi = sum_k log s_k - sum_l m_l log xi_l is the log Gurvits capacity.
385 * The Gaussian correction uses H = diag(m) - p'p, a weighted graph Laplacian on
386 * the columns: H*ones = 0, which is the invariance of the integrand under
387 * th -> th + c*ones that homogeneity creates. That direction is a full period
388 * rather than a Gaussian, so it contributes 2*pi and leaves an (h-1)
389 * dimensional Laplace integral; any principal (h-1) submatrix serves, because
390 * all cofactors of a Laplacian are equal. The estimate is
391 *
392 * log P = sum_l log(m_l!) - (h-1)/2 log(2 pi) + phi - 1/2 log det(H_red).
393 *
394 * ACCURACY, AND WHAT IT IS NOT. Exact for h == 1, where the permanent is
395 * n! prod_k a(k,0). It is a genuine asymptotic expansion as min(m) grows with h
396 * fixed, the ratio to the exact permanent falling from 1.11 at m = (2,2,2) to
397 * 1.02 at m = (3,3). At m = ones the dimension of the integral grows with the
398 * expansion parameter and the leading term keeps a systematic BIAS: on the n x n
399 * matrix of ones it returns (2 pi)^(-(n-1)/2) n^(n+1/2) against the exact n!, a
400 * ratio tending to (e/sqrt(2 pi))^n = 1.084^n, and random positive matrices
401 * track that closely (1.31 at n = 4, 1.87 at n = 8). So at m = ones it
402 * OVERESTIMATES, with a spread across matrices far tighter than the bias
403 * itself, and it is NOT a bound in either direction; `perm_bethe` is the
404 * routine to use when a bound is needed.
405 *
406 * REQUIRES A STRICTLY POSITIVE MATRIX, for the reasons the header states: the
407 * scaling is what needs it, and flooring a zero is not invertible. Positivity
408 * also makes the column graph complete, hence H_red positive definite.
409 */
411 const std::vector<std::size_t>& mult,
412 double tolerance = 1e-11,
413 std::size_t max_iterations = 10000) {
414 PermSpmResult out;
415 const std::size_t n = a.rows();
416 const std::size_t h = a.cols();
417 if (n == 0 || h == 0) return out; // the permanent of the empty matrix is 1
418
419 for (std::size_t i = 0; i < n; ++i)
420 for (std::size_t j = 0; j < h; ++j)
421 if (a(i, j) < 0.0)
422 throw InputError("perm_spm: the matrix must be non-negative; entry (" +
423 std::to_string(i) + ", " + std::to_string(j) + ") is " +
424 std::to_string(a(i, j)));
425
426 std::vector<std::size_t> m = mult;
427 if (m.empty()) {
428 if (h != n)
429 throw InputError("perm_spm: without column multiplicities the matrix must be square;"
430 " it is " + std::to_string(n) + "x" + std::to_string(h));
431 m.assign(n, 1);
432 }
433 if (m.size() != h)
434 throw InputError("perm_spm: the multiplicity vector has " + std::to_string(m.size()) +
435 " entries against " + std::to_string(h) + " columns");
436 std::size_t total = 0;
437 for (std::size_t j = 0; j < h; ++j) total += m[j];
438 if (total != n)
439 throw InputError("perm_spm: the column multiplicities must sum to the number of rows"
440 " (sum(m) = " + std::to_string(total) + " against " + std::to_string(n) +
441 " rows). The integrand is homogeneous of degree " + std::to_string(n) +
442 ", so every other coefficient of it is exactly zero");
443 require_full_support(a, "perm_spm");
444
445 // A column repeated zero times leaves the permanent unchanged, and its xi is a
446 // boundary of the Laplace integral rather than a direction of it, so it must leave
447 // the expansion. Dropping it is exact: setting z_l = 0 removes the column, and
448 // prod_l m_l! is unchanged because 0! = 1.
449 std::vector<std::size_t> keep;
450 for (std::size_t j = 0; j < h; ++j)
451 if (m[j] > 0) keep.push_back(j);
452 const std::size_t hk = keep.size();
453 std::vector<double> mk(hk);
454 for (std::size_t l = 0; l < hk; ++l) mk[l] = static_cast<double>(m[keep[l]]);
455
456 std::vector<double> ak(n * hk);
457 for (std::size_t i = 0; i < n; ++i)
458 for (std::size_t l = 0; l < hk; ++l) ak[i * hk + l] = a(i, keep[l]);
459
460 // Saddle point: scale to row sums 1 and column sums mk. The row sums are 1 by
461 // construction of p, so only the column sums are iterated on.
462 std::vector<double> xik(hk, 1.0), s(n, 0.0), colsum(hk, 0.0);
463 bool converged = false;
464 double margin = std::numeric_limits<double>::infinity();
465 for (std::size_t it = 0; it < max_iterations && !converged; ++it) {
466 for (std::size_t i = 0; i < n; ++i) {
467 double acc = 0.0;
468 for (std::size_t l = 0; l < hk; ++l) acc += ak[i * hk + l] * xik[l];
469 s[i] = acc;
470 }
471 margin = 0.0;
472 for (std::size_t l = 0; l < hk; ++l) {
473 double acc = 0.0;
474 for (std::size_t i = 0; i < n; ++i) acc += ak[i * hk + l] / s[i];
475 colsum[l] = xik[l] * acc;
476 margin = std::max(margin, std::fabs(colsum[l] - mk[l]));
477 }
478 if (margin < tolerance) {
479 converged = true;
480 break;
481 }
482 double logmean = 0.0;
483 for (std::size_t l = 0; l < hk; ++l) {
484 xik[l] *= mk[l] / colsum[l];
485 logmean += std::log(xik[l]);
486 }
487 logmean /= static_cast<double>(hk);
488 const double scale = std::exp(logmean);
489 for (std::size_t l = 0; l < hk; ++l) xik[l] /= scale; // the saddle is a ray; pin its scale
490 }
491 if (!converged)
492 throw InputError("perm_spm: the scaling to row sums 1 and column sums m did not converge"
493 " in " + std::to_string(max_iterations) + " sweeps (margin error " +
494 std::to_string(margin) + " against a tolerance of " +
495 std::to_string(tolerance) + "). The expansion assumes the saddle point,"
496 " so no value is returned. The usual cause is a matrix without total"
497 " support.");
498
499 out.xi.assign(h, 0.0);
500 for (std::size_t l = 0; l < hk; ++l) out.xi[keep[l]] = xik[l];
501
502 for (std::size_t i = 0; i < n; ++i) {
503 double acc = 0.0;
504 for (std::size_t l = 0; l < hk; ++l) acc += ak[i * hk + l] * xik[l];
505 s[i] = acc;
506 }
507 std::vector<double> p(n * hk);
508 for (std::size_t i = 0; i < n; ++i)
509 for (std::size_t l = 0; l < hk; ++l) p[i * hk + l] = ak[i * hk + l] * xik[l] / s[i];
510
511 double log_capacity = 0.0;
512 for (std::size_t i = 0; i < n; ++i) log_capacity += std::log(s[i]);
513 for (std::size_t l = 0; l < hk; ++l) log_capacity -= mk[l] * std::log(xik[l]);
514
515 // H = diag(mk) - p'p is a Laplacian, so it is singular along ones and all of its
516 // principal cofactors are equal; the last index is dropped only because one has to
517 // be. Strict positivity makes the column graph complete, hence H_red positive
518 // definite and Cholesky the right factor. At hk == 1 no direction survives the
519 // homogeneity, and the determinant of the empty matrix is 1.
520 double log_det = 0.0;
521 if (hk > 1) {
522 const std::size_t k = hk - 1;
523 std::vector<double> hred(k * k);
524 for (std::size_t l = 0; l < k; ++l)
525 for (std::size_t j = 0; j < k; ++j) {
526 double dot = 0.0;
527 for (std::size_t i = 0; i < n; ++i) dot += p[i * hk + l] * p[i * hk + j];
528 hred[l * k + j] = (l == j ? mk[l] : 0.0) - dot;
529 }
530 log_det = approxdetail::log_det_cholesky(hred, k, "perm_spm");
531 }
532
533 double log_fact = 0.0;
534 for (std::size_t l = 0; l < hk; ++l) log_fact += std::lgamma(mk[l] + 1.0);
535
536 out.log_capacity = log_capacity;
537 out.log_value = log_fact - 0.5 * static_cast<double>(hk - 1) * std::log(2.0 * M_PI) +
538 log_capacity - 0.5 * log_det;
539 out.value = std::exp(out.log_value);
540 return out;
541}
542
543/** The saddle-point estimate of the permanent of a square strictly positive matrix. */
544inline double perm_spm(const Matrix<double>& a, double tolerance = 1e-11,
545 std::size_t max_iterations = 10000) {
546 return perm_spm_expand(a, std::vector<std::size_t>(), tolerance, max_iterations).value;
547}
548
549/** The saddle-point estimate with column l of `a` repeated `mult[l]` times. */
550inline double perm_spm(const Matrix<double>& a, const std::vector<std::size_t>& mult,
551 double tolerance = 1e-11, std::size_t max_iterations = 10000) {
552 return perm_spm_expand(a, mult, tolerance, max_iterations).value;
553}
554
555} // namespace perm
556} // namespace line
557
558#endif // LINE_API_PERM_PERM_APPROX_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Dense matrix and non-owning view.
double perm_spm(const Matrix< double > &a, double tolerance=1e-11, std::size_t max_iterations=10000)
The saddle-point estimate of the permanent of a square strictly positive matrix.
PermSpmResult perm_spm_expand(const Matrix< double > &a, const std::vector< std::size_t > &mult, double tolerance=1e-11, std::size_t max_iterations=10000)
Saddle-point (SPM) approximation of the permanent.
void require_full_support(const Matrix< double > &m, const char *who)
Sinkhorn scaling toward double stochasticity.
double perm_heur(const Matrix< double > &m, double tolerance=1e-10, std::size_t max_iterations=1000)
The Sinkhorn heuristic.
double perm_bethe(const Matrix< double > &m, double epsilon=0.001, std::size_t max_iteration=200000)
The Bethe permanent, by sum-product message passing.
void sinkhorn_scaling(const Matrix< double > &in, Matrix< double > *B, std::vector< double > *r, std::vector< double > *c, double tolerance=1e-10, std::size_t max_iterations=1000)
Outcome of the saddle-point expansion: the estimate and what produced it.
std::vector< double > xi
saddle point, unit geometric mean, 0 on a dropped column
double log_capacity
log Gurvits capacity, an upper bound on the log permanent
double value
the approximate permanent
double log_value
its logarithm, correct even when value overflows