使用 Forth 语言绘制 Mandelbrot 集

Mandelbrot 集是最著名的分形图形之一。下面的 Forth 程序用来绘制 Mandelbrot 集。

定点数的表示

采用 Qint.frac 格式的定点数进行计算。int 为整数部分(包含符号位,若有)的位数,frac 为小数部分的位数。在下面的程序中,定点数的宽度为 16 位(与 Forth 中的单精度整数相同),即 int+frac=16

Qint.frac 格式的定点数也就是以 \(2^{\mathtt{frac}}\) 为分母的分数,存储时只存储分子。显然,这种格式的定点数的加减法与整数运算相同,一个定点数与一个整数的乘法也与两个整数运算相同。计算两个定点数的乘法时,先将它们按整数相乘(中间结果为双精度整数),然后右移 frac 位(也就是除以 \(2^{\mathtt{frac}}\))。如果相乘后的结果没有溢出,则只保留低 16 位为结果即可。1

定点数计算的 Forth 实现

Q6.10 为例。定义

10 constant FRAC
1 frac lshift constant ONE  \ 2^10=1024
one 1- constant MASK        \ 1023
16 frac - constant INT      \ 6

将单精度整数转换为 Qint.frac 定点数使用 frac LSHIFT ( d -- q );将单精度整数和小数部分(小于 one)转换为 Qint.frac 定点数使用 frac LSHIFT OR ( d_frac d_int -- q )

将双精度中间结果右移 frac 位,保留低 16 位:

: QRSHIFT   ( L H -- H_L>>frac )
   swap     ( H L )
   frac rshift  ( H L>>frac )
   swap     ( L>>frac H )
   mask and ( L>>frac H_lower_frac_bits )
   int lshift or \ result is (L>>frac)|(H_lower_frac_bits<<int)
   ;

两个定点数相乘:

: Q* ( q1 q2 -- q1*q2 ) m* qrshift ;

打印定点数:

: Q. ( q -- ) dup abs ( q |q| )
   <#
      dup mask and 10000 one */ 0 ( q |q| d_|q|_frac_decimal )
      # # # # 2drop \ print frac part in decimal
      [char] . hold \ print decimal point
      frac rshift 0 ( q |q| d_|q|_int ) #s \ print int part
      rot sign  \ print sign
   #> type space ;

测试:

0 q. 0.0000  ok
32767 q. 31.9990  ok
32768 q. -32.0000  ok
one one 1 rshift + q. 1.5000  ok
one one 1 rshift + . 1536  ok
1536 1536 q* q. 2.2500  ok
1536 -1536 q* q. -2.2500  ok
-1536 1536 q* q. -2.2500  ok
-1536 -1536 q* q. 2.2500  ok

Mandelbrot 集的计算

对复平面上的点 \(c\),计算映射

\[ z_{n+1}=z_n^2+c: \quad z, c \in \mathbb{C}, z_0=0 \]

直到 \(|z|>2\). 设定最大迭代次数(如 15),按达到 \(|z|>2\) 或最大迭代次数时的 \(n\) 值为不同颜色在复平面上 \(c\) 点位置画点。

将 \(z\) 写为 \(z_\mathrm{x} + z_\mathrm{y} \mathrm{i}\) 的形式,有:

\[ \begin{align*} {z_\mathrm{x}}_{n+1} = & {z_\mathrm{x}}_n^2 - {z_\mathrm{y}}_n^2 + c_\mathrm{x}, \\ {z_\mathrm{y}}_{n+1} = & 2 {z_\mathrm{x}}_n {z_\mathrm{y}}_n + c_\mathrm{y}. \end{align*} \]

计算复数的模及比较其是否大于 2(为避免开方,实际比较的是 \(|z|^2>4\)):

: QSQR ( q -- q*q ) dup q* ;
: Q-MAG-SQR ( qa qb -- qmag^2 ) qsqr swap qsqr + ;
4 one * constant FOUR
: MAG2> ( qa qb -- is_mag_>_2 ) q-mag-sqr four > ;

使用 ANSI 转义序列改变字符的背景色:

: COLOR-CODE ( c -- ) \ print color escape codes
  0 <# [char] m hold #s [char] [ hold 27 hold #> type ;
: COLOR ( 0~15 -- )
   dup 8 and if
      7 and 100     \ 8~15: 100...107
   else 
      40            \ 0~7: 40...47
   then  
   + color-code ; 

一次迭代:

: MANDEL-ITER   ( qzx qzy qcx qcy -- qzx1 qzy1 )
   2swap 2dup   ( qcx qcy qzx qzy qzx qzy )
   q* 2*        ( qcx qcy qzx qzy 2*qzx*qzy )
   3 roll +     ( qcx qzx qzy qzy1=2*qzx*qzy+qcy )
   -rot         ( qcx qzy1 qzx qzy )
   qsqr swap    ( qcx qzy1 qzy^2 qzx )
   qsqr swap    ( qcx qzy1 qzx^2 qzy^2 )
   -            ( qcx qzy1 qzx^2-qzy^2 )
   rot +        ( qzy1 qzx1=qzx^2-qzy^2+qcx )
   swap ;       ( qzx1 qzy1 )

