// // LolRemez - Remez algorithm implementation // // Copyright © 2005-2015 Sam Hocevar // // This program is free software. It comes without any warranty, to // the extent permitted by applicable law. You can redistribute it // and/or modify it under the terms of the Do What the Fuck You Want // to Public License, Version 2, as published by the WTFPL Task Force. // See http://www.wtfpl.net/ for more details. // #if HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include "matrix.h" #include "solver.h" using lol::real; remez_solver::remez_solver(int order, int decimals) : m_order(order), m_decimals(decimals), m_has_weight(false) { } void remez_solver::run(real a, real b, char const *func, char const *weight) { using std::printf; m_func.parse(func); if (weight) { m_weight.parse(weight); m_has_weight = true; } m_k1 = (b + a) / 2; m_k2 = (b - a) / 2; m_epsilon = pow((real)10, (real)-(m_decimals + 2)); remez_init(); print_poly(); for (int n = 0; ; n++) { real old_error = m_error; find_extrema(); remez_step(); if (m_error >= (real)0 && fabs(m_error - old_error) < m_error * m_epsilon) break; print_poly(); find_zeroes(); } print_poly(); } /* * This is basically the first Remez step: we solve a system of * order N+1 and get a good initial polynomial estimate. */ void remez_solver::remez_init() { /* m_order + 1 zeroes of the error function */ m_zeroes.Resize(m_order + 1); /* m_order + 2 control points */ m_control.Resize(m_order + 2); /* Initial estimates for the x_i where the error will be zero and * precompute f(x_i). */ array fxn; for (int i = 0; i < m_order + 1; i++) { m_zeroes[i] = (real)(2 * i - m_order) / (real)(m_order + 1); fxn.Push(eval_func(m_zeroes[i])); } /* We build a matrix of Chebishev evaluations: row i contains the * evaluations of x_i for polynomial order n = 0, 1, ... */ LinearSystem system(m_order + 1); for (int n = 0; n < m_order + 1; n++) { auto p = polynomial::chebyshev(n); for (int i = 0; i < m_order + 1; i++) system[i][n] = p.eval(m_zeroes[i]); } /* Solve the system */ system = system.inverse(); /* Compute new Chebyshev estimate */ m_estimate = polynomial(); for (int n = 0; n < m_order + 1; n++) { real weight = 0; for (int i = 0; i < m_order + 1; i++) weight += system[n][i] * fxn[i]; m_estimate += weight * polynomial::chebyshev(n); } } /* * Every subsequent iteration of the Remez algorithm: we solve a system * of order N+2 to both refine the estimate and compute the error. */ void remez_solver::remez_step() { /* Pick up x_i where error will be 0 and compute f(x_i) */ array fxn; for (int i = 0; i < m_order + 2; i++) fxn.Push(eval_func(m_control[i])); /* We build a matrix of Chebishev evaluations: row i contains the * evaluations of x_i for polynomial order n = 0, 1, ... */ LinearSystem system(m_order + 2); for (int n = 0; n < m_order + 1; n++) { auto p = polynomial::chebyshev(n); for (int i = 0; i < m_order + 2; i++) system[i][n] = p.eval(m_control[i]); } /* The last line of the system is the oscillating error */ for (int i = 0; i < m_order + 2; i++) { real error = fabs(eval_weight(m_control[i])); system[i][m_order + 1] = (i & 1) ? error : -error; } /* Solve the system */ system = system.inverse(); /* Compute new polynomial estimate */ m_estimate = polynomial(); for (int n = 0; n < m_order + 1; n++) { real weight = 0; for (int i = 0; i < m_order + 2; i++) weight += system[n][i] * fxn[i]; m_estimate += weight * polynomial::chebyshev(n); } /* Compute the error (FIXME: unused?) */ real error = 0; for (int i = 0; i < m_order + 2; i++) error += system[m_order + 1][i] * fxn[i]; } void remez_solver::find_zeroes() { m_stats_cheby = m_stats_func = m_stats_weight = 0.f; /* Find m_order + 1 zeroes of the error function. No need to * compute the relative error: its zeroes are at the same * place as the absolute error! */ for (int i = 0; i < m_order + 1; i++) { struct { real value, error; } a, b, c; a.value = m_control[i]; a.error = eval_estimate(a.value) - eval_func(a.value); b.value = m_control[i + 1]; b.error = eval_estimate(b.value) - eval_func(b.value); static real limit = ldexp((real)1, -500); static real zero = (real)0; while (fabs(a.value - b.value) > limit) { /* Interpolate linearly instead of taking the midpoint, this * leads to far better convergence (6:1 speedups). */ real t = abs(b.error) / (abs(a.error) + abs(b.error)); real newc = b.value + t * (a.value - b.value); /* If the third point didn't change since last iteration, * we may be at an inflection point. Use the midpoint to get * out of this situation. */ c.value = newc == c.value ? (a.value + b.value) / 2 : newc; c.error = eval_estimate(c.value) - eval_func(c.value); if (c.error == zero) break; if ((a.error < zero && c.error < zero) || (a.error > zero && c.error > zero)) a = c; else b = c; } m_zeroes[i] = c.value; } printf(" -:- timings for zeroes: estimate %f func %f weight %f\n", m_stats_cheby, m_stats_func, m_stats_weight); } /* XXX: this is the real costly function */ void remez_solver::find_extrema() { using std::printf; /* Find m_order + 2 extrema of the error function. We need to * compute the relative error, since its extrema are at slightly * different locations than the absolute error’s. */ m_error = 0; m_stats_cheby = m_stats_func = m_stats_weight = 0.f; int evals = 0, rounds = 0; for (int i = 0; i < m_order + 2; i++) { real a = -1, b = 1; if (i > 0) a = m_zeroes[i - 1]; if (i < m_order + 1) b = m_zeroes[i]; for (int r = 0; ; ++r, ++rounds) { real maxerror = 0, maxweight = 0; int best = -1; real c = a, delta = (b - a) / 4; for (int k = 0; k <= 4; k++) { if (r == 0 || (k & 1)) { ++evals; real error = fabs(eval_estimate(c) - eval_func(c)); real weight = fabs(eval_weight(c)); /* if error/weight >= maxerror/maxweight */ if (error * maxweight >= maxerror * weight) { maxerror = error; maxweight = weight; best = k; } } c += delta; } switch (best) { case 0: b = a + delta * 2; break; case 4: a = b - delta * 2; break; default: b = a + delta * (best + 1); a = a + delta * (best - 1); break; } if (delta < m_epsilon) { real e = fabs(maxerror / maxweight); if (e > m_error) m_error = e; m_control[i] = (a + b) / 2; break; } } } printf(" -:- timings for extrema: estimate %f func %f weight %f\n", m_stats_cheby, m_stats_func, m_stats_weight); printf(" -:- calls: %d rounds, %d evals\n", rounds, evals); printf(" -:- error: "); m_error.print(m_decimals); printf("\n"); } void remez_solver::print_poly() { using std::printf; /* Transform our polynomial in the [-1..1] range into a polynomial * in the [a..b] range by composing it with q: * q(x) = 2x / (b-a) - (b+a) / (b-a) */ polynomial q ({ -m_k1 / m_k2, real(1) / m_k2 }); polynomial r = m_estimate.eval(q); printf("\n"); for (int j = 0; j < m_order + 1; j++) { if (j) printf(" + x**%i * ", j); r[j].print(m_decimals); } printf("\n\n"); } real remez_solver::eval_estimate(real const &x) { Timer t; real ret = m_estimate.eval(x); m_stats_cheby += t.Get(); return ret; } real remez_solver::eval_func(real const &x) { Timer t; real ret = m_func.eval(x * m_k2 + m_k1); m_stats_func += t.Get(); return ret; } real remez_solver::eval_weight(real const &x) { Timer t; real ret = m_has_weight ? m_weight.eval(x * m_k2 + m_k1) : real(1); m_stats_weight += t.Get(); return ret; }