Skip to content

Commit f096773

Browse files
authored
feat(ratf): Added rational functions support and implemented Pade approximation (#66)
1. New features: - Created file `ALFI/ALFI/ratf.h` with `alfi::ratf` namespace for rational functions. - Declared `RationalFunction` type as an alias for `std::pair<Container<Number>, Container<Number>>`. - Added `val`, `val_mul`, and `val_div` functions for scalars and vectors. - Implemented `pade` function for computing Pade approximant about $x = 0$ using extended Euclid algorithm. 2. New utility functions: - Created file `ALFI/ALFI/util/poly.h` with utility functions for polynomial operations. - Implemented `normalize`, `mul`, and `div` functions in `alfi::util::poly`. - Added `alfi::util::numeric::binpow` function for binary exponentiation. 3. Added documentation for all new functions and namespaces. 4. Implemented basic tests and benchmarks for rational functions and Pade approximation.
1 parent 760916b commit f096773

File tree

10 files changed

+543
-1
lines changed

10 files changed

+543
-1
lines changed

.idea/runConfigurations/bench_ratf.xml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ALFI/ALFI.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
#include "ALFI/dist.h"
66
#include "ALFI/misc.h"
77
#include "ALFI/poly.h"
8+
#include "ALFI/ratf.h"
89
#include "ALFI/spline.h"

ALFI/ALFI/ratf.h

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#pragma once
2+
3+
#include <cmath>
4+
5+
#include "config.h"
6+
#include "util/numeric.h"
7+
#include "util/poly.h"
8+
9+
/**
10+
@namespace alfi::ratf
11+
@brief Namespace providing support for rational functions.
12+
13+
This namespace provides types and functions for representing, computing and evaluating rational functions of the form
14+
\f[
15+
f(x) = \frac{A(x)}{B(x)}
16+
\f]
17+
where \f(A(x)\f) and \f(B(x)\f) are polynomials.
18+
*/
19+
namespace alfi::ratf {
20+
/**
21+
@brief Represents a rational function \f(\displaystyle f(x) = \frac{A(x)}{B(x)}\f), where \f(A(x)\f) and \f(B(x)\f) are polynomials.
22+
23+
A pair (`std::pair`) of polynomials `{.first as numerator, .second as denominator}` stored as containers of coefficients in descending degree order.
24+
*/
25+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
26+
using RationalFunction = std::pair<Container<Number>, Container<Number>>;
27+
28+
/**
29+
@brief Evaluates the rational function at a scalar point using Horner's method.
30+
31+
Computes \f(f(x) = \frac{A(x)}{B(x)}\f) by evaluating the values of the numerator \f(A\f) and the denominator \f(B\f)
32+
using Horner's method, and then dividing the results.
33+
34+
@ref val utilizes this function for \f(|x| \leq 1\f).
35+
*/
36+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
37+
std::enable_if_t<!traits::has_size<Number>::value, Number>
38+
val_mul(const RationalFunction<Number, Container>& rf, Number x) {
39+
Number n = 0;
40+
for (const auto& c : rf.first) {
41+
n = n * x + c;
42+
}
43+
Number d = 0;
44+
for (const auto& c : rf.second) {
45+
d = d * x + c;
46+
}
47+
return n / d;
48+
}
49+
50+
/**
51+
@brief Evaluates the rational function at each point in the container using @ref val_mul for scalar values.
52+
*/
53+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
54+
std::enable_if_t<traits::has_size<Container<Number>>::value, Container<Number>>
55+
val_mul(const RationalFunction<Number, Container>& rf, const Container<Number>& xx) {
56+
Container<Number> result(xx.size());
57+
#if defined(_OPENMP) && !defined(ALFI_DISABLE_OPENMP)
58+
#pragma omp parallel for
59+
#endif
60+
for (SizeT i = 0; i < xx.size(); ++i) {
61+
result[i] = val_mul(rf, xx[i]);
62+
}
63+
return result;
64+
}
65+
66+
/**
67+
@brief Evaluates the rational function at a scalar point by factoring out powers of x.
68+
69+
Computes \f(f(x) = \frac{A(x)}{B(x)}\f) by evaluating both numerator and denominator in reverse order,
70+
effectively factoring out the dominant power of \f(x\f) to improve numerical stability for large \f(|x|\f).
71+
72+
@ref val utilizes this function for \f(|x| > 1\f).
73+
*/
74+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
75+
std::enable_if_t<!traits::has_size<Number>::value, Number>
76+
val_div(const RationalFunction<Number, Container>& rf, Number x) {
77+
const auto& numerator = rf.first;
78+
const auto& denominator = rf.second;
79+
Number n = 0;
80+
for (auto i = numerator.rbegin(); i != numerator.rend(); ++i) {
81+
n = n / x + *i;
82+
}
83+
Number d = 0;
84+
for (auto i = denominator.rbegin(); i != denominator.rend(); ++i) {
85+
d = d / x + *i;
86+
}
87+
const auto numerator_degree = numerator.empty() ? 0 : numerator.size() - 1;
88+
const auto denominator_degree = denominator.empty() ? 0 : denominator.size() - 1;
89+
if (numerator_degree >= denominator_degree) {
90+
return n / d * util::numeric::binpow(x, numerator_degree - denominator_degree);
91+
} else {
92+
return n / d / util::numeric::binpow(x, denominator_degree - numerator_degree);
93+
}
94+
}
95+
96+
/**
97+
@brief Evaluates the rational function at each point in the container using @ref val_div for scalar values.
98+
*/
99+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
100+
std::enable_if_t<traits::has_size<Container<Number>>::value, Container<Number>>
101+
val_div(const RationalFunction<Number, Container>& rf, const Container<Number>& xx) {
102+
Container<Number> result(xx.size());
103+
#if defined(_OPENMP) && !defined(ALFI_DISABLE_OPENMP)
104+
#pragma omp parallel for
105+
#endif
106+
for (SizeT i = 0; i < xx.size(); ++i) {
107+
result[i] = val_div(rf, xx[i]);
108+
}
109+
return result;
110+
}
111+
112+
/**
113+
@brief Evaluates the rational function at a scalar point.
114+
115+
Calls @ref val_mul for \f(|x| \leq 1\f) and @ref val_div otherwise for the sake of numerical stability.
116+
*/
117+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
118+
std::enable_if_t<!traits::has_size<Number>::value, Number>
119+
val(const RationalFunction<Number, Container>& rf, Number x) {
120+
if (std::abs(x) <= 1) {
121+
return val_mul(rf, x);
122+
} else {
123+
return val_div(rf, x);
124+
}
125+
}
126+
127+
/**
128+
@brief Evaluates the rational function at each point in the container using @ref val for scalar values.
129+
*/
130+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
131+
std::enable_if_t<traits::has_size<Container<Number>>::value, Container<Number>>
132+
val(const RationalFunction<Number, Container>& rf, const Container<Number>& xx) {
133+
Container<Number> result(xx.size());
134+
#if defined(_OPENMP) && !defined(ALFI_DISABLE_OPENMP)
135+
#pragma omp parallel for
136+
#endif
137+
for (SizeT i = 0; i < xx.size(); ++i) {
138+
result[i] = val(rf, xx[i]);
139+
}
140+
return result;
141+
}
142+
143+
/**
144+
@brief Computes the [@p n / @p m] Pade approximant of the polynomial @p P about the point \f(x = 0\f).
145+
146+
The Pade approximant is given by the formula
147+
\f[
148+
P(x) = \frac{A(x)}{B(x)} = \frac{\sum_{i=0}^{n}{a_ix^i}}{b_0+\sum_{j=0}^{m}{b_jx^j}}
149+
\f]
150+
where \f(A(x)\f) and \f(B(x)\f) are the numerator and denominator polynomials, respectively; \f(b_0 \neq 0\f).
151+
152+
This function computes the Pade approximant of a polynomial by applying a modified extended Euclidean algorithm for polynomials.
153+
154+
The modification consists in that:
155+
- the algorithm may terminate early if the numerator's degree already meets the requirements,
156+
- or perform an extra iteration involving a division by a zero polynomial in special cases.
157+
158+
The latter is necessary to avoid false negatives, for example, when computing the `[2/2]` approximant of the function \f(x^5\f).
159+
160+
Without the additional check, this also may lead to false positives, as in the case of computing the `[2/2]` approximant of \f(x^4\f).@n
161+
This is prevented by verifying that the constant term of the denominator is non-zero after the algorithm completes.
162+
163+
@param P the polynomial to approximate (a container of coefficients in descending degree order)
164+
@param n the maximum degree for the numerator
165+
@param m the maximum degree for the denominator
166+
@param epsilon the tolerance used to determine whether a coefficient is considered zero (default is machine epsilon)
167+
@return a pair `{numerator, denominator}` representing the Pade approximant; if an approximant does not exist, an empty pair is returned
168+
*/
169+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
170+
RationalFunction<Number,Container> pade(Container<Number> P, SizeT n, SizeT m, Number epsilon = std::numeric_limits<Number>::epsilon()) {
171+
if constexpr (std::is_signed_v<SizeT>) {
172+
if (n < 0 || m < 0) {
173+
return {{}, {}};
174+
}
175+
}
176+
177+
util::poly::normalize(P);
178+
179+
Container<Number> Xnm1(n + m + 2, 0);
180+
Xnm1[0] = 1;
181+
182+
// Modified extended Euclidean algorithm `gcd(a,b)=as+bt` without s variable
183+
// a = Xnm1
184+
// b = P
185+
Container<Number> old_r, r, old_t, t;
186+
if (Xnm1.size() >= P.size()) {
187+
old_r = std::move(Xnm1), r = std::move(P);
188+
old_t = {0}, t = {1};
189+
} else {
190+
old_r = std::move(P), r = std::move(Xnm1);
191+
old_t = {1}, t = {0};
192+
}
193+
194+
// `old_r.size()` strictly decreases, except maybe the first iteration
195+
// ReSharper disable once CppDFALoopConditionNotUpdated
196+
while (old_r.size() > n + 1) {
197+
auto [q, new_r] = util::poly::div(old_r, r, epsilon);
198+
199+
const auto qt = util::poly::mul(q, t);
200+
const auto new_t_size = std::max(old_t.size(), qt.size());
201+
Container<Number> new_t(new_t_size, 0);
202+
for (SizeT i = 0, offset = new_t_size - old_t.size(); i < old_t.size(); ++i) {
203+
new_t[offset+i] = old_t[i];
204+
}
205+
for (SizeT i = 0, offset = new_t_size - qt.size(); i < qt.size(); ++i) {
206+
new_t[offset+i] -= qt[i];
207+
}
208+
209+
old_r = std::move(r);
210+
r = std::move(new_r);
211+
old_t = std::move(t);
212+
t = std::move(new_t);
213+
214+
util::poly::normalize(old_r, epsilon);
215+
}
216+
217+
util::poly::normalize(old_t, epsilon);
218+
219+
if (old_t.size() > m + 1 || std::abs(old_t[old_t.size()-1]) <= epsilon) {
220+
return {{}, {}};
221+
}
222+
223+
return std::make_pair(old_r, old_t);
224+
}
225+
}

ALFI/ALFI/util/numeric.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,34 @@ namespace alfi::util::numeric {
99
bool are_equal(Number a, Number b, Number epsilon = std::numeric_limits<Number>::epsilon()) {
1010
return std::abs(a - b) <= epsilon || std::abs(a - b) <= std::max(std::abs(a), std::abs(b)) * epsilon;
1111
}
12+
13+
/**
14+
@brief Computes the power of a number using binary exponentiation.
15+
16+
Calculates \f(x^n\f) in \f(O(\log{n})\f) operations using the binary (exponentiation by squaring) method.
17+
18+
It supports both signed and unsigned exponent types (@p ExponentType).@n
19+
If the exponent is negative, the function computes the reciprocal of the positive exponentiation.
20+
21+
@param x the base
22+
@param n the exponent
23+
@return \f(x^n\f)
24+
*/
25+
template <typename Number, typename ExponentType>
26+
Number binpow(Number x, ExponentType n) {
27+
if constexpr (std::is_signed_v<ExponentType>) {
28+
if (n < 0) {
29+
return 1 / binpow(x, -n);
30+
}
31+
}
32+
Number result = 1;
33+
while (n > 0) {
34+
if ((n & 1) == 1) {
35+
result *= x;
36+
}
37+
x *= x;
38+
n >>= 1;
39+
}
40+
return result;
41+
}
1242
}

ALFI/ALFI/util/poly.h

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <limits>
5+
6+
#include "../config.h"
7+
8+
/**
9+
@namespace alfi::util::poly
10+
@brief Namespace with utility functions for polynomial operations.
11+
12+
This namespace provides a set of functions for manipulating polynomials, such as normalization, multiplication, and division.
13+
All polynomials are represented as containers of coefficients in descending degree order.
14+
*/
15+
namespace alfi::util::poly {
16+
/**
17+
@brief Normalizes a polynomial by removing leading zero coefficients.
18+
19+
Removes leading coefficients that are considered zero within a given tolerance @p epsilon.
20+
If all coefficients are close to zero, the last one is preserved.
21+
If the container is empty, a single zero coefficient is inserted.
22+
23+
@param p the polynomial to normalize (a container of coefficients in descending degree order)
24+
@param epsilon the tolerance used to determine whether a coefficient is considered zero (default is machine epsilon)
25+
*/
26+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
27+
void normalize(Container<Number>& p, Number epsilon = std::numeric_limits<Number>::epsilon()) {
28+
if (p.empty()) {
29+
return p.push_back(0);
30+
}
31+
auto p_start = std::find_if(p.begin(), p.end(), [&epsilon](Number v) { return std::abs(v) > epsilon; });
32+
if (p_start == p.end()) {
33+
--p_start;
34+
}
35+
if (p_start > p.begin()) {
36+
p.erase(p.begin(), p_start);
37+
}
38+
}
39+
40+
/**
41+
@brief Multiplies two polynomials.
42+
43+
Given two polynomials \f(p_1\f) and \f(p_2\f) represented as containers of coefficients,
44+
this function computes their product using the convolution formula:
45+
\f[
46+
(p_1 \cdot p_2)[k] = \sum_{i+j=k}{p_1[i] \cdot p_2[j]}
47+
\f]
48+
49+
If either polynomial is empty, the function returns an empty container.
50+
51+
@param p1 the first polynomial
52+
@param p2 the second polynomial
53+
@return the product polynomial (either empty or of size `p1.size() + p2.size() - 1`)
54+
*/
55+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
56+
Container<Number> mul(const Container<Number>& p1, const Container<Number>& p2) {
57+
if (p1.empty() || p2.empty()) {
58+
return {};
59+
}
60+
Container<Number> result(p1.size() + p2.size() - 1);
61+
for (SizeT i = 0; i < p1.size(); ++i) {
62+
for (SizeT j = 0; j < p2.size(); ++j) {
63+
result[i+j] += p1[i] * p2[j];
64+
}
65+
}
66+
return result;
67+
}
68+
69+
/**
70+
@brief Divides one polynomial by another.
71+
72+
This function divides the @p dividend polynomial \f(A\f) by the @p divisor polynomial \f(B\f),
73+
and returns the @p quotient \f(Q\f) and the @p remainder \f(R\f) such that:
74+
\f[
75+
A(x) = B(X) \cdot Q(x) + R(x)
76+
\f]
77+
with either \f(R\f) being effectively zero or the degree of \f(R\f) being lower than the degree of \f(B\f).
78+
79+
The division is performed using a tolerance @p epsilon to determine when a coefficient is considered zero.
80+
81+
If the divisor is effectively zero or if the dividend has a lower degree than the divisor,
82+
the function returns an empty quotient and the dividend as the remainder.
83+
84+
@param dividend the polynomial to be divided
85+
@param divisor the polynomial to divide by
86+
@param epsilon the tolerance used to determine whether a coefficient is considered zero (default is machine epsilon)
87+
@return a pair `{quotient, remainder}`
88+
*/
89+
template <typename Number = DefaultNumber, template <typename, typename...> class Container = DefaultContainer>
90+
std::pair<Container<Number>,Container<Number>> div(const Container<Number>& dividend, const Container<Number>& divisor, Number epsilon = std::numeric_limits<Number>::epsilon()) {
91+
const auto divisor_start = std::find_if(divisor.begin(), divisor.end(), [&epsilon](Number v) { return std::abs(v) > epsilon; });
92+
93+
if (divisor_start == divisor.end() || dividend.size() < divisor.size()) {
94+
return {{}, dividend};
95+
}
96+
97+
const auto divisor_start_idx = divisor_start - divisor.begin();
98+
99+
const auto n = dividend.size();
100+
const auto m = divisor.size() - divisor_start_idx;
101+
102+
Container<Number> quotient(n - m + 1, 0);
103+
Container<Number> remainder = dividend;
104+
105+
for (SizeT i = 0; i <= n - m; ++i) {
106+
const Number factor = remainder[i] / divisor[divisor_start_idx];
107+
quotient[i] = factor;
108+
for (SizeT j = 0; j < m; ++j) {
109+
remainder[i+j] -= factor * divisor[divisor_start_idx+j];
110+
}
111+
}
112+
113+
remainder.erase(remainder.begin(), remainder.end() - m + 1);
114+
return {quotient, remainder};
115+
}
116+
}

0 commit comments

Comments
 (0)