sin of calc

Table of Contents

doubleの精度

ふと前回やったsinとかを求めてくれるfdlibmのコードを見ていて、疑問がわ いてきた。

half =  5.00000000000000000000e-01, /* 0x3FE00000, 0x00000000 */
 :
S6  =  1.58969099521155010221e-10; /* 0x3DE5D93A, 0x5ACFD57C */

これの何が疑問って、有効数字の桁数が、多すぎないか?

>>> len('1.58969099521155010221')
22

小数点を除いても、21桁も数字が並んでいるぞ。確か浮動小数点の仮数部に割 り当てられてる桁は、52Bitだ。これで表現できる10進数の桁は、

gosh$ (log (expt 2 52) 10)
15.65355977452702

こんなものだ。ネットで調べてみても、せいぜい16桁と説明されてた。これっ て過剰品質ではなかろうか? このコードの発祥地は今は無きSun社なので、確 かめる術はない。 そうだ、math.h に聞いてみよう。これなら十分に流通して るし、世間の目が光っているから、ローカル仕様と言う事もあるまい。

[sakae@deb fdlibm]$ grep M_PI /usr/include/math.h | head -5
# define M_PI           3.14159265358979323846  /* pi */
# define M_PI_2         1.57079632679489661923  /* pi/2 */
# define M_PI_4         0.78539816339744830962  /* pi/4 */
# define M_PIl          3.141592653589793238462643383279502884L /* pi */
# define M_PI_2l        1.570796326794896619231321691639751442L /* pi/2 */

M_PIl は、long double なんだな。まあdoubleの方を確認しよう。

>>> len('3.14159265358979323846')
22

何故だ? 全世界子供ネット相談室へ問い合わせ。

How Many Decimals of Pi Do We Really Need? によると、NASAではpiの精度を16桁目を四捨五入した15桁で利 用してるらしい。これで十分みたいだ。それより問題となるのは、浮動小数点 に特有の計算方法による精度落ちの方が、よっぽど難しいぞ。

そしてNASAと言うか米国ならではの、超ーー恥しい間違い、 メートル法とヤードポンド法は間違えやすい! 固くなに田舎の単位を使ってるか らだ。

そう言えば、ジェット旅客機の操縦法なんて本を読んだ事がある。決っしテロリス トになろうとした訳では無いので、誤解無きように。ボーイング社は、田舎の 単位に固執してる。エアバス社は国際単位ね。パイロットさんは大変だ。 間違いを減らす為、使う単位系を切り替えられるようになってるのかな?

ask ChatGPT

Q: /usr/include/mth.h に パイの定数が M_PI として定義されてます。 3.14159265358979323846 これ小数点を除くと21桁です。 一方、ieee754の定義では、仮数部22Bit で、10進数換算16桁となります。 ソースの過剰定義と思えてなりません。何か理由が有るのですか?

A: `<mth.h>` における `M_PI` の定義が小数点以下の桁数で IEEE 754 に準拠していないのは、確かに興味深い点ですね。一般的には、数学定数としての円周率 π は 3.14159265358979323846 と表されますが、IEEE 754 の倍精度浮動小数点数では約 15 〜 16 桁の精度しか保証されません。

一部のライブラリやヘッダーファイルでは、これを補完的な定数として提供していることがありますが、これは厳密な浮動小数点数の精度とは異なります。

このような過剰な桁数を定義する理由は、主に数学的な精度や扱いやすさを考慮した結果として考えられます。C 言語の `<mth.h>` など、特定のヘッダーファイルやライブラリでは、数学的な定数を便宜上拡張して提供することがあります。これは、プログラマがその精度を要求する場合には適切に丸めることが期待されるものです。

また、特定のプラットフォームやコンパイラに依存する場合もあります。そのプラットフォームやコンパイラが、自身のニーズに合わせて定義を行っている可能性も考えられます。

要するに、C 言語のヘッダーファイル内の定数は、数学的な定義とプログラミング上の実装のバランスを取ったものであり、必ずしも厳密な浮動小数点数の規格に合致する必要はない場合があります。

