Source code for quchip.utils.state_versioning

"""StateVersioned — shared mutation-tracking / cache-invalidation mixin.

The engine caches assembled Hamiltonians (and dressed-state analyses) keyed
on a per-object ``state_version`` counter. After construction finishes,
every tracked public mutation bumps the counter so stale caches invalidate
deterministically. Both :class:`~quchip.devices.base.BaseDevice` and
:class:`~quchip.chip.coupling_base.BaseCoupling` need exactly this
machinery, so it lives here once instead of being copy-pasted into each
base.

:meth:`_finish_init` flips mutation tracking on and fires exactly once per
instance. :meth:`__init_subclass__` wraps each concrete subclass's own
``__init__``; the wrapper calls :meth:`_finish_init` only when
``type(self).__init__`` resolves to itself — the entry-point init for the
instance's actual type — so ``super().__init__`` chains and explicit
inner-init forwards never re-fire it.

Subclass-authored init machinery that synthesizes ``__init__`` after the
``__init_subclass__`` super-chain runs (the declarative ``DeviceModel``)
must call :func:`_wrap_init_for_finish` on the freshly-installed init
itself; the helper is idempotent, so a repeat call is harmless.

The JAX pytree ``_unflatten`` path bypasses ``__init__`` entirely — it
builds via ``cls.__new__`` and sets ``_tracking_enabled`` directly — and so
bypasses the wrapper as well.
"""

from __future__ import annotations

import functools
from typing import Any, ClassVar


def _wrap_init_for_finish(cls: type) -> None:
    """Wrap ``cls.__dict__['__init__']`` so :meth:`_finish_init` fires once.

    No-op when *cls* defines no own ``__init__`` or when its ``__init__`` is
    already wrapped (idempotent). The wrapper runs the original init, then calls
    ``_finish_init`` only when ``type(self).__init__`` resolves to this wrapper
    — the entry-point init for the instance's actual type — so nested
    ``super().__init__`` / explicit inner-init calls never re-fire it.
    """
    init = cls.__dict__.get("__init__")
    if init is None or getattr(init, "_sv_wrapped", False):
        return

    @functools.wraps(init)
    def _wrapped(self: Any, *args: Any, **kwargs: Any) -> None:
        init(self, *args, **kwargs)
        if type(self).__init__ is _wrapped:
            self._finish_init()

    _wrapped._sv_wrapped = True  # type: ignore[attr-defined]
    cls.__init__ = _wrapped  # type: ignore[misc]


[docs] class StateVersioned: """Mixin: monotone ``state_version`` bumped on tracked public mutations.""" #: Public attribute names that must NOT bump ``_state_version`` when set. #: Subclasses (BaseDevice, BaseCoupling) extend this with their structural / #: identity attributes. Private/dunder names are always excluded separately. _untracked_names: ClassVar[frozenset[str]] = frozenset() #: Engine-visible cache-invalidation counter. Class-level default ``0``; the #: bump installs a per-instance value via ``object.__setattr__``. _state_version: int = 0 #: Whether mutation tracking is live. Flipped on by :meth:`_finish_init` #: after the outermost ``__init__`` returns. _tracking_enabled: bool = False def __init_subclass__(cls, **kwargs: Any) -> None: """Wrap *cls*'s own ``__init__`` so :meth:`_finish_init` fires exactly once.""" super().__init_subclass__(**kwargs) _wrap_init_for_finish(cls) def __setattr__(self, name: str, value: Any) -> None: """Set *name*, then run the cache-invalidation hook and bump ``state_version`` for tracked writes. The bump runs in a ``finally`` block so a raising :meth:`_on_attr_set` hook cannot leave the mutated attribute paired with a stale ``state_version``. """ # ``object.__setattr__`` is used for the bump so it doesn't itself # retrigger the hook. object.__setattr__(self, name, value) try: self._on_attr_set(name) finally: if ( not name.startswith("_") and name not in type(self)._untracked_names and getattr(self, "_tracking_enabled", False) ): object.__setattr__(self, "_state_version", self._state_version + 1) def _on_attr_set(self, name: str) -> None: """Handle an attribute set; default no-op cache-invalidation hook.""" def _finish_init(self) -> None: """Enable mutation tracking; fired automatically once after construction.""" object.__setattr__(self, "_tracking_enabled", True) @property def state_version(self) -> int: """Return the monotone counter bumped on every tracked public-parameter mutation.""" return self._state_version