基本的Mandelbrot集
波兰出生的法国和美国数学家Benoit Mandelbrot(1924-2010)以其与复杂的自相似表面相关的工作而闻名,他称之为分形。 他涉及分形的工作包括对递归公式的研究,该公式生成一个分形图像,现在称为Mandelbrot集。
Mandelbrot集在复杂平面上绘制,其中每个坐标都是一个复数形式:
𝑐 = 𝑥 + 𝑦i
实部x沿水平轴绘制,左侧为负值,右侧为正值。 虚部y沿垂直轴绘制,从底部的负值增加到正值。
要计算Mandelbrot集,首先取这个平面上的任意点并将其称为c,并将z初始化为零:
𝑐 = 𝑥 + 𝑦𝑖
𝑧 = 0
现在执行以下递归操作:
𝑧 ← 𝑧*z + 𝑐
结果将分流到无穷大或不会。如果z不发散到无穷大,那么c被认为是Mandelbrot集的成员。否则,它不是Mandelbrot集的成员。
您需要对复杂平面中的每个感兴趣点执行此计算。通常,结果绘制在位图上,这意味着位图中的每个像素对应于特定的复杂坐标。在其最简单的演绎中,属于Mandelbrot集的点是黑色的,其他像素是白色的。
对于某些复数,很容易确定该点是否属于Mandelbrot集。例如,复数(0 + 0i)显然属于Mandelbrot集,您可以快速确定(1 + 0i)没有。但总的来说,您需要执行递归计算。而且因为这是一个分形,所以不能采用快捷方式。例如,如果您知道两个值c1和c2属于Mandelbrot集,则不能假设这两个点之间的所有点也属于Mandelbrot集。无视插值是分形的基本特征。
在确定特定复数是否属于Mandelbrot集之前,您需要执行多少次递归计算迭代?事实证明,如果递归计算中z的绝对值变为2或更大,那么这些值最终会发散到无穷大,并且该点不属于Mandelbrot集。 (复数的绝对值也称为数的大小;它可以计算为x和y值的平方和的平方根,这是毕达哥拉斯定理。)
但是,如果在经过一定次数的迭代后,递归计算尚未达到2的幅度,则无法保证它不会随着重复迭代而发散。出于这个原因,Mandelbrot集是众所周知的计算密集型,并且是执行的辅助线程的理想选择。
MandelbrotSet程序演示了如何完成此操作。为了渲染图像,程序使用Xamarin.FormsBook.Toolkit库中的BmpMaker类(在第13章“Bitmaps”中介绍)。该库还包含以下结构来表示复数:
namespace Xamarin.FormsBook.Toolkit
{
// Mostly a subset of System.Numerics.Complex.
public struct Complex : IEquatable<Complex>, IFormattable
{
bool gotMagnitude, gotMagnitudeSquared;
double magnitude, magnitudeSquared;
public Complex(double real, double imaginary) : this()
{
Real = real;
Imaginary = imaginary;
}
public double Real { private set; get; }
public double Imaginary { private set; get; }
// MagnitudeSquare and Magnitude calculated on demand and saved.
public double MagnitudeSquared
{
get
{
if (gotMagnitudeSquared)
{
return magnitudeSquared;
}
magnitudeSquared = Real * Real + Imaginary * Imaginary;
gotMagnitudeSquared = true;
return magnitudeSquared;
}
}
public double Magnitude
{
get
{
if (gotMagnitude)
{
return magnitude;
}
magnitude = Math.Sqrt(magnitudeSquared);
gotMagnitude = true;
return magnitude;
}
}
public static Complex operator +(Complex left, Complex right)
{
return new Complex(left.Real + right.Real, left.Imaginary + right.Imaginary);
}
public static Complex operator -(Complex left, Complex right)
{
return new Complex(left.Real - right.Real, left.Imaginary - right.Imaginary);
}
public static Complex operator *(Complex left, Complex right)
{
return new Complex(left.Real * right.Real - left.Imaginary * right.Imaginary,
left.Real * right.Imaginary + left.Imaginary * right.Real);
}
public static bool operator ==(Complex left, Complex right)
{
return left.Real == right.Real && left.Imaginary == right.Imaginary;
}
public static bool operator !=(Complex left, Complex right)
{
return !(left == right);
}
public static implicit operator Complex(double value)
{
return new Complex(value, 0);
}
public static implicit operator Complex(int value)
{
return new Complex(value, 0);
}
public override int GetHashCode()
{
return Real.GetHashCode() + Imaginary.GetHashCode();
}
public override bool Equals(Object value)
{
return Real.Equals(((Complex)value).Real) &&
Imaginary.Equals(((Complex)value).Imaginary);
}
public bool Equals(Complex value)
{
return Real.Equals(value) && Imaginary.Equals(value);
}
public override string ToString()
{
return String.Format("{0} {1} {2}i", Real,
RealImaginaryConnector(Imaginary),
Math.Abs(Imaginary));
}
public string ToString(string format)
{
return String.Format("{0} {1} {2}i", Real.ToString(format),
RealImaginaryConnector(Imaginary),
Math.Abs(Imaginary).ToString(format));
}
public string ToString(IFormatProvider formatProvider)
{
return String.Format("{0} {1} {2}i", Real.ToString(formatProvider),
RealImaginaryConnector(Imaginary),
Math.Abs(Imaginary).ToString(formatProvider));
}
public string ToString(string format, IFormatProvider formatProvider)
{
return String.Format("{0} {1} {2}i", Real.ToString(format, formatProvider),
RealImaginaryConnector(Imaginary),
Math.Abs(Imaginary).ToString(format, formatProvider));
}
string RealImaginaryConnector(double value)
{
return Math.Sign(value) > 0 ? "+" : "\u2013";
}
}
}
正如顶部的注释所示,这主要是.NET System.Numerics命名空间中Complex结构的一个子集,遗憾的是,Xamarin.Forms项目中的可移植类库无法使用它。 但是,此Complex结构中的ToString方法的工作方式略有不同,原始的Complex结构没有MagnitudeSquared属性。 对于Mandelbrot计算,MagnitudeSquared属性非常方便:检查Magnitude属性是否小于2与检查MagnitudeSquared属性是否小于4相同,但没有平方根计算。
MandelbrotSet程序具有以下XAML文件:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MandelbrotSet.MandelbrotSetPage">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 20, 0, 0" />
</ContentPage.Padding>
<StackLayout>
<Grid VerticalOptions="FillAndExpand">
<ContentView Padding="10, 0"
VerticalOptions="Center">
<ActivityIndicator x:Name="activityIndicator" />
</ContentView>
<Image x:Name="image" />
</Grid>
<Button x:Name="calculateButton"
Text="Calculate"
FontSize="Large"
HorizontalOptions="Center"
Clicked="OnCalculateButtonClicked" />
</StackLayout>
</ContentPage>
ActivityIndicator通知用户程序正忙于后台作业。 Image元素和ActivityIndicator共享一个单元格Grid,以便ActivityIndicator可以更多地朝向屏幕的垂直中心,然后在出现位图时被覆盖。底部是一个开始计算的按钮。
下面的代码隐藏文件首先定义了几个常量。前四个常量与程序构造的位图有关,以显示Mandelbrot集的图像。在整个练习中,这些位图将始终是方形的,但代码本身更加通用,并且应该能够适应矩形尺寸。
中心字段是对应于位图中心的复杂点,而大小字段指示位图上实部和虚拟坐标的范围。这些特定的中心和大小字段意味着实际坐标的范围从位图左侧的-2到右侧的0.5,虚拟坐标的范围从底部的-1.25到顶部的1.25。 pixelWidth和pixelHeight值指示位图的宽度和高度(以像素为单位)。迭代字段是在程序假定该点属于Mandelbrot集之前递归公式的最大迭代次数:
public partial class MandelbrotSetPage : ContentPage
{
static readonly Complex center = new Complex(-0.75, 0);
static readonly Size size = new Size(2.5, 2.5);
const int pixelWidth = 1000;
const int pixelHeight = 1000;
const int iterations = 100;
public MandelbrotSetPage()
{
InitializeComponent();
}
async void OnCalculateButtonClicked(object sender, EventArgs args)
{
calculateButton.IsEnabled = false;
activityIndicator.IsRunning = true;
BmpMaker bmpMaker = new BmpMaker(pixelWidth, pixelHeight);
await CalculateMandelbrotAsync(bmpMaker);
image.Source = bmpMaker.Generate();
activityIndicator.IsRunning = false;
}
Task CalculateMandelbrotAsync(BmpMaker bmpMaker)
{
return Task.Run(() =>
{
for (int row = 0; row < pixelHeight; row++)
{
double y = center.Imaginary - size.Height / 2 + row * size.Height / pixelHeight;
for (int col = 0; col < pixelWidth; col++)
{
double x = center.Real - size.Width / 2 + col * size.Width / pixelWidth;
Complex c = new Complex(x, y);
Complex z = 0;
int iteration = 0;
do
{
z = z * z + c;
iteration++;
}
while (iteration < iterations && z.MagnitudeSquared < 4);
bool isMandelbrotSet = iteration == iterations;
bmpMaker.SetPixel(row, col, isMandelbrotSet ? Color.Black : Color.White);
}
}
});
}
}
OnCalculateButtonClicked处理程序标记为异步。 它首先禁用Button以避免多个同时计算并启动ActivityIndicator显示。 然后,它创建一个具有所需像素大小的BmpMaker对象,并将其传递给CalculateMandelbrotAsync。 完成该方法后,Clicked处理程序将继续将位图设置为Image对象并关闭ActivityIndicator。 Button未重新启用。
传递给Task.Run方法的lambda函数循环遍历由BmpMaker创建的位图的行和列,并且对于每个像素,它从x和y坐标值计算复数c。 小的do-while循环继续,直到达到最大迭代次数或幅度为2或更大。 此时,像素可以设置为黑色或白色。
按下按钮后,您的手机可能需要一分钟左右才能遍历所有像素,但随后您将看到经典图像:
CalculateMandelbrotAsync方法的结构存在一点危险。 它传递一个BmpMaker对象,后台线程用像素填充,但主线程也可以访问这个BmpMaker对象。 如果此对象保存为字段,则主线程可能包含一些代码,这些代码在后台线程正在工作时更改或设置像素。 当然,这可能是一个错误,但一般来说,如果参数仅限于值类型而不是引用类型,则可以使同步方法更具防弹性。 如果这不太可能或不方便,请不要太担心,但在程序的下一个版本中,CalculateMandelbrotAsync方法本身将创建BmpMaker对象并将其返回。