Engee documentation

Vectorisation and logical indexing

Vectorisation

Vectorisation is the adaptation of a function to work with all elements of a vector or matrix at once, instead of usage of loops and scalar operations. The Engee programming language, Julia, supports vectorisation operations. Vectorisation provides the following advantages:

  • Reduces the number of lines of code, reducing the chance of errors;

  • Vectorised code most often looks like mathematical expressions, making it easier to understand and verify;

  • Vectorised code runs faster because Julia handles vector and matrix operations with high efficiency.

Adding a dot before an operator (.*, .^, ./) turns it into an array operation, allowing it to be applied to a specific array element. Basic operators that support vectorisation include +, -, *, *, /, ^ and comparisons such as <, >, <=, >=, ==, !=.

In Julia, almost all basic operations support vectorisation through the addition of a dot before the operator. Below is a list of basic operations that can be vectorised:

List of operations that support vectorisation in Julia

The following is a list of the basic operations that support vectorisation in Julia. However, the exact list includes even more functions, since vectorisation in Julia extends to many built-in functions. It is important to realise that vectorisation can be applied not only to basic arithmetic and logical operators, but also to mathematical functions, some functions from standard libraries (if they support array handling, e.g. .min, .max) and user-defined functions if they are designed to support array handling.

*Arithmetic operations:

  1. .+ - element-by-element addition;

  2. .- - element-by-element subtraction;

  3. .* - element-by-element multiplication;

  4. ./ - elemental division;

  5. .^ - elemental degree ascension;

  6. .% - elemental remainder from division.

*Comparison operations:

  1. .== - elemental equality;

  2. .!= - elemental inequality;

  3. .> - elemental greater;

  4. .< - elemental less;

  5. .>= - elemental greater than or equal to;

  6. .< - elemental less than or equal to.

*Logic operations:

  1. .& - element-by-element logical "and";

  2. .| - elemental logical "or";

  3. .⊻ - elemental logical "exclusive or";

  4. .! - elemental logical "not".

*Bit operations:

  1. .>> - element-by-element shift to the right;

  2. .<< - elemental shift to the left;

  3. .& - element-by-element bitwise "and";

  4. .| - element-by-element bitwise "or";

  5. .⊻ - element-by-element bitwise "exclusive or";

  6. .~ - element-by-element bitwise "not".

*Unary operations:

  1. .√ - elemental square root;

  2. .log - elemental logarithm;

  3. .exp - elemental exponential transformation;

  4. .sin, .cos, .tan and other trigonometric functions also support element-by-element operation.

Not all operations require vectorisation. For example, the assignment operator = and some specific functions (e.g., working with one entire array, such as sort) do not support vectorisation.

Supported operations

Vectorisation allows operations to be applied to all elements of an array simultaneously. This reduces the need for explicit loops, which can slow down program execution, and allows the compiler to optimise execution.

In Julia, vectorisation is often achieved through built-in functions and array manipulation tools, making it easy to create code that closely resembles mathematical formulas. For example, you want to calculate the sine for 1001 values from 0 to 10. A typical Julia code would look like this:

y = []
for t in 0:0.01:10
    push!(y, sin(t))
end

Then the vectorised code might look like this:

t = 0:0.01:10
y = sin.(t)

Here sin.(t) means that the function sin is applied to each element of the array t. This version of the code is faster than the first one and is a more efficient usage of Julia as a language for mathematical calculations.

In Julia, you can perform operations on arrays by applying them to all elements simultaneously. For example, if you have data on the diameter D and height H for 10000 geometric cones, you can calculate their volumes without usage of loops:

D = rand(10_000)
H = rand(10_000)
V = (1 / 12) * π * D.^2 .* H

Logical indexing

Logical indexing is a method of accessing array elements based on logical conditions. Instead of specifying the element index, an array of logical values (true or false) is passed, where true indicates which elements to select. This simplifies data filtering, especially when working with large arrays. Logical indexing allows:

  • Quickly select data that matches a certain condition;

  • Eliminate incorrect or unwanted values;

  • Create concise algorithms with a minimum number of lines of code.