人生の秒数

doubleの有効桁数が16桁と知ったけど、それってドンダケー凄いの? 身の周 りで実感してみる。そう、世界が100人の村だったらって本が昔流行したみた いに、身辺感覚で捉えてみるのさ。

今の時代、人生100年と良く言われる。生まれた時に、蝋燭に点灯。それが燃 えつきたら、天寿を全うしたって奴ね。その蝋燭には、秒の刻みがついている。 さて、その刻みは、いか程に。また、何人分の刻みが、16桁で実現できる?

schemeでササッと計算。

gosh$ (/. (expt 2 52) (* 3600 24 365 100))
1428082.0736207813

goshを入れていない人向けに、bc で、検算する例をば。ドットは直前の演算 結果を保持しています。

3600 * 24 * 365 * 100
3153600000
2 ^ 52 / .
1428082
. / 10000
142

142万人分の人生を刻めるとな。とんでもない桁数だ。2021年の100才以上の日 本の人 口は、86510人らしい。また、都市人口だと、ピッタリは流石にない。京都市 が146万人で一番近いかな。次点はさいたま市の132万人あたりか。

why radian

もう一つ、相談が有るんだ。

Q: sin,cos,tanの計算に弧度法が使われるのは、何故ですか?

角度なら、普通360度の表現だろうにね。その謂れは、多分こうだったんじゃ なかろうか劇場で再現(by NHK)。

ケーキなりピザを等分に分ける時に便利な数は? 10進数じゃなくて、12進数 が良い。10進数じゃ、2、5、10の時しか、等分に分割できない。対して12進数なら、 2、3、4、6、12という具合に、色々に分割できる。普通の生活では、これで十分。 でも、困った人がいる。そう、測量が大事な人達。船員さんとかだ。

12より細かく分割したい。そこで目をつけたのは、1年が365日って所。でも、 この数値では、ちょいと座りが悪くて計算が面倒。そこで360を採用。かくし て、それが普通の人にも利用されるようになった。でも、数学的には???

A: sin、cos、tanの計算に弧度法が使われる理由はいくつかあります。

  1. 円周率との関係性: 弧度法は角度を円周率(π)を含む半径1の円の弧の長さとして表現します。これにより、三角関数の計算が円や周期的な現象との関連で簡単に扱えます。特に、三角関数のMaclaurin展開(テイラー展開の一種)では弧度法を使用しており、この展開はsinやcos、tanなどの三角関数の値を計算するための高速な近似式として使われます。
  2. 微分と関連性: 微分や積分の操作において、弧度法を用いた三角関数の導関数や不定積分が扱いやすい形になります。例えば、sinやcosの導関数を計算する場合、弧度法を使用すると計算が簡略化されます。
  3. 数学的な連続性: 弧度法は三角関数の数学的性質を調べる際に便利です。角度を弧度法で表すことで、sinやcos、tanなどの三角関数が滑らかで連続的な性質を持つことがわかりやすくなります。

弧度法は数学的な操作において三角関数をよりシンプルに扱えるため、三角関数を使う数学や物理の分野で広く採用されています。

病院での時間潰しで、たまたま持参した 『関数とはなんだろう』って本に、 ラジアン便利な説明が載ってた。半円を考えた時、その弧の長さはpiになるで しょ。半円なんだから、扇がバーと180度まで開いたのと一緒。だったら、180 度 = pi と考えてもいいな。中心角がθなら、弧の長さもθで都合がよい。度 だと計算が面倒だぞー、ってのが中坊向けの説明だったよ。なんだか騙された ような気がしないでもないけど、まー、そういう事だわさ。

debug calc

と、項目名を決めてみたけど、やりたい事は、emacsの電卓で sinの計算はど う実現されてるか調べる事だ。

カシオやシャープが発売してる(た)関数電卓の中身を調べる事に相当する。前回は libmを調べたけど、あちらは、いわば禁断の石FPUが、どうなってるかの調べ だったので、今回はより身近な存在の調査って訳だ。

まずは、定義場所の同定だな。場所が判らなければBPを置けませんから。エィっ と検索。

