Skip to content

API

Tinymesh exposes direct objects over tinygrad tensors. There is no model factory, trainer, or PyTorch compatibility layer.

Core

Graph dataclass

Graph(nodes: int, source: list[int] | tuple[int, ...], target: list[int] | tuple[int, ...])

An immutable directed graph over ordinary tinygrad tensors.

Methods:

  • cartesian

    Return the directed Cartesian product over left-major node pairs.

  • edge_values

    Gather node values into original COO edge order.

  • in_degree

    Return incoming degree on one device.

  • mean

    Mean incoming values over node axis -2, with zero for empty rows.

  • softmax

    Normalize scalar edge scores over each target's incoming edges.

  • sum

    Sum incoming values over node axis -2 with optional shared edge weights.

  • sum_edges

    Sum COO-ordered edge values at their target nodes over axis -2.

Source code in src/tinymesh/graph.py
def __init__(
    self,
    nodes: int,
    source: list[int] | tuple[int, ...],
    target: list[int] | tuple[int, ...],
) -> None:
    source, target = tuple(source), tuple(target)
    if nodes <= 0:
        raise ValueError("nodes must be positive")
    if len(source) != len(target):
        raise ValueError("source and target must have the same length")
    if any(node < 0 or node >= nodes for edge in (source, target) for node in edge):
        raise ValueError(f"node IDs must be in [0, {nodes})")

    object.__setattr__(self, "nodes", nodes)
    object.__setattr__(self, "source", source)
    object.__setattr__(self, "target", target)
    object.__setattr__(self, "_csr", _CSR(nodes, source, target))

cartesian

cartesian(other: Graph) -> Graph

Return the directed Cartesian product over left-major node pairs.

Left-factor edges precede right-factor edges in the returned COO order. The one-node edgeless graph is an exact identity. Products are associative as edge multisets; regrouping factors can permute COO edge order.

Source code in src/tinymesh/graph.py
def cartesian(self, other: Graph) -> Graph:
    """Return the directed Cartesian product over left-major node pairs.

    Left-factor edges precede right-factor edges in the returned COO order.
    The one-node edgeless graph is an exact identity. Products are associative
    as edge multisets; regrouping factors can permute COO edge order.
    """
    return Graph(
        self.nodes * other.nodes,
        [source * other.nodes + node for source in self.source for node in range(other.nodes)]
        + [node * other.nodes + source for node in range(self.nodes) for source in other.source],
        [target * other.nodes + node for target in self.target for node in range(other.nodes)]
        + [node * other.nodes + target for node in range(self.nodes) for target in other.target],
    )

edge_values

edge_values(values: Tensor, *, endpoint: Literal['source', 'target']) -> Tensor

Gather node values into original COO edge order.

Source code in src/tinymesh/graph.py
def edge_values(
    self,
    values: Tensor,
    *,
    endpoint: Literal["source", "target"],
) -> Tensor:
    """Gather node values into original COO edge order."""
    self._validate_node_values(values)
    if endpoint not in ("source", "target"):
        raise ValueError("endpoint must be 'source' or 'target'")

    shape = values.shape
    order = (values.ndim - 2, *range(values.ndim - 2), values.ndim - 1)
    flat = values.permute(order).reshape(self.nodes, -1)
    output = self._csr.edge_values(flat, source=endpoint == "source")
    return output.reshape(self.edges, *shape[:-2], shape[-1]).permute(
        *range(1, values.ndim - 1),
        0,
        values.ndim - 1,
    )

in_degree

in_degree(*, device: str) -> Tensor

Return incoming degree on one device.

Source code in src/tinymesh/graph.py
def in_degree(self, *, device: str) -> Tensor:
    """Return incoming degree on one device."""
    if not isinstance(device, str):
        raise ValueError("in_degree requires one device")
    return self._csr.in_degree(device)

mean

mean(values: Tensor) -> Tensor

Mean incoming values over node axis -2, with zero for empty rows.

