Skip to content

Commit 68a5261

Browse files
[MLIR][Presburger] Implement function to evaluate the number of terms in a generating function. (#78078)
We implement `computeNumTerms()`, which counts the number of terms in a generating function by substituting the unit vector in it. This is the main function in Barvinok's algorithm – the number of points in a polytope is given by the number of terms in the generating function corresponding to it. We also modify the GeneratingFunction class to have `const` getters and improve the simplification of QuasiPolynomials.
1 parent 5fb39ef commit 68a5261

File tree

9 files changed

+464
-6
lines changed

9 files changed

+464
-6
lines changed

mlir/include/mlir/Analysis/Presburger/Barvinok.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ QuasiPolynomial getCoefficientInRationalFunction(unsigned power,
9999
ArrayRef<QuasiPolynomial> num,
100100
ArrayRef<Fraction> den);
101101

102+
/// Find the number of terms in a generating function, as
103+
/// a quasipolynomial in the parameter space of the input function.
104+
/// The generating function must be such that for all values of the
105+
/// parameters, the number of terms is finite.
106+
QuasiPolynomial computeNumTerms(const GeneratingFunction &gf);
107+
102108
} // namespace detail
103109
} // namespace presburger
104110
} // namespace mlir

mlir/include/mlir/Analysis/Presburger/GeneratingFunction.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ class GeneratingFunction {
6262
#endif // NDEBUG
6363
}
6464

65-
unsigned getNumParams() { return numParam; }
65+
unsigned getNumParams() const { return numParam; }
6666

67-
SmallVector<int> getSigns() { return signs; }
67+
SmallVector<int> getSigns() const { return signs; }
6868

69-
std::vector<ParamPoint> getNumerators() { return numerators; }
69+
std::vector<ParamPoint> getNumerators() const { return numerators; }
7070

71-
std::vector<std::vector<Point>> getDenominators() { return denominators; }
71+
std::vector<std::vector<Point>> getDenominators() const {
72+
return denominators;
73+
}
7274

