Документация Engee
Notebook

Использование Genie для визуализации действия линейного оператора

Статья посвящена применению GenieFramework для разработки интерактивного веб-приложения, демонстрирующего действие линейного оператора. В качестве примера рассмотрено преобразование набора точек, представляющих круг, с использованием матрицы 2×2. Для реализации задействованы пакеты Stipple и PlotlyBase. В тексте подробно описаны ключевые элементы кода с пояснениями, следующими за соответствующими фрагментами. Особое внимание уделено обоснованию применения реактивных макросов @in и @out для управления элементами матрицы (m11, m12, m21, m22), а также рассмотрены возможности использования пользовательских типов, таких как mutable struct, на основе документации Stipple.


Концепция приложения

circle.png

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


Создание круга из набора точек

using GenieFramework, Stipple, Stipple.ReactiveTools, StippleUI, PlotlyBase

const circ_range = -1:0.05:1
const circle = [[i, j] for i in circ_range for j in circ_range if i^2 + j^2 <= 1]
const x_axis_points = findall(x -> x[1] == 0 && x[2] >= 0, circle)
const y_axis_points = findall(x -> x[2] == 0 && x[1] >= 0, circle)
const circle_matrix = Base.stack(circle)

Этот фрагмент формирует исходные данные для визуализации. Определён диапазон circ_range от -1 до 1 с шагом 0.05 для координат x и y. С помощью спискового включения сгенерированы пары координат [i, j], отфильтрованные по уравнению круга i^2 + j^2 <= 1, что даёт набор точек, аппроксимирующий круг радиуса 1. Переменные x_axis_points и y_axis_points содержат индексы точек, лежащих на положительных частях осей Y и X соответственно, для выделения базисных векторов. Функция Base.stack преобразует список координат в матрицу, где первая строка — x-координаты, а вторая — y-координаты. Константы (const) используются для неизменяемых данных. Дискретный шаг приводит к некоторой ступенчатости контура круга, но метод прост в реализации.


Разработка функции преобразования графика

function create_plot_data(m11::Float64, m12::Float64, m21::Float64, m22::Float64)
    transformed = [m11 m12; m21 m22] * circle_matrix
    [
        scatter(x=transformed[1, :], y=transformed[2, :], mode="markers", name="Точки круга"),
        scatter(x=transformed[1, x_axis_points], y=transformed[2, x_axis_points], name="Ось Y"),
        scatter(x=transformed[1, y_axis_points], y=transformed[2, y_axis_points], name="Ось X", aspect_ratio=:equal)
    ]
end

const initial_plot_data = create_plot_data(1.0, 0.0, 0.0, 1.0)

Функция create_plot_data отвечает за преобразование точек и подготовку данных для графика. Принимает четыре аргумента — элементы матрицы 2×2 (m11, m12, m21, m22). Выполняет умножение матрицы на circle_matrix, получая новые координаты точек в transformed. Возвращает массив из трёх объектов scatter: все точки круга, точки на оси Y и точки на оси X. Параметр aspect_ratio=:equal обеспечивает равномерный масштаб осей. Константа initial_plot_data задаёт начальное состояние графика с единичной матрицей, не изменяющей круг.


Настройка макета графика

const plot_layout = PlotlyBase.Layout(
    title="Линейное преобразование круга",
    xaxis=attr(title="Ось X", showgrid=true, range=[-2, 2]),
    yaxis=attr(title="Ось Y", showgrid=true, range=[-2, 2]),
    width=600, height=550
)

Фрагмент определяет макет графика с помощью PlotlyBase.Layout. Устанавливаются заголовок, подписи осей, сетка и диапазон значений от -2 до 2. Размеры графика фиксированы: ширина 600 пикселей, высота 550 пикселей. Макет объявлен как константа, так как не изменяется в процессе работы приложения.


Обеспечение реактивности

