Source code for quchip.utils.values

"""Ownership and cache keys for authored numerical values."""

from __future__ import annotations

import copy
from typing import Any, Callable, TypeGuard, TypeVar
from dataclasses import dataclass, field

import jax
import jax.tree_util as jtu
import numpy as np
from jax.core import Tracer

try:
    from jax.extend.core import get_opaque_trace_state
except ImportError:  # JAX before 0.10 exports it only from jax.core.
    from jax.core import get_opaque_trace_state


[docs] def copy_value(value: Any, *, readonly: bool = False) -> Any: """Copy mutable values without detaching native JAX arrays or tracers.""" memo: dict[int, Any] = {} def capture(item: Any) -> Any: if id(item) in memo: return memo[id(item)] if isinstance(item, (jax.Array, Tracer)): memo[id(item)] = item return item if isinstance(item, np.ndarray): copied = copy.deepcopy(item, memo) copied.flags.writeable = not readonly return copied if isinstance(item, dict): copied_dict: dict[Any, Any] = {} memo[id(item)] = copied_dict copied_dict.update((key, capture(value)) for key, value in item.items()) return copied_dict if isinstance(item, list): copied_list: list[Any] = [] memo[id(item)] = copied_list copied_list.extend(capture(value) for value in item) return copied_list # Seed deepcopy's memo from registered trees, including immutable JAX # leaves inside native backend objects and extension payloads. for leaf in jtu.tree_leaves(item, is_leaf=lambda child: child is not item): if leaf is not item: memo[id(leaf)] = capture(leaf) return copy.deepcopy(item, memo) return capture(value)
_T = TypeVar("_T")
[docs] @dataclass(frozen=True) class TracedKey: """Cache-key stand-in for a JAX tracer: its identity in the trace that owns it.""" ident: int
[docs] def value_fingerprint(value: Any, *, traced: bool = False) -> Any: """Hash the contents of supported values; reject opaque mutable payloads. It rejects a tracer unless ``traced`` is true. If ``traced`` is true, the key is the tracer's identity (:class:`TracedKey`). Such keys are valid only in the owning trace, so you must scope their cache entries with :func:`scoped_entry`. """ if isinstance(value, Tracer): if traced: return TracedKey(id(value)) raise ValueError("Traced values cannot key an eager calculation cache.") if value is None or isinstance(value, (str, bytes, bool, int, float, complex, type)): return value if isinstance(value, (np.ndarray, np.generic, jax.Array)): array = np.asarray(value) if array.dtype.hasobject: raise ValueError("Object arrays cannot key a calculation cache.") return array.shape, array.dtype.str, array.tobytes() if isinstance(value, dict): return tuple((value_fingerprint(key, traced=traced), value_fingerprint(item, traced=traced)) for key, item in value.items()) if isinstance(value, (list, tuple)): return type(value), tuple(value_fingerprint(item, traced=traced) for item in value) leaves, tree = jtu.tree_flatten(value) if len(leaves) == 1 and leaves[0] is value: raise ValueError(f"Opaque {type(value).__name__} cannot key a calculation cache.") def structure(node: Any) -> Any: data = node.node_data() return value_fingerprint(data, traced=traced), tuple(structure(child) for child in node.children()) return structure(tree), tuple(value_fingerprint(leaf, traced=traced) for leaf in leaves)
[docs] def scoped_entry(key: Any, value: _T, *, traced: bool) -> tuple[Any, Any, _T]: """A cache entry ``(key, scope, value)``. An entry with a traced value, or with a key that holds a :class:`TracedKey`, records the current JAX trace. Such an entry belongs to that trace, and quchip does not reuse it in a nested or later trace. Other entries are valid everywhere. """ scoped = traced or any(isinstance(leaf, TracedKey) for leaf in jtu.tree_leaves(key)) return key, get_opaque_trace_state() if scoped else None, value
[docs] def scoped_hit(entry: tuple[Any, Any, _T] | None, key: Any) -> TypeGuard[tuple[Any, Any, _T]]: """Return if *entry* holds *key* and, when scoped, belongs to the current trace.""" return (entry is not None and entry[0] == key and (entry[1] is None or entry[1] == get_opaque_trace_state()))
[docs] @dataclass(eq=False) class DeferredValue: """Evaluate captured inputs on demand; cache only concrete values.""" evaluate: Callable[[], Any] = field(repr=False) _value: Any = field(default=None, init=False, repr=False) def __call__(self) -> Any: from quchip.utils.jax_utils import contains_tracer if self._value is None: value = self.evaluate() if not contains_tracer(value): self._value = value return value return self._value