Choose a Hook, Stage, or Callback#

Cross-layer

The prerequisite is wanting to add observation or post-processing to an existing execution path.

What you’ll be able to do

By the end of this page, you’ll be able to distinguish Hooks that change data, Stages that replace execution units, and Callbacks that observe events.

See the optimization components index; for a custom contract, continue to Custom components.

Compare the boundaries#

Mechanism

Purpose

Execution point

Can change state?

Suitable uses

with_post

Post-process an existing Operator’s output

After Crossover or Mutation

Can change candidates through its return value

Repair, rounding, and bounding

with_post_fit

Process after a Surrogate fit

After fit()

Can update the model or external state in the Hook

Fit records and model post-processing

Stage

Implement or replace a compatibility execution unit

At a Stage position in the Pipeline

Can update state by returning an OptimizationState

Add generation processing and swap a Stage

CallbackManager

Observe events

When a registered Event fires

event.ctx is read-only; do not use it to replace Pipeline input

Logging, history, and condition checks

Change data with a Hook#

To change an Operator’s candidate array, use with_post() rather than CallbackManager. with_post() returns a copy with the Hook added without modifying the original instance.

import numpy as np
from saealib.operators import MutationUniform


def snap_to_grid(offspring, parents, rng, ctx):
    return np.round(offspring * 10.0) / 10.0


mutation = MutationUniform(0.3).with_post(snap_to_grid)

For post-processing after a Surrogate fit, pass fn(train_x, train_y, ctx) -> None to with_post_fit(). See the Surrogate concept for the contract used by this Hook.

Replace an execution unit with a Stage#

Stage is the compatibility boundary execute(state) -> state. Return a new State with state.replace() when updating state. It is not the boundary for implementing a structured Framework Component or Compiler directly; for that, see Framework extensions.

from saealib import Stage


class LogGenerationStage(Stage):
    name = "log_generation"

    def execute(self, state):
        print(state.gen)
        return state

See the Stage concept for Pipeline composition and the Stage contract.

Observe with a Callback#

CallbackManager.register() registers a function that receives an Event type and event. The event.ctx read by the handler is the public read boundary for generation and evaluation counts.

import numpy as np
from saealib import GenerationEndEvent
from saealib import Optimizer, Problem


def objective(x):
    return np.sum(x**2)


problem = Problem(
    objective,
    dim=3,
    n_obj=1,
    direction=np.array([-1.0]),
    lb=[-5.0] * 3,
    ub=[5.0] * 3,
)
optimizer = Optimizer(problem, seed=0)

history = []


def record(event):
    history.append((event.ctx.gen, event.ctx.fe))


optimizer.cbmanager.register(GenerationEndEvent, record)

Replacing candidate arrays or rewriting state is outside a Callback’s responsibility. Use Callbacks for event observation, logging, and recording history.

Use Custom components when you need to implement a component contract, and see Framework extensions when changing the contract or Runtime itself.