Skip to content

cilpy.solver Documentation

The solver module: Defines the optimization algorithm interface.

This module provides the abstract "contract" for all optimization algorithms (solvers) within the cilpy library.

The core component is the Solver abstract base class. Any algorithm that inherits from this class and implements its methods can be used by the ExperimentRunner to solve any cilpy problem.

Solver

Bases: ABC, Generic[SolutionType, FitnessType]

An abstract interface for a problem solver.

This class is the blueprint for all optimization algorithms in cilpy. To create a new solver, inherit from this class and implement its abstract methods. The interface is generic to support different solution types (e.g., List[float], np.ndarray) and fitness structures.

Attributes:

Name Type Description
problem Problem

The problem instance that the solver is optimizing.

name str

The name of the solver instance.

comparator ConstraintHandler

The constraint-handling comparator used to comparesolutions.

Example

A minimal implementation for a Random Search solver.

.. code-block:: python

import random
from cilpy.problem import Problem, Evaluation
from cilpy.solver import Solver

class RandomSearch(Solver[list[float], float]):
    def __init__(self, problem: Problem, name: str):
        super().__init__(problem, name)
        self.best_solution = None
        self.best_eval = Evaluation(fitness=float('inf'))

    def step(self) -> None:
        # Generate one random solution
        lower, upper = self.problem.bounds
        solution = [random.uniform(l, u) for l, u in zip(lower, upper)]
        evaluation = self.problem.evaluate(solution)

        # Update best if necessary
        if evaluation.fitness < self.best_eval.fitness:
            self.best_solution = solution
            self.best_eval = evaluation

    def get_result(self) -> list[tuple[list[float], Evaluation[float]]]:
        return [(self.best_solution, self.best_eval)]
