Эффект бабочки

Компьютеры должны применяться для решения серьезных задач, поэтому вместо написания очередного инструмента или методики создания приложений создания, чтения, обновления и удаления была сделана статья, показывающая забавные трюки с помощью C# и .NET.

•    Скачать исходники (VS 2008) - 26.2 Кб

Предпосылки

При написании «Краткой истории времени» Стивену Хокингу сообщили, что понятность его книги была обратно пропорциональна числу приведенных им уравнений. К несчастью, объяснение кода перегружено математикой, полностью рассмотренной на этом сайте. Его аудитория достаточно подкована, чтобы понять хотя бы азы.

Многие динамические системы в классической физике можно полностью описать обыкновенными дифференциальными уравнениями двух типов.  Задачи с начальными значениями имеют начальные условия, которые серьезно влияют на решения. Например, представьте боулинговый шар в боулинге с десятью кеглями. То, где окажутся кегли, сильно зависит от начальной подачи метателей. Граничные значения обычно имеют дело с такими же ситуациями, но на них налагаются дополнительные ограничения. Например, врезающиеся в стены волны.

В 70-е Эд Лоренц изучал образование облака с помощью вычислительных методов для решения указанных типов проблем. Его дифференциальные уравнения касались течения жидкости в атмосфере, разбитого на 3 размерности:
•    dx/dt = s(y - x), где s = число Прандтля
•    dy/dt = x(p - z) – y, где p = число Релея
•    dz/dt = xy - Bz

Используя метод, именуемый алгоритмом Рунге-Кутта 4го порядка, он смог построить графики решений и смоделировать их изменение в зависимости от времени. При вычислении эффекта изменения начальных условий Лоренц заметил, что построенные на графике формы радикально менялись в зависимости от очень маленьких изменений. Более того, он нашел диапазон устойчивых начальных значений, дающих на графике красивые и симметричные формы. Его именуют «аттрактор Лоренца», но часто неверно называют «бабочкой «эффекта бабочки».

Алгоритм Рунге-Кутта сводится к последовательной генерации графика координат в каждой размерности. Если вам знакомы такие понятия, как ряды Тейлора, биномиальные ряды и ряды Фибоначчи, то вы  легко поймете алгоритм Рунге-Кутта. Решение для экземпляра временного шага n из Y:

Yn+1 = Yn + k1/6 + k2/3 + k3/3 + k4/6 + 0 (h^5)

где:

k1 = hf'(Xn, Yn)
k2 = hf'(Xn + h/2, Yn + k1/2)
k3 = hf'(Xn + h/2, Yn + k2/2)
k4 = hf'(Xn + h, Yn + k3)

h   - постоянная, представляющая длину временного шага.
f' -  представляет компонент дифференциала в значениях X и Y, полученных Лоренцом в начале статьи.

Достаточно математики, пришло время для кода.

Чтобы решить уравнения Лоренца для каждой координаты в пространстве-времени, они переделываются с помощью C# так:

private float CalculateLorenzIComponent(float x, float y, float t)
{
    float I = t * ((-_sigma * x) + (_sigma * y));
    return I;
}
private float CalculateLorenzJComponent(float x, float y, float z, float t)
{
    float K = t * ((_rho * x) - y - (x * z));
    return K;
}
private float CalculateLorenzKComponent(float x, float y, float z, float t)
{
    float L = t * ((x * y) - (_beta * z));
    return L;
}

Но это расчет временных шагов, следовательно, нужна функция организации цикла, увеличивающая переменную t и вычисляющая члены k1, k2, k3 и k4 для более крупных уравнений. Более того, чтобы можно было наглядно представить решение в виде графика, надо создать какой-то список рисуемых объектов для отображения посредством WPF(API-интерфейс для создания настольных графических программ). Это осуществляет нижеприведенная функция LorenzRoutine(), которую стоит переработать с помощью делегатов или обобщений.