Source code in src/tinymesh/graph.py
def mean(self, values: Tensor) -> Tensor:
    """Mean incoming values over node axis -2, with zero for empty rows."""
    self._validate_node_values(values)
    if not dtypes.is_float(values.dtype):
        raise ValueError(f"mean values must have a floating dtype, got {values.dtype}")
    assert isinstance(values.device, str)
    degree = self.in_degree(device=values.device).maximum(1).cast(values.dtype)
    degree = degree.reshape((1,) * (values.ndim - 2) + (self.nodes, 1))
    return self.sum(values) / degree

softmax

softmax(edge_score: Tensor) -> Tensor

Normalize scalar edge scores over each target's incoming edges.

Source code in src/tinymesh/graph.py
def softmax(self, edge_score: Tensor) -> Tensor:
    """Normalize scalar edge scores over each target's incoming edges."""
    if edge_score.ndim != 1 or edge_score.shape[0] != self.edges:
        raise ValueError(f"edge_score must have shape [{self.edges}], got {edge_score.shape}")
    if not dtypes.is_float(edge_score.dtype):
        raise ValueError(f"edge_score must have a floating dtype, got {edge_score.dtype}")
    if not isinstance(edge_score.device, str):
        raise ValueError("graph softmax requires one device")
    return self._csr.softmax(edge_score)

sum

sum(values: Tensor, edge_weight: Tensor | None = None) -> Tensor

Sum incoming values over node axis -2 with optional shared edge weights.

Source code in src/tinymesh/graph.py
def sum(self, values: Tensor, edge_weight: Tensor | None = None) -> Tensor:
    """Sum incoming values over node axis -2 with optional shared edge weights."""
    self._validate_node_values(values)
    if edge_weight is not None:
        if edge_weight.ndim != 1 or edge_weight.shape[0] != self.edges:
            raise ValueError(f"edge_weight must have shape [{self.edges}], got {edge_weight.shape}")
        if values.dtype != edge_weight.dtype:
            raise ValueError(f"values and edge_weight must have the same dtype, got {values.dtype} and {edge_weight.dtype}")
        if edge_weight.device != values.device:
            raise ValueError("weighted graph sum requires one shared device")

    shape = values.shape
    order = (values.ndim - 2, *range(values.ndim - 2), values.ndim - 1)
    flat = values.permute(order).reshape(self.nodes, -1)
    output = self._csr.sum(flat) if edge_weight is None else self._csr.weighted_sum(flat, edge_weight)
    return output.reshape(self.nodes, *shape[:-2], shape[-1]).permute(
        *range(1, values.ndim - 1),
        0,
        values.ndim - 1,
    )

sum_edges

sum_edges(values: Tensor) -> Tensor

Sum COO-ordered edge values at their target nodes over axis -2.

Source code in src/tinymesh/graph.py
def sum_edges(self, values: Tensor) -> Tensor:
    """Sum COO-ordered edge values at their target nodes over axis -2."""
    if values.ndim < 2:
        raise ValueError(f"values must have shape [..., E, H], got {values.shape}")
    if values.shape[-2] != self.edges:
        raise ValueError(f"values must have {self.edges} edge rows, got {values.shape[-2]}")
    if not isinstance(values.device, str):
        raise ValueError("edge values require one device")

    shape = values.shape
    order = (values.ndim - 2, *range(values.ndim - 2), values.ndim - 1)
    flat = values.permute(order).reshape(self.edges, prod(shape[:-2]) * shape[-1])
    output = self._csr._segment_sum(flat)
    return output.reshape(self.nodes, *shape[:-2], shape[-1]).permute(
        *range(1, values.ndim - 1),
        0,
        values.ndim - 1,
    )

StaticGraphTemporalSignal dataclass

StaticGraphTemporalSignal(graph: Graph, node_ids: tuple[str, ...], x: Tensor, y: Tensor, edge_weight: Tensor | None = None)

Bases: Sequence[tuple[Tensor, Tensor]]

An ordered tensor signal over one immutable graph.

Methods:

  • batches

    Yield causal sequence-to-one windows as [B, L, N, F] and [B, N, Y].

  • split

    Split once along time, preserving order and topology.

