Source code for quchip.utils.state_versioning
"""Track public attribute assignments to invalidate device and coupling caches.
After construction, tracked assignments increment ``state_version``. Private
attributes and ``_untracked_names`` are excluded. The counter tracks writes,
not in-place edits to mutable attribute values.
The outermost ``__init__`` turns on tracking once.
"""
# Classes that synthesize an initializer after `__init_subclass__` must apply
# `_wrap_init_for_finish` themselves. JAX pytree unflattening bypasses
# initialization and installs the tracking state directly.
from __future__ import annotations
import functools
from typing import Any, ClassVar
def _wrap_init_for_finish(cls: type) -> None:
"""Wrap the class's own initializer to enable tracking after its outermost call.
Does nothing if the initializer is absent or already wrapped. The identity
check prevents nested ``super().__init__`` calls from finishing early.
"""
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: a monotone ``state_version`` that increments 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 that :meth:`_finish_init` runs 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 increment ``state_version`` for tracked writes.
"""
# The increment runs in a `finally` block, so if the `_on_attr_set` hook
# raises an error, the changed attribute cannot stay 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 that increments on each tracked mutation of a public parameter."""
return self._state_version