Tracers ======= Tracers provide an agnostic interface to implement logging, reporting, and real-time monitoring of the annealing process. They allow you to "hook" into each iteration of the algorithm to capture the current state without modifying the core logic of the optimization. Definition ---------- A tracer is defined as a callable that accepts the current :class:`AnnealingState` and returns nothing. In the core implementation, it follows this type alias: .. code-block:: python type Tracer[T] = Callable[[AnnealingState[T]], None] Usage ----- To use a tracer, you assign it to the ``tracer`` property of a :class:`SimulatedAnnealing` instance. By default, the algorithm uses a no-op tracer (``_no_trace``) which does nothing. Function-Based Tracers ---------------------- For simple logging or printing, a plain function is often sufficient: .. code-block:: python def simple_logger(state): if state.iteration % 100 == 0: print(f"Iteration {state.iteration}: Best Value = {state.best_value}") sa = SimulatedAnnealing(...) sa.tracer = simple_logger sa.simulate(initial_state) Object-Based Tracers -------------------- For more complex scenarios, such as accumulating data into a data structure or generating plots, you can implement a tracer as a class with a ``__call__`` method. This allows the tracer to maintain its own internal state across iterations. Example: A simple history accumulator: .. code-block:: python class HistoryTracer: def __init__(self): self.history = [] def __call__(self, state): # Store a copy of the current best value self.history.append(state.best_value) history_tracer = HistoryTracer() sa.tracer = history_tracer sa.simulate(...) print(f"Captured {len(history_tracer.history)} data points.") Customization ------------- Since tracers are agnostic, you can easily wrap existing logging libraries (like `loguru` or standard `logging`) or even send metrics to external services like Weights & Biases or MLflow by simply implementing the tracer interface. .. include:: dataframe.rst .. include:: plot.rst