Is it possible to optimize a function with discrete variables in `mealpy`?

I have been working with the mealpy Python library for quite some time– the repository of around 200 metaheuristic algorithms including GA, DE, ACO, PSO, …authored by Nguyen Van Thieu, alias thieu1995.

However, working with discrete variables (particularly categoric variables in the form of strings, or integer variables) remains a nightmare. Variables have to be encoded, then decoded again in the fitness function, or outside– which are prone to cause several anomalies and vulnerabilities, especially when there are some dependent variables in the model, alongside discrete constraints or ranges.

Same (though somewhat less painful) for integer variables, which have to be rounded off or truncated everytime a fitness function is calculated, and once again when the best solution is presented.

Is it possible to encode integer or categoric variables with mealpy ?

Till October 2023

mealpy version 2.x.y had its “problem_dictionary” defined as :

problem_dict = {
     "fit_func": fitness_function,
     "lb": lb,
     "ub": ub,
     "minmax": "min",
     "log_to": None,
}

where lower and upper bounds lb and ub had to be mandatorily specified as floating point (real-valued) ranges.

New in November 2023

mealpy version 3.0.0 onward, there are options to define several new variable types, denoted in :

  • FloatVar,
  • IntegerVar,
  • StringVar,
  • BoolVar,
  • PermutationVar,
  • BinaryVar, and
  • MixedSetVar

classes

Here’s the code to try them out on three simple 3-varaible problems, where :

  1. all variables are int,
  2. all variables are str, and
  3. one is int, another is str, while the third is float.
""" More at : https://github.com/thieu1995/mealpy/blob/master/mealpy/utils/space.py """

import numpy as np
from mealpy import IntegerVar, StringVar, MixedSetVar
from mealpy import GA


class Optimize_With_String_Variable():
    def __init__(self, number_of_variables=3):
        self.number_of_variables = number_of_variables
        self.one_valid_set = ['1', '3', '5', '7', '9', '11', '13']
        self.bounds = StringVar(valid_sets=[self.one_valid_set]*number_of_variables)
        self.problem_dict = {
                "obj_func": self.objective_func,
                "bounds": self.bounds,
                "minmax": "min",
                "log_to": "file", 
                "log_file": "ga_results.txt"
            }
    
    def objective_func(self, solution):
        decoded_solution = self.bounds.decode(solution)
        int_sol = [int(n) for n in decoded_solution]
        result = np.sum((np.array(int_sol) - 3)**2)

        return result

    def optimize(self):
        optimizer = GA.BaseGA(epoch=100, pop_size=50, pc=0.85, pm=0.1)
        optimizer.solve(self.problem_dict)

        best_solution = optimizer.g_best.solution
        best_solution = self.bounds.decode(best_solution)

        best_fitness = optimizer.g_best.target.fitness

        return best_solution, best_fitness


class Optimize_With_Integer_Variable():
    def __init__(self, number_of_variables=3):
        self.number_of_variables = number_of_variables
        self.bounds = IntegerVar(lb=[1,]*number_of_variables, ub=[13,]*number_of_variables)
        self.problem_dict = {
                "obj_func": self.objective_func,
                "bounds": self.bounds,
                "minmax": "min",
                "log_to": "file", 
                "log_file": "ga_results.txt"
            }
    
    def objective_func(self, solution):
        result = np.sum((np.array(solution) - 3)**2)

        return result

    def optimize(self):
        optimizer = GA.BaseGA(epoch=100, pop_size=50, pc=0.85, pm=0.1)
        optimizer.solve(self.problem_dict)

        best_solution = optimizer.g_best.solution
        best_fitness = optimizer.g_best.target.fitness

        return best_solution, best_fitness


class Optimize_With_Mixed_Set_Variable():
    def __init__(self, number_of_variables=3):
        self.number_of_variables = number_of_variables
        ##  string variable
        first_valid_set = ['a', 'c', 'e', 'g', 'i', 'k', 'm']
        ##  integer variable
        second_valid_set = [1, 3, 5, 7, 9, 11, 13]
        ##  float variable  (encoded as pesudo-float with 2-digit precision)
        third_valid_set = np.linspace(1, 13, 121)
        self.bounds = MixedSetVar(valid_sets=[first_valid_set, second_valid_set, third_valid_set])
        self.problem_dict = {
                "obj_func": self.objective_func,
                "bounds": self.bounds,
                "minmax": "min",
                "log_to": "file", 
                "log_file": "ga_results.txt"
            }
    
    def objective_func(self, solution):

        def integer_from_mixed_type(x):
            """ 
            if `x` is `int` or `float`:
                return `x`, 
            
            elif `x` is `char`: 
                return `ord(x) - 96`  

                [assume `"a" = 1`]  """
            if isinstance(x, int):
                return x
            elif isinstance(x, float):
                return x
            elif isinstance(x, str):
                return ord(x) - 96
            else:
                raise Exception('solution element', x, 'is none of int/float/str')

        decoded_solution = self.bounds.decode(solution)
        result = np.sum((np.array([integer_from_mixed_type(x) for x in decoded_solution]) - 3)**2)

        return result

    def optimize(self):
        optimizer = GA.BaseGA(epoch=100, pop_size=50, pc=0.85, pm=0.1)
        optimizer.solve(self.problem_dict)

        best_solution = optimizer.g_best.solution
        best_solution = self.bounds.decode(best_solution)

        best_fitness = optimizer.g_best.target.fitness

        return best_solution, best_fitness




if __name__ == "__main__":
    
    for op_problem in (
        ##  string (categoric) variables
        Optimize_With_String_Variable(3),
        ##
        ##  integer (discrete) variables
        Optimize_With_Integer_Variable(3),
        ##
        ##  combination of string/int/float (all discrete) variables
        Optimize_With_Mixed_Set_Variable(3),
    ):
        print (op_problem.bounds.name)
        best_solution, best_fitness = op_problem.optimize()

        print ('Best Solution =', best_solution)
        print ('Best Fitness=", best_fitness)
        print ()

which produces the output :

string
Best Solution = ["3', '3', '3']
Best Fitness = 0.0

integer
Best Solution = [3. 3. 3.]
Best Fitness = 0.0

mixed-set-var
Best Solution = ['c', 3, 3.0]
Best Fitness = 0.0

Caveat

Unfortunately, a real float variable cannot be mixed with discrete variables in mealpy still, and they will have to be coded as “pseudo-float” with a finite precision.

We can expect this feature in their future releases. But still, what’s here is precious enough !

Leave a Comment