分形之曼德勃罗集图象的精确计算与生成-C++/GMP实现和验证


  

(图库见这个网页。)

网上有个程序backfract(链接在此Backfract。其作者还发布了一个 围棋程序前端cgoban,百度中搜backfract搜不到,搜cgoban可以搜到),用来显示分形中的曼德勃罗集。这个程序可以自动全屏显示Mandelbrot集的 区域图象,效果不错(我在此显示了一些效果图)。这个程序是运行在_nix/X上的。 网页上介绍其是“随机”选取“有趣的”区域进行显示,但不纯是放大,有时放大,有时接着又缩小。 一些视频网上也有称放大10的“N”次方倍的分形视频,视频放一会就没了。我想能否自己无穷放大,至少尽量放大。
数学上,Mandelbrot集有两种定义。 在《混沌分形及其应用》、《分形的计算机图象及其应用》中大致是这样说的: Mandelbrot集与另一种集合Julia集有联系。记以点c为参数的二次映射$ f_{c} = z^2 + c $ , 复平面任意一点c都各有相应的Julia集, 有的点c相应的Julia集是连通的,有的点c相应的Julia集是不连通的,从这有如下的Mandelbrot集的定义:

Mandelbrot集M是使c的$f_{c}$映射下的Julia集为连通的参数c的集合,即

$$ \begin{aligned} M = \left \{ c \in \mathbb{C} \space \space | \space \space J(f_{c}) \text{为连通的} \right \} \end{aligned} $$ 如果在复平面上把属于连通的Julia集的每个c都标绘出来,则此参数c的集合就是M集的分形图案。这个定义不好用计算机处理,幸好数学上 证明了Mandelbrot集的另一个等价的定义: $$ \begin{aligned} M = \left \{ c \in \mathbb{C} \space \space | \space \space \lim_{n \to \infty} Z_{n} \nrightarrow \infty \right \} \end{aligned} $$ 其中: $$ \begin{aligned} Z_{0} & = 0 \\ Z_{n+1} & = Z_{n}^2 + c \end{aligned} $$ 在这个网页(The Mandelbrot Set)讲了一些实际绘制时的问题处理。
(1)判断无穷 数学上可以证明Mandelbrot集完全被包含在以原点为中心,半径为2的闭圆盘内,即: $$ \begin{aligned} M \subset \left \{ c \in \mathbb{C} \space \space | \space \space \left| c \right| \leqslant 2 \right \} \end{aligned} $$ 所以如果迭代中间结果模大于2了,则继续迭代肯定趋于无穷了。因此用模2作一个阈值。
(2)迭代次数 上述网页的作者用了个50次的例子。但我遇到过迭代50次与迭代200或1000次结果不同的情形。所以这个问题的数学根据我还不太清楚。
(3)着色 实际上Mandelbrot集只是中间那个(黑色)形状的部分。其外的点都不属于Mandelbrot集。简单地绘制一个属于或不属于的黑白图象 没太多好看。我们通常看到的的颜色丰富的各种细节有赖于着色。常见用“逃逸”的点的迭代次数取模来选取颜色。(另有的类似等高线绘制,这里不用。) 但简单的着色比不了象backfract这样的程序的效果。
(计算绘图窗口坐标$(i,j)$的点是否属于M集并着色的伪码):
$x_{0} = $ left_coord + delta* $i$;
$y_{0} = $ above_coord - delta* $j$;
$ x = x_{0}$ ;      // or $ x = 0 $
$ y = y_{0}$ ;      // or $ y = 0 $
int $ k = 0 $ ;
while( $ k \lt $ MAX_ITERATION )
{
   real = $ x^2 - y^2 + x_{0} $;
   imag = $ 2xy + y_{0} $ ;
   $ {|z|}^2 = $ real*real + imag*imag ;
   if( $ {|z|}^2 \gt 4 $ )
     break;
   else{ $ x = $ real; $ y = $ imag; }
   $k$ ++;
}
if( $ k \lt $ MAX_ITERATION )
  SetPixel(hdc, $i$, $j$ , colors[ $k$ %50]) ;
else
  SetPixel(hdc, $i$, $j$ , RGB(0, 0, 0)) ;

一般的浮点运算有IEEE 754描述,计算机体系结构的书会涉及。还有书如《浮点计算编程原理、实现与应用》(刘纯根著)进行论述。网上有篇 著名的文章:What Every Computer Scientist Should Know About Floating-Point Arithmetic.

