第二十章:异步和文件I/O.(十七)

简介:

基本的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>

ActivityIndi​​cator通知用户程序正忙于后台作业。 Image元素和ActivityIndi​​cator共享一个单元格Grid,以便ActivityIndi​​cator可以更多地朝向屏幕的垂直中心,然后在出现位图时被覆盖。底部是一个开始计算的按钮。
下面的代码隐藏文件首先定义了几个常量。前四个常量与程序构造的位图有关,以显示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或更大。 此时,像素可以设置为黑色或白色。
按下按钮后,您的手机可能需要一分钟左右才能遍历所有像素,但随后您将看到经典图像:
2018_12_26_094855
CalculateMandelbrotAsync方法的结构存在一点危险。 它传递一个BmpMaker对象,后台线程用像素填充,但主线程也可以访问这个BmpMaker对象。 如果此对象保存为字段,则主线程可能包含一些代码,这些代码在后台线程正在工作时更改或设置像素。 当然,这可能是一个错误,但一般来说,如果参数仅限于值类型而不是引用类型,则可以使同步方法更具防弹性。 如果这不太可能或不方便,请不要太担心,但在程序的下一个版本中,CalculateMandelbrotAsync方法本身将创建BmpMaker对象并将其返回。

目录
相关文章
|
存储 JavaScript Android开发
|
Web App开发 Android开发
第二十章:异步和文件I/O.(二十三)
回到网上在本章之前,本书中唯一的异步代码涉及使用可移植类库WebRequest中唯一可用于此目的的合理类进行Web访问。 WebRequest类使用称为异步编程模型或APM的旧异步协议。 APM涉及两种方法,在WebRequest的情况下,这些方法称为BeginGetResponse和EndGetResponse。
740 0