Transformation from MATLAB to PyTorch and EvoX#

This document aims to guide MATLAB users in transitioning to PyTorch and EvoX for evolutionary computation. We will highlight the core differences between MATLAB and PyTorch in terms of syntax, data structures, and workflow. We will then illustrate these differences using a Particle Swarm Optimization (PSO) example in both MATLAB and PyTorch.

Syntax Differences#

Array Creation and Indexing#

MATLAB#

  • Uses 1-based indexing.

  • Vectors and matrices are declared using square brackets and semicolons (e.g., [1 2 3; 4 5 6]). Random initialization with rand() returns values in the interval \([0, 1)\).

  • Slicing is performed using the (start:end) syntax and utilizes 1-based indexing.

PyTorch#

  • Uses 0-based indexing.

  • Arrays (tensors) are typically created using constructors like torch.rand(), torch.zeros(), or Python lists converted to tensors with torch.tensor().

  • Slicing is done using [start:end] with 0-based indices.

Matrix Computation#

MATLAB#

  • Performs linear algebraic matrix multiplication by *.

  • Uses .* to multiply corresponding elements of matrices of the same size.

  • / represents the matrix right division.

  • .^ represents the element-wise power.

  • Trailing and leading dimension(s) of tensors with length 1 is/are ignored.

  • Automatically find broadcastable dimensions for element-wise operations and perform implicit dimension extension.

PyTorch#

  • Performs linear algebraic matrix multiplication by @ or torch.matmul().

  • Directly uses * to multiply corresponding elements of tensors of the same shape or broadcastable shapes.

  • / represents the element-wise division.

  • ** represents the element-wise power.

  • Dimension(s) of tensors with length 1 is/are preserved and treated as broadcast dimension.

  • Prevent most implicit dimension extension, broadcast dimension(s) are usually required.

Functions and Definitions#

MATLAB#

  • A function is defined by the function keyword.

  • A file can contain multiple functions, but typically the primary function shares the file name.

  • Anonymous functions (e.g., @(x) sum(x.^2)) are used for short inline calculations.

PyTorch#

  • Functions are defined using the def keyword, typically within a single .py file or module.

  • Classes are used to encapsulate data and methods in an object-oriented manner.

  • Lambdas serve as short anonymous functions (lambda x: x.sum()), but multi-line lambdas are not allowed.

Control Flow#

MATLAB#

  • Uses for i = 1:Nend loops with 1-based indexing.

  • Conditional statements like if, elseif, and else.

PyTorch#

  • Uses for i in range(N): with 0-based indexing.

  • Indentation is significant for scoping in loops and conditionals (no end keyword).

Printing and Comments#

MATLAB#

  • Uses fprintf() functions for formatted output.

  • Uses % for single-line comments.

PyTorch#

  • Uses print with f-strings for formatted output.

  • Uses # for single-line comments.

Multi-line Coding#

MATLAB#

  • Uses ... at the trailing of a line to indicate that the next line shall be treated as the same line as.

Python#

  • Uses \ at the trailing of a line to indicate that the next line shall be treated as the same line as.

  • If multiple lines are inside parentheses, no specific trailing symbol is required.

How to Write Evolutionary Computation Algorithm via EvoX?#

MATLAB#

A MATLAB code example for PSO algorithm is as follows:

function [] = example_pso()
    pso = init_pso(100, [-10, -10], [10, 10], 0.6, 2.5, 0.8);
    test_fn = @(x) (sum(x .* x, 2));
    for i = 1:20
        pso = step_pso(pso, test_fn);
        fprintf("Iteration = %d, global best = %f\n", i, pso.global_best_fitness);
    end
end


function [self] = init_pso(pop_size, lb, ub, w, phi_p, phi_g)
    self = struct();
    self.pop_size = pop_size;
    self.dim = length(lb);
    self.w = w;
    self.phi_p = phi_p;
    self.phi_g = phi_g;
    % setup
    range = ub - lb;
    population = rand(self.pop_size, self.dim);
    population = range .* population + lb;
    velocity = rand(self.pop_size, self.dim);
    velocity = 2 .* range .* velocity - range;
    self.lb = lb;
    self.ub = ub;
    % mutable
    self.population = population;
    self.velocity = velocity;
    self.local_best_location = population;
    self.local_best_fitness = Inf(self.pop_size, 1);
    self.global_best_location = population(1, :);
    self.global_best_fitness = Inf;
end