batches

batches(*, batch_size: int, history: int) -> Iterator[tuple[Tensor, Tensor]]

Yield causal sequence-to-one windows as [B, L, N, F] and [B, N, Y].

Source code in src/tinymesh/temporal.py
def batches(self, *, batch_size: int, history: int) -> Iterator[tuple[Tensor, Tensor]]:
    """Yield causal sequence-to-one windows as [B, L, N, F] and [B, N, Y]."""
    for name, value in (("batch_size", batch_size), ("history", history)):
        if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
            raise ValueError(f"{name} must be a positive integer")
    if history > len(self):
        raise ValueError(f"history must not exceed the {len(self)} time steps")

    windows = len(self) - history + 1
    for start in range(0, windows, batch_size):
        stop = min(start + batch_size, windows)
        yield (
            Tensor.stack(
                *(self.x[start + lag:stop + lag] for lag in range(history)),
                dim=1,
            ),
            self.y[start + history - 1:stop + history - 1],
        )

split

split(train_ratio: float) -> tuple[StaticGraphTemporalSignal, StaticGraphTemporalSignal]

Split once along time, preserving order and topology.

Source code in src/tinymesh/temporal.py
def split(self, train_ratio: float) -> tuple[StaticGraphTemporalSignal, StaticGraphTemporalSignal]:
    """Split once along time, preserving order and topology."""
    if not 0 < train_ratio < 1:
        raise ValueError("train_ratio must be between zero and one")
    train_steps = int(train_ratio * len(self))
    if train_steps == 0 or train_steps == len(self):
        raise ValueError("train and test must both be non-empty")
    return self[:train_steps], self[train_steps:]

TemporalEdges dataclass

TemporalEdges(nodes: int, source: tuple[int, ...], target: tuple[int, ...], timestamp: tuple[int, ...])

Timestamped directed edges over one stable node universe.

Methods:

  • prefix

    Return the events with timestamps strictly before cutoff.

prefix

prefix(cutoff: int) -> TemporalEdges

Return the events with timestamps strictly before cutoff.

Source code in src/tinymesh/temporal.py
def prefix(self, cutoff: int) -> TemporalEdges:
    """Return the events with timestamps strictly before ``cutoff``."""
    if not isinstance(cutoff, int) or isinstance(cutoff, bool):
        raise ValueError("cutoff must be an integer timestamp")
    stop = bisect_left(self.timestamp, cutoff)
    return TemporalEdges(
        self.nodes,
        self.source[:stop],
        self.target[:stop],
        self.timestamp[:stop],
    )

Neural networks

SAGEConv

SAGEConv(in_features: int, out_features: int, bias: bool = True)

Mean GraphSAGE over one homogeneous graph.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, in_features: int, out_features: int, bias: bool = True) -> None:
    self.neighbor = nn.Linear(in_features, out_features, bias=bias)
    self.root = nn.Linear(in_features, out_features, bias=False)

GINEConv

GINEConv(node_features: int, edge_features: int, out_features: int, eps: float = 0.0)

Edge-aware graph isomorphism convolution over shared edge features.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, node_features: int, edge_features: int, out_features: int, eps: float = 0.0) -> None:
    if node_features <= 0 or edge_features <= 0 or out_features <= 0:
        raise ValueError("feature counts must be positive")
    self.edge = nn.Linear(edge_features, node_features)
    self.hidden = nn.Linear(node_features, out_features)
    self.output = nn.Linear(out_features, out_features)
    self.node_features, self.edge_features, self.eps = node_features, edge_features, eps

GCNConv

GCNConv(in_features: int, out_features: int, bias: bool = True)

Unweighted GCN over caller-supplied edges and self-loops.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, in_features: int, out_features: int, bias: bool = True) -> None:
    self.linear = nn.Linear(in_features, out_features, bias=bias)

GATConv

GATConv(in_features: int, out_features: int, heads: int = 1, negative_slope: float = 0.2, bias: bool = True)

Graph attention with independently normalized concatenated heads.