@app begin
    @in m11 = 1.0
    @in m12 = 0.0
    @in m21 = 0.0
    @in m22 = 1.0
    @out plot_data = initial_plot_data
    @out plot_layout = plot_layout

    @onchange m11, m12, m21, m22 begin
        plot_data = create_plot_data(m11, m12, m21, m22)
    end
end

Блок @app определяет реактивную модель приложения. Макросы @in объявляют элементы матрицы как входные переменные с начальными значениями, соответствующими единичной матрице. Макрос @out задаёт выходные данные: plot_data для графика и plot_layout для макета. Макрос @onchange отслеживает изменения значений m11, m12, m21, m22 и вызывает create_plot_data для обновления plot_data.


Настройка пользовательского интерфейса

function ui()
    sliders = [row([column([h6("m$(i)$(j)={{m$(i)$(j)}}"), slider(-2:0.1:2, Symbol("m$(i)$(j)"), color="purple")], size=3) for j in 1:2]) for i in 1:2]
    [
        row([
            column(sliders, size=4),
            column(plot(:plot_data, layout=:plot_layout))
        ], size=3)
    ]
end

@page("/", ui)

Функция ui формирует интерфейс. Переменная sliders создаёт массив из двух строк, каждая из которых содержит два ползунка с подписями (m11, m12, m21, m22). Диапазон ползунков — от -2 до 2 с шагом 0.1. Интерфейс состоит из строки (row) с двумя столбцами: слева ползунки, справа график, отображаемый через plot. Макрос @page привязывает интерфейс к корневому маршруту "/".


Необходимость применения @in и @out

Реактивность в Stipple

Stipple реализует реактивную модель, обеспечивая синхронизацию данных и интерфейса. Макросы @in и @out интегрируют элементы в эту модель. @in связывает данные с элементами интерфейса (ползунками), позволяя пользователю их изменять. @out обновляет выходные данные (график) при изменении состояния. При использовании стандартных объявлений, например m11 = 1.0, элементы интерфейса теряют связь с значениями, исключая их изменение; @onchange не реагирует, так как Stipple не отслеживает такие данные; интерактивность нарушается, поскольку значения не передаются в JavaScript.

Альтернатива с пользовательскими типами

Согласно документации Stipple (Типы переменных в Stipple), возможно использование mutable struct как реактивных переменных:

mutable struct MatrixState
    m11::Float64
    m12::Float64
    m21::Float64
    m22::Float64
end

@app begin
    @in state = MatrixState(1.0, 0.0, 0.0, 1.0)
    @out plot_data = initial_plot_data
    @out plot_layout = plot_layout

    @onchange state begin
        plot_data = create_plot_data(state.m11, state.m12, state.m21, state.m22)
    end
end

Структура MatrixState объявлена через @in, что позволяет Stipple отслеживать изменения её полей. Однако в данном приложении предпочтение отдано индивидуальным переменным (@in m11 и т.д.), так как это упрощает управление элементами матрицы через отдельные ползунки.

Без @in и @out элементы матрицы остаются как бы изолированными в Julia, не взаимодействуя с интерфейсом. Реактивные макросы или mutable struct с @in/@out обеспечивают необходимую связь, при этом выбор подхода зависит от структуры данных и требований интерфейса.


Для запуска приложения, воспользуемся уже знакомой по предыдущим статьям конструкцией:

In [ ]:
using Markdown
cd(@__DIR__)

app_url = string(engee.genie.start(string(@__DIR__,"/app.jl")))

Markdown.parse(match(r"'(https?://[^']+)'",app_url)[1])

Заключение

В статье продемонстрировано применение GenieFramework для создания интерактивного приложения, визуализирующего действие линейного оператора. Реактивные макросы @in и @out обеспечили связь между интерфейсом и логикой, а возможность использования mutable struct предоставляет гибкость для сложных сценариев. Приложение подчёркивает возможности Genie для разработки образовательных инструментов.