Advance Guide
Stopping Condition (Termination)
In meta-heuristic algorithms, the optimization process involves iteratively generating and evolving a population of candidate solutions (individuals) to the problem. Each generation consists of evaluating the fitness of each individual, selecting the best individuals for reproduction, and applying specific operators to generate a new population.
By setting a maximum number of generations as a stopping condition, the algorithm will terminate after a certain number of iterations, even if a satisfactory solution has not been found. This is highly useful to prevent the algorithm from running indefinitely, especially if there are no clear convergence criteria or if the fitness landscape is complex and difficult to navigate.
Important
Resource Management & Convergence
The choice of the maximum number of generations must be based on the specific problem being solved and the available computational resources. A number that is too small may prevent the algorithm from converging to a satisfactory solution, while a number that is too large may result in unnecessary computational expense.
Note
Default Behavior
By default, when creating an optimizer in MEALPY, the default stopping condition is based purely on epochs (generations/iterations).
Attention
Advanced Termination Strategies
You can explore different stopping conditions by creating a Termination dictionary. Furthermore, you can combine multiple stopping criteria together to improve and strictly control your model’s execution time!
Termination Types
There are 4 termination types available in the Termination class:
MG (Maximum Generations): Also known as Epochs or Iterations.
FE (Function Evaluations): The maximum number of times the objective function is evaluated.
TB (Time Bound): Forces the algorithm to run for a fixed amount of time (e.g., K seconds). This is especially useful when benchmarking or comparing different algorithms.
ES (Early Stopping): Inspired by neural network training, this stops the program if the global best solution has not improved by an \(\epsilon\) margin after K consecutive epochs.
Configuration Parameters
To configure these conditions, provide the following parameters to the Termination class (set any parameter to None if you do not want to use it):
max_epoch(int): Indicates the maximum number of generations for the MG type.max_fe(int): Indicates the maximum number of function evaluations for the FE type.max_time(float): Indicates the maximum amount of time in seconds for the TB type.max_early_stop(int): Indicates the maximum number of epochs for the ES type.epsilon(float): (Optional) The tolerance value used for the ES termination type (default value:1e-10).termination(dict): (Optional) A dictionary encompassing your chosen termination criteria.
1. MG (Maximum Generations / Epochs): This is default in all algorithms
term_dict = { # When creating this object, it will override the default epoch you define in your model
"max_epoch": 1000 # 1000 epochs
}
2. FE (Number of Function Evaluation)
term_dict = {
"max_fe": 100000 # 100000 number of function evaluation
}
3. TB (Time Bound): If you want your algorithm to run for a fixed amount of time (e.g., K seconds), especially when comparing different algorithms.
term_dict = {
"max_time": 60 # 60 seconds to run this algorithm only
}
4. ES (Early Stopping): Similar to the idea in training neural networks (stop the program if the global best solution has not improved by epsilon after K epochs).
term_dict = {
"max_early_stop": 30 # after 30 epochs, if the global best doesn't improve then we stop the program
}
Setting multiple stopping criteria together. The first one that occurs will be used.
# Use max epochs and max function evaluations together
term_dict = {
"max_epoch": 1000,
"max_fe": 60000
}
# Use max function evaluations and time bound together
term_dict = {
"max_fe": 60000,
"max_time": 40
}
# Use max function evaluations and early stopping together
term_dict = {
"max_fe": 55000,
"max_early_stop": 15
}
# Use max epochs, max FE and early stopping together
term_dict = {
"max_epoch": 1200,
"max_fe": 55000,
"max_early_stop": 25
}
# Use all available stopping conditions together
term_dict = {
"max_epoch": 1100,
"max_fe": 80000,
"max_time": 10.5,
"max_early_stop": 25
}
After import and create a termination object, and an optimizer object, you can pass termination object to solve() function
model3 = SMA.OriginalSMA(epoch=100, pop_size=50, pr=0.03)
model3.solve(problem_dict1, termination=term_dict)
Reproducibility (Set Seed)
In meta-heuristic algorithms, randomness plays a significant role in population initialization and the stochastic operators used during evolution. Controlling this randomness is essential for robust experimentation and code verification.
Note
Default Stochastic Behavior
By default, the seed parameter in the solve() method is set to None. When seed=None, the algorithm will use a different random seed each time it runs, leading to potentially different outcomes across multiple executions.
Important
Guaranteeing Reproducibility
If you set a specific integer value for seed (e.g., seed=42), the algorithm will produce the exact same results every time it is run with that particular seed. This deterministic behavior is crucial for debugging, verifying algorithmic improvements, and ensuring your research is fully reproducible by others.
Attention
Best Practice for Statistical Analysis (N-Trials)
For scenarios where you need to run the algorithm multiple times (e.g., 30 independent runs for statistical validation) while still maintaining reproducibility for each individual trial, it is best practice to set a different specific seed for each trial (e.g., seed=100 + i). This allows you to recreate any specific trial’s outcome while still exploring the stochastic nature of the algorithm across the entire experiment.
from mealpy import SMA
# Assuming 'problem' is already defined, e.g.:
# problem = {
# "obj_func": lambda solution: sum(x**2 for x in solution),
# "bounds": FloatVar(lb=(-10., )*30, ub=(10., )*30),
# "minmax": "min",
# }
# Example 1: Running with default seed (None) - results may vary each time
model_default = SMA.OriginalSMA(epoch=100, pop_size=50, pr=0.03)
g_best_default = model_default.solve(problem=problem)
print(f"Run 1 (default seed) - Best fitness: {g_best_default.target.fitness}")
g_best_default_2 = model_default.solve(problem=problem) # Rerunning the same model instance might reset some internal states, but generally results will differ due to new random seed
print(f"Run 2 (default seed) - Best fitness: {g_best_default_2.target.fitness}")
# Example 2: Running with a specific seed - results will be identical for seed=10
model_seeded_1 = SMA.OriginalSMA(epoch=100, pop_size=50, pr=0.03)
g_best_seeded_1 = model_seeded_1.solve(problem=problem, seed=10)
print(f"Run with seed=10 - Best fitness: {g_best_seeded_1.target.fitness}")
model_seeded_2 = SMA.OriginalSMA(epoch=100, pop_size=50, pr=0.03) # Create a new model instance for clarity
g_best_seeded_2 = model_seeded_2.solve(problem=problem, seed=10)
print(f"Run with seed=10 (again) - Best fitness: {g_best_seeded_2.target.fitness}") # This will yield the same fitness as above
# Example 3: Running multiple trials with different seeds for reproducibility of each trial
print("\nRunning 3 trials with different seeds:")
for i in range(3):
current_seed = 100 + i # Use a unique seed for each trial
model_trial = SMA.OriginalSMA(epoch=100, pop_size=50, pr=0.03)
g_best_trial = model_trial.solve(problem=problem, seed=current_seed)
print(f" Trial {i+1} (seed={current_seed}) - Best fitness: {g_best_trial.target.fitness}")
Multi-objective Optimization
Important
The Weighting Method
MEALPY currently utilizes the “weighting method” (scalarization) to solve multi-objective optimization problems. This straightforward approach aggregates multiple objectives into a single scalar fitness value by multiplying each objective by a predefined weight and summing them up.
To implement this, you simply need to design your objective function to return a list of objective values and specify the corresponding weights.
Configuration Parameters
obj_func(callable): Your objective function, which must return a list of numerical values.bounds(list/FloatVar): The lower and upper boundaries of the problem space.minmax(str): Indicates whether the optimization goal is to minimize ("min") or maximize ("max") the objectives.obj_weights(list): (Optional) A list of fractional weights corresponding to each objective.
Note
Default Weights Behavior
If you do not specify obj_weights, the algorithm defaults to assigning an equal weight of 1 to all objectives (e.g., [1, 1, ..., 1]).
Approach 1: Using a Problem Dictionary
import numpy as np
from mealpy import PSO, FloatVar, Problem
## This is how you design multi-objective function
#### Link: https://en.wikipedia.org/wiki/Test_functions_for_optimization
def objective_multi(solution):
def booth(x, y):
return (x + 2*y - 7)**2 + (2*x + y - 5)**2
def bukin(x, y):
return 100 * np.sqrt(np.abs(y - 0.01 * x**2)) + 0.01 * np.abs(x + 10)
def matyas(x, y):
return 0.26 * (x**2 + y**2) - 0.48 * x * y
return [booth(solution[0], solution[1]), bukin(solution[0], solution[1]), matyas(solution[0], solution[1])]
## Design a problem dictionary for multiple objective functions above
problem_multi = {
"obj_func": objective_multi,
"bounds": FloatVar(lb=[-10, -10], ub=[10, 10]),
"minmax": "min",
"obj_weights": [0.4, 0.1, 0.5] # Define it or default value will be [1, 1, 1]
}
## Define the model and solve the problem
model = PSO.OriginalPSO(epoch=1000, pop_size=50)
model.solve(problem=problem_multi)
Approach 2: Creating a Custom Problem Class
Hint
Best Practice for Complex Problems
Inheriting from the Problem class is highly recommended for complex multi-objective scenarios. It keeps your codebase modular, readable, and perfectly encapsulated.
import numpy as np
from mealpy import PSO, FloatVar, Problem
## Define a custom child class of Problem class.
class MOP(Problem):
def __init__(self, bounds=None, minmax="min", **kwargs):
super().__init__(bounds, minmax, **kwargs)
def booth(self, x, y):
return (x + 2*y - 7)**2 + (2*x + y - 5)**2
def bukin(self, x, y):
return 100 * np.sqrt(np.abs(y - 0.01 * x**2)) + 0.01 * np.abs(x + 10)
def matyas(self, x, y):
return 0.26 * (x**2 + y**2) - 0.48 * x * y
def obj_func(self, solution):
return [self.booth(solution[0], solution[1]), self.bukin(solution[0], solution[1]), self.matyas(solution[0], solution[1])]
## Create an instance of MOP class
problem_multi = MOP(bounds=FloatVar(lb=[-10, ] * 2, ub=[10, ] * 2), minmax="min", obj_weights=[0.4, 0.1, 0.5])
## Define the model and solve the problem
model = PSO.OriginalPSO(epoch=1000, pop_size=50)
model.solve(problem=problem_multi)
Constraint Optimization
For this problem, we recommend that the user defines a punishment function. The more the objective is violated, the greater the punishment function will be. As a result, the fitness will increase, and the solution will have a lower chance of being selected in the updating process.
Approach 1: Using a Problem Dictionary
import numpy as np
from mealpy import PSO, FloatVar
## This is how you design Constrained Benchmark Function (G01)
#### Link: https://onlinelibrary.wiley.com/doi/pdf/10.1002/9781119136507.app2
def objective_function(solution):
def g1(x):
return 2 * x[0] + 2 * x[1] + x[9] + x[10] - 10
def g2(x):
return 2 * x[0] + 2 * x[2] + x[9] + x[10] - 10
def g3(x):
return 2 * x[1] + 2 * x[2] + x[10] + x[11] - 10
def g4(x):
return -8 * x[0] + x[9]
def g5(x):
return -8 * x[1] + x[10]
def g6(x):
return -8 * x[2] + x[11]
def g7(x):
return -2 * x[3] - x[4] + x[9]
def g8(x):
return -2 * x[5] - x[6] + x[10]
def g9(x):
return -2 * x[7] - x[8] + x[11]
def violate(value):
return 0 if value <= 0 else value
fx = 5 * np.sum(solution[:4]) - 5 * np.sum(solution[:4] ** 2) - np.sum(solution[4:13])
## Increase the punishment for g1 and g4 to boost the algorithm (You can choice any constraint instead of g1 and g4)
fx += violate(g1(solution)) ** 2 + violate(g2(solution)) + violate(g3(solution)) + \
2 * violate(g4(solution)) + violate(g5(solution)) + violate(g6(solution)) + \
violate(g7(solution)) + violate(g8(solution)) + violate(g9(solution))
return fx
## Design a problem dictionary for constrained objective function above
problem_constrained = {
"obj_func": objective_function,
"bounds": FloatVar(lb=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ub=[1, 1, 1, 1, 1, 1, 1, 1, 1, 100, 100, 100, 1]),
"minmax": "min",
}
## Define the model and solve the problem
model = PSO.OriginalPSO(epoch=1000, pop_size=50)
model.solve(problem=problem_multi)
Approach 2: Creating a Custom Problem Class
import numpy as np
from mealpy import PSO, FloatVar
from mealpy.utils.problem import Problem
## Define a custom child class of Problem class.
class COP(Problem):
def __init__(self, lb, ub, minmax, **kwargs):
super().__init__(lb, ub, minmax, **kwargs)
def g1(self, x):
return 2 * x[0] + 2 * x[1] + x[9] + x[10] - 10
def g2(self, x):
return 2 * x[0] + 2 * x[2] + x[9] + x[10] - 10
def g3(self, x):
return 2 * x[1] + 2 * x[2] + x[10] + x[11] - 10
def g4(self, x):
return -8 * x[0] + x[9]
def g5(self, x):
return -8 * x[1] + x[10]
def g6(self, x):
return -8 * x[2] + x[11]
def g7(self, x):
return -2 * x[3] - x[4] + x[9]
def g8(self, x):
return -2 * x[5] - x[6] + x[10]
def g9(self, x):
return -2 * x[7] - x[8] + x[11]
def violate(self, value):
return 0 if value <= 0 else value
def obj_func(self, solution):
## This is how you design Constrained Benchmark Function (G01)
#### Link: https://onlinelibrary.wiley.com/doi/pdf/10.1002/9781119136507.app2
fx = 5 * np.sum(solution[:4]) - 5 * np.sum(solution[:4] ** 2) - np.sum(solution[4:13])
## Increase the punishment for g1 and g4 to boost the algorithm (You can choice any constraint instead of g1 and g4)
fx += self.violate(self.g1(solution)) ** 2 + self.violate(self.g2(solution)) + self.violate(self.g3(solution)) + \
2 * self.violate(self.g4(solution)) + self.violate(self.g5(solution)) + self.violate(self.g6(solution)) + \
self.violate(self.g7(solution)) + self.violate(self.g8(solution)) + self.violate(self.g9(solution))
return fx
## Create an instance of MOP class
bounds = FloatVar(lb=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ub=[1, 1, 1, 1, 1, 1, 1, 1, 1, 100, 100, 100, 1]),
problem_cop = COP(bounds=bounds, minmax="min")
## Define the model and solve the problem
model = PSO.OriginalPSO(epoch=1000, pop_size=50)
model.solve(problem=problem_cop)
Discrete Optimization
For this type of problem, we recommend creating a custom child class of the Problem class and overriding the necessary functions. At a minimum, the following three functions are typically considered for overriding:
obj_func(self, solution): The fitness function to evaluate the solution.generate_position(self, lb, ub): A function that generates a valid initial solution.amend_position(self, solution, lb, ub): A function that brings the solution back within valid boundaries.
Important
Decision Variables: No Need to Override Core Functions!
Before you dive into overriding generate_position and amend_position manually, let’s look at the tools MEALPY provides. MEALPY has natively implemented almost all types of decision variables you might need.
By simply selecting the appropriate Variable class and passing it to the bounds parameter, MEALPY handles the initialization (generate_position) and boundary constraints (amend_position) automatically under the hood. Most of the time, you only need to override the ``obj_func``!
To assist you in choosing the right tools, refer to the table below. It outlines different types of decision variables available in MEALPY, along with their syntax and common problem applications.
Class |
Syntax |
Problem Types |
|---|---|---|
FloatVar |
|
Continuous Problem |
IntegerVar |
|
LP, IP, NLP, QP, MIP |
StringVar |
|
ML, AI-optimize |
BinaryVar |
|
Networks |
BoolVar |
|
ML, AI-optimize |
PermutationVar |
|
Combinatorial Optimization |
CategoricalVar |
|
MIP, MILP |
SequenceVar |
|
Hyper-parameter tuning |
TransferBoolVar |
|
ML, AI-optimize, Feature Selection |
TransferBinaryVar |
|
Networks, Feature Selection |
Example: Travelling Salesman Problem (TSP)
Let’s say we want to solve the Travelling Salesman Problem (TSP). Because this is a permutation problem, we can simply utilize MEALPY’s built-in PermutationVar. This specific variable type automatically handles the generation and boundary-repair logic for us, meaning we only have to write the objective function.
import numpy as np
from mealpy import Problem, PermutationVar, PSO
class DOP(Problem):
def __init__(self, bounds, minmax, CITY_POSITIONS=None, **kwargs):
super().__init__(bounds, minmax, **kwargs)
self.CITY_POSITIONS = CITY_POSITIONS
def obj_func(self, solution):
## Objective for this problem is the sum of distance between all cities that the salesman has passed.
## This can be changed depending on your specific requirements.
# Decode the solution based on the PermutationVar definition
x = self.decode_solution(solution)["per"]
# Get the coordinates of the cities in the order of the permutation
city_coord = self.CITY_POSITIONS[x]
line_x = city_coord[:, 0]
line_y = city_coord[:, 1]
# Calculate the Euclidean distance between consecutive cities
total_distance = np.sum(np.sqrt(np.square(np.diff(line_x)) + np.square(np.diff(line_y))))
return total_distance
## Define coordinates for 13 random cities for this example
random_cities = np.random.rand(13, 2) * 100
## Create an instance of the DOP class
## For the Travelling Salesman Problem, the solution should be a permutation of city indices
problem_cop = DOP(
bounds=PermutationVar(valid_set=list(range(13)), name="per"),
minmax="min",
CITY_POSITIONS=random_cities,
log_to="file",
log_file="dop-results.txt"
)
## Define the model and solve the problem
model = PSO.OriginalPSO(epoch=1000, pop_size=50)
model.solve(problem=problem_cop)
Log Training Process
MEALPY offers flexible logging options to help you monitor the optimization process. You can choose to print the progress to the console, save it to a log file, or disable logging entirely to keep your output clean.
Note
Default Behavior
If the log_to keyword is not specified in your problem definition, MEALPY will automatically default to logging the training progress directly to the console.
Important
Logging to a File
When setting log_to="file", you should specify the target destination using the log_file keyword (e.g., "result.log"). If you omit log_file, MEALPY will automatically save the output to a default file named mealpy.log.
Here is how you can configure each of the three available logging modes:
from mealpy import FloatVar
# Option 1: Log to Console (Default)
problem_console = {
"obj_func": F5,
"bounds": FloatVar(lb=[-3, -5, 1, -10], ub=[5, 10, 100, 30]),
"minmax": "min",
"log_to": "console", # Optional: This is the default behavior
}
# Option 2: Log to File
problem_file = {
"obj_func": F5,
"bounds": FloatVar(lb=[-3, -5, 1, -10], ub=[5, 10, 100, 30]),
"minmax": "min",
"log_to": "file",
"log_file": "result.log", # Optional: Defaults to "mealpy.log" if not provided
}
# Option 3: Disable Logging (Silent Mode)
problem_silent = {
"obj_func": F5,
"bounds": FloatVar(lb=[-3, -5, 1, -10], ub=[5, 10, 100, 30]),
"minmax": "min",
"log_to": None, # Set to None for no console output and no file generation
}
Custom Problem
While problem dictionaries are quick to set up, we highly recommend defining a custom child class of the Problem class for complex optimization tasks.
Important
The Power of Encapsulation
Object-oriented design is crucial when your objective function depends on external variables or heavy data structures. For instance, when optimizing a Neural Network, you must pass datasets, hyperparameters, or training states to the fitness function. A custom class allows you to neatly encapsulate all this additional data via the __init__ method, avoiding messy global variables.
Here is a practical example of how to implement a custom problem class for Neural Network weight optimization:
from mealpy import PSO, FloatVar, Problem
class NeuralNetworkProblem(Problem):
def __init__(self, bounds=None, minmax="min", dataset=None, additional=None, **kwargs):
# 1. Initialize the parent Problem class
super().__init__(bounds, minmax, **kwargs)
# 2. Store your custom external data
self.dataset = dataset
self.additional = additional
def obj_func(self, solution):
# 3. Decode the solution and use your external data
# weights = self.decode_solution(solution)["weights"]
# (Conceptual Code) Initialize network and apply the optimizer's weights
# network = NET(dataset=self.dataset, config=self.additional)
# network.set_weights(weights)
# loss = network.calculate_loss()
loss = 0.5 # Dummy loss for demonstration
return loss
## Mock data for demonstration
my_dataset = {"X_train": [...], "y_train": [...]}
my_config = {"learning_rate": 0.01, "batch_size": 32}
## Create an instance of your custom class
problem_nn = NeuralNetworkProblem(
bounds=FloatVar(lb=[-3, -5, 1, -10], ub=[5, 10, 100, 30], name="weights"),
minmax="min",
dataset=my_dataset,
additional=my_config,
name="NN_Optimization" # Extra kwargs are passed smoothly to the parent class
)
## Define the model and solve the problem
model = PSO.OriginalPSO(epoch=1000, pop_size=50)
model.solve(problem=problem_nn)
Model’s Parameters
1. Hint Validation for Hyper-parameters
If you are unsure about the acceptable range or data type for a specific optimizer parameter, MEALPY has a built-in safety net. You can deliberately set it to an invalid value, and the optimizer’s Hint Validation system will raise an informative error, guiding you on how to set it correctly.
from mealpy import PSO
# Passing strings instead of integers
model = PSO.OriginalPSO(epoch="hello", pop_size="world")
model.solve(problem)
# Console output:
# 2022/03/22 08:59:16 AM, ERROR, mealpy.utils.validator.Validator [line: 31]: 'epoch' is an integer and value should be in range: [1, 100000].
model = PSO.OriginalPSO(epoch=10, pop_size="world")
model.solve(problem)
# Console output:
# 2022/03/22 09:01:51 AM, ERROR, mealpy.utils.validator.Validator [line: 31]: 'pop_size' is an integer and value should be in range: [10, 10000].
2. Setting Parameters via Dictionary
Instead of passing arguments sequentially, you can define your hyperparameters in a standard Python dictionary and unpack them using the **kwargs syntax. This keeps your code clean and highly modular.
from mealpy import DE, FloatVar
problem = {
"obj_func": lambda x: sum(x**2),
"bounds": FloatVar(lb=[-10,]*10, ub=[30,]*10),
"minmax": "min",
}
paras_de = {
"epoch": 20,
"pop_size": 50,
"wf": 0.7,
"cr": 0.9,
"strategy": 0,
}
model = DE.OriginalDE(**paras_de)
model.solve(problem)
Hint
Hyperparameter Tuning
The dictionary unpacking approach is exceptionally powerful when you want to tune your model’s parameters using grid search libraries, such as ParameterGrid from scikit-learn.
from sklearn.model_selection import ParameterGrid
from mealpy import DE, FloatVar
problem = {
"obj_func": lambda x: sum(x**2),
"bounds": FloatVar(lb=[-10,]*10, ub=[30,]*10),
"minmax": "min",
}
paras_de_grid = {
"epoch": [100, 200, 300, 500, 1000],
"pop_size": [50, 100],
"wf": [0.5, 0.6, 0.7, 0.8, 0.9],
"cr": [0.6, 0.7, 0.8, 0.9],
"strategy": [0, 1, 2, 3, 4],
}
# Iterate through all combinations in the grid
for paras_de in list(ParameterGrid(paras_de_grid)):
model = DE.OriginalDE(**paras_de)
model.solve(problem)
3. Retrieving Model Parameters
If you need to log or inspect the configuration of an instantiated model, you can easily retrieve its hyper-parameters using the get_parameters() method.
# Returns the model's parameters as a Python dictionary
params_dict = model.get_parameters()
# If you want to log or print it, convert it to a string using the built-in str() function
params_str = str(model.get_parameters())
Setting Model and Problem Names
While it is not strictly required to assign custom names to your optimizer and problem, doing so is considered a best practice. It significantly helps in tracking, logging, and organizing results, especially when conducting multi-task experiments or benchmarking various algorithms.
Hint
Better Data Exporting and Visualization
When exporting your results to DataFrames, CSV/JSON files, or generating visual plots using MEALPY’s visualization tools, the library will automatically utilize these defined names. This prevents confusion when analyzing large batches of experimental data!
1. Naming the Problem
You can assign a name to your problem directly within the problem dictionary by adding the name keyword. (Note: If you are defining a custom child class of the Problem class, you can pass this via the ``name`` parameter in the initialization).
from mealpy import FloatVar
# Define a dummy objective function
def F5(solution):
return sum(x**2 for x in solution)
problem = {
"obj_func": F5,
"bounds": FloatVar(lb=[-3, -5, 1, -10], ub=[5, 10, 100, 30]),
"minmax": "min",
"name": "Benchmark Function 5th" # Set the problem's custom name here
}
2. Naming the Optimizer Model
Similarly, you can assign a specific name to your optimizer during initialization by passing the name argument. This is exceptionally useful when you are testing the same algorithm with different hyperparameters.
from mealpy import PSO
model = PSO.OriginalPSO(epoch=10, pop_size=50, name="Normal PSO")
model.solve(problem=problem)
3. Retrieving the Names
Once instantiated, you can dynamically access these names programmatically through the model object’s attributes.
print(model.name) # Output: Normal PSO
print(model.problem.name) # Output: Benchmark Function 5th
Agent’s History (Trajectory)
By default, MEALPY tracks essential metrics (like best/worst fitness, runtime, and diversity) across generations without saving the complete state of every agent. This ensures high performance and low memory consumption.
However, if you want to visualize the trajectory chart of search agents, you can enable full population tracking by setting the save_population keyword to True in the problem definition.
Warning
Memory Out-of-Bounds Risk
Enabling save_population: True will save the full position and fitness of every single agent in every single generation. For large population sizes, high dimensions, or a massive number of epochs, this will rapidly consume your RAM and may crash your program. Only enable this for small-scale problems or when strictly necessary for trajectory visualization.
Enabling Population Tracking
from mealpy import FloatVar
problem_dict1 = {
"obj_func": lambda x: sum(x**2),
"bounds": FloatVar(lb=[-3, -5, 1, -10], ub=[5, 10, 100, 30]),
"minmax": "min",
"log_to": "console",
"save_population": True, # Set to True to enable trajectory tracking. Default is False.
}
Accessing the History Object
After the optimization process finishes, you can access the detailed history of the agents and the population through the model.history object. It contains the following built-in lists:
- Solutions (Agents):
list_global_best: The global best SOLUTION found so far across all previous generations.list_current_best: The best SOLUTION in each specific generation.list_global_worst: The global worst SOLUTION found so far across all previous generations.list_current_worst: The worst SOLUTION in each specific generation.
- Fitness Values:
list_global_best_fit: The global best FITNESS found so far across all previous generations.list_current_best_fit: The best FITNESS in each specific generation.
- Metrics & Analytics:
list_epoch_time: The runtime (in seconds) for each generation.list_diversity: The spatial diversity of the swarm across generations.list_exploitation: The exploitation percentage metric for each generation.list_exploration: The exploration percentage metric for each generation.
- Full Trajectory:
list_population: The complete POPULATION array in each generation. (Only populated if ``save_population=True``)
Example: Retrieving History Data
import numpy as np
from mealpy import PSO, FloatVar
def objective_function(solution):
return np.sum(solution**2)
problem_dict = {
"obj_func": objective_function,
"bounds": FloatVar(lb=[-3, -5, 1, -10], ub=[5, 10, 100, 30]),
"minmax": "min",
"verbose": True,
"save_population": False # You cannot draw trajectory charts with this set to False
}
model = PSO.OriginalPSO(epoch=1000, pop_size=50)
model.solve(problem=problem_dict)
# Accessing essential metrics (Available even if save_population=False)
print("Global best solutions:", model.history.list_global_best)
print("Epoch runtime history:", model.history.list_epoch_time)
print("Global best fitness history:", model.history.list_global_best_fit)
print("Exploration history:", model.history.list_exploration)
# Accessing full population history
# NOTE: This will be empty or missing because save_population was set to False
print("Full population history:", model.history.list_population)
Saving and Loading Model
From the above tutorials, you learned that you can track the population’s history after each epoch by setting "save_population": True in the problem dictionary.
Warning
In-Memory vs. Disk Storage
Setting save_population: True strictly means storing the population of each epoch inside the model’s history object in your system’s RAM. As warned previously, if your problem size or epoch count is too large, this will cause memory overflow issues. It does NOT save the model to a file.
To physically save your trained optimizer to a file on your local machine (and load it back later), you must use the io module from mealpy.utils.
Important
Pickle Serialization
The io.save_model() function utilizes Python’s built-in pickle serialization. This allows you to completely freeze the optimizer’s state, export it as a .pkl file, and resume or analyze your model in an entirely different Python session.
Code Example: Exporting and Importing
import numpy as np
from mealpy import GA, FloatVar
from mealpy.utils import io
def objective_function(solution):
return np.sum(solution**2)
problem = {
"obj_func": objective_function,
"bounds": FloatVar(lb=[-100, ] * 50, ub=[100, ] * 50),
"minmax": "min",
}
## 1. Run the algorithm
model = GA.BaseGA(epoch=100, pop_size=50)
g_best = model.solve(problem)
print(f"Best solution: {g_best.solution}, Best fitness: {g_best.target.fitness}")
## 2. Save the fully trained model to a file
# Note: Ensure the "results" directory exists or provide a valid path
io.save_model(model, "results/model.pkl")
## 3. Load the model from the file in a new session
optimizer = io.load_model("results/model.pkl")
print(f"Loaded solution: {optimizer.g_best.solution}, Loaded fitness: {optimizer.g_best.target.fitness}")
Starting Solutions (Custom Initialization)
Warning
Use With Caution!
We generally do not recommend manually providing starting solutions. Meta-heuristic algorithms rely heavily on uniform random initialization to effectively explore the entire search space. Forcing specific initial positions can bias the swarm and severely increase the risk of premature convergence at local optima.
However, there are advanced scenarios (such as hybrid algorithms, warm-starting, or injecting known good solutions from prior runs) where custom initialization is necessary. You can inject these solutions using the starting_solutions parameter in the solve() method.
Important
Data Shape Requirement
Your custom starting_solutions must be strictly structured as a 2D array (matrix) or a list of vectors. The number of rows must exactly match your pop_size, and the number of columns must exactly match the problem’s dimensionality (number of variables).
Code Example
import numpy as np
from mealpy import TLO, FloatVar
def frequency_modulated(pos):
# range: [-6.4, 6.35], f(X*) = 0, phi = 2pi / 100
phi = 2 * np.pi / 100
result = 0
for t in range(0, 101):
y_t = pos[0] * np.sin(pos[3] * t * phi + pos[1] * np.sin(pos[4] * t * phi + pos[2] * np.sin(pos[5] * t * phi)))
y_t0 = 1.0 * np.sin(5.0 * t * phi - 1.5 * np.sin(4.8 * t * phi + 2.0 * np.sin(4.9 * t * phi)))
result += (y_t - y_t0)**2
return result
fm_problem = {
"obj_func": frequency_modulated,
"bounds": FloatVar(lb=[-6.4, ] * 6, ub=[6.35, ] * 6),
"minmax": "min",
"log_to": "console",
}
## Custom function to generate starting positions (2D Matrix: pop_size x n_dims)
def create_starting_solutions(n_dims, pop_size, base_value=1):
# Adding random uniform noise to avoid identical overlapping agents
return np.ones((pop_size, n_dims)) * base_value + np.random.uniform(-1, 1, size=(pop_size, n_dims))
## 1. Define the model
model = TLO.OriginalTLO(epoch=100, pop_size=50)
## 2. Generate and inject starting positions via the "starting_solutions" keyword
list_pos = create_starting_solutions(n_dims=6, pop_size=50, base_value=2)
best_agent = model.solve(fm_problem, starting_solutions=list_pos)
print(f"Run 1 - Best solution: {model.g_best.solution}, Best fitness: {best_agent.target.fitness}")
## 3. Training again with completely different starting positions
list_pos2 = create_starting_solutions(n_dims=6, pop_size=50, base_value=-1)
best_agent2 = model.solve(fm_problem, starting_solutions=list_pos2)
print(f"Run 2 - Best solution: {model.g_best.solution}, Best fitness: {best_agent2.target.fitness}")