function [self] = step_pso(self, evaluate)
    % Evaluate
    fitness = evaluate(self.population);
    % Update the local best
    compare = find(self.local_best_fitness > fitness);
    self.local_best_location(compare, :) = self.population(compare, :);
    self.local_best_fitness(compare) = fitness(compare);
    % Update the global best
    values = [self.global_best_location; self.population];
    keys = [self.global_best_fitness; fitness];
    [min_val, min_index] = min(keys);
    self.global_best_location = values(min_index, :);
    self.global_best_fitness = min_val;
    % Update velocity and position
    rg = rand(self.pop_size, self.dim);
    rp = rand(self.pop_size, self.dim);
    velocity = self.w .* self.velocity ...
        + self.phi_p .* rp .* (self.local_best_location - self.population) ...
        + self.phi_g .* rg .* (self.global_best_location - self.population);
    population = self.population + velocity;
    self.population = min(max(population, self.lb), self.ub);
    self.velocity = min(max(velocity, self.lb), self.ub);
end

In MATLAB, function init_pso() initializes the algorithm, and a separate function step_pso() performs an iteration step and the main function example_pso() orchestrates the loop.

EvoX#

In EvoX, you can construct the PSO algorithm in following way:

First, it is recommended to import necessary modules and functions from EvoX and PyTorch.

import torch

from evox.core import *
from evox.utils import *
from evox.workflows import *
from evox.problems.numerical import Sphere

Then, you can transform the MATLAB code to the python code correspondingly according to the “Syntax Differences” section.

def main():
    pso = PSO(pop_size=10, lb=torch.tensor([-10.0, -10.0]), ub=torch.tensor([10.0, 10.0]))
    wf = StdWorkflow()
    wf.setup(algorithm=pso, problem=Sphere())
    for i in range(1, 21):
        wf.step()
        print(f"Iteration = {i}, global best = {wf.algorithm.global_best_fitness}")

@jit_class
class PSO(Algorithm):
    def __init__(self, pop_size, lb, ub, w=0.6, phi_p=2.5, phi_g=0.8):
        super().__init__()
        self.pop_size = pop_size
        self.dim = lb.shape[0]
        self.w = w
        self.phi_p = phi_p
        self.phi_g = phi_g
        # setup
        lb = lb.unsqueeze(0)
        ub = ub.unsqueeze(0)
        range = ub - lb
        population = torch.rand(self.pop_size, self.dim)
        population = range * population + lb
        velocity = torch.rand(self.pop_size, self.dim)
        velocity = 2 * range * velocity - range
        self.lb = lb
        self.ub = ub
        # mutable
        self.population = population
        self.velocity = velocity
        self.local_best_location = population
        self.local_best_fitness = torch.full((self.pop_size,), fill_value=torch.inf)
        self.global_best_location = population[0, :]
        self.global_best_fitness = torch.tensor(torch.inf)

    def step(self):
        # Evaluate
        fitness = self.evaluate(self.population)
        # Update the local best
        compare = self.local_best_fitness > fitness
        self.local_best_location = torch.where(compare.unsqueeze(1), self.population, self.local_best_location)
        self.local_best_fitness = torch.where(compare, fitness, self.local_best_fitness)
        # Update the global best
        values = torch.cat([self.global_best_location.unsqueeze(0), self.population], dim=0)
        keys = torch.cat([self.global_best_fitness.unsqueeze(0), fitness], dim=0)
        min_index = torch.argmin(keys)
        self.global_best_location = values[min_index]
        self.global_best_fitness = keys[min_index]
        # Update velocity and position
        rg = torch.rand(self.pop_size, self.dim)
        rp = torch.rand(self.pop_size, self.dim)
        velocity = (
            self.w * self.velocity
            + self.phi_p * rp * (self.local_best_location - self.population)
            + self.phi_g * rg * (self.global_best_location - self.population)
        )
        population = self.population + velocity
        self.population = clamp(population, self.lb, self.ub)
        self.velocity = clamp(velocity, self.lb, self.ub)


# Run the main function
if __name__ == "__main__":
    main()

Note

It is worth noting that we use [] with ; and , in MATLAB to concatenate matrices and vectors along specific dimension; however, in EvoX, the torch.cat must be invoked with argument dim to indicate the concatenation dimension. Moreover, in PyTorch, tensors to be concatenated must have the same number of dimensions; therefore, additional XXX.unsqueeze(0) is applied to add a new dimension of length 1 before the first dimension.

In EvoX, the PSO logic is encapsulated within a class that inherits from Algorithm. This object-oriented design simplifies state management and iteration, and introduces following advantages:

  • Inherited evaluate() method You can simply call self.evaluate(self.population) to compute fitness values, rather than manually passing your objective function each iteration.

  • Built-In Workflow Integration When you register your PSO class with a workflow StdWorkflow, it handles iterative calls to step() on your behalf.

By extending Algorithm, __init__() sets up all major PSO components (population, velocity, local/global best, etc.) in a standard Python class constructor.