Engine ownership and callbacks ============================== A ``Solver`` owns its native engine. Its domain views retain that owner, so a view cannot outlive the Python object that manages its native memory. Calling ``close()`` or ``destroy()`` invalidates retained 2D and infiltration views; structural edits also invalidate these views. Reacquire ``solver.surface2d`` after reopening or editing a model. Invalidated views raise ``StaleObjectError`` before entering the native library. Use ``solver.surface2d`` or ``Surface2D(solver)``. Passing ``solver.handle`` to ``Surface2D`` is deprecated. For compatibility it is accepted only when the address belongs to a live registered Python owner. Arbitrary native addresses are rejected with ``BadHandleError``. ``ModelBuilder.to_solver()`` transfers ownership once. Existing builder views become stale, and a second transfer raises ``BadHandleError``. The returned solver has the same initialized Python state as a normally constructed solver. Threads ------- Different solvers may advance on different threads. Calls that release the Python GIL pin their engine owner for the duration of the native operation. Access from another thread to that same engine raises ``LifecycleError`` promptly. This includes attempts to destroy the engine while it is advancing. Coordinate access in the application and collect snapshots between steps. Callbacks --------- Step callbacks run on the advancing thread. They may inspect results and apply forcing supported at that stage by the engine. They must not reenter lifecycle operations such as ``step()``, ``close()`` or ``destroy()``, or replace callback registrations while a callback is active; these operations raise ``LifecycleError``. A Python exception cannot unwind through the native callback ABI. The binding captures the first exception and re-raises that same exception when the native operation returns. Later callbacks in that operation are suppressed. The current native step may finish, so callback failure does not roll back the simulation. This behavior applies to ``step()``, ``stride()``, ``steps()`` and ``until()``. Unregister or repair the failing callback before advancing again. Testing a local build --------------------- Use the project's ``openswmm`` Conda environment for validation:: conda run --no-capture-output -n openswmm python -m pytest tests/engine Run this command from ``python/`` after building/installing the checkout into that environment, or set ``PYTHONPATH`` to a staged build. Verify the loaded extension path before testing:: conda run -n openswmm python -c "import openswmm.engine._solver as m; print(m.__file__)" A successful test of an older installed package does not validate a changed checkout. Rebuild all extensions after changing a shared ``.pxd`` declaration.