LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
permanent.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_PERMANENT_H
6#define LINE_API_PERM_PERMANENT_H
7
8/**
9 * @file
10 * @ingroup api_perm
11 * The PERMANENT of a matrix, exactly, by four algorithms.
12 *
13 * Port of python/line_solver/api/perm/exact.py. PYTHON-ONLY: no MATLAB or JAR
14 * twin, so native Python is the reference.
15 *
16 * WHY A QUEUEING LIBRARY WANTS PERMANENTS. The normalizing constant of a closed
17 * multiclass network with distinguishable jobs is a permanent of the demand
18 * matrix with COLUMN MULTIPLICITIES given by the class populations -- see
19 * `api/pfqn/pfqn_lcfsqn_nc.h`, which already relies on that identity. The
20 * permanent looks like a determinant with every sign made positive, and that
21 * single change removes the multilinear cancellation Gaussian elimination
22 * lives on, which is why there is no polynomial algorithm and why four of them
23 * are carried here rather than one.
24 *
25 * THE FOUR, and when each is the right choice:
26 *
27 * - MULTIPLICITY: inclusion-exclusion over the DISTINCT columns, weighted by
28 * binomial coefficients. Its cost is set by the number of distinct columns,
29 * not by n, so on the matrices this library actually forms -- a station's
30 * demand repeated once per job of a class -- it is the only tractable one.
31 * This is the default for exactly that reason.
32 * - RYSER: the textbook 2^n subset sum, O(2^n n^2). No structure exploited,
33 * and the one to compare the others against.
34 * - RYSER-GRAY: the same sum walked in GRAY-CODE order, so consecutive subsets
35 * differ in one column and the row sums update in O(n) instead of O(n^2).
36 * Same value, one factor of n cheaper.
37 * - NAIVE: every permutation, n!. Unusable past about ten, and kept because it
38 * is the definition -- it is what the others are verified against.
39 *
40 * ALL FOUR RETURN THE SAME NUMBER, and the tests assert exactly that on random
41 * matrices. A permanent has no cheap independent check, so agreement between an
42 * O(n!) definition and an O(2^n) formula IS the verification.
43 *
44 * ARITHMETIC: field. Ryser's alternating sum cancels heavily, so on a large
45 * matrix the exact instantiation is not a luxury.
46 */
47
48#include <algorithm>
49#include <cmath>
50#include <cstddef>
51#include <vector>
52
53#include "line/num/number.h"
54#include "line/util/error.h"
55#include "line/util/matrix.h"
56
57namespace line {
58namespace perm {
59
60/** Which algorithm `permanent` should use. */
62
63namespace permdetail {
64
65/** Binomial coefficient, exactly, at the sizes a permanent reaches. */
66template <class T>
67T binom(std::size_t n, std::size_t k) {
68 if (k > n) return num_traits<T>::from_int(0);
70 for (std::size_t i = 0; i < k; ++i)
71 v = T(v * num_traits<T>::from_int(static_cast<long>(n - i)) /
72 num_traits<T>::from_int(static_cast<long>(i + 1)));
73 return v;
74}
75
76/** The distinct columns of `m`, and how many times each occurs. */
77template <class T>
78void unique_columns(const Matrix<T>& m, Matrix<T>* uniq, std::vector<std::size_t>* mult) {
79 const std::size_t n = m.rows(), c = m.cols();
80 std::vector<std::size_t> rep; // representative column of each group
81 mult->clear();
82 for (std::size_t j = 0; j < c; ++j) {
83 bool found = false;
84 for (std::size_t g = 0; g < rep.size() && !found; ++g) {
85 bool same = true;
86 for (std::size_t i = 0; i < n && same; ++i)
87 if (m(i, j) != m(i, rep[g])) same = false;
88 if (same) {
89 ++(*mult)[g];
90 found = true;
91 }
92 }
93 if (!found) {
94 rep.push_back(j);
95 mult->push_back(1);
96 }
97 }
98 *uniq = Matrix<T>(n, rep.size(), num_traits<T>::from_int(0));
99 for (std::size_t g = 0; g < rep.size(); ++g)
100 for (std::size_t i = 0; i < n; ++i) (*uniq)(i, g) = m(i, rep[g]);
101}
102
103/** The next vector of the box product 0 <= f_k <= mult_k, or false at the end. */
104inline bool pprod_next(std::vector<std::size_t>& f, const std::vector<std::size_t>& mult) {
105 for (std::size_t k = 0; k < f.size(); ++k) {
106 if (f[k] < mult[k]) {
107 ++f[k];
108 return true;
109 }
110 f[k] = 0;
111 }
112 return false;
113}
114
115} // namespace permdetail
116
117/**
118 * Inclusion-exclusion over the DISTINCT columns.
119 *
120 * With R distinct columns of multiplicities m_1..m_R the sum runs over the box
121 * `0 <= f_k <= m_k` rather than over 2^n subsets, so a matrix whose columns
122 * repeat -- which is what a class population produces -- costs
123 * `prod (m_k + 1)` terms instead of `2^(sum m_k)`.
124 */
125template <class T>
127 const std::size_t n = m.rows();
128 if (n == 0) return num_traits<T>::from_int(1);
129 if (m.cols() != n) throw InputError("permanent: the matrix must be square");
130
131 Matrix<T> uniq;
132 std::vector<std::size_t> mult;
133 permdetail::unique_columns(m, &uniq, &mult);
134 const std::size_t R = mult.size();
135
136 T value = num_traits<T>::from_int(0);
137 std::vector<std::size_t> f(R, 0);
138 do {
139 std::size_t fsum = 0;
140 for (std::size_t k = 0; k < R; ++k) fsum += f[k];
141 T term = num_traits<T>::from_int((fsum % 2 == 0) ? 1 : -1);
142 for (std::size_t j = 0; j < R; ++j) term *= permdetail::binom<T>(mult[j], f[j]);
143 for (std::size_t i = 0; i < n; ++i) {
145 for (std::size_t k = 0; k < R; ++k)
146 s += num_traits<T>::from_int(static_cast<long>(f[k])) * uniq(i, k);
147 term *= s;
148 }
149 value += term;
150 } while (permdetail::pprod_next(f, mult));
151 return T(num_traits<T>::from_int((n % 2 == 0) ? 1 : -1) * value);
152}
153
154/** Ryser's formula over explicit column subsets: O(2^n n^2). */
155template <class T>
157 const std::size_t n = m.rows();
158 if (n == 0) return num_traits<T>::from_int(1);
159 if (m.cols() != n) throw InputError("permanent: the matrix must be square");
160 if (n > 30) throw InputError("permanent_ryser: 2^n subsets is not enumerable past n = 30");
161
162 T total = num_traits<T>::from_int(0);
163 const unsigned long long lim = 1ULL << n;
164 for (unsigned long long sub = 0; sub < lim; ++sub) {
165 std::size_t bits = 0;
166 for (std::size_t j = 0; j < n; ++j)
167 if (sub & (1ULL << j)) ++bits;
168 T prod = num_traits<T>::from_int(1);
169 for (std::size_t i = 0; i < n; ++i) {
170 T rs = num_traits<T>::from_int(0);
171 for (std::size_t j = 0; j < n; ++j)
172 if (sub & (1ULL << j)) rs += m(i, j);
173 prod *= rs;
174 }
175 total += num_traits<T>::from_int(((n - bits) % 2 == 0) ? 1 : -1) * prod;
176 }
177 return total;
178}
179
180/**
181 * Ryser's formula in GRAY-CODE order: O(2^n n).
182 *
183 * Consecutive subsets differ in exactly one column, so the row sums are updated
184 * rather than recomputed. That is where the factor of n goes.
185 */
186template <class T>
188 const std::size_t n = m.rows();
189 if (n == 0) return num_traits<T>::from_int(1);
190 if (m.cols() != n) throw InputError("permanent: the matrix must be square");
191 if (n > 30) throw InputError("permanent_ryser_gray: 2^n steps is not enumerable past n = 30");
192
193 std::vector<T> rowsum(n, num_traits<T>::from_int(0));
194 std::vector<char> on(n, 0);
195 T value = num_traits<T>::from_int(0);
196 std::size_t bits = 0;
197
198 const unsigned long long steps = (1ULL << n) - 1ULL;
199 for (unsigned long long s = 1; s <= steps; ++s) {
200 // The bit that changes between Gray codes s-1 and s is the index of the
201 // lowest set bit of s.
202 std::size_t j = 0;
203 unsigned long long t = s;
204 while ((t & 1ULL) == 0ULL) {
205 t >>= 1;
206 ++j;
207 }
208 on[j] = !on[j];
209 const T sign = num_traits<T>::from_int(on[j] ? 1 : -1);
210 for (std::size_t i = 0; i < n; ++i) rowsum[i] += sign * m(i, j);
211 bits = on[j] ? bits + 1 : bits - 1;
212
213 T prod = num_traits<T>::from_int(1);
214 for (std::size_t i = 0; i < n; ++i) prod *= rowsum[i];
215 value += num_traits<T>::from_int((bits % 2 == 0) ? 1 : -1) * prod;
216 }
217 return T(num_traits<T>::from_int((n % 2 == 0) ? 1 : -1) * value);
218}
219
220/** Every permutation: n!. The definition, and what the rest are checked against. */
221template <class T>
223 const std::size_t n = m.rows();
224 if (n == 0) return num_traits<T>::from_int(1);
225 if (m.cols() != n) throw InputError("permanent: the matrix must be square");
226 if (n > 12) throw InputError("permanent_naive: n! is not enumerable past n = 12");
227
228 std::vector<std::size_t> p(n);
229 for (std::size_t i = 0; i < n; ++i) p[i] = i;
230 T value = num_traits<T>::from_int(0);
231 do {
232 T prod = num_traits<T>::from_int(1);
233 for (std::size_t i = 0; i < n; ++i) prod *= m(i, p[i]);
234 value += prod;
235 } while (std::next_permutation(p.begin(), p.end()));
236 return value;
237}
238
239/**
240 * The permanent, by the chosen method.
241 *
242 * The default exploits repeated columns, which is the case this library
243 * generates: a class of N jobs contributes N identical columns.
244 */
245template <class T>
247 switch (method) {
249 case PermMethod::Ryser: return permanent_ryser(m);
251 case PermMethod::Naive: return permanent_naive(m);
252 }
253 throw InputError("permanent: unknown method");
254}
255
256/**
257 * Round a matrix's entries onto a coarse lattice so repeated columns are found.
258 *
259 * NOT the reference's `preprocessing_ds`, despite what this comment used to
260 * claim: that one is the Sinkhorn scaling to double stochasticity (python
261 * `preprocessing_ds`, JAR `QueueingNetwork.preprocessingDS`) and returns a
262 * rescaling factor alongside the matrix. This is an unrelated operation that
263 * merely shared the name. The multiplicity algorithm keys on EXACT
264 * column equality, so two demands that differ in the last bits are two distinct
265 * columns and the saving is lost; snapping to a tolerance recovers it. This is
266 * a deliberate perturbation of the input, not a numerical tidy-up, so it is a
267 * separate call rather than something `permanent` does silently.
268 */
269template <class T>
270Matrix<T> snap_to_lattice(const Matrix<T>& m, double tolerance = 0.001) {
271 if (!(tolerance > 0.0)) throw InputError("snap_to_lattice: the tolerance must be positive");
272 Matrix<T> out(m.rows(), m.cols(), num_traits<T>::from_int(0));
273 for (std::size_t i = 0; i < m.rows(); ++i)
274 for (std::size_t j = 0; j < m.cols(); ++j) {
275 const double v = num_traits<T>::to_double(m(i, j));
276 out(i, j) = num_traits<T>::from_double(std::round(v / tolerance) * tolerance);
277 }
278 return out;
279}
280
281} // namespace perm
282} // namespace line
283
284#endif // LINE_API_PERM_PERMANENT_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.
T permanent_naive(const Matrix< T > &m)
Every permutation: n!
Definition permanent.h:222
T permanent_ryser_gray(const Matrix< T > &m)
Ryser's formula in GRAY-CODE order: O(2^n n).
Definition permanent.h:187
T permanent_multiplicity(const Matrix< T > &m)
Inclusion-exclusion over the DISTINCT columns.
Definition permanent.h:126
Matrix< T > snap_to_lattice(const Matrix< T > &m, double tolerance=0.001)
Round a matrix's entries onto a coarse lattice so repeated columns are found.
Definition permanent.h:270
PermMethod
Which algorithm permanent should use.
Definition permanent.h:61
T permanent_ryser(const Matrix< T > &m)
Ryser's formula over explicit column subsets: O(2^n n^2).
Definition permanent.h:156
T permanent(const Matrix< T > &m, PermMethod method=PermMethod::Multiplicity)
The permanent, by the chosen method.
Definition permanent.h:246
Number-type abstraction for the templated API port.