本文前后涉及3个程序:
第一个是用机器浮点double类型、VC++绘制Mandelbrot集;另可用“橡皮筋”选取区域放大各局部;
第二个在以上代码中加入作者float算法实现,比较上述机器浮点运算结果;但尚未修改完成“橡皮筋”选取放大功能;
第三个是未用图形界面,纯比较笔者float算法实现的计算结果和GMP分数运算结果的一致性的程序。
最初3个程序是在(虚拟机)Win XP + VC++6环境下开发的,后来升级到(虚拟机)Win7 + vs2015环境,有略微的修改。 这里两个版本都提供:
VC++6版本      vs2015版本
(代码未加整理,:-()

第1个程序

(1)double实现
这个运算部分代码和伪码差不多了:
my_mandel_test_3(machine_float)\my_mandel_testView.cpp:
    for(i = 0; i < w; i++)
	{
		for(j = 0; j < h; j++)
		{
			BOOL b_is_boundary = FALSE;
			if(delta_w > delta_h)
			{
			     if(   j ==     (int)( (h*delta - (old_above_coordinate-      \
			                                       old_below_coordinate))/(2*delta) )
					|| j == ( h-(int)( (h*delta - (old_above_coordinate-      \
					                               old_below_coordinate))/(2*delta) ) )
				   )
				   b_is_boundary = TRUE;
			}
			else
			{
			     if(    i ==     (int)( (w*delta - (old_right_coordinate-     \
			                                        old_left_coordinate))/(2*delta) )
					 || i == ( w-(int)( (w*delta - (old_right_coordinate-     \
					                                old_left_coordinate))/(2*delta) ) )
				   )
				   b_is_boundary = TRUE;
			}

			double x0 = left_coordinate  + delta * (double)i;
			double y0 = above_coordinate - delta * (double)j;
			double x  = x0;
			double y  = y0;

			int k = 0;
#define MAX_ITERATION 1000
			while(k < MAX_ITERATION)
			{
				double real_part      = x*x-y*y+x0;
				double imaginary_part = 2*x*y+y0;
                double z_abs_square = real_part*real_part + imaginary_part*imaginary_part;
				if(z_abs_square > 4.0)
				{
					break;
				}
				else
				{
                    x = real_part;
					y = imaginary_part;
				}
			    k++;
			}
			if(k < MAX_ITERATION)
			{
				//unsigned long colors[16] =
				unsigned long colors[50] =
				{
					RGB(0xDE , 0xB8 , 0x87),
					       ...
					RGB(0x19 , 0x19 , 0x70 )

				};
				//SetPixel(hdc, i, j , colors[k%16]);
				SetPixel(hdc, i, j , colors[k%50]);
				if(b_is_boundary)
					SetPixel(hdc, i, j , RGB(255, 255, 255));
			}
			else
			{
	            SetPixel(hdc, i, j , RGB(0, 0, 0));
				if(b_is_boundary)
					SetPixel(hdc, i, j , RGB(255, 255, 255));
			}
		}
	} 
 

运行图如:(为避免太占版面,下面是网页显示比例缩小了的;原比例的在此。 也可右键下载图片到本地看。)


选取的放大区域与绘图窗口比例可能不一致,所以将其作一定的延展来适应绘图窗口,让整个绘图窗口的像素点都有图象。上面图象 中的两道白线内部即为用户选取的区域(小图可能有些显示不清,可看原图),相对绘图窗口可能过于扁平或过于瘦高。上面代码中 b_is_boundary的判断处理即是为此。
(程序运行后产生两个中间文本文件draw_coordinates.txt和arguments.txt,下次运行前要把它们删去。)
机器浮点,二进制精度53位,相当于(十进制)小数点后约16位。起始坐标上下、左右均为约(-2,2)量级,若每次选取放大的 区域线度约为原图象的十分之一,那么可以发现大约16次后图象基本上就“马赛克”了。仅仅放大16次,离无穷放大太远了。 而且,后面还将看到机器浮点运算的误差累积在约40次迭代后就已出现明显差误的log记录。

第2个程序

(2) my floats实现
由于计算中仅用了乘法和加法,对有限位小数应该可以作完全精确的计算。按照小学运算的方法即可。为了只需计算有限位的小数, 只使用有限位的起始坐标和有限位的步进值,后者通过规定图象宽度高度为特定值,如500、512、800、1000、1024,从而像素代表的点 之间间距也由简单的有限位小数来表示。
所以我在View.cpp中修改了代码,只支持这些特定值。
my_mandel_test_my_floats_vs2015\my_mandel_test_my_floats_vs2015View.cpp:
    if (!(w == 200 || w == 250 || w == 400 || w == 500
		|| w == 800 || w == 1000 /*|| w == 1024*/)
		|| !(h == 200 || h == 250 || h == 400 || h == 500
			|| h == 800 || h == 1000 /*|| h == 1024*/)
		)

	{
		AfxMessageBox(L"width and height not supported");
		return;
	}      
	
	char* one_w_strI = (w == 200) ? "0.005" : (
		w == 250 ? "0.004" : (
			w == 400 ? "0.0025" : (
				w == 500 ? "0.002" : (
					w == 800 ? "0.00125" : (
						w == 1000 ? "0.001" : ("error characters")
						)
					)
				)
			)
		);

	char* one_h_strI = (h == 200) ? "0.005" : (
		...

 

读者可能还需在View.h中改代码,设置符合上述规定的合适你显示区域的程序窗口宽高:
my_mandel_test_my_floats_vs2015\my_mandel_test_my_floats_vs2015View.h:
#define DEFAULT_CLIENT_AREA_WIDTH   500//1000
#define DEFAULT_CLIENT_AREA_HEIGHT  250// 800
 

起始坐标则是x:-2.5到2.5,y:-2.0到2.0:
my_mandel_test_my_floats_vs2015\my_mandel_test_my_floats_vs2015View.h:
fI  fI_left_coord("-2.25");
fI  fI_right_coord("2.25");
fI  fI_above_coord("2.0");
fI  fI_below_coord("-2.0");
 

加入了实现my floats的三个新文件my_floats.h、my_floatI.cpp、my_floatII.cpp和几个辅助函数。(我在VC6编译项目时有时会报不通过, 把文件重新加入工程一次后又通过了,在vs2015上则完全通不过,报nafxcwd.lib/uafxcwd.lib与libcmtd.lib中有重复定义。遇到这种情况 可能得调整一下工程,在项目的link选项的input栏中先忽略这两个库,然后在其他依赖库中按如上先后顺序加入这两个库。) 我先后用了 两种数据结构来实现float数类,由于实现了前一种后想到后一种可能更好,所以先后完成了两种实现。
实现一:
my_floats.h:
class float_number{
public:
   bool   minus;
   char*  number;  //in absolute value form
   int    point_pos;
   
   ...
};
 

实现二:
my_floats.h:
class floatII{
public:
   bool  minus;         //with '-' sign?
   char* digits;        //no '.' inside
   int   point_pos_r;   //counting from the right side
   
   ...
};
 

有时简记为:
my_mandel_test_my_floats_vs2015\my_mandel_test_my_floats_vs2015View.cpp:
typedef float_number fI;
typedef floatII      fII;
 
两者都用单独的成员minus表示正负,所以数字字符串(number或digits)中都不再带符号。区别一是实现前一种时数字字符串中还保留着小数点, 这在后者中去掉了;区别二是小数点的位置,前者是从左到右数的(point_pos),后者则改为从右往左数了(point_pos_r)。由此代码实现起来 后一种简洁一点。这两种实现可以互相比较结果,进行验证。这是验证的方法之二。不过这两者的算法核心是一样的代码,所以可能验证的功能因此有限。 完全的验证还有赖于下面第3个实现了与GMP计算结果对照的程序,作为验证的方法之三,也可以说是真正的验证。(验证方法一是一种局部的验证, 只验证加法,即在作加法后作逆运算回去,看结果是否正确还原。这个没怎么用。)
实现了float类,以fI实现为例,运算代码变成了大致这样子:(实际的代码由于要相互比较,代码间会互相穿插。这里以早期代码版本为例。)
my_mandel_test_my_floats_0\my_mandel_testView.cpp:
fI  fI_left_coord ("-2.25");
fI  fI_right_coord( "2.25");
fI  fI_above_coord( "2.0" );
fI  fI_below_coord("-2.0" );

fI delta_wI, delta_hI, deltaI;
    
#if 1				
fI w_factorI(one_w_strI);
fI h_factorI(one_h_strI);

delta_wI = (fI_right_coord - fI_left_coord ) * w_factorI;
delta_hI = (fI_above_coord - fI_below_coord) * h_factorI;

if(delta_wI > delta_hI)
	deltaI = delta_wI;
else
	deltaI = delta_hI;
#endif

#if 1
fI  x0I(fI_left_coord );
fI  y0I(fI_above_coord);
fI  aI("2");
fI  threshold("4.0");
#endif

#if 1
    for(i = 0; i < w; i++)
	{
		y0I = fI_above_coord;
		for(j = 0; j < h; j++)
		{
			fI  xI("0");
			fI  yI("0");
		
			int k = 0;
#define MAX_ITERATION  10//20//100
			while(k < MAX_ITERATION)
			{
				fI  real_part      = xI*xI-yI*yI+x0I;
				fI imaginary_part  = aI*xI*yI   +y0I;
                fI z_abs_square = real_part*real_part + imaginary_part*imaginary_part;
				if(z_abs_square > threshold)
				{
					break;
				}
				else
				{
                    xI = real_part;
					yI = imaginary_part;
				}
			    k++;
			}

			if(k < MAX_ITERATION)
			{
				SetPixel(hdc, i, j , colors[k%50]);
			}
			else
			{
	            SetPixel(hdc, i, j , RGB(0, 0, 0));
			}
			
			y0I = y0I - deltaI;
		}
		x0I = x0I + deltaI;
	}
#endif
 

fII代码与此类似,基本上只是把I换成II或者加个II后缀罢了。

运行效果图:(现在是可以看到程序在一个一个像素写屏。下面是前后两幅图象。)

下面对log文件内容图示说明:
(蓝色线框依次标记出窗口坐标,复平面上坐标(字串有fI和fII两种形式),迭代序数,退出计算循环时总迭代次数)
(红色线框依次标记出机器浮点算出的实部、虚部、z的模方)
(绿色线框依次标记出我实现的浮点类(fI和fII,结果相等)作高精度近似计算算出的实部、虚部、z的模方的(高精度近似)值)
               
          
可见位于(0 0)这个点第1次迭代即退出了循环,在M集的外边。然后计算下一个点(0 1)。
下面再举位于(16 125)这个点为例:
               
          
(中间……略去)
               
          

(数字末尾紫色线框标记出的内容不一样,不是计算结果不等,是结果字串长200多位,我这里最大只打印到200位。fI表示比fII表示多了一个 小数点'.'字符,所以少打印一位。)
可见位于(16 125)这个点在实轴上,属于M集,所以一直迭代到最大次数(这里是10次)还未超过阈值。不再算了,然后计算下一个点(16 126)。

使用我自己的float类进行精确计算,带来了密集计算。我的代码中运算是直接在OnDraw函数中进行的,这导致程序不响应外部消息。 在Win XP中不响应外部消息也罢,还能看到程序还在一个像素一个像素地写屏;在Win 7中看到程序画了少量像素后就不写屏了,只显示 “程序不响应”。
为了不让密集计算阻塞消息循环(阻塞消息循环,不是阻塞进程;OnDraw函数在主线程中,在其中密集计算会使主线程长久在这个函数中运行, 拖住了(MFC自身实现的)消息循环的进行。),要么另开一个工作线程来进行计算,要么在密集计算中另插入自己的(非阻塞的)消息处理循环。 这里采用了后一种方法。但在关闭窗口时有点问题,进程没有退出,OnDraw还在运行,只好在View中加了OnDestroy函数处理了一下。
有了自己的float类的精确计算,可以发现机器浮点double类型近似计算的误差累积。下面是log例子。 (log文件下载(下面节选部分在log文件最后处))
(由于迭代次数较高,这里my floats用了一定精度的近似值来与机器浮点double的结果作比较,当然,这个精度远远高过double精度。)
这里观察的是坐标(i,j)为(16,125)的点的迭代计算:

(红方框内为机器浮点计算结果,绿方框内为自己实现的计算结果)

k= 0时,结果是一样的;


                           
          

k=40时,差别还不大;


                           
          

k=47时,小数点后第1位已经有差别了;

                           
          

k=52时,已经不能认为两者值接近了;

                           
          

k=53开始,结果正负都不一样了。

                           
          

(backfract好象是用long double类型来实现的,但这仍然是太少了。)
区域放大功能这一版本中尚未实现。此外,M集的放大是无穷无尽的,在计算结果得到一定保证之后,我们要考虑究竟怎样放大:手动选取?自动选取? 要怎样呈现M集的图案?

第3个程序

(3) GMP实现
GMP是GNU的大数库。我用的GMP库是在虚拟机WinXP上用MinGW编译出来的,然后可以给VC++6下应用程序直接链接使用,再需加一些MinGW中的库 和Windows的库。我在升级/更新程序到虚拟机Win 7+vs2015环境时,这个库仍然可用,仅仅MinGW中的libmsvcrt.a会影响到fprintf函数不能使用, 所以我将vs2015下的代码中的fprintf都改成了fwrite。运行时另外再加上了一些提示需要的VS中VC的库和MS的SDK的库。
GNU的大数库包括GMP、MPFR、MPC库。GMP是大数整数分数库,本来也包含的浮点库功能部分现已可被MPFR代替(GMP手册语);MPFR是大数任意精度 浮点库,基于大数整数库GMP;MPC是大数复数库,基于前两者。(有意思的是构建GCC的话要依赖这些库。)此外,还有一个分支 MPIR从GMP分出来,据说其 着眼于让该大整数库可在Windows下用VS编译,因GMP不行,我这里未用该库。本来我以为要用MPFR库来实现所需功能,所以文件名用了mpfr(mandel_with_mpfr_test), 后来发现只需用GMP库分数部分即可,而且后者以绝对精度进行计算,而不是止于前者的“任意精度”。但文件名没改掉,代码只用了MPFR作了 一处打印日志的功能,其他与MPFR均无关系。
                                      
          

我在WinXP虚拟机上安装了网上下载的MinGW+MSYS离线安装包,按照手册和configure帮助配置、编译了GMP、MPFR(MPFR源码包中有个INSTALL文件讲了 其Windows下三种方式编译:MinGW、Cygwin、vs2015),没有什么太特别的,不过一次只能指定编一个要么静态库要么动态库,我只编译了静态库。 生成的静态库libgmp.a、libmpfr.a可直接给VC++6(及vs2015)使用:
mandel_with_mpfr_test_vs2015\mpfr_inc.h:
#pragma comment(lib, "libmpfr.a")
#pragma comment(lib, "libgmp.a")
#pragma comment(lib, "libgcc.a")
#pragma comment(lib, "libgcc_s.a")
#pragma comment(lib, "libmingwex.a")
#pragma comment(lib, "libmsvcrt.a")
 
其他lib*.a均为编译程序时所需的MinGW中的库。此外运行还需MinGW下的libgcc_s_dw2-1.dll和一些Windows上的dll。
GMP是c实现的,有一个C++的包装库可以配置编译,但手册上说了,该c++库一般只能给同一个(c++)编译器的应用程序使用。开发中我发现, 必须用c编译器而非c++编译器处理包含gmp.h的文件,因此mpfr_lib.c必须为.c文件而非.cpp文件。
链接MPFR库有顺序要求,要先libmpfr.a,然后libgmp.a。此外手册上提到VC++使用GMP库在一定情况下必须指示Windows的cl编译器使用/MD选项编译。
(有一个分形的开源项目FractalNow从版本8.0开始加入了使用MPFR计算,该项目基于Qt,我在Ubuntu和WinXp MinGW下编译了该项目。Ubuntu上有 这个项目的包,构建起来相对容易;Windows上其使用的Qt4.8要求MinGW的GCC必须为4.4的,我只好又从网上搜了个GCC4.4的MinGW下载,然后拷贝 以前的MSYS目录使用,此外还下载了一个pthreads-w32的包才完成编译。)
运行图:
                 
          

log文件中主要是多了些GMP计算结果和用MPFR打印的内容:
下面这第一张图是坐标(22 0)的点。(如果你不改下载的程序源码,很可能看到的log开头就是这儿,因我调试时是跳过了前面的坐标点,从列22开始的, 代码没整理,没去掉这个。)
蓝、绿、红的线框标识的含义仍同前所述。赭色的是GMP的计算结果用MPFR打印出的小数。多了相应颜色的加粗线框是新增的,打印的是绝对精确值 而非高精度近似值。先后就实部精确值、实部近似值、虚部精确值、虚部近似值、z的模方精确值、z的模方近似值使用GMP对我自己的float实现进行了验证。
               
          
上图循环1次即结束了,log较短。再以坐标(22 125)的点的第k=9次迭代进一步看看,这下log长了,一张图显示不下,而且我这里只好把其中冗长的多行数字 用*行代替。下面第1张完成了实部精确值、近似值的比较:
               
          
第2张完成了虚部精确值、近似值的比较。因该点在实轴上,虚部为0,所以log很短。
               
          
最后第3张中完成了z的模方的精确值、近似值的比较。
               
          
这些图中没标记的“[FRACTION ... / ...]”字样的部分是GMP采用的分数形式表示,即 分子 / 分母,且是不可约的。

这第3个程序没有了图形界面功能,只剩纯粹的计算。代码从View.cpp换到了main.cpp,新增GMP实现的函数均以“GMP_”开头,在mpfr_inc.h和mpfr_lib.c 两个文件中(如上所述,用的mpfr的文件名)。把前面的伪码变成用GMP的代码示意:


(伪码):
x0(left );
y0(above);
a("2");
threshold("4.0");
for(i = 0; i < w; i++)
{
   y0 = above;
   for(j = 0; j < h; j++)
   {
     x("0");
     y("0");
     k = 0;
     while(k++ < MAX_ITER)
     {
       real = x*x-y*y +x0;
       imag = a*x*y +y0;
       z_square = real*real + imag*imag;
       if(z_square > threshold)
         break;
       else{
         x = real;
         y = imag;
       }
     }
     y0 = y0 - delta;
   }
   x0 = x0 + delta;
}
(对应GMP实现):



GMP_init_calc()


   GMP_reset_y0()
  
  
     GMP_reset_x()
     GMP_reset_y()
    
    
    
       GMP_calc_real()
       GMP_calc_imag()
       GMP_calc_z_square()
       if(GMP_cmp_threshold() > 0)
        
      
         GMP_set_x_as_real()
         GMP_set_y_as_imag()
      
    
     GMP_update_y0()
  
   GMP_update_x0()
GMP_clear()



这第3个程序对my floats类代码的唯一修改在于:在作近似计算时,要继续比较两种实现结果的一致性,需要对GMP的计算结果也作相应近似,那么必须在 我自己的float类作近似时有一个输出,告知GMP实现怎样作相应近似。这就是在truncate_floatII函数加了一个参数p_pos告诉GMP实现方面:我是在第几位 截断的,那么你也要在那一位作相应截断。
改变前后:
右边完整代码:
mandel_with_mpfr_test_vs2015\my_floats.h:
extern floatII  truncate_floatII(int x_precision, int y_precision, int delta_precision,
								  int min_precision_to_truncate,
								  floatII&  f, 
								  int add_to_tail_precision,
								  int* p_pos);
 
my_floats.cpp中作的相应改动下面即将看到。
truncate_floatI目前未作改动,因为floatI和floatII的结果一致,我目前使用其中的一种就可以了,目前使用的是floatII。
加了GMP实现后,发现修正了my_floatI.cpp(my_floatII.cpp中同样)的一处(初级,:-))bug:
                
          

怎么又要作近似计算了呢?由于绝对精确的计算(不作任何近似)对存储小数的位数需求是随迭代次数幂指数式增长的,10次迭代以K计,20次迭代以M计, 30次迭代就以G计了。 此外这样计算在PC上迭代到第10次左右差不多千位乘千位数量级时算一个点已经感觉有点慢了。所以实际计算必须作一定的近似,比如让存储所需位数 随迭代线性增长。并考虑估量取得结果的速度。
目前我考虑的近似算法大致是这样的:
第(0)步:定一个精度提高位数要求,比如在原有精度上加16位;
第(1)步:定一个精度达到位数,需考虑(A)起始坐标精度;(B)坐标步进值精度;再加上第(0)步期望;(C)随迭代次数增加(?)
第(2)步:计算结果是否超出精度需求?若否,则不改变值;
第(3)步:若(2)是,计至小数点后精度位,然后:
   (A)若前面已有有效数字,则向后加计最多如16位;
   (B)若前面尚未有有效数字(全'0'),除非0值,向后找到第1位有效数字(非'0'值),然后向后加计最多如16位;
实现:(以floatII为例)((高亮行即上面所说该函数的改动添加))
mandel_with_mpfr_test_vs2015\my_floatII.cpp:
 floatII truncate_floatII(int x_precision, int y_precision, int delta_precision,
								  int min_precision_to_truncate,
								  floatII&  f, 
								  int add_to_tail_precision, 
								  int* p_pos)
{
	*p_pos = 0;

	int truncate_part_len = 0;
	int i;
    //if(f.point_pos_r < min_precision_to_truncate)
	//	return f;

	floatII approximation = f;

	int max = x_precision ;
	max = y_precision > max ? y_precision : max ;
	max = delta_precision > max ? delta_precision : max ;

	if(f.point_pos_r <= max)
		return approximation;

	max += add_to_tail_precision;
	if(f.point_pos_r <= max)
		return approximation;
	else
		truncate_part_len = f.point_pos_r - max;

	max -= add_to_tail_precision;
	//go to NO.`max' after '.' of f
    /*
	   result (= x * y)  ((precison of a by b )) :
	   (a) common case: (some non-'0' ahead)
	         0.02003004080509760000000001300xxxxxxxxxxxxxxxxx
	   (b)   rare case: (all '0's ahead)
	         0.000000000000000000000000013000000050000xxxxxxxx
          or 0.0000000000000000000000000130    (shorter vs. above)
	*/
	int len = strlen(f.digits);
	int int_len = len - f.point_pos_r;
	char* p = approximation.digits;
	bool is_there_nonzero = false;
	for(i = 0; i < (int_len + max); i++)
	{
		if(p[i] != '0')
		{
			is_there_nonzero = true;
			break;
		}
	}

	if(is_there_nonzero)
	{
         approximation.point_pos_r -= truncate_part_len;
		 int len2 = len - truncate_part_len;
		 memset(&p[len2], 0, truncate_part_len);
		 p[len2]  = '\0'; //clear again ( redundant though)

         *p_pos = len2 - int_len + 1;//*p_pos = &p[len2] - &p[int_len] + 1;
		 return approximation;// (.)
		                      //531415
                              //0123456
		                      //     ^
	}
	else //TODO: to log and check this rare/corner case. 
	{

		for(i = (int_len + max); i < len; i++)
		{
			if(p[i] != '0')
				break;
		}
		if(i == len)
		{
            floatII   result("0");

			return  result;
		}
		else
		{
		    if( (len - i -1) <= add_to_tail_precision  )
				return approximation;
			else
			{
                truncate_part_len = len - (i + 1 + add_to_tail_precision);
				approximation.point_pos_r -= truncate_part_len;
				memset(&p[i+1+add_to_tail_precision], 0, truncate_part_len);

				*p_pos = i+1+add_to_tail_precision - int_len + 1;
	
				return approximation;
			}
		}
	
	}
}
 
使用近似计算的代码所需改动:
                
          
其中GMP截断近似的实现:(以GMP_truncate_real()为例。GMP_truncate_imag()代码只是把"real"换成"imag"而已。)
mandel_with_mpfr_test_vs2015\mpfr_lib.c:
void GMP_truncate_real(char* str_numerator, char* str_denominator, 
				   char* str_gmp_mpfr, int* p_exp, int mpfr_precision,
				   int len, int trunc_pos)
{
    mpz_t   numerator;
    mpz_t   denominator;

    mpq_t   a;

	mpfr_t   f1, f2;
	mpfr_exp_t  exp;

    mpz_init(numerator);
    mpz_init(denominator);

    mpq_init(a);

   
    mpq_set(gmp_real_approx, gmp_real);

    if(trunc_pos == 0)
	{
		 //return;
	}
    else
	{
	   int n;
	   char* str;
	   char* p;
	   mpz_set(denominator, mpq_denref(gmp_real_approx));
	   //gmp_printf("denominator is %Zd\n", denominator);
	   mpq_set_si(a, 10,  1);
	   n = 0;
	   while( mpz_cmp_si(denominator, 1) != 0)
	   {
		   mpq_mul(gmp_real_approx, gmp_real_approx, a);
		   //gmp_printf("%#040Qd\n", gmp_real_approx);

		   mpz_set(denominator, mpq_denref(gmp_real_approx));
		   n++;
	   }
	   //printf("\n multiplied by 10^ %d\n" , n);
	   mpz_set(numerator, mpq_numref(gmp_real_approx));
	   //gmp_printf("numerator is %Zd\n", numerator);

       str = (char*)malloc(len+16); //some more space
	   memset(str, 0, len+16);//buggy: memset(str, 0, sizeof(str));
	   mpz_get_str(str, 10, numerator);
	   //printf("str got:%s\n", str);
	   p = str + strlen(str);
	   p -= (n-trunc_pos)+1;             //53.1415926
	   while( *p != '\0')
		   *p++ = '0';

	   mpq_set_str(gmp_real_approx, str, 10);
	   mpq_canonicalize(gmp_real_approx);
	   while(n > 0)
	   {
		   mpq_div(gmp_real_approx, gmp_real_approx, a);
		   //gmp_printf("%#040Qd\n", approx);
		   n--;
	   }

	   free(str);
	}

    mpz_set(numerator, mpq_numref(gmp_real_approx));
    mpz_get_str(str_numerator, 10, numerator);

    mpz_set(denominator, mpq_denref(gmp_real_approx));
    mpz_get_str(str_denominator, 10, denominator);

      //for visualizing the string
 	  mpfr_init2(f1, (mpfr_prec_t)mpfr_precision);
	  mpfr_init2(f2, (mpfr_prec_t)mpfr_precision);
   
	  mpfr_set_z(f1, numerator  , MPFR_RNDZ);
      mpfr_set_z(f2, denominator, MPFR_RNDZ);
	  mpfr_div(f1, f1, f2, MPFR_RNDZ);
   	  mpfr_get_str(str_gmp_mpfr, &exp, 10, 0, f1, MPFR_RNDZ);
	  *p_exp =(int)exp;
 
	  mpfr_clear(f1);
	  mpfr_clear(f2);
    mpz_clear(numerator);
    mpz_clear(denominator);

   mpq_clear(a);


}
 
这个截断实现的原理是这样的: 由于程序中的分数都被设计为有限小数,不是无限小数,(更非无理数),对一等于形如小数53.1415926等等的 计算结果,让其乘以10的若干(将会是小数位数)次方直至得到整数(判断方法是分母为1),然后按照指定的截断位置,将整数字串该位置 及以后置为字符“0”,用此字串构造mpq_t,再除以10的相应次方。即完成了计算结果的截断。函数代码高亮第30行取得分母,第32行为待用乘数10, 34、36行可看到循环乘10并判断分母是否成了1。43行取得此时的分子,高亮第48行取得此时分子的字串。50-53行将此字串在截断位置后部置“0”。 55行构造新的GMP分数后,高亮第57、59行可见循环除以10的n次方。

讲了这么多,还没看到加法和乘法的代码。虽然这只是小学数学运算,这里还是大致看一下。虽然上面GMP显示了很强的功能,或许以后 更多计算用GMP(及MPFR)编程是更好的替代(因其可用分数,即无限循环小数,等等其他自己不具备的功能),在可用的地方多一种实现 并行可能更好。在不可用的地方只好用GMP了。
以floatII类的实现为例,floatI类的实现冗长一些,floatII类的简洁一点,算法是一样的。加法没太多好说的:
mandel_with_mpfr_test_vs2015\my_floatII.cpp:
floatII  floatII::operator+ (      floatII& f2)
{
    //TODO: 0?

    int sign, pos;

    //align point
    floatII f1 = *this;
    int int_len1  = strlen(f1.digits) - f1.point_pos_r;
    int int_len2  = strlen(f2.digits) - f2.point_pos_r;
    int frac_len1 = f1.point_pos_r;
    int frac_len2 = f2.point_pos_r;
    int max_int_len  = int_len1  >= int_len2  ?  int_len1 : int_len2 ;
    int max_frac_len = frac_len1 >= frac_len2 ? frac_len1 : frac_len2;

	floatII f1_old = f1;
	floatII f2_old = f2;

    //pos
    pos = max_frac_len;    

    int len =  max_int_len + max_frac_len;

    char* s1 = f1.extend_zero2( max_int_len-int_len1, max_frac_len-frac_len1);
    char* s2 = f2.extend_zero2( max_int_len-int_len2, max_frac_len-frac_len2);

    if(f1.minus == f2.minus)
    {
          //minus assign
          sign = f1.minus? -1 : +1;

          int* carry = (int*  )malloc( (len+1)   *sizeof(int));
          char*    r = (char* )malloc(  len );
          memset(carry, 0, (len+1)*sizeof(int));
          char* q = s1+len-1;
          char* p = s2+len-1;
          int   m;
          int   k = 0;
          while(q >= s1)
          {
              m = chtoi(*q) + chtoi(*p) + carry[k];
              if(m >= 10)
              {
                 carry[k+1] += 1;
                 r[k] = itoch(m-10);
              }
              else
              {
                 r[k] = itoch(m);
              }

              p--;
              q--;
              k++;
          } 

          char* str = (char* )malloc(len+1+1); //doesn't care if more
          int i = 0;
          int j;
          if(carry[k] > 0)
              str[i++] = itoch(carry[k]);
          for(j = 0; j < len; j++)
              str[i++] = r[len-1-j];
          str[i] = '\0';

          floatII str_result(str, sign, pos);

          free(str); //? do it in destructor?

          free(r);
          free(carry);
		  free(s1);
		  free(s2);

          return str_result;
    }
    else
    {
          //to decide sign
          char* p = s1;
          char* q = s2;
          while(*p != '\0') //  && *q != '\0' 
          {
              if(chtoi(*p) != chtoi(*q))
                   break;
              p++;
              q++;
          }
          if(*p == '\0')
          {
			  free(s1);
			  free(s2);

              floatII  f4("0");
              if(MY_DEBUG) printf("they are equal,minus result is 0\n");
              return f4;

          }
          else
          {
              bool t  = chtoi(*p) > chtoi(*q) ? f1.minus : f2.minus;
              sign = t? -1 : +1;

              char* big   = (*p > *q) ? s1 : s2;
              char* small = (*p > *q) ? s2 : s1;

              p = big   + len-1;
              q = small + len-1;

              int* borrow = (int*  )malloc( (len)   *sizeof(int));
              int  n;
              char*     r = (char* )malloc( (len));
              memset(borrow, 0, (len)*sizeof(int));  
              int  k = 0;

              while(p >= big)
              {
                  if( (chtoi(*p) + borrow[k]) >= chtoi(*q))
                  {
                      n = chtoi(*p) + borrow[k] -chtoi(*q) ;
                  }
                  else
                  {
                      char* l = p-1; 
                      while( chtoi(*l) == 0 )
                      {
                          *l = '9';
                          l--;
                      }
                      borrow[k] = 10;
                      *l -= 1;
                      n = chtoi(*p) + borrow[k] - chtoi(*q) ;
                  }
                  r[k] = itoch(n);

                  p--;
                  q--;
                  k++;
              }
              //TODO: to remove leading 0s in integer part

              char* str = (char* )malloc(len+1);  
              int i = 0;
              int j;
              for(j = 0; j < len; j++)
                  str[i++] = r[len-1-j];
              str[i] = '\0';

              floatII    str_result2(str, sign,  pos);

              free(str);
 
              free(r);
              free(borrow);

			  free(s1);
			  free(s2);

              return str_result2;
          }
    }
}
 
高亮第7行开始,准备对齐小数点。20行得到小数点位置。24、25行把'0'字符补齐。27行同号,准备加,下面就是相加,记录进位值。 57行开始准备结果字符串,计算是从低到高即从右到左进行的,而计算结果字符串是从左到右表示。66行用字符串等构造fII类对象返回。 84行异号相减先比较绝对值大小。94行比较结果为相等,则返回0。104行准备用大的减小的。下面就是作相减、借位等。完成后 同样构造字符串后,149行构造fII类对象,完成。

乘法则用到两个重载*号的函数。两个类对象的相乘被转化成一个类对象与一个整数类型值相乘,即一个数与另一数的每一位相乘,然后把各个结果 作相应的小数点移位后相加。(比如对$f_{1}$ * $f_{2}$, $f_{2}$形如xyzwr.st0,结果则为 $$ \begin{aligned} result & = f_{1} * x * 10^m \space \space \space \space \space \space \text{(m=max_int_len-1)} \\ & + f_{1} * y * 10^{m-1} \\ & + f_{1} * z * 10^{m-2} \\ & + ... \\ & + f_{1} * t * 10^{-2} \\ & + f_{1} * 0 * 10^{-3} \space \space \space \space \space \space \text{(-3=-max_frac_len)} \end{aligned} $$ )。 故用了两个重载操作符的函数。
mandel_with_mpfr_test_vs2015\my_floatII.cpp:
floatII  floatII::operator* (int   m)   // 0<=m<=9 
{
      //TODO: 0?

      int sign, pos;

      floatII& f1 = *this;
      sign = f1.minus? -1: +1; //does not care

      //pos
      pos = f1.point_pos_r;    //doesn't change

      int len = strlen(f1.digits);
      char* s = f1.digits;

          int* carry = (int*  )malloc( (len+1)   *sizeof(int));
          char*    r = (char* )malloc( (len));
          memset(carry, 0, (len+1)*sizeof(int));
          char* q = s+len-1;
          int   d;
          int   k = 0;
          while(q >= s)
          {
              d = chtoi(*q) * m + carry[k];
              if(d >= 10)
              {
                 carry[k+1] += d/10;
                 d = d % 10;
              }
              r[k] = itoch(d);

              q--;
              k++;
          } 
          //copied from add operation code part
          char* str = (char* )malloc(len+1+1); //doesn't care if more
          int i = 0;
          int j;
          if(carry[k] > 0)
              str[i++] = itoch(carry[k]);
          for(j = 0; j < len; j++)
              str[i++] = r[len-1-j];
          str[i] = '\0';

          floatII str_result(str, sign, pos);

          free(str); //? do it in destructor?
          //copied from add operation code part -- end

          free(r);
          free(carry);

          return str_result;
}


floatII  floatII::operator* (const floatII& f2)
{
      //TODO: 0?

      int sign; // pos;

      floatII& f1 = *this;
      if(f1.minus == f2.minus)
          sign = +1;
      else
          sign = -1;

          char*p = f1.digits;
          char*q = f2.digits;   //(choose the shorter to be multiplier?)
          floatII multi("0");
          int len2 = strlen(f2.digits);
          for(int i = 0; i < len2; i++)
          {
              floatII t1 = f1 * chtoi(q[i]);

              floatII t2 = shift_floatII(t1, len2 - f2.point_pos_r - 1 - i);
              multi = multi + t2;
          }

          multi.minus = (sign==-1)? true : false;
          return multi;

}

floatII&  shift_floatII (floatII&  f, int m)
{
    if(m > 0) 
    {
        if(m >= f.point_pos_r)     
        {
            f.extend_zero(0, m - f.point_pos_r);
            f.point_pos_r = 0;
        }
        else
            f.point_pos_r -= m;
    }
    else if(m < 0)
    {
        int int_len = strlen(f.digits) - f.point_pos_r;
        if(-m >= int_len)
            f.extend_zero(-m-int_len+1, 0);

        f.point_pos_r += -m;
    }

    return f;
}
 
高亮行即所述实现。第一个函数则与加法有类似的进位。
一直以来都只是c程序员,对c++知之甚少。这里进行数类的运算,使用c++,用重载运算符实现比较自然。使用GMP时,又感到用c库 比c++移植性好。
有一次发现第3个程序很快耗用内存太大,且不断增长。调试后发现少量把malloc/free改成new/delete的代码导致了这个问题,这还 不是内存泄漏,关闭进程后内存马上恢复了,但运行时这样的内存耗用下去马上程序就无法运行了,似乎new的内存delete后还没有 立即交还给OS,而free则立即交还了。由于程序要持续进行大量计算,不发生内存泄漏对程序是至关重要的。
自己实现下来用了少量代码取得了与GMP运算一致的结果,效果还不错。GMP用分数形式,比我用小数形式大大拓展了运算范围,当然, 算法更复杂了。我此处的算法只有乘法和加法,只适用于计算Mandelbrot集(不知是否适用于计算Julia集),其他分形计算不一定够用。
除了Win/VC,也准备在Linux/GTK上实现。
本想能在ReactOS上发布程序最好。但我在虚拟机上试用了ReactOS,其兼容性和稳定性都还不行。 ……
(附及:MPFR的精度是指二进制精度,这与我的float小数实现用的10进制精度匹配的话可能有点问题。)

试图建立一个Mandelbrot集的图库,附上所写的PHP前端和c程序后端, 参看PDF电子书 及其制作shell等。但由于所需计算性能极高,要更精确计算还不够实用。




  

More powered by