babylon.engine.observer_adapter

ProtocolObserverAdapter for thread-safe GUI callback delivery.

This module provides a thread-safe bridge between the simulation engine’s observer notifications and GUI callbacks.

Feature: 006-gui-protocol-extension Date: 2026-01-31

Thread Safety Architecture:
  1. _lock protects _callbacks list during register/unregister

  2. notify() creates snapshot BEFORE iterating callbacks

  3. Callbacks receive SimulationSnapshot (frozen), NOT SimulationState reference

  4. Callback exceptions are caught and logged (per ADR003)

Why This Matters:
  • GUI callbacks NEVER hold a reference to mutable Simulation internals

  • Snapshot is created at a single consistent point in time

  • GUI thread can process the snapshot at leisure without races

  • Complete isolation between engine thread and GUI thread

See also

  • data-model.md#ProtocolObserverAdapter: Class specification

  • plan.md#Per-Tick Update Rule: Notification sequence

  • research.md#1: PyQt6 thread communication research

Classes

ProtocolObserverAdapter(simulation)

Thread-safe bridge between simulation and GUI callbacks.

class babylon.engine.observer_adapter.ProtocolObserverAdapter(simulation)[source]

Bases: object

Thread-safe bridge between simulation and GUI callbacks.

This adapter ensures GUI callbacks receive frozen snapshots rather than live references to mutable simulation state, providing complete thread safety for cross-thread GUI integration.

Parameters:

simulation (SimulationState)

_simulation

Reference to simulation for snapshot creation.

_callbacks

Registered GUI callbacks.

_lock

Synchronization for callback list modification.

Example

>>> from babylon.engine.simulation import Simulation
>>> sim = Simulation.from_sqlite(["26163"])
>>> adapter = ProtocolObserverAdapter(sim)
>>>
>>> def my_callback(tick: int, snapshot: SimulationSnapshot) -> None:
...     print(f"Tick {tick}: {len(snapshot.territories)} territories")
>>>
>>> adapter.register(my_callback)
>>> # ... simulation runs in another thread ...
>>> adapter.notify(tick=5)  # Called by engine after step()
__init__(simulation)[source]

Initialize adapter with simulation reference.

Parameters:

simulation (SimulationState) – Simulation instance implementing SimulationState protocol. Used to create snapshots via get_snapshot().

Return type:

None

register(callback)[source]

Register a GUI callback for tick notifications.

Thread-safe: may be called from any thread. Idempotent: duplicate registration is ignored (callback invoked once per tick).

Parameters:

callback (Callable[[int, SimulationSnapshot], None]) – Function to call with (tick, snapshot) after each step().

Return type:

None

unregister(callback)[source]

Remove a previously registered callback.

Thread-safe: may be called from any thread. No-op if callback was not registered.

Parameters:

callback (Callable[[int, SimulationSnapshot], None]) – The callback function to remove.

Return type:

None

notify(tick)[source]

Notify all registered callbacks with frozen snapshot.

Thread-safe: creates snapshot before iteration, exceptions logged. Callbacks receive immutable snapshot, not live simulation reference.

Critical: Snapshot is created BEFORE iterating callbacks. This ensures: 1. All callbacks see the same consistent state 2. GUI code cannot race with engine mutations 3. Callback processing time does not affect snapshot consistency

Parameters:

tick (int) – Current simulation tick number.

Return type:

None