计算某个 \(c\) 点的 \(n\) 值:

16 constant MAX-ITER
: MANDEL-POINT ( qcx qcy -- iter )
   0 0 0            ( qcx qcy qzx0=0 qzy0=0 iter0=0 )
   max-iter 0 do    ( qcx qcy qzx qzy iter )
      drop          ( qcx qcy qzx qzy )
      2dup mag2>    ( qcx qcy qzx qzy is_mag_>_2 )
      if 
         i leave    ( qcx qcy qzx qzy iter ) 
      then          ( qcx qcy qzx qzy )
      2over         ( qcx qcy qzx qzy qcx qcy )
      mandel-iter   ( qcx qcy qzx1 qzy1 ) 
      i ( qcx qcy qzx1 qzy1 iter )
   loop
   ( qcx qcy qzxn qzyn iter ) 
   nip nip nip nip ( iter ) ;

绘制 Mandelbrot 集时,\(x\) 的范围为 \(-2 \cdots 1\),\(y\) 的范围为\(-1 \cdots 1.5\) 加适当余量,并合理设置步长:

one 2 * negate constant XMIN
one constant XMAX
one 24 / constant XSTEP
one 8 / constant YSTEP
one negate ystep 2* - constant YMIN
one one 2 rshift + ystep + constant YMAX

使用二重循环绘制 Mandelbrot 集:

: MANDEL ( -- )
   page
   ymax ymin do
      xmax xmin do
         i j mandel-point color space
      xstep +loop
      cr
   ystep +loop
   0 color-code ;

绘制下图在 22.1184MHz 的 My4TH 上约用时 2 分 52 秒。

Mandelbrot Set

完整的程序如下:

----[200]-------------------------------------------------------
\ C Mandelbrot Set

\ Qint.frac format fixed-point number
10 constant FRAC   1 frac lshift constant ONE
one 1- constant MASK  16 frac - constant INT
: QRSHIFT  ( L H -- H_L>>frac )
   swap frac rshift swap mask and int lshift or ;
: Q* ( q1 q2 -- q1*q2 ) m* qrshift ;
: Q. ( q -- ) dup abs  <# dup mask and 10000 one */ 0 # # # #
   2drop  [char] . hold  frac rshift 0 #s rot sign #>
   type space ;
\ utils for Mandelbrot set calculation
: QSQR ( q -- q*q ) dup q* ;
: Q-MAG-SQR ( qa qb -- qmag^2 ) qsqr swap qsqr + ;
4 one * constant FOUR
: MAG2> ( qa qb -- is_mag_>_2 ) q-mag-sqr four > ;           -->
----[201]-------------------------------------------------------
\ iteration and point calculation for the Mandelbrot set

: MANDEL-ITER ( qzx qzy qcx qcy -- qzx1 qzy1 )
   \ qzx1 = qzx^2 - qzy^2 + qcx
   \ qzy1 = 2*qzx*qzy + qcy
   2swap 2dup q* 2* 3 roll + ( qcx qzx qzy qzy1 )
   -rot qsqr swap qsqr swap - rot + swap ; ( qzx1 qzy1 )
16 constant MAX-ITER
: MANDEL-POINT ( qcx qcy -- iter )
   0 0 0 max-iter 0 do  drop \ drop i in new iter
      2dup mag2> if i leave then ( qcx qcy qzx qzy )
      2over mandel-iter ( qcx qcy qzx1 qzy1 ) i loop
   ( qcx qcy qzxn qzyn i ) nip nip nip nip ( i ) ;


                                                             -->
----[202]-------------------------------------------------------
\ plot the Mandelbrot set

: COLOR-CODE ( c -- ) \ print color escape codes
  0 <# [char] m hold #s [char] [ hold 27 hold #> type ;
: COLOR ( 0~15 -- )
   dup 8 and  if 7 and 100 else 40 then  + color-code ;

one 2 * negate constant XMIN  one constant XMAX
one 24 / constant XSTEP  one 8 / constant YSTEP
one negate ystep 2* - constant YMIN
one one 2 rshift + ystep + constant YMAX

: MANDEL ( -- )  page
   ymax ymin do  xmax xmin do
      i j mandel-point color space
   xstep +loop  cr  ystep +loop  0 color-code ;
----[EOF]-------------------------------------------------------

  1. 除法的计算:一个定点数除以一个整数,与整数运算相同;两个定点数相除时,先将被除数左移 frac 位(乘以 \(2^{\mathtt{frac}}\)),然后除以除数。注意中间结果的位数必须足够宽,Forth 中可以用 M*/ 计算。 ↩︎