>,
+
+ /// The objective space, useful for observers that need to convert
+ /// raw objective values to minimization-oriented form.
+ pub objectives: &'a ObjectiveSpace,
+}
diff --git a/src/prelude.rs b/src/prelude.rs
index 02f1b68..5f38f85 100644
--- a/src/prelude.rs
+++ b/src/prelude.rs
@@ -11,6 +11,14 @@ pub use crate::core::{
pub use crate::traits::{Initializer, Optimizer, Repair, Variation};
+#[cfg(feature = "tracing")]
+pub use crate::observer::builtin::TracingObserver;
+pub use crate::observer::{
+ Observer, Snapshot,
+ builtin::{AllOf, AnyOf, MaxIterations, MaxTime, Periodic, Stagnation, TargetFitness},
+};
+pub use std::ops::ControlFlow;
+
pub use crate::pareto::{
Dominance, ParetoArchive, best_candidate, crowding_distance, das_dennis, non_dominated_sort,
pareto_compare, pareto_front,
diff --git a/src/traits/optimizer.rs b/src/traits/optimizer.rs
index 2a8ac76..712b479 100644
--- a/src/traits/optimizer.rs
+++ b/src/traits/optimizer.rs
@@ -1,18 +1,71 @@
//! The single trait users implement to add a new optimizer.
+use std::time::{Duration, Instant};
+
use crate::core::problem::Problem;
use crate::core::result::OptimizationResult;
+use crate::observer::{Observer, Snapshot};
/// An optimizer that runs to completion in a single call.
///
/// Implementations own their main loop, manage their own state, and return an
-/// [`OptimizationResult`]. v1 deliberately does not expose a step-by-step API
-/// or an associated error type — invalid configuration may panic with a clear
-/// message.
+/// [`OptimizationResult`]. Invalid configuration panics with a clear
+/// message rather than returning a `Result`.
pub trait Optimizer
where
P: Problem,
{
/// Run the optimizer to completion against `problem`.
fn run(&mut self, problem: &P) -> OptimizationResult;
+
+ /// Run with an [`Observer`] called after each generation.
+ ///
+ /// The observer can halt the run by returning
+ /// [`std::ops::ControlFlow::Break`]; the partial result is still
+ /// returned. Built-in observers in
+ /// [`heuropt::observer::builtin`](crate::observer::builtin) cover
+ /// the common stop conditions (`MaxTime`, `TargetFitness`,
+ /// `Stagnation`, …).
+ ///
+ /// **Default impl:** falls back to `run` plus a single final
+ /// notification. Algorithms that override this method get true
+ /// per-generation observation; algorithms that don't get a single
+ /// notification at the end. The trait-level docstring on each
+ /// algorithm calls out which behavior it supports.
+ fn run_with(&mut self, problem: &P, observer: &mut O) -> OptimizationResult
+ where
+ O: Observer,
+ {
+ let started = Instant::now();
+ let result = self.run(problem);
+ let elapsed = started.elapsed();
+ notify_final(&result, elapsed, problem, observer);
+ result
+ }
+}
+
+/// Helper used by the default `run_with` impl: build a single final-
+/// state snapshot and hand it to the observer once. Algorithms that
+/// override `run_with` for per-generation reporting don't go through
+/// this path — they construct their own per-iteration snapshots.
+fn notify_final(
+ result: &OptimizationResult,
+ elapsed: Duration,
+ problem: &P,
+ observer: &mut O,
+) where
+ P: Problem,
+ O: Observer,
+{
+ let objectives = problem.objectives();
+ let snap = Snapshot {
+ iteration: result.generations,
+ evaluations: result.evaluations,
+ elapsed,
+ population: result.population.as_slice(),
+ pareto_front: Some(result.pareto_front.as_slice()),
+ best: result.best.as_ref(),
+ objectives: &objectives,
+ };
+ let _ = observer.observe(&snap);
}