Using Genie to visualise the action of a linear operator
This article focuses on the use of the GenieFramework to develop an interactive web application that demonstrates the action of a linear operator. As an example, we consider the transformation of a set of points representing a circle using a 2×2 matrix. The Stipple and PlotlyBase packages are used for implementation. The text describes in detail the key elements of the code, with explanations provided alongside the relevant code snippets. Particular attention is paid to justifying the use of reactive macros @in and @out to control the elements of the matrix (m11, m12, m21, m22), and the possibilities of using custom types, such as mutable struct, based on the Stipple documentation.
: Application Concept
The appendix illustrates the effect of a 2x2 matrix as a linear operator on a two-dimensional set of points. Initially, the points form a circle of radius 1, and the user can change the elements of the matrix using sliders, observing the effects of transformations such as stretching, compression or rotation. This approach has educational potential by providing an interactive tool for learning linear algebra.
: Creating a circle from a set of points
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)
This fragment provides the initial data for visualisation. The range is defined circ_range –1 to 1 in increments of 0.05 for the x and y coordinates. Coordinate pairs are generated using the list inclusion function [i, j] , filtered by the circle equation i^2 + j^2 <= 1``, which yields a set of points approximating a circle of radius 1. The variables x_axis_points and y_axis_points contain indices of points lying on the positive parts of the Y and X axes, respectively, to highlight the basis vectors. The function Base.stack converts a list of coordinates into a matrix, where the first row contains the x-coordinates and the second row contains the y-coordinates. Constants (const) are used for immutable data. The discrete step results in a stepped contour of the circle, but the method is straightforward to implement.
Developing a graph conversion function
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)
The function `create_plot_data is responsible for converting points and preparing data for the graph. It accepts four arguments — elements of a 2×2 matrix (```m11```, m12```, ` m21, ` ``m22). Performs matrix multiplication by circle_matrix, obtaining the new coordinates of the points in transformed. Returns an array of three objects scatter: all points on the circle, points on the Y-axis and points on the X-axis. The parameter aspect_ratio=:equal provides a uniform scale for the axes. The constant initial_plot_data sets the initial state of a graph using a unit matrix that does not alter the circle.
---### : Configuring the graph layout
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
)
The fragment defines the graph layout using PlotlyBase.Layout``. The heading, axis labels, grid and value range from -2 to 2 are set. The graph dimensions are fixed: width 600 pixels, height 550 pixels. The layout is declared as a constant, as it does not change whilst the application is running.
Ensuring reactivity
@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
The block @app defines the reactive application model. The macros @in declare the matrix elements as input variables with initial values corresponding to the identity matrix. The macro @out sets the output data: plot_data for the schedule and plot_layout for the layout. The macro @onchange tracks changes in the values m11, m12, m21 and m22, and causes create_plot_data to update plot_data.
Customising the user interface
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)
The function ui forms the interface. The variable sliders creates an array of two lines, each of which contains two sliders with captions (m11, m12, m21, m22). The sliders range from -2 to 2 in increments of 0.1. The interface consists of a row (row) with two columns: sliders on the left and a graph on the right, displayed via plot. The macro @page binds the interface to the root route "/".
The need for application- @in and @out#### Reactivity in Stipple
Stipple implements a reactive model, ensuring the synchronisation of data and the interface. Macros @in and @out integrate the elements into this model. @in binds data to interface elements (sliders), allowing the user to change them. @out updates the output data (graph) when the state changes. When using standard ads, for example m11 = 1.0, the interface elements lose their connection with the values, preventing them from being modified; @onchange it does not respond because Stipple does not track such data; interactivity is disrupted because values are not passed to JavaScript.#### An alternative with custom types
According to the Stipple documentation (Types of variables in Stipple), it is possible to use mutable struct as reactive variables:
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
Structure- MatrixState, announced via @in, which allows Stipple to track changes in its fields. However, in this application, preference is given to individual variables (@in m11, etc.), as this simplifies the management of matrix elements via separate sliders.
Without @in and @out, the matrix elements remain isolated in Julia, without interacting with the interface. Reactive macros or mutable struct with @in/@out provide the necessary communication, and the choice of approach depends on the data structure and interface requirements.
To launch the application, we will use the one already familiar from previous articles by design:
using Markdown
cd(@__DIR__)
app_url = string(engee.genie.start(string(@__DIR__,"/app.jl")))
Markdown.parse(match(r"'(https?://[^']+)'",app_url)[1])
Conclusion
This article demonstrates the use of GenieFramework to create an interactive application that visualises the action of a linear operator. Reactive macros @in and @out have provided a link between the interface and the logic, whilst the ability to use mutable struct offers flexibility for complex scenarios. The app highlights Genie’s capabilities for developing educational tools.---