- 星ネズミ
- Feb 26, 2018
Java でのsin(π)の扱い
sin(π)の計算値が0にならない場合などの対処法を考えてみた
Android用の電卓アプリを、作っていた時に困った問題が起こりました。それはsin(π)を計算させると0にならないというものです。プログラムではJavaを利用しているのですが、Javaではsin(π)を計算するにはMath.sin(Math.PI)と書きます。これを実行させると結果は0ではなく、1.2246467991473532E-16 となります。
これは、当たり前の結果だといえます。Javaに限らずプログラム内では実数は二進数で扱われていますから十進数に変換する場合、誤差が生じますし有限の桁でしか扱う事ができません。例えばJavaのdouble型は有効桁数が十進数で約16桁なのでπのような数値では(小数点以上の数が3で一桁だから)小数点以下15位までが使える事になります。実際Math.PIの値を出力させてみると 3.141592653589793となります。こういった事柄から生じる誤差でsin(π)が0にならなくても何の不思議もないというより、むしろ当然の事なのですが勝手にそこはMath.sin関数内でうまく処理してくれているのではないかと思い込んでいました。ちなみに筆者のスマホに入っているASUSの電卓アプリでsin(π)を計算させると0と出力されます。
2.計算値を制限する
この問題に対する対処法として、まず考えられるのは出力値がある値以下になった時は0にしてしまうというものです。例えば1.0E-15以下なら0として出力するというものです。以下にプログラム例を示します。
public static void main(String[] args) { double arg_val = Math.PI; System.out.println("sin("+arg_val +")= " + SinFunc(arg_val)); arg_val = Math.PI + 0.1; System.out.println("sin("+arg_val +")= " + SinFunc(arg_val)); } public static double SinFunc(double arg_val){ double calc_val = Math.sin(arg_val); if(Math.abs(calc_val) > 1.0E-15) return calc_val; return 0.0; }メソッドSinFuncが出力制限したプログラムです。出力値の絶対値をとっていることに注意してください。これを忘れるとsin(-π/2)など結果が負になる場合などがみんな0になってしまいます。
さてこれで問題は解決…と思ったのですが、気になることがありました。これ引数が10πとか100πとか大きな値になったらどうなるのでしょうか?上でも書いたように実際のπの値と計算で使う値には誤差がありますが、これが10倍、100倍となれば実際の値からのずれが大きくなるはずです。実際計算させるとsin(10π)が-1.2246467991473533E-15 でsin(100π)が1.964386723728472E-15となり上のSinFuncでは当然0にはなりません。もちろん1.0E-15をもっと大きな数にすればいいのですが、もっと他の方法はないものか考えてみました。一つは引数が特定の値の時のみ0にしてしまうというものです。上で上げたASUSの電卓アプリですがsin(π)を計算すると0になりますが、sin(3.141592653589794)を計算させると-7.657137E-16になります。ちなみにMath.sinで同じ計算をさせると-7.657137397853899E-16になります。これから考えるとこのアプリは計算結果ではなく引数の方で制限をかけているのではないかと推測できます。ちなみにsin(200π)でも0を出力しましたがsin(250π)では0にはなりませんでした。
3引数を平行移動する
他に考えたのがsinなどの三角関数は周期関数なのだから、ある程度以上大きな引数は平行移動して例えば -π~π(または0 ~2π)にして扱う方法でした。それを実行するのが以下のプログラムです。TransRAngInMPPiの引数には変換したい値をラジアン単位で与えます。
//Math.sin計算の引数θを -Π < θ <= Π 内に制限する(変換) public static double TransRAngInMPPi(double ang_urad){//ラジアン単位の角度 double divisor = Math.PI*2.0 ;//2π double the_remind = ang_urad%divisor; if(the_remind <= -Math.PI) the_remind += divisor; else if(the_remind > Math.PI) the_remind -= divisor; return the_remind; } //TransRAngInMPPiと余りを計算する部分が違うメソッド public static double TransRAngInMPPi2(double ang_urad){//ラジアン単位の角度) double divisor = Math.PI*2.0 ;//2π int num_dv= (int)(ang_urad/divisor);//以下三行が double int_part = divisor*num_dv ;// double the_remind = ang_urad%divisor;の double the_remind = ang_urad - int_part;//代わり if(the_remind <= -Math.PI) the_remind += divisor; else if(the_remind > Math.PI) the_remind -= divisor; return the_remind; }ただし変換前の数値に誤差が含まれているので、それを平行移動してもあまり意味がない事に後になって気がつきました。実際計算すると場合によっては何もしない場合よりかえって悪くなる場合があります。
そこで平行移動するのは同じですが、BigDecimalクラスを使ったプログラムを作ってみました。
//TransRAngInMPPi をBigDecimalを使って計算 public static BigDecimal TransRAngInMPPi(BigDecimal trgval_bd,int setPrec){//丸目に必要な桁数 BigDecimal divisor_bd =new BigDecimal("6.283185307179586");//2π BigDecimal pi_bd = new BigDecimal("3.141592653589793"); BigDecimal piM_bd = new BigDecimal("-3.141592653589793"); BigDecimal the_renind ; BigDecimal trInt_p = trgval_bd.divideToIntegralValue(divisor_bd); System.out.println("divideToIntegerValue " + trInt_p.toString()); BigDecimal rem_p = trgval_bd.subtract(trInt_p.multiply(divisor_bd)); //System.out.println("rem :"+rem_p.toString()+"==>"+ trgval_bd.toString()+"-"+ trInt_p.multiply(divisor_bd).toString() ); if(setPrec < 0) the_renind = trgval_bd.remainder(divisor_bd); else the_renind = trgval_bd.remainder(divisor_bd,new MathContext(setPrec)); int the_subtrc = CompSize(the_renind ,pi_bd); if(the_subtrc > 0) the_renind = the_renind.subtract(divisor_bd);//引く else if(CompSize(the_renind ,piM_bd) <= 0) the_renind = the_renind.add(divisor_bd);//足す return the_renind; } //BigDecimal 不等号のかわり 上のTransRAngInMPPi内で使用 //target_1 > target_2 :1 target_1 == target_2 :0 target_1 < target_2 : -1 public static int CompSize(BigDecimal target_1 ,BigDecimal target_2){ double subtrac = ( target_1.subtract(target_2) ).doubleValue(); if(subtrac > 0) return 1 ; else if(subtrac < 0) return -1; return 0 ; }メソッドTransRAngInMPPiの第二引数は有効桁数を指定します。指定しないときは-1を入れるようになっています。
これを使ってsin(100π)とsin(101π)を計算したところよい結果を得る事ができました。
ただしBigDecimalクラスを利用する場合も、計算値やπに与える桁数などきちんと考慮しないと、変な答えが出てくる場合が、ありそうなので、もう少し考える必要がありそうです。
実は最初BigDecimalクラスを利用するなら、そもそもπの値の桁をもっと取ればいいのではと思ったのですが、最後にMath.sinを使うのでそんな事をしても全く意味がなかったです。
Comments
Add your comment