[sakae@fb /usr/local/share/emacs/29.1/lisp/calc]$ zgrep sin *.el.gz | grep defun
 :
calc-math.el.gz:(defun calc-sin (arg)
calc-math.el.gz:(defun calc-arcsin (arg)
calc-math.el.gz:(defun calc-sinh (arg)
calc-math.el.gz:(defun calc-arcsinh (arg)
calc-math.el.gz:(defun calc-sincos ()
 :

どうも、このファイルに詰っているみたい。calc-sin の所で、C-u C-M-x してBPを設定。そして実行。

(defun calc-sin (arg)
  (interactive "P")
  (calc-slow-wrapper
   (if (calc-is-hyperbolic)
       (if (calc-is-inverse)
           (calc-unary-op "asnh" 'calcFunc-arcsinh arg)
         (calc-unary-op "sinh" 'calcFunc-sinh arg))
     (if (calc-is-inverse)
         (calc-unary-op "asin" 'calcFunc-arcsin arg)
=>     (calc-unary-op "sin" 'calcFunc-sin arg)))))

どうもこの関数は、振り分けしてるみたいだな。実体は、calcFunc-sinっぽい。 今度は、そちらにBPを置いてみたけど、ヒットしない。何故かな?

野生の感で、このdirでdebuggerを使うのは不埒な事と思えた。なぜって、圧 縮したソースやら、*elcが同居してるから。

ならば、calc/ からソースだけを取り出してきて展開し、それをload-pathの 冒頭に配置してしまえ。そうすれば、一時的に、オイラーが全てのcalcを記述 しました。これからそれをdebugしますって、状況になるだろう。

load-path

今迄、余り気にしなかったけど、パッケージの検索順ってどうなってるのだろ う?

 *** Welcome to IELM ***  Type (describe-mode) or press C-h m for help.
ELISP> load-path
("~/.emacs.d/lisp" "/home/sakae/.emacs.d/elpa/cargo-20230125.1253" ...

ちょっと醜いので、ielmな結果をファイルに書き出して整理してみた。

[sakae@fb /tmp]$ cat path | tr ' ' '\n'
("~/.emacs.d/lisp"
"/home/sakae/.emacs.d/elpa/cargo-20230125.1253"
"/home/sakae/.emacs.d/elpa/cargo-mode-20230213.401"
  :
"/usr/local/share/emacs/29.1/lisp/calendar"
"/usr/local/share/emacs/29.1/lisp/calc"
"/usr/local/share/emacs/29.1/lisp/obsolete")

ほー、calcは最後に検索されるっていう不遇な位置になってるのね。

setup for calc

こんな風にして、配布された物のコピーを作る。

vbox$ mkdir calc
vbox$ cd calc/
vbox$ cp /usr/local/share/emacs/29.1/lisp/calc/*.el.gz .
vbox$ for f in *
> do
> gunzip $f
> done

忘れないように下記を、.emacs/init.el に追加。

(setq load-path (append '("~/calc" ) load-path))

これで検索順が一番になるはず。

debug again

(defun calcFunc-sin (x)   ; [N N] [Public]
=>(cond ((and (integerp x)
              (if (eq calc-angle-mode 'deg)
                  (= (% x 90) 0)
                (= x 0)))

おお、来た来た。丁度 x を評価し終ったんだけど、mini-bufferには、 Result: 45 (#o55, #x2d, ?-) と、入力した値が表示されてる。

歩を進めると、一瞬で、

        (t (calc-record-why 'scalarp x)
=>         (list 'calcFunc-sin x))))

に到達して、mini-bufferには、こんな結果が表示された。

Result: (float 707106781187 -12)

もういちど、のろまに進んでみるか。 それよりもキビキビとソースの海を泳いでみるか。

etags

TAGSを作成すれば、どんどん関数間を移動できる。使わない手はないな。

vbox$ etags --help
  :
  c          .c .h
  c++        .C .c++ .cc .cpp .cxx .H .h++ .hh .hpp .hxx .M .pdb
  c*         .cs .hs
  cobol      .COB .cob
  lisp       .cl .clisp .el .l .lisp .LSP .lsp .ml
   :
  python     .py
  ruby       Rakefile Thorfile .rb .ru .rbw
  rust       .rs
  scheme     .oak .rkt .sch .scheme .SCM .scm .SM .sm .ss .t
   :
vbox$ etags -V
etags (GNU Emacs 29.1)
Copyright (C) 2023 Free Software Foundation, Inc.
This program is distributed under the terms in ETAGS.README

昔と違って多様な言語をサポートしているな。ctagsグループの支援を得てい るのでしょうか。

etags *.el して、すいすいとソースの海を徘徊できる様になった。あちこち をウロウロしたけど泳ぎ 疲れて、結局は核心に迫れず。

ここで一つ注意がある。load-pathに登録されてるソースは、所定の場所にあ ると想定されるTAGSを参照するって事。だから、規定の場所からソースを取り 出してきてTAGSを作成しても、それは参照されない。結果として、そんな関数 は登録されていないとなってしまう。

もう一つ注意。補完すると、こんなのが出現。これしか、飛んでいけない。

In this buffer, type RET to select the completion near point.

13 possible completions:
calc                      calc-dispatch             calc-embedded
calc-embedded-activate    calc-eval                 calc-grab-rectangle
calc-grab-region          calc-grab-sum-across      calc-grab-sum-down
calc-keypad               calc-undo                 calculate-lisp-indent
calculator

自由にあちこち飛ぶには、calcを一度実行する必要があるみたい。そうすると 多数の候補が出現して、よりどりみどりになる。

これって、パッケージがロードされると、それも対象にしてくれるからかな? math- で始まる語句は、どうやらcalcを下支えするプリミティブみたいだけど、 これも堂々とノミネートされるよ。

折角etagsが出てきたので、原点を探ってみる。ctagsとペアね。

[sakae@deb lib-src]$ wc ctags.c etags.c
     2      5     35 ctags.c
  8071  30615 211848 etags.c
  8073  30620 211883 total
[sakae@deb lib-src]$ cat ctags.c
#define CTAGS 1
#include "etags.c"

うり二つのソーセージもとえ双生児だわい。で、中身のrubyの項でも散策。

static void Prolog_functions (FILE *);
static void Python_functions (FILE *);
static void Ruby_functions (FILE *);
static void Rust_entries (FILE *);
static void Scheme_functions (FILE *);

static const char *Ruby_suffixes [] =
  { "rb", "ru", "rbw", NULL };
static const char *Ruby_filenames [] =
  { "Rakefile", "Thorfile", NULL };
static const char Ruby_help [] =
  "In Ruby code, 'def' or 'class' or 'module' at the beginning of\n\
a line generate a tag.  Constants also generate a tag.";
static const char *Ruby_interpreters [] =
  { "ruby", NULL };

static language lang_names [] =
{
    :
  { "ruby",      Ruby_help,      Ruby_functions,    Ruby_suffixes,
                 Ruby_filenames, Ruby_interpreters },

/*
 * Ruby support
 * Original code by Xi Lu <lx@shellcodes.org> (2015)
 */
static void
Ruby_functions (FILE *inf)
{

これ、rubyで検索して出てきたもの。肝心の本体部分は、165行であった。 pythonは誰の寄贈かと調べたら、Idea by Eric S. Raymond だってさ。見てい て、あきないな。

see manual

困った時のマニュアル頼リとばかり、Programmingに狙いを定めてざっと見し てたんだ。そしたら、17.5.1 Defining New Functions で、calc専用のマクロ が紹介されてた。例は勿論lisp界のハロワね。

(defmath myfact (n)
  (if (> n 0)
      (* n (myfact (1- n)))
    1))

こういうマクロ相当を記述すると、

(defun calcFunc-myfact (n)
  (if (math-posp n)
      (math-mul n (calcFunc-myfact (math-add n -1)))
    1))

こんな風に展開されるとね。これから発展していって、

自前のsin

17.5.5.2 The Sine Function に ピッタリの例が出てた。

(defmath mysin ((float (anglep x)))
       (interactive 1 "mysn")
       (setq x (to-radians x))    ; Convert from current angular mode.
       (let ((sum x)              ; Initial term of Taylor expansion of sin.
             newsum
             (nfact 1)            ; "nfact" equals "n" factorial at all times.
             (xnegsqr :"-(x^2)")) ; "xnegsqr" equals -x^2.
         (for ((n 3 100 2))       ; Upper limit of 100 is a good precaution.
           (working "mysin" sum)  ; Display "Working" message, if enabled.
           (setq nfact (* nfact (1- n) n)
                 x (* x xnegsqr)
                 newsum (+ sum (/ x nfact)))
           (if (~= newsum sum)    ; If newsum is "nearly equal to" sum,
               (break))           ;  then we are done.
           (setq sum newsum))
         sum))

ここでも、テーラー展開である。defmathの中では、for構文が使える。これで、 展開の次数をどんどんと増加しながら計算。前の結果とほぼ等しかったら(浮 動小数点なんで、等しいかの比較は不可)終了という戦略だ。

これに続く節では、色々と注意点が説明されてた。

このマクロがどう展開されるかの補助関数は、残念ながら提供されていない。

組み込み

うえのコードを実際に確認できる。.emacs/calc.el ってファイルに記述す るだけ。

'mysin(45) と入力。代数式って扱かいだ。

alg' mysin(45)
     0.707106781179

calcの右窓に、こんな結果が出てくる。

別の方法として、この関数をキー・バインドしちゃって、計算したい値をスタッ クに置き、それからキー操作するって事もできる。

まずは、キーの登録。ZDと入力すると、下記の質問がなされる。

Define user key: z-q
Set key z q to command: calc-mysin

使ってみる。もう普通の気分で、30 zq するだけだ。

     30
mysn 0.499999999998

defmath

どんな風になってるんだろう? 今迄tag jumpは、関数名の所にカーソルを合 わせてから、M-. で、飛んでけー だと思ってた。

何も無い所で、M-. すると、何をお探しでしょうかご主人様って聞いてくるん で、適当に数文字を入力してからTABで補完。

今回なら、def TAB したら、defmacro,defmath って、絞りこんでくれたぞ。 なかなか便利な機能だ。

このマクロの実体は、calc-prog.elに有った。

;;;; User-programmability.

;;; Compiling Lisp-like forms to use the math library.

(defun math-do-defmath (func args body)
  :

この中で、defunを利用して、動的に関数を登録してる。

           `(defun ,(intern (concat "calc-" (symbol-name func)))
              ,(if (or hasprefix hasmulti) '(&optional n) ())
              ,@doc
      :
(defun ,fname ,clargs
  ,@doc
  ,@(math-do-arg-list-check args nil nil)
  ,@body))))

ゆえに、マクロ展開なんてのは無理な相談なのかな?

How many times

でも、面白いのを見付た。

(defconst math-prim-funcs
  '( (~= . math-nearly-equal)
     (% . math-mod)
      :

mysinの所で出てきた、ほぼ等しいって関数の内部定義、math-nearly-equal。 これ、どうやってるのだろうか? これに注目するのには訳がある。 そう、何回callされてるか、edebuggerの起動回数で確認できるからね。

;;; True if A and B differ only in the last digit of precision.  [P F F]
(defun math-nearly-equal-float (a b)
=>(let ((ediff (- (nth 2 a) (nth 2 b))))
    (cond ((= ediff 0)   ;; Expanded out for speed
     :

math-nearly-equalでは、型によって場合分けしてて、実働部分は、 math-nearly-equal-floatの方になる。

p 4 して、4桁の精度を指定し、mysin(45)を計算させると、4回で終了した。p 30 だと、45回のcallで、やっと結果が得られた。0.707106781186547524400844362106

floatの内部表現は、

Result: (float 704652651209167527908691275333 -30)

こんな風に、仮数部を、整数の多倍長で保持し、それに指数部って事にしてる んだな。これなら、無限長の浮動小数点を扱えて便利。


This year's Index

Home