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+ }
0 commit comments