Source code in src/tinymesh/nn/__init__.py
def __init__(
    self,
    in_features: int,
    out_features: int,
    heads: int = 1,
    negative_slope: float = 0.2,
    bias: bool = True,
) -> None:
    if heads <= 0:
        raise ValueError("heads must be positive")
    if negative_slope < 0:
        raise ValueError("negative_slope must be non-negative")
    self.linear = nn.Linear(in_features, heads * out_features, bias=False)
    bound = 1 / sqrt(out_features)
    self.source_attention = Tensor.uniform(heads, out_features, low=-bound, high=bound)
    self.target_attention = Tensor.uniform(heads, out_features, low=-bound, high=bound)
    self.bias: Tensor | None = Tensor.zeros(heads * out_features) if bias else None
    self.heads, self.out_features = heads, out_features
    self.negative_slope = negative_slope

ChebConv

ChebConv(in_features: int, out_features: int, order: int)

Chebyshev graph convolution for symmetric, loop-free unit edges.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, in_features: int, out_features: int, order: int) -> None:
    if in_features <= 0 or out_features <= 0 or order <= 0:
        raise ValueError("feature counts and order must be positive")
    self.linear = nn.Linear(order * in_features, out_features)
    self.in_features, self.order = in_features, order

TGCN

TGCN(in_features: int, hidden_features: int)

One temporal graph convolutional recurrent step.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, in_features: int, hidden_features: int) -> None:
    if in_features <= 0 or hidden_features <= 0:
        raise ValueError("feature counts must be positive")
    self.graph_projection = GCNConv(in_features, 3 * hidden_features, bias=False)
    self.update = nn.Linear(2 * hidden_features, hidden_features)
    self.reset = nn.Linear(2 * hidden_features, hidden_features)
    self.candidate = nn.Linear(2 * hidden_features, hidden_features)
    self.hidden_features = hidden_features

PeriodAttention

PeriodAttention(periods: int)

Learned convex mixture over a fixed number of same-shaped states.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, periods: int) -> None:
    if periods <= 0:
        raise ValueError("periods must be positive")
    self.weight = Tensor.uniform(periods)
    self.periods = periods

A3TGCN

A3TGCN(in_features: int, hidden_features: int, periods: int)

Attention over T-GCN encodings of a fixed number of periods.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, in_features: int, hidden_features: int, periods: int) -> None:
    self.cell = TGCN(in_features, hidden_features)
    self.attention = PeriodAttention(periods)
    self.in_features, self.hidden_features, self.periods = in_features, hidden_features, periods

GConvGRU

GConvGRU(in_features: int, hidden_features: int, order: int)

One Chebyshev graph-convolutional recurrent step.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, in_features: int, hidden_features: int, order: int) -> None:
    if in_features <= 0 or hidden_features <= 0:
        raise ValueError("feature counts must be positive")
    self.gates = ChebConv(in_features + hidden_features, 2 * hidden_features, order)
    self.candidate = ChebConv(in_features + hidden_features, hidden_features, order)
    self.in_features, self.hidden_features = in_features, hidden_features

DirectedDiffusion

DirectedDiffusion(graph: Graph, affinity: Tensor)

Bidirectional propagation for caller-validated positive affinity.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, graph: Graph, affinity: Tensor) -> None:
    if affinity.ndim != 1 or affinity.shape[0] != graph.edges:
        raise ValueError(f"affinity must have shape [{graph.edges}], got {affinity.shape}")
    if not dtypes.is_float(affinity.dtype):
        raise ValueError(f"affinity must have a floating dtype, got {affinity.dtype}")
    if not isinstance(affinity.device, str):
        raise ValueError("directed diffusion requires one device")

    self.graph = graph
    self.reverse = Graph(graph.nodes, graph.target, graph.source)
    one = Tensor.ones(graph.nodes, 1, dtype=affinity.dtype, device=affinity.device)
    outgoing = self.reverse.sum(one, edge_weight=affinity)
    incoming = graph.sum(one, edge_weight=affinity)
    self.forward_weight = affinity / graph.edge_values(outgoing, endpoint="source").flatten()
    self.reverse_weight = affinity / graph.edge_values(incoming, endpoint="target").flatten()

