0.ごあいさつと目次

 【ごあいさつ】
  技術者にとって、時々自作を迫られる数学手法についての備忘録です。
  記憶の水底に沈んだ知識をサルベージしたので、理学系の方には噴飯物
  かと思いますが、工学系の方には実用的であることを心掛けました。

 【目次】
  0.ごあいさつと目次
  1.整数主義 Integerism
  2.三角関数を造る Taylor / Maclaurin Expansion
      〜 テーラー or マクローリン展開 〜
  3.逆三角関数を造る Ark Tangent & PI
  4.平方根を造る Newton-Raphson Method
      〜 ニュートンラプソン法 〜
  5.平方根と三角関数の使用例 その1.
      〜 ブツシュミ(物理シミュレーション) 〜
  6.平方根と三角関数の使用例 その2.
      〜 レイトレーシング 〜
  7.平方根と三角関数の使用例 その3.
      〜 フーリエ変換(不可逆圧縮) 〜
  8.リンク
      〜 参考サイト・文献 〜




1.整数主義 Integerism  計算機で処理する場合、整数は実数よりずっと速い。というのはどうやら迷信で、  下手に組んだ整数演算よりも、匠の手による実数演算の方が早い事も多いようです。  しかし、原理的に不連続な数しか扱えない計算機に、連続な実数を処理させる場合、  不条理な事が起ります。有名なのは、1.0 から 0.1 を 10 回引いても 0.0 と等しくないとか。  一方、連続でない整数を用い上下限も規定すれば、一見窮屈ですが全体に目が届く  安心感があります。
2.三角関数を造る Taylor / Maclaurin Expansion 〜 テーラー or マクローリン展開 〜  sin や cos といった三角関数は、 という特徴を持ちます、こうした関数の近似にうってつけの  方法の一つに、マクローリン展開法が有ります  概念としては、関数上のある一点を拠点として、  その点での傾きから、dxだけ離れた点の値を推定し、  その点での傾きの変化率から、dxだけ離れた点の傾きを推定し、  その点での傾きの変化率の変化率から、dxだけ離れた点の傾きの変化率を推定し、  ・・ 以下、変化率が求められる(その点で微分可能である)限り繰り返し ・・
 テイラー展開
関数 f(x) は、x=a+dx において、以下のように展開近似出来る。
  f(a+dx) = f(a) + f'(a)dx + 1 f''(a)dx2 ・・ +  1 fk(a)dxk ・・ 
2! k!
= Σ 1 fk(a)dxk
k=0 k!
 マクローリン展開は、テイラー展開の内、a=0のケース
関数 f(x) は、x=dx において、以下のように展開近似出来る。
  f(dx) = f(0) + f'(0)dx + 1 f''(0)dx2 ・・ +  1 fk(0)dxk ・・ 
2! k!
= Σ 1 fk(0)dxk
k=0 k!
 それでは実装です。  関数の名前は、整数型であることから、  int ISin(int),ICos(int); とします。  本来の sin, cos は、引数が 0〜2π(周期) 、戻り数が -1 〜 +1 ですが、  このままで整数化すると寂しすぎるので、スケーリングします。  0〜2π を 1024 分割し、戻り値は -163 〜 +163 としてみます。  1024 は 2の10乗 でおなじみの数ですが、163 は見慣れない数です。  これは、三角関数に固有の便利な特性、即ち、   lim(x→0) sin(x) = x  という関係が成り立つようにするために、設定しました。  これは、x が 0 から最小単位(この場合は 1 )変化すると、sin 関数の値も  最小単位(これも 1 )変化するというものです。  lim(x→0) sin(2π/1024*x) == 2π/1024 * x ですので、2π/1024 で割って、  ISin(x) == 1024/2π * sin(2π/1024*x) とすれば、  lim(x→0) ISin(x) == 1024/2π * 2π/1024 * x == x  となり好都合です。この 1024/2π が、すなわち 163 なわけです。
 ICos(dx), ISin(dx) のマクローリン展開
ICos(dx) = 163 * cos(dx/163)
 ICos(dx) = ICos(0) + ICos'(0)*dx + 1 ICos''(0)*dx2 + 1 ICos'''(0)*dx3 + ・・ 
2! 3!
 
= 163*cos(0/163) - 163/163*sin(0/163)*dx
 -    163    cos(0/163)*dx2 +   163   sin(0/163)*dx3 - ・・ 
 2!*1632 3!*1633
 
= Σ    (-1)k (  dx  )k*2*163
k=0(k*2)! 163
ISin(dx) = 163 * sin(dx/163)
 ISin(dx) = ISin(0) + ISin'(0)*dx + 1 ISin''(0)*dx2 + 1 ISin'''(0)*dx3 + ・・ 