for (int n = 0; n < _iterations; n++)
{
    //приближение 1-го порядка
    I1 = CalculateLorenzIComponent(X1, Y1, T1);
    J1 = CalculateLorenzJComponent(X1, Y1, Z1, T1);
    K1 = CalculateLorenzKComponent(X1, Y1, Z1, T1);

    //приближение 2-го порядка
    I2 = CalculateLorenzIComponent(X1 + (h / 2) * I1, Y1 + (h / 2) * J1, T1 + (h / 2));
    J2 = CalculateLorenzJComponent(X1 + (h / 2) *
        I1, Y1 + (h / 2) * J1, Z1 + (h / 2) * K1, T1 + h / 2);
    K2 = CalculateLorenzKComponent(X1 + (h / 2) *
        I1, Y1 + (h / 2) * J1, Z1 + (h / 2) * K1, T1 + (h / 2));
   
    //приближение 3-го порядка
    I3 = CalculateLorenzIComponent(X1 + (h / 2) * I2, Y1 + (h / 2) * J2, T1 + h / 2);
    J3 = CalculateLorenzJComponent(X1 + (h / 2) *
        I2, Y1 + (h / 2) * J2, Z1 + (h / 2) * K1, T1 + (h / 2));
    K3 = CalculateLorenzKComponent(X1 + (h / 2) *
        I2, X1 + (h / 2) * J2, Z1 + (h / 2) * K1, T1 + (h / 2));
 
    //приближение 4-го порядка
    I4 = CalculateLorenzIComponent(X1 + (h / 2) * I3, Y1 + (h / 2) * J3, T1 + (h / 2));
    J4 = CalculateLorenzJComponent(X1 + (h / 2) *
        I3, Y1 + (h / 2) * J3, Z1 + (h / 2) * K1, T1 + (h / 2));
    K4 = CalculateLorenzKComponent(X1 + (h / 2) *
        I3, X1 + (h / 2) * J3, Z1 + (h / 2) * K1, T1 + (h / 2));
  
    //Расширение ряда Тейлора в 3-х размерностях
    float X2 = X1 + (h / 6) * (I1 + 2 * I2 + 2 * I3 + I4);
    float Y2 = Y1 + (h / 6) * (J1 + 2 * J2 + 2 * J3 + J4);
    float Z2 = Z1 + (h / 6) * (K1 + 2 * K2 + 2 * K3 + J4);
 
    if (n > 0)
    {
        BuildVector3D(new Point3D(X1, Y1, Z1),
        new Point3D(X2, Y2, Z2), 1, 0x000000, 100, 0x000000, 100);
    }
 
    X1 = X2;
    Y1 = Y2;
    Z1 = Z2;
}

Функция BuildVector3D() просто добавляет новый 3-мерный вектор к списку объектов, позже используемому в RenderScreen() для рисования каждого элемента графика в управляющем элементе WPF AttractorCanvas.

public void RenderScreen(System.Windows.Controls.Canvas c, float originX, float originY)
{
    System.Windows.Media.SolidColorBrush scb
        = new SolidColorBrush(System.Windows.Media.Colors.Black);
 
    PathGeometry pathGeometry = new PathGeometry();
    PathFigureCollection pathFigureCollection = new PathFigureCollection();
   
    int n = 0;
    foreach (Vector3D v in this.drawableObjects)
    {
        PathFigure pathFigure = new PathFigure();
        QuadraticBezierSegment quadraticBezierSegment = new QuadraticBezierSegment();
   
        if (pathFigureCollection.Count > 0)
        {
            var oldPathFigure = pathFigureCollection[n - 1];
            if (oldPathFigure != null)
                pathFigure.StartPoint =
        ((QuadraticBezierSegment)oldPathFigure.Segments[0]).Point2;
        }
        float[] point1 = MatrixVectorMultiply(TransformMatrix, v.Origin.ToArray());
        float[] point2 = MatrixVectorMultiply(TransformMatrix, v.Destination.ToArray());
  
        quadraticBezierSegment.Point1 = new Point(
                point1[0] / (1 - (point1[2] / FocalLength)) + originX,
                point1[1] / (1 - (point1[2] / FocalLength)) + originY);
        quadraticBezierSegment.Point2 = new Point(
                (point2[0] / (1 - (point2[2] / FocalLength))) + originX,
                point2[1] / (1 - (point2[2] / FocalLength)) + originY);
    
        pathFigure.Segments.Add(quadraticBezierSegment);
   
        pathFigureCollection.Add(pathFigure);
        n++;
    }
    pathGeometry.Figures = pathFigureCollection;
    Path path = new Path();
    path.Stroke = scb;
    path.StrokeThickness = 1;
    path.Data = pathGeometry;
    c.Children.Add(path);
}

RenderScreen() принимает график векторов и выполняет все необходимые шаги для рисования графика на холсте. Предполагается отобразить сцену в WPF, для которой будет выбран сегмент квадратичного Безье для достижения плавности. Затем, чтобы повернуть изображение, над вектором производится матричное умножение в 3-х измерениях с помощью метода MatrixVectorMultiply, обсуждение которого заслуживает отдельной статьи. Наконец, чтобы обеспечить масштабирование, вычисляется фокусное расстояние для обеих точек в сегменте Безье, чтобы увеличить или уменьшить длину сегмента.

Использование кода

Остальной код в загрузке рисует графики на экране и дает пользователю возможность менять начальные условия, вращать и обновлять изображение. Поскольку отображение на экране производится в реальном времени, не задавайте слишком много итераций и не просите приложение вращать график слишком быстро, иначе компьютер не справится.