DiffusionGRU

DiffusionGRU(in_features: int, hidden_features: int)

One gated recurrent step over bidirectional directed diffusion.

Source code in src/tinymesh/nn/__init__.py
def __init__(self, in_features: int, hidden_features: int) -> None:
    if in_features <= 0 or hidden_features <= 0:
        raise ValueError("feature counts must be positive")
    width = 3 * (in_features + hidden_features)
    self.gates = nn.Linear(width, 2 * hidden_features)
    self.candidate = nn.Linear(width, hidden_features)
    self.in_features, self.hidden_features = in_features, hidden_features

Datasets

chickenpox

chickenpox(path: str | Path | None = None, *, lags: int = 4, device: str | None = None) -> StaticGraphTemporalSignal

Load the PyG Temporal Hungary chickenpox signal.

Source code in src/tinymesh/datasets.py
def chickenpox(
    path: str | Path | None = None,
    *,
    lags: int = 4,
    device: str | None = None,
) -> StaticGraphTemporalSignal:
    """Load the PyG Temporal Hungary chickenpox signal."""
    if not isinstance(lags, int) or isinstance(lags, bool) or lags <= 0:
        raise ValueError("lags must be a positive integer")
    source = Path(path) if path is not None else fetch(_CHICKENPOX_URL, sha256=_CHICKENPOX_SHA256)
    data = json.loads(source.read_bytes())
    edges, node_ids, values = _parse_chickenpox(data)
    if lags >= len(values):
        raise ValueError(f"lags must be smaller than the {len(values)} time steps")

    x = Tensor(
        [
            [[values[time + lag][node] for lag in range(lags)] for node in range(len(node_ids))]
            for time in range(len(values) - lags)
        ],
        device=device,
    ).realize()
    y = Tensor(
        [
            [[values[time + lags][node]] for node in range(len(node_ids))]
            for time in range(len(values) - lags)
        ],
        device=device,
    ).realize()
    graph = Graph(
        len(node_ids),
        [source for source, _ in edges],
        [target for _, target in edges],
    )
    edge_weight = Tensor.ones(graph.edges, dtype=x.dtype, device=x.device).realize()
    return StaticGraphTemporalSignal(graph, node_ids, x, y, edge_weight)

MontevideoBus dataclass

MontevideoBus(signal: StaticGraphTemporalSignal, position: Tensor, road_distance: Tensor)

The aligned PyG Temporal Montevideo bus signal.

montevideo_bus

montevideo_bus(path: str | Path | None = None, *, lags: int = 4, device: str | None = None) -> MontevideoBus

Load the PyG Temporal Montevideo bus signal without normalization.

Source code in src/tinymesh/datasets.py
def montevideo_bus(
    path: str | Path | None = None,
    *,
    lags: int = 4,
    device: str | None = None,
) -> MontevideoBus:
    """Load the PyG Temporal Montevideo bus signal without normalization."""
    if not isinstance(lags, int) or isinstance(lags, bool) or lags <= 0:
        raise ValueError("lags must be a positive integer")
    source = _read_montevideo(path)
    steps = len(source.features[0])
    if lags >= steps:
        raise ValueError(f"lags must be smaller than the {steps} time steps")

    features = Tensor(source.features, device=device).T
    targets = Tensor(source.targets, dtype=features.dtype, device=features.device).T
    snapshots = steps - lags
    x = Tensor.stack(*(features[lag:lag + snapshots] for lag in range(lags)), dim=2).realize()
    y = targets[lags:].unsqueeze(2).realize()
    graph = Graph(len(source.node_ids), source.source, source.target)
    signal = StaticGraphTemporalSignal(graph, tuple(str(node_id) for node_id in source.node_ids), x, y)
    position = Tensor(source.position, dtype=x.dtype, device=x.device).realize()
    road_distance = Tensor(source.road_distance, dtype=x.dtype, device=x.device).realize()
    return MontevideoBus(signal, position, road_distance)