2! 3!
 
= 163*sin(0/163) + 163/163*cos(0/163)*dx
 -    163    sin(0/163)*dx2 -   163   cos(0/163)*dx3 + ・・ 
 2!*1632 3!*1633
 
= Σ    (-1)k (  dx  )k*2+1*163
k=0(k*2+1)! 163
 後は実物で解説します。  下のソースは大きく分けて3つの部分から成ります。
  1. 引数の整形
    • 引数が負の場合、cos は引数の反転、sin は引数と戻り数の反転 により、引数を負でない範囲に集約します。    sinの例 
    • 次に、2Π の周期を持つので、引数を2Π の剰余に集約します。    sinの例 
    • また、引数が Π〜2Π の場合、引数を Π ずらすとともに戻り数を 反転して、引数を 0〜Π の範囲に集約します。    sinの例 
    • さらに、引数が Π/2〜Π の場合、cos は引数を Π/2 を中心にして 反転するとともに戻り数を反転、sin は引数を Π/2 を中心にして 反転することにより、引数を 0〜Π/2 の範囲に集約します。    sinの例 
  2. 守備範囲の判定  引数は 0〜Π/2 の範囲に集約されましたが、それでも 0 から離れるに  つれて近似精度が下がります、そこで、Π/4 付近を境に、それより  大きい領域は、他方の関数に処理を任せます(sinはcosに、cosはsinに)。       イメージ   ここで処理を任された側の関数は、すでに 0〜Π/2 の範囲に集約され  た引数を受取ります。  そこで、引数の整形を省略した関数( int ICosR(int), ISinR(int) )を  別途準備し、若干の速度アップを図りました。関数の数が増えて煩雑に思  われる場合は、ICos(int), ISin(int)に任せても良いです。
  3. マクローリン式の計算  除算が避けられませんので、分母を強引に 2 のべき乗にしました。  ISin(0) == 0、ICos(0) == 163 の 2 点をチェックして、分子の係数の  切上げ切捨てをしてください。
 int, unsigned int は32bitの一般的なものです。

//ICos(a)=163*cos(a*PI/512)
int ICos(int a)
{
    int ratio=1;
    unsigned int b,c;

    ///////////////////
    // 1.引数の整形  //
    ///////////////////
    if(a<0){    //引数が負なら引数反転
        a=-a;
    }
    a%=1024;    //引数を0〜2PIに
//  a&=0x3ffff
    if(a>=512){ //引数>=PIなら引数をPIシフトし、戻り値反転
        a-=512;
        ratio=-1;
    }
    if(a==256) return 0;
    if(a>256){  //引数>=PI/2なら引数を鏡像にし、戻り値反転
        a=512-a;
        ratio=-ratio;
    }

    ///////////////////////
    // 2.守備範囲の判定  //
    ///////////////////////
    //およそπ/4以上の領域では、sinで計算した方が高精度
//  if(a>104) return ratio*ISin(256-a);  //閾値(104)は実計算で決定
    if(a>104) return ratio*ISinR(256-a); //閾値(104)は実計算で決定

    /////////////////////////////
    // 3.マクローリン式の計算  //
    /////////////////////////////
    b=a*a;
/*
    // aは8bit以下となった
    // 4項6次までテイラー展開。(演算子^はべき乗を示す。a^b=pow(a,b))
    //  163 -163/2!*(a/163)^2 +163/4!*(a/163)^4 -163/6!*(a/163)^6 
    // =163 -a^2/2/163 +a^4/24/163^3 -a^6/720/163^5 
    // =163 -a^2/2/163 +a^4/24/163^3 -(a^4/163^3)*(a^2/163)/720/163 
    // アンダーフロー抑制//分子を変数範囲いっぱいに拡大
    // ={163^4*2+a^4/24*2-[a^2*163^2 +(a^4/163^2)*(a^2)/720*2]}/163/163/163/2
    // ={1411823522+a^4/12-[a^2*26569 +(a^4/26569)*(a^2)/360]}/8661494  //31bit/24bit
    // unsigned32で作業  a^4+αまでオーバーフロー回避
    // より高速/高精度が要求される場合、引数のレンジ別に使用する項数(次数)を使い分ける
    c=(1411823522+b*b/12-(b*26569 +(b*b/26569)/360*b))/8661494;  //4項6次
    c=(1411823522+b*b/12-b*26569)/8661494;        //3項4次
    c=(1367343104-b*(25731-13*b/161))>>23;        //3項4次分母を強引に2のべき乗化
    c=(1411823522-b*26569)/8661494;               //2項2次
*/
    c=(1367343104-b*25731)>>23;                   //2項2次分母を強引に2のべき乗化
    ratio*=c;                                     //cにratioを掛けて終了
    return ratio;
}