Examples

Loop replacement

Vectorisation is especially useful when performing specific tasks. For example, finding the accumulated sum on every fifth element of an array. Without vectorisation, the code will look like this:

x = 1:10000
y = []
for n in 5:5:length(x)
    push!(y, sum(x[1:n]))
end

Then the vectorised code will look like this:

x = 1:10000
y = cumsum(x)[5:5:end]

Here the cumsum function calculates the accumulated sum for all array elements, which avoids usage of the loop.

Vectorisation and logical indexing

Julia supports vectorisation of logical operations. For example, if we have arrays of cone sizes and some of the diameter values turned out to be negative, we can easily determine the correct values using vectorisation.

To do this, first create arrays D and H and calculate the volumes of the cones:

# Определяем массивы D и H
D = [-0.2, 1.0, 1.5, 3.0, -1.0, 4.2, 3.14]
H = [1.0, 2.0, 2.5, 3.5, 1.5, 4.5, 3.0]

# Рассчитываем объемы
V = (1 / 12) * π * D.^2 .* H

The result will be an array V with the calculated volumes, in which you can apply logical indexing (see Sect. Logical indexing) to filter out only the correct values. The logical operation will create an array of true and false, where true corresponds to positive values:

valid_D = D .>= 0
Vgood = V[valid_D]

Now the variable Vgood contains volumes only for those cones whose diameter is positive:

engee> 5-element Vector{Float64}:
  0.5235987755982988
  1.4726215563702154
  8.246680715673207
 20.78163540349648
  7.743711731833481

In Julia, logical indexing can be combined with vectorisation, for example:

# Исходные данные (массив температур)
temps = [15.2, -3.0, 22.5, 0.0, 25.1, -10.5, 30.0]

# Условие - отбор только положительных значений
positive_temps = temps[temps .> 0]

println(positive_temps)

Logical indexing allows you to apply not only simple but also compound conditions. For example:

# Исходные данные (массив лет)
ages = [25, 17, 34, 45, 15, 27, 18]

# Условие - отбор совершеннолетних (>= 18), но моложе 30
valid_ages = ages[(ages .>= 18) .& (ages .< 30)]

println(valid_ages)

Operations with matrices

Often vectorisation helps to create matrices of the desired size and structure. For example, if you need to create a 5x5 matrix with all elements equal to 10, you can use the fill function:

A = fill(10, 5, 5)

The code above will output the result:

5×5 Matrix{Int64}:
 10  10  10  10  10
 10  10  10  10  10
 10  10  10  10  10
 10  10  10  10  10
 10  10  10  10  10

Using vectorisation, matrices of different sizes can be folded if they are compatible for the operation broadcast. For example, if matrix A is a 3 × 3 matrix and B is a vector of length 3:

A = [1 2 3; 4 5 6; 7 8 9]
B = [1, 2, 3]
C = A .+ B

This will create a new matrix C, where each element of vector B is added to the corresponding column of matrix A. The result will be: C = [2 3 4; 6 7 8; 10 11 12].


Consider multiplication in Julia. There are two types of multiplication operations:

  • performs standard matrix multiplication. performs matrix multiplication corresponding to linear algebra. Each element in the resulting matrix is obtained by computing the sum of the products of the row elements of the first matrix by the column elements of the second matrix;

  • . performs element-by-element multiplication. . performs element-by-element multiplication, where each element of the first matrix is multiplied by the corresponding element of the second matrix.

For example:

# Определение двух матриц
A = [1 2; 3 4]  # Матрица 2x2
B = [2 0; 1 3]  # Матрица 2x2

# Матричное умножение
C = A * B

# Поэлементное умножение
D = A .* B

Results:

  • For C: C = [4 6; 10 12].

  • For D: D = [2 0; 3 12].

If the matrix is multiplied by a scalar, then both operators ( and .) work the same way.