METRLA dataclass

METRLA(graph: Graph, sensor_ids: tuple[str, ...], timestamps: tuple[datetime, ...], speed: Tensor, affinity: Tensor)

Raw METR-LA traffic speed aligned with the directed DCRNN graph.

Attributes:

  • observed (Tensor) –

    Mask the zero sentinel used by the reference METR-LA protocol.

observed property

observed: Tensor

Mask the zero sentinel used by the reference METR-LA protocol.

metr_la

metr_la(path: str | Path | None = None, *, device: str | None = None) -> METRLA

Load raw METR-LA speed and reproduce the directed DCRNN affinity.

Source code in src/tinymesh/datasets.py
def metr_la(
    path: str | Path | None = None,
    *,
    device: str | None = None,
) -> METRLA:
    """Load raw METR-LA speed and reproduce the directed DCRNN affinity."""
    traffic_path, sensor_path, distance_path = _metr_la_sources(path)
    sensor_ids = _read_sensor_ids(sensor_path)
    timestamps, values = _read_traffic(traffic_path, sensor_ids)
    graph, affinity_values = _read_road_graph(distance_path, sensor_ids)
    speed = _float_tensor(values, (len(timestamps), len(sensor_ids)), device)
    if not isinstance(speed.device, str):
        raise ValueError("METR-LA requires one device")
    affinity = _float_tensor(affinity_values, (graph.edges,), speed.device)
    return METRLA(graph, sensor_ids, timestamps, speed, affinity)

MUTAG dataclass

MUTAG(graphs: tuple[Graph, ...], node_labels: tuple[Tensor, ...], edge_labels: tuple[Tensor, ...], labels: tuple[int, ...])

MUTAG molecular graphs with categorical atom, bond, and graph labels.

mutag

mutag(path: str | Path | None = None, *, device: str | None = None) -> MUTAG

Load the TU Dortmund MUTAG molecular graph collection.

Source code in src/tinymesh/datasets.py
def mutag(path: str | Path | None = None, *, device: str | None = None) -> MUTAG:
    """Load the TU Dortmund MUTAG molecular graph collection."""
    try:
        with ZipFile(BytesIO(_read_mutag(path))) as archive:
            return _parse_mutag(archive, device)
    except BadZipFile as error:
        raise ValueError("MUTAG source must be a ZIP archive") from error

CollegeMsg dataclass

CollegeMsg(events: TemporalEdges, node_ids: tuple[int, ...])

Directed private-message events with retained source node identities.

college_msg

college_msg(path: str | Path | None = None) -> CollegeMsg

Load the checksum-pinned CollegeMsg temporal interaction stream.

Source code in src/tinymesh/datasets.py
def college_msg(path: str | Path | None = None) -> CollegeMsg:
    """Load the checksum-pinned CollegeMsg temporal interaction stream."""
    source = _fetch_source(_COLLEGE_MSG_SOURCE) if path is None else Path(path)
    payload = source.read_bytes()
    if len(payload) != _COLLEGE_MSG_SOURCE[2] or hashlib.sha256(payload).hexdigest() != _COLLEGE_MSG_SOURCE[3]:
        raise ValueError("CollegeMsg source identity mismatch")
    try:
        with gzip.open(source, "rt", encoding="ascii") as rows:
            raw = _parse_college_msg(rows)
    except (gzip.BadGzipFile, UnicodeDecodeError, OSError) as error:
        raise ValueError("CollegeMsg source must be an ASCII gzip") from error

    node_ids = tuple(sorted({node for source, target, _ in raw for node in (source, target)}))
    node_index = {node_id: index for index, node_id in enumerate(node_ids)}
    return CollegeMsg(
        TemporalEdges(
            len(node_ids),
            tuple(node_index[source] for source, _, _ in raw),
            tuple(node_index[target] for _, target, _ in raw),
            tuple(timestamp for _, _, timestamp in raw),
        ),
        node_ids,
    )