我正在为PIC微控制器编写一个C程序,它需要做一个非常特定的指数函数。我需要计算以下内容:
A = k。 (1 - (p / p0)^ 0.19029)
k和p0是常数,所以除了找到x ^ 0.19029
之外,它们都非常简单(p / p0)比率始终在0-1范围内。
如果我添加math.h并使用power函数,它会很有效,除了耗尽所有可用的16 kB程序存储器。谈论英国媒体报道! (没有电源功能的其余程序= ~20%闪存使用率;添加math.h和电源功能,= 100%)。
我希望该计划也可以做其他一些事情。我想知道我是否可以为x ^ 0.19029编写一个特殊的案例实现,可能涉及迭代和某种查找表。
我的想法是为函数x ^ 0.19029生成一个查找表,在0-1范围内可能有10-100个x值。代码将找到一个紧密匹配,然后(不知何故)通过重新缩放查找表值来迭代地改进它。然而,这是我迷路的地方,因为我的小脑无法想象所涉及的数学。
这种方法有用吗?
或者,我已经研究过使用Exp(x)和Ln(x),它可以用泰勒展开来实现。 b ^ x可以找到:
b ^ x =(e ^(ln b))^ x = e ^(x.ln(b))
(参见:Wikipedia - Powers via Logarithms)
但这看起来有点棘手和复杂。我是否可能比编译器的数学库更小实现,并且我可以根据我的特殊情况简化它(即base = 0-1,exponent始终为0.19029)?
请注意,此刻RAM使用率还可以,但我在Flash上运行不足(用于代码存储)。速度并不重要。有人已经建议我使用更大的微处理器和更多的闪存,但这听起来像挥霍无度!
[编辑]当我说“(p / p0)比率总是在0-1”范围内时,我很懒。实际上它永远不会达到0,我昨晚做了一些计算并确定实际上0.3到1的范围就足够了!这意味着下面的一些更简单的解决方案应该是合适的。另外,上面的“k”是44330,我希望最终结果中的误差小于0.1。我猜这意味着(p / p0)^ 0.19029中的误差需要小于1/443300或2.256e-6
答案 0 :(得分:15)
使用样条线。功能的相关部分如下图所示。它大致类似于第5根,因此有问题的区域接近p / p0 = 0
。有数学理论如何最佳地放置花键结以最小化误差(参见Carl de Boor:Splines实用指南)。通常,提前以B形式构造样条曲线(使用工具箱,如Matlab的样条工具箱 - 也由C. de Boor编写),然后转换为分段多项式表示以进行快速评估。
在C. de Boor,PGS中,函数g(x) = sqrt(x + 1)
实际上是作为一个例子(第12章,例II)。这正是您需要的。这本书回到这个案例几次,因为由于x = -1
处的无限导数,它无疑是任何插值方案的难题。来自PGS的所有软件在netlib中作为PPPACK免费提供,其中大部分也是SLATEC的一部分(也来自netlib)。
编辑(删除)
(乘以x
一次没有太大帮助,因为它只对一阶导数进行正则化,而x = 0
处的所有其他导数仍然无穷大。)
编辑2
我的感觉是,对于相对较低的精度要求,最佳构造的样条(在de Boor之后)将是最好的(并且最快的)。如果准确度要求很高(例如1e-8),可能会被迫回到数学家一直在研究世纪的算法。此时,最好只是简单地下载glibc
的来源并复制(提供GPL是可以接受的)
glibc-2.19/sysdeps/ieee754/dbl-64/e_pow.c
由于我们不必包含整个math.h
,因此内存应该没有问题,但我们只能通过固定指数获得轻微利润。
编辑3
以下是来自netlib的e_pow.c的改编版本,由@Joni发现。这似乎是上面提到的glibc
更现代化实现的祖父。旧版本有两个优点:(1)它是公共域,(2)它使用有限数量的常量,如果内存是一个紧张的资源,这是有益的(glibc
的版本定义超过10000行常量!)。以下是完全独立的代码,它将x^0.19029
的{{1}}计算为双精度(我根据Python的幂函数测试它,发现最多2位不同):
0 <= x <= 1
显然,50多年的研究已经深入研究,因此可能很难做得更好。 (人们必须意识到整个算法中有0个循环,只有2个分区,只有6个#define __LITTLE_ENDIAN
#ifdef __LITTLE_ENDIAN
#define __HI(x) *(1+(int*)&x)
#define __LO(x) *(int*)&x
#else
#define __HI(x) *(int*)&x
#define __LO(x) *(1+(int*)&x)
#endif
static const double
bp[] = {1.0, 1.5,},
dp_h[] = { 0.0, 5.84962487220764160156e-01,}, /* 0x3FE2B803, 0x40000000 */
dp_l[] = { 0.0, 1.35003920212974897128e-08,}, /* 0x3E4CFDEB, 0x43CFD006 */
zero = 0.0,
one = 1.0,
two = 2.0,
two53 = 9007199254740992.0, /* 0x43400000, 0x00000000 */
/* poly coefs for (3/2)*(log(x)-2s-2/3*s**3 */
L1 = 5.99999999999994648725e-01, /* 0x3FE33333, 0x33333303 */
L2 = 4.28571428578550184252e-01, /* 0x3FDB6DB6, 0xDB6FABFF */
L3 = 3.33333329818377432918e-01, /* 0x3FD55555, 0x518F264D */
L4 = 2.72728123808534006489e-01, /* 0x3FD17460, 0xA91D4101 */
L5 = 2.30660745775561754067e-01, /* 0x3FCD864A, 0x93C9DB65 */
L6 = 2.06975017800338417784e-01, /* 0x3FCA7E28, 0x4A454EEF */
P1 = 1.66666666666666019037e-01, /* 0x3FC55555, 0x5555553E */
P2 = -2.77777777770155933842e-03, /* 0xBF66C16C, 0x16BEBD93 */
P3 = 6.61375632143793436117e-05, /* 0x3F11566A, 0xAF25DE2C */
P4 = -1.65339022054652515390e-06, /* 0xBEBBBD41, 0xC5D26BF1 */
P5 = 4.13813679705723846039e-08, /* 0x3E663769, 0x72BEA4D0 */
lg2 = 6.93147180559945286227e-01, /* 0x3FE62E42, 0xFEFA39EF */
lg2_h = 6.93147182464599609375e-01, /* 0x3FE62E43, 0x00000000 */
lg2_l = -1.90465429995776804525e-09, /* 0xBE205C61, 0x0CA86C39 */
ovt = 8.0085662595372944372e-0017, /* -(1024-log2(ovfl+.5ulp)) */
cp = 9.61796693925975554329e-01, /* 0x3FEEC709, 0xDC3A03FD =2/(3ln2) */
cp_h = 9.61796700954437255859e-01, /* 0x3FEEC709, 0xE0000000 =(float)cp */
cp_l = -7.02846165095275826516e-09, /* 0xBE3E2FE0, 0x145B01F5 =tail of cp_h*/
ivln2 = 1.44269504088896338700e+00, /* 0x3FF71547, 0x652B82FE =1/ln2 */
ivln2_h = 1.44269502162933349609e+00, /* 0x3FF71547, 0x60000000 =24b 1/ln2*/
ivln2_l = 1.92596299112661746887e-08; /* 0x3E54AE0B, 0xF85DDF44 =1/ln2 tail*/
double pow0p19029(double x)
{
double y = 0.19029e+00;
double z,ax,z_h,z_l,p_h,p_l;
double y1,t1,t2,r,s,t,u,v,w;
int i,j,k,n;
int hx,hy,ix,iy;
unsigned lx,ly;
hx = __HI(x); lx = __LO(x);
hy = __HI(y); ly = __LO(y);
ix = hx&0x7fffffff; iy = hy&0x7fffffff;
ax = x;
/* special value of x */
if(lx==0) {
if(ix==0x7ff00000||ix==0||ix==0x3ff00000){
z = ax; /*x is +-0,+-inf,+-1*/
return z;
}
}
s = one; /* s (sign of result -ve**odd) = -1 else = 1 */
double ss,s2,s_h,s_l,t_h,t_l;
n = ((ix)>>20)-0x3ff;
j = ix&0x000fffff;
/* determine interval */
ix = j|0x3ff00000; /* normalize ix */
if(j<=0x3988E) k=0; /* |x|<sqrt(3/2) */
else if(j<0xBB67A) k=1; /* |x|<sqrt(3) */
else {k=0;n+=1;ix -= 0x00100000;}
__HI(ax) = ix;
/* compute ss = s_h+s_l = (x-1)/(x+1) or (x-1.5)/(x+1.5) */
u = ax-bp[k]; /* bp[0]=1.0, bp[1]=1.5 */
v = one/(ax+bp[k]);
ss = u*v;
s_h = ss;
__LO(s_h) = 0;
/* t_h=ax+bp[k] High */
t_h = zero;
__HI(t_h)=((ix>>1)|0x20000000)+0x00080000+(k<<18);
t_l = ax - (t_h-bp[k]);
s_l = v*((u-s_h*t_h)-s_h*t_l);
/* compute log(ax) */
s2 = ss*ss;
r = s2*s2*(L1+s2*(L2+s2*(L3+s2*(L4+s2*(L5+s2*L6)))));
r += s_l*(s_h+ss);
s2 = s_h*s_h;
t_h = 3.0+s2+r;
__LO(t_h) = 0;
t_l = r-((t_h-3.0)-s2);
/* u+v = ss*(1+...) */
u = s_h*t_h;
v = s_l*t_h+t_l*ss;
/* 2/(3log2)*(ss+...) */
p_h = u+v;
__LO(p_h) = 0;
p_l = v-(p_h-u);
z_h = cp_h*p_h; /* cp_h+cp_l = 2/(3*log2) */
z_l = cp_l*p_h+p_l*cp+dp_l[k];
/* log2(ax) = (ss+..)*2/(3*log2) = n + dp_h + z_h + z_l */
t = (double)n;
t1 = (((z_h+z_l)+dp_h[k])+t);
__LO(t1) = 0;
t2 = z_l-(((t1-t)-dp_h[k])-z_h);
/* split up y into y1+y2 and compute (y1+y2)*(t1+t2) */
y1 = y;
__LO(y1) = 0;
p_l = (y-y1)*t1+y*t2;
p_h = y1*t1;
z = p_l+p_h;
j = __HI(z);
i = __LO(z);
/*
* compute 2**(p_h+p_l)
*/
i = j&0x7fffffff;
k = (i>>20)-0x3ff;
n = 0;
if(i>0x3fe00000) { /* if |z| > 0.5, set n = [z+0.5] */
n = j+(0x00100000>>(k+1));
k = ((n&0x7fffffff)>>20)-0x3ff; /* new k for n */
t = zero;
__HI(t) = (n&~(0x000fffff>>k));
n = ((n&0x000fffff)|0x00100000)>>(20-k);
if(j<0) n = -n;
p_h -= t;
}
t = p_l+p_h;
__LO(t) = 0;
u = t*lg2_h;
v = (p_l-(t-p_h))*lg2+t*lg2_l;
z = u+v;
w = v-(z-u);
t = z*z;
t1 = z - t*(P1+t*(P2+t*(P3+t*(P4+t*P5))));
r = (z*t1)/(t1-two)-(w+z*w);
z = one-(r-z);
__HI(z) += (n<<20);
return s*z;
}
语句!)这个原因同样是if
处的行为,其中所有导数都是发散,这使得控制错误非常困难:我曾经有一个18节的样条表示,最好是x = 0
,绝对和相对错误x = 1e-4
到处都是,但是去{ {1}}再次破坏了一切。
因此,除非放宽任意接近于零的要求,否则我建议使用上面给出的< 5e-4
的改编版本。
编辑4
现在我们知道域x = 1e-5
已足够,并且我们的准确度要求非常低,编辑3 显然有点过分。正如@MvG所证明的那样,该函数的表现非常好,以至于7阶多项式足以满足精度要求,可以将其视为单个样条曲线段。 @ MvG的解决方案可以最大限度地减少积分误差,这已经非常好了。
问题是我们还能做得多好吗?找到给定程度的多项式将最小化感兴趣区间中的最大误差将是有趣的。答案是minimax
多项式,可以使用Remez算法找到,该算法在Boost库中实现。我喜欢@ MvG的想法,将值e_pow.c
限制为0.3 <= x <= 1
,我也会这样做。这是x = 1
:
1
由于我们使用的boost的所有部分都只是标题,因此只需使用:
进行构建 minimax.cpp
我们最终得到系数,它们乘以44330:
#include <ostream>
#define TARG_PREC 64
#define WORK_PREC (TARG_PREC*2)
#include <boost/multiprecision/cpp_dec_float.hpp>
typedef boost::multiprecision::number<boost::multiprecision::cpp_dec_float<WORK_PREC> > dtype;
using boost::math::pow;
#include <boost/math/tools/remez.hpp>
boost::shared_ptr<boost::math::tools::remez_minimax<dtype> > p_remez;
dtype f(const dtype& x) {
static const dtype one(1), y(0.19029);
return one - pow(one - x, y);
}
void out(const char *descr, const dtype& x, const char *sep="") {
std::cout << descr << boost::math::tools::real_cast<double>(x) << sep << std::endl;
}
int main() {
dtype a(0), b(0.7); // range to optimise over
bool rel_error(false), pin(true);
int orderN(7), orderD(0), skew(0), brake(50);
int prec = 2 + (TARG_PREC * 3010LL)/10000;
std::cout << std::scientific << std::setprecision(prec);
p_remez.reset(new boost::math::tools::remez_minimax<dtype>(
&f, orderN, orderD, a, b, pin, rel_error, skew, WORK_PREC));
out("Max error in interpolated form: ", p_remez->max_error());
p_remez->set_brake(brake);
unsigned i, count(50);
for (i = 0; i < count; ++i) {
std::cout << "Stepping..." << std::endl;
dtype r = p_remez->iterate();
out("Maximum Deviation Found: ", p_remez->max_error());
out("Expected Error Term: ", p_remez->error_term());
out("Maximum Relative Change in Control Points: ", r);
}
boost::math::tools::polynomial<dtype> n = p_remez->numerator();
for(i = n.size(); i--; ) {
out("", n[i], ",");
}
}
以下误差图表明这是最好的7次多项式逼近,因为所有极值都是相等的量级(0.06659):
如果要求发生变化(同时仍然保持远离0!),上面的C ++程序可以简单地适应吐出新的最优多项式逼近。
答案 1 :(得分:8)
我不使用查找表,而是使用多项式近似:
1 - x 0.19029 ≈-1073365.91783x 15 + 8354695.40833x 14 - 29422576.6529x 13 + 61993794.537 x 12 - 87079891.4988x 11 + 86005723.842x 10 - 61389954.7459x 9 + 32053170.1149x 8 < / sup> - 12253383.4372x 7 + 3399819.97536x 6 - 672003.142815x 5 + 91817.6782072x 4 - 8299.75873768x 3 + 469.530204564x 2 - 16.6572179869x + 0.722044145701
或代码:
double f(double x) {
double fx;
fx = - 1073365.91783;
fx = fx*x + 8354695.40833;
fx = fx*x - 29422576.6529;
fx = fx*x + 61993794.537;
fx = fx*x - 87079891.4988;
fx = fx*x + 86005723.842;
fx = fx*x - 61389954.7459;
fx = fx*x + 32053170.1149;
fx = fx*x - 12253383.4372;
fx = fx*x + 3399819.97536;
fx = fx*x - 672003.142815;
fx = fx*x + 91817.6782072;
fx = fx*x - 8299.75873768;
fx = fx*x + 469.530204564;
fx = fx*x - 16.6572179869;
fx = fx*x + 0.722044145701;
return fx;
}
我使用最小二乘法计算了这个:
f(x) = 1-x^(19029/100000) # your function
d = 16 # number of terms, i.e. degree + 1
A = matrix(d, d, lambda r, c: integrate(x^r*x^c, (x, 0, 1)))
b = vector([integrate(x^r*f(x), (x, 0, 1)) for r in range(d)])
A.solve_right(b).change_ring(RDF)
以下是错误的情节:
蓝色是我的16项多项式的误差,而红色是你从具有16个等距值的分段线性插值得到的误差。正如您所看到的,对于该范围的大多数部分,这两个误差都非常小,但是在接近x = 0时会非常大。我实际上修剪了那里的情节。如果您可以以某种方式缩小可能值的范围,则可以将其用作集成的域,并获得更适合相关范围的值。当然,以外面更糟糕的为代价。您也可以增加术语数量以获得更接近的拟合,但这也可能导致更高的振荡。
我想你也可以将这种方法与Stefan发布的方法结合起来:用他的方法将域分成几个部分,然后用我的方法为每个部分找到一个接近的低次多项式。
由于您更新了问题的规范,关于域和错误,以下是满足这些要求的最小解决方案:
44330(1 - x 0.19029 )≈+ 23024.9160933(1-x) 7 - 39408.6473636(1-x) 6 + 31379.9086193 (1-x) 5 -10098.7031260(1-x) 4 + 4339.44098317(1-x) 3 + 3202.85705860(1-x) 2 + 8442.42528906(1-x)
double f(double x) {
double fx, x1 = 1. - x;
fx = + 23024.9160933;
fx = fx*x1 - 39408.6473636;
fx = fx*x1 + 31379.9086193;
fx = fx*x1 - 10098.7031260;
fx = fx*x1 + 4339.44098317;
fx = fx*x1 + 3202.85705860;
fx = fx*x1 + 8442.42528906;
fx = fx*x1;
return fx;
}
我将x从0.293整合到1或等效1 - x从0到0.707,以保持相关域外的最差振荡。我也省略了常数项,以确保x = 1时的精确结果。范围[0.3,1]的最大误差现在发生在x = 0.3260并且等于0.0972 <0。 0.1。这是一个错误图,当然由于比例因子k = 44330已经包含在这里,因此它具有比上面更大的绝对误差。
我还可以说该函数的前三个导数将在所讨论的范围内具有常数符号,因此该函数是单调的,凸的,并且通常非常好。
答案 2 :(得分:2)
不是要回答这个问题,但它说明了“不要走的路”,因此可能会有所帮助:
这个快速而肮脏的C代码计算pow(i, 0.19029)
为0.000到1.000,步长为0.01。当存储为1/65536th时,前半部分以百分比显示错误(因为理论上 提供略高于4位精度的小数)。后半部分以0.001为步长显示插值和计算值,以及这两者之间的差异。
如果你自下而上阅读,那里看起来很好,所有的100和99.99,但是从0.001到0.020的前20个值毫无价值。
#include <stdio.h>
#include <math.h>
float powers[102];
int main (void)
{
int i, as_int;
double as_real, low, high, delta, approx, calcd, diff;
printf ("calculating and storing:\n");
for (i=0; i<=101; i++)
{
as_real = pow(i/100.0, 0.19029);
as_int = (int)round(65536*as_real);
powers[i] = as_real;
diff = 100*as_real/(as_int/65536.0);
printf ("%.5f %.5f %.5f ~ %.3f\n", i/100.0, as_real, as_int/65536.0, diff);
}
printf ("\n");
printf ("-- interpolating in 1/10ths:\n");
for (i=0; i<1000; i++)
{
as_real = i/1000.0;
low = powers[i/10];
high = powers[1+i/10];
delta = (high-low)/10.0;
approx = low + (i%10)*delta;
calcd = pow(as_real, 0.19029);
diff = 100.0*approx/calcd;
printf ("%.5f ~ %.5f = %.5f +/- %.5f%%\n", as_real, approx, calcd, diff);
}
return 0;
}
答案 3 :(得分:2)
您可以在fdlibm中找到pow
的完整,正确的独立实施。它大约有200行代码,其中大约一半处理特殊情况。如果您删除处理特殊情况的代码,那么您对我不感兴趣,我怀疑您的程序中是否存在问题。
答案 4 :(得分:1)
LutzL的回答非常好:将你的力量计算为(x ^ 1.52232)^(1/8),通过样条插值或其他方法计算内部功率。第八根处理零附近的病理性非微分行为。我冒昧地以这种方式嘲笑实现。然而,下面只进行线性插值来做x ^ 1.52232,并且你需要使用自己喜欢的数学数学工具来获得全系数。您将添加几十行代码以获得所需的功率,加上您选择用于样条的许多结,这取决于您所需的准确度。
不要被#include <math.h>
吓到;它只是用于对代码进行基准测试。
#include <stdio.h>
#include <math.h>
double my_sqrt(double x) {
/* Newton's method for a square root. */
int i = 0;
double res = 1.0;
if (x > 0) {
for (i = 0; i < 10; i++) {
res = 0.5 * (res + x / res);
}
} else {
res = 0.0;
}
return res;
}
double my_152232(double x) {
/* Cubic spline interpolation for x ** 1.52232. */
int i = 0;
double res = 0.0;
/* coefs[i] will give the cubic polynomial coefficients between x =
i and x = i+1. Out of laziness, the below numbers give only a
linear interpolation. You'll need to do some work and research
to get the spline coefficients. */
double coefs[3][4] = {{0.0, 1.0, 0.0, 0.0},
{-0.872526, 1.872526, 0.0, 0.0},
{-2.032706, 2.452616, 0.0, 0.0}};
if ((x >= 0) && (x < 3.0)) {
i = (int) x;
/* Horner's method cubic. */
res = (((coefs[i][3] * x + coefs[i][2]) * x) + coefs[i][1] * x)
+ coefs[i][0];
} else if (x >= 3.0) {
/* Scaled x ** 1.5 once you go off the spline. */
res = 1.024824 * my_sqrt(x * x * x);
}
return res;
}
double my_019029(double x) {
return my_sqrt(my_sqrt(my_sqrt(my_152232(x))));
}
int main() {
int i;
double x = 0.0;
for (i = 0; i < 1000; i++) {
x = 1e-2 * i;
printf("%f %f %f \n", x, my_019029(x), pow(x, 0.19029));
}
return 0;
}
编辑:如果您只对[0,1]这样的小区域感兴趣,更简单的是剥离一个sqrt(x)并使用泰勒计算x ^ 1.02232,这是非常好的表现系列:
double my_152232(double x) {
double part_050000 = my_sqrt(x);
double part_102232 = 1.02232 * x + 0.0114091 * x * x - 3.718147e-3 * x * x * x;
return part_102232 * part_050000;
}
这使得你在大约[0.1,6]的精确力量的1%范围内,尽管完全正确地获得奇点始终是一个挑战。即便如此,这个三项泰勒系列让你在x = 0.001的情况下在2.3%之内。