Source code in cilpy/solver/__init__.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
class Solver(ABC, Generic[SolutionType, FitnessType]):
    """An abstract interface for a problem solver.

    This class is the blueprint for all optimization algorithms in `cilpy`. To
    create a new solver, inherit from this class and implement its abstract
    methods. The interface is generic to support different solution types
    (e.g., `List[float]`, `np.ndarray`) and fitness structures.

    Attributes:
        problem (cilpy.problem.Problem): The problem instance that the solver is
            optimizing.
        name (str): The name of the solver instance.
        comparator (cilpy.solver.chm.ConstraintHandler): The constraint-handling
            comparator used to comparesolutions.

    Example:
        A minimal implementation for a Random Search solver.

        .. code-block:: python

            import random
            from cilpy.problem import Problem, Evaluation
            from cilpy.solver import Solver

            class RandomSearch(Solver[list[float], float]):
                def __init__(self, problem: Problem, name: str):
                    super().__init__(problem, name)
                    self.best_solution = None
                    self.best_eval = Evaluation(fitness=float('inf'))

                def step(self) -> None:
                    # Generate one random solution
                    lower, upper = self.problem.bounds
                    solution = [random.uniform(l, u) for l, u in zip(lower, upper)]
                    evaluation = self.problem.evaluate(solution)

                    # Update best if necessary
                    if evaluation.fitness < self.best_eval.fitness:
                        self.best_solution = solution
                        self.best_eval = evaluation

                def get_result(self) -> list[tuple[list[float], Evaluation[float]]]:
                    return [(self.best_solution, self.best_eval)]
    """

    @abstractmethod
    def __init__(
        self,
        problem: Problem[SolutionType, FitnessType],
        name: str,
        constraint_handler: Optional[ConstraintHandler[FitnessType]] = None,
        **kwargs,
    ):
        """Initializes the solver.

        Subclasses must call `super().__init__(...)` and can use `**kwargs` to
        accept algorithm-specific hyperparameters.

        Args:
            problem: The optimization problem to solve, which must
                implement the `Problem` interface.
            name: The name of the solver instance.
            constraint_handler: An optional strategy object for handling
                constraints. If None, a default fitness-only comparator is used.
            **kwargs: A dictionary for algorithm-specific parameters. For
                example, a GA might accept `population_size=100` or
                `mutation_rate=0.1`.
        """
        self.problem = problem
        self.name = name
        self.comparator = constraint_handler or DefaultComparator()

    @abstractmethod
    def step(self) -> None:
        """Performs one iteration of the optimization algorithm.

        This method contains the core logic of the solver. The
        `ExperimentRunner` will call this method repeatedly in a loop. A single
        step could be one generation in a GA, one iteration in a PSO, or the
        evaluation of one new solution in a simpler algorithm.
        """
        pass

    @abstractmethod
    def get_result(self) -> List[Tuple[SolutionType, Evaluation[FitnessType]]]:
        """Returns the best solution(s) found so far.

        This method provides the current result of the optimization process. It
        is called by the `ExperimentRunner` after each step to log progress.

        Returns:
            A list of tuples, where each tuple contains `(solution,
            evaluation)`.
            - For single-objective solvers, this list should contain a single
              tuple with the best solution found.
            - For multi-objective solvers, this list should contain the set
              of non-dominated solutions (the Pareto front archive).

            Example return for a single-objective solver:
            `[([0.1, -0.5], Evaluation(fitness=0.26))]`
        """
        pass

    def get_population(self) -> List[SolutionType]:
        """
        Returns the entire current population or set of candidate solutions.

        This method is optional and should be implemented by population-based
        algorithms. It is required for certain performance metrics like
        diversity measurement.

        Raises:
            NotImplementedError: If the solver is not swarm based.

        Returns:
            All individuals in the solver's population.
        """
        raise NotImplementedError(
            f"""The solver '{self.name}' does not have a population. Implement
             get_population() to use metrics that require it."""
        )

    def get_population_evaluations(self) -> List[Evaluation[FitnessType]]:
        """
        Returns the evaluations of the entire current population or set of
        candidate solutions.

        This method is optional and should be implemented by population-based
        algorithms. It is required for certain performance metrics like
        percentage of feasible solutions.

        Raises:
            NotImplementedError: If the solver is not swarm based.

        Returns:
            Evaluations for all individuals in the solver's population.
        """
        raise NotImplementedError(
            f"""The solver '{self.name}' does not have a population. Implement
             get_population_evaluations() to use metrics that require it."""
        )

__init__(problem, name, constraint_handler=None, **kwargs) abstractmethod

Initializes the solver.

Subclasses must call super().__init__(...) and can use **kwargs to accept algorithm-specific hyperparameters.

Parameters:

Name Type Description Default
problem Problem[SolutionType, FitnessType]

The optimization problem to solve, which must implement the Problem interface.

required
name str

The name of the solver instance.

required
constraint_handler Optional[ConstraintHandler[FitnessType]]

An optional strategy object for handling constraints. If None, a default fitness-only comparator is used.

None
**kwargs

A dictionary for algorithm-specific parameters. For example, a GA might accept population_size=100 or mutation_rate=0.1.

{}
Source code in cilpy/solver/__init__.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@abstractmethod
def __init__(
    self,
    problem: Problem[SolutionType, FitnessType],
    name: str,
    constraint_handler: Optional[ConstraintHandler[FitnessType]] = None,
    **kwargs,
):
    """Initializes the solver.

    Subclasses must call `super().__init__(...)` and can use `**kwargs` to
    accept algorithm-specific hyperparameters.

    Args:
        problem: The optimization problem to solve, which must
            implement the `Problem` interface.
        name: The name of the solver instance.
        constraint_handler: An optional strategy object for handling
            constraints. If None, a default fitness-only comparator is used.
        **kwargs: A dictionary for algorithm-specific parameters. For
            example, a GA might accept `population_size=100` or
            `mutation_rate=0.1`.
    """
    self.problem = problem
    self.name = name
    self.comparator = constraint_handler or DefaultComparator()

get_population()

Returns the entire current population or set of candidate solutions.

This method is optional and should be implemented by population-based algorithms. It is required for certain performance metrics like diversity measurement.

