CallbackManager#
Optimizer delegates notifications about execution progress to CallbackManager.
CallbackManager supports observation such as logging, collecting convergence history, and making conditional decisions.
Events observe execution boundaries; changing an Event field does not replace a Pipeline input.
CallbackManager’s role#
CallbackManager keeps a list of handlers per event type.
Method |
Description |
|---|---|
|
Registers |
|
Calls every registered handler, in registration order |
|
Removes a registered handler |
|
Replaces a registered handler with a different one |
An Event’s ctx is an _EventContext Protocol for reading values such as archive, comparator, n_obj, gen, and fe.
In ordinary sequential execution its concrete value is OptimizationState, but the Event’s public contract is limited to those read capabilities.
Treat ctx as read-only; use an official boundary such as with_post, Component, or Stage to change values.
List of available events#
Event |
Fired when |
Main fields |
|---|---|---|
|
Once, at the start of a run |
— |
|
Once, at the end of a run |
— |
|
At the start of each generation |
— |
|
At the end of each generation (before the state is yielded) |
— |
|
Before/after surrogate-based scoring |
|
|
Before/after acquisition scoring |
|
|
After crossover and repair |
|
|
After mutation and repair |
|
|
After all of |
|
|
After the surrogate is fitted |
|
|
After true evaluation of the chosen candidates |
|
|
After initial sampling, before initial evaluation |
|
|
After initial evaluation, before the archive is sorted |
|
PostSurrogateFitEvent is fired by the built-in Stage after the Surrogate is fit.
Events fired by built-in Stages may not set train_x and train_f.
PostEvaluationEvent may include request_id, candidate_ids, and status in addition to the evaluated offspring.
Default logging output#
Optimizer automatically registers logging_generation on GenerationStartEvent at construction time.
Configuring the standard library’s logging module makes per-generation progress (evaluation count, best objective value, or for multi-objective, the size and range of the first front) appear in the log.
Registering custom handlers and recording convergence history#
Register any handler with cbmanager.register(EventType, handler).
To record convergence history, register a handler holding a list accumulated via closure.
from saealib import GenerationEndEvent
history = []
def record_best(event):
f = event.ctx.archive.get_array("f")
history.append(float(f.min()))
optimizer.cbmanager.register(GenerationEndEvent, record_best)
ctx = optimizer.run()
print(history)
Tracking hypervolume#
logging_generation_hv(reference_point) returns a handler that logs the first front’s hypervolume relative to the specified reference point, every generation.
from saealib import GenerationStartEvent, logging_generation_hv
optimizer.cbmanager.register(
GenerationStartEvent,
logging_generation_hv(reference_point=np.array([1.1, 1.1])),
)
Replacing the default handler#
You can remove the auto-registered logging_generation with unregister(event_type, func), or replace it with a different handler via replace(event_type, old, new).
from saealib import GenerationStartEvent, logging_generation
optimizer.cbmanager.unregister(GenerationStartEvent, logging_generation)
The candidates field is for observation only#
The candidates field on PostCrossoverEvent/PostMutationEvent/PostAskEvent is for observation; reassigning it inside a handler (event.candidates = new_array) has no effect on the pipeline’s output.
GA keeps using its own local array reference after the event fires, so an in-place change (event.candidates[:] = ...) does reach GA.
PSO, on the other hand, fires this event after Population.extend() has already finished copying the candidates, so even an in-place change is too late and has no effect at all.
If you want to actually swap out the candidate array, use with_post(fn) on Crossover/Mutation instead of CallbackManager.
Think of CallbackManager as a mechanism for observation (logging, recording, conditional branching decisions) by design, not a means of rewriting pipeline data.
Switching configuration at runtime#
Event is for observation, and directly changing Optimizer internals from a Callback closure is not the standard procedure.
Use with_post() or with_post_fit() to change data flow, and switch execution configuration at a step boundary in iterate() or run().
When optimizer.set_*() or a component attribute changes at a step boundary, the execution environment detects the change, recompiles the plan, and applies it from the next generation.
This procedure works on both the Stage compatibility path and the graph-native path.
For a Component-requested recompilation path, see OptimizationStrategy’s “Behavior of runtime swapping”.
A Callback handler cannot return a RuntimeCommand.
A change made by calling optimizer.set_*() in a Callback closure is applied through the path above, but configuration changes are documented as a procedure on the iterate() side.
When to use CallbackManager vs. iterate()#
Aspect |
CallbackManager |
|
|---|---|---|
Granularity of invocation |
When a specific event occurs |
Per generation (a |
Main use |
Logging, observation, conditional side effects |
Intervening in the loop structure itself, e.g. switching components based on surrogate accuracy |
Relationship to |
Works with either |
Used in place of |
The switcher classes in Surrogate accuracy evaluation and dynamic switching are meant to be used inside an iterate() loop.
CheckpointCallback#
CheckpointCallback is an example Callback implementation.
The npz format uses OptimizationState.save(), while the pickle format uses Optimizer.save_pickle().
Because the saved values and resumption conditions differ by format, see Checkpointing for details.
Use this Callback as a reference for registering a custom Callback.