LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
perm_sampling.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_SAMPLING_H
6#define LINE_API_PERM_PERM_SAMPLING_H
7
8/**
9 * @file
10 * @ingroup api_perm
11 * RANDOMIZED permanents: the AdaPart rejection sampler and the Huber-Law
12 * acceptance-rejection importance sampler.
13 *
14 * Port of python/line_solver/api/perm/sampling.py, itself a twin of
15 * jline.lib.perm.AdaPartSampler and jline.lib.perm.HuberLawSampler, and of
16 * MATLAB's perm_adapart.m and perm_huberlaw.m.
17 *
18 * WHAT THESE ARE FOR. The exact algorithms in permanent.h cost 2^n or n!, and
19 * the two schemes in perm_approx.h buy their speed with a bias no caller can
20 * bound. These two are unbiased Monte Carlo estimators instead: the estimate
21 * carries sampling noise, but no systematic error, so averaging more draws
22 * converges to the permanent rather than to a nearby number.
23 *
24 * - `perm_adapart` recursively partitions the permutation space, bounds each
25 * part by the Soules column bound, and draws a part with probability
26 * proportional to its bound. The acceptance ratio scales the root bound into
27 * an unbiased estimate.
28 * - `perm_huberlaw` rescales the matrix to doubly stochastic, draws a
29 * permutation column by column under the Huber-Law bound on the remaining
30 * permanent, and multiplies the acceptance ratio by the rescaling constant.
31 *
32 * TWO ADAPART FIXES ARE CARRIED HERE, and both are load-bearing rather than
33 * cosmetic:
34 *
35 * 1. The column search considers ONLY the columns still unassigned. Scoring an
36 * already-assigned column just re-derives its own constraint, which always
37 * looks cheapest, so the sampler re-splits the same column forever and never
38 * completes an assignment.
39 * 2. The expansion discounts the bound of the element ACTUALLY REMOVED, not the
40 * root bound. With the root bound the running total drifts and the "refine
41 * until improved" loop cannot exit.
42 *
43 * Even so the Soules bound is TIGHT on a matrix with equal entries, so no
44 * refinement can improve it; the loop therefore stops on the first
45 * non-improving expansion, which is exactly the shape a replicated demand
46 * matrix produces (see pfqn_jointmarg).
47 *
48 * BOTH REQUIRE A NONNEGATIVE MATRIX, and both need FULL SUPPORT: an exact zero
49 * makes the Huber-Law rescaling degenerate and starves the AdaPart acceptance.
50 * The callers in the pfqn layer refuse a structurally zero matrix rather than
51 * flooring it.
52 *
53 * REPRODUCIBILITY: the draws come from a seeded std::mt19937_64, so a run with
54 * a fixed seed repeats. The Python twin draws from a numpy Generator and the
55 * JAR from java.util.Random, so the three agree in distribution but not sample
56 * by sample.
57 *
58 * ARITHMETIC: double. Both are floating-point Monte Carlo schemes.
59 */
60
61#include <algorithm>
62#include <cmath>
63#include <cstddef>
64#include <cstdint>
65#include <map>
66#include <random>
67#include <set>
68#include <string>
69#include <vector>
70
71#include "line/util/error.h"
72#include "line/util/matrix.h"
73
74namespace line {
75namespace perm {
76
77namespace samplingdetail {
78
79/** n! in double by the plain product, as in the JAR and the Python twin. */
80inline double factorial_plain(std::size_t n) {
81 double result = 1.0;
82 for (std::size_t i = 1; i <= n; ++i) result *= static_cast<double>(i);
83 return result;
84}
85
86/** Rejects a matrix that is not square or carries a negative entry. */
87inline void require_square_nonnegative(const Matrix<double>& m, const char* who) {
88 if (m.rows() != m.cols())
89 throw InputError(std::string(who) + ": the matrix must be square, got " +
90 std::to_string(m.rows()) + " by " + std::to_string(m.cols()));
91 for (std::size_t i = 0; i < m.rows(); ++i)
92 for (std::size_t j = 0; j < m.cols(); ++j)
93 if (m(i, j) < 0.0)
94 throw InputError(std::string(who) + ": entry (" + std::to_string(i + 1) + "," +
95 std::to_string(j + 1) + ") is negative");
96}
97
98/**
99 * Refuse a matrix the samplers cannot take. Twin of the approx-header guard.
100 *
101 * A zero used to be floored before scaling, and that substitution is not
102 * invertible: it changes the permanent by n!*eps, which is O(1) by n = 18.
103 * Positivity is sufficient but not necessary -- the sharp precondition is
104 * TOTAL SUPPORT -- but it is O(n^2) and is the contract the headers state.
105 */
106inline void require_full_support(const Matrix<double>& m, const char* who) {
107 for (std::size_t i = 0; i < m.rows(); ++i)
108 for (std::size_t j = 0; j < m.cols(); ++j)
109 if (!(m(i, j) > 0.0))
110 throw InputError(std::string(who) +
111 ": requires a strictly positive matrix, but entry (" +
112 std::to_string(i + 1) + "," + std::to_string(j + 1) +
113 ") is " + std::to_string(m(i, j)) +
114 ", so the matrix has no full support. Flooring it would change"
115 " the permanent by n!*eps, which is O(1) by n=18. Use the exact"
116 " engine.");
117}
118
119/**
120 * Maximum-weight perfect assignment, by the O(n^3) Hungarian algorithm.
121 *
122 * Replaces the row-by-row greedy that used to stand in for it. The greedy
123 * returns a zero-weight assignment on inputs that admit a positive one: on
124 * [[1,2],[0,3]] row 0 takes the larger entry in column 1, leaving row 1 with
125 * the zero. That weight is alpha3, which sets the flooring level
126 * alpha1 = alpha3*delta/(3 n!) of the Huber-Law bound, so a suboptimal
127 * assignment weakens the method's own guarantee. The MATLAB, JAR and python
128 * twins solve the same problem and agree on the optimal VALUE, which is all
129 * alpha3 depends on; they need not agree on the permutation when it is
130 * degenerate.
131 */
132inline std::vector<std::size_t> max_weight_assignment(const Matrix<double>& weight) {
133 const std::size_t n = weight.rows();
134 std::vector<std::size_t> assignment(n, 0);
135 if (n == 0) return assignment;
136
137 const double kInf = std::numeric_limits<double>::infinity();
138 std::vector<std::vector<double>> cost(n + 1, std::vector<double>(n + 1, 0.0));
139 for (std::size_t i = 1; i <= n; ++i)
140 for (std::size_t j = 1; j <= n; ++j) cost[i][j] = -weight(i - 1, j - 1);
141
142 std::vector<double> u(n + 1, 0.0), v(n + 1, 0.0);
143 std::vector<std::size_t> p(n + 1, 0), way(n + 1, 0);
144
145 for (std::size_t i = 1; i <= n; ++i) {
146 p[0] = i;
147 std::size_t j0 = 0;
148 std::vector<double> minv(n + 1, kInf);
149 std::vector<bool> used(n + 1, false);
150 do {
151 used[j0] = true;
152 const std::size_t i0 = p[j0];
153 std::size_t j1 = 0;
154 double delta = kInf;
155 for (std::size_t j = 1; j <= n; ++j) {
156 if (!used[j]) {
157 const double cur = cost[i0][j] - u[i0] - v[j];
158 if (cur < minv[j]) {
159 minv[j] = cur;
160 way[j] = j0;
161 }
162 if (minv[j] < delta) {
163 delta = minv[j];
164 j1 = j;
165 }
166 }
167 }
168 for (std::size_t j = 0; j <= n; ++j) {
169 if (used[j]) {
170 u[p[j]] += delta;
171 v[j] -= delta;
172 } else {
173 minv[j] -= delta;
174 }
175 }
176 j0 = j1;
177 } while (p[j0] != 0);
178 do {
179 const std::size_t j1 = way[j0];
180 p[j0] = p[j1];
181 j0 = j1;
182 } while (j0 != 0);
183 }
184
185 for (std::size_t j = 1; j <= n; ++j)
186 if (p[j] != 0) assignment[p[j] - 1] = j - 1;
187 return assignment;
188}
189
190/**
191 * Soules upper bound of the permanent, a product of column bounds.
192 *
193 * gamma(k) = (k!)^(1/k), delta(i) = gamma(n-i) - gamma(n-i-1), and each column
194 * is sorted ascending before the weights are applied.
195 */
196inline double soules_bound(const Matrix<double>& m) {
197 const std::size_t n = m.rows();
198 if (n == 0) return 1.0;
199 std::vector<double> gamma(n + 1, 0.0);
200 double fact = 1.0;
201 for (std::size_t k = 1; k <= n; ++k) {
202 fact *= static_cast<double>(k);
203 gamma[k] = std::pow(fact, 1.0 / static_cast<double>(k));
204 }
205 std::vector<double> delta(n, 0.0);
206 for (std::size_t i = 0; i < n; ++i) delta[i] = gamma[n - i] - gamma[n - i - 1];
207
208 double prod = 1.0;
209 std::vector<double> col(n);
210 for (std::size_t j = 0; j < n; ++j) {
211 for (std::size_t i = 0; i < n; ++i) col[i] = m(i, j);
212 std::sort(col.begin(), col.end());
213 double s = 0.0;
214 for (std::size_t i = 0; i < n; ++i) s += delta[i] * col[i];
215 prod *= s;
216 }
217 return prod;
218}
219
220} // namespace samplingdetail
221
222/**
223 * Adaptive partitioning (AdaPart) sampler for the permanent.
224 *
225 * The partial assignment of the partition is a vector t of length n whose entry
226 * j is the row assigned to column j, or n when column j is still free.
227 */
229 public:
230 /** Draw budgets; 'classic' is the reference default. */
231 enum class Mode { Classic, Time, Sample };
232
233 /**
234 * @param matrix nonnegative square matrix
235 * @param maximum_accepted_samples acceptance budget of Classic
236 * @param maximum_time time budget in milliseconds of Time
237 * @param maximum_samples draw budget of Sample
238 * @param mode which budget applies
239 * @param seed seed of the draws
240 */
241 explicit AdaPartSampler(const Matrix<double>& matrix, int maximum_accepted_samples = 100,
242 double maximum_time = 30000.0, int maximum_samples = 450,
243 Mode mode = Mode::Classic, std::uint64_t seed = 0)
244 : matrix_(matrix),
245 n_(matrix.rows()),
246 maximum_accepted_samples_(maximum_accepted_samples),
247 maximum_time_(maximum_time),
248 maximum_samples_(maximum_samples),
249 mode_(mode),
250 rng_(seed),
251 value_(0.0) {
252 samplingdetail::require_square_nonnegative(matrix_, "AdaPartSampler");
253 if (n_ > 0) samplingdetail::require_full_support(matrix_, "AdaPartSampler");
254 }
255
256 /** Run the sampler in the configured mode and return the estimate. */
257 double solve() {
258 const double z_ub = samplingdetail::soules_bound(matrix_);
259 long accepted = 0, total = 0;
260 const std::clock_t start = std::clock();
261 // Bounded independently of the scaling: with perm(A) = 0 the acceptance
262 // probability is 0 and Classic mode would never terminate. A cap that
263 // RETURNS a number would be a workaround, so it throws.
264 const long kMaxDraws = 1000000;
265 while (true) {
266 if (mode_ == Mode::Classic && accepted >= maximum_accepted_samples_) break;
267 if (mode_ == Mode::Classic && total >= kMaxDraws)
268 throw InputError("perm_adapart: only " + std::to_string(accepted) + " of the " +
269 std::to_string(maximum_accepted_samples_) +
270 " required acceptances were obtained in " +
271 std::to_string(total) +
272 " draws. Use the exact engine.");
273 if (mode_ == Mode::Sample && total >= maximum_samples_) break;
274 if (mode_ == Mode::Time && elapsed_ms(start) >= maximum_time_) break;
275 accepted += sample(start);
276 ++total;
277 }
278 value_ = (total > 0) ? z_ub * static_cast<double>(accepted) / static_cast<double>(total)
279 : 0.0;
280 return value_;
281 }
282
283 /** Estimate of the last solve. */
284 double value() const { return value_; }
285
286 private:
287 typedef std::vector<std::size_t> Assignment;
288
289 static double elapsed_ms(std::clock_t start) {
290 return 1000.0 * static_cast<double>(std::clock() - start) / CLOCKS_PER_SEC;
291 }
292
293 bool within_time(std::clock_t start) const {
294 if (mode_ != Mode::Time) return true;
295 return elapsed_ms(start) < maximum_time_;
296 }
297
298 /** True while some partition element still has an unassigned column. */
299 bool any_free(const std::set<Assignment>& s_set) const {
300 for (std::set<Assignment>::const_iterator it = s_set.begin(); it != s_set.end(); ++it)
301 if (has_free(*it)) return true;
302 return false;
303 }
304
305 bool has_free(const Assignment& t) const {
306 for (std::size_t j = 0; j < t.size(); ++j)
307 if (t[j] == n_) return true;
308 return false;
309 }
310
311 /** Zero out the entries excluded by the partial assignment t. */
312 Matrix<double> modify_matrix(const Matrix<double>& m, const Assignment& t) const {
313 Matrix<double> out(n_, n_, 0.0);
314 std::vector<bool> row_used(n_, false);
315 for (std::size_t j = 0; j < t.size(); ++j)
316 if (t[j] != n_) row_used[t[j]] = true;
317 for (std::size_t j = 0; j < t.size(); ++j) {
318 if (t[j] != n_) {
319 out(t[j], j) = m(t[j], j);
320 } else {
321 for (std::size_t i = 0; i < n_; ++i)
322 if (!row_used[i]) out(i, j) = m(i, j);
323 }
324 }
325 return out;
326 }
327
328 /**
329 * Pick the column whose expansion minimizes the summed Soules bound.
330 *
331 * Only columns still unassigned in s_sub are candidates; see the header.
332 */
333 std::size_t select_column(const Matrix<double>& s_matrix, double removed_ub, double ub,
334 const Assignment& s_sub, double* new_ub) const {
335 double best = std::numeric_limits<double>::infinity();
336 std::size_t best_col = 0;
337 bool found = false;
338 for (std::size_t i = 0; i < n_; ++i) {
339 if (s_sub[i] != n_) continue;
340 double total = 0.0;
341 for (std::size_t j = 0; j < n_; ++j) {
342 Assignment a(n_, n_);
343 a[i] = j;
344 total += samplingdetail::soules_bound(modify_matrix(s_matrix, a));
345 }
346 if (!found || total < best) {
347 best = total;
348 best_col = i;
349 found = true;
350 }
351 }
352 if (!found) {
353 *new_ub = ub;
354 return 0;
355 }
356 *new_ub = ub - removed_ub + best;
357 return best_col;
358 }
359
360 /** Draw a partition element, or the slack index that means rejection. */
361 std::size_t compute_probabilities(const std::set<Assignment>& s_set, double zub_s) {
362 std::vector<double> p;
363 p.reserve(s_set.size() + 1);
364 double sum = 0.0;
365 for (std::set<Assignment>::const_iterator it = s_set.begin(); it != s_set.end(); ++it) {
366 const double b = samplingdetail::soules_bound(modify_matrix(matrix_, *it));
367 p.push_back(b);
368 sum += b;
369 }
370 if (sum > 0.0 && zub_s > 0.0) {
371 double norm = 0.0;
372 for (std::size_t i = 0; i < p.size(); ++i) {
373 p[i] /= zub_s;
374 norm += p[i];
375 }
376 p.push_back(1.0 - norm);
377 } else {
378 p.push_back(1.0);
379 }
380 double total = 0.0;
381 for (std::size_t i = 0; i < p.size(); ++i) total += std::fabs(p[i]);
382 if (total > 0.0)
383 for (std::size_t i = 0; i < p.size(); ++i) p[i] = std::fabs(p[i]) / total;
384
385 const double u = uniform_();
386 double cum = 0.0;
387 for (std::size_t i = 0; i < p.size(); ++i) {
388 cum += p[i];
389 if (u <= cum) return i;
390 }
391 return p.size() - 1;
392 }
393
394 /** Keep the drawn element, completing it when one column is left. */
395 std::set<Assignment> subset(const std::set<Assignment>& s_set, std::size_t c) const {
396 std::set<Assignment>::const_iterator it = s_set.begin();
397 std::advance(it, static_cast<long>(c));
398 Assignment s_inter = *it;
399 std::size_t free_count = 0, free_pos = 0;
400 for (std::size_t j = 0; j < s_inter.size(); ++j)
401 if (s_inter[j] == n_) {
402 ++free_count;
403 free_pos = j;
404 }
405 if (free_count == 1) {
406 std::vector<bool> used(n_, false);
407 for (std::size_t j = 0; j < s_inter.size(); ++j)
408 if (s_inter[j] != n_) used[s_inter[j]] = true;
409 for (std::size_t v = 0; v < n_; ++v)
410 if (!used[v]) {
411 s_inter[free_pos] = v;
412 break;
413 }
414 }
415 std::set<Assignment> out;
416 out.insert(s_inter);
417 return out;
418 }
419
420 /** Draw one partition path, returning 1 if accepted and 0 if rejected. */
421 int sample(std::clock_t start) {
422 std::set<Assignment> s_set;
423 s_set.insert(Assignment(n_, n_));
424 while (any_free(s_set) && within_time(start)) {
425 const Assignment s_init = *s_set.begin();
426 const double zub_s = samplingdetail::soules_bound(modify_matrix(matrix_, s_init));
427 double ub = zub_s;
428 bool init = true;
429 while ((ub >= zub_s || init) && within_time(start)) {
430 init = false;
431 // Only elements with a free position can be refined; expanding a
432 // complete assignment yields no children and stalls the sampler.
433 std::vector<Assignment> s_list;
434 for (std::set<Assignment>::const_iterator it = s_set.begin(); it != s_set.end();
435 ++it)
436 if (has_free(*it)) s_list.push_back(*it);
437 if (s_list.empty()) break;
438 const std::size_t pick =
439 static_cast<std::size_t>(uniform_() * static_cast<double>(s_list.size()));
440 const Assignment s_sub = s_list[pick < s_list.size() ? pick : s_list.size() - 1];
441 s_set.erase(s_sub);
442 const Matrix<double> sub_matrix = modify_matrix(matrix_, s_sub);
443 // Discount the bound of the element actually removed, not the
444 // root bound; see the header.
445 const double sub_ub = samplingdetail::soules_bound(sub_matrix);
446 double new_ub = ub;
447 const std::size_t j = select_column(sub_matrix, sub_ub, ub, s_sub, &new_ub);
448 for (std::size_t i = 0; i < n_; ++i) {
449 bool taken = false;
450 for (std::size_t k = 0; k < s_sub.size(); ++k)
451 if (s_sub[k] == i) taken = true;
452 if (taken) continue;
453 Assignment s_add = s_sub;
454 s_add[j] = i;
455 s_set.insert(s_add);
456 }
457 const bool no_progress = new_ub >= ub;
458 ub = new_ub;
459 // The Soules bound is tight on matrices with equal entries, so
460 // refinement cannot improve it and "refine until improved" would
461 // never exit. Stop on the first non-improving expansion instead;
462 // a tight bound means the draw is accepted with probability 1.
463 if (no_progress) break;
464 }
465 const std::size_t c = compute_probabilities(s_set, zub_s);
466 if (c == s_set.size()) return 0;
467 s_set = subset(s_set, c);
468 }
469 return 1;
470 }
471
472 double uniform_() {
473 return std::generate_canonical<double, 53>(rng_);
474 }
475
476 Matrix<double> matrix_;
477 std::size_t n_;
478 int maximum_accepted_samples_;
479 double maximum_time_;
480 int maximum_samples_;
481 Mode mode_;
482 std::mt19937_64 rng_;
483 double value_;
484};
485
486/**
487 * Huber-Law acceptance-rejection sampler for the permanent.
488 *
489 * The matrix is rescaled to be doubly stochastic, a permutation is drawn column
490 * by column under the Huber-Law upper bound on the remaining permanent, and the
491 * acceptance ratio times the rescaling constant estimates the permanent.
492 */
494 public:
495 enum class Mode { Classic, Time, Sample };
496
497 /**
498 * @param matrix nonnegative square matrix
499 * @param delta relative accuracy target, sets the budget K
500 * @param alpha2 convergence threshold of the rescaling
501 * @param epsilon failure probability target, sets the budget K
502 * @param mode which budget applies
503 * @param number_of_samples draw budget of Sample
504 * @param maximum_time time budget in milliseconds of Time
505 * @param seed seed of the draws
506 */
507 explicit HuberLawSampler(const Matrix<double>& matrix, double delta = 0.1,
508 double alpha2 = 0.000001, double epsilon = 0.1,
509 Mode mode = Mode::Classic, int number_of_samples = 1000,
510 double maximum_time = 30000.0, std::uint64_t seed = 0)
511 : matrix_(matrix),
512 n_(matrix.rows()),
513 delta_(delta),
514 alpha2_(alpha2),
515 epsilon_(epsilon),
516 mode_(mode),
517 number_of_samples_(number_of_samples),
518 maximum_time_(maximum_time),
519 rng_(seed),
520 c_matrix_(matrix.rows(), matrix.cols(), 0.0),
521 rescaling_constant_(1.0),
522 value_(0.0) {
523 samplingdetail::require_square_nonnegative(matrix_, "HuberLawSampler");
524 if (n_ > 0) samplingdetail::require_full_support(matrix_, "HuberLawSampler");
525 }
526
527 /** Run the sampler in the configured mode and return the estimate. */
528 double solve() {
529 rescale();
530 const long k = static_cast<long>(14.0 * std::pow(delta_, -2.0) *
531 std::log(2.0 / epsilon_));
532 const std::clock_t start = std::clock();
533 long accepted = 0, total = 0;
534 // Bounded independently of the scaling, as in the AdaPart sampler.
535 const long kMaxDraws = 1000000;
536 while (true) {
537 if (mode_ == Mode::Classic && accepted >= k) break;
538 if (mode_ == Mode::Classic && total >= kMaxDraws)
539 throw InputError("perm_huberlaw: only " + std::to_string(accepted) + " of the " +
540 std::to_string(k) +
541 " required acceptances were obtained in " +
542 std::to_string(total) +
543 " draws. Relax delta or use the exact engine.");
544 if (mode_ == Mode::Sample && total >= number_of_samples_) break;
545 if (mode_ == Mode::Time && elapsed_ms(start) >= maximum_time_) break;
546 accepted += sample();
547 ++total;
548 }
549 value_ = (total > 0) ? static_cast<double>(accepted) / static_cast<double>(total) *
550 rescaling_constant_
551 : 0.0;
552 return value_;
553 }
554
555 /** Estimate of the last solve. */
556 double value() const { return value_; }
557
558 private:
559 static double elapsed_ms(std::clock_t start) {
560 return 1000.0 * static_cast<double>(std::clock() - start) / CLOCKS_PER_SEC;
561 }
562
563 /** Huber-Law bound factor of a row with remaining mass r. */
564 static double h(double r) {
565 if (r >= 1.0) return r + 0.5 * std::log(r) + M_E - 1.0;
566 return 1.0 + (M_E - 1.0) * r;
567 }
568
569 /** Unnormalized selection weights of each row for column j. */
570 std::vector<double> precomputing(const Matrix<double>& m, std::size_t j) const {
571 std::vector<double> hr(n_, 0.0), c(n_, 0.0);
572 double hr_product = 1.0;
573 for (std::size_t i = 0; i < n_; ++i) {
574 c[i] = m(i, j);
575 double rowsum = 0.0;
576 for (std::size_t k = 0; k < n_; ++k) rowsum += m(i, k);
577 hr[i] = h(rowsum - c[i]);
578 hr_product *= hr[i];
579 }
580 const double exp_factor = std::exp(static_cast<double>(n_) - 1.0);
581 std::vector<double> out(n_, 0.0);
582 for (std::size_t i = 0; i < n_; ++i)
583 out[i] = (hr[i] != 0.0) ? hr_product / hr[i] * c[i] / exp_factor : 0.0;
584 return out;
585 }
586
587 /** Draw one permutation; a rejected draw is reported by the return value. */
588 int sample() {
589 Matrix<double> m = c_matrix_;
590 for (std::size_t j = 0; j < n_; ++j) {
591 std::vector<double> p = precomputing(m, j);
592 double rowprod = 1.0;
593 for (std::size_t i = 0; i < n_; ++i) {
594 double rowsum = 0.0;
595 for (std::size_t k = 0; k < n_; ++k) rowsum += m(i, k);
596 rowprod *= h(rowsum);
597 }
598 const double ub = rowprod / std::exp(static_cast<double>(n_));
599 std::vector<double> prob(n_ + 1, 0.0);
600 double psum = 0.0;
601 for (std::size_t i = 0; i < n_; ++i) {
602 prob[i] = (ub > 0.0) ? p[i] / ub : 0.0;
603 psum += prob[i];
604 }
605 prob[n_] = 1.0 - psum;
606 if (prob[n_] < 0.0) {
607 if (psum > 0.0)
608 for (std::size_t i = 0; i < n_; ++i) prob[i] /= psum;
609 prob[n_] = 0.0;
610 }
611 const double u = uniform_();
612 std::size_t selected = n_;
613 double cum = 0.0;
614 for (std::size_t i = 0; i <= n_; ++i) {
615 cum += prob[i];
616 if (u <= cum) {
617 selected = i;
618 break;
619 }
620 }
621 if (selected == n_) return 0; // rejected
622 Matrix<double> next(n_, n_, 0.0);
623 for (std::size_t a = 0; a < n_; ++a)
624 for (std::size_t b = 0; b < n_; ++b)
625 if (a != selected && b != j) next(a, b) = m(a, b);
626 next(selected, j) = m(selected, j);
627 m = next;
628 }
629 return 1;
630 }
631
632 /** Greedy row-by-row assignment maximizing the cost, as in the JAR. */
633 /** Alternate column and row normalization, filling the scaling diagonals. */
634 Matrix<double> make_doubly_stochastic(const Matrix<double>& m, std::vector<double>* x,
635 std::vector<double>* y) const {
636 Matrix<double> result = m;
637 x->assign(n_, 1.0);
638 y->assign(n_, 1.0);
639 double max_row_error = std::numeric_limits<double>::infinity();
640 double max_col_error = std::numeric_limits<double>::infinity();
641 // Capped: a row that sums to zero leaves max_row_error at 1 forever and
642 // the guarded normalization below skips it, so this loop used to spin
643 // without terminating. A cap that RETURNS is a workaround; this throws.
644 const std::size_t kMaxSweeps = 10000;
645 std::size_t sweeps = 0;
646 while (max_row_error > alpha2_ || max_col_error > alpha2_) {
647 if (++sweeps > kMaxSweeps)
648 throw InputError(
649 "make_doubly_stochastic: did not converge in " +
650 std::to_string(kMaxSweeps) + " sweeps (row error " +
651 std::to_string(max_row_error) + ", column error " +
652 std::to_string(max_col_error) + " against a tolerance of " +
653 std::to_string(alpha2_) +
654 "). The usual cause is a matrix without total support.");
655 for (std::size_t j = 0; j < n_; ++j) {
656 double s = 0.0;
657 for (std::size_t i = 0; i < n_; ++i) s += result(i, j);
658 if (s > 0.0) {
659 for (std::size_t i = 0; i < n_; ++i) result(i, j) /= s;
660 (*y)[j] /= s;
661 }
662 }
663 for (std::size_t i = 0; i < n_; ++i) {
664 double s = 0.0;
665 for (std::size_t j = 0; j < n_; ++j) s += result(i, j);
666 if (s > 0.0) {
667 for (std::size_t j = 0; j < n_; ++j) result(i, j) /= s;
668 (*x)[i] /= s;
669 }
670 }
671 max_col_error = 0.0;
672 max_row_error = 0.0;
673 for (std::size_t j = 0; j < n_; ++j) {
674 double s = 0.0;
675 for (std::size_t i = 0; i < n_; ++i) s += result(i, j);
676 max_col_error = std::max(max_col_error, std::fabs(s - 1.0));
677 }
678 for (std::size_t i = 0; i < n_; ++i) {
679 double s = 0.0;
680 for (std::size_t j = 0; j < n_; ++j) s += result(i, j);
681 max_row_error = std::max(max_row_error, std::fabs(s - 1.0));
682 }
683 }
684 return result;
685 }
686
687 /** Rescale to doubly stochastic and set the scaling constant. */
688 void rescale() {
689 Matrix<double> log_matrix(n_, n_, 0.0);
690 double max_element = 0.0;
691 for (std::size_t i = 0; i < n_; ++i)
692 for (std::size_t j = 0; j < n_; ++j) {
693 // strictly positive here (require_full_support), so no floor
694 log_matrix(i, j) = std::log(matrix_(i, j));
695 max_element = std::max(max_element, matrix_(i, j));
696 }
697 if (!(max_element > 0.0))
698 throw InputError("HuberLawSampler: the matrix is identically zero");
699
700 const std::vector<std::size_t> assignment =
701 samplingdetail::max_weight_assignment(log_matrix);
702
703 Matrix<double> m_scaled(n_, n_, 0.0);
704 for (std::size_t i = 0; i < n_; ++i)
705 for (std::size_t j = 0; j < n_; ++j) m_scaled(i, j) = matrix_(i, j) / max_element;
706
707 // alpha3 is a permanent lower bound of the SCALED matrix, the one floored below
708 double alpha3 = 1.0;
709 for (std::size_t i = 0; i < n_; ++i) alpha3 *= m_scaled(i, assignment[i]);
710 const double alpha1 = alpha3 * delta_ / 3.0 / samplingdetail::factorial_plain(n_);
711 for (std::size_t i = 0; i < n_; ++i)
712 for (std::size_t j = 0; j < n_; ++j)
713 m_scaled(i, j) = std::max(m_scaled(i, j), alpha1);
714
715 std::vector<double> x, y;
716 const Matrix<double> ds = make_doubly_stochastic(m_scaled, &x, &y);
717
718 std::vector<double> z(n_, 1.0);
719 for (std::size_t i = 0; i < n_; ++i) {
720 double rowmax = 0.0;
721 for (std::size_t j = 0; j < n_; ++j) rowmax = std::max(rowmax, ds(i, j));
722 z[i] = (rowmax > 0.0) ? 1.0 / rowmax : 1.0;
723 }
724 for (std::size_t i = 0; i < n_; ++i)
725 for (std::size_t j = 0; j < n_; ++j) c_matrix_(i, j) = z[i] * ds(i, j);
726
727 double h_product = 1.0;
728 for (std::size_t i = 0; i < n_; ++i) {
729 double rowsum = 0.0;
730 for (std::size_t j = 0; j < n_; ++j) rowsum += c_matrix_(i, j);
731 h_product *= h(rowsum) / M_E;
732 }
733 double diagonal_product = 1.0;
734 for (std::size_t i = 0; i < n_; ++i) diagonal_product *= x[i] * y[i] * z[i];
735 rescaling_constant_ = h_product / diagonal_product *
736 std::pow(max_element, static_cast<double>(n_));
737 }
738
739 double uniform_() {
740 return std::generate_canonical<double, 53>(rng_);
741 }
742
743 Matrix<double> matrix_;
744 std::size_t n_;
745 double delta_;
746 double alpha2_;
747 double epsilon_;
748 Mode mode_;
749 int number_of_samples_;
750 double maximum_time_;
751 std::mt19937_64 rng_;
752 Matrix<double> c_matrix_;
753 double rescaling_constant_;
754 double value_;
755};
756
757/**
758 * AdaPart estimate of the permanent, with the reference defaults.
759 *
760 * Twin of MATLAB perm_adapart.m and of python perm/sampling.py.
761 */
762inline double perm_adapart(const Matrix<double>& m, std::uint64_t seed = 0) {
763 if (m.rows() == 0) return 1.0;
764 AdaPartSampler s(m, 100, 30000.0, 450, AdaPartSampler::Mode::Classic, seed);
765 return s.solve();
766}
767
768/**
769 * Huber-Law estimate of the permanent, with the reference defaults.
770 *
771 * Twin of MATLAB perm_huberlaw.m and of python perm/sampling.py.
772 */
773inline double perm_huberlaw(const Matrix<double>& m, std::uint64_t seed = 0) {
774 if (m.rows() == 0) return 1.0;
775 HuberLawSampler s(m, 0.1, 0.000001, 0.1, HuberLawSampler::Mode::Classic, 1000, 30000.0, seed);
776 return s.solve();
777}
778
779} // namespace perm
780} // namespace line
781
782#endif // LINE_API_PERM_PERM_SAMPLING_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
Adaptive partitioning (AdaPart) sampler for the permanent.
Mode
Draw budgets; 'classic' is the reference default.
double value() const
Estimate of the last solve.
double solve()
Run the sampler in the configured mode and return the estimate.
AdaPartSampler(const Matrix< double > &matrix, int maximum_accepted_samples=100, double maximum_time=30000.0, int maximum_samples=450, Mode mode=Mode::Classic, std::uint64_t seed=0)
Huber-Law acceptance-rejection sampler for the permanent.
double solve()
Run the sampler in the configured mode and return the estimate.
HuberLawSampler(const Matrix< double > &matrix, double delta=0.1, double alpha2=0.000001, double epsilon=0.1, Mode mode=Mode::Classic, int number_of_samples=1000, double maximum_time=30000.0, std::uint64_t seed=0)
double value() const
Estimate of the last solve.
The exception types the port throws.
Dense matrix and non-owning view.
double perm_adapart(const Matrix< double > &m, std::uint64_t seed=0)
AdaPart estimate of the permanent, with the reference defaults.
double perm_huberlaw(const Matrix< double > &m, std::uint64_t seed=0)
Huber-Law estimate of the permanent, with the reference defaults.