Basic cat swarm optimizer written in Python. Modified from the adaptive timestep PSO optimizer by jonathan46000 to keep a consistent format between optimizers in AntennaCAT.
Now featuring AntennaCAT hooks for GUI integration and user input handling.
- Cat Swarm Optimization
- Requirements
- Implementation
- Examples
- References
- Related Publications and Repositories
- Licensing
Cat Swarm Optimization (CSO) is a nature-inspired optimization algorithm based on the behaviors of cats. It was introduced in "Cat Swarm Optimization" [1] in 2006. This algorithm resembles the traditional Particle Swarm Optimization (PSO) algorithm as it has location and velocity aspects, but the agents/particles in this algorithm have two options for movement in the update step. These two options are modeled off of two primary behaviors of cats: seeking and tracing. These modes represent the exploration and exploitation phases in optimization, respectively.
CSO divides the population of candidate solutions (cats) into two groups: one group for the seeking mode and another for the tracing mode. Each cat can switch between these modes according to a specified probability.
- Seeking Mode:
The seeking mode is responsible for exploring the search space to discover new and potentially better solutions. In this mode, cats simulate a behavior where they observe their surroundings and make decisions to move to new positions based on several possible moves. This helps the algorithm avoid getting stuck in local optima.
- Tracing Mode:
The tracing mode is responsible for exploiting the search space by following the best solutions found so far. In this mode, cats simulate a behavior where they move towards a promising position, akin to a cat chasing prey. This helps refine solutions and converge towards the global optimum.
This project requires numpy, pandas, and matplotlib for the full demos. To run the optimizer without visualization, only numpy and pandas are requirements
Use 'pip install -r requirements.txt' to install the following dependencies:
contourpy==1.2.1
cycler==0.12.1
fonttools==4.51.0
importlib_resources==6.4.0
kiwisolver==1.4.5
matplotlib==3.8.4
numpy==1.26.4
packaging==24.0
pandas==2.2.3
pillow==10.3.0
pyparsing==3.1.2
python-dateutil==2.9.0.post0
pytz==2025.1
six==1.16.0
tzdata==2025.1
zipp==3.18.1
Optionally, requirements can be installed manually with:
pip install matplotlib, numpy, pandas
This is an example for if you've had a difficult time with the requirements.txt file. Sometimes libraries are packaged together.
# Constant variables
NO_OF_PARTICLES = 11 # Number of particles in swarm
TOL = 10 ** -18 # Convergence Tolerance
MAXIT = 10000 # Maximum allowed iterations
BOUNDARY = 1 # int boundary 1 = random, 2 = reflecting
# 3 = absorbing, 4 = invisible
# Objective function dependent variables
func_F = func_configs.OBJECTIVE_FUNC # objective function
constr_F = func_configs.CONSTR_FUNC # constraint function
LB = func_configs.LB # Lower boundaries, [[0.21, 0, 0.1]]
UB = func_configs.UB # Upper boundaries, [[1, 1, 0.5]]
OUT_VARS = func_configs.OUT_VARS # Number of output variables (y-values)
TARGETS = func_configs.TARGETS # Target values for output
# optimizer constants
WEIGHTS = [[0.5, 0.7, 0.78]] # Update vector weights
VLIM = 1 # Initial velocity limit
best_eval = 1
parent = None # for the optimizer test ONLY
suppress_output = True # Suppress the console output of particle swarm
allow_update = True # Allow objective call to update state
# Constant variables
opt_params = {'NO_OF_PARTICLES': [NO_OF_PARTICLES], # Number of particles in swarm
'BOUNDARY': [BOUNDARY], # int boundary 1 = random, 2 = reflecting
# 3 = absorbing, 4 = invisible
'WEIGHTS': [WEIGHTS], # Update vector weights
'VLIM': [VLIM] } # Initial velocity limit
opt_df = pd.DataFrame(opt_params)
myOptimizer = swarm(LB, UB, TARGETS, TOL, MAXIT,
func_F, constr_F,
opt_df,
parent=parent)
# arguments should take the form:
# swarm([[float, float, ...]], [[float, float, ...]], [[float, ...]], float, int,
# func, func,
# dataFrame,
# class obj)
#
# opt_df contains class-specific tuning parameters
# NO_OF_PARTICLES: int
# weights: [[float, float, float]]
# boundary: int. 1 = random, 2 = reflecting, 3 = absorbing, 4 = invisible
# vlim: float
This optimizer uses a state machine structure to control the movement of the particles, call to the objective function, and the evaluation of current positions. The state machine implementation preserves the initial algorithm while making it possible to integrate other programs, classes, or functions as the objective function.
A controller with a while loop
to check the completion status of the optimizer drives the process. Completion status is determined by at least 1) a set MAX number of iterations, and 2) the convergence to a given target using the L2 norm. Iterations are counted by calls to the objective function.
Within this while loop
are three function calls to control the optimizer class:
- complete: the
complete function
checks the status of the optimizer and if it has met the convergence or stop conditions. - step: the
step function
takes a boolean variable (suppress_output) as an input to control detailed printout on current particle (or agent) status. This function moves the optimizer one step forward. - call_objective: the
call_objective function
takes a boolean variable (allow_update) to control if the objective function is able to be called. In most implementations, this value will always be true. However, there may be cases where the controller or a program running the state machine needs to assert control over this function without stopping the loop.
Additionally, get_convergence_data can be used to preview the current status of the optimizer, including the current best evaluation and the iterations.
The code below is an example of this process:
while not myOptimizer.complete():
# step through optimizer processing
# this will update particle or agent locations
myOptimizer.step(suppress_output)
# call the objective function, control
# when it is allowed to update and return
# control to optimizer
myOptimizer.call_objective(allow_update)
# check the current progress of the optimizer
# iter: the number of objective function calls
# eval: current 'best' evaluation of the optimizer
iter, eval = myOptimizer.get_convergence_data()
if (eval < best_eval) and (eval != 0):
best_eval = eval
# optional. if the optimizer is not printing out detailed
# reports, preview by checking the iteration and best evaluation
if suppress_output:
if iter%100 ==0: #print out every 100th iteration update
print("Iteration")
print(iter)
print("Best Eval")
print(best_eval)
Users must create their own constraint function for their problems, if there are constraints beyond the problem bounds. This is then passed into the constructor. If the default constraint function is used, it always returns true (which means there are no constraints).
This optimizer has 4 different types of bounds, Random (Particles that leave the area respawn), Reflection (Particles that hit the bounds reflect), Absorb (Particles that hit the bounds lose velocity in that direction), Invisible (Out of bound particles are no longer evaluated).
Some updates have not incorporated appropriate handling for all boundary conditions. This bug is known and is being worked on. The most consistent boundary type at the moment is Random. If constraints are violated, but bounds are not, currently random bound rules are used to deal with this problem.
The no preference method of multi-objective optimization, but a Pareto Front is not calculated. Instead, the best choice (smallest norm of output vectors) is listed as the output.
The optimizer minimizes the absolute value of the difference of the target outputs and the evaluated outputs. Future versions may include options for function minimization when target values are absent.
Custom objective functions can be used by creating a directory with the following files:
- configs_F.py
- constr_F.py
- func_F.py
configs_F.py
contains lower bounds, upper bounds, the number of input variables, the number of output variables, the target values, and a global minimum if known. This file is used primarily for unit testing and evaluation of accuracy. If these values are not known, or are dynamic, then they can be included experimentally in the controller that runs the optimizer's state machine.
constr_F.py
contains a function called constr_F
that takes in an array, X
, of particle positions to determine if the particle or agent is in a valid or invalid location.
func_F.py
contains the objective function, func_F
, which takes two inputs. The first input, X
, is the array of particle or agent positions. The second input, NO_OF_OUTS
, is the integer number of output variables, which is used to set the array size. In included objective functions, the default value is hardcoded to work with the specific objective function.
Below are examples of the format for these files.
configs_F.py
:
OBJECTIVE_FUNC = func_F
CONSTR_FUNC = constr_F
OBJECTIVE_FUNC_NAME = "one_dim_x_test.func_F" #format: FUNCTION NAME.FUNCTION
CONSTR_FUNC_NAME = "one_dim_x_test.constr_F" #format: FUNCTION NAME.FUNCTION
# problem dependent variables
LB = [[0]] # Lower boundaries
UB = [[1]] # Upper boundaries
IN_VARS = 1 # Number of input variables (x-values)
OUT_VARS = 1 # Number of output variables (y-values)
TARGETS = [0] # Target values for output
GLOBAL_MIN = [] # Global minima sample, if they exist.
constr_F.py
, with no constraints:
def constr_F(x):
F = True
return F
constr_F.py
, with constraints:
def constr_F(X):
F = True
# objective function/problem constraints
if (X[2] > X[0]/2) or (X[2] < 0.1):
F = False
return F
func_F.py
:
import numpy as np
import time
def func_F(X, NO_OF_OUTS=1):
F = np.zeros((NO_OF_OUTS))
noErrors = True
try:
x = X[0]
F = np.sin(5 * x**3) + np.cos(5 * x) * (1 - np.tanh(x ** 2))
except Exception as e:
print(e)
noErrors = False
return [F], noErrors
There are three functions included in the repository:
- Himmelblau's function, which takes 2 inputs and has 1 output
- A multi-objective function with 3 inputs and 2 outputs (see lundquist_3_var)
- A single-objective function with 1 input and 1 output (see one_dim_x_test)
Each function has four files in a directory:
- configs_F.py - contains imports for the objective function and constraints, CONSTANT assignments for functions and labeling, boundary ranges, the number of input variables, the number of output values, and the target values for the output
- constr_F.py - contains a function with the problem constraints, both for the function and for error handling in the case of under/overflow.
- func_F.py - contains a function with the objective function.
- graph.py - contains a script to graph the function for visualization.
Other multi-objective functions can be applied to this project by following the same format (and several have been collected into a compatible library, and will be released in a separate repo)
Plotted Himmelblau’s Function with 3D Plot on the Left, and a 2D Contour on the Right
Global Minima | Boundary | Constraints |
---|---|---|
f(3, 2) = 0 | ||
f(-2.805118, 3.121212) = 0 | ||
f(-3.779310, -3.283186) = 0 | ||
f(3.584428, -1.848126) = 0 |
Plotted Multi-Objective Function Feasible Decision Space and Objective Space with Pareto Front
Num. Input Variables | Boundary | Constraints |
---|---|---|
3 |
|
|
Plotted Single Input, Single-objective Function Feasible Decision Space and Objective Space with Pareto Front
Num. Input Variables | Boundary | Constraints |
---|---|---|
1 |
Local minima at
Global minima at
main_test.py
provides a sample use case of the optimizer.
main_test_details.py
provides an example using a parent class, and the self.suppress_output flag to control error messages that are passed back to the parent class to be printed with a timestamp. This implementation sets up the hooks for integration with AntennaCAT to provide the user feedback of warnings and errors.
main_test_graph.py
provides an example using a parent class, and the self.suppress_output flag to control error messages that are passed back to the parent class to be printed with a timestamp. Additionally, a realtime graph shows particle locations at every step. In this example, the cat swarm is not well-tuned to the problem and is not fast to converge,
but the error from the target is relatively small.
NOTE: if you close the graph as the code is running, the code will continue to run, but the graph will not re-open.
[1] S.-C. Chu, P. Tsai, and J.-S. Pan, “Cat Swarm Optimization,” Lecture Notes in Computer Science, pp. 854–858, 2006, doi: https://doi.org/10.1007/978-3-540-36668-3_94.
This software works as a stand-alone implementation, and as one of the optimizers integrated into AntennaCAT.
The code in this repository has been released under GPL-2.0