7375
GeneratingFunction operator+(GeneratingFunction &gf) const {
7476
assert(numParam == gf.getNumParams() &&

mlir/include/mlir/Analysis/Presburger/QuasiPolynomial.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ class QuasiPolynomial : public PresburgerSpace {
5959
QuasiPolynomial operator*(const QuasiPolynomial &x) const;
6060
QuasiPolynomial operator/(const Fraction x) const;
6161

62-
// Removes terms which evaluate to zero from the expression.
62+
// Removes terms which evaluate to zero from the expression
63+
// and folds affine functions which are constant into the
64+
// constant coefficients.
6365
QuasiPolynomial simplify();
6466

67+
// Group together like terms in the expression.
68+
QuasiPolynomial collectTerms();
69+
6570
Fraction getConstantTerm();
6671

6772
private:

mlir/include/mlir/Analysis/Presburger/Utils.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ SmallVector<MPInt, 8> getComplementIneq(ArrayRef<MPInt> ineq);
281281
/// The vectors must have the same sizes.
282282
Fraction dotProduct(ArrayRef<Fraction> a, ArrayRef<Fraction> b);
283283

284+
/// Find the product of two polynomials, each given by an array of
285+
/// coefficients.
286+
std::vector<Fraction> multiplyPolynomials(ArrayRef<Fraction> a,
287+
ArrayRef<Fraction> b);
288+
284289
} // namespace presburger
285290
} // namespace mlir
286291

mlir/lib/Analysis/Presburger/Barvinok.cpp

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//===----------------------------------------------------------------------===//
88

99
#include "mlir/Analysis/Presburger/Barvinok.h"
10+
#include "mlir/Analysis/Presburger/Utils.h"
1011
#include "llvm/ADT/Sequence.h"
1112
#include <algorithm>
1213

@@ -245,3 +246,241 @@ QuasiPolynomial mlir::presburger::detail::getCoefficientInRationalFunction(
245246
}
246247
return coefficients[power].simplify();
247248
}
249+
250+
/// Substitute x_i = t^μ_i in one term of a generating function, returning
251+
/// a quasipolynomial which represents the exponent of the numerator
252+
/// of the result, and a vector which represents the exponents of the
253+
/// denominator of the result.
254+
/// If the returned value is {num, dens}, it represents the function
255+
/// t^num / \prod_j (1 - t^dens[j]).
256+
/// v represents the affine functions whose floors are multiplied by the
257+
/// generators, and ds represents the list of generators.
258+
std::pair<QuasiPolynomial, std::vector<Fraction>>
259+
substituteMuInTerm(unsigned numParams, ParamPoint v, std::vector<Point> ds,
260+
Point mu) {
261+
unsigned numDims = mu.size();
262+
for (const Point &d : ds)
263+
assert(d.size() == numDims &&
264+
"μ has to have the same number of dimensions as the generators!");
265+
266+
// First, the exponent in the numerator becomes
267+
// - (μ • u_1) * (floor(first col of v))
268+
// - (μ • u_2) * (floor(second col of v)) - ...
269+
// - (μ • u_d) * (floor(d'th col of v))
270+
// So we store the negation of the dot products.
271+
272+
// We have d terms, each of whose coefficient is the negative dot product.
273+
SmallVector<Fraction> coefficients;
274+
coefficients.reserve(numDims);
275+
for (const Point &d : ds)
276+
coefficients.push_back(-dotProduct(mu, d));
277+
278+
// Then, the affine function is a single floor expression, given by the
279+
// corresponding column of v.
280+
ParamPoint vTranspose = v.transpose();
281+
std::vector<std::vector<SmallVector<Fraction>>> affine;
282+
affine.reserve(numDims);
283+
for (unsigned j = 0; j < numDims; ++j)
284+
affine.push_back({SmallVector<Fraction>(vTranspose.getRow(j))});
285+
286+
QuasiPolynomial num(numParams, coefficients, affine);
287+
num = num.simplify();
288+
289+
std::vector<Fraction> dens;
290+
dens.reserve(ds.size());
291+
// Similarly, each term in the denominator has exponent
292+
// given by the dot product of μ with u_i.
293+
for (const Point &d : ds) {
294+
// This term in the denominator is
295+
// (1 - t^dens.back())
296+
dens.push_back(dotProduct(d, mu));
297+
}
298+
299+
return {num, dens};
300+
}
301+
302+
/// Normalize all denominator exponents `dens` to their absolute values
303+
/// by multiplying and dividing by the inverses, in a function of the form
304+
/// sign * t^num / prod_j (1 - t^dens[j]).
305+
/// Here, sign = ± 1,
306+
/// num is a QuasiPolynomial, and
307+
/// each dens[j] is a Fraction.
308+
void normalizeDenominatorExponents(int &sign, QuasiPolynomial &num,
309+
std::vector<Fraction> &dens) {
310+
// We track the number of exponents that are negative in the
311+
// denominator, and convert them to their absolute values.
312+
unsigned numNegExps = 0;
313+
Fraction sumNegExps(0, 1);
314+
for (unsigned j = 0, e = dens.size(); j < e; ++j) {
315+
if (dens[j] < 0) {
316+
numNegExps += 1;
317+
sumNegExps += dens[j];
318+
}
319+
}
320+
321+
// If we have (1 - t^-c) in the denominator, for positive c,
322+
// multiply and divide by t^c.
323+
// We convert all negative-exponent terms at once; therefore
324+
// we multiply and divide by t^sumNegExps.
325+
// Then we get
326+
// -(1 - t^c) in the denominator,
327+
// increase the numerator by c, and
328+
// flip the sign of the function.
329+
if (numNegExps % 2 == 1)
330+
sign = -sign;
331+
num = num - QuasiPolynomial(num.getNumInputs(), sumNegExps);
332+
}
333+
334+
/// Compute the binomial coefficients nCi for 0 ≤ i ≤ r,
335+
/// where n is a QuasiPolynomial.
336+
std::vector<QuasiPolynomial> getBinomialCoefficients(QuasiPolynomial n,
337+
unsigned r) {
338+
unsigned numParams = n.getNumInputs();
339+
std::vector<QuasiPolynomial> coefficients;
340+
coefficients.reserve(r + 1);
341+
coefficients.push_back(QuasiPolynomial(numParams, 1));
342+
for (unsigned j = 1; j <= r; ++j)
343+
// We use the recursive formula for binomial coefficients here and below.
344+
coefficients.push_back(
345+
(coefficients[j - 1] * (n - QuasiPolynomial(numParams, j - 1)) /
346+
Fraction(j, 1))
347+
.simplify());
348+
return coefficients;
349+
}
350+
351+
/// Compute the binomial coefficients nCi for 0 ≤ i ≤ r,
352+
/// where n is a QuasiPolynomial.
353+
std::vector<Fraction> getBinomialCoefficients(Fraction n, Fraction r) {
354+
std::vector<Fraction> coefficients;
355+
coefficients.reserve((int64_t)floor(r));
356+
coefficients.push_back(1);
357+
for (unsigned j = 1; j <= r; ++j)
358+
coefficients.push_back(coefficients[j - 1] * (n - (j - 1)) / (j));
359+
return coefficients;
360+
}
361+
362+
/// We have a generating function of the form
363+
/// f_p(x) = \sum_i sign_i * (x^n_i(p)) / (\prod_j (1 - x^d_{ij})
364+
///
365+
/// where sign_i is ±1,
366+
/// n_i \in Q^p -> Q^d is the sum of the vectors d_{ij}, weighted by the
367+
/// floors of d affine functions on p parameters.
368+
/// d_{ij} \in Q^d are vectors.
369+
///
370+
/// We need to find the number of terms of the form x^t in the expansion of
371+
/// this function.
372+
/// However, direct substitution (x = (1, ..., 1)) causes the denominator
373+
/// to become zero.
374+
///
375+
/// We therefore use the following procedure instead:
376+
/// 1. Substitute x_i = (s+1)^μ_i for some vector μ. This makes the generating
377+
/// function a function of a scalar s.
378+
/// 2. Write each term in this function as P(s)/Q(s), where P and Q are
379+
/// polynomials. P has coefficients as quasipolynomials in d parameters, while
380+
/// Q has coefficients as scalars.
381+
/// 3. Find the constant term in the expansion of each term P(s)/Q(s). This is
382+
/// equivalent to substituting s = 0.
383+
///
384+
/// Verdoolaege, Sven, et al. "Counting integer points in parametric
385+
/// polytopes using Barvinok's rational functions." Algorithmica 48 (2007):
386+
/// 37-66.
387+
QuasiPolynomial
388+
mlir::presburger::detail::computeNumTerms(const GeneratingFunction &gf) {
389+
// Step (1) We need to find a μ such that we can substitute x_i =
390+
// (s+1)^μ_i. After this substitution, the exponent of (s+1) in the
391+
// denominator is (μ_i • d_{ij}) in each term. Clearly, this cannot become
392+
// zero. Hence we find a vector μ that is not orthogonal to any of the
393+
// d_{ij} and substitute x accordingly.
394+
std::vector<Point> allDenominators;
395+
for (ArrayRef<Point> den : gf.getDenominators())
396+
allDenominators.insert(allDenominators.end(), den.begin(), den.end());
397+
Point mu = getNonOrthogonalVector(allDenominators);
398+
399+
unsigned numParams = gf.getNumParams();
400+
const std::vector<std::vector<Point>> &ds = gf.getDenominators();
401+
QuasiPolynomial totalTerm(numParams, 0);
402+
for (unsigned i = 0, e = ds.size(); i < e; ++i) {
403+
int sign = gf.getSigns()[i];
404+
405+
// Compute the new exponents of (s+1) for the numerator and the
406+
// denominator after substituting μ.
407+
auto [numExp, dens] =
408+
substituteMuInTerm(numParams, gf.getNumerators()[i], ds[i], mu);
409+
// Now the numerator is (s+1)^numExp
410+
// and the denominator is \prod_j (1 - (s+1)^dens[j]).
411+
412+
// Step (2) We need to express the terms in the function as quotients of
413+
// polynomials. Each term is now of the form
414+
// sign_i * (s+1)^numExp / (\prod_j (1 - (s+1)^dens[j]))
415+
// For the i'th term, we first normalize the denominator to have only
416+
// positive exponents. We convert all the dens[j] to their
417+
// absolute values and change the sign and exponent in the numerator.
418+
normalizeDenominatorExponents(sign, numExp, dens);
419+
420+
// Then, using the formula for geometric series, we replace each (1 -
421+
// (s+1)^(dens[j])) with
422+
// (-s)(\sum_{0 ≤ k < dens[j]} (s+1)^k).
423+
for (unsigned j = 0, e = dens.size(); j < e; ++j)
424+
dens[j] = abs(dens[j]) - 1;
425+
// Note that at this point, the semantics of `dens[j]` changes to mean
426+
// a term (\sum_{0 ≤ k ≤ dens[j]} (s+1)^k). The denominator is, as before,
427+
// a product of these terms.
428+
429+
// Since the -s are taken out, the sign changes if there is an odd number
430+
// of such terms.
431+
unsigned r = dens.size();
432+
if (dens.size() % 2 == 1)
433+
sign = -sign;
434+
435+
// Thus the term overall now has the form
436+
// sign'_i * (s+1)^numExp /
437+
// (s^r * \prod_j (\sum_{0 ≤ k < dens[j]} (s+1)^k)).
438+
// This means that
439+
// the numerator is a polynomial in s, with coefficients as
440+
// quasipolynomials (given by binomial coefficients), and the denominator
441+
// is a polynomial in s, with integral coefficients (given by taking the
442+
// convolution over all j).
443+
444+
// Step (3) We need to find the constant term in the expansion of each
445+
// term. Since each term has s^r as a factor in the denominator, we avoid
446+
// substituting s = 0 directly; instead, we find the coefficient of s^r in
447+
// sign'_i * (s+1)^numExp / (\prod_j (\sum_k (s+1)^k)),
448+
// Letting P(s) = (s+1)^numExp and Q(s) = \prod_j (...),
449+
// we need to find the coefficient of s^r in P(s)/Q(s),
450+
// for which we use the `getCoefficientInRationalFunction()` function.
451+
452+
// First, we compute the coefficients of P(s), which are binomial
453+
// coefficients.
454+
// We only need the first r+1 of these, as higher-order terms do not
455+
// contribute to the coefficient of s^r.
456+
std::vector<QuasiPolynomial> numeratorCoefficients =
457+
getBinomialCoefficients(numExp, r);
458+
459+
// Then we compute the coefficients of each individual term in Q(s),
460+
// which are (dens[i]+1) C (k+1) for 0 ≤ k ≤ dens[i].
461+
std::vector<std::vector<Fraction>> eachTermDenCoefficients;
462+
std::vector<Fraction> singleTermDenCoefficients;
463+
eachTermDenCoefficients.reserve(r);
464+
for (const Fraction &den : dens) {
465+
singleTermDenCoefficients = getBinomialCoefficients(den + 1, den + 1);
466+
eachTermDenCoefficients.push_back(
467+
ArrayRef<Fraction>(singleTermDenCoefficients).slice(1));
468+
}
469+
470+
// Now we find the coefficients in Q(s) itself
471+
// by taking the convolution of the coefficients
472+
// of all the terms.
473+
std::vector<Fraction> denominatorCoefficients;
474+
denominatorCoefficients = eachTermDenCoefficients[0];
475+
for (unsigned j = 1, e = eachTermDenCoefficients.size(); j < e; ++j)
476+
denominatorCoefficients = multiplyPolynomials(denominatorCoefficients,
477+
eachTermDenCoefficients[j]);
478+
479+
totalTerm =
480+
totalTerm + getCoefficientInRationalFunction(r, numeratorCoefficients,
481+
denominatorCoefficients) *
482+
QuasiPolynomial(numParams, sign);
483+
}
484+
485+
return totalTerm.simplify();
486+
}

