--- primary_layer: layer3 page_type: concept --- # 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](../execution_and_evaluation/strategies.md), the evaluation plan, and `FeedbackPolicy`. ## Built-in Algorithms | Class | Search method | |---|---| | `GA` | Genetic algorithm via crossover and mutation | | `PSO` | Particle swarm optimization via velocity and position updates | | `GenomeGA` | Genetic algorithm that handles the `SearchSpace` Genome directly | `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`. ```python 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](crossover.md): Generates offspring from parent individuals - [Mutation](mutation.md): Adds perturbation to offspring - [ParentSelection](parent_selection.md): Chooses the parent individuals used for crossover - [SurvivorSelection](survivor_selection.md): 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](../problem_and_ranking/problem.md)'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.clip`s a set of candidates into `(lb, ub)`. `GA` itself repairs candidates via [ConstraintHandler](../problem_and_ranking/constraints.md)'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](https://pymoo.org/) 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_offspring` passed to `ask()` is ignored; the offspring count is fixed by the wrapped algorithm's own configuration (e.g. its `pop_size`). - `PreSelectionStrategy`'s top-k truncation hands `tell()` only a subset of what `ask()` produced. By default this raises `ConfigurationError`, since index-coupled algorithms (e.g. differential evolution) would silently misalign parents and offspring under a partial `tell()`; pass `allow_partial_tell=True` to opt in anyway. See [Installation](../../getting_started/installation.md) 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. ```python 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`. ## Related components - [Crossover](crossover.md) / [Mutation](mutation.md) / [ParentSelection](parent_selection.md) / [SurvivorSelection](survivor_selection.md): The four operators `GA` combines - [Problem](../problem_and_ranking/problem.md): Defining mixed variables via `variables` - [ConstraintHandler](../problem_and_ranking/constraints.md): The `repair()` that `GA` uses to repair candidates - [OptimizationStrategy](../execution_and_evaluation/strategies.md): Decides whether to perform true evaluation between `ask()`/`tell()` - [Population](../observation_and_state/population.md): Population and `GenomeBatch` used on Algorithm's compatibility path ## References - {py:class}`saealib.Algorithm` - {py:class}`saealib.GA` - {py:class}`saealib.PSO` - {py:class}`saealib.PymooAlgorithm` - {py:class}`saealib.DuplicateElimination` - {py:func}`saealib.repair_clipping`