Source code for pytcl.diagnostics

"""
Diagnostics: opt-in logging, instrumentation, and progress reporting.

pytcl is completely silent by default: importing it disables the ``pytcl``
loguru namespace and installs no handlers. Call :func:`enable_debug_logging`
to see gating rejections, association decisions, filter-health symptoms,
and data-file resolution at DEBUG level; :func:`disable_debug_logging`
returns to silence. This module is the redesigned successor to the
``pytcl.logging_config`` module removed in v2.0.0; no compatibility is
provided.

Examples
--------
>>> import pytcl
>>> pytcl.diagnostics.diagnostics_enabled()
False
"""

import sys
from typing import Any, Optional, Sequence

from loguru import logger as _logger

# Filter-health thresholds.
NIS_WINDOW = 20
NIS_OUTLIER_FACTOR = 3.0
CONDITION_WARN = 1e12

# The library never speaks unless spoken to.
_logger.disable("pytcl")

logger = _logger  # instrumentation sites import this and bind site=...

_handler_id: Optional[int] = None
_enabled: bool = False
# Id of loguru's stock stderr handler (0 at interpreter start). Tracked
# across enable/disable cycles so we only ever remove/restore the one
# handler that stands in for "the host's default logging", never our own.
_default_handler_id: Optional[int] = 0
# Whether *we* removed the handler at _default_handler_id above -- disable()
# only restores a stand-in if this is True, so a default handler the host
# application removed itself (or already had removed) is never resurrected.
_removed_default_handler: bool = False


[docs] def diagnostics_enabled() -> bool: """Whether diagnostic logging is currently enabled. Hot paths consult this before constructing log payloads, so the disabled path costs one boolean check. """ return _enabled
[docs] def enable_debug_logging(level: str = "DEBUG") -> None: """ Enable pytcl's diagnostic logging with a rich-formatted handler. Parameters ---------- level : str, optional Minimum level to emit ("DEBUG", "INFO", "WARNING", ...). Notes ----- Idempotent: calling again replaces the previous handler rather than stacking a second one. Output goes to stderr, ASCII-safe. A bad ``level`` raises before anything is touched: the new handler is added first, so the failure happens while state is still exactly what it was before the call (namespace disabled, or however a prior successful call last left it). loguru installs a default stderr handler (id 0) at import time; left alive, it double-prints every record next to the one pytcl's own handler emits. This function removes it the first time diagnostics are enabled and remembers that it did so; :func:`disable_debug_logging` then puts an equivalent handler back (``logger.add(sys.stderr)``, loguru's own default configuration) so the host application's logging is not left worse off than before pytcl touched it. That replacement is loguru's stock handler, not a byte-for-byte restoration of whatever the host had configured at id 0 -- if the host had customized it, this function has no way to know and does not try to reproduce it. """ global _handler_id, _enabled, _default_handler_id, _removed_default_handler # Add the new handler FIRST: an invalid level raises here, before any # module state changes, so a failed call leaves everything untouched. new_handler_id = _logger.add( sys.stderr, level=level, format=( "<green>{time:HH:mm:ss.SSS}</green> | " "<level>{level: <8}</level> | " "<cyan>{name}</cyan> - {message}" ), filter=lambda record: ( record["name"] == "pytcl" or record["name"].startswith("pytcl.") ), ) if _handler_id is not None: # Idempotent re-enable: retire our previous handler. The host may # have already removed it out from under us -- that is not our # error to raise. try: _logger.remove(_handler_id) except ValueError: pass _handler_id = None if _default_handler_id is not None: # First enable since interpreter start (or since the last # disable() restored a stand-in): take down the default handler # so it stops double-printing alongside ours. try: _logger.remove(_default_handler_id) _removed_default_handler = True except ValueError: # Already gone -- the host removed it, or a previous call did. _removed_default_handler = False _default_handler_id = None _handler_id = new_handler_id _logger.enable("pytcl") _enabled = True
[docs] def disable_debug_logging() -> None: """Return the library to complete silence. Idempotent. If :func:`enable_debug_logging` removed loguru's default stderr handler to prevent double-printing, this restores an equivalent one (see that function's docstring for the exact caveat). """ global _handler_id, _enabled, _default_handler_id, _removed_default_handler if _handler_id is not None: try: _logger.remove(_handler_id) except ValueError: pass _handler_id = None _logger.disable("pytcl") _enabled = False if _removed_default_handler: _default_handler_id = _logger.add(sys.stderr) _removed_default_handler = False
[docs] def log_filter_health( track_id: Any, nis_value: float, nis_window: Sequence[float], cov_condition: float, ) -> None: """Log a per-track filter-health snapshot (NIS + covariance condition). Guarded internally by :func:`diagnostics_enabled` -- callers on a hot path may call this bare without checking first, since a disabled namespace makes the call a single boolean-check no-op. Plain floats/sequences only; this module takes no dependency on ``pytcl``'s tracker types. Parameters ---------- track_id : Any Identifier of the track this health snapshot belongs to. nis_value : float Normalized innovation squared for the current update. nis_window : sequence of float Recent NIS history used as the local baseline for outlier detection. Includes the current sample, per call-site convention. cov_condition : float Condition number of the track's state covariance. Notes ----- Symptomatic (logged at WARNING instead of DEBUG) when either: - ``nis_value`` exceeds ``NIS_OUTLIER_FACTOR`` times the mean of ``nis_window`` AND that mean is positive -- with an empty or all-zero window the NIS branch cannot fire regardless of ``nis_value``, since any threshold scaled from a zero mean would flag everything -- or - ``cov_condition`` exceeds ``CONDITION_WARN`` (covariance going numerically singular). The caller owns ``nis_window``'s lifecycle; this function only reads it. Callers that keep a rolling window across an enable/disable toggle (as ``MultiTargetTracker`` does) will blend pre-disable history into the first post-re-enable call -- that's a call-site persistence choice, not something this function corrects. """ if not diagnostics_enabled(): return window = list(nis_window) # mean_nis == 0 only when every sample in the window is exactly 0 (or # the window is empty); guarding against it means an all-zero window # can never trip the NIS-outlier branch, however large nis_value gets -- # such a track is still caught by the cov_condition branch below. mean_nis = sum(window) / len(window) if window else 0.0 symptomatic = ( nis_value > NIS_OUTLIER_FACTOR * mean_nis and mean_nis > 0 ) or cov_condition > CONDITION_WARN bound = logger.bind(site="filter_health") message = "track {}: nis={:.4f} (window_mean={:.4f}, n={}) cov_condition={:.4e}" if symptomatic: bound.warning( message, track_id, nis_value, mean_nis, len(window), cov_condition ) else: bound.debug(message, track_id, nis_value, mean_nis, len(window), cov_condition)
from pytcl.diagnostics.render import progress_bar, track_table # noqa: E402 __all__ = [ "logger", "diagnostics_enabled", "enable_debug_logging", "disable_debug_logging", "track_table", "progress_bar", "log_filter_health", "NIS_WINDOW", "NIS_OUTLIER_FACTOR", "CONDITION_WARN", ]