Celsia's picture
Upload folder using huggingface_hub
ecc81b3 verified
Raw
History Blame Contribute Delete
5.11 kB
"""Turning observed coordinates into a lattice.
This is the piece that exists to prevent one specific, silent failure. Given
rows keyed by ``(state, commodity, flow)`` — or any other tuple — a hand-written
mapping from those keys to grid positions is easy to get subtly wrong, and a
mis-shuffled lattice still trains, still converges, and is quietly wrong.
Deriving the mapping once, here, makes that class of bug impossible downstream.
Coordinates may be integers or any hashable categoricals (strings, dates,
tuples). Each axis gets its own vocabulary, sorted where the values are
orderable and first-seen otherwise, so a lattice built twice from the same data
is identical.
"""
from __future__ import annotations
import math
from collections.abc import Sequence
from dataclasses import dataclass
import torch
from torch_dimensions.lattice import Lattice
__all__ = ["CoordMap", "from_coords"]
def _vocab(values: Sequence) -> tuple:
"""Distinct values in a deterministic order."""
seen = list(dict.fromkeys(values))
try:
return tuple(sorted(seen))
except TypeError:
# Mixed or unorderable types: fall back to first appearance, which is
# still deterministic for a given input ordering.
return tuple(seen)
@dataclass
class CoordMap:
"""The lattice implied by a set of observed coordinates, plus the mapping
back and forth."""
lattice: Lattice
vocabs: tuple[tuple, ...]
index: torch.Tensor # (N,) flat cell index, one per input row
@property
def names(self) -> tuple[str, ...]:
assert self.lattice.names is not None
return self.lattice.names
def encode(self, coords: Sequence[Sequence]) -> torch.Tensor:
"""Map coordinate tuples to flat cell indices, for data the lattice was
not built from. Unknown values raise rather than silently landing in
cell zero."""
lookups = [{v: i for i, v in enumerate(v_)} for v_ in self.vocabs]
strides = _strides(self.lattice.shape)
out = []
for row in coords:
if len(row) != len(self.vocabs):
raise ValueError(f"expected {len(self.vocabs)} coordinates, got {len(row)}")
flat = 0
for k, (value, lookup) in enumerate(zip(row, lookups, strict=True)):
if value not in lookup:
raise KeyError(f"{value!r} is not a known {self.names[k]!r} value")
flat += lookup[value] * strides[k]
out.append(flat)
return torch.tensor(out, dtype=torch.long)
def decode(self, flat: int) -> tuple:
"""Flat cell index back to the original coordinate values."""
out = []
for size, vocab in zip(reversed(self.lattice.shape), reversed(self.vocabs), strict=True):
flat, r = divmod(flat, size)
out.append(vocab[r])
return tuple(reversed(out))
def _strides(shape: Sequence[int]) -> list[int]:
strides, acc = [], 1
for size in reversed(shape):
strides.append(acc)
acc *= size
return list(reversed(strides))
def from_coords(
coords: Sequence[Sequence],
names: Sequence[str] | None = None,
*,
dense: bool = False,
time: bool = True,
) -> CoordMap:
"""Infer a lattice from observed coordinate tuples.
Args:
coords: ``(N, K)`` — one tuple per observation. Rows may repeat.
names: axis names; defaults to ``dim0..dimK``.
dense: treat every combination as present. By default a combination
that never appears is marked absent, which is the honest reading of
"we have no data for it" and is what lets the model mask it.
time: give the lattice a time axis. On by default because coordinates
almost always index a series, but a static grid is legitimate.
Returns:
A :class:`CoordMap` whose ``.lattice`` is ready to hand to a model.
"""
if isinstance(coords, torch.Tensor):
coords = coords.tolist()
rows = [tuple(r) for r in coords]
if not rows:
raise ValueError("need at least one coordinate row")
k = len(rows[0])
if any(len(r) != k for r in rows):
raise ValueError("all coordinate rows must have the same length")
vocabs = tuple(_vocab([r[axis] for r in rows]) for axis in range(k))
shape = tuple(len(v) for v in vocabs)
lookups = [{v: i for i, v in enumerate(v_)} for v_ in vocabs]
strides = _strides(shape)
index = torch.tensor(
[sum(lookups[a][r[a]] * strides[a] for a in range(k)) for r in rows],
dtype=torch.long,
)
valid = None
if not dense:
flags = torch.zeros(math.prod(shape), dtype=torch.bool)
flags[index] = True
# Every combination observed means the grid really is dense; saying so
# is more honest than carrying an all-True mask around.
valid = None if bool(flags.all()) else flags.reshape(shape)
lattice = Lattice(shape=shape, names=tuple(names) if names else None, valid=valid, time=time)
return CoordMap(lattice=lattice, vocabs=vocabs, index=index)