mlir/lib/Analysis/Presburger/QuasiPolynomial.cpp

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,18 @@ QuasiPolynomial QuasiPolynomial::operator/(const Fraction x) const {
9797
return qp;
9898
}
9999

100-
// Removes terms which evaluate to zero from the expression.
100+
// Removes terms which evaluate to zero from the expression and
101+
// integrate affine functions which are constants into the
102+
// coefficients.
101103
QuasiPolynomial QuasiPolynomial::simplify() {
104+
Fraction newCoeff = 0;
102105
SmallVector<Fraction> newCoeffs({});
106+
107+
std::vector<SmallVector<Fraction>> newAffineTerm({});
103108
std::vector<std::vector<SmallVector<Fraction>>> newAffine({});
109+
110+
unsigned numParam = getNumInputs();
111+
104112
for (unsigned i = 0, e = coefficients.size(); i < e; i++) {
105113
// A term is zero if its coefficient is zero, or
106114
if (coefficients[i] == Fraction(0, 1))
@@ -114,9 +122,46 @@ QuasiPolynomial QuasiPolynomial::simplify() {
114122
});
115123
if (product_is_zero)
116124
continue;
125+
126+
// Now, we know the term is nonzero.
127+
128+
// We now eliminate the affine functions which are constant
129+
// by merging them into the coefficients.
130+
newAffineTerm = {};
131+
newCoeff = coefficients[i];
132+
for (ArrayRef<Fraction> term : affine[i]) {
133+
bool allCoeffsZero = llvm::all_of(
134+
term.slice(0, numParam), [](const Fraction c) { return c == 0; });
135+
if (allCoeffsZero)
136+
newCoeff *= term[numParam];
137+
else
138+
newAffineTerm.push_back(SmallVector<Fraction>(term));
139+
}
140+
141+
newCoeffs.push_back(newCoeff);
142+
newAffine.push_back(newAffineTerm);
143+
}
144+
return QuasiPolynomial(getNumInputs(), newCoeffs, newAffine);
145+
}
146+
147+
QuasiPolynomial QuasiPolynomial::collectTerms() {
148+
SmallVector<Fraction> newCoeffs({});
149+
std::vector<std::vector<SmallVector<Fraction>>> newAffine({});
150+
151+
for (unsigned i = 0, e = affine.size(); i < e; i++) {
152+
bool alreadyPresent = false;
153+
for (unsigned j = 0, f = newAffine.size(); j < f; j++) {
154+
if (affine[i] == newAffine[j]) {
155+
newCoeffs[j] += coefficients[i];
156+
alreadyPresent = true;
157+
}
158+
}
159+
if (alreadyPresent)
160+
continue;
117161
newCoeffs.push_back(coefficients[i]);
118162
newAffine.push_back(affine[i]);
119163
}
164+
120165
return QuasiPolynomial(getNumInputs(), newCoeffs, newAffine);
121166
}
122167

0 commit comments

Comments
 (0)