//ISin(a)=163*sin(a*PI/512)
int ISin(int a)
{
    int ratio=1;
    unsigned int b,c,d;

    ///////////////////
    // 1.引数の整形  //
    ///////////////////
    if(a<0){    //引数が負なら引数と戻り値反転
        a=-a;
        ratio=-1;
    }
    a%=1024;    //引数を0〜2PIに
//  a&=0x3ffff
    if(a>=512){ //引数>=PIなら引数をPIシフトし、戻り値反転
        a-=512;
        ratio=-ratio;
    }
    if(a>256){  //引数>=PI/2なら引数を鏡像に
        a=512-a;
    }

    ///////////////////////
    // 2.守備範囲の判定  //
    ///////////////////////
    //およそπ/4以上の領域では、cosで計算した方が高精度
//  if(a>151) return ratio*ICos(256-a);
    if(a>151) return ratio*ICosR(256-a);

    /////////////////////////////
    // 3.マクローリン式の計算  //
    /////////////////////////////
    b=a*a;
    d=a;
/*
    // aは8bit以下となったた
    // 3項5次までテイラー展開。(演算子^はべき乗を示す。a^b=pow(a,b))
    //  a -163/3!*(a/163)^3 +163/5!*(a/163)^5
    // =a -a^3/6/163^2 +a^5/120/163^5
    // アンダーフロー抑制//分子を変数範囲いっぱいに拡大
    // =[a*163^3*2 -a^3/3*163 +a^5/60/163^2]/163^3/2
    // =[a*163^3*2*3 -a^3*163 +(a^4*3/163^2)*a/60]/163^3/2/3
    // =[a*25984482 -a^3*163 +(a^4*3/26569)*a/60]/25984482  //32bit/25bit 
    // unsigned32で作業  a^4+αまでオーバーフロー回避
    // より高速/高精度が要求される場合、引数のレンジ別に使用する項数(次数)を使い分ける
    c=(d*25984482 )/25984482;                     //1項1次
    c=(d*25984482 -d*b*163)/25984482;             //2項3次
    c=(d*25984482 +(b*b*3/26569)*d/60 -d*b*163)/25984482;  //3項5次
    c=(d*16777216-b*d*105)>>24;                   //2項3次分母を強引に2のべき乗化
*/
    c=(d*(16777216-b*105))>>24;                   //2項3次分母を強引に2のべき乗化
    ratio*=c;                                     //cにratioを掛けて終了
    return ratio;
}



//引数チェック省略版。ISin()以外から呼ぶのは危険。
int ICosR(int a)
{
    unsigned int b,c;
    b=a*a;
    c=(1367343104-b*25731)>>23;                   //2項2次分母を強引に2のべき乗化
    return (int)c;
}



//引数チェック省略版。ICos()以外から呼ぶのは危険。
int ISinR(int a)
{
    unsigned int b,c,d;
    b=a*a;
    d=a;
    c=(d*(16777216-b*105))>>24;                   //2項3次分母を強引に2のべき乗化
    return (int)c;
}
 いかがでしょうか。  精度的には、最大で ±1 ( ±1/±163 = 0.6% ) の誤差が発生します。  次数を上げれば、速度と引換えに精度は向上します。          半径163pxの円弧を、純正の実数関数(白)とICos,ISin関数(黒)      で描いたものです。π/4の前後で1pxのずれが発生しています。  速度は、実数( double/64bit )の sin, cos に比べ、6 倍くらい出ます。  手元の Pen4-2.26GHz DualCH-DDR266 では、58 百万回/sec. 程度です。  データ長が半分である事を差し引いてもまずまずの結果と思います。  更なるスピードをお望みの場合は・・・テーブルですね。  トリッキーな内容はありませんが、注意すべき点を 2 つ  まず、% (剰余)演算子の扱いについて、第一 and/or 第二オペランドが負数  の場合は不定義のはずですが、MicroSoft VisualC++ 6.0は独自に定義して  値を返します。両オペランドの絶対値の剰余に、第一オペランドの符号が  付いた値が返ります。例えば、(-5)%3 == -(5%3) == -2 です。  mod(a,b)= a<0 ? -(abs(a)%abs(b)) : abs(a)%abs(b); なようです。  同じ事を Excel2000 で試してみると、(-5)%3 == 1 です。  これは、-5 = 3*(-2) + 1 との解釈です。  感覚的には Excel の方がしっくりくるし、今回作った関数の中でも負数の  反転が不用になって好都合なのですが。  次に、int の演算順序には注意してください。  2*3/4 == 1 ですが、2/4*3 == 0 です。  そんな事はわかっておる、と怒られそうですが、私はよく間違えます。  おまけ:正しく接する
次へ