Raises:

Type Description
NotImplementedError

If the solver is not swarm based.

Returns:

Type Description
List[SolutionType]

All individuals in the solver's population.

Source code in cilpy/solver/__init__.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def get_population(self) -> List[SolutionType]:
    """
    Returns the entire current population or set of candidate solutions.

    This method is optional and should be implemented by population-based
    algorithms. It is required for certain performance metrics like
    diversity measurement.

    Raises:
        NotImplementedError: If the solver is not swarm based.

    Returns:
        All individuals in the solver's population.
    """
    raise NotImplementedError(
        f"""The solver '{self.name}' does not have a population. Implement
         get_population() to use metrics that require it."""
    )

get_population_evaluations()

Returns the evaluations of the entire current population or set of candidate solutions.

This method is optional and should be implemented by population-based algorithms. It is required for certain performance metrics like percentage of feasible solutions.

Raises:

Type Description
NotImplementedError

If the solver is not swarm based.

Returns:

Type Description
List[Evaluation[FitnessType]]

Evaluations for all individuals in the solver's population.

Source code in cilpy/solver/__init__.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def get_population_evaluations(self) -> List[Evaluation[FitnessType]]:
    """
    Returns the evaluations of the entire current population or set of
    candidate solutions.

    This method is optional and should be implemented by population-based
    algorithms. It is required for certain performance metrics like
    percentage of feasible solutions.

    Raises:
        NotImplementedError: If the solver is not swarm based.

    Returns:
        Evaluations for all individuals in the solver's population.
    """
    raise NotImplementedError(
        f"""The solver '{self.name}' does not have a population. Implement
         get_population_evaluations() to use metrics that require it."""
    )

get_result() abstractmethod

Returns the best solution(s) found so far.

This method provides the current result of the optimization process. It is called by the ExperimentRunner after each step to log progress.

Returns:

Type Description
List[Tuple[SolutionType, Evaluation[FitnessType]]]

A list of tuples, where each tuple contains `(solution,

List[Tuple[SolutionType, Evaluation[FitnessType]]]

evaluation)`.

List[Tuple[SolutionType, Evaluation[FitnessType]]]
  • For single-objective solvers, this list should contain a single tuple with the best solution found.
List[Tuple[SolutionType, Evaluation[FitnessType]]]
  • For multi-objective solvers, this list should contain the set of non-dominated solutions (the Pareto front archive).
List[Tuple[SolutionType, Evaluation[FitnessType]]]

Example return for a single-objective solver:

List[Tuple[SolutionType, Evaluation[FitnessType]]]

[([0.1, -0.5], Evaluation(fitness=0.26))]

Source code in cilpy/solver/__init__.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@abstractmethod
def get_result(self) -> List[Tuple[SolutionType, Evaluation[FitnessType]]]:
    """Returns the best solution(s) found so far.

    This method provides the current result of the optimization process. It
    is called by the `ExperimentRunner` after each step to log progress.

    Returns:
        A list of tuples, where each tuple contains `(solution,
        evaluation)`.
        - For single-objective solvers, this list should contain a single
          tuple with the best solution found.
        - For multi-objective solvers, this list should contain the set
          of non-dominated solutions (the Pareto front archive).

        Example return for a single-objective solver:
        `[([0.1, -0.5], Evaluation(fitness=0.26))]`
    """
    pass

step() abstractmethod

Performs one iteration of the optimization algorithm.

This method contains the core logic of the solver. The ExperimentRunner will call this method repeatedly in a loop. A single step could be one generation in a GA, one iteration in a PSO, or the evaluation of one new solution in a simpler algorithm.

Source code in cilpy/solver/__init__.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
@abstractmethod
def step(self) -> None:
    """Performs one iteration of the optimization algorithm.

    This method contains the core logic of the solver. The
    `ExperimentRunner` will call this method repeatedly in a loop. A single
    step could be one generation in a GA, one iteration in a PSO, or the
    evaluation of one new solution in a simpler algorithm.
    """
    pass