Algorithm#
saealib separates candidate generation from feedback consumption and places that contract in Algorithm.
Replace an Algorithm with Optimizer.set_algorithm(algorithm).
The current Algorithm contract combines a Proposer that generates candidates and a FeedbackConsumer that consumes evaluation results.
Users do not need to think about this contract during ordinary use.
Implement the following state boundaries only when you are building a custom search algorithm.
Algorithm’s role#
Algorithm receives a read-only StateView and returns results without changing state directly.
ask(request, state) -> ProposalBatch: Generates a candidate batch from ProposalRequest and StateView.
When request.n_offspring is None, the algorithm determines the candidate count from the current Population.
tell(feedback, state) -> StatePatch: Consumes a FeedbackBatch and returns the state changes to apply as a StatePatch.
tell() does not modify OptimizationState or Population directly.
This separation leaves the decision about which candidates receive expensive true evaluations outside Algorithm.
That decision belongs to the combination of OptimizationStrategy, the evaluation plan, and FeedbackPolicy.
Built-in Algorithms#
Class |
Search method |
|---|---|
|
Genetic algorithm via crossover and mutation |
|
Particle swarm optimization via velocity and position updates |
|
Genetic algorithm that handles the |
GenomeGA is an Algorithm on the Genome-native path that uses the GenomeBatch and SearchSpace contracts.
It preserves the Genome representation and space services instead of implicitly converting them to the same x column path used by fixed-width dense-vector GA.
GA: a container that combines operators#
GA itself has no search logic; it’s a container assembled by injecting four operators: crossover/mutation/parent_selection/survivor_selection.
GA(crossover, mutation, parent_selection, survivor_selection, *,
duplicate_elimination=None,
variation_execution="batch",
integer_crossover=None, integer_mutation=None,
categorical_crossover=None, categorical_mutation=None)
The behavior and parameters of each operator are covered on their own dedicated pages.
Crossover: Generates offspring from parent individuals
Mutation: Adds perturbation to offspring
ParentSelection: Chooses the parent individuals used for crossover
SurvivorSelection: Chooses the individuals kept for the next generation
variation_execution selects batch or sequential execution for crossover and mutation, and a dictionary can control them independently.
Batch is the default; for the full operation/hook ordering and reproducibility semantics, see the GA API reference below.
GA.tell() combines the current Population and the candidates generated by ask() in one pool, then selects the next Population with survivor_selection.
This corresponds to a \((\mu+\lambda)\)-style update.
What goes into the pool (whether parents are included) is GA’s own responsibility, and doesn’t appear in SurvivorSelection’s interface itself.
GA: handling mixed-variable problems#
For problems whose design variables mix integer and categorical variables, GA uses type-specific operators in addition to the crossover/mutation for continuous variables.
If integer_crossover/integer_mutation/categorical_crossover/categorical_mutation are omitted, CrossoverIntegerSBX/MutationIntegerUniform/CrossoverCategorical/MutationCategorical are supplied automatically, respectively.
The probability parameters (prob/prob_var) of the automatically supplied operators are inherited from the continuous-variable crossover/mutation.
Even for operators supplied per type, n_children/n_parents must match the continuous-variable crossover.
A mismatch raises ConfigurationError.
This is because GA splits parent individuals into columns by variable type, applies each operator only to its corresponding columns, and then reassembles the results into a single individual — an implementation under which the number of children and parents must not diverge between types.
The default batch mode calls each type-specific operator’s batch method on its columns.
variation_execution="sequential" uses the earlier per-pair/per-individual routing and preserves its random-number sequence.
Variable types are defined via Problem’s variables argument.
GA: auxiliary utilities#
Passing DuplicateElimination(atol=1e-16, rtol=0.0, max_retries=100) to the duplicate_elimination argument replaces offspring that duplicate the current population by regenerating them.
The tolerance for duplicate detection is given by atol/rtol, and the maximum number of regeneration attempts by max_retries.
If omitted (the default None), no duplicate elimination is performed.
saealib.repair_clipping(candidates, bounds) is a standalone utility function that simply np.clips a set of candidates into (lb, ub).
GA itself repairs candidates via ConstraintHandler’s repair() (which also defaults to np.clip) and Problem.repair() (per-Variable projection); repair_clipping is not automatically wired into GA’s internal processing.
It’s exposed for cases where you want the same clipping behavior standalone — for example, when writing a custom evaluation pipeline that doesn’t go through ConstraintHandler.
PSO#
PSO(w=0.7, c1=1.5, c2=1.5, v_max=None) updates velocity using weights for the inertia term w, attraction to the personal best c1, and attraction to the swarm best c2, then advances position by that velocity.
Specifying v_max clamps each dimension’s velocity magnitude to that value.
Unlike GA, it isn’t assembled by injecting operators — ask()/tell() directly perform the velocity and position updates and track each particle’s pbest (personal best).
The swarm best (leader) is chosen from all particles’ pbests using ctx.comparator, so any single-objective Comparator is automatically supported.
Multi-objective PSO (MOPSO) requires a dedicated subclass that manages a non-dominated solution set; the built-in PSO targets single-objective problems.
External library adapters#
PymooAlgorithm(pymoo_algorithm, *, allow_partial_tell=False) wraps an already-constructed pymoo algorithm (e.g. NSGA2(), DE()) so an existing pymoo algorithm can drive saealib’s ask-tell loop and surrogate-assisted strategies unchanged.
Unlike GA/PSO, which treat ctx.population as the source of truth, PymooAlgorithm runs in “engine mode”: the wrapped pymoo algorithm owns its own population and internal survival state, and ctx.population is refreshed from it at the end of every tell() — a mirror, not the source of truth.
This is the only way to reuse a pymoo algorithm’s own tested survival logic unchanged, but it comes with real limits worth knowing before reaching for it:
No checkpoint/resume —
OptimizationState.save()only serializes saealib’s own arrays, not the wrapped algorithm’s internal state.n_offspringpassed toask()is ignored; the offspring count is fixed by the wrapped algorithm’s own configuration (e.g. itspop_size).PreSelectionStrategy’s top-k truncation handstell()only a subset of whatask()produced. By default this raisesConfigurationError, since index-coupled algorithms (e.g. differential evolution) would silently misalign parents and offspring under a partialtell(); passallow_partial_tell=Trueto opt in anyway.
See Installation for the pymoo extra.
Implementing a custom Algorithm#
To implement a custom search algorithm, subclass Algorithm and implement ask() and tell().
Implement get_required_attrs(), population_class, and archive_class as well when using the existing Population compatibility path.
In a new implementation, ask() returns a ProposalBatch, and tell() returns a StatePatch.
state is a StateView that can access only declared state keys.
Candidate counts, candidate IDs, and the mapping to evaluation results follow the ProposalBatch and FeedbackBatch contracts.
The following skeleton shows only the boundaries a custom Algorithm implements.
from saealib import Algorithm, Archive, Population
from saealib.algorithms import ProposalRequest
from saealib.core.contracts import FeedbackBatch, ProposalBatch
from saealib.core import StatePatch, StateView
class CustomAlgorithm(Algorithm):
"""Skeleton for an Algorithm that generates candidates and consumes Feedback."""
def get_required_attrs(self, problem):
return []
@property
def population_class(self):
return Population
@property
def archive_class(self):
return Archive
def ask(self, request: ProposalRequest, state: StateView) -> ProposalBatch:
# Read the current Population from state.context and declared StateKeys.
# Return a ProposalBatch with candidate IDs and FeedbackRequirement.
...
def tell(self, feedback: FeedbackBatch, state: StateView) -> StatePatch:
# Read the FeedbackBatch and collect changes to Population and other state in a StatePatch.
...
The Feedback received by tell() preserves its evaluation path and its mapping to candidate IDs.
Whether the algorithm requires only true evaluations or accepts predictions is declared by its contract and FeedbackPolicy.
Persistent auxiliary information such as PSO’s pbest is handled as a Population attribute or a declared StateKey.