From 8706aaf071d33f6e7786da73b2141fbbb53a10f2 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 30 Apr 2026 23:15:30 +0900 Subject: [PATCH 01/45] feat: add partitioned Clifford algebra kernel --- core/__init__.py | 2 + core/partitioned_algebra.py | 1638 +++++++++++++++++++++++++++++++++++ 2 files changed, 1640 insertions(+) create mode 100644 core/partitioned_algebra.py diff --git a/core/__init__.py b/core/__init__.py index 5b13bda..e8bd29c 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -37,12 +37,14 @@ signature_trace_form, ) from .multivector import Multivector +from .partitioned_algebra import PartitionedCliffordAlgebra from .validation import check_channels, check_multivector __all__ = [ # algebra "CliffordAlgebra", "Multivector", + "PartitionedCliffordAlgebra", # device / validation "DeviceConfig", "resolve_device", diff --git a/core/partitioned_algebra.py b/core/partitioned_algebra.py new file mode 100644 index 0000000..4c45b87 --- /dev/null +++ b/core/partitioned_algebra.py @@ -0,0 +1,1638 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Recursive Clifford algebra kernel for high-dimensional products. + +The dense :class:`core.algebra.CliffordAlgebra` stores a full Cayley table and +is therefore practical only for small dimensions. ``PartitionedCliffordAlgebra`` +keeps that dense implementation as the leaf kernel, while internal nodes factor +the basis into left and right sub-algebras: + +``split_index = (left_index << right_n) | right_index``. + +The public basis order is always the canonical bitmask order used by +``CliffordAlgebra``. Some recursive splits use a different internal bit order so +that repeated signature tiles, such as two copies of ``Cl(2,1,1)``, share a +single sub-algebra object. ``_BasisPermutation`` is the only place that converts +between public coefficients and that split-local coefficient order. +""" + +import math +from dataclasses import dataclass +from math import gcd +from typing import Optional, Sequence + +import torch +import torch.nn as nn + +from core.algebra import CliffordAlgebra +from core.validation import check_multivector + +_MAX_LEFT_MATRIX_LEAF_N = 6 + + +def _signature_for_range(p: int, q: int, r: int, start: int, width: int) -> tuple[int, int, int]: + """Return ``(p, q, r)`` counts covered by a contiguous public bit range.""" + end = start + width + p_count = max(0, min(end, p) - start) + q_count = max(0, min(end, p + q) - max(start, p)) + r_count = max(0, min(end, p + q + r) - max(start, p + q)) + assert p_count + q_count + r_count == width + return p_count, q_count, r_count + + +def _signature_gcd(p: int, q: int, r: int) -> int: + """Return the greatest common divisor across nonzero signature blocks. + + A value larger than one means the signature can be represented as repeated + copies of a smaller signature tile. Example: ``Cl(8,4,4)`` has gcd 4, so it + can be split into repeated ``Cl(2,1,1)`` tiles. + """ + counts = [count for count in (p, q, r) if count > 0] + if not counts: + return 1 + + result = counts[0] + for count in counts[1:]: + result = gcd(result, count) + return result + + +def _signature_prefix_dims( + p: int, + q: int, + r: int, + p_count: int, + q_count: int, + r_count: int, +) -> tuple[list[int], list[int]]: + """Return selected and remaining public bit positions by signature block. + + The selected bits are prefixes of each signature block: first positive + dimensions, then negative dimensions, then null dimensions. This preserves + the local ``(p, q, r)`` order inside a tiled child while still letting the + child draw dimensions from non-contiguous public bit positions. + """ + assert 0 <= p_count <= p + assert 0 <= q_count <= q + assert 0 <= r_count <= r + + selected = list(range(0, p_count)) + list(range(p, p + q_count)) + list(range(p + q, p + q + r_count)) + remaining = list(range(p_count, p)) + list(range(p + q_count, p + q)) + list(range(p + q + r_count, p + q + r)) + + return selected, remaining + + +@dataclass(frozen=True) +class _PartitionSplit: + """Child signatures and public bit positions for one recursive split. + + ``right_dims`` become the low split-local bits and ``left_dims`` become the + high split-local bits. The order is important because recursive products use + ``(left_index << right_n) | right_index`` after converting into split order. + """ + + right_signature: tuple[int, int, int] + left_signature: tuple[int, int, int] + right_dims: tuple[int, ...] + left_dims: tuple[int, ...] + + @property + def split_dims(self) -> tuple[int, ...]: + """Return public bit positions in split-order bit layout.""" + return self.right_dims + self.left_dims + + +def _partition_split(p: int, q: int, r: int) -> _PartitionSplit: + """Return the single recursive split used by the partitioned algebra. + + Repeated signature tiles are grouped first so common sub-algebras have the + same local signature and can share one module instance. Signatures without + repeatable tiles fall back to a balanced contiguous split. + """ + n = p + q + r + + tile_count = _signature_gcd(p, q, r) + if tile_count > 1: + tile_p = p // tile_count + tile_q = q // tile_count + tile_r = r // tile_count + right_tile_count = tile_count // 2 + + right_p = tile_p * right_tile_count + right_q = tile_q * right_tile_count + right_r = tile_r * right_tile_count + + right_dims, left_dims = _signature_prefix_dims( + p, + q, + r, + right_p, + right_q, + right_r, + ) + left_p = p - right_p + left_q = q - right_q + left_r = r - right_r + return _PartitionSplit( + right_signature=(right_p, right_q, right_r), + left_signature=(left_p, left_q, left_r), + right_dims=tuple(right_dims), + left_dims=tuple(left_dims), + ) + + right_width = n // 2 + left_width = n - right_width + right_p, right_q, right_r = _signature_for_range(p, q, r, 0, right_width) + left_p, left_q, left_r = _signature_for_range(p, q, r, right_width, left_width) + right_dims = tuple(range(right_width)) + left_dims = tuple(range(right_width, n)) + + return _PartitionSplit( + right_signature=(right_p, right_q, right_r), + left_signature=(left_p, left_q, left_r), + right_dims=right_dims, + left_dims=left_dims, + ) + + +def _grade_index(n: int, device) -> torch.Tensor: + """Return the grade, i.e. popcount, for basis indices ``0..2**n-1``.""" + basis_indices = torch.arange(2**n, dtype=torch.long, device=device) + grades = torch.zeros_like(basis_indices) + remaining_bits = basis_indices + for _ in range(n): + grades += remaining_bits & 1 + remaining_bits = remaining_bits >> 1 + return grades + + +def _subalgebra_cache_key( + p: int, + q: int, + r: int, + device, + dtype: torch.dtype, + leaf_n: int, + product_chunk_size: Optional[int], + exp_policy, + fixed_iterations: int, + accumulation_dtype: Optional[torch.dtype], +) -> tuple: + """Return the per-tree cache key for structurally identical sub-algebras.""" + return ( + p, + q, + r, + str(torch.device(device)), + str(dtype), + leaf_n, + product_chunk_size, + getattr(exp_policy, "value", str(exp_policy)), + fixed_iterations, + str(accumulation_dtype), + ) + + +def _basis_product_signs( + indices_a: torch.Tensor, + indices_b: torch.Tensor, + p: int, + q: int, + r: int, + dtype: torch.dtype, +) -> torch.Tensor: + """Return basis-product signs for equal-shaped bitmask index tensors. + + The output is the scalar coefficient of ``e_indices_a * e_indices_b`` before + the XOR result index is applied. Positive dimensions contribute ``+1``, + negative dimensions contribute ``-1`` when repeated, and null dimensions + annihilate products that repeat the same null basis vector. + """ + n = p + q + r + popcount = _grade_index(n, indices_a.device) + + # Reordering ``A`` basis vectors past lower-numbered ``B`` basis vectors + # gives the anticommutation sign. + swap_counts = torch.zeros_like(indices_a) + for bit in range(n): + a_bit = (indices_a >> bit) & 1 + lower_bits = indices_b & ((1 << bit) - 1) + swap_counts += a_bit * popcount[lower_bits] + + sign = torch.where( + swap_counts % 2 == 0, + torch.ones((), dtype=dtype, device=indices_a.device), + -torch.ones((), dtype=dtype, device=indices_a.device), + ) + + # Repeated negative basis vectors square to -1. + negative_mask = 0 + for bit in range(p, p + q): + negative_mask |= 1 << bit + negative_intersection = indices_a & indices_b & negative_mask + negative_count = popcount[negative_intersection] + sign = torch.where(negative_count % 2 == 0, sign, -sign) + + if r > 0: + # Repeated null basis vectors square to 0, annihilating the term. + null_mask = 0 + for bit in range(p + q, n): + null_mask |= 1 << bit + sign = torch.where((indices_a & indices_b & null_mask) == 0, sign, torch.zeros_like(sign)) + + return sign + + +class _BasisPermutation(nn.Module): + """Convert coefficients between public canonical order and split-local order. + + ``split_dims[split_bit]`` tells which public basis-vector bit occupies that + split-local bit position. For contiguous balanced splits this is the identity + mapping and all buffers stay empty. For tiled splits, bit positions are + permuted so identical child signatures can share sub-algebra modules. + + Coefficients need signs as well as index permutations. A basis blade stores + an ordered wedge of basis vectors; permuting vector dimensions changes that + orientation by ``(-1) ** inversion_count``. + """ + + def __init__(self, split_dims: Sequence[int], device): + super().__init__() + self.split_dims = tuple(split_dims) + self.n = len(self.split_dims) + self.dim = 2**self.n + self.uses_permutation = self.split_dims != tuple(range(self.n)) + + if not self.uses_permutation: + # Keep the identity case allocation-free in the hot path. Empty + # buffers preserve old introspection behavior and still move with + # ``module.to(device)``. + empty = torch.empty(0, dtype=torch.long, device=device) + self.register_buffer("split_to_public", empty, persistent=False) + self.register_buffer("public_to_split", empty, persistent=False) + self.register_buffer("split_signs", torch.empty(0, dtype=torch.int8, device=device), persistent=False) + return + + split_to_public_indices = [] + split_orientation_signs = [] + for split_index in range(self.dim): + public_index, orientation_sign = self._split_basis_term(split_index) + split_to_public_indices.append(public_index) + split_orientation_signs.append(orientation_sign) + + split_to_public = torch.tensor(split_to_public_indices, dtype=torch.long, device=device) + public_to_split = torch.empty_like(split_to_public) + public_to_split[split_to_public] = torch.arange(self.dim, dtype=torch.long, device=device) + split_signs = torch.tensor(split_orientation_signs, dtype=torch.int8, device=device) + + self.register_buffer("split_to_public", split_to_public, persistent=False) + self.register_buffer("public_to_split", public_to_split, persistent=False) + self.register_buffer("split_signs", split_signs, persistent=False) + + def _split_basis_term(self, split_index: int) -> tuple[int, int]: + """Return ``(public_index, orientation_sign)`` for one split-order blade.""" + public_index = 0 + public_bits = [] + for split_bit, public_bit in enumerate(self.split_dims): + if split_index & (1 << split_bit): + public_index |= 1 << public_bit + public_bits.append(public_bit) + + # ``public_bits`` are encountered in split-local order. Count how many + # swaps are needed to rewrite the same blade in canonical public order. + inversions = 0 + for i, public_i in enumerate(public_bits): + for public_j in public_bits[i + 1 :]: + if public_i > public_j: + inversions += 1 + + sign = -1 if inversions % 2 else 1 + return public_index, sign + + def to_split_order(self, mv: torch.Tensor) -> torch.Tensor: + """Convert public canonical coefficients to split-local coefficient order. + + ``split_to_public[k]`` is the public source index for split coefficient + ``k``. Multiplying by ``split_signs[k]`` accounts for blade orientation. + """ + if not self.uses_permutation: + return mv + signs = self.split_signs.to(dtype=mv.dtype) + return torch.index_select(mv, -1, self.split_to_public) * signs + + def to_public_order(self, mv: torch.Tensor) -> torch.Tensor: + """Convert split-local coefficients back to public canonical order.""" + if not self.uses_permutation: + return mv + signs = torch.index_select(self.split_signs, 0, self.public_to_split).to(dtype=mv.dtype) + return torch.index_select(mv, -1, self.public_to_split) * signs + + +class PartitionedCliffordAlgebra(nn.Module): + """Partitioned Clifford algebra kernel using recursive tensor products. + + The basis order matches :class:`core.algebra.CliffordAlgebra`: basis blades + are indexed by bitmasks, with lower-numbered vector dimensions in lower + bits. Recursive nodes use a binary split, with the low-order internal block + on the right and the high-order internal block on the left: + + ``global_index = (left_index << right_n) | right_index``. + + With this layout, the bridge sign for basis factors is + ``(-1) ** (grade(left_A) * grade(right_B))``. Local metric signs remain in + the left and right subalgebras. + + ``accumulation_dtype`` can promote recursive product accumulation, e.g. + fp32 inputs with fp64 intermediate sums, while returning the input dtype. + + The split algorithm keeps the public basis order canonical but permutes + coefficients internally when repeated signature tiles such as ``Cl(2,1,1)`` + inside ``Cl(8,4,4)`` can share subalgebra modules. + + Args: + p (int): Positive signature dimensions. + q (int, optional): Negative signature dimensions. Defaults to 0. + r (int, optional): Null signature dimensions. Defaults to 0. + device (str or torch.device, optional): Device for generated buffers. + dtype (torch.dtype, optional): Floating-point dtype for sign buffers and + dense leaf algebras. + leaf_n (int, optional): Maximum dimension handled by dense + ``CliffordAlgebra`` leaves. + product_chunk_size (int, optional): Number of right-basis product pairs + processed per recursive chunk. ``None`` chooses a memory-conscious + default from the node shape. + exp_policy (str or ExpPolicy, optional): Bivector exponential policy. + fixed_iterations (int, optional): Fixed iteration budget for decomposed + exponential paths. ``None`` derives it from policy, dtype, and n. + accumulation_dtype (torch.dtype, optional): Optional promoted dtype for + recursive product accumulation. + """ + + def __init__( + self, + p: int, + q: int = 0, + r: int = 0, + device="cuda", + dtype: torch.dtype = torch.float32, + leaf_n: int = 6, + product_chunk_size: Optional[int] = None, + exp_policy: str = "balanced", + fixed_iterations: Optional[int] = None, + accumulation_dtype: Optional[torch.dtype] = None, + _subalgebra_cache: Optional[dict] = None, + ): + super().__init__() + + assert p >= 0, f"p must be non-negative, got {p}" + assert q >= 0, f"q must be non-negative, got {q}" + assert r >= 0, f"r must be non-negative, got {r}" + assert leaf_n >= 1, f"leaf_n must be >= 1, got {leaf_n}" + + self.p, self.q, self.r = p, q, r + self.n = p + q + r + self.dim = 2**self.n + self.leaf_n = leaf_n + self.product_chunk_size = product_chunk_size + self.accumulation_dtype = accumulation_dtype + + # Exp regime: dispatch at init. The branch is signature-wide, so it can + # remain a Python branch without causing data-dependent graph breaks. + if p == 0 or q == 0: + self._exp_regime = "elliptic" + elif p == 1 and q == 1 and r == 0: + self._exp_regime = "hyperbolic" + else: + self._exp_regime = "mixed" + + # Exp policy: controls the decomposition iteration budget used by + # compiled-safe bivector exponentials. + from core.decomposition import ExpPolicy, resolve_fixed_iterations + + self._exp_policy = exp_policy if isinstance(exp_policy, ExpPolicy) else ExpPolicy(exp_policy) + + self._exp_fixed_iterations: int = ( + int(fixed_iterations) + if fixed_iterations is not None + else resolve_fixed_iterations(self._exp_policy, dtype, self.n) + ) + + if _subalgebra_cache is None: + _subalgebra_cache = {} + + grade_index = _grade_index(self.n, device) + self.register_buffer("grade_index", grade_index, persistent=False) + self.register_buffer( + "_grade_values", + torch.arange(self.n + 1, dtype=torch.long, device=device), + persistent=False, + ) + + self._init_structural_buffers(device, dtype) + + # Leaf nodes delegate to the existing dense Cayley-table engine. + if self.n <= leaf_n: + # Leaves operate in public order and therefore use the identity + # permutation. Keeping ``basis_permutation`` present on every node + # lets product code treat leaves and recursive nodes uniformly. + self.basis_permutation = _BasisPermutation(tuple(range(self.n)), device) + self.core = CliffordAlgebra( + p, + q, + r, + device=device, + dtype=dtype, + exp_policy=self._exp_policy, + fixed_iterations=self._exp_fixed_iterations, + ) + self._init_leaf_product_buffers(device, dtype) + self.left_sub = None + self.right_sub = None + self.left_n = 0 + self.right_n = 0 + self.left_dim = 0 + self.right_dim = 0 + self._right_pair_count = 0 + self._product_chunk_size = 0 + self._right_pair_full = False + self._has_sparse_right_interaction = False + self._right_dims = () + self._left_dims = () + return + + split = _partition_split(p, q, r) + right_signature = split.right_signature + left_signature = split.left_signature + + # The split order keeps right-child public coordinates in the low + # internal bits and left-child public coordinates in the high bits: + # split_index = (left_index << right_n) | right_index. + split_order_dims = split.split_dims + assert sorted(split_order_dims) == list(range(self.n)) + + right_n = len(split.right_dims) + left_n = len(split.left_dims) + self.left_n = left_n + self.right_n = right_n + self.left_dim = 2**left_n + self.right_dim = 2**right_n + self._right_dims = split.right_dims + self._left_dims = split.left_dims + self.basis_permutation = _BasisPermutation(split_order_dims, device) + + self.core = None + self.left_sub = self._get_or_create_subalgebra( + *left_signature, + device=device, + dtype=dtype, + leaf_n=leaf_n, + product_chunk_size=product_chunk_size, + exp_policy=self._exp_policy, + fixed_iterations=self._exp_fixed_iterations, + accumulation_dtype=accumulation_dtype, + subalgebra_cache=_subalgebra_cache, + ) + self.right_sub = self._get_or_create_subalgebra( + *right_signature, + device=device, + dtype=dtype, + leaf_n=leaf_n, + product_chunk_size=product_chunk_size, + exp_policy=self._exp_policy, + fixed_iterations=self._exp_fixed_iterations, + accumulation_dtype=accumulation_dtype, + subalgebra_cache=_subalgebra_cache, + ) + + # A left factor from A must cross a right factor from B: + # (L_A R_A)(L_B R_B) = (-1)^(grade(L_A) grade(R_B)) (L_A L_B)(R_A R_B). + left_grade_by_index = _grade_index(left_n, device) + right_grade_by_index = _grade_index(right_n, device) + bridge_signs = torch.where( + (right_grade_by_index.unsqueeze(1) * left_grade_by_index.unsqueeze(0)) % 2 == 0, + torch.ones((), device=device, dtype=torch.int8), + -torch.ones((), device=device, dtype=torch.int8), + ) + self.register_buffer("bridge_signs", bridge_signs, persistent=False) + + self._init_product_buffers() + + def _init_leaf_product_buffers(self, device, dtype: torch.dtype) -> None: + """Precompute a small left-multiplication tensor for dense leaf GP. + + ``CliffordAlgebra.geometric_product`` gathers ``B[..., cayley_indices]``. + In partitioned products, leaves receive large leading dimensions from + right-pair batching, so that gather became the dominant allocation in + profiles. For small leaves we instead build + ``left_gp_mats[i, j, k]`` such that: + + ``(basis_i * basis_j)`` contributes ``left_gp_mats[i, j, k]`` to + output basis ``k``. + + Runtime then contracts ``A`` into a left-multiplication matrix and + applies it to ``B``. The tensor is ``O(dim^3)``, so large user-forced + leaves fall back to the dense core kernel rather than allocating an + unreasonable table. + """ + if self.n > _MAX_LEFT_MATRIX_LEAF_N: + self.register_buffer("_leaf_left_gp_mats", torch.empty(0, dtype=dtype, device=device), persistent=False) + return + + left_gp_mats = torch.zeros(self.dim, self.dim, self.dim, dtype=dtype, device=device) + for left_index in range(self.dim): + for output_index in range(self.dim): + right_index = int(self.core.cayley_indices[left_index, output_index].item()) + left_gp_mats[left_index, right_index, output_index] = self.core.gp_signs[ + left_index, + output_index, + ] + self.register_buffer("_leaf_left_gp_mats", left_gp_mats, persistent=False) + + @classmethod + def _get_or_create_subalgebra( + cls, + p: int, + q: int, + r: int, + *, + device, + dtype: torch.dtype, + leaf_n: int, + product_chunk_size: Optional[int], + exp_policy, + fixed_iterations: int, + accumulation_dtype: Optional[torch.dtype], + subalgebra_cache: dict, + ) -> "PartitionedCliffordAlgebra": + """Create or reuse a child with the same algebraic structure. + + The cache is per root construction call, not global. That keeps module + ownership local to one tree while still ensuring repeated logical + sub-algebras, for example the left and right ``Cl(4,2,2)`` nodes inside + ``Cl(8,4,4)``, point at the same Python module object. + """ + cache_key = _subalgebra_cache_key( + p, + q, + r, + device, + dtype, + leaf_n, + product_chunk_size, + exp_policy, + fixed_iterations, + accumulation_dtype, + ) + subalgebra = subalgebra_cache.get(cache_key) + if subalgebra is None: + subalgebra = cls( + p, + q, + r, + device=device, + dtype=dtype, + leaf_n=leaf_n, + product_chunk_size=product_chunk_size, + exp_policy=exp_policy, + fixed_iterations=fixed_iterations, + accumulation_dtype=accumulation_dtype, + _subalgebra_cache=subalgebra_cache, + ) + subalgebra_cache[cache_key] = subalgebra + return subalgebra + + def _init_structural_buffers(self, device, dtype: torch.dtype) -> None: + """Precompute static tables that scale linearly in basis dimension. + + Recursive nodes intentionally avoid full ``[dim, dim]`` Cayley tables, + but many unary operations need only a per-basis sign or index vector. + These buffers mirror the dense ``CliffordAlgebra`` public contract while + keeping memory usage ``O(dim)``. + """ + # Reversion sign: reverse a grade-k blade by k(k-1)/2 swaps. + rev_signs = ((-1.0) ** (self.grade_index * (self.grade_index - 1) // 2)).to( + dtype=dtype, + ) + + # Main involution and Clifford conjugation are diagonal operations in + # the canonical basis. + involution_signs = torch.where( + self.grade_index % 2 == 0, + torch.ones((), dtype=dtype, device=device), + -torch.ones((), dtype=dtype, device=device), + ) + conj_signs = (involution_signs * rev_signs).to(dtype=dtype) + + self.register_buffer("rev_signs", rev_signs, persistent=False) + self.register_buffer("_involution_signs", involution_signs, persistent=False) + self.register_buffer("conj_signs", conj_signs, persistent=False) + + # ``cayley_diag[i]`` is the scalar sign of basis_i * reverse(basis_i). + # It is enough to implement norm and Hermitian forms without a full + # Cayley table. + basis_indices = torch.arange(self.dim, dtype=torch.long, device=device) + negative_mask = 0 + for bit in range(self.p, self.p + self.q): + negative_mask |= 1 << bit + negative_count = self.grade_index[basis_indices & negative_mask] + metric_signs = torch.where( + negative_count % 2 == 0, + torch.ones((), dtype=dtype, device=device), + -torch.ones((), dtype=dtype, device=device), + ) + if self.r > 0: + null_mask = 0 + for bit in range(self.p + self.q, self.n): + null_mask |= 1 << bit + metric_signs = torch.where( + (basis_indices & null_mask) == 0, + metric_signs, + torch.zeros_like(metric_signs), + ) + + cayley_diag = rev_signs * metric_signs + self.register_buffer("_cayley_diag", cayley_diag, persistent=False) + self.register_buffer("_norm_sq_signs", (rev_signs * cayley_diag).clone(), persistent=False) + self.register_buffer("_hermitian_signs", (conj_signs * cayley_diag).clone(), persistent=False) + + # Multiplication by the pseudoscalar is also a fixed permutation/sign + # vector: x * I maps source basis ``i ^ I`` into target basis ``i``. + pseudoscalar_index = self.dim - 1 + ps_source = basis_indices ^ pseudoscalar_index + ps_target = torch.full_like(ps_source, pseudoscalar_index) + ps_signs = _basis_product_signs(ps_source, ps_target, self.p, self.q, self.r, dtype) + self.register_buffer("_ps_source", ps_source, persistent=False) + self.register_buffer("_ps_signs", ps_signs, persistent=False) + + if self.n >= 2: + bv_indices = (self.grade_index == 2).nonzero(as_tuple=False).squeeze(-1) + bv_sq_scalar = torch.zeros(len(bv_indices), dtype=dtype, device=device) + rc_action = torch.zeros(len(bv_indices), self.n, self.n, dtype=dtype, device=device) + for bivector_position, blade_index in enumerate(bv_indices.tolist()): + active_bits = [bit for bit in range(self.n) if blade_index & (1 << bit)] + if len(active_bits) != 2: + continue + first_bit, second_bit = active_bits + first_square = self._vector_square(first_bit) + second_square = self._vector_square(second_bit) + bv_sq_scalar[bivector_position] = -first_square * second_square + rc_action[bivector_position, first_bit, second_bit] = second_square + rc_action[bivector_position, second_bit, first_bit] = -first_square + else: + bv_indices = torch.zeros(0, dtype=torch.long, device=device) + bv_sq_scalar = torch.zeros(0, dtype=dtype, device=device) + rc_action = torch.zeros(0, self.n, self.n, dtype=dtype, device=device) + + self.register_buffer("_bv_indices", bv_indices, persistent=False) + self.register_buffer("bv_sq_scalar", bv_sq_scalar, persistent=False) + self.register_buffer("rc_action", rc_action, persistent=False) + + g1_idx = (1 << torch.arange(self.n, device=device)).long() + self.register_buffer("_g1_indices", g1_idx, persistent=False) + + # Left contraction keeps grade pairs (a, b) where a <= b and then + # projects the product to grade b-a. + lc_grade_a = [] + lc_grade_b = [] + lc_grade_result = [] + for grade_a in range(self.n + 1): + for grade_b in range(grade_a, self.n + 1): + lc_grade_a.append(grade_a) + lc_grade_b.append(grade_b) + lc_grade_result.append(grade_b - grade_a) + + self.register_buffer( + "_lc_grade_a", + torch.tensor(lc_grade_a, dtype=torch.long, device=device), + persistent=False, + ) + self.register_buffer( + "_lc_grade_b", + torch.tensor(lc_grade_b, dtype=torch.long, device=device), + persistent=False, + ) + self.register_buffer( + "_lc_grade_result", + torch.tensor(lc_grade_result, dtype=torch.long, device=device), + persistent=False, + ) + + # Common products are linear combinations of AB and BA. Rows encode: + # wedge, inner, commutator, anti-commutator. + product_weights = torch.tensor( + [ + [0.5, -0.5], + [0.5, 0.5], + [1.0, -1.0], + [1.0, 1.0], + ], + dtype=dtype, + device=device, + ) + self.register_buffer("_product_pair_weights", product_weights, persistent=False) + + _finfo = torch.finfo(dtype) + self.eps: float = float(_finfo.eps) + self.eps_sq: float = float(_finfo.eps**2) + + def _init_product_buffers(self) -> None: + """Precompute right-block product routing for recursive GP. + + A recursive product sums over all right-child basis products: + + ``(A_l,a * B_l,b)`` contributes to right result ``a ^ b`` with the + right-subalgebra metric sign and the bridge sign. The left products are + still computed recursively at runtime; these buffers only describe how + to select right blocks and merge them back. + """ + right_indices = torch.arange(self.right_dim, device=self.device) + right_a_indices, right_b_indices = torch.meshgrid(right_indices, right_indices, indexing="ij") + right_a_indices = right_a_indices.reshape(-1) + right_b_indices = right_b_indices.reshape(-1) + right_result_indices = (right_a_indices ^ right_b_indices).long() + + right_product_signs = _basis_product_signs( + right_a_indices, + right_b_indices, + self.right_sub.p, + self.right_sub.q, + self.right_sub.r, + torch.int8, + ) + + if self.right_sub.r == 0: + # Non-degenerate right algebras have no zero products, so + # pair_a/pair_b can be reconstructed from a linear range instead + # of stored as two extra ``right_dim ** 2`` buffers. + pair_count = self.right_dim * self.right_dim + right_product_signs = right_product_signs.reshape(-1) + self._right_pair_full = True + self.register_buffer("_right_pair_signs", right_product_signs, persistent=False) + else: + # Degenerate signatures have repeated null factors that produce + # zero. Store only nonzero right interactions so runtime never + # computes left products that will be discarded. + nonzero = right_product_signs != 0 + right_a_pair_indices = right_a_indices[nonzero].long() + right_b_pair_indices = right_b_indices[nonzero].long() + right_result_indices = right_result_indices[nonzero].long() + right_product_signs = right_product_signs[nonzero] + pair_count = int(right_a_pair_indices.numel()) + + self._right_pair_full = False + self.register_buffer("_right_pair_a", right_a_pair_indices, persistent=False) + self.register_buffer("_right_pair_b", right_b_pair_indices, persistent=False) + self.register_buffer("_right_pair_result", right_result_indices, persistent=False) + self.register_buffer("_right_pair_signs", right_product_signs, persistent=False) + + self._right_pair_count = pair_count + if self.product_chunk_size is None: + # Full-vectorize shallow nodes. Deeper nodes default to chunks so a + # high-dimensional product does not materialize all right-pair left + # products at once. + default_chunk = pair_count if self.left_n <= self.leaf_n else min(pair_count, 64) + self._product_chunk_size = max(1, default_chunk) + else: + self._product_chunk_size = max(1, int(self.product_chunk_size)) + + self._has_sparse_right_interaction = self._product_chunk_size >= self._right_pair_count + if self._has_sparse_right_interaction: + self._init_right_interaction_buffers(right_result_indices, right_product_signs) + + def _init_right_interaction_buffers( + self, + right_result_indices: torch.Tensor, + right_product_signs: torch.Tensor, + ) -> None: + """Precompute sparse right-product routing from pair terms to result blocks.""" + pair_columns = torch.arange(self._right_pair_count, dtype=torch.long, device=self.device) + interaction_indices = torch.stack((right_result_indices.long(), pair_columns)) + interaction = torch.sparse_coo_tensor( + interaction_indices, + right_product_signs.to(dtype=self.dtype), + (self.right_dim, self._right_pair_count), + device=self.device, + ).coalesce() + self.register_buffer("_right_interaction", interaction, persistent=False) + + @property + def _uses_basis_permutation(self) -> bool: + """Whether this node needs public/split basis conversion.""" + return self.basis_permutation.uses_permutation + + @property + def _to_split_basis(self) -> torch.Tensor: + """Public source indices for split-order coefficients.""" + return self.basis_permutation.split_to_public + + @property + def _to_public_basis(self) -> torch.Tensor: + """Split source indices for public-order coefficients.""" + return self.basis_permutation.public_to_split + + @property + def _split_basis_signs(self) -> torch.Tensor: + """Orientation signs indexed by split-order basis index.""" + return self.basis_permutation.split_signs + + def _to_split_order(self, mv: torch.Tensor) -> torch.Tensor: + """Convert public canonical coefficients to this node's split order.""" + return self.basis_permutation.to_split_order(mv) + + def _to_public_order(self, mv: torch.Tensor) -> torch.Tensor: + """Convert split-order coefficients back to public canonical order.""" + return self.basis_permutation.to_public_order(mv) + + def _vector_square(self, bit: int) -> float: + """Return ``e_bit ** 2`` from the global signature.""" + if bit < self.p: + return 1.0 + if bit < self.p + self.q: + return -1.0 + return 0.0 + + @property + def device(self): + """Return the device of the algebra buffers.""" + return self.grade_index.device + + @property + def dtype(self) -> torch.dtype: + """Return the floating-point dtype used by structural sign buffers.""" + return self.rev_signs.dtype + + def _apply(self, fn): + """Propagate device/dtype moves and keep eps tolerances in sync.""" + result = super()._apply(fn) + _finfo = torch.finfo(self.dtype) + self.eps = float(_finfo.eps) + self.eps_sq = float(_finfo.eps**2) + return result + + @property + def grade_masks(self): + """Grade masks indexed by grade: ``grade_masks[k]`` -> ``[dim]`` bool.""" + return self.grade_index.unsqueeze(0) == self._grade_values.unsqueeze(1) + + @property + def grade_masks_float(self): + """Float grade masks indexed by grade: ``grade_masks_float[k]`` -> ``[dim]`` float.""" + return self.grade_masks.to(dtype=self.dtype) + + @property + def is_leaf(self) -> bool: + """Whether this node delegates directly to the monolithic atomic kernel.""" + return self.core is not None + + def describe_tree(self) -> str: + """Return a readable split tree for debugging partition structure. + + The reported bit ranges use global basis-vector bit positions. Because + structurally identical subalgebras are shared, repeated module objects + are annotated with ``shared_with=`` while still being shown + at each logical tree position. + """ + lines: list[str] = [] + seen: dict[int, str] = {} + + self._describe_tree_node( + lines=lines, + path="root", + public_bits=tuple(range(self.n)), + depth=0, + seen=seen, + ) + + return "\n".join(lines) + + @staticmethod + def _format_public_bits(public_bits: tuple[int, ...]) -> str: + """Format global public bit positions compactly when contiguous.""" + if not public_bits: + return "[]" + + start = public_bits[0] + contiguous = tuple(range(start, start + len(public_bits))) + if public_bits == contiguous: + return f"[{start}, {start + len(public_bits)})" + + return "[" + ", ".join(str(bit) for bit in public_bits) + "]" + + def print_tree(self) -> None: + """Print ``describe_tree()`` for interactive debugging.""" + print(self.describe_tree()) + + def _describe_tree_node( + self, + *, + lines: list[str], + path: str, + public_bits: tuple[int, ...], + depth: int, + seen: dict[int, str], + ) -> None: + """Append this node and children to a tree description.""" + indent = " " * depth + signature = f"Cl({self.p},{self.q},{self.r})" + bits_text = self._format_public_bits(public_bits) + + node_id = id(self) + shared_suffix = "" + if node_id in seen: + shared_suffix = f", shared_with={seen[node_id]}" + else: + seen[node_id] = path + + if self.core is not None: + lines.append( + f"{indent}{path}: {signature}, n={self.n}, dim={self.dim}, bits={bits_text}, leaf_core{shared_suffix}" + ) + return + + right_bits = tuple(public_bits[bit] for bit in self._right_dims) + left_bits = tuple(public_bits[bit] for bit in self._left_dims) + right_bits_text = self._format_public_bits(right_bits) + left_bits_text = self._format_public_bits(left_bits) + + lines.append( + f"{indent}{path}: {signature}, n={self.n}, dim={self.dim}, " + f"bits={bits_text}, split left={self.left_n} bits={left_bits_text}, right={self.right_n} " + f"bits={right_bits_text}, pairs={self._right_pair_count}, " + f"chunk={self._product_chunk_size}{shared_suffix}" + ) + + self.left_sub._describe_tree_node( + lines=lines, + path=f"{path}.L", + public_bits=left_bits, + depth=depth + 1, + seen=seen, + ) + self.right_sub._describe_tree_node( + lines=lines, + path=f"{path}.R", + public_bits=right_bits, + depth=depth + 1, + seen=seen, + ) + + @property + def exp_policy(self): + """Active :class:`core.decomposition.ExpPolicy` controlling ``exp()`` dispatch.""" + return self._exp_policy + + @exp_policy.setter + def exp_policy(self, value): + from core.decomposition import ExpPolicy, resolve_fixed_iterations + + self._exp_policy = value if isinstance(value, ExpPolicy) else ExpPolicy(value) + self._exp_fixed_iterations = resolve_fixed_iterations(self._exp_policy, self.dtype, self.n) + if self.core is not None: + self.core.exp_policy = self._exp_policy + else: + self.left_sub.exp_policy = self._exp_policy + self.right_sub.exp_policy = self._exp_policy + + @property + def num_grades(self) -> int: + """Counts the number of grades.""" + return self.n + 1 + + def embed_vector(self, vectors: torch.Tensor) -> torch.Tensor: + """Inject vectors into the grade-1 subspace.""" + if self.core is not None: + return self.core.embed_vector(vectors) + mv = torch.zeros(*vectors.shape[:-1], self.dim, device=vectors.device, dtype=vectors.dtype) + mv.scatter_(-1, self._g1_indices.expand_as(vectors), vectors) + return mv + + def get_grade_norms(self, mv: torch.Tensor) -> torch.Tensor: + """Calculate per-grade Euclidean coefficient norms.""" + if self.core is not None: + return self.core.get_grade_norms(mv) + check_multivector(mv, self, "get_grade_norms(mv)") + batch_shape = mv.shape[:-1] + sq = mv.pow(2) + flat = sq.reshape(-1, self.dim) + grade_index = self.grade_index.unsqueeze(0).expand_as(flat) + result = torch.zeros(flat.shape[0], self.num_grades, device=mv.device, dtype=mv.dtype) + result.scatter_add_(1, grade_index, flat) + return result.reshape(*batch_shape, self.num_grades).clamp(min=self.eps).sqrt() + + def _combine_ab_ba(self, A: torch.Tensor, B: torch.Tensor, weight_index: int) -> torch.Tensor: + """Compute a weighted combination of ``AB`` and ``BA`` in one recursive pass.""" + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + A = A.to(dtype=output_dtype) + B = B.to(dtype=output_dtype) + + A_broadcast, B_broadcast = torch.broadcast_tensors(A, B) + left_operands = torch.stack((A_broadcast, B_broadcast), dim=-2) + right_operands = torch.stack((B_broadcast, A_broadcast), dim=-2) + products = self.geometric_product(left_operands, right_operands) + + weights = self._product_pair_weights[weight_index] + if weights.dtype != products.dtype: + weights = weights.to(dtype=products.dtype) + return torch.einsum("...pd,p->...d", products, weights) + + def _leaf_geometric_product( + self, + A: torch.Tensor, + B: torch.Tensor, + output_dtype: torch.dtype, + compute_dtype: torch.dtype, + ) -> torch.Tensor: + """Compute a dense leaf product with the profiled small-leaf kernel.""" + A_compute = A.to(dtype=compute_dtype) + B_compute = B.to(dtype=compute_dtype) + + if self._leaf_left_gp_mats.numel() == 0: + result = self.core.geometric_product(A_compute, B_compute) + else: + left_gp_mats = self._leaf_left_gp_mats + if left_gp_mats.dtype != compute_dtype: + left_gp_mats = left_gp_mats.to(dtype=compute_dtype) + + # left_matrices[..., j, k] = sum_i A[..., i] * sign(i,j,k). + # Multiplying B as a row vector then gives result[..., k]. + left_matrices = torch.einsum("...i,ijk->...jk", A_compute, left_gp_mats) + result = torch.matmul(B_compute.unsqueeze(-2), left_matrices).squeeze(-2) + + if result.dtype != output_dtype: + result = result.to(dtype=output_dtype) + return result + + def geometric_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + """Compute ``A * B`` through recursive tensor-product partitioning. + + For recursive nodes the public coefficient vector is first converted to + split order, then reshaped into ``[..., right_dim, left_dim]``. Each + right basis-pair selects two left-subalgebra multivectors, multiplies + them recursively, and merges the result into the appropriate right block. + """ + check_multivector(A, self, "geometric_product(A)") + check_multivector(B, self, "geometric_product(B)") + + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + compute_dtype = self._geometric_product_compute_dtype(output_dtype) + + if self.core is not None: + return self._leaf_geometric_product(A, B, output_dtype, compute_dtype) + + # Store each right-block coefficient as a contiguous left-subalgebra + # multivector, so recursive products receive cache-local dense leaves. + A_split = self._to_split_order(A.to(dtype=compute_dtype)) + B_split = self._to_split_order(B.to(dtype=compute_dtype)) + + A_by_left_then_right = A_split.reshape( + *A.shape[:-1], + self.left_dim, + self.right_dim, + ) + B_by_left_then_right = B_split.reshape( + *B.shape[:-1], + self.left_dim, + self.right_dim, + ) + A_by_right_blade = A_by_left_then_right.transpose(-1, -2).contiguous() + B_by_right_blade = B_by_left_then_right.transpose(-1, -2).contiguous() + + if self._product_chunk_size >= self._right_pair_count: + result_blocks = self._geometric_product_pair_range( + A_by_right_blade, + B_by_right_blade, + 0, + self._right_pair_count, + ) + output_shape = result_blocks.shape[:-2] + result = result_blocks.reshape(*output_shape, self.dim) + return self._to_public_order(result).to(dtype=output_dtype) + + result_blocks = None + for start in range(0, self._right_pair_count, self._product_chunk_size): + end = min(start + self._product_chunk_size, self._right_pair_count) + chunk_blocks = self._geometric_product_pair_range(A_by_right_blade, B_by_right_blade, start, end) + if result_blocks is None: + result_blocks = chunk_blocks + else: + result_blocks.add_(chunk_blocks) + + output_shape = result_blocks.shape[:-2] + result = result_blocks.reshape(*output_shape, self.dim) + return self._to_public_order(result).to(dtype=output_dtype) + + def _geometric_product_pair_range( + self, + A_by_right_blade: torch.Tensor, + B_by_right_blade: torch.Tensor, + start: int, + end: int, + ) -> torch.Tensor: + """Compute all contributions from a contiguous right-pair range.""" + ( + right_a_indices, + right_b_indices, + right_result_indices, + right_product_signs, + ) = self._right_product_slice(start, end) + + A_terms = torch.index_select(A_by_right_blade, -2, right_a_indices) + B_terms = torch.index_select(B_by_right_blade, -2, right_b_indices) + + # ``bridge_signs[right_b, left_a]`` depends on the left basis index of + # each selected A term, so broadcasting over the final left_dim axis + # attaches the sign before the recursive left product. + bridge_signs = torch.index_select(self.bridge_signs, 0, right_b_indices) + if bridge_signs.dtype != A_terms.dtype: + bridge_signs = bridge_signs.to(dtype=A_terms.dtype) + A_terms = A_terms * bridge_signs + + left_products = self.left_sub.geometric_product(A_terms, B_terms) + + return self._merge_right_interactions(left_products, start, end, right_result_indices, right_product_signs) + + def _merge_right_interactions( + self, + left_products: torch.Tensor, + start: int, + end: int, + right_result_indices: torch.Tensor, + right_product_signs: torch.Tensor, + ) -> torch.Tensor: + """Merge left products into ``[..., left_dim, right_dim]`` result blocks.""" + if not self._use_sparse_right_interaction(start, end): + return self._merge_right_interactions_index_add(left_products, right_result_indices, right_product_signs) + + return self._merge_right_interactions_sparse(left_products) + + def _use_sparse_right_interaction(self, start: int, end: int) -> bool: + """Return whether the static sparse interaction should handle this range.""" + return ( + self._has_sparse_right_interaction + and self.device.type == "cuda" + and start == 0 + and end == self._right_pair_count + ) + + def _merge_right_interactions_sparse(self, left_products: torch.Tensor) -> torch.Tensor: + """Merge a full right-pair range with the baked sparse interaction matrix.""" + if not self._has_sparse_right_interaction: + raise RuntimeError("sparse right interaction is only available for full-pair product nodes") + pair_count = self._right_pair_count + interaction = self._right_interaction_tensor(left_products.dtype) + batch_shape = left_products.shape[:-2] + # sparse.mm expects [right_dim, pair_count] @ [pair_count, batch*left_dim]. + flat_terms = left_products.transpose(-1, -2).reshape(-1, pair_count) + merged = torch.sparse.mm(interaction, flat_terms.transpose(0, 1)) + return merged.transpose(0, 1).reshape(*batch_shape, self.left_dim, self.right_dim) + + def _right_interaction_tensor(self, dtype: torch.dtype) -> torch.Tensor: + """Return the full sparse interaction tensor in the requested dtype.""" + interaction = self._right_interaction + return interaction if interaction.dtype == dtype else interaction.to(dtype=dtype) + + def _merge_right_interactions_index_add( + self, + left_products: torch.Tensor, + right_result_indices: torch.Tensor, + right_product_signs: torch.Tensor, + ) -> torch.Tensor: + """Fallback merge for devices without sparse COO matmul support.""" + signed_products = left_products.transpose(-1, -2) * right_product_signs.to(dtype=left_products.dtype) + result_blocks = left_products.new_zeros( + *left_products.shape[:-2], + self.left_dim, + self.right_dim, + ) + result_blocks.index_add_(-1, right_result_indices, signed_products) + return result_blocks + + def _geometric_product_compute_dtype(self, output_dtype: torch.dtype) -> torch.dtype: + """Return the dtype used for product accumulation.""" + if self.accumulation_dtype is None or not output_dtype.is_floating_point: + return output_dtype + return torch.promote_types(output_dtype, self.accumulation_dtype) + + def _promote_with_algebra_dtype(self, *dtypes: torch.dtype) -> torch.dtype: + """Promote operand dtypes with the algebra's floating-point table dtype.""" + result = self.dtype + for dtype in dtypes: + result = torch.promote_types(result, dtype) + return result + + def _right_product_slice( + self, + start: int, + end: int, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Return right-block routing tensors for pair range ``[start, end)``. + + Returns: + tuple: ``(right_a_indices, right_b_indices, right_result_indices, + right_product_signs)``. Each position describes one right basis-pair + contribution in the recursive product. + """ + if self._right_pair_full: + linear_pair_indices = torch.arange(start, end, dtype=torch.long, device=self.device) + right_a_indices = torch.div(linear_pair_indices, self.right_dim, rounding_mode="floor") + right_b_indices = linear_pair_indices.remainder(self.right_dim) + right_result_indices = right_a_indices ^ right_b_indices + right_product_signs = self._right_pair_signs.reshape(-1)[start:end] + return right_a_indices, right_b_indices, right_result_indices, right_product_signs + + return ( + self._right_pair_a[start:end], + self._right_pair_b[start:end], + self._right_pair_result[start:end], + self._right_pair_signs[start:end], + ) + + def grade_projection(self, mv: torch.Tensor, grade: int) -> torch.Tensor: + """Project a multivector onto a grade using the same mask contract as the core algebra.""" + if self.core is not None: + return self.core.grade_projection(mv, grade) + check_multivector(mv, self, "grade_projection(mv)") + return mv * (self.grade_index == grade).to(dtype=mv.dtype) + + def reverse(self, mv: torch.Tensor) -> torch.Tensor: + """Compute Clifford reversion.""" + if self.core is not None: + return self.core.reverse(mv) + check_multivector(mv, self, "reverse(mv)") + return mv * self.rev_signs.to(dtype=mv.dtype) + + def wedge(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + """Compute ``(AB - BA) / 2`` through the partitioned product.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + return self.core.wedge(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) + return self._combine_ab_ba(A, B, 0) + + def right_contraction(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + """Compute the bivector-vector right contraction used by decomposition.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + return self.core.right_contraction(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) + check_multivector(A, self, "right_contraction(A)") + check_multivector(B, self, "right_contraction(B)") + + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + bv_coeffs = torch.index_select(A, -1, self._bv_indices).to(dtype=output_dtype) + v_coeffs = torch.index_select(B, -1, self._g1_indices).to(dtype=output_dtype) + + rc = self.rc_action.to(dtype=output_dtype) + action = torch.einsum("...b, bij -> ...ij", bv_coeffs, rc) + result_v = torch.matmul(action, v_coeffs.unsqueeze(-1)).squeeze(-1) + + result = result_v.new_zeros(*result_v.shape[:-1], self.dim) + g1_idx_exp = self._g1_indices.expand(*result_v.shape[:-1], -1) + result.scatter_(-1, g1_idx_exp, result_v) + return result + + def inner_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + """Compute ``(AB + BA) / 2`` through the partitioned product.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + return self.core.inner_product(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) + return self._combine_ab_ba(A, B, 1) + + def commutator(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + """Compute the Lie bracket ``AB - BA``.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + return self.core.commutator(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) + return self._combine_ab_ba(A, B, 2) + + def anti_commutator(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + """Compute the anti-commutator ``AB + BA``.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + return self.core.anti_commutator(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) + return self._combine_ab_ba(A, B, 3) + + def blade_inverse(self, blade: torch.Tensor) -> torch.Tensor: + """Compute the inverse of a non-degenerate blade.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(blade.dtype) + return self.core.blade_inverse(blade.to(dtype=output_dtype)) + blade_rev = self.reverse(blade) + blade_sq = self.geometric_product(blade, blade_rev) + scalar = blade_sq[..., 0:1].clamp(min=self.eps_sq) + return blade_rev / scalar + + def sandwich_product(self, R: torch.Tensor, x: torch.Tensor, R_rev: torch.Tensor = None) -> torch.Tensor: + """Compute ``R x R~`` using recursive products.""" + if self.core is not None: + dtypes = [R.dtype, x.dtype] + if R_rev is not None: + dtypes.append(R_rev.dtype) + output_dtype = self._promote_with_algebra_dtype(*dtypes) + R_rev = None if R_rev is None else R_rev.to(dtype=output_dtype) + return self.core.sandwich_product(R.to(dtype=output_dtype), x.to(dtype=output_dtype), R_rev) + if R_rev is None: + R_rev = self.reverse(R) + left = self.geometric_product(R.unsqueeze(-2), x) + return self.geometric_product(left, R_rev.unsqueeze(-2)) + + def per_channel_sandwich(self, R: torch.Tensor, x: torch.Tensor, R_rev: torch.Tensor = None) -> torch.Tensor: + """Compute per-channel sandwich products using recursive products.""" + if self.core is not None: + dtypes = [R.dtype, x.dtype] + if R_rev is not None: + dtypes.append(R_rev.dtype) + output_dtype = self._promote_with_algebra_dtype(*dtypes) + R_rev = None if R_rev is None else R_rev.to(dtype=output_dtype) + return self.core.per_channel_sandwich(R.to(dtype=output_dtype), x.to(dtype=output_dtype), R_rev) + if R_rev is None: + R_rev = self.reverse(R) + left = self.geometric_product(R.unsqueeze(0), x) + return self.geometric_product(left, R_rev.unsqueeze(0)) + + def multi_rotor_sandwich(self, R: torch.Tensor, x: torch.Tensor, R_rev: torch.Tensor = None) -> torch.Tensor: + """Apply K rotors to ``[B, C, D]`` inputs using recursive products.""" + if self.core is not None: + dtypes = [R.dtype, x.dtype] + if R_rev is not None: + dtypes.append(R_rev.dtype) + output_dtype = self._promote_with_algebra_dtype(*dtypes) + R_rev = None if R_rev is None else R_rev.to(dtype=output_dtype) + return self.core.multi_rotor_sandwich(R.to(dtype=output_dtype), x.to(dtype=output_dtype), R_rev) + if R_rev is None: + R_rev = self.reverse(R) + left = self.geometric_product(R.view(1, 1, *R.shape), x.unsqueeze(2)) + return self.geometric_product(left, R_rev.view(1, 1, *R_rev.shape)) + + def pseudoscalar_product(self, x: torch.Tensor) -> torch.Tensor: + """Multiply by the unit pseudoscalar using a static permutation/sign vector.""" + if self.core is not None: + return self.core.pseudoscalar_product(x) + check_multivector(x, self, "pseudoscalar_product(x)") + return x[..., self._ps_source] * self._ps_signs.to(dtype=x.dtype) + + def blade_project(self, mv: torch.Tensor, blade: torch.Tensor) -> torch.Tensor: + """Project a multivector onto a blade subspace.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(mv.dtype, blade.dtype) + return self.core.blade_project(mv.to(dtype=output_dtype), blade.to(dtype=output_dtype)) + inner = self.inner_product(mv, blade) + return self.geometric_product(inner, self.blade_inverse(blade)) + + def blade_reject(self, mv: torch.Tensor, blade: torch.Tensor) -> torch.Tensor: + """Reject a multivector from a blade subspace.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(mv.dtype, blade.dtype) + return self.core.blade_reject(mv.to(dtype=output_dtype), blade.to(dtype=output_dtype)) + return mv - self.blade_project(mv, blade) + + def grade_involution(self, mv: torch.Tensor) -> torch.Tensor: + """Apply the main involution.""" + if self.core is not None: + return self.core.grade_involution(mv) + check_multivector(mv, self, "grade_involution(mv)") + return mv * self._involution_signs.to(dtype=mv.dtype) + + def clifford_conjugation(self, mv: torch.Tensor) -> torch.Tensor: + """Apply Clifford conjugation.""" + if self.core is not None: + return self.core.clifford_conjugation(mv) + check_multivector(mv, self, "clifford_conjugation(mv)") + return mv * self.conj_signs.to(dtype=mv.dtype) + + def norm_sq(self, mv: torch.Tensor) -> torch.Tensor: + """Compute ``_0`` using pre-merged static signs.""" + if self.core is not None: + return self.core.norm_sq(mv) + check_multivector(mv, self, "norm_sq(mv)") + signs = self._norm_sq_signs + if signs.dtype != mv.dtype: + signs = signs.to(dtype=mv.dtype) + return torch.matmul(mv * mv, signs.unsqueeze(-1)) + + def left_contraction(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + """Compute left contraction by static grade-pair dispatch.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + return self.core.left_contraction(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) + check_multivector(A, self, "left_contraction(A)") + check_multivector(B, self, "left_contraction(B)") + + output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) + A = A.to(dtype=output_dtype) + B = B.to(dtype=output_dtype) + + A_b, B_b = torch.broadcast_tensors(A, B) + result = None + pair_count = int(self._lc_grade_a.numel()) + chunk_size = max( + 1, + min( + pair_count, + self._product_chunk_size if self._product_chunk_size > 0 else pair_count, + ), + ) + grade_index = self.grade_index.unsqueeze(0) + for start in range(0, pair_count, chunk_size): + end = min(start + chunk_size, pair_count) + a_masks = grade_index == self._lc_grade_a[start:end].unsqueeze(1) + b_masks = grade_index == self._lc_grade_b[start:end].unsqueeze(1) + result_masks = grade_index == self._lc_grade_result[start:end].unsqueeze(1) + a_masks = a_masks.to(dtype=A_b.dtype) + b_masks = b_masks.to(dtype=A_b.dtype) + result_masks = result_masks.to(dtype=A_b.dtype) + + A_terms = A_b.unsqueeze(-2) * a_masks + B_terms = B_b.unsqueeze(-2) * b_masks + products = self.geometric_product(A_terms, B_terms) + chunk = torch.einsum("...pd,pd->...d", products, result_masks) + result = chunk if result is None else result + chunk + return result + + def dual(self, mv: torch.Tensor) -> torch.Tensor: + """Hodge dual alias for pseudoscalar multiplication.""" + if self.core is not None: + return self.core.dual(mv) + return self.pseudoscalar_product(mv) + + def reflect(self, x: torch.Tensor, n: torch.Tensor) -> torch.Tensor: + """Reflect ``x`` through the hyperplane orthogonal to vector ``n``.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(x.dtype, n.dtype) + return self.core.reflect(x.to(dtype=output_dtype), n.to(dtype=output_dtype)) + n_hat = self.grade_involution(n) + n_inv = self.blade_inverse(n) + if x.dim() == 3 and n.dim() == 2 and x.shape[0] != n.shape[0]: + n_hat = n_hat.unsqueeze(0) + n_inv = n_inv.unsqueeze(0) + elif x.dim() == 3 and n.dim() == 2 and x.shape[0] == n.shape[0]: + n_hat = n_hat.unsqueeze(1) + n_inv = n_inv.unsqueeze(1) + return self.geometric_product(self.geometric_product(n_hat, x), n_inv) + + def versor_product(self, V: torch.Tensor, x: torch.Tensor) -> torch.Tensor: + """Apply the general versor transformation ``hat(V) x V^{-1}``.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(V.dtype, x.dtype) + return self.core.versor_product(V.to(dtype=output_dtype), x.to(dtype=output_dtype)) + V_inv = self.blade_inverse(V) + V_hat = self.grade_involution(V) + return self.geometric_product(self.geometric_product(V_hat, x), V_inv) + + def exp(self, mv: torch.Tensor) -> torch.Tensor: + """Exponentiates a bivector to produce a rotor. + + Dispatch mirrors :class:`core.algebra.CliffordAlgebra`: + + - ``n <= 3`` -- every bivector is simple; closed-form is exact. + - ``n >= 4`` -- compiled-safe decomposition; per-element selects + closed-form vs decomposed via ``torch.where(simple)``. + + Args: + mv (torch.Tensor): Pure bivector [..., dim]. + + Returns: + torch.Tensor: Rotor exp(mv) [..., dim]. + """ + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(mv.dtype) + return self.core.exp(mv.to(dtype=output_dtype)) + if self.n <= 3: + return self._exp_bivector_closed(mv) + return self._exp_compiled_safe(mv) + + def _exp_bivector_closed(self, B: torch.Tensor) -> torch.Tensor: + """Closed-form exponential for simple bivectors in arbitrary signature. + + Uses zero geometric products. Exact for simple bivectors in any + Clifford algebra Cl(p,q,r). + """ + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(B.dtype) + return self.core._exp_bivector_closed(B.to(dtype=output_dtype)) + + output_dtype = self._promote_with_algebra_dtype(B.dtype) + B = B.to(dtype=output_dtype) + + bv_coeffs = torch.index_select(B, -1, self._bv_indices) + bv_sq_scalar = self.bv_sq_scalar.to(dtype=output_dtype) + + # Signed squared norm: alpha = Sum_k b_k^2 . (e_k)^2 + # alpha < 0 -> elliptic (Euclidean-like), alpha > 0 -> hyperbolic + alpha = torch.matmul(bv_coeffs * bv_coeffs, bv_sq_scalar.unsqueeze(-1)) + + abs_alpha = alpha.abs().clamp(min=self.eps_sq) + theta = torch.sqrt(abs_alpha) + + g0_mask = self.grade_masks_float[0].to(dtype=output_dtype) + + # Dispatch by signature regime (Python branch, no graph break) + if self._exp_regime == "elliptic": + # Pure Euclidean: alpha is always negative, only cos/sinc needed + cos_theta = torch.cos(theta) + sinc_theta = torch.where( + theta > self.eps, + torch.sin(theta) / theta, + 1.0 - abs_alpha / 6.0, + ) + return cos_theta * g0_mask + sinc_theta * B + + if self._exp_regime == "hyperbolic": + # Pure negative: alpha is always positive, only cosh/sinhc needed + cosh_theta = torch.cosh(theta) + sinhc_theta = torch.where( + theta > self.eps, + torch.sinh(theta) / theta, + 1.0 + abs_alpha / 6.0, + ) + return cosh_theta * g0_mask + sinhc_theta * B + + # Mixed signature: need both branches + runtime select + cos_theta = torch.cos(theta) + sinc_theta = torch.where( + theta > self.eps, + torch.sin(theta) / theta, + 1.0 - abs_alpha / 6.0, + ) + cosh_theta = torch.cosh(theta) + sinhc_theta = torch.where( + theta > self.eps, + torch.sinh(theta) / theta, + 1.0 + abs_alpha / 6.0, + ) + + is_elliptic = alpha < -self.eps_sq + is_hyperbolic = alpha > self.eps_sq + + scalar_part = torch.where( + is_elliptic, + cos_theta, + torch.where(is_hyperbolic, cosh_theta, torch.ones_like(theta)), + ) + coeff_part = torch.where( + is_elliptic, + sinc_theta, + torch.where(is_hyperbolic, sinhc_theta, torch.ones_like(theta)), + ) + + return scalar_part * g0_mask + coeff_part * B + + def _exp_compiled_safe(self, B: torch.Tensor) -> torch.Tensor: + """Compiled-safe exponential using partitioned products. + + Runs both closed-form and decomposed paths, then selects per element + via ``torch.where`` based on simplicity. Both paths are computed + unconditionally so there is no data-dependent branching. + """ + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(B.dtype) + return self.core._exp_compiled_safe(B.to(dtype=output_dtype)) + from core.decomposition import compiled_safe_decomposed_exp + + R_closed = self._exp_bivector_closed(B) + R_decomposed = compiled_safe_decomposed_exp( + self, + B, + fixed_iterations=self._exp_fixed_iterations, + ) + + BB = self.geometric_product(B, B) + # Subtract scalar part, check if residual is negligible + scalar_part = self.grade_projection(BB, 0) + non_scalar_energy = (BB - scalar_part).norm(dim=-1, keepdim=True) + is_simple = non_scalar_energy < self.eps * 100 + + return torch.where(is_simple, R_closed, R_decomposed) + + def _exp_taylor(self, mv: torch.Tensor, order: int = 8) -> torch.Tensor: + """Taylor series exponential with scaling-and-squaring.""" + if self.core is not None: + output_dtype = self._promote_with_algebra_dtype(mv.dtype) + return self.core._exp_taylor(mv.to(dtype=output_dtype), order=order) + norm = mv.norm(dim=-1, keepdim=True) + k = torch.ceil(torch.log2(torch.clamp(norm, min=1.0))).int() + + max_k = k.max().item() + if max_k > 0: + mv_scaled = mv / (2.0**max_k) + else: + mv_scaled = mv + + res = torch.zeros_like(mv) + res[..., 0] = 1.0 + + term = torch.zeros_like(mv) + term[..., 0] = 1.0 + + for i in range(1, order + 1): + term = self.geometric_product(term, mv_scaled) + res = res + term / math.factorial(i) + + if max_k > 0: + for _ in range(int(max_k)): + res = self.geometric_product(res, res) + + return res From 8ad6c19ba92d6f2ec9706879f88530e46357114f Mon Sep 17 00:00:00 2001 From: Concode0 Date: Fri, 1 May 2026 15:23:01 +0900 Subject: [PATCH 02/45] refactor: organize partitioned algebra internals --- core/partitioned_algebra.py | 938 ++++++++++++++++++------------------ 1 file changed, 476 insertions(+), 462 deletions(-) diff --git a/core/partitioned_algebra.py b/core/partitioned_algebra.py index 4c45b87..9178106 100644 --- a/core/partitioned_algebra.py +++ b/core/partitioned_algebra.py @@ -32,7 +32,8 @@ from core.algebra import CliffordAlgebra from core.validation import check_multivector -_MAX_LEFT_MATRIX_LEAF_N = 6 +_LOCAL_DENSE_LEAF_N = 4 +_DEFAULT_PRODUCT_CHUNK_SIZE = 64 def _signature_for_range(p: int, q: int, r: int, start: int, width: int) -> tuple[int, int, int]: @@ -107,6 +108,32 @@ def split_dims(self) -> tuple[int, ...]: return self.right_dims + self.left_dims +@dataclass(frozen=True) +class _ExpSettings: + """Resolved exponential dispatch settings for one algebra signature.""" + + regime: str + policy: object + fixed_iterations: int + + +@dataclass(frozen=True) +class _StructuralBuffers: + """Purely generated buffers plus dtype-derived scalar tolerances.""" + + buffers: tuple[tuple[str, torch.Tensor], ...] + eps: float + eps_sq: float + + +@dataclass(frozen=True) +class _ProductPlan: + """Runtime product chunk plan for one recursive node.""" + + right_pair_count: int + chunk_size: int + + def _partition_split(p: int, q: int, r: int) -> _PartitionSplit: """Return the single recursive split used by the partitioned algebra. @@ -171,6 +198,106 @@ def _grade_index(n: int, device) -> torch.Tensor: return grades +def _bit_range_mask(start: int, end: int) -> int: + """Return an integer bit mask covering ``[start, end)``.""" + mask = 0 + for bit in range(start, end): + mask |= 1 << bit + return mask + + +def _vector_square(bit: int, p: int, q: int) -> float: + """Return the metric square of one basis vector.""" + if bit < p: + return 1.0 + if bit < p + q: + return -1.0 + return 0.0 + + +def _resolve_exp_settings( + p: int, + q: int, + r: int, + dtype: torch.dtype, + exp_policy, + fixed_iterations: Optional[int], +) -> _ExpSettings: + """Resolve signature-wide exponential policy without mutating a module.""" + if p == 0 or q == 0: + regime = "elliptic" + elif p == 1 and q == 1 and r == 0: + regime = "hyperbolic" + else: + regime = "mixed" + + from core.decomposition import ExpPolicy, resolve_fixed_iterations + + policy = exp_policy if isinstance(exp_policy, ExpPolicy) else ExpPolicy(exp_policy) + iterations = ( + int(fixed_iterations) + if fixed_iterations is not None + else resolve_fixed_iterations(policy, dtype, p + q + r) + ) + return _ExpSettings(regime=regime, policy=policy, fixed_iterations=iterations) + + +def _default_product_chunk_size(pair_count: int) -> int: + """Choose a memory-conscious right-pair chunk size.""" + return max(1, min(pair_count, _DEFAULT_PRODUCT_CHUNK_SIZE)) + + +def _right_pair_count(right_n: int, right_r: int) -> int: + """Return the number of right basis pairs that survive the null metric.""" + non_null_n = right_n - right_r + return (4**non_null_n) * (3**right_r) + + +def _product_plan(right_n: int, right_r: int, requested_chunk_size: Optional[int]) -> _ProductPlan: + """Return the recursive product plan without allocating routing tables.""" + right_pair_count = _right_pair_count(right_n, right_r) + chunk_size = ( + _default_product_chunk_size(right_pair_count) + if requested_chunk_size is None + else max(1, int(requested_chunk_size)) + ) + return _ProductPlan(right_pair_count=right_pair_count, chunk_size=chunk_size) + + +def _compact_surviving_basis_pairs( + compact_pair_indices: torch.Tensor, + n: int, + p: int, + q: int, + r: int, +) -> tuple[torch.Tensor, torch.Tensor]: + """Map compact mixed-radix pair indices to surviving basis-product pairs. + + Non-null basis-vector bits have four states: absent, left only, right only, + or both. Null bits have only three active states because the ``both`` case + squares a null vector and annihilates the product. + """ + right_a_indices = torch.zeros_like(compact_pair_indices) + right_b_indices = torch.zeros_like(compact_pair_indices) + quotient = compact_pair_indices + non_null_n = p + q + + for bit in range(n): + if bit < non_null_n: + digit = quotient.remainder(4) + quotient = torch.div(quotient, 4, rounding_mode="floor") + right_a_indices = right_a_indices | ((digit & 1) << bit) + right_b_indices = right_b_indices | (((digit >> 1) & 1) << bit) + else: + digit = quotient.remainder(3) + quotient = torch.div(quotient, 3, rounding_mode="floor") + right_a_indices = right_a_indices | ((digit == 1).to(torch.long) << bit) + right_b_indices = right_b_indices | ((digit == 2).to(torch.long) << bit) + + assert r == n - non_null_n + return right_a_indices, right_b_indices + + def _subalgebra_cache_key( p: int, q: int, @@ -205,6 +332,7 @@ def _basis_product_signs( q: int, r: int, dtype: torch.dtype, + popcount: Optional[torch.Tensor] = None, ) -> torch.Tensor: """Return basis-product signs for equal-shaped bitmask index tensors. @@ -214,7 +342,8 @@ def _basis_product_signs( annihilate products that repeat the same null basis vector. """ n = p + q + r - popcount = _grade_index(n, indices_a.device) + if popcount is None: + popcount = _grade_index(n, indices_a.device) # Reordering ``A`` basis vectors past lower-numbered ``B`` basis vectors # gives the anticommutation sign. @@ -231,23 +360,187 @@ def _basis_product_signs( ) # Repeated negative basis vectors square to -1. - negative_mask = 0 - for bit in range(p, p + q): - negative_mask |= 1 << bit + negative_mask = _bit_range_mask(p, p + q) negative_intersection = indices_a & indices_b & negative_mask negative_count = popcount[negative_intersection] sign = torch.where(negative_count % 2 == 0, sign, -sign) if r > 0: # Repeated null basis vectors square to 0, annihilating the term. - null_mask = 0 - for bit in range(p + q, n): - null_mask |= 1 << bit + null_mask = _bit_range_mask(p + q, n) sign = torch.where((indices_a & indices_b & null_mask) == 0, sign, torch.zeros_like(sign)) return sign +def _involution_buffers( + grade_index: torch.Tensor, + dtype: torch.dtype, + device, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Return reversion, grade-involution, and Clifford-conjugation signs.""" + rev_signs = ((-1.0) ** (grade_index * (grade_index - 1) // 2)).to(dtype=dtype) + involution_signs = torch.where( + grade_index % 2 == 0, + torch.ones((), dtype=dtype, device=device), + -torch.ones((), dtype=dtype, device=device), + ) + conj_signs = (involution_signs * rev_signs).to(dtype=dtype) + return rev_signs, involution_signs, conj_signs + + +def _basis_square_metric_signs( + basis_indices: torch.Tensor, + grade_index: torch.Tensor, + p: int, + q: int, + r: int, + dtype: torch.dtype, + device, +) -> torch.Tensor: + """Return the metric-only part of ``e_I * e_I`` for every basis blade.""" + negative_mask = _bit_range_mask(p, p + q) + negative_count = grade_index[basis_indices & negative_mask] + metric_signs = torch.where( + negative_count % 2 == 0, + torch.ones((), dtype=dtype, device=device), + -torch.ones((), dtype=dtype, device=device), + ) + + if r > 0: + null_mask = _bit_range_mask(p + q, p + q + r) + metric_signs = torch.where( + (basis_indices & null_mask) == 0, + metric_signs, + torch.zeros_like(metric_signs), + ) + + return metric_signs + + +def _pseudoscalar_buffers( + basis_indices: torch.Tensor, + p: int, + q: int, + r: int, + dtype: torch.dtype, + popcount: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: + """Return source permutation and signs for right multiplication by ``I``.""" + pseudoscalar_index = basis_indices.numel() - 1 + ps_source = basis_indices ^ pseudoscalar_index + ps_target = torch.full_like(ps_source, pseudoscalar_index) + ps_signs = _basis_product_signs(ps_source, ps_target, p, q, r, dtype, popcount=popcount) + return ps_source, ps_signs + + +def _bivector_buffers( + n: int, + p: int, + q: int, + grade_index: torch.Tensor, + dtype: torch.dtype, + device, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Return bivector indices, squared scalars, and right-contraction action.""" + if n < 2: + return ( + torch.zeros(0, dtype=torch.long, device=device), + torch.zeros(0, dtype=dtype, device=device), + torch.zeros(0, n, n, dtype=dtype, device=device), + ) + + bivector_indices = [blade_index for blade_index in range(1 << n) if blade_index.bit_count() == 2] + bv_indices = torch.tensor(bivector_indices, dtype=torch.long, device=device) + bv_sq_scalar = torch.zeros(len(bv_indices), dtype=dtype, device=device) + rc_action = torch.zeros(len(bv_indices), n, n, dtype=dtype, device=device) + + for bivector_position, blade_index in enumerate(bv_indices.tolist()): + active_bits = [bit for bit in range(n) if blade_index & (1 << bit)] + if len(active_bits) != 2: + continue + first_bit, second_bit = active_bits + first_square = _vector_square(first_bit, p, q) + second_square = _vector_square(second_bit, p, q) + bv_sq_scalar[bivector_position] = -first_square * second_square + rc_action[bivector_position, first_bit, second_bit] = second_square + rc_action[bivector_position, second_bit, first_bit] = -first_square + + return bv_indices, bv_sq_scalar, rc_action + + +def _left_contraction_grade_buffers(n: int, device) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Return compact grade-pair dispatch vectors for left contraction.""" + grade_pairs = [ + (grade_a, grade_b, grade_b - grade_a) + for grade_a in range(n + 1) + for grade_b in range(grade_a, n + 1) + ] + lc_grade_a, lc_grade_b, lc_grade_result = zip(*grade_pairs) + return ( + torch.tensor(lc_grade_a, dtype=torch.long, device=device), + torch.tensor(lc_grade_b, dtype=torch.long, device=device), + torch.tensor(lc_grade_result, dtype=torch.long, device=device), + ) + + +def _product_pair_weights(dtype: torch.dtype, device) -> torch.Tensor: + """Return AB/BA linear-combination weights for common binary products.""" + return torch.tensor( + [ + [0.5, -0.5], + [0.5, 0.5], + [1.0, -1.0], + [1.0, 1.0], + ], + dtype=dtype, + device=device, + ) + + +def _structural_buffers(p: int, q: int, r: int, device, dtype: torch.dtype) -> _StructuralBuffers: + """Build all linear-size structural tensors without mutating a module.""" + n = p + q + r + dim = 2**n + basis_indices = torch.arange(dim, dtype=torch.long, device=device) + grade_index = _grade_index(n, device) + grade_values = torch.arange(n + 1, dtype=torch.long, device=device) + + rev_signs, involution_signs, conj_signs = _involution_buffers(grade_index, dtype, device) + metric_signs = _basis_square_metric_signs(basis_indices, grade_index, p, q, r, dtype, device) + cayley_diag = rev_signs * metric_signs + ps_source, ps_signs = _pseudoscalar_buffers(basis_indices, p, q, r, dtype, grade_index) + bv_indices, bv_sq_scalar, rc_action = _bivector_buffers(n, p, q, grade_index, dtype, device) + lc_grade_a, lc_grade_b, lc_grade_result = _left_contraction_grade_buffers(n, device) + g1_indices = (1 << torch.arange(n, device=device)).long() + + finfo = torch.finfo(dtype) + return _StructuralBuffers( + buffers=( + ("grade_index", grade_index), + ("_grade_values", grade_values), + ("rev_signs", rev_signs), + ("_involution_signs", involution_signs), + ("conj_signs", conj_signs), + ("_cayley_diag", cayley_diag), + ("_norm_sq_signs", (rev_signs * cayley_diag).clone()), + ("_hermitian_signs", (conj_signs * cayley_diag).clone()), + ("_ps_source", ps_source), + ("_ps_signs", ps_signs), + ("_bv_indices", bv_indices), + ("bv_sq_scalar", bv_sq_scalar), + ("rc_action", rc_action), + ("_g1_indices", g1_indices), + ("_lc_grade_a", lc_grade_a), + ("_lc_grade_b", lc_grade_b), + ("_lc_grade_result", lc_grade_result), + ("_product_pair_weights", _product_pair_weights(dtype, device)), + ), + eps=float(finfo.eps), + eps_sq=float(finfo.eps**2), + ) + + class _BasisPermutation(nn.Module): """Convert coefficients between public canonical order and split-local order. @@ -361,8 +654,10 @@ class PartitionedCliffordAlgebra(nn.Module): device (str or torch.device, optional): Device for generated buffers. dtype (torch.dtype, optional): Floating-point dtype for sign buffers and dense leaf algebras. - leaf_n (int, optional): Maximum dimension handled by dense - ``CliffordAlgebra`` leaves. + leaf_n (int, optional): Maximum basis-vector count handled by local + leaves. The default targets ``2**leaf_n == 16`` coefficients so + deep-learning products use small dense kernels and indexed global + merge routing. product_chunk_size (int, optional): Number of right-basis product pairs processed per recursive chunk. ``None`` chooses a memory-conscious default from the node shape. @@ -380,7 +675,7 @@ def __init__( r: int = 0, device="cuda", dtype: torch.dtype = torch.float32, - leaf_n: int = 6, + leaf_n: int = _LOCAL_DENSE_LEAF_N, product_chunk_size: Optional[int] = None, exp_policy: str = "balanced", fixed_iterations: Optional[int] = None, @@ -394,6 +689,27 @@ def __init__( assert r >= 0, f"r must be non-negative, got {r}" assert leaf_n >= 1, f"leaf_n must be >= 1, got {leaf_n}" + self._init_signature(p, q, r, leaf_n, product_chunk_size, accumulation_dtype) + self._init_exp_settings(dtype, exp_policy, fixed_iterations) + self._init_structural_buffers(device, dtype) + + if self.n <= leaf_n: + self._init_leaf_node(device, dtype) + return + + subalgebra_cache = {} if _subalgebra_cache is None else _subalgebra_cache + self._init_recursive_node(device, dtype, subalgebra_cache) + + def _init_signature( + self, + p: int, + q: int, + r: int, + leaf_n: int, + product_chunk_size: Optional[int], + accumulation_dtype: Optional[torch.dtype], + ) -> None: + """Store constructor inputs that define this algebra node.""" self.p, self.q, self.r = p, q, r self.n = p + q + r self.dim = 2**self.n @@ -401,157 +717,84 @@ def __init__( self.product_chunk_size = product_chunk_size self.accumulation_dtype = accumulation_dtype - # Exp regime: dispatch at init. The branch is signature-wide, so it can - # remain a Python branch without causing data-dependent graph breaks. - if p == 0 or q == 0: - self._exp_regime = "elliptic" - elif p == 1 and q == 1 and r == 0: - self._exp_regime = "hyperbolic" - else: - self._exp_regime = "mixed" - - # Exp policy: controls the decomposition iteration budget used by - # compiled-safe bivector exponentials. - from core.decomposition import ExpPolicy, resolve_fixed_iterations - - self._exp_policy = exp_policy if isinstance(exp_policy, ExpPolicy) else ExpPolicy(exp_policy) - - self._exp_fixed_iterations: int = ( - int(fixed_iterations) - if fixed_iterations is not None - else resolve_fixed_iterations(self._exp_policy, dtype, self.n) - ) - - if _subalgebra_cache is None: - _subalgebra_cache = {} - - grade_index = _grade_index(self.n, device) - self.register_buffer("grade_index", grade_index, persistent=False) - self.register_buffer( - "_grade_values", - torch.arange(self.n + 1, dtype=torch.long, device=device), - persistent=False, - ) - - self._init_structural_buffers(device, dtype) - - # Leaf nodes delegate to the existing dense Cayley-table engine. - if self.n <= leaf_n: - # Leaves operate in public order and therefore use the identity - # permutation. Keeping ``basis_permutation`` present on every node - # lets product code treat leaves and recursive nodes uniformly. - self.basis_permutation = _BasisPermutation(tuple(range(self.n)), device) - self.core = CliffordAlgebra( - p, - q, - r, - device=device, - dtype=dtype, - exp_policy=self._exp_policy, - fixed_iterations=self._exp_fixed_iterations, - ) - self._init_leaf_product_buffers(device, dtype) - self.left_sub = None - self.right_sub = None - self.left_n = 0 - self.right_n = 0 - self.left_dim = 0 - self.right_dim = 0 - self._right_pair_count = 0 - self._product_chunk_size = 0 - self._right_pair_full = False - self._has_sparse_right_interaction = False - self._right_dims = () - self._left_dims = () - return - - split = _partition_split(p, q, r) - right_signature = split.right_signature - left_signature = split.left_signature - - # The split order keeps right-child public coordinates in the low - # internal bits and left-child public coordinates in the high bits: - # split_index = (left_index << right_n) | right_index. - split_order_dims = split.split_dims - assert sorted(split_order_dims) == list(range(self.n)) - - right_n = len(split.right_dims) - left_n = len(split.left_dims) - self.left_n = left_n - self.right_n = right_n - self.left_dim = 2**left_n - self.right_dim = 2**right_n - self._right_dims = split.right_dims - self._left_dims = split.left_dims - self.basis_permutation = _BasisPermutation(split_order_dims, device) - - self.core = None - self.left_sub = self._get_or_create_subalgebra( - *left_signature, - device=device, - dtype=dtype, - leaf_n=leaf_n, - product_chunk_size=product_chunk_size, - exp_policy=self._exp_policy, - fixed_iterations=self._exp_fixed_iterations, - accumulation_dtype=accumulation_dtype, - subalgebra_cache=_subalgebra_cache, + def _init_exp_settings(self, dtype: torch.dtype, exp_policy, fixed_iterations: Optional[int]) -> None: + """Attach resolved exponential settings to this node.""" + settings = _resolve_exp_settings( + self.p, + self.q, + self.r, + dtype, + exp_policy, + fixed_iterations, ) - self.right_sub = self._get_or_create_subalgebra( - *right_signature, + self._exp_regime = settings.regime + self._exp_policy = settings.policy + self._exp_fixed_iterations = settings.fixed_iterations + + def _init_leaf_node(self, device, dtype: torch.dtype) -> None: + """Configure a leaf node backed by the dense local Clifford kernel.""" + self.basis_permutation = _BasisPermutation(tuple(range(self.n)), device) + self.core = CliffordAlgebra( + self.p, + self.q, + self.r, device=device, dtype=dtype, - leaf_n=leaf_n, - product_chunk_size=product_chunk_size, exp_policy=self._exp_policy, fixed_iterations=self._exp_fixed_iterations, - accumulation_dtype=accumulation_dtype, - subalgebra_cache=_subalgebra_cache, ) + self.left_sub = None + self.right_sub = None + self.left_n = 0 + self.right_n = 0 + self.left_dim = 0 + self.right_dim = 0 + self._right_pair_count = 0 + self._product_chunk_size = 0 + self._right_dims = () + self._left_dims = () + + def _init_recursive_node(self, device, dtype: torch.dtype, subalgebra_cache: dict) -> None: + """Configure split layout, child modules, and runtime product planning.""" + split = _partition_split(self.p, self.q, self.r) + self._init_split_layout(split, device) + self.core = None + self.left_sub, self.right_sub = self._create_child_subalgebras(split, device, dtype, subalgebra_cache) + self._init_product_plan() - # A left factor from A must cross a right factor from B: - # (L_A R_A)(L_B R_B) = (-1)^(grade(L_A) grade(R_B)) (L_A L_B)(R_A R_B). - left_grade_by_index = _grade_index(left_n, device) - right_grade_by_index = _grade_index(right_n, device) - bridge_signs = torch.where( - (right_grade_by_index.unsqueeze(1) * left_grade_by_index.unsqueeze(0)) % 2 == 0, - torch.ones((), device=device, dtype=torch.int8), - -torch.ones((), device=device, dtype=torch.int8), - ) - self.register_buffer("bridge_signs", bridge_signs, persistent=False) - - self._init_product_buffers() - - def _init_leaf_product_buffers(self, device, dtype: torch.dtype) -> None: - """Precompute a small left-multiplication tensor for dense leaf GP. - - ``CliffordAlgebra.geometric_product`` gathers ``B[..., cayley_indices]``. - In partitioned products, leaves receive large leading dimensions from - right-pair batching, so that gather became the dominant allocation in - profiles. For small leaves we instead build - ``left_gp_mats[i, j, k]`` such that: - - ``(basis_i * basis_j)`` contributes ``left_gp_mats[i, j, k]`` to - output basis ``k``. + def _init_split_layout(self, split: _PartitionSplit, device) -> None: + """Store recursive split shape and basis permutation.""" + assert sorted(split.split_dims) == list(range(self.n)) - Runtime then contracts ``A`` into a left-multiplication matrix and - applies it to ``B``. The tensor is ``O(dim^3)``, so large user-forced - leaves fall back to the dense core kernel rather than allocating an - unreasonable table. - """ - if self.n > _MAX_LEFT_MATRIX_LEAF_N: - self.register_buffer("_leaf_left_gp_mats", torch.empty(0, dtype=dtype, device=device), persistent=False) - return + self.left_n = len(split.left_dims) + self.right_n = len(split.right_dims) + self.left_dim = 2**self.left_n + self.right_dim = 2**self.right_n + self._right_dims = split.right_dims + self._left_dims = split.left_dims + self.basis_permutation = _BasisPermutation(split.split_dims, device) - left_gp_mats = torch.zeros(self.dim, self.dim, self.dim, dtype=dtype, device=device) - for left_index in range(self.dim): - for output_index in range(self.dim): - right_index = int(self.core.cayley_indices[left_index, output_index].item()) - left_gp_mats[left_index, right_index, output_index] = self.core.gp_signs[ - left_index, - output_index, - ] - self.register_buffer("_leaf_left_gp_mats", left_gp_mats, persistent=False) + def _create_child_subalgebras( + self, + split: _PartitionSplit, + device, + dtype: torch.dtype, + subalgebra_cache: dict, + ) -> tuple["PartitionedCliffordAlgebra", "PartitionedCliffordAlgebra"]: + """Return cached left and right child modules for a recursive node.""" + child_kwargs = { + "device": device, + "dtype": dtype, + "leaf_n": self.leaf_n, + "product_chunk_size": self.product_chunk_size, + "exp_policy": self._exp_policy, + "fixed_iterations": self._exp_fixed_iterations, + "accumulation_dtype": self.accumulation_dtype, + "subalgebra_cache": subalgebra_cache, + } + left_sub = self._get_or_create_subalgebra(*split.left_signature, **child_kwargs) + right_sub = self._get_or_create_subalgebra(*split.right_signature, **child_kwargs) + return left_sub, right_sub @classmethod def _get_or_create_subalgebra( @@ -607,255 +850,64 @@ def _get_or_create_subalgebra( return subalgebra def _init_structural_buffers(self, device, dtype: torch.dtype) -> None: - """Precompute static tables that scale linearly in basis dimension. - - Recursive nodes intentionally avoid full ``[dim, dim]`` Cayley tables, - but many unary operations need only a per-basis sign or index vector. - These buffers mirror the dense ``CliffordAlgebra`` public contract while - keeping memory usage ``O(dim)``. + """Register linear-size structural tensors generated by pure builders.""" + structural = _structural_buffers(self.p, self.q, self.r, device, dtype) + self._register_structural_buffers(structural.buffers) + self.eps = structural.eps + self.eps_sq = structural.eps_sq + + def _register_structural_buffers(self, buffers: tuple[tuple[str, torch.Tensor], ...]) -> None: + """Attach generated tensors as non-persistent buffers.""" + for name, tensor in buffers: + self.register_buffer(name, tensor, persistent=False) + + def _init_product_plan(self) -> None: + """Initialize recursive product shape planning without baked routing. + + Right-pair indices, metric signs, and bridge signs are derived per + product range at runtime. This keeps the + recursive kernel from carrying signature-specific pair tables. """ - # Reversion sign: reverse a grade-k blade by k(k-1)/2 swaps. - rev_signs = ((-1.0) ** (self.grade_index * (self.grade_index - 1) // 2)).to( - dtype=dtype, - ) + plan = _product_plan(self.right_n, self.right_sub.r, self.product_chunk_size) + self._right_pair_count = plan.right_pair_count + self._product_chunk_size = plan.chunk_size - # Main involution and Clifford conjugation are diagonal operations in - # the canonical basis. - involution_signs = torch.where( - self.grade_index % 2 == 0, - torch.ones((), dtype=dtype, device=device), - -torch.ones((), dtype=dtype, device=device), - ) - conj_signs = (involution_signs * rev_signs).to(dtype=dtype) - - self.register_buffer("rev_signs", rev_signs, persistent=False) - self.register_buffer("_involution_signs", involution_signs, persistent=False) - self.register_buffer("conj_signs", conj_signs, persistent=False) - - # ``cayley_diag[i]`` is the scalar sign of basis_i * reverse(basis_i). - # It is enough to implement norm and Hermitian forms without a full - # Cayley table. - basis_indices = torch.arange(self.dim, dtype=torch.long, device=device) - negative_mask = 0 - for bit in range(self.p, self.p + self.q): - negative_mask |= 1 << bit - negative_count = self.grade_index[basis_indices & negative_mask] - metric_signs = torch.where( - negative_count % 2 == 0, - torch.ones((), dtype=dtype, device=device), - -torch.ones((), dtype=dtype, device=device), - ) - if self.r > 0: - null_mask = 0 - for bit in range(self.p + self.q, self.n): - null_mask |= 1 << bit - metric_signs = torch.where( - (basis_indices & null_mask) == 0, - metric_signs, - torch.zeros_like(metric_signs), - ) + def _to_split_order(self, mv: torch.Tensor) -> torch.Tensor: + """Convert public canonical coefficients to this node's split order.""" + return self.basis_permutation.to_split_order(mv) - cayley_diag = rev_signs * metric_signs - self.register_buffer("_cayley_diag", cayley_diag, persistent=False) - self.register_buffer("_norm_sq_signs", (rev_signs * cayley_diag).clone(), persistent=False) - self.register_buffer("_hermitian_signs", (conj_signs * cayley_diag).clone(), persistent=False) - - # Multiplication by the pseudoscalar is also a fixed permutation/sign - # vector: x * I maps source basis ``i ^ I`` into target basis ``i``. - pseudoscalar_index = self.dim - 1 - ps_source = basis_indices ^ pseudoscalar_index - ps_target = torch.full_like(ps_source, pseudoscalar_index) - ps_signs = _basis_product_signs(ps_source, ps_target, self.p, self.q, self.r, dtype) - self.register_buffer("_ps_source", ps_source, persistent=False) - self.register_buffer("_ps_signs", ps_signs, persistent=False) - - if self.n >= 2: - bv_indices = (self.grade_index == 2).nonzero(as_tuple=False).squeeze(-1) - bv_sq_scalar = torch.zeros(len(bv_indices), dtype=dtype, device=device) - rc_action = torch.zeros(len(bv_indices), self.n, self.n, dtype=dtype, device=device) - for bivector_position, blade_index in enumerate(bv_indices.tolist()): - active_bits = [bit for bit in range(self.n) if blade_index & (1 << bit)] - if len(active_bits) != 2: - continue - first_bit, second_bit = active_bits - first_square = self._vector_square(first_bit) - second_square = self._vector_square(second_bit) - bv_sq_scalar[bivector_position] = -first_square * second_square - rc_action[bivector_position, first_bit, second_bit] = second_square - rc_action[bivector_position, second_bit, first_bit] = -first_square - else: - bv_indices = torch.zeros(0, dtype=torch.long, device=device) - bv_sq_scalar = torch.zeros(0, dtype=dtype, device=device) - rc_action = torch.zeros(0, self.n, self.n, dtype=dtype, device=device) - - self.register_buffer("_bv_indices", bv_indices, persistent=False) - self.register_buffer("bv_sq_scalar", bv_sq_scalar, persistent=False) - self.register_buffer("rc_action", rc_action, persistent=False) - - g1_idx = (1 << torch.arange(self.n, device=device)).long() - self.register_buffer("_g1_indices", g1_idx, persistent=False) - - # Left contraction keeps grade pairs (a, b) where a <= b and then - # projects the product to grade b-a. - lc_grade_a = [] - lc_grade_b = [] - lc_grade_result = [] - for grade_a in range(self.n + 1): - for grade_b in range(grade_a, self.n + 1): - lc_grade_a.append(grade_a) - lc_grade_b.append(grade_b) - lc_grade_result.append(grade_b - grade_a) - - self.register_buffer( - "_lc_grade_a", - torch.tensor(lc_grade_a, dtype=torch.long, device=device), - persistent=False, - ) - self.register_buffer( - "_lc_grade_b", - torch.tensor(lc_grade_b, dtype=torch.long, device=device), - persistent=False, - ) - self.register_buffer( - "_lc_grade_result", - torch.tensor(lc_grade_result, dtype=torch.long, device=device), - persistent=False, - ) + def _to_public_order(self, mv: torch.Tensor) -> torch.Tensor: + """Convert split-order coefficients back to public canonical order.""" + return self.basis_permutation.to_public_order(mv) - # Common products are linear combinations of AB and BA. Rows encode: - # wedge, inner, commutator, anti-commutator. - product_weights = torch.tensor( - [ - [0.5, -0.5], - [0.5, 0.5], - [1.0, -1.0], - [1.0, 1.0], - ], - dtype=dtype, - device=device, + def _bridge_signs_for_right_b(self, right_b_indices: torch.Tensor, dtype: torch.dtype) -> torch.Tensor: + """Compute ``(-1) ** (grade(left_A) * grade(right_B))`` for a pair slice.""" + right_grades = torch.index_select(self.right_sub.grade_index, 0, right_b_indices).unsqueeze(1) + left_grades = self.left_sub.grade_index.unsqueeze(0) + signs = torch.where( + (right_grades * left_grades) % 2 == 0, + torch.ones((), dtype=dtype, device=right_b_indices.device), + -torch.ones((), dtype=dtype, device=right_b_indices.device), ) - self.register_buffer("_product_pair_weights", product_weights, persistent=False) + return signs - _finfo = torch.finfo(dtype) - self.eps: float = float(_finfo.eps) - self.eps_sq: float = float(_finfo.eps**2) - - def _init_product_buffers(self) -> None: - """Precompute right-block product routing for recursive GP. - - A recursive product sums over all right-child basis products: - - ``(A_l,a * B_l,b)`` contributes to right result ``a ^ b`` with the - right-subalgebra metric sign and the bridge sign. The left products are - still computed recursively at runtime; these buffers only describe how - to select right blocks and merge them back. - """ - right_indices = torch.arange(self.right_dim, device=self.device) - right_a_indices, right_b_indices = torch.meshgrid(right_indices, right_indices, indexing="ij") - right_a_indices = right_a_indices.reshape(-1) - right_b_indices = right_b_indices.reshape(-1) - right_result_indices = (right_a_indices ^ right_b_indices).long() - - right_product_signs = _basis_product_signs( + def _right_product_signs( + self, + right_a_indices: torch.Tensor, + right_b_indices: torch.Tensor, + dtype: torch.dtype, + ) -> torch.Tensor: + """Compute right-child basis-product signs for one runtime pair slice.""" + return _basis_product_signs( right_a_indices, right_b_indices, self.right_sub.p, self.right_sub.q, self.right_sub.r, - torch.int8, + dtype, + popcount=self.right_sub.grade_index, ) - if self.right_sub.r == 0: - # Non-degenerate right algebras have no zero products, so - # pair_a/pair_b can be reconstructed from a linear range instead - # of stored as two extra ``right_dim ** 2`` buffers. - pair_count = self.right_dim * self.right_dim - right_product_signs = right_product_signs.reshape(-1) - self._right_pair_full = True - self.register_buffer("_right_pair_signs", right_product_signs, persistent=False) - else: - # Degenerate signatures have repeated null factors that produce - # zero. Store only nonzero right interactions so runtime never - # computes left products that will be discarded. - nonzero = right_product_signs != 0 - right_a_pair_indices = right_a_indices[nonzero].long() - right_b_pair_indices = right_b_indices[nonzero].long() - right_result_indices = right_result_indices[nonzero].long() - right_product_signs = right_product_signs[nonzero] - pair_count = int(right_a_pair_indices.numel()) - - self._right_pair_full = False - self.register_buffer("_right_pair_a", right_a_pair_indices, persistent=False) - self.register_buffer("_right_pair_b", right_b_pair_indices, persistent=False) - self.register_buffer("_right_pair_result", right_result_indices, persistent=False) - self.register_buffer("_right_pair_signs", right_product_signs, persistent=False) - - self._right_pair_count = pair_count - if self.product_chunk_size is None: - # Full-vectorize shallow nodes. Deeper nodes default to chunks so a - # high-dimensional product does not materialize all right-pair left - # products at once. - default_chunk = pair_count if self.left_n <= self.leaf_n else min(pair_count, 64) - self._product_chunk_size = max(1, default_chunk) - else: - self._product_chunk_size = max(1, int(self.product_chunk_size)) - - self._has_sparse_right_interaction = self._product_chunk_size >= self._right_pair_count - if self._has_sparse_right_interaction: - self._init_right_interaction_buffers(right_result_indices, right_product_signs) - - def _init_right_interaction_buffers( - self, - right_result_indices: torch.Tensor, - right_product_signs: torch.Tensor, - ) -> None: - """Precompute sparse right-product routing from pair terms to result blocks.""" - pair_columns = torch.arange(self._right_pair_count, dtype=torch.long, device=self.device) - interaction_indices = torch.stack((right_result_indices.long(), pair_columns)) - interaction = torch.sparse_coo_tensor( - interaction_indices, - right_product_signs.to(dtype=self.dtype), - (self.right_dim, self._right_pair_count), - device=self.device, - ).coalesce() - self.register_buffer("_right_interaction", interaction, persistent=False) - - @property - def _uses_basis_permutation(self) -> bool: - """Whether this node needs public/split basis conversion.""" - return self.basis_permutation.uses_permutation - - @property - def _to_split_basis(self) -> torch.Tensor: - """Public source indices for split-order coefficients.""" - return self.basis_permutation.split_to_public - - @property - def _to_public_basis(self) -> torch.Tensor: - """Split source indices for public-order coefficients.""" - return self.basis_permutation.public_to_split - - @property - def _split_basis_signs(self) -> torch.Tensor: - """Orientation signs indexed by split-order basis index.""" - return self.basis_permutation.split_signs - - def _to_split_order(self, mv: torch.Tensor) -> torch.Tensor: - """Convert public canonical coefficients to this node's split order.""" - return self.basis_permutation.to_split_order(mv) - - def _to_public_order(self, mv: torch.Tensor) -> torch.Tensor: - """Convert split-order coefficients back to public canonical order.""" - return self.basis_permutation.to_public_order(mv) - - def _vector_square(self, bit: int) -> float: - """Return ``e_bit ** 2`` from the global signature.""" - if bit < self.p: - return 1.0 - if bit < self.p + self.q: - return -1.0 - return 0.0 - @property def device(self): """Return the device of the algebra buffers.""" @@ -1047,21 +1099,11 @@ def _leaf_geometric_product( output_dtype: torch.dtype, compute_dtype: torch.dtype, ) -> torch.Tensor: - """Compute a dense leaf product with the profiled small-leaf kernel.""" + """Compute a dense leaf product with the local dense kernel.""" A_compute = A.to(dtype=compute_dtype) B_compute = B.to(dtype=compute_dtype) - if self._leaf_left_gp_mats.numel() == 0: - result = self.core.geometric_product(A_compute, B_compute) - else: - left_gp_mats = self._leaf_left_gp_mats - if left_gp_mats.dtype != compute_dtype: - left_gp_mats = left_gp_mats.to(dtype=compute_dtype) - - # left_matrices[..., j, k] = sum_i A[..., i] * sign(i,j,k). - # Multiplying B as a row vector then gives result[..., k]. - left_matrices = torch.einsum("...i,ijk->...jk", A_compute, left_gp_mats) - result = torch.matmul(B_compute.unsqueeze(-2), left_matrices).squeeze(-2) + result = self.core.geometric_product(A_compute, B_compute) if result.dtype != output_dtype: result = result.to(dtype=output_dtype) @@ -1099,19 +1141,8 @@ def geometric_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: self.left_dim, self.right_dim, ) - A_by_right_blade = A_by_left_then_right.transpose(-1, -2).contiguous() - B_by_right_blade = B_by_left_then_right.transpose(-1, -2).contiguous() - - if self._product_chunk_size >= self._right_pair_count: - result_blocks = self._geometric_product_pair_range( - A_by_right_blade, - B_by_right_blade, - 0, - self._right_pair_count, - ) - output_shape = result_blocks.shape[:-2] - result = result_blocks.reshape(*output_shape, self.dim) - return self._to_public_order(result).to(dtype=output_dtype) + A_by_right_blade = A_by_left_then_right.transpose(-1, -2) + B_by_right_blade = B_by_left_then_right.transpose(-1, -2) result_blocks = None for start in range(0, self._right_pair_count, self._product_chunk_size): @@ -1141,60 +1172,40 @@ def _geometric_product_pair_range( right_product_signs, ) = self._right_product_slice(start, end) + if right_product_signs.numel() == 0: + batch_shape = torch.broadcast_shapes(A_by_right_blade.shape[:-2], B_by_right_blade.shape[:-2]) + return A_by_right_blade.new_zeros(*batch_shape, self.left_dim, self.right_dim) + A_terms = torch.index_select(A_by_right_blade, -2, right_a_indices) B_terms = torch.index_select(B_by_right_blade, -2, right_b_indices) # ``bridge_signs[right_b, left_a]`` depends on the left basis index of # each selected A term, so broadcasting over the final left_dim axis # attaches the sign before the recursive left product. - bridge_signs = torch.index_select(self.bridge_signs, 0, right_b_indices) + bridge_signs = self._bridge_signs_for_right_b(right_b_indices, A_terms.dtype) if bridge_signs.dtype != A_terms.dtype: bridge_signs = bridge_signs.to(dtype=A_terms.dtype) A_terms = A_terms * bridge_signs left_products = self.left_sub.geometric_product(A_terms, B_terms) - return self._merge_right_interactions(left_products, start, end, right_result_indices, right_product_signs) + return self._merge_right_interactions(left_products, right_result_indices, right_product_signs) def _merge_right_interactions( self, left_products: torch.Tensor, - start: int, - end: int, right_result_indices: torch.Tensor, right_product_signs: torch.Tensor, ) -> torch.Tensor: """Merge left products into ``[..., left_dim, right_dim]`` result blocks.""" - if not self._use_sparse_right_interaction(start, end): - return self._merge_right_interactions_index_add(left_products, right_result_indices, right_product_signs) - - return self._merge_right_interactions_sparse(left_products) - - def _use_sparse_right_interaction(self, start: int, end: int) -> bool: - """Return whether the static sparse interaction should handle this range.""" - return ( - self._has_sparse_right_interaction - and self.device.type == "cuda" - and start == 0 - and end == self._right_pair_count - ) + if right_product_signs.numel() == 0: + return left_products.new_zeros( + *left_products.shape[:-2], + self.left_dim, + self.right_dim, + ) - def _merge_right_interactions_sparse(self, left_products: torch.Tensor) -> torch.Tensor: - """Merge a full right-pair range with the baked sparse interaction matrix.""" - if not self._has_sparse_right_interaction: - raise RuntimeError("sparse right interaction is only available for full-pair product nodes") - pair_count = self._right_pair_count - interaction = self._right_interaction_tensor(left_products.dtype) - batch_shape = left_products.shape[:-2] - # sparse.mm expects [right_dim, pair_count] @ [pair_count, batch*left_dim]. - flat_terms = left_products.transpose(-1, -2).reshape(-1, pair_count) - merged = torch.sparse.mm(interaction, flat_terms.transpose(0, 1)) - return merged.transpose(0, 1).reshape(*batch_shape, self.left_dim, self.right_dim) - - def _right_interaction_tensor(self, dtype: torch.dtype) -> torch.Tensor: - """Return the full sparse interaction tensor in the requested dtype.""" - interaction = self._right_interaction - return interaction if interaction.dtype == dtype else interaction.to(dtype=dtype) + return self._merge_right_interactions_index_add(left_products, right_result_indices, right_product_signs) def _merge_right_interactions_index_add( self, @@ -1202,7 +1213,7 @@ def _merge_right_interactions_index_add( right_result_indices: torch.Tensor, right_product_signs: torch.Tensor, ) -> torch.Tensor: - """Fallback merge for devices without sparse COO matmul support.""" + """Merge right-pair contributions with direct indexed accumulation.""" signed_products = left_products.transpose(-1, -2) * right_product_signs.to(dtype=left_products.dtype) result_blocks = left_products.new_zeros( *left_products.shape[:-2], @@ -1230,27 +1241,30 @@ def _right_product_slice( start: int, end: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """Return right-block routing tensors for pair range ``[start, end)``. + """Derive right-block routing tensors for pair range ``[start, end)``. Returns: tuple: ``(right_a_indices, right_b_indices, right_result_indices, right_product_signs)``. Each position describes one right basis-pair contribution in the recursive product. """ - if self._right_pair_full: - linear_pair_indices = torch.arange(start, end, dtype=torch.long, device=self.device) - right_a_indices = torch.div(linear_pair_indices, self.right_dim, rounding_mode="floor") - right_b_indices = linear_pair_indices.remainder(self.right_dim) - right_result_indices = right_a_indices ^ right_b_indices - right_product_signs = self._right_pair_signs.reshape(-1)[start:end] - return right_a_indices, right_b_indices, right_result_indices, right_product_signs + pair_indices = torch.arange(start, end, dtype=torch.long, device=self.device) + if self.right_sub.r == 0: + right_a_indices = torch.div(pair_indices, self.right_dim, rounding_mode="floor") + right_b_indices = pair_indices.remainder(self.right_dim) + else: + right_a_indices, right_b_indices = _compact_surviving_basis_pairs( + pair_indices, + self.right_n, + self.right_sub.p, + self.right_sub.q, + self.right_sub.r, + ) - return ( - self._right_pair_a[start:end], - self._right_pair_b[start:end], - self._right_pair_result[start:end], - self._right_pair_signs[start:end], - ) + right_result_indices = right_a_indices ^ right_b_indices + right_product_signs = self._right_product_signs(right_a_indices, right_b_indices, torch.int8) + + return right_a_indices, right_b_indices, right_result_indices, right_product_signs def grade_projection(self, mv: torch.Tensor, grade: int) -> torch.Tensor: """Project a multivector onto a grade using the same mask contract as the core algebra.""" From abde2e8741f2ec32d7df4ef584eae3f4a55ff67a Mon Sep 17 00:00:00 2001 From: Concode0 Date: Fri, 1 May 2026 20:43:56 +0900 Subject: [PATCH 03/45] test: add partitioned algebra verification coverage --- tests/test_partitioned_algebra.py | 746 ++++++++++++++++++++++++++++++ tests/test_partitioned_highdim.py | 728 +++++++++++++++++++++++++++++ 2 files changed, 1474 insertions(+) create mode 100644 tests/test_partitioned_algebra.py create mode 100644 tests/test_partitioned_highdim.py diff --git a/tests/test_partitioned_algebra.py b/tests/test_partitioned_algebra.py new file mode 100644 index 0000000..b1d36ed --- /dev/null +++ b/tests/test_partitioned_algebra.py @@ -0,0 +1,746 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +import pytest +import torch +import torch.nn as nn + +from core.algebra import CliffordAlgebra +from core.partitioned_algebra import PartitionedCliffordAlgebra + +pytestmark = pytest.mark.unit + +DEVICE = "cpu" + + +def _dtype_tolerance(dtype: torch.dtype) -> float: + if dtype == torch.float16: + return 5e-3 + if dtype == torch.bfloat16: + return 5e-2 + if dtype == torch.float32: + return 2e-5 + return 1e-10 + + +def _make_pair(p=3, q=1, r=0, *, leaf_n=2, product_chunk_size=None, dtype=torch.float64): + reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype) + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=dtype, + leaf_n=leaf_n, + product_chunk_size=product_chunk_size, + ) + return reference, algebra + + +def _assert_matches_monolithic(p, q=0, r=0, *, leaf_n=6, shape=(3,), dtype=torch.float64): + torch.manual_seed(17) + reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype) + algebra = PartitionedCliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype, leaf_n=leaf_n) + + dim = 2 ** (p + q + r) + A = torch.randn(*shape, dim, dtype=dtype) + B = torch.randn(*shape, dim, dtype=dtype) + + expected = reference.geometric_product(A, B) + actual = algebra.geometric_product(A, B) + + assert torch.allclose(actual, expected, atol=1e-9, rtol=1e-9) + + +class _PartitionedProductLayer(nn.Module): + def __init__(self, p, q=0, r=0): + super().__init__() + self.algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float32, + leaf_n=2, + product_chunk_size=4, + ) + self.weight = nn.Parameter(torch.randn(self.algebra.dim)) + + def forward(self, x): + weight = self.weight.expand_as(x) + return self.algebra.geometric_product(x, weight) + + +class TestPartitionedCliffordAlgebra: + def test_leaf_matches_core_kernel(self): + _assert_matches_monolithic(3, 1, 0, leaf_n=6, shape=(2,)) + + def test_forced_recursive_euclidean_matches_core_kernel(self): + _assert_matches_monolithic(4, 0, 0, leaf_n=2, shape=(4,)) + + def test_default_recursive_cl8_matches_core_kernel(self): + _assert_matches_monolithic(8, 0, 0, leaf_n=6, shape=(2,)) + + def test_recursive_tree_uses_balanced_binary_splits(self): + algebra = PartitionedCliffordAlgebra(20, 0, 0, device=DEVICE, leaf_n=6) + + assert algebra.left_n == 10 + assert algebra.right_n == 10 + assert algebra.left_sub.left_n == 5 + assert algebra.left_sub.right_n == 5 + assert algebra.right_sub.left_n == 5 + assert algebra.right_sub.right_n == 5 + + assert not hasattr(algebra.left_sub, "cayley_indices") + assert not hasattr(algebra.right_sub, "cayley_indices") + + def test_describe_tree_reports_split_layout_and_shared_nodes(self, capsys): + algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=2) + + tree = algebra.describe_tree() + lines = tree.splitlines() + + assert lines[0] == ( + "root: Cl(8,0,0), n=8, dim=256, bits=[0, 8), " + "split left=4 bits=[4, 8), right=4 bits=[0, 4), pairs=256, chunk=64" + ) + assert "root.L: Cl(4,0,0), n=4, dim=16, bits=[4, 8)" in lines[1] + assert "root.R: Cl(4,0,0), n=4, dim=16, bits=[0, 4)" in tree + assert "shared_with=root.L" in tree + + algebra.print_tree() + assert capsys.readouterr().out.strip() == tree + + def test_repeated_signature_tiles_share_subalgebras_automatically(self): + algebra = PartitionedCliffordAlgebra( + 8, + 4, + 4, + device=DEVICE, + leaf_n=4, + ) + + assert (algebra.left_sub.p, algebra.left_sub.q, algebra.left_sub.r) == (4, 2, 2) + assert (algebra.right_sub.p, algebra.right_sub.q, algebra.right_sub.r) == (4, 2, 2) + assert algebra.left_sub is algebra.right_sub + assert (algebra.left_sub.left_sub.p, algebra.left_sub.left_sub.q, algebra.left_sub.left_sub.r) == (2, 1, 1) + assert algebra.left_sub.left_sub is algebra.left_sub.right_sub + assert algebra.basis_permutation.uses_permutation + + tree = algebra.describe_tree() + assert "root: Cl(8,4,4)" in tree + assert "root.L: Cl(4,2,2)" in tree + assert "root.L.L: Cl(2,1,1)" in tree + assert "shared_with=root.L" in tree + assert "shared_with=root.L.L" in tree + + def test_repeated_signature_tile_product_matches_core_kernel_with_basis_permutation(self): + torch.manual_seed(107) + reference = CliffordAlgebra(4, 2, 2, device=DEVICE, dtype=torch.float64) + algebra = PartitionedCliffordAlgebra( + 4, + 2, + 2, + device=DEVICE, + dtype=torch.float64, + leaf_n=4, + ) + A = torch.randn(2, algebra.dim, dtype=torch.float64) + B = torch.randn(2, algebra.dim, dtype=torch.float64) + + assert algebra.basis_permutation.uses_permutation + assert torch.allclose( + algebra.geometric_product(A, B), reference.geometric_product(A, B), atol=1e-10, rtol=1e-10 + ) + + def test_repeated_signature_tile_product_gradients_match_core_kernel(self): + torch.manual_seed(109) + reference = CliffordAlgebra(4, 2, 2, device=DEVICE, dtype=torch.float64) + algebra = PartitionedCliffordAlgebra( + 4, + 2, + 2, + device=DEVICE, + dtype=torch.float64, + leaf_n=4, + ) + + A_ref = torch.randn(2, algebra.dim, dtype=torch.float64, requires_grad=True) + B_ref = torch.randn(2, algebra.dim, dtype=torch.float64, requires_grad=True) + A_partitioned = A_ref.detach().clone().requires_grad_(True) + B_partitioned = B_ref.detach().clone().requires_grad_(True) + + reference.geometric_product(A_ref, B_ref).square().sum().backward() + algebra.geometric_product(A_partitioned, B_partitioned).square().sum().backward() + + assert torch.allclose(A_partitioned.grad, A_ref.grad, atol=1e-9, rtol=1e-9) + assert torch.allclose(B_partitioned.grad, B_ref.grad, atol=1e-9, rtol=1e-9) + + def test_identical_recursive_subalgebras_are_shared(self): + algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=2) + + assert algebra.left_sub is algebra.right_sub + assert algebra.left_sub.left_sub is algebra.left_sub.right_sub + + def test_recursive_node_uses_compact_memory_layout(self): + algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, leaf_n=2) + + assert "_grade_masks_float" not in algebra._buffers + assert "_grade_masks_float_T" not in algebra._buffers + assert torch.equal(algebra.grade_masks[2], algebra.grade_index == 2) + assert algebra.grade_masks_float.dtype == algebra.dtype + + assert algebra._right_pair_count == algebra.right_dim * algebra.right_dim + assert not hasattr(algebra, "_right_pair_full") + assert not hasattr(algebra, "_right_pair_a") + assert not hasattr(algebra, "_right_pair_b") + assert not hasattr(algebra, "_right_pair_result") + assert not hasattr(algebra, "_right_pair_signs") + assert not hasattr(algebra, "_right_interaction") + assert not hasattr(algebra, "bridge_signs") + assert not hasattr(algebra, "_uses_basis_permutation") + assert not hasattr(algebra, "_to_split_basis") + assert not hasattr(algebra, "_to_public_basis") + assert not hasattr(algebra, "_split_basis_signs") + _, _, pair_result, pair_signs = algebra._right_product_slice(0, algebra._right_pair_count) + assert pair_result.shape == (algebra._right_pair_count,) + assert pair_signs.dtype == torch.int8 + assert not algebra.basis_permutation.uses_permutation + assert algebra.basis_permutation.split_to_public.numel() == 0 + assert algebra.basis_permutation.public_to_split.numel() == 0 + assert algebra.basis_permutation.split_signs.numel() == 0 + + def test_default_recursive_mixed_signature_matches_core_kernel(self): + _assert_matches_monolithic(5, 2, 1, leaf_n=6, shape=(2,)) + + def test_recursive_product_supports_extra_batch_axes(self): + _assert_matches_monolithic(4, 1, 0, leaf_n=3, shape=(2, 3)) + + def test_recursive_product_gradients_match_core_kernel(self): + torch.manual_seed(23) + reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) + algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64, leaf_n=2) + + A_ref = torch.randn(2, 16, dtype=torch.float64, requires_grad=True) + B_ref = torch.randn(2, 16, dtype=torch.float64, requires_grad=True) + A_partitioned = A_ref.detach().clone().requires_grad_(True) + B_partitioned = B_ref.detach().clone().requires_grad_(True) + + reference.geometric_product(A_ref, B_ref).square().sum().backward() + algebra.geometric_product(A_partitioned, B_partitioned).square().sum().backward() + + assert torch.allclose(A_partitioned.grad, A_ref.grad, atol=1e-9, rtol=1e-9) + assert torch.allclose(B_partitioned.grad, B_ref.grad, atol=1e-9, rtol=1e-9) + + @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32, torch.float64]) + def test_recursive_operations_support_generic_floating_dtypes(self, dtype): + torch.manual_seed(67) + reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=dtype) + algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=dtype, leaf_n=2) + atol = _dtype_tolerance(dtype) + + A = torch.randn(2, algebra.dim, dtype=dtype) * 0.25 + B = torch.randn(2, algebra.dim, dtype=dtype) * 0.25 + + for method_name in [ + "geometric_product", + "wedge", + "inner_product", + "commutator", + "anti_commutator", + "left_contraction", + ]: + expected = getattr(reference, method_name)(A, B) + actual = getattr(algebra, method_name)(A, B) + assert actual.dtype == dtype + assert torch.allclose(actual.float(), expected.float(), atol=atol, rtol=atol), method_name + + mv = torch.randn(2, algebra.dim, dtype=dtype) * 0.25 + assert algebra.norm_sq(mv).dtype == dtype + assert torch.allclose(algebra.norm_sq(mv).float(), reference.norm_sq(mv).float(), atol=atol, rtol=atol) + + bivector = torch.zeros(1, algebra.dim, dtype=dtype) + bivector[0, 3] = 0.125 + actual_exp = algebra.exp(bivector) + expected_exp = reference.exp(bivector) + assert actual_exp.dtype == dtype + assert torch.allclose(actual_exp.float(), expected_exp.float(), atol=atol, rtol=atol) + + @pytest.mark.parametrize( + ("algebra_dtype", "input_dtype", "expected_dtype"), + [ + (torch.float64, torch.float32, torch.float64), + (torch.float32, torch.float16, torch.float32), + (torch.float16, torch.bfloat16, torch.float32), + (torch.bfloat16, torch.float16, torch.float32), + ], + ) + @pytest.mark.parametrize("leaf_n", [2, 6]) + def test_geometric_product_promotes_inputs_with_algebra_dtype( + self, + algebra_dtype, + input_dtype, + expected_dtype, + leaf_n, + ): + torch.manual_seed(71) + algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=algebra_dtype, leaf_n=leaf_n) + reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=expected_dtype) + atol = _dtype_tolerance(expected_dtype) + + A = torch.randn(2, algebra.dim, dtype=input_dtype) * 0.25 + B = torch.randn(2, algebra.dim, dtype=input_dtype) * 0.25 + + actual = algebra.geometric_product(A, B) + expected = reference.geometric_product(A.to(dtype=expected_dtype), B.to(dtype=expected_dtype)) + + assert actual.dtype == expected_dtype + assert torch.allclose(actual.float(), expected.float(), atol=atol, rtol=atol) + + bivector = torch.zeros(1, algebra.dim, dtype=input_dtype) + bivector[0, 3] = 0.125 + actual_exp = algebra.exp(bivector) + expected_exp = reference.exp(bivector.to(dtype=expected_dtype)) + + assert actual_exp.dtype == expected_dtype + assert torch.allclose(actual_exp.float(), expected_exp.float(), atol=atol, rtol=atol) + + def test_stable_accumulation_reduces_cumulative_forward_error(self): + torch.manual_seed(73) + reference = CliffordAlgebra(8, 0, 0, device=DEVICE, dtype=torch.float64) + standard = PartitionedCliffordAlgebra( + 8, 0, 0, device=DEVICE, dtype=torch.float32, leaf_n=4, product_chunk_size=1 + ) + stable = PartitionedCliffordAlgebra( + 8, + 0, + 0, + device=DEVICE, + dtype=torch.float32, + leaf_n=4, + product_chunk_size=1, + accumulation_dtype=torch.float64, + ) + assert stable.left_sub.accumulation_dtype == torch.float64 + + factors = [torch.randn(1, standard.dim, dtype=torch.float32) * 0.02 for _ in range(5)] + expected = factors[0].double() + actual_standard = factors[0] + actual_stable = factors[0] + for factor in factors[1:]: + expected = reference.geometric_product(expected, factor.double()) + actual_standard = standard.geometric_product(actual_standard, factor) + actual_stable = stable.geometric_product(actual_stable, factor) + + standard_error = (actual_standard.double() - expected).norm() + stable_error = (actual_stable.double() - expected).norm() + + assert actual_stable.dtype == torch.float32 + assert stable_error < standard_error * 0.5 + + def test_stable_accumulation_reduces_cumulative_backward_error(self): + torch.manual_seed(79) + reference = CliffordAlgebra(8, 0, 0, device=DEVICE, dtype=torch.float64) + standard = PartitionedCliffordAlgebra( + 8, 0, 0, device=DEVICE, dtype=torch.float32, leaf_n=4, product_chunk_size=1 + ) + stable = PartitionedCliffordAlgebra( + 8, + 0, + 0, + device=DEVICE, + dtype=torch.float32, + leaf_n=4, + product_chunk_size=1, + accumulation_dtype=torch.float64, + ) + + A = torch.randn(2, standard.dim, dtype=torch.float32) * 0.02 + B = torch.randn(2, standard.dim, dtype=torch.float32) * 0.02 + A_ref = A.double().requires_grad_(True) + B_ref = B.double().requires_grad_(True) + A_standard = A.detach().clone().requires_grad_(True) + B_standard = B.detach().clone().requires_grad_(True) + A_stable = A.detach().clone().requires_grad_(True) + B_stable = B.detach().clone().requires_grad_(True) + + reference.geometric_product(reference.geometric_product(A_ref, B_ref), B_ref).square().sum().backward() + standard.geometric_product( + standard.geometric_product(A_standard, B_standard), B_standard + ).square().sum().backward() + stable.geometric_product(stable.geometric_product(A_stable, B_stable), B_stable).square().sum().backward() + + standard_error = (A_standard.grad.double() - A_ref.grad).norm() + (B_standard.grad.double() - B_ref.grad).norm() + stable_error = (A_stable.grad.double() - A_ref.grad).norm() + (B_stable.grad.double() - B_ref.grad).norm() + + assert A_stable.grad.dtype == torch.float32 + assert B_stable.grad.dtype == torch.float32 + assert stable_error < standard_error * 0.7 + + def test_recursive_product_chunked_pair_merge_matches_core_kernel(self): + torch.manual_seed(29) + reference, algebra = _make_pair(5, 1, 0, leaf_n=2, product_chunk_size=3) + A = torch.randn(2, 1, algebra.dim, dtype=torch.float64) + B = torch.randn(1, 3, algebra.dim, dtype=torch.float64) + + expected = reference.geometric_product(A, B) + actual = algebra.geometric_product(A, B) + + assert torch.allclose(actual, expected, atol=1e-9, rtol=1e-9) + + @pytest.mark.parametrize("pair_range", ["full", "chunk"]) + def test_indexed_right_interaction_merge_matches_reference(self, pair_range): + torch.manual_seed(31) + product_chunk_size = None if pair_range == "full" else 3 + algebra = PartitionedCliffordAlgebra( + 5, + 1, + 0, + device=DEVICE, + dtype=torch.float64, + leaf_n=2, + product_chunk_size=product_chunk_size, + ) + if pair_range == "full": + start, end = 0, algebra._right_pair_count + else: + start, end = algebra._product_chunk_size, 2 * algebra._product_chunk_size + + _, _, pair_result, pair_signs = algebra._right_product_slice(start, end) + pair_count = int(pair_signs.numel()) + left_products = torch.randn(2, pair_count, algebra.left_dim, dtype=torch.float64) + merged_terms = left_products.clone().requires_grad_(True) + index_terms = left_products.clone().requires_grad_(True) + + actual = algebra._merge_right_interactions(merged_terms, pair_result, pair_signs) + expected = algebra._merge_right_interactions_index_add(index_terms, pair_result, pair_signs) + + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + weight = torch.randn_like(actual) + (actual * weight).sum().backward() + (expected * weight).sum().backward() + + assert torch.allclose(merged_terms.grad, index_terms.grad, atol=1e-12, rtol=1e-12) + + @pytest.mark.parametrize(("p", "q", "r"), [(5, 1, 0), (1, 1, 4)]) + def test_vectorized_full_pair_product_matches_chunked_with_gradients(self, p, q, r): + torch.manual_seed(83 + p * 13 + q * 7 + r) + reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=torch.float64) + vectorized = PartitionedCliffordAlgebra(p, q, r, device=DEVICE, dtype=torch.float64, leaf_n=3) + chunked = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float64, + leaf_n=3, + product_chunk_size=5, + ) + + assert vectorized._product_chunk_size >= vectorized._right_pair_count + assert chunked._product_chunk_size < chunked._right_pair_count + + A = torch.randn(2, vectorized.dim, dtype=torch.float64) + B = torch.randn(2, vectorized.dim, dtype=torch.float64) + A_ref = A.clone().requires_grad_(True) + B_ref = B.clone().requires_grad_(True) + A_vectorized = A.clone().requires_grad_(True) + B_vectorized = B.clone().requires_grad_(True) + A_chunked = A.clone().requires_grad_(True) + B_chunked = B.clone().requires_grad_(True) + + expected = reference.geometric_product(A_ref, B_ref) + actual_vectorized = vectorized.geometric_product(A_vectorized, B_vectorized) + actual_chunked = chunked.geometric_product(A_chunked, B_chunked) + + assert torch.allclose(actual_vectorized, expected, atol=1e-10, rtol=1e-10) + assert torch.allclose(actual_chunked, expected, atol=1e-10, rtol=1e-10) + + weight = torch.linspace(-0.3, 0.4, vectorized.dim, dtype=torch.float64) + (expected * weight).sum().backward() + (actual_vectorized * weight).sum().backward() + (actual_chunked * weight).sum().backward() + + assert torch.allclose(A_vectorized.grad, A_ref.grad, atol=1e-10, rtol=1e-10) + assert torch.allclose(B_vectorized.grad, B_ref.grad, atol=1e-10, rtol=1e-10) + assert torch.allclose(A_chunked.grad, A_ref.grad, atol=1e-10, rtol=1e-10) + assert torch.allclose(B_chunked.grad, B_ref.grad, atol=1e-10, rtol=1e-10) + + def test_unit_rotor_chain_maintains_normalization_beyond_depth_threshold(self): + algebra = PartitionedCliffordAlgebra( + 8, + 0, + 0, + device=DEVICE, + dtype=torch.float32, + leaf_n=4, + product_chunk_size=16, + accumulation_dtype=torch.float64, + ) + bivector = torch.zeros(1, algebra.dim, dtype=torch.float32) + bivector[0, (1 << 0) | (1 << 6)] = 0.03125 + step = algebra._exp_bivector_closed(bivector) + + rotor = torch.zeros_like(step) + rotor[0, 0] = 1.0 + identity = rotor.clone() + + max_error = 0.0 + for depth in range(1, 129): + rotor = algebra.geometric_product(rotor, step) + if depth % 16 == 0: + rotor_norm = algebra.geometric_product(rotor, algebra.reverse(rotor)) + max_error = max(max_error, (rotor_norm - identity).abs().max().item()) + + assert max_error < 5e-5 + + def test_bridge_sign_for_high_times_low_vector(self): + reference = CliffordAlgebra(5, 0, 0, device=DEVICE, dtype=torch.float64) + algebra = PartitionedCliffordAlgebra(5, 0, 0, device=DEVICE, dtype=torch.float64, leaf_n=4) + + A = torch.zeros(1, 32, dtype=torch.float64) + B = torch.zeros(1, 32, dtype=torch.float64) + A[0, 16] = 1.0 # e5, the high block's first vector after a 4D low split + B[0, 1] = 1.0 # e1, a low-block vector + + actual = algebra.geometric_product(A, B) + expected = reference.geometric_product(A, B) + + assert actual[0, 17].item() == -1.0 + assert torch.equal(actual, expected) + + def test_null_cross_split_matches_core_kernel(self): + _assert_matches_monolithic(4, 2, 2, leaf_n=4, shape=(2,)) + + def test_minkowski_signature_matches_core_kernel(self): + _assert_matches_monolithic(1, 3, 0, leaf_n=2, shape=(3,)) + + def test_degenerate_signature_matches_core_kernel(self): + _assert_matches_monolithic(2, 1, 2, leaf_n=2, shape=(2,)) + + @pytest.mark.parametrize( + ("p", "q", "r"), + [ + (0, 3, 0), + (0, 0, 4), + (2, 0, 2), + (0, 2, 2), + (2, 2, 1), + ], + ) + def test_general_signature_sweep_matches_core_kernel(self, p, q, r): + torch.manual_seed(19 + p * 11 + q * 7 + r) + reference, algebra = _make_pair(p, q, r, leaf_n=2, product_chunk_size=3) + A = torch.randn(2, algebra.dim, dtype=torch.float64) + B = torch.randn(2, algebra.dim, dtype=torch.float64) + + for method_name in [ + "geometric_product", + "wedge", + "inner_product", + "commutator", + "anti_commutator", + "left_contraction", + ]: + expected = getattr(reference, method_name)(A, B) + actual = getattr(algebra, method_name)(A, B) + assert torch.allclose(actual, expected, atol=1e-10, rtol=1e-10), method_name + + for method_name in [ + "reverse", + "pseudoscalar_product", + "dual", + "grade_involution", + "clifford_conjugation", + "norm_sq", + ]: + expected = getattr(reference, method_name)(A) + actual = getattr(algebra, method_name)(A) + assert torch.allclose(actual, expected, atol=1e-10, rtol=1e-10), method_name + + def test_recursive_node_does_not_allocate_global_cayley_table(self): + algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=6) + + assert not hasattr(algebra, "cayley_indices") + assert not hasattr(algebra, "cayley_signs") + assert algebra.left_sub.dim == 16 + assert algebra.right_sub.dim == 16 + + def test_static_structural_sign_buffers_match_core_kernel(self): + reference, algebra = _make_pair(3, 1, 1, leaf_n=2) + + for name in [ + "grade_index", + "rev_signs", + "_involution_signs", + "conj_signs", + "_cayley_diag", + "_norm_sq_signs", + "_hermitian_signs", + "_ps_source", + "_ps_signs", + "_bv_indices", + "bv_sq_scalar", + "rc_action", + ]: + expected = getattr(reference, name) + actual = getattr(algebra, name) + if expected.dtype.is_floating_point: + assert torch.allclose(actual, expected) + else: + assert torch.equal(actual, expected) + + def test_unary_operations_match_core_kernel(self): + torch.manual_seed(31) + reference, algebra = _make_pair(3, 1, 0, leaf_n=2) + mv = torch.randn(2, algebra.dim, dtype=torch.float64) + vectors = torch.randn(2, algebra.n, dtype=torch.float64) + + assert torch.allclose(algebra.embed_vector(vectors), reference.embed_vector(vectors)) + assert torch.allclose(algebra.get_grade_norms(mv), reference.get_grade_norms(mv), atol=1e-12, rtol=1e-12) + for grade in range(algebra.num_grades): + assert torch.allclose(algebra.grade_projection(mv, grade), reference.grade_projection(mv, grade)) + + for method_name in [ + "reverse", + "pseudoscalar_product", + "dual", + "grade_involution", + "clifford_conjugation", + "norm_sq", + ]: + expected = getattr(reference, method_name)(mv) + actual = getattr(algebra, method_name)(mv) + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + def test_binary_operations_match_core_kernel(self): + torch.manual_seed(37) + reference, algebra = _make_pair(3, 1, 0, leaf_n=2, product_chunk_size=3) + A = torch.randn(2, 1, algebra.dim, dtype=torch.float64) + B = torch.randn(1, 3, algebra.dim, dtype=torch.float64) + + for method_name in [ + "wedge", + "inner_product", + "commutator", + "anti_commutator", + "left_contraction", + ]: + expected = getattr(reference, method_name)(A, B) + actual = getattr(algebra, method_name)(A, B) + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + def test_bivector_vector_right_contraction_matches_core_kernel(self): + torch.manual_seed(41) + reference, algebra = _make_pair(4, 0, 0, leaf_n=2) + A = reference.grade_projection(torch.randn(3, algebra.dim, dtype=torch.float64), 2) + B = reference.grade_projection(torch.randn(3, algebra.dim, dtype=torch.float64), 1) + + expected = reference.right_contraction(A, B) + actual = algebra.right_contraction(A, B) + + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + def test_blade_and_versor_operations_match_core_kernel(self): + torch.manual_seed(43) + reference, algebra = _make_pair(3, 1, 0, leaf_n=2) + mv = torch.randn(2, algebra.dim, dtype=torch.float64) + blade = torch.zeros(2, algebra.dim, dtype=torch.float64) + blade[:, 1] = 1.0 + blade[:, 2] = 0.25 + + for method_name in ["blade_inverse", "blade_project", "blade_reject", "reflect", "versor_product"]: + if method_name == "blade_inverse": + expected = reference.blade_inverse(blade) + actual = algebra.blade_inverse(blade) + elif method_name in {"blade_project", "blade_reject"}: + expected = getattr(reference, method_name)(mv, blade) + actual = getattr(algebra, method_name)(mv, blade) + elif method_name == "versor_product": + expected = reference.versor_product(blade, mv) + actual = algebra.versor_product(blade, mv) + else: + expected = getattr(reference, method_name)(mv, blade) + actual = getattr(algebra, method_name)(mv, blade) + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + def test_sandwich_variants_match_core_kernel(self): + torch.manual_seed(47) + reference, algebra = _make_pair(3, 0, 0, leaf_n=2) + bivector = torch.zeros(4, algebra.dim, dtype=torch.float64) + bivector[:, 3] = torch.linspace(0.05, 0.2, 4, dtype=torch.float64) + rotors = reference.exp(bivector) + + x_batch_channel = torch.randn(2, 4, algebra.dim, dtype=torch.float64) + x_same_batch = torch.randn(4, 3, algebra.dim, dtype=torch.float64) + x_multi = torch.randn(2, 3, algebra.dim, dtype=torch.float64) + + expected = reference.per_channel_sandwich(rotors, x_batch_channel) + actual = algebra.per_channel_sandwich(rotors, x_batch_channel) + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + expected = reference.sandwich_product(rotors, x_same_batch) + actual = algebra.sandwich_product(rotors, x_same_batch) + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + expected = reference.multi_rotor_sandwich(rotors, x_multi) + actual = algebra.multi_rotor_sandwich(rotors, x_multi) + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + def test_exp_paths_match_core_kernel(self): + reference, algebra = _make_pair(4, 0, 0, leaf_n=2) + B = torch.zeros(2, algebra.dim, dtype=torch.float64) + B[:, 3] = torch.tensor([0.125, -0.25], dtype=torch.float64) + + assert torch.allclose( + algebra._exp_bivector_closed(B), reference._exp_bivector_closed(B), atol=1e-12, rtol=1e-12 + ) + assert torch.allclose(algebra.exp(B), reference.exp(B), atol=1e-12, rtol=1e-12) + + mv = torch.zeros(2, algebra.dim, dtype=torch.float64) + mv[:, 0] = 0.1 + mv[:, 1] = torch.tensor([0.02, -0.03], dtype=torch.float64) + mv[:, 3] = torch.tensor([0.04, 0.05], dtype=torch.float64) + assert torch.allclose( + algebra._exp_taylor(mv, order=6), reference._exp_taylor(mv, order=6), atol=1e-12, rtol=1e-12 + ) + + @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") + def test_compile_geometric_product_matches_eager(self): + torch.manual_seed(53) + algebra = PartitionedCliffordAlgebra(4, 1, 0, device=DEVICE, dtype=torch.float32, leaf_n=2) + A = torch.randn(3, algebra.dim) + B = torch.randn(3, algebra.dim) + + def product(x, y): + return algebra.geometric_product(x, y) + + compiled_product = torch.compile(product, backend="aot_eager") + + assert torch.allclose(compiled_product(A, B), product(A, B), atol=1e-5, rtol=1e-5) + + @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") + def test_compile_training_backward_matches_eager(self): + torch.manual_seed(59) + eager_layer = _PartitionedProductLayer(4, 0, 0) + compiled_layer = _PartitionedProductLayer(4, 0, 0) + compiled_layer.weight.data.copy_(eager_layer.weight.data) + + x_eager = torch.randn(3, eager_layer.algebra.dim, requires_grad=True) + x_compiled = x_eager.detach().clone().requires_grad_(True) + + eager_loss = eager_layer(x_eager).square().sum() + eager_loss.backward() + + compiled_forward = torch.compile(compiled_layer, backend="aot_eager") + compiled_loss = compiled_forward(x_compiled).square().sum() + compiled_loss.backward() + + assert torch.allclose(compiled_loss, eager_loss, atol=1e-5, rtol=1e-5) + assert torch.allclose(x_compiled.grad, x_eager.grad, atol=1e-4, rtol=1e-4) + assert torch.allclose(compiled_layer.weight.grad, eager_layer.weight.grad, atol=1e-4, rtol=1e-4) diff --git a/tests/test_partitioned_highdim.py b/tests/test_partitioned_highdim.py new file mode 100644 index 0000000..1e204c2 --- /dev/null +++ b/tests/test_partitioned_highdim.py @@ -0,0 +1,728 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""High-dimensional verification for partitioned Clifford algebra. + +These tests avoid monolithic Cayley-table references. For n >= 12, the +reference is computed from axiomatic bitmask rules, sub-algebraic isomorphisms, +algebraic identities, or closed forms inside known two-dimensional subalgebras. +""" + +import math + +import pytest +import torch + +from core.algebra import CliffordAlgebra +from core.partitioned_algebra import PartitionedCliffordAlgebra + +pytestmark = pytest.mark.slow + +DEVICE = "cpu" + + +def _signature_for_range_reference(p: int, q: int, r: int, start: int, width: int) -> tuple[int, int, int]: + end = start + width + p_count = max(0, min(end, p) - start) + q_count = max(0, min(end, p + q) - max(start, p)) + r_count = max(0, min(end, p + q + r) - max(start, p + q)) + assert p_count + q_count + r_count == width + return p_count, q_count, r_count + + +def _shift_index(index: int, offset: int) -> int: + shifted = 0 + bit = 0 + while index: + if index & 1: + shifted |= 1 << (bit + offset) + index >>= 1 + bit += 1 + return shifted + + +def _make_orthonormal_subspace_basis( + algebra: PartitionedCliffordAlgebra, + frame: torch.Tensor, +) -> torch.Tensor: + """Return embedded basis blades for an orthonormal frame.""" + width = frame.shape[-1] + vector_indices = (1 << torch.arange(algebra.n, dtype=torch.long, device=frame.device)).long() + vectors = torch.zeros(width, algebra.dim, dtype=frame.dtype, device=frame.device) + vectors[:, vector_indices] = frame.transpose(0, 1) + + basis = torch.zeros(2**width, algebra.dim, dtype=frame.dtype, device=frame.device) + basis[0, 0] = 1.0 + for basis_index in range(1, basis.shape[0]): + blade = torch.zeros(1, algebra.dim, dtype=frame.dtype, device=frame.device) + blade[0, 0] = 1.0 + for bit in range(width): + if basis_index & (1 << bit): + blade = algebra.geometric_product(blade, vectors[bit : bit + 1]) + basis[basis_index] = blade[0] + return basis + + +def _basis_product_reference(index_a: int, index_b: int, p: int, q: int, r: int) -> tuple[int, float]: + n = p + q + r + swap_count = 0 + for bit in range(n): + if index_a & (1 << bit): + swap_count += (index_b & ((1 << bit) - 1)).bit_count() + + sign = -1.0 if swap_count % 2 else 1.0 + + negative_mask = sum(1 << bit for bit in range(p, p + q)) + if ((index_a & index_b & negative_mask).bit_count() % 2) == 1: + sign = -sign + + null_mask = sum(1 << bit for bit in range(p + q, n)) + if (index_a & index_b & null_mask) != 0: + sign = 0.0 + + return index_a ^ index_b, sign + + +def _partitioned_basis_product(algebra, index_a: int, index_b: int) -> tuple[int, float]: + if algebra.core is not None: + result_index = int(algebra.core.cayley_indices[index_a, index_b].item()) + sign = float(algebra.core.cayley_signs[index_a, index_b].item()) + return result_index, sign + + input_sign = 1.0 + if algebra.basis_permutation.uses_permutation: + split_a = int(algebra.basis_permutation.public_to_split[index_a].item()) + split_b = int(algebra.basis_permutation.public_to_split[index_b].item()) + input_sign *= float(algebra.basis_permutation.split_signs[split_a].item()) + input_sign *= float(algebra.basis_permutation.split_signs[split_b].item()) + index_a = split_a + index_b = split_b + + right_mask = algebra.right_dim - 1 + left_a, right_a = index_a >> algebra.right_n, index_a & right_mask + left_b, right_b = index_b >> algebra.right_n, index_b & right_mask + + left_result, left_sign = _partitioned_basis_product(algebra.left_sub, left_a, left_b) + right_result, right_sign = _partitioned_basis_product(algebra.right_sub, right_a, right_b) + right_b_index = torch.tensor([right_b], dtype=torch.long, device=algebra.device) + bridge_sign = float(algebra._bridge_signs_for_right_b(right_b_index, torch.float64)[0, left_a].item()) + result_index = (left_result << algebra.right_n) | right_result + sign = input_sign * left_sign * right_sign * bridge_sign + + if algebra.basis_permutation.uses_permutation: + sign *= float(algebra.basis_permutation.split_signs[result_index].item()) + result_index = int(algebra.basis_permutation.split_to_public[result_index].item()) + + return result_index, sign + + +def _sparse_product_reference( + entries_a: list[tuple[int, float]], + entries_b: list[tuple[int, float]], + p: int, + q: int, + r: int, +) -> dict[int, float]: + result = {} + for index_a, value_a in entries_a: + for index_b, value_b in entries_b: + result_index, sign = _basis_product_reference(index_a, index_b, p, q, r) + if sign == 0.0: + continue + result[result_index] = result.get(result_index, 0.0) + value_a * value_b * sign + return result + + +def _signature_sweep_entries(p: int, q: int, r: int) -> tuple[list[tuple[int, float]], list[tuple[int, float]]]: + n = p + q + r + entries_a = [ + (0, 0.375), + ((1 << 0) | (1 << (n // 2)), -0.5), + ((1 << (n - 3)) | (1 << (n - 1)), 0.875), + ((1 << 1) | (1 << (n - 2)), -1.125), + ] + entries_b = [ + (1 << 0, -0.75), + ((1 << (n // 2)) | (1 << (n - 4)), 1.25), + ((1 << (n - 1)) | (1 << 2), -0.625), + ((1 << 1) | (1 << (n - 5)) | (1 << (n - 2)), 0.5), + ] + + if r > 0: + null_bit = p + q + entries_a.append(((1 << null_bit) | (1 << 1), 0.25)) + entries_b.append(((1 << null_bit) | (1 << 2), -1.5)) + + return entries_a, entries_b + + +def _make_sparse_multivector(algebra: PartitionedCliffordAlgebra, entries, dtype: torch.dtype) -> torch.Tensor: + mv = torch.zeros(1, algebra.dim, dtype=dtype) + for index, value in entries: + mv[0, index] += value + return mv + + +def _make_expected_multivector(algebra: PartitionedCliffordAlgebra, entries, dtype: torch.dtype) -> torch.Tensor: + expected = torch.zeros(1, algebra.dim, dtype=dtype) + for index, value in entries.items(): + expected[0, index] = value + return expected + + +def _long_taylor_simple_bivector(theta: float, square: float, order: int = 80) -> tuple[float, float]: + scalar = 0.0 + bivector = 0.0 + power = 1.0 + for k in range(order + 1): + if k > 0: + power *= theta + if k % 2 == 0: + scalar += power * (square ** (k // 2)) / math.factorial(k) + else: + bivector += power * (square ** ((k - 1) // 2)) / math.factorial(k) + return scalar, bivector + + +def _canonical_basis_term(index: int, coefficient: float) -> tuple[int, float]: + if coefficient == 0.0: + return 0, 0.0 + return index, coefficient + + +def _multiply_signed_basis( + algebra: PartitionedCliffordAlgebra, + left: tuple[int, float], + right: tuple[int, float], +) -> tuple[int, float]: + left_index, left_coeff = left + right_index, right_coeff = right + if left_coeff == 0.0 or right_coeff == 0.0: + return 0, 0.0 + result_index, sign = _partitioned_basis_product(algebra, left_index, right_index) + return _canonical_basis_term(result_index, left_coeff * right_coeff * sign) + + +def _reverse_sign(index: int) -> float: + grade = index.bit_count() + return -1.0 if (grade * (grade - 1) // 2) % 2 else 1.0 + + +def _grade_involution_sign(index: int) -> float: + return -1.0 if index.bit_count() % 2 else 1.0 + + +class TestPartitionedHighDimensionalVerification: + def test_cl12_sparse_multivector_product_matches_direct_bitmask_reference(self): + p, q, r = 8, 3, 1 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float64, + leaf_n=6, + product_chunk_size=32, + ) + entries_a = [(0, 0.25), (3, -1.5), (257, 0.75), (2049, 2.0)] + entries_b = [(1, -0.5), (384, 1.25), (1025, -2.0), (4095, 0.5)] + + A = torch.zeros(1, algebra.dim, dtype=torch.float64) + B = torch.zeros(1, algebra.dim, dtype=torch.float64) + for index, value in entries_a: + A[0, index] = value + for index, value in entries_b: + B[0, index] = value + + actual = algebra.geometric_product(A, B) + expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) + + expected = torch.zeros_like(actual) + for index, value in expected_sparse.items(): + expected[0, index] = value + + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + @pytest.mark.parametrize( + ("p", "q", "r", "dtype", "atol"), + [ + (12, 0, 0, torch.float64, 1e-12), + (0, 12, 0, torch.float64, 1e-12), + (6, 6, 0, torch.float64, 1e-12), + (8, 3, 1, torch.float64, 1e-12), + (4, 4, 4, torch.float64, 1e-12), + (10, 4, 2, torch.float32, 1e-6), + ], + ) + def test_sparse_multivector_products_match_bitmask_rules_across_highdim_signatures( + self, + p, + q, + r, + dtype, + atol, + ): + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=dtype, + leaf_n=6, + product_chunk_size=8 if p + q + r >= 16 else 16, + ) + entries_a, entries_b = _signature_sweep_entries(p, q, r) + A = _make_sparse_multivector(algebra, entries_a, dtype) + B = _make_sparse_multivector(algebra, entries_b, dtype) + + actual = algebra.geometric_product(A, B) + expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) + expected = _make_expected_multivector(algebra, expected_sparse, dtype) + + assert torch.allclose(actual, expected, atol=atol, rtol=atol) + + def test_automatic_tiled_cl12_product_matches_bitmask_reference(self): + p, q, r = 6, 3, 3 + dtype = torch.float64 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=dtype, + leaf_n=4, + product_chunk_size=16, + ) + entries_a, entries_b = _signature_sweep_entries(p, q, r) + A = _make_sparse_multivector(algebra, entries_a, dtype) + B = _make_sparse_multivector(algebra, entries_b, dtype) + + actual = algebra.geometric_product(A, B) + expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) + expected = _make_expected_multivector(algebra, expected_sparse, dtype) + + assert algebra.basis_permutation.uses_permutation + assert algebra.left_sub.left_sub is algebra.right_sub + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + def test_cl12_sparse_multivectors_satisfy_numerical_identities(self): + p, q, r = 7, 3, 2 + dtype = torch.float64 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=dtype, + leaf_n=6, + product_chunk_size=64, + ) + deep_algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=dtype, + leaf_n=3, + product_chunk_size=17, + ) + entries_a, entries_b = _signature_sweep_entries(p, q, r) + entries_c = [(0, -0.25), (5, 0.5), ((1 << 4) | (1 << 9), -0.75), ((1 << 10) | 3, 1.125)] + + A = _make_sparse_multivector(algebra, entries_a, dtype) + B = _make_sparse_multivector(algebra, entries_b, dtype) + C = _make_sparse_multivector(algebra, entries_c, dtype) + + AB = algebra.geometric_product(A, B) + BC = algebra.geometric_product(B, C) + AC = algebra.geometric_product(A, C) + + assert torch.allclose(AB, deep_algebra.geometric_product(A, B), atol=1e-10, rtol=1e-10) + assert torch.allclose(A + B, B + A, atol=0.0, rtol=0.0) + assert torch.allclose(algebra.geometric_product(A, B + C), AB + AC, atol=1e-10, rtol=1e-10) + assert torch.allclose(algebra.geometric_product(A + B, C), AC + BC, atol=1e-10, rtol=1e-10) + + assert torch.allclose( + algebra.geometric_product(AB, C), + algebra.geometric_product(A, BC), + atol=1e-10, + rtol=1e-10, + ) + assert torch.allclose( + algebra.reverse(AB), + algebra.geometric_product(algebra.reverse(B), algebra.reverse(A)), + atol=1e-10, + rtol=1e-10, + ) + assert torch.allclose( + algebra.grade_involution(AB), + algebra.geometric_product(algebra.grade_involution(A), algebra.grade_involution(B)), + atol=1e-10, + rtol=1e-10, + ) + + scalar = torch.zeros_like(A) + scalar[..., 0] = -1.75 + assert torch.allclose(algebra.geometric_product(scalar, A), -1.75 * A, atol=1e-12, rtol=1e-12) + assert torch.allclose(algebra.geometric_product(A, scalar), -1.75 * A, atol=1e-12, rtol=1e-12) + + e0 = _make_sparse_multivector(algebra, [(1 << 0, 1.0)], dtype) + e2 = _make_sparse_multivector(algebra, [(1 << 2, 1.0)], dtype) + e0e2 = algebra.geometric_product(e0, e2) + e2e0 = algebra.geometric_product(e2, e0) + assert torch.allclose(e0e2, -e2e0, atol=1e-12, rtol=1e-12) + assert not torch.allclose(e0e2, e2e0, atol=1e-12, rtol=1e-12) + + def test_cl12_backward_matches_finite_difference_directional_derivative(self): + p, q, r = 7, 3, 2 + dtype = torch.float64 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=dtype, + leaf_n=6, + product_chunk_size=16, + accumulation_dtype=torch.float64, + ) + + entries_a, entries_b = _signature_sweep_entries(p, q, r) + entries_da = [(1, 0.125), ((1 << 5) | (1 << 8), -0.25), ((1 << 10) | 3, 0.375)] + entries_db = [(2, -0.2), ((1 << 6) | (1 << 11), 0.15), ((1 << 7) | (1 << 9), -0.3)] + entries_w = [(0, -0.5), ((1 << 1) | (1 << 7), 0.75), ((1 << 8) | (1 << 11), -1.25)] + + A = _make_sparse_multivector(algebra, entries_a, dtype).requires_grad_(True) + B = _make_sparse_multivector(algebra, entries_b, dtype).requires_grad_(True) + dA = _make_sparse_multivector(algebra, entries_da, dtype) + dB = _make_sparse_multivector(algebra, entries_db, dtype) + weight = _make_sparse_multivector(algebra, entries_w, dtype) + + loss = (algebra.geometric_product(A, B) * weight).sum() + loss.backward() + directional_grad = (A.grad * dA).sum() + (B.grad * dB).sum() + + eps = 1e-6 + loss_plus = (algebra.geometric_product(A.detach() + eps * dA, B.detach() + eps * dB) * weight).sum() + loss_minus = (algebra.geometric_product(A.detach() - eps * dA, B.detach() - eps * dB) * weight).sum() + finite_difference = (loss_plus - loss_minus) / (2.0 * eps) + + assert torch.allclose(directional_grad, finite_difference, atol=1e-9, rtol=1e-9) + + def test_cl16_dense_basis_product_matches_direct_bitmask_reference(self): + p, q, r = 10, 4, 2 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float32, + leaf_n=6, + product_chunk_size=8, + ) + index_a = 0b1001_0010_0110_1011 + index_b = 0b0110_1101_1000_1110 + result_index, sign = _basis_product_reference(index_a, index_b, p, q, r) + + A = torch.zeros(1, algebra.dim) + B = torch.zeros(1, algebra.dim) + A[0, index_a] = 1.25 + B[0, index_b] = -0.5 + + actual = algebra.geometric_product(A, B) + expected = torch.zeros_like(actual) + expected[0, result_index] = 1.25 * -0.5 * sign + + assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) + + def test_cl12_embedded_subalgebra_product_matches_local_isomorphism(self): + p, q, r = 6, 4, 2 + offset, width = 3, 6 + local_p, local_q, local_r = _signature_for_range_reference(p, q, r, offset, width) + + global_algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float64, + leaf_n=6, + product_chunk_size=16, + ) + local_algebra = PartitionedCliffordAlgebra( + local_p, + local_q, + local_r, + device=DEVICE, + dtype=torch.float64, + leaf_n=4, + product_chunk_size=16, + ) + + entries_a = [(0, 0.5), (3, -1.25), (17, 0.75), (63, -0.2)] + entries_b = [(1, 2.0), (10, -0.5), (48, 1.5)] + local_a = torch.zeros(1, local_algebra.dim, dtype=torch.float64) + local_b = torch.zeros(1, local_algebra.dim, dtype=torch.float64) + global_a = torch.zeros(1, global_algebra.dim, dtype=torch.float64) + global_b = torch.zeros(1, global_algebra.dim, dtype=torch.float64) + + for index, value in entries_a: + local_a[0, index] = value + global_a[0, _shift_index(index, offset)] = value + for index, value in entries_b: + local_b[0, index] = value + global_b[0, _shift_index(index, offset)] = value + + local_product = local_algebra.geometric_product(local_a, local_b) + actual = global_algebra.geometric_product(global_a, global_b) + + expected = torch.zeros_like(actual) + shifted_indices = torch.tensor( + [_shift_index(index, offset) for index in range(local_algebra.dim)], + dtype=torch.long, + ) + expected[..., shifted_indices] = local_product + + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + def test_cl12_random_three_dimensional_subspace_projects_to_n3_engine(self): + dtype = torch.float64 + global_algebra = PartitionedCliffordAlgebra( + 12, + 0, + 0, + device=DEVICE, + dtype=dtype, + leaf_n=6, + product_chunk_size=64, + ) + local_algebra = CliffordAlgebra(3, 0, 0, device=DEVICE, dtype=dtype) + + generator = torch.Generator(device=DEVICE).manual_seed(211) + frame, _ = torch.linalg.qr(torch.randn(global_algebra.n, 3, dtype=dtype, generator=generator)) + basis = _make_orthonormal_subspace_basis(global_algebra, frame.contiguous()) + + gram = basis @ basis.transpose(0, 1) + assert torch.allclose(gram, torch.eye(local_algebra.dim, dtype=dtype), atol=1e-12, rtol=1e-12) + + local_a = torch.randn(2, local_algebra.dim, dtype=dtype, generator=generator) + local_b = torch.randn(2, local_algebra.dim, dtype=dtype, generator=generator) + global_a = local_a @ basis + global_b = local_b @ basis + + local_expected = local_algebra.geometric_product(local_a, local_b) + global_product = global_algebra.geometric_product(global_a, global_b) + projected = global_product @ basis.transpose(0, 1) + embedded_expected = local_expected @ basis + + assert torch.allclose(projected, local_expected, atol=1e-10, rtol=1e-10) + assert torch.allclose(global_product, embedded_expected, atol=1e-10, rtol=1e-10) + + def test_cl20_recursive_sign_merge_matches_direct_bitmask_reference(self): + p, q, r = 12, 6, 2 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float32, + leaf_n=6, + product_chunk_size=4, + ) + pairs = [ + (0, 0), + (1, 1 << 19), + (0xABCDE, 0x13579), + (0xFFFFF, 0x00011), + (0x22222, 0xDDDDD), + (0x7A5C3, 0xC3A57), + ((1 << 18) | 7, (1 << 18) | 3), + ] + + for index_a, index_b in pairs: + expected = _basis_product_reference(index_a, index_b, p, q, r) + actual = _partitioned_basis_product(algebra, index_a, index_b) + assert actual == expected + + def test_cl20_basis_products_satisfy_algebraic_identities(self): + p, q, r = 12, 6, 2 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float32, + leaf_n=6, + product_chunk_size=4, + ) + triples = [ + (0x12345, 0x00F0F, 0xABCDE), + (0x70001, 0x02A80, 0x11111), + ((1 << 18) | 0x35, 0x04440, 0x21001), + (0x7A5C3, (1 << 19) | 0x81, 0x00013), + ] + + for index_a, index_b, index_c in triples: + left = _multiply_signed_basis( + algebra, + _multiply_signed_basis(algebra, (index_a, 1.0), (index_b, 1.0)), + (index_c, 1.0), + ) + right = _multiply_signed_basis( + algebra, + (index_a, 1.0), + _multiply_signed_basis(algebra, (index_b, 1.0), (index_c, 1.0)), + ) + assert left == right + + pairs = [ + (0x12345, 0x00F0F), + (0x7A5C3, 0xC3A57), + ((1 << 18) | 0x101, (1 << 18) | 0x077), + ((1 << 19) | 0x222, 0x13579), + ] + for index_a, index_b in pairs: + ab = _multiply_signed_basis(algebra, (index_a, 1.0), (index_b, 1.0)) + reverse_ab = _canonical_basis_term(ab[0], ab[1] * _reverse_sign(ab[0])) + reverse_ba = _multiply_signed_basis( + algebra, + (index_b, _reverse_sign(index_b)), + (index_a, _reverse_sign(index_a)), + ) + assert reverse_ab == reverse_ba + + involution_ab = _canonical_basis_term(ab[0], ab[1] * _grade_involution_sign(ab[0])) + involution_product = _multiply_signed_basis( + algebra, + (index_a, _grade_involution_sign(index_a)), + (index_b, _grade_involution_sign(index_b)), + ) + assert involution_ab == involution_product + + def test_cl20_simple_bivector_exp_matches_long_taylor_reference(self): + p, q, r = 20, 0, 0 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float64, + leaf_n=6, + product_chunk_size=4, + ) + bivector_index = (1 << 0) | (1 << 17) + theta = 0.375 + square = -1.0 + + B = torch.zeros(1, algebra.dim, dtype=torch.float64) + B[0, bivector_index] = theta + actual = algebra._exp_bivector_closed(B) + + scalar_ref, bivector_ref = _long_taylor_simple_bivector(theta, square, order=80) + + assert torch.allclose(actual[0, 0], torch.tensor(scalar_ref, dtype=torch.float64), atol=1e-14, rtol=1e-14) + assert torch.allclose( + actual[0, bivector_index], + torch.tensor(bivector_ref, dtype=torch.float64), + atol=1e-14, + rtol=1e-14, + ) + assert torch.count_nonzero(actual).item() == 2 + + @pytest.mark.parametrize( + ("p", "q", "r", "bivector_index", "theta", "scalar_ref", "bivector_ref"), + [ + (20, 0, 0, (1 << 0) | (1 << 17), 0.375, math.cos(0.375), math.sin(0.375)), + (1, 19, 0, (1 << 0) | (1 << 1), 0.25, math.cosh(0.25), math.sinh(0.25)), + (18, 0, 2, (1 << 0) | (1 << 18), 0.5, 1.0, 0.5), + ], + ) + def test_cl20_simple_bivector_exp_matches_closed_form( + self, + p, + q, + r, + bivector_index, + theta, + scalar_ref, + bivector_ref, + ): + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float64, + leaf_n=6, + product_chunk_size=4, + ) + + B = torch.zeros(1, algebra.dim, dtype=torch.float64) + B[0, bivector_index] = theta + actual = algebra._exp_bivector_closed(B) + + assert torch.allclose(actual[0, 0], torch.tensor(scalar_ref, dtype=torch.float64), atol=1e-14, rtol=1e-14) + assert torch.allclose( + actual[0, bivector_index], + torch.tensor(bivector_ref, dtype=torch.float64), + atol=1e-14, + rtol=1e-14, + ) + assert torch.count_nonzero(actual).item() == 2 + + def test_cl20_lorentzian_bivector_exp_matches_long_taylor_reference(self): + p, q, r = 1, 19, 0 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float64, + leaf_n=6, + product_chunk_size=4, + ) + bivector_index = (1 << 0) | (1 << 1) + theta = 0.25 + square = 1.0 + + B = torch.zeros(1, algebra.dim, dtype=torch.float64) + B[0, bivector_index] = theta + actual = algebra._exp_bivector_closed(B) + + scalar_ref, bivector_ref = _long_taylor_simple_bivector(theta, square, order=80) + + assert torch.allclose(actual[0, 0], torch.tensor(scalar_ref, dtype=torch.float64), atol=1e-14, rtol=1e-14) + assert torch.allclose( + actual[0, bivector_index], + torch.tensor(bivector_ref, dtype=torch.float64), + atol=1e-14, + rtol=1e-14, + ) + assert torch.count_nonzero(actual).item() == 2 + + def test_cl20_degenerate_repeated_null_factor_annihilates_product(self): + p, q, r = 12, 6, 2 + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=torch.float32, + leaf_n=6, + product_chunk_size=4, + ) + null_bit = p + q + pairs = [ + ((1 << null_bit) | 0x35, (1 << null_bit) | 0xC0), + ((1 << (null_bit + 1)) | 0x1A5, (1 << (null_bit + 1)) | 0x21), + ] + + for index_a, index_b in pairs: + expected = _basis_product_reference(index_a, index_b, p, q, r) + actual = _partitioned_basis_product(algebra, index_a, index_b) + assert expected[1] == 0.0 + assert actual == expected From d4934ee18d189f288eeb2530412c3a7b888984de Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 6 May 2026 13:31:31 +0900 Subject: [PATCH 04/45] chore: move clifford module into core --- __init__.py | 2 + core/__init__.py | 2 + core/module.py | 60 ++++++ core/partitioned_algebra.py | 246 +++++++++++++++++-------- docs/api/core.md | 3 + docs/api/layers.md | 3 - examples/tasks/cgenn.py | 2 +- examples/tasks/clifford_pde.py | 2 +- examples/tasks/hyperbolic.py | 3 +- examples/tasks/manifold.py | 3 +- examples/tasks/sanity_check.py | 3 +- experiments/_gdo/benchmarks.py | 2 +- experiments/_templates/inc_template.py | 2 +- experiments/dbg_linear_basis_mixing.py | 2 +- experiments/dbg_lorentz.py | 2 +- experiments/dbg_maxwell_equations.py | 2 +- experiments/dbg_navier_stokes.py | 2 +- experiments/dbg_yang_mills.py | 2 +- experiments/inc_embed_compress.py | 2 +- experiments/inc_lattice_morph.py | 2 +- experiments/inc_pendulum_dynamics.py | 2 +- experiments/inc_sta_trajectory.py | 2 +- functional/activation.py | 2 +- functional/loss.py | 2 +- functional/orthogonality.py | 2 +- layers/__init__.py | 3 +- layers/adapters/conformal.py | 3 +- layers/adapters/embedding.py | 3 +- layers/adapters/gnn.py | 2 +- layers/adapters/mother.py | 2 +- layers/adapters/projective.py | 3 +- layers/blocks/attention.py | 2 +- layers/blocks/multi_rotor_ffn.py | 2 +- layers/blocks/transformer.py | 2 +- layers/primitives/base.py | 65 ------- layers/primitives/linear.py | 3 +- layers/primitives/multi_rotor.py | 3 +- layers/primitives/normalization.py | 3 +- layers/primitives/projection.py | 3 +- layers/primitives/reflection.py | 3 +- layers/primitives/rotor.py | 3 +- layers/primitives/rotor_gadget.py | 3 +- models/blocks/gbn.py | 2 +- models/blocks/multi_rotor.py | 2 +- models/blocks/time_series.py | 2 +- models/deap/eeg_net.py | 2 +- models/lqa/glr_net.py | 2 +- models/lqa/heads.py | 2 +- models/md17/forcenet.py | 3 +- models/sr/net.py | 3 +- 50 files changed, 284 insertions(+), 199 deletions(-) create mode 100644 core/module.py delete mode 100644 layers/primitives/base.py diff --git a/__init__.py b/__init__.py index d0fe8f1..d520a36 100644 --- a/__init__.py +++ b/__init__.py @@ -3,11 +3,13 @@ __version__ = "1.0.0" from core.algebra import CliffordAlgebra +from core.module import CliffordModule from layers import CliffordLinear, RotorLayer __all__ = [ "__version__", "CliffordAlgebra", + "CliffordModule", "RotorLayer", "CliffordLinear", ] diff --git a/core/__init__.py b/core/__init__.py index e8bd29c..81ce57c 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -36,6 +36,7 @@ signature_norm_squared, signature_trace_form, ) +from .module import CliffordModule from .multivector import Multivector from .partitioned_algebra import PartitionedCliffordAlgebra from .validation import check_channels, check_multivector @@ -43,6 +44,7 @@ __all__ = [ # algebra "CliffordAlgebra", + "CliffordModule", "Multivector", "PartitionedCliffordAlgebra", # device / validation diff --git a/core/module.py b/core/module.py new file mode 100644 index 0000000..4f28798 --- /dev/null +++ b/core/module.py @@ -0,0 +1,60 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Base PyTorch module for components that share a Clifford algebra.""" + +import torch.nn as nn + +from .algebra import CliffordAlgebra + + +class CliffordModule(nn.Module): + """Base module for Clifford algebra-aware components. + + ``CliffordModule`` belongs to :mod:`core` because it is shared by layers, + functional losses/activations, models, examples, and experiments. Keeping it + out of :mod:`layers` prevents functional code from importing the eager layer + package just to subclass this base type. + + The module stores a shared :class:`CliffordAlgebra` reference without + registering it as a PyTorch submodule. In Versor, one algebra instance often + owns the precomputed geometric tensors used by many modules. + """ + + def __init__(self, algebra: CliffordAlgebra): + """Set up the module with a shared algebra instance.""" + super().__init__() + # Bypass nn.Module.__setattr__ to avoid registering algebra as a child. + object.__setattr__(self, "_algebra", algebra) + + @property + def algebra(self) -> CliffordAlgebra: + """Return the shared algebra instance.""" + return self._algebra + + @property + def p(self): + return self._algebra.p + + @property + def q(self): + return self._algebra.q + + @property + def r(self): + return self._algebra.r + + def _apply(self, fn): + """Apply device/dtype moves to this module and its shared algebra.""" + result = super()._apply(fn) + if self._algebra is not None: + self._algebra._apply(fn) + return result + + def forward(self, x): + """Perform the forward pass computation.""" + raise NotImplementedError diff --git a/core/partitioned_algebra.py b/core/partitioned_algebra.py index 9178106..4a67a0d 100644 --- a/core/partitioned_algebra.py +++ b/core/partitioned_algebra.py @@ -36,6 +36,32 @@ _DEFAULT_PRODUCT_CHUNK_SIZE = 64 +@dataclass(frozen=True) +class _Signature: + """Small immutable signature value used by split planning helpers.""" + + p: int + q: int + r: int + + @property + def n(self) -> int: + return self.p + self.q + self.r + + def as_tuple(self) -> tuple[int, int, int]: + return self.p, self.q, self.r + + def subtract(self, other: "_Signature") -> "_Signature": + return _Signature(self.p - other.p, self.q - other.q, self.r - other.r) + + def scaled_tile(self, tile_count: int, selected_tiles: int) -> "_Signature": + return _Signature( + self.p // tile_count * selected_tiles, + self.q // tile_count * selected_tiles, + self.r // tile_count * selected_tiles, + ) + + def _signature_for_range(p: int, q: int, r: int, start: int, width: int) -> tuple[int, int, int]: """Return ``(p, q, r)`` counts covered by a contiguous public bit range.""" end = start + width @@ -63,6 +89,22 @@ def _signature_gcd(p: int, q: int, r: int) -> int: return result +def _signature_prefix_dims_for_split( + signature: _Signature, + right_signature: _Signature, +) -> tuple[tuple[int, ...], tuple[int, ...]]: + """Return right/left public bit positions for a prefix-by-signature split.""" + right_dims, left_dims = _signature_prefix_dims( + signature.p, + signature.q, + signature.r, + right_signature.p, + right_signature.q, + right_signature.r, + ) + return tuple(right_dims), tuple(left_dims) + + def _signature_prefix_dims( p: int, q: int, @@ -134,6 +176,23 @@ class _ProductPlan: chunk_size: int +@dataclass(frozen=True) +class _RightProductSlice: + """Runtime routing tensors for one range of right-basis products.""" + + right_a_indices: torch.Tensor + right_b_indices: torch.Tensor + right_result_indices: torch.Tensor + right_product_signs: torch.Tensor + + def __iter__(self): + """Preserve tuple-unpack compatibility for older private tests.""" + yield self.right_a_indices + yield self.right_b_indices + yield self.right_result_indices + yield self.right_product_signs + + def _partition_split(p: int, q: int, r: int) -> _PartitionSplit: """Return the single recursive split used by the partitioned algebra. @@ -141,47 +200,63 @@ def _partition_split(p: int, q: int, r: int) -> _PartitionSplit: same local signature and can share one module instance. Signatures without repeatable tiles fall back to a balanced contiguous split. """ - n = p + q + r + signature = _Signature(p, q, r) + tiled_split = _tiled_signature_split(signature) + if tiled_split is not None: + return tiled_split - tile_count = _signature_gcd(p, q, r) - if tile_count > 1: - tile_p = p // tile_count - tile_q = q // tile_count - tile_r = r // tile_count - right_tile_count = tile_count // 2 + return _balanced_contiguous_split(signature) - right_p = tile_p * right_tile_count - right_q = tile_q * right_tile_count - right_r = tile_r * right_tile_count - right_dims, left_dims = _signature_prefix_dims( - p, - q, - r, - right_p, - right_q, - right_r, - ) - left_p = p - right_p - left_q = q - right_q - left_r = r - right_r - return _PartitionSplit( - right_signature=(right_p, right_q, right_r), - left_signature=(left_p, left_q, left_r), - right_dims=tuple(right_dims), - left_dims=tuple(left_dims), - ) +def _tiled_signature_split(signature: _Signature) -> Optional[_PartitionSplit]: + """Split repeated signature tiles so identical child trees can be shared.""" + tile_count = _signature_gcd(*signature.as_tuple()) + if tile_count <= 1: + return None + + right_tile_count = tile_count // 2 + right_signature = signature.scaled_tile(tile_count, right_tile_count) + left_signature = signature.subtract(right_signature) + right_dims, left_dims = _signature_prefix_dims_for_split(signature, right_signature) - right_width = n // 2 - left_width = n - right_width - right_p, right_q, right_r = _signature_for_range(p, q, r, 0, right_width) - left_p, left_q, left_r = _signature_for_range(p, q, r, right_width, left_width) + return _build_partition_split( + right_signature=right_signature, + left_signature=left_signature, + right_dims=right_dims, + left_dims=left_dims, + ) + + +def _balanced_contiguous_split(signature: _Signature) -> _PartitionSplit: + """Split a signature into balanced contiguous public bit ranges.""" + right_width = signature.n // 2 + left_width = signature.n - right_width + right_signature = _Signature(*_signature_for_range(*signature.as_tuple(), 0, right_width)) + left_signature = _Signature(*_signature_for_range(*signature.as_tuple(), right_width, left_width)) right_dims = tuple(range(right_width)) - left_dims = tuple(range(right_width, n)) + left_dims = tuple(range(right_width, signature.n)) + + return _build_partition_split( + right_signature=right_signature, + left_signature=left_signature, + right_dims=right_dims, + left_dims=left_dims, + ) + +def _build_partition_split( + *, + right_signature: _Signature, + left_signature: _Signature, + right_dims: tuple[int, ...], + left_dims: tuple[int, ...], +) -> _PartitionSplit: + """Create a validated split plan.""" + assert right_signature.n == len(right_dims) + assert left_signature.n == len(left_dims) return _PartitionSplit( - right_signature=(right_p, right_q, right_r), - left_signature=(left_p, left_q, left_r), + right_signature=right_signature.as_tuple(), + left_signature=left_signature.as_tuple(), right_dims=right_dims, left_dims=left_dims, ) @@ -235,9 +310,7 @@ def _resolve_exp_settings( policy = exp_policy if isinstance(exp_policy, ExpPolicy) else ExpPolicy(exp_policy) iterations = ( - int(fixed_iterations) - if fixed_iterations is not None - else resolve_fixed_iterations(policy, dtype, p + q + r) + int(fixed_iterations) if fixed_iterations is not None else resolve_fixed_iterations(policy, dtype, p + q + r) ) return _ExpSettings(regime=regime, policy=policy, fixed_iterations=iterations) @@ -264,6 +337,12 @@ def _product_plan(right_n: int, right_r: int, requested_chunk_size: Optional[int return _ProductPlan(right_pair_count=right_pair_count, chunk_size=chunk_size) +def _product_pair_ranges(pair_count: int, chunk_size: int): + """Yield static right-pair ranges for recursive product chunks.""" + for start in range(0, pair_count, chunk_size): + yield start, min(start + chunk_size, pair_count) + + def _compact_surviving_basis_pairs( compact_pair_indices: torch.Tensor, n: int, @@ -472,9 +551,7 @@ def _bivector_buffers( def _left_contraction_grade_buffers(n: int, device) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Return compact grade-pair dispatch vectors for left contraction.""" grade_pairs = [ - (grade_a, grade_b, grade_b - grade_a) - for grade_a in range(n + 1) - for grade_b in range(grade_a, n + 1) + (grade_a, grade_b, grade_b - grade_a) for grade_a in range(n + 1) for grade_b in range(grade_a, n + 1) ] lc_grade_a, lc_grade_b, lc_grade_result = zip(*grade_pairs) return ( @@ -1109,6 +1186,12 @@ def _leaf_geometric_product( result = result.to(dtype=output_dtype) return result + def _right_blade_blocks(self, mv: torch.Tensor, compute_dtype: torch.dtype) -> torch.Tensor: + """Return split-order coefficients grouped as right-indexed left multivectors.""" + split_order = self._to_split_order(mv.to(dtype=compute_dtype)) + by_left_then_right = split_order.reshape(*mv.shape[:-1], self.left_dim, self.right_dim) + return by_left_then_right.transpose(-1, -2) + def geometric_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Compute ``A * B`` through recursive tensor-product partitioning. @@ -1126,36 +1209,39 @@ def geometric_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: if self.core is not None: return self._leaf_geometric_product(A, B, output_dtype, compute_dtype) - # Store each right-block coefficient as a contiguous left-subalgebra - # multivector, so recursive products receive cache-local dense leaves. - A_split = self._to_split_order(A.to(dtype=compute_dtype)) - B_split = self._to_split_order(B.to(dtype=compute_dtype)) + return self._recursive_geometric_product(A, B, output_dtype, compute_dtype) - A_by_left_then_right = A_split.reshape( - *A.shape[:-1], - self.left_dim, - self.right_dim, - ) - B_by_left_then_right = B_split.reshape( - *B.shape[:-1], - self.left_dim, - self.right_dim, - ) - A_by_right_blade = A_by_left_then_right.transpose(-1, -2) - B_by_right_blade = B_by_left_then_right.transpose(-1, -2) + def _recursive_geometric_product( + self, + A: torch.Tensor, + B: torch.Tensor, + output_dtype: torch.dtype, + compute_dtype: torch.dtype, + ) -> torch.Tensor: + """Compute a recursive node product after validation and dtype resolution.""" + A_by_right_blade = self._right_blade_blocks(A, compute_dtype) + B_by_right_blade = self._right_blade_blocks(B, compute_dtype) + result_blocks = self._accumulate_right_pair_chunks(A_by_right_blade, B_by_right_blade) + output_shape = result_blocks.shape[:-2] + result = result_blocks.reshape(*output_shape, self.dim) + return self._to_public_order(result).to(dtype=output_dtype) + def _accumulate_right_pair_chunks( + self, + A_by_right_blade: torch.Tensor, + B_by_right_blade: torch.Tensor, + ) -> torch.Tensor: + """Accumulate sparse right-block interactions over static product chunks.""" result_blocks = None - for start in range(0, self._right_pair_count, self._product_chunk_size): - end = min(start + self._product_chunk_size, self._right_pair_count) + for start, end in _product_pair_ranges(self._right_pair_count, self._product_chunk_size): chunk_blocks = self._geometric_product_pair_range(A_by_right_blade, B_by_right_blade, start, end) if result_blocks is None: result_blocks = chunk_blocks else: - result_blocks.add_(chunk_blocks) + result_blocks = result_blocks + chunk_blocks - output_shape = result_blocks.shape[:-2] - result = result_blocks.reshape(*output_shape, self.dim) - return self._to_public_order(result).to(dtype=output_dtype) + assert result_blocks is not None + return result_blocks def _geometric_product_pair_range( self, @@ -1165,31 +1251,30 @@ def _geometric_product_pair_range( end: int, ) -> torch.Tensor: """Compute all contributions from a contiguous right-pair range.""" - ( - right_a_indices, - right_b_indices, - right_result_indices, - right_product_signs, - ) = self._right_product_slice(start, end) + product_slice = self._right_product_slice(start, end) - if right_product_signs.numel() == 0: + if product_slice.right_product_signs.numel() == 0: batch_shape = torch.broadcast_shapes(A_by_right_blade.shape[:-2], B_by_right_blade.shape[:-2]) return A_by_right_blade.new_zeros(*batch_shape, self.left_dim, self.right_dim) - A_terms = torch.index_select(A_by_right_blade, -2, right_a_indices) - B_terms = torch.index_select(B_by_right_blade, -2, right_b_indices) + A_terms = torch.index_select(A_by_right_blade, -2, product_slice.right_a_indices) + B_terms = torch.index_select(B_by_right_blade, -2, product_slice.right_b_indices) # ``bridge_signs[right_b, left_a]`` depends on the left basis index of # each selected A term, so broadcasting over the final left_dim axis # attaches the sign before the recursive left product. - bridge_signs = self._bridge_signs_for_right_b(right_b_indices, A_terms.dtype) + bridge_signs = self._bridge_signs_for_right_b(product_slice.right_b_indices, A_terms.dtype) if bridge_signs.dtype != A_terms.dtype: bridge_signs = bridge_signs.to(dtype=A_terms.dtype) A_terms = A_terms * bridge_signs left_products = self.left_sub.geometric_product(A_terms, B_terms) - return self._merge_right_interactions(left_products, right_result_indices, right_product_signs) + return self._merge_right_interactions( + left_products, + product_slice.right_result_indices, + product_slice.right_product_signs, + ) def _merge_right_interactions( self, @@ -1240,13 +1325,13 @@ def _right_product_slice( self, start: int, end: int, - ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + ) -> _RightProductSlice: """Derive right-block routing tensors for pair range ``[start, end)``. Returns: - tuple: ``(right_a_indices, right_b_indices, right_result_indices, - right_product_signs)``. Each position describes one right basis-pair - contribution in the recursive product. + _RightProductSlice: Each position describes one right basis-pair + contribution in the recursive product. The result still supports + tuple unpacking for older private tests. """ pair_indices = torch.arange(start, end, dtype=torch.long, device=self.device) if self.right_sub.r == 0: @@ -1264,7 +1349,12 @@ def _right_product_slice( right_result_indices = right_a_indices ^ right_b_indices right_product_signs = self._right_product_signs(right_a_indices, right_b_indices, torch.int8) - return right_a_indices, right_b_indices, right_result_indices, right_product_signs + return _RightProductSlice( + right_a_indices=right_a_indices, + right_b_indices=right_b_indices, + right_result_indices=right_result_indices, + right_product_signs=right_product_signs, + ) def grade_projection(self, mv: torch.Tensor, grade: int) -> torch.Tensor: """Project a multivector onto a grade using the same mask contract as the core algebra.""" diff --git a/docs/api/core.md b/docs/api/core.md index e5e2a28..1970930 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -5,6 +5,9 @@ The mathematical kernel of Versor. ## Algebra ::: core.algebra.CliffordAlgebra +## Module +::: core.module.CliffordModule + ## Multivector ::: core.multivector.Multivector diff --git a/docs/api/layers.md b/docs/api/layers.md index 1b4b7d3..30ff5f7 100644 --- a/docs/api/layers.md +++ b/docs/api/layers.md @@ -1,8 +1,5 @@ # Layers -## Base -::: layers.primitives.base.CliffordModule - ## Primitives ::: layers.primitives.rotor.RotorLayer ::: layers.primitives.multi_rotor.MultiRotorLayer diff --git a/examples/tasks/cgenn.py b/examples/tasks/cgenn.py index aed20cf..259914c 100644 --- a/examples/tasks/cgenn.py +++ b/examples/tasks/cgenn.py @@ -61,6 +61,7 @@ from torch.utils.data import DataLoader, TensorDataset from core.algebra import CliffordAlgebra +from core.module import CliffordModule from functional.activation import GeometricSquare from layers import ( BladeSelector, @@ -68,7 +69,6 @@ CliffordLinear, RotorLayer, ) -from layers.primitives.base import CliffordModule from tasks.base import BaseTask # --------------------------------------------------------------------------- diff --git a/examples/tasks/clifford_pde.py b/examples/tasks/clifford_pde.py index f83749b..4950672 100644 --- a/examples/tasks/clifford_pde.py +++ b/examples/tasks/clifford_pde.py @@ -67,11 +67,11 @@ from torch.utils.data import DataLoader, TensorDataset from core.algebra import CliffordAlgebra +from core.module import CliffordModule from functional.activation import GeometricGELU from layers import ( CliffordLayerNorm, CliffordLinear, - CliffordModule, RotorLayer, ) from tasks.base import BaseTask diff --git a/examples/tasks/hyperbolic.py b/examples/tasks/hyperbolic.py index 0e9027e..b30e6fd 100644 --- a/examples/tasks/hyperbolic.py +++ b/examples/tasks/hyperbolic.py @@ -10,9 +10,10 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from core.visualizer import GeneralVisualizer from functional.loss import GeometricMSELoss -from layers import CliffordModule, RotorLayer +from layers import RotorLayer from tasks.base import BaseTask diff --git a/examples/tasks/manifold.py b/examples/tasks/manifold.py index 0ee3aac..29fa06c 100644 --- a/examples/tasks/manifold.py +++ b/examples/tasks/manifold.py @@ -10,10 +10,11 @@ from torch.utils.data import DataLoader from core.algebra import CliffordAlgebra +from core.module import CliffordModule from core.visualizer import GeneralVisualizer from examples.datasets.synthetic import Figure8Dataset from functional.loss import SubspaceLoss -from layers import BladeSelector, CliffordModule, RotorLayer +from layers import BladeSelector, RotorLayer from tasks.base import BaseTask diff --git a/examples/tasks/sanity_check.py b/examples/tasks/sanity_check.py index da5cfbb..fe7e1bd 100644 --- a/examples/tasks/sanity_check.py +++ b/examples/tasks/sanity_check.py @@ -9,9 +9,10 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from core.visualizer import GeneralVisualizer from functional.loss import GeometricMSELoss -from layers import CliffordModule, RotorLayer +from layers import RotorLayer from tasks.base import BaseTask diff --git a/experiments/_gdo/benchmarks.py b/experiments/_gdo/benchmarks.py index 7db8749..850efa0 100644 --- a/experiments/_gdo/benchmarks.py +++ b/experiments/_gdo/benchmarks.py @@ -32,12 +32,12 @@ import torch.nn.functional as F from core.algebra import CliffordAlgebra +from core.module import CliffordModule from functional.activation import GeometricGELU from layers import ( BladeSelector, CliffordLayerNorm, CliffordLinear, - CliffordModule, GeometricTransformerBlock, MultiRotorLayer, MultivectorEmbedding, diff --git a/experiments/_templates/inc_template.py b/experiments/_templates/inc_template.py index 293ebf1..10d2876 100644 --- a/experiments/_templates/inc_template.py +++ b/experiments/_templates/inc_template.py @@ -50,6 +50,7 @@ # Bootstrap project root so the file runs both via ``-m`` and as a bare script. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))) +from core.module import CliffordModule from experiments._lib import ( count_parameters, ensure_output_dir, @@ -61,7 +62,6 @@ ) from functional.activation import GeometricGELU from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, RotorLayer -from layers.primitives.base import CliffordModule from optimizers.riemannian import RiemannianAdam # --------------------------------------------------------------------------- diff --git a/experiments/dbg_linear_basis_mixing.py b/experiments/dbg_linear_basis_mixing.py index ae761cb..55f1fb5 100644 --- a/experiments/dbg_linear_basis_mixing.py +++ b/experiments/dbg_linear_basis_mixing.py @@ -56,6 +56,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) from core.algebra import CliffordAlgebra +from core.module import CliffordModule from experiments._lib import ( RawDefaultsHelpFormatter, build_visualization_metadata, @@ -69,7 +70,6 @@ signature_metadata, ) from layers import BladeSelector, CliffordLinear -from layers.primitives.base import CliffordModule # ============================================================================== # Synthetic regime generators diff --git a/experiments/dbg_lorentz.py b/experiments/dbg_lorentz.py index 7275f3e..7cc554a 100644 --- a/experiments/dbg_lorentz.py +++ b/experiments/dbg_lorentz.py @@ -61,6 +61,7 @@ signature_norm_squared, signature_trace_form, ) +from core.module import CliffordModule from experiments._lib import ( build_visualization_metadata, ensure_output_dir, @@ -75,7 +76,6 @@ ) from functional.activation import GeometricGELU from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, RotorLayer -from layers.primitives.base import CliffordModule from optimizers.riemannian import RiemannianAdam # --------------------------------------------------------------------------- diff --git a/experiments/dbg_maxwell_equations.py b/experiments/dbg_maxwell_equations.py index a301447..eff8a56 100644 --- a/experiments/dbg_maxwell_equations.py +++ b/experiments/dbg_maxwell_equations.py @@ -58,6 +58,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) from core.algebra import CliffordAlgebra +from core.module import CliffordModule from experiments._lib import ( build_visualization_metadata, ensure_output_dir, @@ -72,7 +73,6 @@ ) from functional.activation import GeometricGELU from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, RotorLayer -from layers.primitives.base import CliffordModule from optimizers.riemannian import RiemannianAdam # --------------------------------------------------------------------------- diff --git a/experiments/dbg_navier_stokes.py b/experiments/dbg_navier_stokes.py index 1d8d7b1..1aa86b3 100644 --- a/experiments/dbg_navier_stokes.py +++ b/experiments/dbg_navier_stokes.py @@ -57,6 +57,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) from core.metric import hermitian_grade_spectrum, hermitian_inner_product +from core.module import CliffordModule from experiments._lib import ( build_visualization_metadata, count_parameters, @@ -72,7 +73,6 @@ ) from functional.activation import GeometricGELU from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, RotorLayer -from layers.primitives.base import CliffordModule from optimizers.riemannian import RiemannianAdam # --------------------------------------------------------------------------- diff --git a/experiments/dbg_yang_mills.py b/experiments/dbg_yang_mills.py index 232823c..cf56a71 100644 --- a/experiments/dbg_yang_mills.py +++ b/experiments/dbg_yang_mills.py @@ -56,6 +56,7 @@ from core.algebra import CliffordAlgebra from core.metric import hermitian_grade_spectrum, hermitian_inner_product +from core.module import CliffordModule from experiments._lib import ( build_visualization_metadata, count_parameters, @@ -72,7 +73,6 @@ from functional.activation import GeometricGELU from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, RotorLayer from layers.adapters.conformal import ConformalEmbedding -from layers.primitives.base import CliffordModule from optimizers.riemannian import RiemannianAdam # --------------------------------------------------------------------------- diff --git a/experiments/inc_embed_compress.py b/experiments/inc_embed_compress.py index fe60805..dce84c8 100644 --- a/experiments/inc_embed_compress.py +++ b/experiments/inc_embed_compress.py @@ -65,6 +65,7 @@ from core.analysis._types import DimensionResult from core.analysis.dimension import DimensionLifter, EffectiveDimensionAnalyzer from core.analysis.spectral import SpectralAnalyzer +from core.module import CliffordModule from experiments._lib import ( build_visualization_metadata, ensure_output_dir, @@ -79,7 +80,6 @@ BladeSelector, CliffordLayerNorm, CliffordLinear, - CliffordModule, RotorLayer, ) from optimizers.riemannian import RiemannianAdam diff --git a/experiments/inc_lattice_morph.py b/experiments/inc_lattice_morph.py index 48df1a2..61c94b3 100644 --- a/experiments/inc_lattice_morph.py +++ b/experiments/inc_lattice_morph.py @@ -51,6 +51,7 @@ from core.algebra import CliffordAlgebra from core.decomposition import ExpPolicy from core.metric import induced_norm +from core.module import CliffordModule from experiments._lib import ( build_visualization_metadata, ensure_output_dir, @@ -60,7 +61,6 @@ set_seed, signature_metadata, ) -from layers.primitives.base import CliffordModule from optimizers.riemannian import RiemannianAdam _DTYPE_MAP = { diff --git a/experiments/inc_pendulum_dynamics.py b/experiments/inc_pendulum_dynamics.py index ca10cb1..5770c87 100644 --- a/experiments/inc_pendulum_dynamics.py +++ b/experiments/inc_pendulum_dynamics.py @@ -54,6 +54,7 @@ from core.algebra import CliffordAlgebra from core.metric import hermitian_norm +from core.module import CliffordModule from experiments._lib import ( apply_residual_block, build_visualization_metadata, @@ -72,7 +73,6 @@ signature_metadata, ) from layers import BladeSelector, CliffordLayerNorm, CliffordLinear -from layers.primitives.base import CliffordModule from optimizers.riemannian import RiemannianAdam # ============================================================================== diff --git a/experiments/inc_sta_trajectory.py b/experiments/inc_sta_trajectory.py index 9beb4c0..4bda41e 100644 --- a/experiments/inc_sta_trajectory.py +++ b/experiments/inc_sta_trajectory.py @@ -58,6 +58,7 @@ from core.algebra import CliffordAlgebra from core.metric import signature_norm_squared +from core.module import CliffordModule from experiments._lib import ( build_visualization_metadata, count_parameters, @@ -74,7 +75,6 @@ ) from functional.activation import GeometricGELU from layers import CliffordLayerNorm, GeometricNeutralizer, MotherEmbedding, RotorLayer -from layers.primitives.base import CliffordModule from optimizers.riemannian import RiemannianAdam # ============================================================================ diff --git a/functional/activation.py b/functional/activation.py index 9527672..c66d84b 100644 --- a/functional/activation.py +++ b/functional/activation.py @@ -14,7 +14,7 @@ import torch.nn as nn import torch.nn.functional as F -from layers.primitives.base import CliffordModule +from core.module import CliffordModule class GeometricGELU(CliffordModule): diff --git a/functional/loss.py b/functional/loss.py index efb4362..a14a9fd 100644 --- a/functional/loss.py +++ b/functional/loss.py @@ -10,7 +10,7 @@ import torch.nn.functional as F from core.metric import hermitian_grade_spectrum -from layers.primitives.base import CliffordModule +from core.module import CliffordModule class GeometricMSELoss(CliffordModule): diff --git a/functional/orthogonality.py b/functional/orthogonality.py index a411bae..6730f4c 100644 --- a/functional/orthogonality.py +++ b/functional/orthogonality.py @@ -52,7 +52,7 @@ import torch import torch.nn as nn -from layers.primitives.base import CliffordModule +from core.module import CliffordModule @dataclass diff --git a/layers/__init__.py b/layers/__init__.py index 24fe1cd..2e2061c 100644 --- a/layers/__init__.py +++ b/layers/__init__.py @@ -3,12 +3,13 @@ Organized into Primitives, Canonical Blocks, and Task-Specific Adapters. """ +from core.module import CliffordModule + from .adapters.embedding import MultivectorEmbedding, RotaryBivectorPE from .adapters.mother import EntropyGatedAttention, MotherEmbedding, PhaseShiftHead from .blocks.attention import GeometricProductAttention from .blocks.multi_rotor_ffn import MultiRotorFFN from .blocks.transformer import GeometricTransformerBlock -from .primitives.base import CliffordModule from .primitives.linear import CliffordLinear from .primitives.multi_rotor import MultiRotorLayer from .primitives.normalization import CliffordLayerNorm diff --git a/layers/adapters/conformal.py b/layers/adapters/conformal.py index ebdb501..cc48056 100644 --- a/layers/adapters/conformal.py +++ b/layers/adapters/conformal.py @@ -8,8 +8,7 @@ import torch from core.algebra import CliffordAlgebra - -from ..primitives.base import CliffordModule +from core.module import CliffordModule class ConformalEmbedding(CliffordModule): diff --git a/layers/adapters/embedding.py b/layers/adapters/embedding.py index b79aa14..beb6300 100644 --- a/layers/adapters/embedding.py +++ b/layers/adapters/embedding.py @@ -9,8 +9,7 @@ import torch.nn as nn from core.algebra import CliffordAlgebra - -from ..primitives.base import CliffordModule +from core.module import CliffordModule class MultivectorEmbedding(CliffordModule): diff --git a/layers/adapters/gnn.py b/layers/adapters/gnn.py index 2c29eaf..71b6591 100644 --- a/layers/adapters/gnn.py +++ b/layers/adapters/gnn.py @@ -9,8 +9,8 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule -from ..primitives.base import CliffordModule from ..primitives.linear import CliffordLinear diff --git a/layers/adapters/mother.py b/layers/adapters/mother.py index 59470f4..29079a0 100644 --- a/layers/adapters/mother.py +++ b/layers/adapters/mother.py @@ -9,9 +9,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from ..blocks.attention import GeometricProductAttention -from ..primitives.base import CliffordModule from ..primitives.normalization import CliffordLayerNorm diff --git a/layers/adapters/projective.py b/layers/adapters/projective.py index c9d9e5f..4058915 100644 --- a/layers/adapters/projective.py +++ b/layers/adapters/projective.py @@ -8,8 +8,7 @@ import torch from core.algebra import CliffordAlgebra - -from ..primitives.base import CliffordModule +from core.module import CliffordModule class ProjectiveEmbedding(CliffordModule): diff --git a/layers/blocks/attention.py b/layers/blocks/attention.py index 7e38267..3f54985 100644 --- a/layers/blocks/attention.py +++ b/layers/blocks/attention.py @@ -12,8 +12,8 @@ import torch.nn.functional as F from core.algebra import CliffordAlgebra +from core.module import CliffordModule -from ..primitives.base import CliffordModule from ..primitives.linear import CliffordLinear # Memory-bounded block size for chunked attention computation diff --git a/layers/blocks/multi_rotor_ffn.py b/layers/blocks/multi_rotor_ffn.py index 7f1fc74..fbc6891 100644 --- a/layers/blocks/multi_rotor_ffn.py +++ b/layers/blocks/multi_rotor_ffn.py @@ -8,9 +8,9 @@ import torch from core.algebra import CliffordAlgebra +from core.module import CliffordModule from functional.activation import GeometricGELU -from ..primitives.base import CliffordModule from ..primitives.linear import CliffordLinear from ..primitives.multi_rotor import MultiRotorLayer from ..primitives.normalization import CliffordLayerNorm diff --git a/layers/blocks/transformer.py b/layers/blocks/transformer.py index ece7695..e750083 100644 --- a/layers/blocks/transformer.py +++ b/layers/blocks/transformer.py @@ -9,9 +9,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from ..adapters.mother import EntropyGatedAttention -from ..primitives.base import CliffordModule from ..primitives.normalization import CliffordLayerNorm from .attention import GeometricProductAttention from .multi_rotor_ffn import MultiRotorFFN diff --git a/layers/primitives/base.py b/layers/primitives/base.py deleted file mode 100644 index 253c73c..0000000 --- a/layers/primitives/base.py +++ /dev/null @@ -1,65 +0,0 @@ -# Versor: Universal Geometric Algebra Neural Network -# Copyright (C) 2026 Eunkyum Kim -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# - -import torch.nn as nn - -from core.algebra import CliffordAlgebra - - -class CliffordModule(nn.Module): - """Base module for Clifford algebra layers. - - This module securely stores a reference to a shared ``CliffordAlgebra`` instance - without registering it as a PyTorch submodule. In the Versor architecture, - a single algebra instance (which contains precomputed geometric tensors) - is heavily shared across multiple layers. - - By bypassing standard submodule registration (via ``object.__setattr__``) and - overriding ``_apply``, this base class ensures that: - 1. No ownership conflicts occur in PyTorch's computational graph. - 2. Device and dtype casting (e.g., ``.to(device)``, ``.cuda()``, ``.half()``) - are automatically and safely propagated to the shared algebra buffers. - """ - - def __init__(self, algebra: CliffordAlgebra): - """Sets up the module. - - Args: - algebra (CliffordAlgebra): The algebra instance. - """ - super().__init__() - # Bypass nn.Module.__setattr__ to avoid registering algebra as submodule. - # Multiple layers share the same algebra - only one should "own" it. - object.__setattr__(self, "_algebra", algebra) - - @property - def algebra(self) -> CliffordAlgebra: - """Return the algebra instance.""" - return self._algebra - - @property - def p(self): - return self._algebra.p - - @property - def q(self): - return self._algebra.q - - @property - def r(self): - return self._algebra.r - - def _apply(self, fn): - """Override to also move the shared algebra tables.""" - result = super()._apply(fn) - if self._algebra is not None: - self._algebra._apply(fn) - return result - - def forward(self, x): - """Performs the forward pass computation.""" - raise NotImplementedError diff --git a/layers/primitives/linear.py b/layers/primitives/linear.py index a6c2a04..6cd5003 100644 --- a/layers/primitives/linear.py +++ b/layers/primitives/linear.py @@ -16,10 +16,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from core.validation import check_channels, check_multivector -from .base import CliffordModule - class CliffordLinear(CliffordModule): """Fully connected layer with optional rotor-based backend. diff --git a/layers/primitives/multi_rotor.py b/layers/primitives/multi_rotor.py index 4375610..d51d385 100644 --- a/layers/primitives/multi_rotor.py +++ b/layers/primitives/multi_rotor.py @@ -14,10 +14,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from core.validation import check_channels, check_multivector -from .base import CliffordModule - class MultiRotorLayer(CliffordModule): """Multi-versor layer with weighted superposition: x' = sum_k w_k hat(V_k) x V_k^{-1}. diff --git a/layers/primitives/normalization.py b/layers/primitives/normalization.py index cf13efb..c10cf2c 100644 --- a/layers/primitives/normalization.py +++ b/layers/primitives/normalization.py @@ -9,8 +9,7 @@ import torch.nn as nn from core.algebra import CliffordAlgebra - -from .base import CliffordModule +from core.module import CliffordModule class CliffordLayerNorm(CliffordModule): diff --git a/layers/primitives/projection.py b/layers/primitives/projection.py index 2e17496..4034292 100644 --- a/layers/primitives/projection.py +++ b/layers/primitives/projection.py @@ -9,10 +9,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from utils.compat import safe_linalg_solve -from .base import CliffordModule - class BladeSelector(CliffordModule): """Blade Selector. Filters insignificant components. diff --git a/layers/primitives/reflection.py b/layers/primitives/reflection.py index 6565d22..b37f252 100644 --- a/layers/primitives/reflection.py +++ b/layers/primitives/reflection.py @@ -9,10 +9,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from core.validation import check_channels, check_multivector -from .base import CliffordModule - class ReflectionLayer(CliffordModule): """Learnable reflection layer via unit vectors. diff --git a/layers/primitives/rotor.py b/layers/primitives/rotor.py index 37fc7ac..51732ef 100644 --- a/layers/primitives/rotor.py +++ b/layers/primitives/rotor.py @@ -9,10 +9,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from core.validation import check_channels, check_multivector -from .base import CliffordModule - class RotorLayer(CliffordModule): """Learnable versor layer with universal grade parameterization. diff --git a/layers/primitives/rotor_gadget.py b/layers/primitives/rotor_gadget.py index 9094634..80b653e 100644 --- a/layers/primitives/rotor_gadget.py +++ b/layers/primitives/rotor_gadget.py @@ -15,10 +15,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from core.validation import check_channels, check_multivector -from .base import CliffordModule - class RotorGadget(CliffordModule): """Rotor-based linear transformation (Generalized Rotor Gadget). diff --git a/models/blocks/gbn.py b/models/blocks/gbn.py index 9ac6e08..79db066 100644 --- a/models/blocks/gbn.py +++ b/models/blocks/gbn.py @@ -9,9 +9,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from functional.activation import GeometricGELU from layers import BladeSelector, CliffordLinear, RotorLayer -from layers.primitives.base import CliffordModule class GeometricBladeNetwork(CliffordModule): diff --git a/models/blocks/multi_rotor.py b/models/blocks/multi_rotor.py index 962c993..60fb452 100644 --- a/models/blocks/multi_rotor.py +++ b/models/blocks/multi_rotor.py @@ -9,9 +9,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from functional.activation import GeometricGELU from layers import CliffordLinear, MultiRotorLayer -from layers.primitives.base import CliffordModule class MultiRotorModel(CliffordModule): diff --git a/models/blocks/time_series.py b/models/blocks/time_series.py index de90440..f301e9d 100644 --- a/models/blocks/time_series.py +++ b/models/blocks/time_series.py @@ -9,8 +9,8 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from layers import CliffordLinear, RotorLayer -from layers.primitives.base import CliffordModule class RotorTCN(CliffordModule): diff --git a/models/deap/eeg_net.py b/models/deap/eeg_net.py index bc2e071..6d2e2b9 100644 --- a/models/deap/eeg_net.py +++ b/models/deap/eeg_net.py @@ -22,13 +22,13 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from layers import ( CliffordLayerNorm, GeometricNeutralizer, GeometricTransformerBlock, MotherEmbedding, ) -from layers.primitives.base import CliffordModule class MultiTargetPhaseShiftHead(CliffordModule): diff --git a/models/lqa/glr_net.py b/models/lqa/glr_net.py index b93cac6..74969a3 100644 --- a/models/lqa/glr_net.py +++ b/models/lqa/glr_net.py @@ -25,10 +25,10 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from layers.adapters.embedding import RotaryBivectorPE from layers.adapters.mother import MotherEmbedding from layers.blocks.transformer import GeometricTransformerBlock -from layers.primitives.base import CliffordModule from layers.primitives.normalization import CliffordLayerNorm from layers.primitives.projection import GeometricNeutralizer diff --git a/models/lqa/heads.py b/models/lqa/heads.py index c74acf5..7eb985d 100644 --- a/models/lqa/heads.py +++ b/models/lqa/heads.py @@ -18,7 +18,7 @@ import torch.nn.functional as F from core.algebra import CliffordAlgebra -from layers.primitives.base import CliffordModule +from core.module import CliffordModule from layers.primitives.projection import GeometricNeutralizer from layers.primitives.rotor import RotorLayer diff --git a/models/md17/forcenet.py b/models/md17/forcenet.py index ac7b3d9..8c630f9 100644 --- a/models/md17/forcenet.py +++ b/models/md17/forcenet.py @@ -10,8 +10,9 @@ import torch.nn.functional as F from core.algebra import CliffordAlgebra +from core.module import CliffordModule from functional.activation import GeometricGELU, GeometricSquare -from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, CliffordModule, MultiRotorLayer +from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, MultiRotorLayer try: from torch_geometric.nn import global_add_pool diff --git a/models/sr/net.py b/models/sr/net.py index 0aea637..a9aa722 100644 --- a/models/sr/net.py +++ b/models/sr/net.py @@ -18,8 +18,9 @@ import torch.nn as nn from core.algebra import CliffordAlgebra +from core.module import CliffordModule from functional.activation import GeometricGELU, GeometricSquare -from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, CliffordModule, RotorLayer +from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, RotorLayer def _blade_name(idx: int, n: int) -> str: From 0c73bed484b7ab404888bda13cb4b2dd53d16a69 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 6 May 2026 17:00:35 +0900 Subject: [PATCH 05/45] feat: add algebra kernel config factory --- __init__.py | 7 + conf/config.yaml | 7 + conf/task/deap_eeg.yaml | 7 + conf/task/lqa.yaml | 7 + conf/task/md17.yaml | 7 + conf/task/sr.yaml | 9 +- core/__init__.py | 8 +- core/config.py | 215 ++++++++++++++++++++++++++++ core/module.py | 40 +++++- core/multivector.py | 12 +- core/partitioned_algebra.py | 265 ++++++++++++++++++++++++++++++++++- docs/api/core.md | 3 + tasks/deap_eeg.py | 5 +- tasks/lqa.py | 4 +- tasks/md17.py | 11 +- tasks/symbolic_regression.py | 14 +- 16 files changed, 594 insertions(+), 27 deletions(-) create mode 100644 core/config.py diff --git a/__init__.py b/__init__.py index d520a36..9fbb5fb 100644 --- a/__init__.py +++ b/__init__.py @@ -3,13 +3,20 @@ __version__ = "1.0.0" from core.algebra import CliffordAlgebra +from core.config import AlgebraConfig, PartitionConfig, make_algebra, make_algebra_from_config from core.module import CliffordModule +from core.partitioned_algebra import PartitionedCliffordAlgebra from layers import CliffordLinear, RotorLayer __all__ = [ "__version__", + "AlgebraConfig", "CliffordAlgebra", "CliffordModule", + "PartitionConfig", + "PartitionedCliffordAlgebra", + "make_algebra", + "make_algebra_from_config", "RotorLayer", "CliffordLinear", ] diff --git a/conf/config.yaml b/conf/config.yaml index 6bf8d7d..075ceb3 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -6,7 +6,14 @@ algebra: p: 3 q: 0 r: 0 + kernel: "auto" # auto: dense for n<=partition_threshold, partitioned above it + partition_threshold: 8 device: "auto" # Auto-detect: cuda > mps > cpu + partition: + leaf_n: 4 + product_chunk_size: null + tree: null # Example: "R=0-3; L.R=4-7; L.L=8-11" + accumulation_dtype: null training: epochs: 100 diff --git a/conf/task/deap_eeg.yaml b/conf/task/deap_eeg.yaml index a7b10e2..975b09f 100644 --- a/conf/task/deap_eeg.yaml +++ b/conf/task/deap_eeg.yaml @@ -5,7 +5,14 @@ algebra: p: 3 q: 1 r: 0 + kernel: "auto" + partition_threshold: 8 device: "auto" + partition: + leaf_n: 4 + product_chunk_size: null + tree: null + accumulation_dtype: null dataset: name: "deap" diff --git a/conf/task/lqa.yaml b/conf/task/lqa.yaml index 6f2d057..84cc608 100644 --- a/conf/task/lqa.yaml +++ b/conf/task/lqa.yaml @@ -8,7 +8,14 @@ algebra: p: 4 q: 1 r: 0 + kernel: auto + partition_threshold: 8 device: auto + partition: + leaf_n: 4 + product_chunk_size: null + tree: null + accumulation_dtype: null model: channels: 16 diff --git a/conf/task/md17.yaml b/conf/task/md17.yaml index c68d29b..b9300cd 100644 --- a/conf/task/md17.yaml +++ b/conf/task/md17.yaml @@ -4,7 +4,14 @@ algebra: p: 3 q: 0 r: 1 + kernel: "auto" + partition_threshold: 8 device: "auto" + partition: + leaf_n: 4 + product_chunk_size: null + tree: null + accumulation_dtype: null dataset: name: "rmd17" molecule: "ethanol" diff --git a/conf/task/sr.yaml b/conf/task/sr.yaml index 514ded9..46b8f37 100644 --- a/conf/task/sr.yaml +++ b/conf/task/sr.yaml @@ -5,8 +5,15 @@ algebra: p: 4 q: 0 r: 0 + kernel: "auto" + partition_threshold: 8 device: "cpu" auto: true + partition: + leaf_n: 4 + product_chunk_size: null + tree: null + accumulation_dtype: null dataset: name: "sr" @@ -92,4 +99,4 @@ basis: corr_threshold: 0.05 max_expansion_factor: 3 dynamic_range_threshold: 100.0 - exp_max_input: 700.0 \ No newline at end of file + exp_max_input: 700.0 diff --git a/core/__init__.py b/core/__init__.py index 81ce57c..c0eccf5 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -12,6 +12,7 @@ """ from .algebra import CliffordAlgebra +from .config import AlgebraConfig, PartitionConfig, make_algebra, make_algebra_from_config from .decomposition import ( ExpPolicy, compiled_safe_decomposed_exp, @@ -36,7 +37,7 @@ signature_norm_squared, signature_trace_form, ) -from .module import CliffordModule +from .module import AlgebraLike, CliffordModule from .multivector import Multivector from .partitioned_algebra import PartitionedCliffordAlgebra from .validation import check_channels, check_multivector @@ -44,9 +45,14 @@ __all__ = [ # algebra "CliffordAlgebra", + "AlgebraConfig", + "AlgebraLike", "CliffordModule", "Multivector", + "PartitionConfig", "PartitionedCliffordAlgebra", + "make_algebra", + "make_algebra_from_config", # device / validation "DeviceConfig", "resolve_device", diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000..4a5c80f --- /dev/null +++ b/core/config.py @@ -0,0 +1,215 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Algebra construction config and dense/partitioned kernel dispatch.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Literal, Mapping, Optional + +import torch + +from core.algebra import CliffordAlgebra +from core.device import resolve_device +from core.module import AlgebraLike +from core.partitioned_algebra import PartitionedCliffordAlgebra + +AlgebraKernel = Literal["auto", "dense", "partitioned"] + + +@dataclass(frozen=True) +class PartitionConfig: + """Options specific to :class:`PartitionedCliffordAlgebra`.""" + + leaf_n: int = 4 + product_chunk_size: Optional[int] = None + tree: Optional[str] = None + accumulation_dtype: Optional[torch.dtype] = None + + @classmethod + def from_mapping(cls, config: Optional[Mapping[str, Any]]) -> "PartitionConfig": + """Build partition options from a Hydra/OmegaConf-compatible mapping.""" + if config is None: + return cls() + return cls( + leaf_n=int(_mapping_get(config, "leaf_n", 4)), + product_chunk_size=_optional_int(_mapping_get(config, "product_chunk_size", None)), + tree=_optional_str(_mapping_get(config, "tree", None)), + accumulation_dtype=_optional_dtype(_mapping_get(config, "accumulation_dtype", None)), + ) + + +@dataclass(frozen=True) +class AlgebraConfig: + """Dense/partitioned algebra declaration.""" + + p: int + q: int = 0 + r: int = 0 + kernel: AlgebraKernel = "auto" + partition_threshold: int = 8 + device: str = "cuda" + dtype: torch.dtype = torch.float32 + exp_policy: str = "balanced" + fixed_iterations: Optional[int] = None + partition: PartitionConfig = field(default_factory=PartitionConfig) + + @classmethod + def from_mapping(cls, config: Mapping[str, Any], **overrides) -> "AlgebraConfig": + """Build an algebra declaration from Hydra/OmegaConf config.""" + partition_mapping = _mapping_get(config, "partition", None) + if partition_mapping is None: + partition_mapping = _flat_partition_mapping(config) + + values = { + "p": int(_mapping_get(config, "p", 0)), + "q": int(_mapping_get(config, "q", 0)), + "r": int(_mapping_get(config, "r", 0)), + "kernel": _mapping_get(config, "kernel", "auto"), + "partition_threshold": int(_mapping_get(config, "partition_threshold", 8)), + "device": _mapping_get(config, "device", "cuda"), + "dtype": _optional_dtype(_mapping_get(config, "dtype", torch.float32)) or torch.float32, + "exp_policy": _mapping_get(config, "exp_policy", "balanced"), + "fixed_iterations": _optional_int(_mapping_get(config, "fixed_iterations", None)), + "partition": PartitionConfig.from_mapping(partition_mapping), + } + values.update({key: value for key, value in overrides.items() if value is not None}) + if not isinstance(values["partition"], PartitionConfig): + values["partition"] = PartitionConfig.from_mapping(values["partition"]) + values["dtype"] = _optional_dtype(values["dtype"]) or torch.float32 + return cls(**values) + + +def make_algebra( + p: int, + q: int = 0, + r: int = 0, + *, + kernel: AlgebraKernel = "auto", + partition_threshold: int = 8, + partition: Optional[PartitionConfig] = None, + device="cuda", + dtype: torch.dtype = torch.float32, + exp_policy: str = "balanced", + fixed_iterations: Optional[int] = None, +) -> AlgebraLike: + """Construct a dense or partitioned algebra according to a kernel policy.""" + kernel = _normalize_kernel(kernel) + n = p + q + r + selected_kernel = "partitioned" if kernel == "auto" and n > partition_threshold else kernel + if selected_kernel == "auto": + selected_kernel = "dense" + + resolved_device = resolve_device(device) if str(device) == "auto" else device + resolved_dtype = _optional_dtype(dtype) or torch.float32 + + if selected_kernel == "dense": + return CliffordAlgebra( + p, + q, + r, + device=resolved_device, + dtype=resolved_dtype, + exp_policy=exp_policy, + fixed_iterations=fixed_iterations, + ) + + partition = PartitionConfig() if partition is None else partition + return PartitionedCliffordAlgebra( + p, + q, + r, + device=resolved_device, + dtype=resolved_dtype, + leaf_n=partition.leaf_n, + product_chunk_size=partition.product_chunk_size, + exp_policy=exp_policy, + fixed_iterations=fixed_iterations, + accumulation_dtype=partition.accumulation_dtype, + partition_tree=partition.tree, + ) + + +def make_algebra_from_config(config: Mapping[str, Any], **overrides) -> AlgebraLike: + """Construct an algebra from a Hydra/OmegaConf-compatible config mapping.""" + algebra_config = AlgebraConfig.from_mapping(config, **overrides) + return make_algebra( + algebra_config.p, + algebra_config.q, + algebra_config.r, + kernel=algebra_config.kernel, + partition_threshold=algebra_config.partition_threshold, + partition=algebra_config.partition, + device=algebra_config.device, + dtype=algebra_config.dtype, + exp_policy=algebra_config.exp_policy, + fixed_iterations=algebra_config.fixed_iterations, + ) + + +def _mapping_get(config: Mapping[str, Any], key: str, default): + """Return a value from plain mappings or OmegaConf DictConfig objects.""" + if config is None: + return default + return config.get(key, default) + + +def _flat_partition_mapping(config: Mapping[str, Any]) -> dict[str, Any]: + """Return partition options from flat ``algebra.*`` aliases.""" + return { + "leaf_n": _mapping_get(config, "leaf_n", 4), + "product_chunk_size": _mapping_get(config, "product_chunk_size", None), + "tree": _mapping_get(config, "partition_tree", _mapping_get(config, "tree", None)), + "accumulation_dtype": _mapping_get(config, "accumulation_dtype", None), + } + + +def _normalize_kernel(kernel: str) -> AlgebraKernel: + """Validate and normalize algebra kernel names.""" + normalized = str(kernel).lower() + if normalized not in {"auto", "dense", "partitioned"}: + raise ValueError(f"Unknown algebra kernel {kernel!r}; expected 'auto', 'dense', or 'partitioned'") + return normalized # type: ignore[return-value] + + +def _optional_int(value) -> Optional[int]: + if value is None: + return None + return int(value) + + +def _optional_str(value) -> Optional[str]: + if value is None: + return None + text = str(value).strip() + return text or None + + +def _optional_dtype(value) -> Optional[torch.dtype]: + if value is None or isinstance(value, torch.dtype): + return value + if isinstance(value, str): + normalized = value.strip().lower() + if not normalized: + return None + aliases = { + "float16": torch.float16, + "fp16": torch.float16, + "half": torch.float16, + "bfloat16": torch.bfloat16, + "bf16": torch.bfloat16, + "float32": torch.float32, + "fp32": torch.float32, + "float": torch.float32, + "float64": torch.float64, + "fp64": torch.float64, + "double": torch.float64, + } + if normalized in aliases: + return aliases[normalized] + raise ValueError(f"Unsupported torch dtype declaration: {value!r}") diff --git a/core/module.py b/core/module.py index 4f28798..73ee8b3 100644 --- a/core/module.py +++ b/core/module.py @@ -7,9 +7,37 @@ """Base PyTorch module for components that share a Clifford algebra.""" +from typing import Protocol, runtime_checkable + +import torch import torch.nn as nn -from .algebra import CliffordAlgebra + +@runtime_checkable +class AlgebraLike(Protocol): + """Protocol implemented by dense and partitioned Clifford kernels.""" + + p: int + q: int + r: int + n: int + dim: int + eps: float + eps_sq: float + + @property + def device(self): + """Return the device of algebra-owned buffers.""" + ... + + @property + def dtype(self) -> torch.dtype: + """Return the dtype of algebra-owned floating-point buffers.""" + ... + + def _apply(self, fn): + """Move/cast algebra-owned buffers.""" + ... class CliffordModule(nn.Module): @@ -20,19 +48,19 @@ class CliffordModule(nn.Module): out of :mod:`layers` prevents functional code from importing the eager layer package just to subclass this base type. - The module stores a shared :class:`CliffordAlgebra` reference without - registering it as a PyTorch submodule. In Versor, one algebra instance often - owns the precomputed geometric tensors used by many modules. + The module stores a shared algebra reference without registering it as a + PyTorch submodule. In Versor, one algebra instance often owns the + precomputed geometric tensors used by many modules. """ - def __init__(self, algebra: CliffordAlgebra): + def __init__(self, algebra: AlgebraLike): """Set up the module with a shared algebra instance.""" super().__init__() # Bypass nn.Module.__setattr__ to avoid registering algebra as a child. object.__setattr__(self, "_algebra", algebra) @property - def algebra(self) -> CliffordAlgebra: + def algebra(self) -> AlgebraLike: """Return the shared algebra instance.""" return self._algebra diff --git a/core/multivector.py b/core/multivector.py index 313bd98..1e6ecd8 100644 --- a/core/multivector.py +++ b/core/multivector.py @@ -11,34 +11,34 @@ import torch -from core.algebra import CliffordAlgebra +from core.module import AlgebraLike class Multivector: """Object-oriented multivector wrapper with operator overloading. - Wraps a raw coefficient tensor and its parent ``CliffordAlgebra``, + Wraps a raw coefficient tensor and its parent algebra kernel, exposing every core algebra operation as a method or Python operator. Attributes: - algebra (CliffordAlgebra): The backend. + algebra (AlgebraLike): The backend. tensor (torch.Tensor): The raw data [..., Dim]. """ __slots__ = ("algebra", "tensor") - def __init__(self, algebra: CliffordAlgebra, tensor: torch.Tensor): + def __init__(self, algebra: AlgebraLike, tensor: torch.Tensor): self.algebra = algebra self.tensor = tensor @classmethod - def from_vectors(cls, algebra: CliffordAlgebra, vectors: torch.Tensor) -> Multivector: + def from_vectors(cls, algebra: AlgebraLike, vectors: torch.Tensor) -> Multivector: """Promotes vectors to multivectors (Grade 1).""" return cls(algebra, algebra.embed_vector(vectors)) @classmethod def scalar( - cls, algebra: CliffordAlgebra, value: float | torch.Tensor, batch_shape: tuple[int, ...] = () + cls, algebra: AlgebraLike, value: float | torch.Tensor, batch_shape: tuple[int, ...] = () ) -> Multivector: """Creates a scalar multivector (grade 0 only).""" dim = 2**algebra.n diff --git a/core/partitioned_algebra.py b/core/partitioned_algebra.py index 4a67a0d..2c8a3ba 100644 --- a/core/partitioned_algebra.py +++ b/core/partitioned_algebra.py @@ -193,6 +193,29 @@ def __iter__(self): yield self.right_product_signs +@dataclass(frozen=True) +class _PartitionTreeSpec: + """Validated relative split tree for one partitioned node.""" + + right_dims: tuple[int, ...] + left_dims: tuple[int, ...] + right: Optional["_PartitionTreeSpec"] = None + left: Optional["_PartitionTreeSpec"] = None + + @property + def split_dims(self) -> tuple[int, ...]: + return self.right_dims + self.left_dims + + def fingerprint(self) -> tuple: + """Return a relative structural fingerprint for safe subalgebra reuse.""" + return ( + self.right_dims, + None if self.right is None else self.right.fingerprint(), + self.left_dims, + None if self.left is None else self.left.fingerprint(), + ) + + def _partition_split(p: int, q: int, r: int) -> _PartitionSplit: """Return the single recursive split used by the partitioned algebra. @@ -262,6 +285,224 @@ def _build_partition_split( ) +def _partition_tree_fingerprint(partition_tree: Optional[_PartitionTreeSpec]) -> object: + """Return the cache fingerprint for a child partition tree.""" + return "auto" if partition_tree is None else partition_tree.fingerprint() + + +def _split_from_tree_spec(p: int, q: int, r: int, tree: _PartitionTreeSpec) -> _PartitionSplit: + """Create a concrete split from an explicit relative tree node.""" + assert sorted(tree.split_dims) == list(range(p + q + r)) + right_signature = _signature_for_dims(p, q, r, tree.right_dims) + left_signature = _signature_for_dims(p, q, r, tree.left_dims) + return _PartitionSplit( + right_signature=right_signature, + left_signature=left_signature, + right_dims=tree.right_dims, + left_dims=tree.left_dims, + ) + + +def _signature_for_dims(p: int, q: int, r: int, dims: Sequence[int]) -> tuple[int, int, int]: + """Return child signature counts for selected local basis-vector dims.""" + n = p + q + r + p_count = 0 + q_count = 0 + r_count = 0 + for dim in dims: + if dim < 0 or dim >= n: + raise ValueError(f"Partition dim {dim} is outside [0, {n})") + if dim < p: + p_count += 1 + elif dim < p + q: + q_count += 1 + else: + r_count += 1 + return p_count, q_count, r_count + + +def _normalize_partition_tree( + partition_tree, + p: int, + q: int, + r: int, +) -> Optional[_PartitionTreeSpec]: + """Resolve a public partition tree expression into a relative tree spec.""" + if partition_tree is None: + return None + if isinstance(partition_tree, _PartitionTreeSpec): + return partition_tree + if isinstance(partition_tree, str): + expression = partition_tree.strip() + if not expression or expression.lower() in {"auto", "none", "null"}: + return None + assignments = _parse_partition_tree_expression(expression, p + q + r) + root_dims = _canonical_global_dims(tuple(range(p + q + r)), p, q, r) + return _build_partition_tree_from_assignments(assignments, (), root_dims, p, q, r) + raise TypeError( + "partition_tree must be None, 'auto', a path expression string, " + f"or an internal _PartitionTreeSpec, got {type(partition_tree).__name__}" + ) + + +def _parse_partition_tree_expression(expression: str, n: int) -> dict[tuple[str, ...], tuple[int, ...]]: + """Parse ``R=0-3; L.R=4-7`` style partition expressions.""" + assignments: dict[tuple[str, ...], tuple[int, ...]] = {} + used_dims: dict[int, tuple[str, ...]] = {} + + for raw_entry in expression.split(";"): + entry = raw_entry.strip() + if not entry: + continue + if "=" not in entry: + raise ValueError(f"Invalid partition tree entry {entry!r}; expected PATH=DIMS") + raw_path, raw_dims = entry.split("=", 1) + path = _parse_partition_path(raw_path) + dims = _parse_dim_expression(raw_dims, n) + if path in assignments: + raise ValueError(f"Duplicate partition path {'.'.join(path)}") + for dim in dims: + if dim in used_dims: + previous = ".".join(used_dims[dim]) + current = ".".join(path) + raise ValueError(f"Partition dim {dim} appears in both {previous} and {current}") + used_dims[dim] = path + assignments[path] = dims + + if not assignments: + raise ValueError("Partition tree expression did not contain any PATH=DIMS entries") + + expected_dims = set(range(n)) + actual_dims = set(used_dims) + if actual_dims != expected_dims: + missing = sorted(expected_dims - actual_dims) + extra = sorted(actual_dims - expected_dims) + raise ValueError(f"Partition tree must cover every dimension exactly once; missing={missing}, extra={extra}") + + return assignments + + +def _parse_partition_path(raw_path: str) -> tuple[str, ...]: + """Parse a tree path made of ``L`` and ``R`` segments.""" + path = tuple(segment.strip().upper() for segment in raw_path.strip().split(".") if segment.strip()) + if not path: + raise ValueError("Partition path cannot be empty") + invalid = [segment for segment in path if segment not in {"L", "R"}] + if invalid: + raise ValueError(f"Invalid partition path segment(s): {invalid}; use only L and R") + return path + + +def _parse_dim_expression(raw_dims: str, n: int) -> tuple[int, ...]: + """Parse comma-separated dimensions and inclusive ranges.""" + dims: list[int] = [] + for raw_token in raw_dims.split(","): + token = raw_token.strip() + if not token: + continue + if "-" in token: + raw_start, raw_end = token.split("-", 1) + start = int(raw_start.strip()) + end = int(raw_end.strip()) + if end < start: + raise ValueError(f"Invalid descending partition range {token!r}") + dims.extend(range(start, end + 1)) + else: + dims.append(int(token)) + + if not dims: + raise ValueError("Partition dimension expression cannot be empty") + + unique_dims = tuple(dict.fromkeys(dims)) + if len(unique_dims) != len(dims): + raise ValueError(f"Partition dimension expression contains duplicates: {raw_dims!r}") + for dim in unique_dims: + if dim < 0 or dim >= n: + raise ValueError(f"Partition dim {dim} is outside [0, {n})") + return unique_dims + + +def _build_partition_tree_from_assignments( + assignments: dict[tuple[str, ...], tuple[int, ...]], + path: tuple[str, ...], + node_global_dims: tuple[int, ...], + p: int, + q: int, + r: int, +) -> _PartitionTreeSpec: + """Build a relative tree spec for one node from global path assignments.""" + right_global_dims = _assigned_dims_under(assignments, path + ("R",)) + left_global_dims = _assigned_dims_under(assignments, path + ("L",)) + + if not right_global_dims or not left_global_dims: + label = "root" if not path else ".".join(path) + raise ValueError(f"Partition node {label} must define both L and R children") + + node_dim_set = set(node_global_dims) + if set(right_global_dims) | set(left_global_dims) != node_dim_set: + label = "root" if not path else ".".join(path) + raise ValueError(f"Partition node {label} children must cover exactly its parent dimensions") + if set(right_global_dims) & set(left_global_dims): + label = "root" if not path else ".".join(path) + raise ValueError(f"Partition node {label} has overlapping L/R dimensions") + + right_child_global_dims = _canonical_global_dims(right_global_dims, p, q, r) + left_child_global_dims = _canonical_global_dims(left_global_dims, p, q, r) + local_index = {dim: index for index, dim in enumerate(node_global_dims)} + right_dims = tuple(local_index[dim] for dim in right_child_global_dims) + left_dims = tuple(local_index[dim] for dim in left_child_global_dims) + + right_tree = None + left_tree = None + if _has_descendant_assignment(assignments, path + ("R",)): + right_tree = _build_partition_tree_from_assignments( + assignments, + path + ("R",), + right_child_global_dims, + p, + q, + r, + ) + if _has_descendant_assignment(assignments, path + ("L",)): + left_tree = _build_partition_tree_from_assignments( + assignments, + path + ("L",), + left_child_global_dims, + p, + q, + r, + ) + + return _PartitionTreeSpec(right_dims=right_dims, left_dims=left_dims, right=right_tree, left=left_tree) + + +def _assigned_dims_under( + assignments: dict[tuple[str, ...], tuple[int, ...]], + path: tuple[str, ...], +) -> tuple[int, ...]: + """Return all globally assigned dims under a path.""" + dims: list[int] = [] + for assigned_path, assigned_dims in assignments.items(): + if assigned_path[: len(path)] == path: + dims.extend(assigned_dims) + return tuple(dims) + + +def _has_descendant_assignment( + assignments: dict[tuple[str, ...], tuple[int, ...]], + path: tuple[str, ...], +) -> bool: + """Whether a path has deeper assignments than itself.""" + return any(len(assigned_path) > len(path) and assigned_path[: len(path)] == path for assigned_path in assignments) + + +def _canonical_global_dims(dims: Sequence[int], p: int, q: int, r: int) -> tuple[int, ...]: + """Order global dimensions by local Clifford signature convention.""" + n = p + q + r + dim_set = set(dims) + return tuple(dim for dim in range(n) if dim in dim_set) + + def _grade_index(n: int, device) -> torch.Tensor: """Return the grade, i.e. popcount, for basis indices ``0..2**n-1``.""" basis_indices = torch.arange(2**n, dtype=torch.long, device=device) @@ -388,6 +629,7 @@ def _subalgebra_cache_key( exp_policy, fixed_iterations: int, accumulation_dtype: Optional[torch.dtype], + partition_tree: Optional[_PartitionTreeSpec], ) -> tuple: """Return the per-tree cache key for structurally identical sub-algebras.""" return ( @@ -401,6 +643,7 @@ def _subalgebra_cache_key( getattr(exp_policy, "value", str(exp_policy)), fixed_iterations, str(accumulation_dtype), + _partition_tree_fingerprint(partition_tree), ) @@ -743,6 +986,9 @@ class PartitionedCliffordAlgebra(nn.Module): exponential paths. ``None`` derives it from policy, dtype, and n. accumulation_dtype (torch.dtype, optional): Optional promoted dtype for recursive product accumulation. + partition_tree (str, optional): Explicit split expression such as + ``"R=0-3; L.R=4-7; L.L=8-11"``. ``None`` or ``"auto"`` uses the + automatic repeated-tile/balanced splitter. """ def __init__( @@ -757,6 +1003,7 @@ def __init__( exp_policy: str = "balanced", fixed_iterations: Optional[int] = None, accumulation_dtype: Optional[torch.dtype] = None, + partition_tree=None, _subalgebra_cache: Optional[dict] = None, ): super().__init__() @@ -769,8 +1016,9 @@ def __init__( self._init_signature(p, q, r, leaf_n, product_chunk_size, accumulation_dtype) self._init_exp_settings(dtype, exp_policy, fixed_iterations) self._init_structural_buffers(device, dtype) + self._partition_tree = _normalize_partition_tree(partition_tree, self.p, self.q, self.r) - if self.n <= leaf_n: + if self.n <= leaf_n and self._partition_tree is None: self._init_leaf_node(device, dtype) return @@ -833,7 +1081,11 @@ def _init_leaf_node(self, device, dtype: torch.dtype) -> None: def _init_recursive_node(self, device, dtype: torch.dtype, subalgebra_cache: dict) -> None: """Configure split layout, child modules, and runtime product planning.""" - split = _partition_split(self.p, self.q, self.r) + split = ( + _partition_split(self.p, self.q, self.r) + if self._partition_tree is None + else _split_from_tree_spec(self.p, self.q, self.r, self._partition_tree) + ) self._init_split_layout(split, device) self.core = None self.left_sub, self.right_sub = self._create_child_subalgebras(split, device, dtype, subalgebra_cache) @@ -869,8 +1121,10 @@ def _create_child_subalgebras( "accumulation_dtype": self.accumulation_dtype, "subalgebra_cache": subalgebra_cache, } - left_sub = self._get_or_create_subalgebra(*split.left_signature, **child_kwargs) - right_sub = self._get_or_create_subalgebra(*split.right_signature, **child_kwargs) + left_tree = None if self._partition_tree is None else self._partition_tree.left + right_tree = None if self._partition_tree is None else self._partition_tree.right + left_sub = self._get_or_create_subalgebra(*split.left_signature, partition_tree=left_tree, **child_kwargs) + right_sub = self._get_or_create_subalgebra(*split.right_signature, partition_tree=right_tree, **child_kwargs) return left_sub, right_sub @classmethod @@ -887,6 +1141,7 @@ def _get_or_create_subalgebra( exp_policy, fixed_iterations: int, accumulation_dtype: Optional[torch.dtype], + partition_tree: Optional[_PartitionTreeSpec], subalgebra_cache: dict, ) -> "PartitionedCliffordAlgebra": """Create or reuse a child with the same algebraic structure. @@ -907,6 +1162,7 @@ def _get_or_create_subalgebra( exp_policy, fixed_iterations, accumulation_dtype, + partition_tree, ) subalgebra = subalgebra_cache.get(cache_key) if subalgebra is None: @@ -921,6 +1177,7 @@ def _get_or_create_subalgebra( exp_policy=exp_policy, fixed_iterations=fixed_iterations, accumulation_dtype=accumulation_dtype, + partition_tree=partition_tree, _subalgebra_cache=subalgebra_cache, ) subalgebra_cache[cache_key] = subalgebra diff --git a/docs/api/core.md b/docs/api/core.md index 1970930..8aa53db 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -5,6 +5,9 @@ The mathematical kernel of Versor. ## Algebra ::: core.algebra.CliffordAlgebra +## Algebra Config +::: core.config + ## Module ::: core.module.CliffordModule diff --git a/tasks/deap_eeg.py b/tasks/deap_eeg.py index 2595096..7ee8c2d 100644 --- a/tasks/deap_eeg.py +++ b/tasks/deap_eeg.py @@ -16,7 +16,7 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from datalib.deap import get_deap_loaders, get_group_sizes from log import get_logger from models.deap import EEGNet @@ -45,7 +45,8 @@ def __init__(self, cfg): super().__init__(cfg) def setup_algebra(self): - return CliffordAlgebra( + return make_algebra_from_config( + self.cfg.algebra, p=self.cfg.algebra.get("p", 3), q=self.cfg.algebra.get("q", 1), r=self.cfg.algebra.get("r", 0), diff --git a/tasks/lqa.py b/tasks/lqa.py index 2d049e5..17a4e63 100644 --- a/tasks/lqa.py +++ b/tasks/lqa.py @@ -21,7 +21,7 @@ import torch.nn.functional as F from omegaconf import DictConfig -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from datalib.lqa import get_lqa_loaders from functional.loss import AsymmetryLoss, InvolutionConsistencyLoss from log import get_logger @@ -57,7 +57,7 @@ def setup_algebra(self): p = self.cfg.algebra.get("p", 4) q = self.cfg.algebra.get("q", 1) r = self.cfg.algebra.get("r", 0) - return CliffordAlgebra(p, q, r, device=self.device) + return make_algebra_from_config(self.cfg.algebra, p=p, q=q, r=r, device=self.device) def setup_model(self): """GLRNet with probe-specific head.""" diff --git a/tasks/md17.py b/tasks/md17.py index 4b78566..93bebf7 100644 --- a/tasks/md17.py +++ b/tasks/md17.py @@ -8,7 +8,7 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from core.metric import hermitian_grade_spectrum, hermitian_norm from datalib.md17 import get_md17_loaders from functional.loss import ConservativeLoss, HermitianGradeRegularization @@ -50,7 +50,14 @@ def __init__(self, cfg): def setup_algebra(self): """Use Cl(3,0,1) PGA for SE(3) rigid-body motions.""" exp_policy = self.cfg.model.get("exp_policy", "balanced") - return CliffordAlgebra(p=3, q=0, r=self.cfg.algebra.get("r", 1), device=self.device, exp_policy=exp_policy) + return make_algebra_from_config( + self.cfg.algebra, + p=3, + q=0, + r=self.cfg.algebra.get("r", 1), + device=self.device, + exp_policy=exp_policy, + ) def setup_model(self): """Build MD17ForceNet model with PGA motors, dynamic rotors, and RBF.""" diff --git a/tasks/symbolic_regression.py b/tasks/symbolic_regression.py index 6156402..aef7206 100644 --- a/tasks/symbolic_regression.py +++ b/tasks/symbolic_regression.py @@ -20,7 +20,8 @@ import torch.optim as optim from omegaconf import DictConfig -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config +from core.module import AlgebraLike from datalib.symbolic_regression import _fetch_pmlb_data, get_dataset_ids, get_sr_loaders, get_sr_raw_splits from log import get_logger from models.sr import SRGBN @@ -155,7 +156,7 @@ def _run_metric_search(self, cfg): logger.info(f"MetricSearch: Cl({p},{q},{r}) for {self.dataset_name}") return (p, q, r) - def setup_algebra(self) -> CliffordAlgebra: + def setup_algebra(self) -> AlgebraLike: """Use searched signature or configured Cl(p,q,r).""" if self._searched_signature is not None: p, q, r = self._searched_signature @@ -164,7 +165,14 @@ def setup_algebra(self) -> CliffordAlgebra: q = self.cfg.algebra.get("q", 0) r = self.cfg.algebra.get("r", 0) exp_policy = self.cfg.model.get("exp_policy", "balanced") - return CliffordAlgebra(p=p, q=q, r=r, device=self.device, exp_policy=exp_policy) + return make_algebra_from_config( + self.cfg.algebra, + p=p, + q=q, + r=r, + device=self.device, + exp_policy=exp_policy, + ) def setup_model(self) -> SRGBN: """Build SRGBN with config parameters, optionally auto-sizing.""" From 0f6b6b8a985ccd7d08e58f56aca25dbb51e92ef7 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 6 May 2026 18:19:06 +0900 Subject: [PATCH 06/45] feat: route algebra construction through factory --- benchmarks/benchmark_core.py | 149 +++++++++++++++++++--------- core/__init__.py | 5 +- core/analysis/dimension.py | 13 +-- core/analysis/pipeline.py | 11 +- core/analysis/sampler.py | 4 +- core/analysis/signature.py | 15 +-- core/config.py | 38 ++----- core/device.py | 73 ++++++++++---- examples/demo.py | 4 +- examples/tasks/cgenn.py | 6 +- examples/tasks/clifford_pde.py | 6 +- examples/tasks/gatr.py | 5 +- examples/tasks/hyperbolic.py | 4 +- examples/tasks/manifold.py | 10 +- examples/tasks/sanity_check.py | 4 +- experiments/_gdo/benchmarks.py | 8 +- experiments/_gdo/pre_exploration.py | 7 +- experiments/_lib.py | 49 +++++++-- experiments/inc_lattice_morph.py | 15 ++- models/deap/eeg_net.py | 35 ++++--- models/sr/estimator.py | 4 +- models/sr/grouper.py | 21 ++-- models/sr/implicit.py | 4 +- models/sr/phases/extraction.py | 4 +- models/sr/phases/prep.py | 4 +- tasks/base.py | 1 + tasks/deap_eeg.py | 1 + 27 files changed, 314 insertions(+), 186 deletions(-) diff --git a/benchmarks/benchmark_core.py b/benchmarks/benchmark_core.py index eb00aa1..1b9c33f 100644 --- a/benchmarks/benchmark_core.py +++ b/benchmarks/benchmark_core.py @@ -55,16 +55,12 @@ if str(_REPO_ROOT) not in sys.path: sys.path.insert(0, str(_REPO_ROOT)) -from core.algebra import CliffordAlgebra +from core.config import PartitionConfig, make_algebra from core.decomposition import ExpPolicy, compiled_safe_decomposed_exp # noqa: E402 -from core.device import resolve_device +from core.device import FLOAT_DTYPES, dtype_name as _format_dtype_name, optional_dtype, resolve_device +from core.module import AlgebraLike -DTYPES: dict[str, torch.dtype] = { - "float64": torch.float64, - "float32": torch.float32, - "bfloat16": torch.bfloat16, - "float16": torch.float16, -} +DTYPES: dict[str, torch.dtype] = FLOAT_DTYPES @dataclass(frozen=True) @@ -97,7 +93,7 @@ def label(self) -> str: class CoreOpModule(nn.Module): """Small wrapper so core operators can be passed to torch.compile.""" - def __init__(self, algebra: CliffordAlgebra, op: str): + def __init__(self, algebra: AlgebraLike, op: str): super().__init__() self.algebra = algebra self.op = op @@ -160,20 +156,49 @@ def _parse_signature_csv(value: str) -> list[SignatureSpec]: raise ValueError( f"invalid signature {raw!r}; use n, p:q, or p:q:r entries" ) - if p < 0 or q < 0 or r < 0 or p + q + r < 1 or p + q + r > 12: + if p < 0 or q < 0 or r < 0 or p + q + r < 1 or p + q + r > 16: raise ValueError( f"invalid signature {raw!r}; dimensions must be non-negative " - "and sum to 1..12" + "and sum to 1..16" ) specs.append(SignatureSpec(p, q, r)) return specs def _dtype_name(dtype: torch.dtype) -> str: - for name, candidate in DTYPES.items(): - if candidate == dtype: - return name - return str(dtype).replace("torch.", "") + return _format_dtype_name(dtype) + + +def setup_algebra( + p: int, + q: int = 0, + r: int = 0, + *, + device: str, + dtype: torch.dtype, + exp_policy: str | ExpPolicy = "balanced", + fixed_iterations: int | None = None, + args: argparse.Namespace | None = None, +) -> AlgebraLike: + """Construct benchmark algebras through the shared core factory.""" + partition = PartitionConfig( + leaf_n=getattr(args, "partition_leaf_n", 4), + product_chunk_size=getattr(args, "partition_product_chunk_size", None), + tree=getattr(args, "partition_tree", None), + accumulation_dtype=optional_dtype(getattr(args, "partition_accumulation_dtype", None)), + ) + return make_algebra( + p=p, + q=q, + r=r, + kernel=getattr(args, "algebra_kernel", "auto"), + partition_threshold=getattr(args, "partition_threshold", 8), + partition=partition, + device=device, + dtype=dtype, + exp_policy=exp_policy, + fixed_iterations=fixed_iterations, + ) def _ordered_modes(value: str) -> list[str]: @@ -364,7 +389,7 @@ def _seed_all(seed: int, device: str) -> None: def _supported_dtypes( - device: str, + args: argparse.Namespace, requested: str, probe_n_values: Iterable[int], ) -> list[torch.dtype]: @@ -383,15 +408,15 @@ def _supported_dtypes( if name not in DTYPES: raise ValueError(f"unknown dtype {name!r}; valid: {sorted(DTYPES)}") dtype = DTYPES[name] - algebra: CliffordAlgebra | None = None + algebra: AlgebraLike | None = None x: torch.Tensor | None = None y: torch.Tensor | None = None try: for n in probe_ns: - algebra = CliffordAlgebra(n, 0, device=device, dtype=dtype) - x = torch.randn(2, algebra.dim, device=device, dtype=dtype) + algebra = setup_algebra(n, 0, device=args.device, dtype=dtype, args=args) + x = torch.randn(2, algebra.dim, device=args.device, dtype=dtype) y = algebra.geometric_product(x, x) - _sync(device) + _sync(args.device) if not torch.isfinite(y.float()).all().item(): raise RuntimeError(f"n={n} probe produced non-finite values") algebra = None @@ -399,14 +424,14 @@ def _supported_dtypes( y = None supported.append(dtype) except Exception as exc: - print(f"Skipping dtype {name} on {device}: {exc}") + print(f"Skipping dtype {name} on {args.device}: {exc}") finally: algebra = None x = None y = None - _release_memory(device) + _release_memory(args.device) if not supported: - raise SystemExit(f"No requested dtypes are usable on {device}.") + raise SystemExit(f"No requested dtypes are usable on {args.device}.") return supported @@ -532,7 +557,7 @@ def _nonzero_terms(tensor: torch.Tensor) -> int: def _analytic_forward_metrics( - algebra: CliffordAlgebra, + algebra: AlgebraLike, op: str, batch: int, channels: int, @@ -544,16 +569,20 @@ def _analytic_forward_metrics( io_elements = 0 if op == "gp": - flops = 2.0 * batch * _nonzero_terms(algebra.gp_signs) + signs = getattr(algebra, "gp_signs", None) + flops = 2.0 * batch * _nonzero_terms(signs) if signs is not None else float("nan") io_elements = 3 * batch * dim elif op == "wedge": - flops = 2.0 * batch * _nonzero_terms(algebra.wedge_gp_signs) + signs = getattr(algebra, "wedge_gp_signs", None) + flops = 2.0 * batch * _nonzero_terms(signs) if signs is not None else float("nan") io_elements = 3 * batch * dim elif op == "inner": - flops = 2.0 * batch * _nonzero_terms(algebra.inner_gp_signs) + signs = getattr(algebra, "inner_gp_signs", None) + flops = 2.0 * batch * _nonzero_terms(signs) if signs is not None else float("nan") io_elements = 3 * batch * dim elif op == "commutator": - flops = 2.0 * batch * _nonzero_terms(algebra.comm_gp_signs) + signs = getattr(algebra, "comm_gp_signs", None) + flops = 2.0 * batch * _nonzero_terms(signs) if signs is not None else float("nan") io_elements = 3 * batch * dim elif op in {"grade2", "reverse"}: flops = float(batch * dim) @@ -1044,7 +1073,7 @@ def _commuting_pairs(n: int) -> list[tuple[int, int]]: def _make_commuting_nonsimple_bivector( - algebra: CliffordAlgebra, + algebra: AlgebraLike, batch: int, scale: float, ) -> torch.Tensor: @@ -1065,7 +1094,7 @@ def _make_commuting_nonsimple_bivector( return b -def _exact_commuting_exp(algebra: CliffordAlgebra, b: torch.Tensor) -> torch.Tensor: +def _exact_commuting_exp(algebra: AlgebraLike, b: torch.Tensor) -> torch.Tensor: """Exact exp for the controlled non-simple bivector used here. The bivector is a sum of disjoint coordinate-plane bivectors. Those @@ -1081,7 +1110,7 @@ def _exact_commuting_exp(algebra: CliffordAlgebra, b: torch.Tensor) -> torch.Ten return result -def _grade_leak(algebra: CliffordAlgebra, x: torch.Tensor, grade: int) -> float: +def _grade_leak(algebra: AlgebraLike, x: torch.Tensor, grade: int) -> float: mask = algebra.grade_masks_float[grade] if mask.dtype != x.dtype: mask = mask.to(dtype=x.dtype) @@ -1090,7 +1119,7 @@ def _grade_leak(algebra: CliffordAlgebra, x: torch.Tensor, grade: int) -> float: def _make_speed_inputs( - algebra: CliffordAlgebra, + algebra: AlgebraLike, op: str, batch: int, channels: int, @@ -1120,7 +1149,7 @@ def _make_speed_inputs( def _random_multivector( - algebra: CliffordAlgebra, + algebra: AlgebraLike, shape: tuple[int, ...], scale: float, ) -> torch.Tensor: @@ -1131,7 +1160,7 @@ def _random_multivector( def _random_grade( - algebra: CliffordAlgebra, + algebra: AlgebraLike, shape: tuple[int, ...], grade: int, scale: float, @@ -1164,13 +1193,14 @@ def run_stability_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) -> for sig_index, signature in enumerate(signatures): _release_memory(args.device) try: - algebra = CliffordAlgebra( + algebra = setup_algebra( signature.p, signature.q, signature.r, device=args.device, dtype=dtype, exp_policy=ExpPolicy.PRECISE, + args=args, ) except Exception as exc: _record_stability_failure(rows, args, dtype_name, signature, "setup", exc) @@ -1368,13 +1398,14 @@ def per_channel_sandwich() -> tuple[float, float, str]: def exp_policy_consistency() -> tuple[float, float, str]: if algebra.n < 2: return 0.0, 0.0, "skipped: no bivectors" - balanced = CliffordAlgebra( + balanced = setup_algebra( signature.p, signature.q, signature.r, device=args.device, dtype=dtype, exp_policy=ExpPolicy.BALANCED, + args=args, ) with torch.no_grad(): b = torch.zeros( @@ -1569,12 +1600,13 @@ def run_speed_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) -> list try: with _tf32_context(args.device, tf32_mode): row.update(_current_tf32_flags(args.device, tf32_mode)) - algebra = CliffordAlgebra( + algebra = setup_algebra( n, 0, device=args.device, dtype=dtype, exp_policy=_op_exp_policy(op), + args=args, ) row.update( _analytic_forward_metrics( @@ -1740,12 +1772,13 @@ def run_backward_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) -> l try: with _tf32_context(args.device, tf32_mode): row.update(_current_tf32_flags(args.device, tf32_mode)) - algebra = CliffordAlgebra( + algebra = setup_algebra( n, 0, device=args.device, dtype=dtype, exp_policy=_op_exp_policy(op), + args=args, ) row.update( _analytic_forward_metrics( @@ -1920,12 +1953,13 @@ def run_fusion_suite( try: with _tf32_context(args.device, tf32_mode): row.update(_current_tf32_flags(args.device, tf32_mode)) - algebra = CliffordAlgebra( + algebra = setup_algebra( n, 0, device=args.device, dtype=dtype, exp_policy=_op_exp_policy(op), + args=args, ) row.update( _analytic_forward_metrics( @@ -2070,7 +2104,7 @@ def run_nonsimple_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) -> for n in args.nonsimple_n_values: if n < 4: continue - ref_alg = CliffordAlgebra(n, 0, device="cpu", dtype=torch.float64, exp_policy="precise") + ref_alg = setup_algebra(n, 0, device="cpu", dtype=torch.float64, exp_policy="precise", args=args) b_ref = _make_commuting_nonsimple_bivector(ref_alg, args.error_batch, args.bivector_scale) r_ref = _exact_commuting_exp(ref_alg, b_ref) bb_ref = ref_alg.geometric_product(b_ref, b_ref) @@ -2094,12 +2128,13 @@ def run_nonsimple_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) -> "nonscalar_BB_norm": nonscalar_norm, } try: - algebra = CliffordAlgebra( + algebra = setup_algebra( n, 0, device=args.device, dtype=dtype, exp_policy=policy, + args=args, ) b = _make_commuting_nonsimple_bivector( algebra, @@ -2169,7 +2204,7 @@ def run_cumulative_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) -> if n < 4: continue for batch in args.cumulative_batch_sizes: - ref_alg = CliffordAlgebra(n, 0, device="cpu", dtype=torch.float64, exp_policy="precise") + ref_alg = setup_algebra(n, 0, device="cpu", dtype=torch.float64, exp_policy="precise", args=args) b_ref = _make_commuting_nonsimple_bivector( ref_alg, batch, @@ -2182,7 +2217,14 @@ def run_cumulative_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) -> dtype_name = _dtype_name(dtype) _release_memory(args.device) try: - algebra = CliffordAlgebra(n, 0, device=args.device, dtype=dtype, exp_policy="balanced") + algebra = setup_algebra( + n, + 0, + device=args.device, + dtype=dtype, + exp_policy="balanced", + args=args, + ) b = _make_commuting_nonsimple_bivector( algebra, batch, @@ -2276,7 +2318,7 @@ def run_convergence_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) - for n in args.convergence_n_values: if n < 4: continue - ref_alg = CliffordAlgebra(n, 0, device="cpu", dtype=torch.float64, exp_policy="precise") + ref_alg = setup_algebra(n, 0, device="cpu", dtype=torch.float64, exp_policy="precise", args=args) b_ref = _make_commuting_nonsimple_bivector(ref_alg, args.error_batch, args.bivector_scale) r_ref = _exact_commuting_exp(ref_alg, b_ref) @@ -2296,7 +2338,14 @@ def run_convergence_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) - "error": "", } try: - algebra = CliffordAlgebra(n, 0, device=args.device, dtype=dtype, exp_policy="precise") + algebra = setup_algebra( + n, + 0, + device=args.device, + dtype=dtype, + exp_policy="precise", + args=args, + ) b = _make_commuting_nonsimple_bivector( algebra, args.error_batch, @@ -2307,7 +2356,7 @@ def run_convergence_suite(args: argparse.Namespace, dtypes: list[torch.dtype]) - def exp_fixed( inp: torch.Tensor, - alg: CliffordAlgebra = algebra, + alg: AlgebraLike = algebra, iterations: int = fixed_iterations, ) -> torch.Tensor: return compiled_safe_decomposed_exp( @@ -3441,6 +3490,12 @@ def _collect_runtime_metadata(device: str) -> dict[str, Any]: def make_argparser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Benchmark the Versor core package.") parser.add_argument("--device", default="auto", help="cpu, cuda, mps, or auto") + parser.add_argument("--algebra-kernel", default="auto", choices=("auto", "dense", "partitioned")) + parser.add_argument("--partition-threshold", type=int, default=8) + parser.add_argument("--partition-leaf-n", type=int, default=4) + parser.add_argument("--partition-product-chunk-size", type=int, default=None) + parser.add_argument("--partition-tree", default=None) + parser.add_argument("--partition-accumulation-dtype", default=None) parser.add_argument("--out", default="benchmarks/results", help="artifact root") parser.add_argument("--sections", default="speed,backward,fusion,nonsimple,cumulative,convergence,stability") parser.add_argument("--n-values", type=_parse_int_csv, default=_parse_int_csv("2,3,4,5,6")) @@ -3594,7 +3649,7 @@ def main() -> None: else: args.dtype_probe_n_values = sorted(set(args.dtype_probe_n_values)) - dtypes = _supported_dtypes(args.device, args.dtypes, args.dtype_probe_n_values) + dtypes = _supported_dtypes(args, args.dtypes, args.dtype_probe_n_values) args.dtypes_resolved = dtypes timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") diff --git a/core/__init__.py b/core/__init__.py index c0eccf5..bb1d072 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -20,7 +20,7 @@ exp_simple_bivector, ga_power_iteration, ) -from .device import DeviceConfig, resolve_device +from .device import DeviceConfig, dtype_name, optional_dtype, resolve_device, resolve_dtype from .metric import ( clifford_conjugate, geometric_distance, @@ -55,7 +55,10 @@ "make_algebra_from_config", # device / validation "DeviceConfig", + "dtype_name", + "optional_dtype", "resolve_device", + "resolve_dtype", "check_multivector", "check_channels", # metric diff --git a/core/analysis/dimension.py b/core/analysis/dimension.py index 958e33b..c22c87e 100644 --- a/core/analysis/dimension.py +++ b/core/analysis/dimension.py @@ -17,7 +17,8 @@ import torch -from core.algebra import CliffordAlgebra +from core.config import make_algebra +from core.module import AlgebraLike from ._types import CONSTANTS, DimensionResult @@ -196,7 +197,7 @@ def __init__(self, device: str = "cpu"): def lift( self, data: torch.Tensor, - target_algebra: CliffordAlgebra, + target_algebra: AlgebraLike, fill: float = 1.0, ) -> torch.Tensor: """Lifts data into the grade-1 subspace of a higher-dimensional algebra. @@ -260,7 +261,7 @@ def test( data = data.to(self.device) results: Dict = {} - def _measure(alg: CliffordAlgebra, mv: torch.Tensor) -> Dict: + def _measure(alg: AlgebraLike, mv: torch.Tensor) -> Dict: gf = GeodesicFlow(alg, k=k) coh = gf.coherence(mv) curv = gf.curvature(mv) @@ -273,15 +274,15 @@ def _measure(alg: CliffordAlgebra, mv: torch.Tensor) -> Dict: "causal": (coh > coh_threshold) and (curv < CONSTANTS.curvature_causal_threshold), } - alg_orig = CliffordAlgebra(p, q, device=self.device) + alg_orig = make_algebra(p, q, device=self.device) mv_orig = alg_orig.embed_vector(data[..., : alg_orig.n]) results["original"] = _measure(alg_orig, mv_orig) - alg_pos = CliffordAlgebra(p + 1, q, device=self.device) + alg_pos = make_algebra(p + 1, q, device=self.device) mv_pos = self.lift(data, alg_pos, fill=1.0) results["lift_positive"] = _measure(alg_pos, mv_pos) - alg_null = CliffordAlgebra(p, q + 1, device=self.device) + alg_null = make_algebra(p, q + 1, device=self.device) mv_null = self.lift(data, alg_null, fill=0.0) results["lift_null"] = _measure(alg_null, mv_null) diff --git a/core/analysis/pipeline.py b/core/analysis/pipeline.py index 3f53b87..0761b40 100644 --- a/core/analysis/pipeline.py +++ b/core/analysis/pipeline.py @@ -13,7 +13,8 @@ import torch -from core.algebra import CliffordAlgebra +from core.config import make_algebra +from core.module import AlgebraLike from ._types import AnalysisConfig, AnalysisReport, SamplingConfig from .commutator import CommutatorAnalyzer @@ -50,7 +51,7 @@ def __init__(self, config: Optional[AnalysisConfig] = None): def analyze( self, data: torch.Tensor, - algebra: Optional[CliffordAlgebra] = None, + algebra: Optional[AlgebraLike] = None, ) -> AnalysisReport: """Run the full geometric analysis pipeline. @@ -124,7 +125,7 @@ def _run_full_pipeline(self, data: torch.Tensor, report: AnalysisReport) -> Anal if p + q + r < 2: p = max(p, 2 - q - r) - algebra = CliffordAlgebra(p, q, r, device=cfg.device) + algebra = make_algebra(p, q, r, device=cfg.device) mv_data = self._embed_raw(sampled, algebra) # 5. GA analyses (parallel) @@ -135,7 +136,7 @@ def _run_full_pipeline(self, data: torch.Tensor, report: AnalysisReport) -> Anal def _run_ga_analyses( self, mv_data: torch.Tensor, - algebra: CliffordAlgebra, + algebra: AlgebraLike, report: AnalysisReport, ) -> AnalysisReport: cfg = self.config @@ -173,7 +174,7 @@ def _run_ga_analyses( return report @staticmethod - def _embed_raw(data: torch.Tensor, algebra: CliffordAlgebra) -> torch.Tensor: + def _embed_raw(data: torch.Tensor, algebra: AlgebraLike) -> torch.Tensor: """Embed raw ``[N, D]`` data as grade-1 multivectors ``[N, 1, dim]``.""" n = algebra.n D = data.shape[1] diff --git a/core/analysis/sampler.py b/core/analysis/sampler.py index 20d1665..a9804de 100644 --- a/core/analysis/sampler.py +++ b/core/analysis/sampler.py @@ -91,7 +91,7 @@ def _stratified(data: torch.Tensor, config: SamplingConfig) -> Tuple[torch.Tenso coherence) and unstructured (low coherence) regions of the data without assuming a specific metric signature. """ - from core.algebra import CliffordAlgebra + from core.config import make_algebra from .geodesic import GeodesicFlow @@ -102,7 +102,7 @@ def _stratified(data: torch.Tensor, config: SamplingConfig) -> Tuple[torch.Tenso # Cap algebra dimension for tractability alg_dim = min(D, 6) - algebra = CliffordAlgebra(alg_dim, 0, device=data.device) + algebra = make_algebra(alg_dim, 0, device=data.device) # Embed into algebra (truncate to alg_dim if needed) raw = data[:, :alg_dim].float() diff --git a/core/analysis/signature.py b/core/analysis/signature.py index 608cc25..a936769 100644 --- a/core/analysis/signature.py +++ b/core/analysis/signature.py @@ -21,7 +21,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.config import make_algebra +from core.module import AlgebraLike from layers import BladeSelector, CliffordLinear, RotorLayer from ._types import CONSTANTS, DimensionResult, SamplingConfig, SignatureResult @@ -36,7 +37,7 @@ class _SignatureProbe(nn.Module): is the primary signal for signature discovery. """ - def __init__(self, algebra: CliffordAlgebra, channels: int = 4): + def __init__(self, algebra: AlgebraLike, channels: int = 4): super().__init__() self.algebra = algebra self.linear_in = CliffordLinear(algebra, 1, channels) @@ -57,7 +58,7 @@ def get_rotor_layers(self) -> List[RotorLayer]: def _apply_biased_init( probe: _SignatureProbe, - algebra: CliffordAlgebra, + algebra: AlgebraLike, bias_type: str = "random", ) -> None: """Biases RotorLayer bivector weights based on signature type. @@ -129,7 +130,7 @@ def __init__( self.micro_batch_size = micro_batch_size self.early_stop_patience = early_stop_patience - def _lift_data(self, data: torch.Tensor) -> Tuple[torch.Tensor, CliffordAlgebra]: + def _lift_data(self, data: torch.Tensor) -> Tuple[torch.Tensor, AlgebraLike]: """Lifts [N, X] data into Cl(X+1, 1, 0) via CGA-style embedding.""" data = data.to(self.device) N, X = data.shape @@ -144,7 +145,7 @@ def _lift_data(self, data: torch.Tensor) -> Tuple[torch.Tensor, CliffordAlgebra] ones = torch.ones(N, 1, device=self.device, dtype=data.dtype) lifted = torch.cat([data, norm_sq, ones], dim=-1) - algebra = CliffordAlgebra(X + 1, 1, 0, device=self.device) + algebra = make_algebra(X + 1, 1, 0, device=self.device) mv = algebra.embed_vector(lifted) mv = mv.unsqueeze(1) return mv, algebra @@ -152,7 +153,7 @@ def _lift_data(self, data: torch.Tensor) -> Tuple[torch.Tensor, CliffordAlgebra] def _train_probe( self, mv_data: torch.Tensor, - algebra: CliffordAlgebra, + algebra: AlgebraLike, bias_type: str = "random", ) -> Dict: """Trains a single probe and returns results.""" @@ -217,7 +218,7 @@ def _train_probe( def _analyze_bivector_energy( self, probe: _SignatureProbe, - algebra: CliffordAlgebra, + algebra: AlgebraLike, original_dim: int, ) -> Tuple[Tuple[int, int, int], Dict]: """Maps learned bivector energy to (p, q, r) signature.""" diff --git a/core/config.py b/core/config.py index 4a5c80f..15a8a79 100644 --- a/core/config.py +++ b/core/config.py @@ -15,7 +15,7 @@ import torch from core.algebra import CliffordAlgebra -from core.device import resolve_device +from core.device import optional_dtype, resolve_device, resolve_dtype from core.module import AlgebraLike from core.partitioned_algebra import PartitionedCliffordAlgebra @@ -31,6 +31,9 @@ class PartitionConfig: tree: Optional[str] = None accumulation_dtype: Optional[torch.dtype] = None + def __post_init__(self) -> None: + object.__setattr__(self, "accumulation_dtype", optional_dtype(self.accumulation_dtype)) + @classmethod def from_mapping(cls, config: Optional[Mapping[str, Any]]) -> "PartitionConfig": """Build partition options from a Hydra/OmegaConf-compatible mapping.""" @@ -40,7 +43,7 @@ def from_mapping(cls, config: Optional[Mapping[str, Any]]) -> "PartitionConfig": leaf_n=int(_mapping_get(config, "leaf_n", 4)), product_chunk_size=_optional_int(_mapping_get(config, "product_chunk_size", None)), tree=_optional_str(_mapping_get(config, "tree", None)), - accumulation_dtype=_optional_dtype(_mapping_get(config, "accumulation_dtype", None)), + accumulation_dtype=optional_dtype(_mapping_get(config, "accumulation_dtype", None)), ) @@ -73,7 +76,7 @@ def from_mapping(cls, config: Mapping[str, Any], **overrides) -> "AlgebraConfig" "kernel": _mapping_get(config, "kernel", "auto"), "partition_threshold": int(_mapping_get(config, "partition_threshold", 8)), "device": _mapping_get(config, "device", "cuda"), - "dtype": _optional_dtype(_mapping_get(config, "dtype", torch.float32)) or torch.float32, + "dtype": resolve_dtype(_mapping_get(config, "dtype", torch.float32)), "exp_policy": _mapping_get(config, "exp_policy", "balanced"), "fixed_iterations": _optional_int(_mapping_get(config, "fixed_iterations", None)), "partition": PartitionConfig.from_mapping(partition_mapping), @@ -81,7 +84,7 @@ def from_mapping(cls, config: Mapping[str, Any], **overrides) -> "AlgebraConfig" values.update({key: value for key, value in overrides.items() if value is not None}) if not isinstance(values["partition"], PartitionConfig): values["partition"] = PartitionConfig.from_mapping(values["partition"]) - values["dtype"] = _optional_dtype(values["dtype"]) or torch.float32 + values["dtype"] = resolve_dtype(values["dtype"]) return cls(**values) @@ -106,7 +109,7 @@ def make_algebra( selected_kernel = "dense" resolved_device = resolve_device(device) if str(device) == "auto" else device - resolved_dtype = _optional_dtype(dtype) or torch.float32 + resolved_dtype = resolve_dtype(dtype) if selected_kernel == "dense": return CliffordAlgebra( @@ -188,28 +191,3 @@ def _optional_str(value) -> Optional[str]: return None text = str(value).strip() return text or None - - -def _optional_dtype(value) -> Optional[torch.dtype]: - if value is None or isinstance(value, torch.dtype): - return value - if isinstance(value, str): - normalized = value.strip().lower() - if not normalized: - return None - aliases = { - "float16": torch.float16, - "fp16": torch.float16, - "half": torch.float16, - "bfloat16": torch.bfloat16, - "bf16": torch.bfloat16, - "float32": torch.float32, - "fp32": torch.float32, - "float": torch.float32, - "float64": torch.float64, - "fp64": torch.float64, - "double": torch.float64, - } - if normalized in aliases: - return aliases[normalized] - raise ValueError(f"Unsupported torch dtype declaration: {value!r}") diff --git a/core/device.py b/core/device.py index bf96f69..b53961b 100644 --- a/core/device.py +++ b/core/device.py @@ -12,11 +12,55 @@ from contextlib import nullcontext from dataclasses import dataclass -from typing import ContextManager +from typing import Any, ContextManager, Optional import torch import torch.nn as nn +FLOAT_DTYPES: dict[str, torch.dtype] = { + "float64": torch.float64, + "float32": torch.float32, + "bfloat16": torch.bfloat16, + "float16": torch.float16, +} + +_DTYPE_ALIASES: dict[str, torch.dtype] = { + **FLOAT_DTYPES, + "fp64": torch.float64, + "double": torch.float64, + "fp32": torch.float32, + "float": torch.float32, + "bf16": torch.bfloat16, + "fp16": torch.float16, + "half": torch.float16, +} + + +def optional_dtype(value: Any) -> Optional[torch.dtype]: + """Parse a torch dtype declaration, preserving ``None`` as unset.""" + if value is None or isinstance(value, torch.dtype): + return value + if isinstance(value, str): + normalized = value.strip().lower() + if not normalized: + return None + if normalized in _DTYPE_ALIASES: + return _DTYPE_ALIASES[normalized] + raise ValueError(f"Unsupported torch dtype declaration: {value!r}") + + +def resolve_dtype(value: Any, default: torch.dtype = torch.float32) -> torch.dtype: + """Parse a torch dtype declaration and fall back to ``default`` when unset.""" + return optional_dtype(value) or default + + +def dtype_name(dtype: torch.dtype) -> str: + """Return the canonical short name for a torch dtype.""" + for name, candidate in FLOAT_DTYPES.items(): + if candidate == dtype: + return name + return str(dtype).replace("torch.", "") + def resolve_device(device: str = "auto") -> str: """Resolve ``'auto'`` to the best available accelerator. @@ -47,6 +91,8 @@ class DeviceConfig: (``aot_eager`` for MPS, ``inductor`` for CUDA/CPU). MPS does not fully support the inductor backend. amp: Enable automatic mixed precision (CUDA only). + amp_dtype: Optional autocast dtype. ``None`` uses PyTorch's autocast + default; explicit values should be ``float16`` or ``bfloat16``. cudnn_benchmark: Set :attr:`torch.backends.cudnn.benchmark`. ``None`` -> auto (``True`` for CUDA). """ @@ -57,10 +103,12 @@ class DeviceConfig: compile_model: bool = False compile_backend: str | None = None amp: bool = False + amp_dtype: torch.dtype | str | None = None cudnn_benchmark: bool | None = None def __post_init__(self) -> None: self.device = resolve_device(self.device) + self.amp_dtype = optional_dtype(self.amp_dtype) is_cuda = self.device.startswith("cuda") @@ -74,6 +122,8 @@ def __post_init__(self) -> None: # AMP only makes sense on CUDA if self.amp and not is_cuda: self.amp = False + if self.amp_dtype is not None and self.amp_dtype not in {torch.float16, torch.bfloat16}: + raise ValueError("amp_dtype must be 'float16', 'bfloat16', or null") # Public helpers @@ -115,23 +165,6 @@ def maybe_compile(self, model: nn.Module) -> nn.Module: ) return model - @property - def dtype(self) -> torch.dtype: - """Working dtype for algebra tables and model parameters. - - Returns ``torch.bfloat16`` when AMP is active on CUDA (bfloat16 is - native on Ampere+ GPUs and avoids fp16 overflow). All other - backends keep ``torch.float32``. - - Pass this to :class:`~core.algebra.CliffordAlgebra` as ``dtype`` and - to :meth:`~tasks.base.BaseTask` model setup so that tables and - parameters are created in the correct precision from the start - rather than requiring a post-hoc ``.to()`` cast. - """ - if self.amp and self.device.startswith("cuda"): - return torch.bfloat16 - return torch.float32 - def get_scaler(self) -> torch.amp.GradScaler | None: """Return a :class:`GradScaler` when AMP is active, else ``None``.""" if not self.amp: @@ -142,4 +175,6 @@ def autocast_context(self) -> ContextManager: """Return an ``autocast`` context manager or :func:`nullcontext`.""" if not self.amp: return nullcontext() - return torch.amp.autocast("cuda") + if self.amp_dtype is None: + return torch.amp.autocast("cuda") + return torch.amp.autocast("cuda", dtype=self.amp_dtype) diff --git a/examples/demo.py b/examples/demo.py index 4914e1a..eebca02 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -25,7 +25,7 @@ import torch import torch.optim as optim -from core.algebra import CliffordAlgebra +from core.config import make_algebra from layers import BladeSelector, RotorLayer # Setup Page @@ -102,7 +102,7 @@ def plot_3d_manifold(data, title, color_data=None): # Main app logic # 1. Generate Data -algebra = CliffordAlgebra(3, 0, device="cpu") +algebra = make_algebra(3, 0, device="cpu") t = torch.linspace(0, 2 * np.pi, samples) x = torch.sin(t) y = torch.sin(t) * torch.cos(t) diff --git a/examples/tasks/cgenn.py b/examples/tasks/cgenn.py index 259914c..b840856 100644 --- a/examples/tasks/cgenn.py +++ b/examples/tasks/cgenn.py @@ -60,7 +60,7 @@ import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from core.module import CliffordModule from functional.activation import GeometricSquare from layers import ( @@ -205,9 +205,11 @@ class CGENNTask(BaseTask): """ def setup_algebra(self): - return CliffordAlgebra( + return make_algebra_from_config( + self.cfg.algebra, p=self.cfg.algebra.p, q=self.cfg.algebra.q, + r=self.cfg.algebra.get("r", 0), device=self.device, ) diff --git a/examples/tasks/clifford_pde.py b/examples/tasks/clifford_pde.py index 4950672..0581dce 100644 --- a/examples/tasks/clifford_pde.py +++ b/examples/tasks/clifford_pde.py @@ -66,7 +66,7 @@ import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from core.module import CliffordModule from functional.activation import GeometricGELU from layers import ( @@ -276,9 +276,11 @@ class CliffordPDETask(BaseTask): def setup_algebra(self): # Cl(2,0): dim = 2^2 = 4 components # grade-0: 1 scalar, grade-1: 2 vectors, grade-2: 1 bivector - return CliffordAlgebra( + return make_algebra_from_config( + self.cfg.algebra, p=self.cfg.algebra.p, q=self.cfg.algebra.q, + r=self.cfg.algebra.get("r", 0), device=self.device, ) diff --git a/examples/tasks/gatr.py b/examples/tasks/gatr.py index 45c3bfb..7ab4e74 100644 --- a/examples/tasks/gatr.py +++ b/examples/tasks/gatr.py @@ -63,7 +63,7 @@ import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from layers import CliffordLinear, GeometricTransformerBlock from layers.adapters.projective import ProjectiveEmbedding from tasks.base import BaseTask @@ -220,7 +220,8 @@ def setup_algebra(self): # Cl(3,0,1): 3 Euclidean + 1 degenerate = Projective Geometric Algebra # dim = 2^4 = 16 multivector components r = self.cfg.algebra.get("r", 1) - return CliffordAlgebra( + return make_algebra_from_config( + self.cfg.algebra, p=self.cfg.algebra.p, q=self.cfg.algebra.q, r=r, diff --git a/examples/tasks/hyperbolic.py b/examples/tasks/hyperbolic.py index b30e6fd..11843b4 100644 --- a/examples/tasks/hyperbolic.py +++ b/examples/tasks/hyperbolic.py @@ -9,7 +9,7 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from core.module import CliffordModule from core.visualizer import GeneralVisualizer from functional.loss import GeometricMSELoss @@ -44,7 +44,7 @@ def __init__(self, cfg): def setup_algebra(self): """2D Spacetime Cl(1, 1).""" - return CliffordAlgebra(p=1, q=1, device=self.device) + return make_algebra_from_config(self.cfg.algebra, p=1, q=1, r=0, device=self.device) def setup_model(self): """The Booster.""" diff --git a/examples/tasks/manifold.py b/examples/tasks/manifold.py index 29fa06c..b46c398 100644 --- a/examples/tasks/manifold.py +++ b/examples/tasks/manifold.py @@ -9,7 +9,7 @@ import torch.nn as nn from torch.utils.data import DataLoader -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from core.module import CliffordModule from core.visualizer import GeneralVisualizer from examples.datasets.synthetic import Figure8Dataset @@ -47,7 +47,13 @@ def __init__(self, cfg): def setup_algebra(self): """3D Euclidean.""" - return CliffordAlgebra(p=self.cfg.algebra.p, q=self.cfg.algebra.q, device=self.device) + return make_algebra_from_config( + self.cfg.algebra, + p=self.cfg.algebra.p, + q=self.cfg.algebra.q, + r=self.cfg.algebra.get("r", 0), + device=self.device, + ) def setup_model(self): """The Unbender.""" diff --git a/examples/tasks/sanity_check.py b/examples/tasks/sanity_check.py index fe7e1bd..11c084a 100644 --- a/examples/tasks/sanity_check.py +++ b/examples/tasks/sanity_check.py @@ -8,7 +8,7 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.config import make_algebra_from_config from core.module import CliffordModule from core.visualizer import GeneralVisualizer from functional.loss import GeometricMSELoss @@ -40,7 +40,7 @@ def __init__(self, cfg): def setup_algebra(self): """Standard 3D Euclidean.""" - return CliffordAlgebra(p=3, q=0, device=self.device) + return make_algebra_from_config(self.cfg.algebra, p=3, q=0, r=0, device=self.device) def setup_model(self): """Identity Net.""" diff --git a/experiments/_gdo/benchmarks.py b/experiments/_gdo/benchmarks.py index 850efa0..4dffeb7 100644 --- a/experiments/_gdo/benchmarks.py +++ b/experiments/_gdo/benchmarks.py @@ -31,8 +31,8 @@ import torch.nn as nn import torch.nn.functional as F -from core.algebra import CliffordAlgebra from core.module import CliffordModule +from experiments._lib import setup_algebra from functional.activation import GeometricGELU from layers import ( BladeSelector, @@ -57,7 +57,7 @@ class SmallGBNModel(CliffordModule): """ def __init__(self, p: int = 3, q: int = 0, channels: int = 4, device: str = "cpu"): - algebra = CliffordAlgebra(p, q, device=device) + algebra = setup_algebra(p, q, device=device) super().__init__(algebra) self.norm = CliffordLayerNorm(self.algebra, channels) self.rotor = RotorLayer(self.algebra, channels) @@ -87,7 +87,7 @@ class MultiRotorRegistrationModel(CliffordModule): """ def __init__(self, n_clusters: int = 3, points_per_cluster: int = 20, device: str = "cpu"): - algebra = CliffordAlgebra(3, 0, device=device) + algebra = setup_algebra(3, 0, device=device) super().__init__(algebra) dim = self.algebra.dim @@ -158,7 +158,7 @@ def __init__( batch_size: int = 4, device: str = "cpu", ): - algebra = CliffordAlgebra(p, q, device=device) + algebra = setup_algebra(p, q, device=device) super().__init__(algebra) self.vocab_size = vocab_size self.seq_len = seq_len diff --git a/experiments/_gdo/pre_exploration.py b/experiments/_gdo/pre_exploration.py index ee07428..facbb24 100644 --- a/experiments/_gdo/pre_exploration.py +++ b/experiments/_gdo/pre_exploration.py @@ -24,7 +24,6 @@ import torch.nn as nn import torch.nn.functional as F -from core.algebra import CliffordAlgebra from core.analysis import ( CommutatorAnalyzer as CoreCommutatorAnalyzer, ) @@ -43,6 +42,8 @@ SpectralResult, SymmetryResult, ) +from core.module import AlgebraLike +from experiments._lib import setup_algebra from .config import GDOConfig from .parameter_groups import GeometricParameterController @@ -124,7 +125,7 @@ class PreExplorationAnalyzer: def __init__( self, - algebra: Optional[CliffordAlgebra] = None, + algebra: Optional[AlgebraLike] = None, n_samples: int = 200, sample_radius: float = 0.5, device: str = "cpu", @@ -520,7 +521,7 @@ def analyze( if dim_result is not None and dim_result.intrinsic_dim >= 2: try: land_dim = min(dim_result.intrinsic_dim, 6) - temp_algebra = CliffordAlgebra(land_dim, 0, device=self.device) + temp_algebra = setup_algebra(land_dim, 0, device=self.device) reduced = eda.reduce(sampled, land_dim) mv_land = temp_algebra.embed_vector(reduced) k = min(8, mv_land.shape[0] - 1) diff --git a/experiments/_lib.py b/experiments/_lib.py index 837d90f..7651510 100644 --- a/experiments/_lib.py +++ b/experiments/_lib.py @@ -40,8 +40,9 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.config import PartitionConfig, make_algebra from core.metric import hermitian_grade_spectrum +from core.module import AlgebraLike from functional.activation import GeometricGELU from layers import CliffordLayerNorm, CliffordLinear, RotorLayer @@ -67,9 +68,41 @@ def set_seed(seed: int, deterministic: bool = False) -> None: # --------------------------------------------------------------------------- -def setup_algebra(p: int, q: int = 0, r: int = 0, device: str = "cpu") -> CliffordAlgebra: - """One-line ``CliffordAlgebra(p, q, r, device=device).to(device)`` wrapper.""" - return CliffordAlgebra(p=p, q=q, r=r, device=device).to(device) +def setup_algebra( + p: int, + q: int = 0, + r: int = 0, + device: str = "cpu", + *, + dtype: torch.dtype | str = torch.float32, + kernel: str = "auto", + partition_threshold: int = 8, + leaf_n: int = 4, + product_chunk_size: Optional[int] = None, + partition_tree: Optional[str] = None, + accumulation_dtype: torch.dtype | str | None = None, + exp_policy: str = "balanced", + fixed_iterations: Optional[int] = None, +) -> AlgebraLike: + """Construct the shared experiment algebra through the core factory.""" + partition = PartitionConfig( + leaf_n=leaf_n, + product_chunk_size=product_chunk_size, + tree=partition_tree, + accumulation_dtype=accumulation_dtype, + ) + return make_algebra( + p=p, + q=q, + r=r, + kernel=kernel, + partition_threshold=partition_threshold, + partition=partition, + device=device, + dtype=dtype, + exp_policy=exp_policy, + fixed_iterations=fixed_iterations, + ) # --------------------------------------------------------------------------- @@ -208,12 +241,12 @@ def count_parameters(model: nn.Module) -> int: # --------------------------------------------------------------------------- -def grade1_indices(algebra: CliffordAlgebra) -> List[int]: +def grade1_indices(algebra: AlgebraLike) -> List[int]: """Multivector indices of grade-1 basis elements ``[e1, e2, ..., e_n]``.""" return [1 << i for i in range(algebra.n)] -def extract_grade1(mv: torch.Tensor, algebra: CliffordAlgebra, n: Optional[int] = None) -> torch.Tensor: +def extract_grade1(mv: torch.Tensor, algebra: AlgebraLike, n: Optional[int] = None) -> torch.Tensor: """Slice grade-1 components from a multivector ``[..., dim] → [..., n]``. ``n`` defaults to ``algebra.n`` (all grade-1 slots). Inverse of @@ -228,7 +261,7 @@ def extract_grade1(mv: torch.Tensor, algebra: CliffordAlgebra, n: Optional[int] # --------------------------------------------------------------------------- -def gbn_residual_block(algebra: CliffordAlgebra, channels: int) -> nn.ModuleDict: +def gbn_residual_block(algebra: AlgebraLike, channels: int) -> nn.ModuleDict: """The four-step block every GBN experiment shares. Returns ``{'norm', 'rotor', 'act', 'linear'}`` — no skip, no outer module @@ -262,7 +295,7 @@ def apply_residual_block(block: nn.ModuleDict, h: torch.Tensor) -> torch.Tensor: @torch.no_grad() -def mean_grade_spectrum(mv_iter: Iterable[torch.Tensor], algebra: CliffordAlgebra) -> np.ndarray: +def mean_grade_spectrum(mv_iter: Iterable[torch.Tensor], algebra: AlgebraLike) -> np.ndarray: """Mean Hermitian grade spectrum across an iterable of multivectors. Each element may be any shape ending in ``algebra.dim``; it is flattened diff --git a/experiments/inc_lattice_morph.py b/experiments/inc_lattice_morph.py index 61c94b3..0270cfd 100644 --- a/experiments/inc_lattice_morph.py +++ b/experiments/inc_lattice_morph.py @@ -48,10 +48,9 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.algebra import CliffordAlgebra from core.decomposition import ExpPolicy from core.metric import induced_norm -from core.module import CliffordModule +from core.module import AlgebraLike, CliffordModule from experiments._lib import ( build_visualization_metadata, ensure_output_dir, @@ -59,6 +58,7 @@ save_experiment_figure, section_header, set_seed, + setup_algebra, signature_metadata, ) from optimizers.riemannian import RiemannianAdam @@ -118,7 +118,7 @@ class MorphMode(str, Enum): class StructureTracker: """Computes lattice geometric invariants from basis multivectors.""" - def __init__(self, algebra: CliffordAlgebra): + def __init__(self, algebra: AlgebraLike): self.algebra = algebra def compute_volume(self, basis_mvs: torch.Tensor) -> torch.Tensor: @@ -215,7 +215,7 @@ class MorphStage(CliffordModule): :class:`~core.decomposition.ExpPolicy`. """ - def __init__(self, algebra: CliffordAlgebra, n: int, compound_blades: int = 1): + def __init__(self, algebra: AlgebraLike, n: int, compound_blades: int = 1): """ Args: algebra: Clifford algebra instance. @@ -386,7 +386,7 @@ def inverse(self, basis_mvs: torch.Tensor) -> torch.Tensor: class MorphPipeline(nn.Module): """Sequential composition of MorphStages with intermediate tracking.""" - def __init__(self, algebra: CliffordAlgebra, n: int, num_stages: int = 3, compound_blades: int = 1): + def __init__(self, algebra: AlgebraLike, n: int, num_stages: int = 3, compound_blades: int = 1): super().__init__() self.compound_blades = compound_blades self.stages = nn.ModuleList( @@ -456,8 +456,7 @@ def __init__( p, q = n - 1, 1 else: p, q = n, 0 - self.algebra = CliffordAlgebra(p=p, q=q, device=device, dtype=dtype).to(device) - self.algebra.exp_policy = ExpPolicy.PRECISE + self.algebra = setup_algebra(p=p, q=q, device=device, dtype=dtype, exp_policy=ExpPolicy.PRECISE) self.signature_q = q self.tracker = StructureTracker(self.algebra) self.pipeline = MorphPipeline( @@ -711,7 +710,7 @@ def verify_reconstruction(self, tolerance: float = 1e-5) -> dict: class MorphVisualizer: """Visualization for lattice morphing.""" - def __init__(self, algebra: CliffordAlgebra, n: int, output_dir: str, metadata: str, args: argparse.Namespace): + def __init__(self, algebra: AlgebraLike, n: int, output_dir: str, metadata: str, args: argparse.Namespace): self.algebra = algebra self.n = n self.output_dir = ensure_output_dir(output_dir) diff --git a/models/deap/eeg_net.py b/models/deap/eeg_net.py index 6d2e2b9..eae4a34 100644 --- a/models/deap/eeg_net.py +++ b/models/deap/eeg_net.py @@ -18,11 +18,13 @@ (immediate) and Grade-4 (long-range) for VADL prediction. """ +from typing import Optional + import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.config import make_algebra, make_algebra_from_config +from core.module import AlgebraLike, CliffordModule from layers import ( CliffordLayerNorm, GeometricNeutralizer, @@ -39,7 +41,7 @@ class MultiTargetPhaseShiftHead(CliffordModule): each target can independently shift its prediction range. """ - def __init__(self, algebra: CliffordAlgebra, channels: int, num_targets: int = 4): + def __init__(self, algebra: AlgebraLike, channels: int, num_targets: int = 4): super().__init__(algebra) self.channels = channels self.num_targets = num_targets @@ -66,7 +68,7 @@ class EEGNet(CliffordModule): brain region's Grade-0 is cleaned of its own Grade-2 artifacts independently. """ - def __init__(self, group_sizes, profiles=None, device=None, config=None): + def __init__(self, group_sizes, profiles=None, device=None, config=None, algebra: Optional[AlgebraLike] = None): """Initialize EEGNet. Args: @@ -77,16 +79,21 @@ def __init__(self, group_sizes, profiles=None, device=None, config=None): device: Torch device. config: Hydra DictConfig or plain dict with model hyperparameters. """ - p, q = 3, 1 - if config is not None: - if hasattr(config, "algebra"): - p = config.algebra.get("p", 3) - q = config.algebra.get("q", 1) - elif isinstance(config, dict): - p = config.get("p", 3) - q = config.get("q", 1) - - algebra = CliffordAlgebra(p, q, device=device) + if algebra is None: + algebra_config = None + if config is not None: + if hasattr(config, "algebra"): + algebra_config = config.algebra + elif isinstance(config, dict): + algebra_config = config.get("algebra", config) + + p = algebra_config.get("p", 3) if algebra_config is not None else 3 + q = algebra_config.get("q", 1) if algebra_config is not None else 1 + r = algebra_config.get("r", 0) if algebra_config is not None else 0 + if algebra_config is not None: + algebra = make_algebra_from_config(algebra_config, p=p, q=q, r=r, device=device) + else: + algebra = make_algebra(p=p, q=q, r=r, device=device or "cpu") super().__init__(algebra) if config is not None and hasattr(config, "model"): diff --git a/models/sr/estimator.py b/models/sr/estimator.py index 121890f..d07dcda 100644 --- a/models/sr/estimator.py +++ b/models/sr/estimator.py @@ -16,7 +16,7 @@ import torch from sklearn.base import BaseEstimator, RegressorMixin -from core.algebra import CliffordAlgebra +from core.config import make_algebra from core.decomposition import ExpPolicy from models.sr.net import SRGBN from models.sr.utils import make_lambdify_fn @@ -93,7 +93,7 @@ def fit(self, X, y): y_t = torch.from_numpy(y_norm).unsqueeze(-1) n_vars = X.shape[1] - algebra = CliffordAlgebra(p=self.p, q=self.q, r=self.r, device="cpu", exp_policy=self.exp_policy) + algebra = make_algebra(p=self.p, q=self.q, r=self.r, device="cpu", exp_policy=self.exp_policy) self.model_ = SRGBN( algebra=algebra, diff --git a/models/sr/grouper.py b/models/sr/grouper.py index fd79ed9..467f556 100644 --- a/models/sr/grouper.py +++ b/models/sr/grouper.py @@ -20,7 +20,8 @@ import numpy as np import torch -from core.algebra import CliffordAlgebra +from core.config import make_algebra +from core.module import AlgebraLike from models.sr.utils import safe_svd, standardize, subsample logger = logging.getLogger(__name__) @@ -34,7 +35,7 @@ class VariableGroup: var_indices: Indices into original X columns. var_names: Human-readable variable names. signature: (p, q, r) from MetricSearch. - algebra: CliffordAlgebra for this group. + algebra: Shared dense or partitioned algebra for this group. svd_Vt: SVD right-singular vectors for this group (or None). mother_offset: Bit offset in mother algebra basis. internal_edges: VariableEdge list within this group. @@ -45,7 +46,7 @@ class VariableGroup: var_indices: list var_names: list signature: tuple - algebra: CliffordAlgebra + algebra: AlgebraLike svd_Vt: np.ndarray = None mother_offset: int = 0 internal_edges: list = field(default_factory=list) @@ -223,7 +224,7 @@ def _build_relationship_graph(self, X, y, var_names): n_analysis, ) - algebra = CliffordAlgebra(p, q, r, device=self.device) + algebra = make_algebra(p, q, r, device=self.device) # 5. Embed as grade-1 multivectors alg_n = algebra.n @@ -469,13 +470,13 @@ def build_mother_algebra(self, groups): Q = sum(g.signature[1] for g in groups) R = sum(g.signature[2] for g in groups) - if P + Q + R > 12: - self._reduce_groups(groups, target_n=12) + if P + Q + R > 16: + self._reduce_groups(groups, target_n=16) P = sum(g.signature[0] for g in groups) Q = sum(g.signature[1] for g in groups) R = sum(g.signature[2] for g in groups) - mother = CliffordAlgebra(P, Q, R, device=self.device) + mother = make_algebra(P, Q, R, device=self.device) p_offset = 0 q_offset = P @@ -570,7 +571,7 @@ def _single_group(self, X, y, var_names): p, q, r = safe_metric_search(data, self.device, n_vars) - algebra = CliffordAlgebra(p, q, r, device=self.device) + algebra = make_algebra(p, q, r, device=self.device) return VariableGroup( var_indices=indices, var_names=[var_names[i] for i in indices], @@ -612,7 +613,7 @@ def _build_group(self, X, y, indices, var_names): probe_epochs=20, ) - algebra = CliffordAlgebra(p, q, r, device=self.device) + algebra = make_algebra(p, q, r, device=self.device) return VariableGroup( var_indices=indices, var_names=names_sub, @@ -692,7 +693,7 @@ def _reduce_groups(self, groups, target_n=12): new_r = 0 reduction = (p + q + r) - (new_p + new_q + new_r) g.signature = (new_p, new_q, new_r) - g.algebra = CliffordAlgebra(new_p, new_q, new_r, device=self.device) + g.algebra = make_algebra(new_p, new_q, new_r, device=self.device) total -= reduction if reduction == 0: diff --git a/models/sr/implicit.py b/models/sr/implicit.py index a7272f1..05e1725 100644 --- a/models/sr/implicit.py +++ b/models/sr/implicit.py @@ -24,7 +24,7 @@ import torch import torch.nn.functional as F -from core.algebra import CliffordAlgebra +from core.config import make_algebra from models.sr.net import SRGBN from models.sr.utils import safe_sympy_solve from optimizers.riemannian import RiemannianAdam @@ -111,7 +111,7 @@ def probe_best_mode(self, algebra, X, y, geometric_report=None): # Build implicit algebra: add 1 to p for the y variable p, q, r = algebra.p, algebra.q, algebra.r - impl_algebra = CliffordAlgebra(p + 1, q, r, device=self.device) + impl_algebra = make_algebra(p + 1, q, r, device=self.device) implicit_loss = self._probe_implicit(impl_algebra, Z) diff --git a/models/sr/phases/extraction.py b/models/sr/phases/extraction.py index ab11032..c062003 100644 --- a/models/sr/phases/extraction.py +++ b/models/sr/phases/extraction.py @@ -18,8 +18,8 @@ import torch import torch.nn.functional as F -from core.algebra import CliffordAlgebra from core.analysis import GeodesicFlow, MetricSearch +from core.config import make_algebra from models.sr.net import SRGBN from models.sr.translator import RotorTerm, RotorTranslator from models.sr.utils import ( @@ -83,7 +83,7 @@ def _process_group_implicit(self, group, group_idx, prep, X_orig, y_orig, X_norm # Build augmented algebra (k+1 variables) p, q, r = group.signature - impl_algebra = CliffordAlgebra(p + 1, q, r, device=self.device) + impl_algebra = make_algebra(p + 1, q, r, device=self.device) # Augmented data Z = [X_group_norm, y_norm] X_group_norm = standardize(torch.tensor(X_orig[:, var_indices], dtype=torch.float32, device=self.device)) diff --git a/models/sr/phases/prep.py b/models/sr/phases/prep.py index b5f5649..d6162ed 100644 --- a/models/sr/phases/prep.py +++ b/models/sr/phases/prep.py @@ -17,7 +17,7 @@ import sympy import torch -from core.algebra import CliffordAlgebra +from core.config import make_algebra from models.sr.translator import RotorTerm from models.sr.utils import ( make_lambdify_fn, @@ -130,7 +130,7 @@ def _single_group_fallback(self, X_orig, y_orig, var_names, Vt): max_p=max(n_vars, 2), ) - algebra = CliffordAlgebra(p, q, r, device=self.device) + algebra = make_algebra(p, q, r, device=self.device) return VariableGroup( var_indices=list(range(n_vars)), var_names=var_names or [f"x{i + 1}" for i in range(n_vars)], diff --git a/tasks/base.py b/tasks/base.py index 8435e41..29addc9 100644 --- a/tasks/base.py +++ b/tasks/base.py @@ -50,6 +50,7 @@ def __init__(self, cfg: DictConfig): compile_model=cfg.training.get("compile", False), compile_backend=cfg.training.get("compile_backend", None), amp=cfg.training.get("amp", False), + amp_dtype=cfg.training.get("amp_dtype", None), cudnn_benchmark=cfg.training.get("cudnn_benchmark", None), ) self.device = self.device_config.device diff --git a/tasks/deap_eeg.py b/tasks/deap_eeg.py index 7ee8c2d..76ff22c 100644 --- a/tasks/deap_eeg.py +++ b/tasks/deap_eeg.py @@ -65,6 +65,7 @@ def setup_model(self): profiles=profiles, device=self.device, config=self.cfg, + algebra=self.algebra, ) def _compute_profiles(self, group_sizes): From d919b4fcc8686181f9e4ed46907875d36710be8d Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 6 May 2026 18:19:58 +0900 Subject: [PATCH 07/45] chore: clean hydra algebra defaults --- conf/config.yaml | 6 ++++++ conf/task/deap_eeg.yaml | 10 ---------- conf/task/lqa.yaml | 9 --------- conf/task/md17.yaml | 11 ----------- conf/task/sr.yaml | 13 +------------ examples/conf/task/cgenn.yaml | 1 - examples/conf/task/clifford_pde.yaml | 1 - examples/conf/task/gatr.yaml | 1 - examples/conf/task/hyperbolic.yaml | 3 ++- examples/conf/task/manifold.yaml | 5 +++-- examples/conf/task/sanity.yaml | 3 ++- tasks/md17.py | 2 -- tasks/symbolic_regression.py | 16 ++++++---------- tests/test_symbolic_regression.py | 22 +++++++++++++++++++--- 14 files changed, 39 insertions(+), 64 deletions(-) diff --git a/conf/config.yaml b/conf/config.yaml index 075ceb3..94429d7 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -9,6 +9,8 @@ algebra: kernel: "auto" # auto: dense for n<=partition_threshold, partitioned above it partition_threshold: 8 device: "auto" # Auto-detect: cuda > mps > cpu + dtype: "float32" + exp_policy: "balanced" partition: leaf_n: 4 product_chunk_size: null @@ -24,11 +26,15 @@ training: momentum: 0.9 betas: [0.9, 0.999] max_bivector_norm: 10.0 + scheduler: + factor: 0.5 + patience: 10 num_workers: 2 # DataLoader workers (0 to disable multiprocessing) pin_memory: null # null = auto (true for CUDA) compile: false # torch.compile() wrapper compile_backend: null # null=auto (aot_eager on MPS, inductor on CUDA/CPU) amp: false # Automatic mixed precision (CUDA only) + amp_dtype: null # null=PyTorch autocast default; or "float16"/"bfloat16" cudnn_benchmark: null # null = auto (true for CUDA) checkpoint: null diff --git a/conf/task/deap_eeg.yaml b/conf/task/deap_eeg.yaml index 975b09f..80e6079 100644 --- a/conf/task/deap_eeg.yaml +++ b/conf/task/deap_eeg.yaml @@ -5,14 +5,6 @@ algebra: p: 3 q: 1 r: 0 - kernel: "auto" - partition_threshold: 8 - device: "auto" - partition: - leaf_n: 4 - product_chunk_size: null - tree: null - accumulation_dtype: null dataset: name: "deap" @@ -36,5 +28,3 @@ training: epochs: 100 lr: 0.0005 batch_size: 32 - optimizer_type: "riemannian_adam" - max_bivector_norm: 10.0 diff --git a/conf/task/lqa.yaml b/conf/task/lqa.yaml index 84cc608..a4c83ff 100644 --- a/conf/task/lqa.yaml +++ b/conf/task/lqa.yaml @@ -8,14 +8,6 @@ algebra: p: 4 q: 1 r: 0 - kernel: auto - partition_threshold: 8 - device: auto - partition: - leaf_n: 4 - product_chunk_size: null - tree: null - accumulation_dtype: null model: channels: 16 @@ -40,7 +32,6 @@ training: epochs: 50 lr: 0.001 batch_size: 64 - optimizer_type: riemannian_adam # Auxiliary loss weights isometry_weight: 0.01 asymmetry_weight: 0.1 diff --git a/conf/task/md17.yaml b/conf/task/md17.yaml index b9300cd..f65dc6e 100644 --- a/conf/task/md17.yaml +++ b/conf/task/md17.yaml @@ -4,14 +4,6 @@ algebra: p: 3 q: 0 r: 1 - kernel: "auto" - partition_threshold: 8 - device: "auto" - partition: - leaf_n: 4 - product_chunk_size: null - tree: null - accumulation_dtype: null dataset: name: "rmd17" molecule: "ethanol" @@ -27,7 +19,6 @@ model: max_z: 100 num_rbf: 20 rbf_cutoff: 5.0 - exp_policy: "balanced" use_rotor_backend: true use_geo_square: true use_checkpoint: false @@ -35,7 +26,6 @@ training: epochs: 500 lr: 0.0015 batch_size: 96 - optimizer_type: "riemannian_adam" loss_weights: energy: 0.1 force: 100.0 @@ -43,4 +33,3 @@ training: conservative: 0.0 grade_reg: 0.01 target_spectrum: [0.35, 0.30, 0.20, 0.10, 0.05] - max_bivector_norm: 10.0 diff --git a/conf/task/sr.yaml b/conf/task/sr.yaml index 46b8f37..d3cc5a1 100644 --- a/conf/task/sr.yaml +++ b/conf/task/sr.yaml @@ -5,15 +5,8 @@ algebra: p: 4 q: 0 r: 0 - kernel: "auto" - partition_threshold: 8 device: "cpu" - auto: true - partition: - leaf_n: 4 - product_chunk_size: null - tree: null - accumulation_dtype: null + metric_search: true dataset: name: "sr" @@ -28,16 +21,12 @@ dataset: model: auto_capacity: true - exp_policy: "balanced" training: epochs: 200 lr: 0.003 batch_size: 128 - optimizer_type: "riemannian_adam" - max_bivector_norm: 10.0 sparsity_weight: 0.01 - scheduler_patience: 10 iterative: max_stages: 5 diff --git a/examples/conf/task/cgenn.yaml b/examples/conf/task/cgenn.yaml index 70152b0..71ddea5 100644 --- a/examples/conf/task/cgenn.yaml +++ b/examples/conf/task/cgenn.yaml @@ -4,7 +4,6 @@ name: "cgenn" algebra: p: 3 q: 0 - device: "cpu" dataset: name: "point_cloud_invariant" diff --git a/examples/conf/task/clifford_pde.yaml b/examples/conf/task/clifford_pde.yaml index 6568289..1ca7d10 100644 --- a/examples/conf/task/clifford_pde.yaml +++ b/examples/conf/task/clifford_pde.yaml @@ -4,7 +4,6 @@ name: "clifford_pde" algebra: p: 2 q: 0 - device: "cpu" dataset: name: "taylor_green" diff --git a/examples/conf/task/gatr.yaml b/examples/conf/task/gatr.yaml index deb8273..dfbdb37 100644 --- a/examples/conf/task/gatr.yaml +++ b/examples/conf/task/gatr.yaml @@ -5,7 +5,6 @@ algebra: p: 3 q: 0 r: 1 - device: "cpu" dataset: name: "nbody_spring" diff --git a/examples/conf/task/hyperbolic.yaml b/examples/conf/task/hyperbolic.yaml index 8a32a1f..1d5cd45 100644 --- a/examples/conf/task/hyperbolic.yaml +++ b/examples/conf/task/hyperbolic.yaml @@ -1,9 +1,10 @@ # @package _global_ name: "hyperbolic" + algebra: p: 1 q: 1 - device: "cpu" + dataset: name: "lorentz_boost" samples: 10000 diff --git a/examples/conf/task/manifold.yaml b/examples/conf/task/manifold.yaml index 458c145..ab61ae1 100644 --- a/examples/conf/task/manifold.yaml +++ b/examples/conf/task/manifold.yaml @@ -1,10 +1,11 @@ # @package _global_ name: "manifold" + algebra: p: 3 q: 0 - device: "cpu" + dataset: name: "figure8" samples: 1000 - noise_std: 0.1 \ No newline at end of file + noise_std: 0.1 diff --git a/examples/conf/task/sanity.yaml b/examples/conf/task/sanity.yaml index 1ad2b3e..137455c 100644 --- a/examples/conf/task/sanity.yaml +++ b/examples/conf/task/sanity.yaml @@ -1,9 +1,10 @@ # @package _global_ name: "sanity" + algebra: p: 3 q: 0 - device: "cpu" + dataset: name: "random_noise" samples: 1000 diff --git a/tasks/md17.py b/tasks/md17.py index 93bebf7..99e7f81 100644 --- a/tasks/md17.py +++ b/tasks/md17.py @@ -49,14 +49,12 @@ def __init__(self, cfg): def setup_algebra(self): """Use Cl(3,0,1) PGA for SE(3) rigid-body motions.""" - exp_policy = self.cfg.model.get("exp_policy", "balanced") return make_algebra_from_config( self.cfg.algebra, p=3, q=0, r=self.cfg.algebra.get("r", 1), device=self.device, - exp_policy=exp_policy, ) def setup_model(self): diff --git a/tasks/symbolic_regression.py b/tasks/symbolic_regression.py index aef7206..a669e64 100644 --- a/tasks/symbolic_regression.py +++ b/tasks/symbolic_regression.py @@ -48,7 +48,7 @@ class SRTask(BaseTask): model.hidden_channels : channel count C model.num_layers : residual block count model.num_rotors : K rotors per MultiRotorLayer - model.exp_policy : exp policy ('balanced', 'precise') + algebra.exp_policy : exp policy ('balanced', 'precise') iterative.max_stages : maximum unbending iterations iterative.stage_epochs : epochs per stage iterative.r2_target : R2 threshold to stop @@ -72,13 +72,11 @@ def __init__(self, cfg: DictConfig): # Optional automatic signature discovery self._searched_signature = None - if cfg.algebra.get("auto", False): + if cfg.algebra.get("metric_search", False): self._searched_signature = self._run_metric_search(cfg) # Iterative unbending config self.iterative_cfg = dict(cfg.get("iterative", {})) - # Remove 'enabled' key if present (legacy compat) - self.iterative_cfg.pop("enabled", None) # Merge new pipeline config sections self.iterative_cfg.update( { @@ -98,13 +96,13 @@ def __init__(self, cfg: DictConfig): super().__init__(cfg) - # Override scheduler patience (BaseTask defaults to 3) - sched_patience = cfg.training.get("scheduler_patience", 10) + # Override scheduler after SR-specific setup. + sched_cfg = cfg.training.get("scheduler", {}) self.scheduler = optim.lr_scheduler.ReduceLROnPlateau( self.optimizer, mode="min", - factor=0.5, - patience=sched_patience, + factor=sched_cfg.get("factor", 0.5), + patience=sched_cfg.get("patience", 10), ) def _probe_n_vars(self, cfg): @@ -164,14 +162,12 @@ def setup_algebra(self) -> AlgebraLike: p = self.cfg.algebra.p q = self.cfg.algebra.get("q", 0) r = self.cfg.algebra.get("r", 0) - exp_policy = self.cfg.model.get("exp_policy", "balanced") return make_algebra_from_config( self.cfg.algebra, p=p, q=q, r=r, device=self.device, - exp_policy=exp_policy, ) def setup_model(self) -> SRGBN: diff --git a/tests/test_symbolic_regression.py b/tests/test_symbolic_regression.py index 37bddfa..4158280 100644 --- a/tests/test_symbolic_regression.py +++ b/tests/test_symbolic_regression.py @@ -41,11 +41,27 @@ def small_algebra(): _TEST_CACHE = "./data/pmlb_cache" -def _make_cfg(dataset_name=_TEST_DATASET, hidden_channels=4, num_layers=1, n_samples=200, auto=False): +def _make_cfg(dataset_name=_TEST_DATASET, hidden_channels=4, num_layers=1, n_samples=200, metric_search=False): return OmegaConf.create( { "name": "sr", - "algebra": {"p": 4, "q": 0, "r": 0, "device": "cpu", "auto": auto}, + "algebra": { + "p": 4, + "q": 0, + "r": 0, + "device": "cpu", + "dtype": "float32", + "exp_policy": "balanced", + "kernel": "auto", + "metric_search": metric_search, + "partition_threshold": 8, + "partition": { + "leaf_n": 4, + "product_chunk_size": None, + "tree": None, + "accumulation_dtype": None, + }, + }, "dataset": { "dataset_name": dataset_name, "category": "blackbox", @@ -56,7 +72,6 @@ def _make_cfg(dataset_name=_TEST_DATASET, hidden_channels=4, num_layers=1, n_sam "model": { "hidden_channels": hidden_channels, "num_layers": num_layers, - "exp_policy": "balanced", }, "training": { "epochs": 1, @@ -66,6 +81,7 @@ def _make_cfg(dataset_name=_TEST_DATASET, hidden_channels=4, num_layers=1, n_sam "max_bivector_norm": 10.0, "sparsity_weight": 0.01, "seed": 0, + "scheduler": {"factor": 0.5, "patience": 10}, }, "checkpoint": None, } From c7333ca1579bb66f53a1a807d93482aac6c1163c Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 6 May 2026 19:07:47 +0900 Subject: [PATCH 08/45] test: add partitioned dense-reference coverage --- tests/conftest.py | 55 ++++++ tests/test_partitioned_dense_reference.py | 218 ++++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 tests/test_partitioned_dense_reference.py diff --git a/tests/conftest.py b/tests/conftest.py index 581da86..b7fa42a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ import pytest +import torch from core.algebra import CliffordAlgebra +from core.config import PartitionConfig, make_algebra DEVICE = "cpu" @@ -36,6 +38,59 @@ def algebra_conformal(): return CliffordAlgebra(p=4, q=1, device=DEVICE) +# -- High-dimensional partitioned algebras ------------------------------ +@pytest.fixture +def partitioned_algebra_8d(): + return make_algebra( + p=8, + q=0, + r=0, + kernel="partitioned", + device=DEVICE, + dtype=torch.float64, + partition=PartitionConfig(leaf_n=6, product_chunk_size=32), + ) + + +@pytest.fixture +def partitioned_algebra_12d(): + return make_algebra( + p=12, + q=0, + r=0, + kernel="partitioned", + device=DEVICE, + dtype=torch.float64, + partition=PartitionConfig(leaf_n=6, product_chunk_size=64), + ) + + +@pytest.fixture +def partitioned_algebra_12d_mixed(): + return make_algebra( + p=8, + q=3, + r=1, + kernel="partitioned", + device=DEVICE, + dtype=torch.float64, + partition=PartitionConfig(leaf_n=6, product_chunk_size=32), + ) + + +@pytest.fixture +def partitioned_algebra_16d(): + return make_algebra( + p=10, + q=4, + r=2, + kernel="partitioned", + device=DEVICE, + dtype=torch.float32, + partition=PartitionConfig(leaf_n=6, product_chunk_size=8), + ) + + # -- Module-scoped (used by test_geodesic.py - exact name match) ---------- @pytest.fixture(scope="module") def alg2(): diff --git a/tests/test_partitioned_dense_reference.py b/tests/test_partitioned_dense_reference.py new file mode 100644 index 0000000..40f8222 --- /dev/null +++ b/tests/test_partitioned_dense_reference.py @@ -0,0 +1,218 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Dense-reference checks for partitioned Clifford algebra. + +This file covers the region where both kernels are valid, so regressions show +up as direct numerical error instead of only algebraic identity failures. The +slow Cl12 case is intentionally separated from the regular unit sweep because +the dense reference allocates a monolithic Cayley table. +""" + +import pytest +import torch + +from core.config import PartitionConfig, make_algebra + +pytestmark = pytest.mark.unit + +DEVICE = "cpu" + + +def _make_dense_reference_pair(p: int, q: int, r: int, leaf_n: int, product_chunk_size: int = 32): + reference = make_algebra(p, q, r, kernel="dense", device=DEVICE, dtype=torch.float64) + partitioned = make_algebra( + p, + q, + r, + kernel="partitioned", + device=DEVICE, + dtype=torch.float64, + partition=PartitionConfig(leaf_n=leaf_n, product_chunk_size=product_chunk_size), + ) + return reference, partitioned + + +def _dense_inputs(dim: int, seed: int, batch_shape=(1,)): + generator = torch.Generator(device=DEVICE).manual_seed(seed) + scale = 0.125 + A = torch.randn(*batch_shape, dim, dtype=torch.float64, generator=generator) * scale + B = torch.randn(*batch_shape, dim, dtype=torch.float64, generator=generator) * scale + return A, B + + +def _assert_bounded_error(actual: torch.Tensor, expected: torch.Tensor, label: str, *, atol=2e-9, rtol=2e-9): + diff = actual - expected + max_abs = diff.abs().max().item() + denominator = expected.norm().clamp_min(torch.finfo(expected.dtype).eps) + relative = (diff.norm() / denominator).item() + assert torch.allclose(actual, expected, atol=atol, rtol=rtol), ( + f"{label} exceeded dense-reference error bounds: max_abs={max_abs:.3e}, relative={relative:.3e}" + ) + if rtol == 0.0: + assert max_abs == 0.0, f"{label} expected exact agreement, got max_abs={max_abs:.3e}" + else: + assert relative < rtol * 10.0, f"{label} relative error is excessive: {relative:.3e}" + + +@pytest.mark.parametrize( + ("p", "q", "r", "leaf_n"), + [ + pytest.param(4, 0, 0, 2, id="cl4_forced_recursive"), + pytest.param(5, 2, 1, 4, id="cl8_mixed"), + pytest.param(8, 0, 0, 4, id="cl8_euclidean"), + pytest.param(7, 2, 1, 5, id="cl10_mixed"), + ], +) +def test_dense_comparable_binary_operations_have_bounded_error(p, q, r, leaf_n): + reference, partitioned = _make_dense_reference_pair(p, q, r, leaf_n) + A, B = _dense_inputs(partitioned.dim, seed=503 + p * 17 + q * 11 + r) + + for method_name in [ + "geometric_product", + "wedge", + "inner_product", + "commutator", + "anti_commutator", + "left_contraction", + ]: + expected = getattr(reference, method_name)(A, B) + actual = getattr(partitioned, method_name)(A, B) + _assert_bounded_error(actual, expected, f"Cl({p},{q},{r}).{method_name}") + + +@pytest.mark.parametrize( + ("p", "q", "r", "leaf_n"), + [ + pytest.param(8, 0, 0, 4, id="cl8_euclidean"), + pytest.param(7, 2, 1, 5, id="cl10_mixed"), + ], +) +def test_dense_comparable_unary_operations_have_bounded_error(p, q, r, leaf_n): + reference, partitioned = _make_dense_reference_pair(p, q, r, leaf_n) + mv, _ = _dense_inputs(partitioned.dim, seed=719 + p * 17 + q * 11 + r, batch_shape=(2,)) + + for grade in range(partitioned.num_grades): + expected = reference.grade_projection(mv, grade) + actual = partitioned.grade_projection(mv, grade) + _assert_bounded_error(actual, expected, f"Cl({p},{q},{r}).grade_projection({grade})", atol=0.0, rtol=0.0) + + for method_name in [ + "reverse", + "pseudoscalar_product", + "dual", + "grade_involution", + "clifford_conjugation", + "norm_sq", + ]: + expected = getattr(reference, method_name)(mv) + actual = getattr(partitioned, method_name)(mv) + _assert_bounded_error(actual, expected, f"Cl({p},{q},{r}).{method_name}") + + vectors = torch.randn(2, partitioned.n, dtype=torch.float64, generator=torch.Generator().manual_seed(907)) + _assert_bounded_error(partitioned.embed_vector(vectors), reference.embed_vector(vectors), "embed_vector") + + +def test_partitioned_8d_fixture_matches_dense_reference(partitioned_algebra_8d): + reference = make_algebra(8, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float64) + A, B = _dense_inputs(partitioned_algebra_8d.dim, seed=1013, batch_shape=(2,)) + + expected = reference.geometric_product(A, B) + actual = partitioned_algebra_8d.geometric_product(A, B) + + _assert_bounded_error(actual, expected, "partitioned_algebra_8d.geometric_product") + + +@pytest.mark.slow +def test_partitioned_12d_fixture_dense_reference_error_is_bounded(partitioned_algebra_12d): + reference = make_algebra(12, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float64) + A, B = _dense_inputs(partitioned_algebra_12d.dim, seed=1207) + + expected = reference.geometric_product(A, B) + actual = partitioned_algebra_12d.geometric_product(A, B) + + _assert_bounded_error(actual, expected, "partitioned_algebra_12d.geometric_product", atol=5e-9, rtol=5e-9) + + +@pytest.mark.slow +def test_partitioned_12d_mixed_fixture_matches_bitmask_reference(partitioned_algebra_12d_mixed): + entries_a = [(0, 0.25), (3, -1.5), (257, 0.75), (2049, 2.0)] + entries_b = [(1, -0.5), (384, 1.25), (1025, -2.0), (4095, 0.5)] + A = _make_sparse_multivector(partitioned_algebra_12d_mixed, entries_a, torch.float64) + B = _make_sparse_multivector(partitioned_algebra_12d_mixed, entries_b, torch.float64) + + expected = torch.zeros_like(A) + expected_sparse = _sparse_product_reference(entries_a, entries_b, 8, 3, 1) + for index, value in expected_sparse.items(): + expected[0, index] = value + + actual = partitioned_algebra_12d_mixed.geometric_product(A, B) + + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + +@pytest.mark.slow +def test_partitioned_16d_fixture_basis_product_matches_bitmask_reference(partitioned_algebra_16d): + p, q, r = 10, 4, 2 + index_a = 0b1001_0010_0110_1011 + index_b = 0b0110_1101_1000_1110 + result_index, sign = _basis_product_reference(index_a, index_b, p, q, r) + + A = torch.zeros(1, partitioned_algebra_16d.dim, dtype=torch.float32) + B = torch.zeros(1, partitioned_algebra_16d.dim, dtype=torch.float32) + A[0, index_a] = 1.25 + B[0, index_b] = -0.5 + + expected = torch.zeros_like(A) + expected[0, result_index] = 1.25 * -0.5 * sign + actual = partitioned_algebra_16d.geometric_product(A, B) + + assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) + + +def _make_sparse_multivector(algebra, entries, dtype: torch.dtype) -> torch.Tensor: + mv = torch.zeros(1, algebra.dim, dtype=dtype) + for index, value in entries: + mv[0, index] += value + return mv + + +def _sparse_product_reference( + entries_a: list[tuple[int, float]], + entries_b: list[tuple[int, float]], + p: int, + q: int, + r: int, +) -> dict[int, float]: + result = {} + for index_a, value_a in entries_a: + for index_b, value_b in entries_b: + result_index, sign = _basis_product_reference(index_a, index_b, p, q, r) + if sign == 0.0: + continue + result[result_index] = result.get(result_index, 0.0) + value_a * value_b * sign + return result + + +def _basis_product_reference(index_a: int, index_b: int, p: int, q: int, r: int) -> tuple[int, float]: + n = p + q + r + swap_count = 0 + for bit in range(n): + if index_a & (1 << bit): + swap_count += (index_b & ((1 << bit) - 1)).bit_count() + + sign = -1.0 if swap_count % 2 else 1.0 + + negative_mask = sum(1 << bit for bit in range(p, p + q)) + if ((index_a & index_b & negative_mask).bit_count() % 2) == 1: + sign = -sign + + null_mask = sum(1 << bit for bit in range(p + q, n)) + if (index_a & index_b & null_mask) != 0: + sign = 0.0 + + return index_a ^ index_b, sign From b9b9b1a1b158d564b193b19059adea6f78ef8230 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 6 May 2026 19:08:15 +0900 Subject: [PATCH 09/45] feat: constrain partitioned algebra defaults --- __init__.py | 6 ++-- benchmarks/benchmark_core.py | 11 +++--- conf/config.yaml | 2 +- core/__init__.py | 6 ++-- core/config.py | 15 +++++--- core/partitioned_algebra.py | 10 ++++-- experiments/_lib.py | 4 +-- tests/test_partitioned_algebra.py | 36 ++++++++++++++----- tests/test_partitioned_highdim.py | 58 +++++++++++++++---------------- tests/test_symbolic_regression.py | 2 +- 10 files changed, 92 insertions(+), 58 deletions(-) diff --git a/__init__.py b/__init__.py index 9fbb5fb..292c470 100644 --- a/__init__.py +++ b/__init__.py @@ -3,9 +3,9 @@ __version__ = "1.0.0" from core.algebra import CliffordAlgebra -from core.config import AlgebraConfig, PartitionConfig, make_algebra, make_algebra_from_config +from core.config import DEFAULT_PARTITION_LEAF_N, AlgebraConfig, PartitionConfig, make_algebra, make_algebra_from_config from core.module import CliffordModule -from core.partitioned_algebra import PartitionedCliffordAlgebra +from core.partitioned_algebra import MAX_PARTITIONED_DIMENSIONS, PartitionedCliffordAlgebra from layers import CliffordLinear, RotorLayer __all__ = [ @@ -13,6 +13,8 @@ "AlgebraConfig", "CliffordAlgebra", "CliffordModule", + "DEFAULT_PARTITION_LEAF_N", + "MAX_PARTITIONED_DIMENSIONS", "PartitionConfig", "PartitionedCliffordAlgebra", "make_algebra", diff --git a/benchmarks/benchmark_core.py b/benchmarks/benchmark_core.py index 1b9c33f..29be49f 100644 --- a/benchmarks/benchmark_core.py +++ b/benchmarks/benchmark_core.py @@ -55,9 +55,10 @@ if str(_REPO_ROOT) not in sys.path: sys.path.insert(0, str(_REPO_ROOT)) -from core.config import PartitionConfig, make_algebra +from core.config import DEFAULT_PARTITION_LEAF_N, PartitionConfig, make_algebra from core.decomposition import ExpPolicy, compiled_safe_decomposed_exp # noqa: E402 -from core.device import FLOAT_DTYPES, dtype_name as _format_dtype_name, optional_dtype, resolve_device +from core.device import FLOAT_DTYPES, optional_dtype, resolve_device +from core.device import dtype_name as _format_dtype_name from core.module import AlgebraLike DTYPES: dict[str, torch.dtype] = FLOAT_DTYPES @@ -182,7 +183,7 @@ def setup_algebra( ) -> AlgebraLike: """Construct benchmark algebras through the shared core factory.""" partition = PartitionConfig( - leaf_n=getattr(args, "partition_leaf_n", 4), + leaf_n=getattr(args, "partition_leaf_n", DEFAULT_PARTITION_LEAF_N), product_chunk_size=getattr(args, "partition_product_chunk_size", None), tree=getattr(args, "partition_tree", None), accumulation_dtype=optional_dtype(getattr(args, "partition_accumulation_dtype", None)), @@ -395,7 +396,7 @@ def _supported_dtypes( ) -> list[torch.dtype]: if requested == "auto": candidates = ["float64", "float32"] - if device.startswith("cuda") or device == "mps": + if args.device.startswith("cuda") or args.device == "mps": candidates += ["bfloat16", "float16"] else: candidates = _parse_csv(requested) @@ -3492,7 +3493,7 @@ def make_argparser() -> argparse.ArgumentParser: parser.add_argument("--device", default="auto", help="cpu, cuda, mps, or auto") parser.add_argument("--algebra-kernel", default="auto", choices=("auto", "dense", "partitioned")) parser.add_argument("--partition-threshold", type=int, default=8) - parser.add_argument("--partition-leaf-n", type=int, default=4) + parser.add_argument("--partition-leaf-n", type=int, default=DEFAULT_PARTITION_LEAF_N) parser.add_argument("--partition-product-chunk-size", type=int, default=None) parser.add_argument("--partition-tree", default=None) parser.add_argument("--partition-accumulation-dtype", default=None) diff --git a/conf/config.yaml b/conf/config.yaml index 94429d7..f79e4fb 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -12,7 +12,7 @@ algebra: dtype: "float32" exp_policy: "balanced" partition: - leaf_n: 4 + leaf_n: null # null = partitioned kernel default product_chunk_size: null tree: null # Example: "R=0-3; L.R=4-7; L.L=8-11" accumulation_dtype: null diff --git a/core/__init__.py b/core/__init__.py index bb1d072..d191566 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -12,7 +12,7 @@ """ from .algebra import CliffordAlgebra -from .config import AlgebraConfig, PartitionConfig, make_algebra, make_algebra_from_config +from .config import DEFAULT_PARTITION_LEAF_N, AlgebraConfig, PartitionConfig, make_algebra, make_algebra_from_config from .decomposition import ( ExpPolicy, compiled_safe_decomposed_exp, @@ -39,7 +39,7 @@ ) from .module import AlgebraLike, CliffordModule from .multivector import Multivector -from .partitioned_algebra import PartitionedCliffordAlgebra +from .partitioned_algebra import MAX_PARTITIONED_DIMENSIONS, PartitionedCliffordAlgebra from .validation import check_channels, check_multivector __all__ = [ @@ -49,6 +49,8 @@ "AlgebraLike", "CliffordModule", "Multivector", + "DEFAULT_PARTITION_LEAF_N", + "MAX_PARTITIONED_DIMENSIONS", "PartitionConfig", "PartitionedCliffordAlgebra", "make_algebra", diff --git a/core/config.py b/core/config.py index 15a8a79..c1f2a97 100644 --- a/core/config.py +++ b/core/config.py @@ -17,7 +17,7 @@ from core.algebra import CliffordAlgebra from core.device import optional_dtype, resolve_device, resolve_dtype from core.module import AlgebraLike -from core.partitioned_algebra import PartitionedCliffordAlgebra +from core.partitioned_algebra import DEFAULT_PARTITION_LEAF_N, PartitionedCliffordAlgebra AlgebraKernel = Literal["auto", "dense", "partitioned"] @@ -26,7 +26,7 @@ class PartitionConfig: """Options specific to :class:`PartitionedCliffordAlgebra`.""" - leaf_n: int = 4 + leaf_n: int = DEFAULT_PARTITION_LEAF_N product_chunk_size: Optional[int] = None tree: Optional[str] = None accumulation_dtype: Optional[torch.dtype] = None @@ -39,8 +39,9 @@ def from_mapping(cls, config: Optional[Mapping[str, Any]]) -> "PartitionConfig": """Build partition options from a Hydra/OmegaConf-compatible mapping.""" if config is None: return cls() + leaf_n = _mapping_get(config, "leaf_n", DEFAULT_PARTITION_LEAF_N) return cls( - leaf_n=int(_mapping_get(config, "leaf_n", 4)), + leaf_n=_int_or_default(leaf_n, DEFAULT_PARTITION_LEAF_N), product_chunk_size=_optional_int(_mapping_get(config, "product_chunk_size", None)), tree=_optional_str(_mapping_get(config, "tree", None)), accumulation_dtype=optional_dtype(_mapping_get(config, "accumulation_dtype", None)), @@ -165,7 +166,7 @@ def _mapping_get(config: Mapping[str, Any], key: str, default): def _flat_partition_mapping(config: Mapping[str, Any]) -> dict[str, Any]: """Return partition options from flat ``algebra.*`` aliases.""" return { - "leaf_n": _mapping_get(config, "leaf_n", 4), + "leaf_n": _mapping_get(config, "leaf_n", DEFAULT_PARTITION_LEAF_N), "product_chunk_size": _mapping_get(config, "product_chunk_size", None), "tree": _mapping_get(config, "partition_tree", _mapping_get(config, "tree", None)), "accumulation_dtype": _mapping_get(config, "accumulation_dtype", None), @@ -186,6 +187,12 @@ def _optional_int(value) -> Optional[int]: return int(value) +def _int_or_default(value, default: int) -> int: + if value is None: + return default + return int(value) + + def _optional_str(value) -> Optional[str]: if value is None: return None diff --git a/core/partitioned_algebra.py b/core/partitioned_algebra.py index 2c8a3ba..7c5768a 100644 --- a/core/partitioned_algebra.py +++ b/core/partitioned_algebra.py @@ -32,7 +32,8 @@ from core.algebra import CliffordAlgebra from core.validation import check_multivector -_LOCAL_DENSE_LEAF_N = 4 +DEFAULT_PARTITION_LEAF_N = 6 +MAX_PARTITIONED_DIMENSIONS = 16 _DEFAULT_PRODUCT_CHUNK_SIZE = 64 @@ -975,7 +976,7 @@ class PartitionedCliffordAlgebra(nn.Module): dtype (torch.dtype, optional): Floating-point dtype for sign buffers and dense leaf algebras. leaf_n (int, optional): Maximum basis-vector count handled by local - leaves. The default targets ``2**leaf_n == 16`` coefficients so + leaves. The default targets ``2**leaf_n == 64`` coefficients so deep-learning products use small dense kernels and indexed global merge routing. product_chunk_size (int, optional): Number of right-basis product pairs @@ -998,7 +999,7 @@ def __init__( r: int = 0, device="cuda", dtype: torch.dtype = torch.float32, - leaf_n: int = _LOCAL_DENSE_LEAF_N, + leaf_n: int = DEFAULT_PARTITION_LEAF_N, product_chunk_size: Optional[int] = None, exp_policy: str = "balanced", fixed_iterations: Optional[int] = None, @@ -1011,6 +1012,9 @@ def __init__( assert p >= 0, f"p must be non-negative, got {p}" assert q >= 0, f"q must be non-negative, got {q}" assert r >= 0, f"r must be non-negative, got {r}" + assert p + q + r <= MAX_PARTITIONED_DIMENSIONS, ( + f"p + q + r must be <= {MAX_PARTITIONED_DIMENSIONS}, got {p + q + r}" + ) assert leaf_n >= 1, f"leaf_n must be >= 1, got {leaf_n}" self._init_signature(p, q, r, leaf_n, product_chunk_size, accumulation_dtype) diff --git a/experiments/_lib.py b/experiments/_lib.py index 7651510..6873ca7 100644 --- a/experiments/_lib.py +++ b/experiments/_lib.py @@ -40,7 +40,7 @@ import torch import torch.nn as nn -from core.config import PartitionConfig, make_algebra +from core.config import DEFAULT_PARTITION_LEAF_N, PartitionConfig, make_algebra from core.metric import hermitian_grade_spectrum from core.module import AlgebraLike from functional.activation import GeometricGELU @@ -77,7 +77,7 @@ def setup_algebra( dtype: torch.dtype | str = torch.float32, kernel: str = "auto", partition_threshold: int = 8, - leaf_n: int = 4, + leaf_n: int = DEFAULT_PARTITION_LEAF_N, product_chunk_size: Optional[int] = None, partition_tree: Optional[str] = None, accumulation_dtype: torch.dtype | str | None = None, diff --git a/tests/test_partitioned_algebra.py b/tests/test_partitioned_algebra.py index b1d36ed..f3c8189 100644 --- a/tests/test_partitioned_algebra.py +++ b/tests/test_partitioned_algebra.py @@ -5,18 +5,36 @@ # you may not use this file except in compliance with the License. # +"""Low-dimensional and structural unit coverage for partitioned Clifford algebra. + +Dense-reference sweeps for the Cl8-Cl12 overlap region live in +``test_partitioned_dense_reference.py``. Slow non-monolithic verification for +Cl12+ lives in ``test_partitioned_highdim.py``. +""" + import pytest import torch import torch.nn as nn from core.algebra import CliffordAlgebra -from core.partitioned_algebra import PartitionedCliffordAlgebra +from core.config import PartitionConfig +from core.partitioned_algebra import DEFAULT_PARTITION_LEAF_N, MAX_PARTITIONED_DIMENSIONS, PartitionedCliffordAlgebra pytestmark = pytest.mark.unit DEVICE = "cpu" +def test_partition_config_leaf_default_matches_kernel_default(): + assert PartitionConfig().leaf_n == DEFAULT_PARTITION_LEAF_N + assert PartitionConfig.from_mapping({"leaf_n": None}).leaf_n == DEFAULT_PARTITION_LEAF_N + + +def test_partitioned_algebra_rejects_above_supported_dimension(): + with pytest.raises(AssertionError, match=f"p \\+ q \\+ r must be <= {MAX_PARTITIONED_DIMENSIONS}"): + PartitionedCliffordAlgebra(MAX_PARTITIONED_DIMENSIONS + 1, 0, 0, device=DEVICE) + + def _dtype_tolerance(dtype: torch.dtype) -> float: if dtype == torch.float16: return 5e-3 @@ -86,14 +104,14 @@ def test_default_recursive_cl8_matches_core_kernel(self): _assert_matches_monolithic(8, 0, 0, leaf_n=6, shape=(2,)) def test_recursive_tree_uses_balanced_binary_splits(self): - algebra = PartitionedCliffordAlgebra(20, 0, 0, device=DEVICE, leaf_n=6) - - assert algebra.left_n == 10 - assert algebra.right_n == 10 - assert algebra.left_sub.left_n == 5 - assert algebra.left_sub.right_n == 5 - assert algebra.right_sub.left_n == 5 - assert algebra.right_sub.right_n == 5 + algebra = PartitionedCliffordAlgebra(16, 0, 0, device=DEVICE, leaf_n=6) + + assert algebra.left_n == 8 + assert algebra.right_n == 8 + assert algebra.left_sub.left_n == 4 + assert algebra.left_sub.right_n == 4 + assert algebra.right_sub.left_n == 4 + assert algebra.right_sub.right_n == 4 assert not hasattr(algebra.left_sub, "cayley_indices") assert not hasattr(algebra.right_sub, "cayley_indices") diff --git a/tests/test_partitioned_highdim.py b/tests/test_partitioned_highdim.py index 1e204c2..b24b2bf 100644 --- a/tests/test_partitioned_highdim.py +++ b/tests/test_partitioned_highdim.py @@ -522,8 +522,8 @@ def test_cl12_random_three_dimensional_subspace_projects_to_n3_engine(self): assert torch.allclose(projected, local_expected, atol=1e-10, rtol=1e-10) assert torch.allclose(global_product, embedded_expected, atol=1e-10, rtol=1e-10) - def test_cl20_recursive_sign_merge_matches_direct_bitmask_reference(self): - p, q, r = 12, 6, 2 + def test_cl16_recursive_sign_merge_matches_direct_bitmask_reference(self): + p, q, r = 10, 4, 2 algebra = PartitionedCliffordAlgebra( p, q, @@ -535,12 +535,12 @@ def test_cl20_recursive_sign_merge_matches_direct_bitmask_reference(self): ) pairs = [ (0, 0), - (1, 1 << 19), - (0xABCDE, 0x13579), - (0xFFFFF, 0x00011), - (0x22222, 0xDDDDD), - (0x7A5C3, 0xC3A57), - ((1 << 18) | 7, (1 << 18) | 3), + (1, 1 << 15), + (0xABCD, 0x1357), + (0xFFFF, 0x0011), + (0x2222, 0xDDDD), + (0x7A5C, 0xC3A5), + ((1 << 14) | 7, (1 << 14) | 3), ] for index_a, index_b in pairs: @@ -548,8 +548,8 @@ def test_cl20_recursive_sign_merge_matches_direct_bitmask_reference(self): actual = _partitioned_basis_product(algebra, index_a, index_b) assert actual == expected - def test_cl20_basis_products_satisfy_algebraic_identities(self): - p, q, r = 12, 6, 2 + def test_cl16_basis_products_satisfy_algebraic_identities(self): + p, q, r = 10, 4, 2 algebra = PartitionedCliffordAlgebra( p, q, @@ -560,10 +560,10 @@ def test_cl20_basis_products_satisfy_algebraic_identities(self): product_chunk_size=4, ) triples = [ - (0x12345, 0x00F0F, 0xABCDE), - (0x70001, 0x02A80, 0x11111), - ((1 << 18) | 0x35, 0x04440, 0x21001), - (0x7A5C3, (1 << 19) | 0x81, 0x00013), + (0x1234, 0x00F0, 0xABCD), + (0x7001, 0x02A8, 0x1111), + ((1 << 14) | 0x35, 0x4440, 0x2101), + (0x7A5C, (1 << 15) | 0x81, 0x0013), ] for index_a, index_b, index_c in triples: @@ -580,10 +580,10 @@ def test_cl20_basis_products_satisfy_algebraic_identities(self): assert left == right pairs = [ - (0x12345, 0x00F0F), - (0x7A5C3, 0xC3A57), - ((1 << 18) | 0x101, (1 << 18) | 0x077), - ((1 << 19) | 0x222, 0x13579), + (0x1234, 0x00F0), + (0x7A5C, 0xC3A5), + ((1 << 14) | 0x101, (1 << 14) | 0x077), + ((1 << 15) | 0x222, 0x1357), ] for index_a, index_b in pairs: ab = _multiply_signed_basis(algebra, (index_a, 1.0), (index_b, 1.0)) @@ -603,8 +603,8 @@ def test_cl20_basis_products_satisfy_algebraic_identities(self): ) assert involution_ab == involution_product - def test_cl20_simple_bivector_exp_matches_long_taylor_reference(self): - p, q, r = 20, 0, 0 + def test_cl16_simple_bivector_exp_matches_long_taylor_reference(self): + p, q, r = 16, 0, 0 algebra = PartitionedCliffordAlgebra( p, q, @@ -614,7 +614,7 @@ def test_cl20_simple_bivector_exp_matches_long_taylor_reference(self): leaf_n=6, product_chunk_size=4, ) - bivector_index = (1 << 0) | (1 << 17) + bivector_index = (1 << 0) | (1 << 13) theta = 0.375 square = -1.0 @@ -636,12 +636,12 @@ def test_cl20_simple_bivector_exp_matches_long_taylor_reference(self): @pytest.mark.parametrize( ("p", "q", "r", "bivector_index", "theta", "scalar_ref", "bivector_ref"), [ - (20, 0, 0, (1 << 0) | (1 << 17), 0.375, math.cos(0.375), math.sin(0.375)), - (1, 19, 0, (1 << 0) | (1 << 1), 0.25, math.cosh(0.25), math.sinh(0.25)), - (18, 0, 2, (1 << 0) | (1 << 18), 0.5, 1.0, 0.5), + (16, 0, 0, (1 << 0) | (1 << 13), 0.375, math.cos(0.375), math.sin(0.375)), + (1, 15, 0, (1 << 0) | (1 << 1), 0.25, math.cosh(0.25), math.sinh(0.25)), + (14, 0, 2, (1 << 0) | (1 << 14), 0.5, 1.0, 0.5), ], ) - def test_cl20_simple_bivector_exp_matches_closed_form( + def test_cl16_simple_bivector_exp_matches_closed_form( self, p, q, @@ -674,8 +674,8 @@ def test_cl20_simple_bivector_exp_matches_closed_form( ) assert torch.count_nonzero(actual).item() == 2 - def test_cl20_lorentzian_bivector_exp_matches_long_taylor_reference(self): - p, q, r = 1, 19, 0 + def test_cl16_lorentzian_bivector_exp_matches_long_taylor_reference(self): + p, q, r = 1, 15, 0 algebra = PartitionedCliffordAlgebra( p, q, @@ -704,8 +704,8 @@ def test_cl20_lorentzian_bivector_exp_matches_long_taylor_reference(self): ) assert torch.count_nonzero(actual).item() == 2 - def test_cl20_degenerate_repeated_null_factor_annihilates_product(self): - p, q, r = 12, 6, 2 + def test_cl16_degenerate_repeated_null_factor_annihilates_product(self): + p, q, r = 10, 4, 2 algebra = PartitionedCliffordAlgebra( p, q, diff --git a/tests/test_symbolic_regression.py b/tests/test_symbolic_regression.py index 4158280..acd6df8 100644 --- a/tests/test_symbolic_regression.py +++ b/tests/test_symbolic_regression.py @@ -56,7 +56,7 @@ def _make_cfg(dataset_name=_TEST_DATASET, hidden_channels=4, num_layers=1, n_sam "metric_search": metric_search, "partition_threshold": 8, "partition": { - "leaf_n": 4, + "leaf_n": None, "product_chunk_size": None, "tree": None, "accumulation_dtype": None, From 1d8a93ff8224f1c62e0d88b0824927b8cb1e6008 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 6 May 2026 19:21:14 +0900 Subject: [PATCH 10/45] test: add docstrings for test case --- tests/test_partitioned_algebra.py | 113 ++++++++++++++++ tests/test_partitioned_dense_reference.py | 38 ++++++ tests/test_partitioned_highdim.py | 155 +++++++++++++++++++++- 3 files changed, 305 insertions(+), 1 deletion(-) diff --git a/tests/test_partitioned_algebra.py b/tests/test_partitioned_algebra.py index f3c8189..5906673 100644 --- a/tests/test_partitioned_algebra.py +++ b/tests/test_partitioned_algebra.py @@ -10,6 +10,11 @@ Dense-reference sweeps for the Cl8-Cl12 overlap region live in ``test_partitioned_dense_reference.py``. Slow non-monolithic verification for Cl12+ lives in ``test_partitioned_highdim.py``. + +This file is for small enough algebras where a dense ``CliffordAlgebra`` oracle +is cheap. It intentionally forces shallow recursive trees with small ``leaf_n`` +values so tests can inspect internal split structure, chunking, dtype handling, +and compile behavior without relying on large high-dimensional fixtures. """ import pytest @@ -26,11 +31,16 @@ def test_partition_config_leaf_default_matches_kernel_default(): + # ``None`` is the Hydra/YAML spelling for "use the partitioned kernel + # default." Keep this tied to the constructor default so config and direct + # Python construction cannot silently diverge. assert PartitionConfig().leaf_n == DEFAULT_PARTITION_LEAF_N assert PartitionConfig.from_mapping({"leaf_n": None}).leaf_n == DEFAULT_PARTITION_LEAF_N def test_partitioned_algebra_rejects_above_supported_dimension(): + # The partitioned kernel has an explicit physical memory limit. This test + # guards the public error boundary rather than a particular internal split. with pytest.raises(AssertionError, match=f"p \\+ q \\+ r must be <= {MAX_PARTITIONED_DIMENSIONS}"): PartitionedCliffordAlgebra(MAX_PARTITIONED_DIMENSIONS + 1, 0, 0, device=DEVICE) @@ -46,6 +56,7 @@ def _dtype_tolerance(dtype: torch.dtype) -> float: def _make_pair(p=3, q=1, r=0, *, leaf_n=2, product_chunk_size=None, dtype=torch.float64): + """Build matched dense and partitioned algebras for core-kernel comparisons.""" reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype) algebra = PartitionedCliffordAlgebra( p, @@ -60,6 +71,7 @@ def _make_pair(p=3, q=1, r=0, *, leaf_n=2, product_chunk_size=None, dtype=torch. def _assert_matches_monolithic(p, q=0, r=0, *, leaf_n=6, shape=(3,), dtype=torch.float64): + """Compare a partitioned product against the dense monolithic Cayley kernel.""" torch.manual_seed(17) reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype) algebra = PartitionedCliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype, leaf_n=leaf_n) @@ -75,6 +87,8 @@ def _assert_matches_monolithic(p, q=0, r=0, *, leaf_n=6, shape=(3,), dtype=torch class _PartitionedProductLayer(nn.Module): + """Tiny module used to check ``torch.compile`` forward and backward paths.""" + def __init__(self, p, q=0, r=0): super().__init__() self.algebra = PartitionedCliffordAlgebra( @@ -94,16 +108,29 @@ def forward(self, x): class TestPartitionedCliffordAlgebra: + """Small-dimensional regression tests grouped by partitioned-kernel region.""" + def test_leaf_matches_core_kernel(self): + # With n <= leaf_n, the partitioned algebra delegates to the dense leaf + # kernel. This test makes sure the leaf path has no wrapper-level dtype + # or shape differences. _assert_matches_monolithic(3, 1, 0, leaf_n=6, shape=(2,)) def test_forced_recursive_euclidean_matches_core_kernel(self): + # ``leaf_n=2`` forces Cl(4,0) to split recursively even though the dense + # kernel could handle it directly. This is the smallest recursive + # product check with a Euclidean reference. _assert_matches_monolithic(4, 0, 0, leaf_n=2, shape=(4,)) def test_default_recursive_cl8_matches_core_kernel(self): + # Cl8 is the first default-recursive Euclidean case now that the default + # leaf size is six dimensions. _assert_matches_monolithic(8, 0, 0, leaf_n=6, shape=(2,)) def test_recursive_tree_uses_balanced_binary_splits(self): + # Balanced splits keep internal tensors bounded. Cl16 with leaf_n=6 + # should split 16 -> 8 + 8 -> 4 + 4 leaves instead of making one large + # dense child. algebra = PartitionedCliffordAlgebra(16, 0, 0, device=DEVICE, leaf_n=6) assert algebra.left_n == 8 @@ -117,6 +144,9 @@ def test_recursive_tree_uses_balanced_binary_splits(self): assert not hasattr(algebra.right_sub, "cayley_indices") def test_describe_tree_reports_split_layout_and_shared_nodes(self, capsys): + # ``describe_tree`` is the debugging view used when constructing custom + # partition trees. The exact root text documents public bit ranges, + # child sizes, pair count, chunk size, and shared-node annotations. algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=2) tree = algebra.describe_tree() @@ -134,6 +164,9 @@ def test_describe_tree_reports_split_layout_and_shared_nodes(self, capsys): assert capsys.readouterr().out.strip() == tree def test_repeated_signature_tiles_share_subalgebras_automatically(self): + # Cl(8,4,4) can be tiled as repeated Cl(2,1,1) blocks. The planner is + # allowed to permute internal bit order to share identical child modules, + # but it must retain public canonical basis order at the API boundary. algebra = PartitionedCliffordAlgebra( 8, 4, @@ -157,6 +190,10 @@ def test_repeated_signature_tiles_share_subalgebras_automatically(self): assert "shared_with=root.L.L" in tree def test_repeated_signature_tile_product_matches_core_kernel_with_basis_permutation(self): + # This catches sign mistakes introduced by the repeated-tile basis + # permutation. The dense reference uses public bitmask order, so any + # missing input or output permutation sign appears directly in the + # product comparison. torch.manual_seed(107) reference = CliffordAlgebra(4, 2, 2, device=DEVICE, dtype=torch.float64) algebra = PartitionedCliffordAlgebra( @@ -176,6 +213,9 @@ def test_repeated_signature_tile_product_matches_core_kernel_with_basis_permutat ) def test_repeated_signature_tile_product_gradients_match_core_kernel(self): + # The same repeated-tile permutation must also be transparent to + # autograd. Compare gradients against the dense kernel with cloned inputs + # so both graphs receive identical values. torch.manual_seed(109) reference = CliffordAlgebra(4, 2, 2, device=DEVICE, dtype=torch.float64) algebra = PartitionedCliffordAlgebra( @@ -199,12 +239,18 @@ def test_repeated_signature_tile_product_gradients_match_core_kernel(self): assert torch.allclose(B_partitioned.grad, B_ref.grad, atol=1e-9, rtol=1e-9) def test_identical_recursive_subalgebras_are_shared(self): + # Pure Euclidean repeated halves should reuse child objects. This reduces + # memory and ensures recursive caches key by structural signature rather + # than by object construction path. algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=2) assert algebra.left_sub is algebra.right_sub assert algebra.left_sub.left_sub is algebra.left_sub.right_sub def test_recursive_node_uses_compact_memory_layout(self): + # Recursive nodes should not store dense global Cayley-like routing + # tables. They keep structural sign vectors and derive right-pair slices + # lazily so memory scales with child size and chunk size. algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, leaf_n=2) assert "_grade_masks_float" not in algebra._buffers @@ -233,12 +279,17 @@ def test_recursive_node_uses_compact_memory_layout(self): assert algebra.basis_permutation.split_signs.numel() == 0 def test_default_recursive_mixed_signature_matches_core_kernel(self): + # Mixed signatures stress metric signs across the split boundary. _assert_matches_monolithic(5, 2, 1, leaf_n=6, shape=(2,)) def test_recursive_product_supports_extra_batch_axes(self): + # Recursive products must obey PyTorch broadcasting semantics on all + # leading dimensions, matching the dense kernel. _assert_matches_monolithic(4, 1, 0, leaf_n=3, shape=(2, 3)) def test_recursive_product_gradients_match_core_kernel(self): + # Basic backward check for the recursive product path without repeated + # signature permutations. torch.manual_seed(23) reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64, leaf_n=2) @@ -256,6 +307,9 @@ def test_recursive_product_gradients_match_core_kernel(self): @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32, torch.float64]) def test_recursive_operations_support_generic_floating_dtypes(self, dtype): + # The partitioned kernel owns sign buffers in the algebra dtype but must + # still preserve the requested floating dtype for product, norm, and exp + # outputs. Tolerances widen for reduced precision. torch.manual_seed(67) reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=dtype) algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=dtype, leaf_n=2) @@ -305,6 +359,8 @@ def test_geometric_product_promotes_inputs_with_algebra_dtype( expected_dtype, leaf_n, ): + # Mixed input dtypes should promote with the algebra's sign-buffer dtype. + # The test runs both a forced-recursive and leaf-backed path via leaf_n. torch.manual_seed(71) algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=algebra_dtype, leaf_n=leaf_n) reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=expected_dtype) @@ -328,6 +384,9 @@ def test_geometric_product_promotes_inputs_with_algebra_dtype( assert torch.allclose(actual_exp.float(), expected_exp.float(), atol=atol, rtol=atol) def test_stable_accumulation_reduces_cumulative_forward_error(self): + # A long product chain amplifies fp32 accumulation error. Promoting only + # recursive accumulation to fp64 should improve agreement with a dense + # fp64 reference while returning fp32 outputs. torch.manual_seed(73) reference = CliffordAlgebra(8, 0, 0, device=DEVICE, dtype=torch.float64) standard = PartitionedCliffordAlgebra( @@ -361,6 +420,9 @@ def test_stable_accumulation_reduces_cumulative_forward_error(self): assert stable_error < standard_error * 0.5 def test_stable_accumulation_reduces_cumulative_backward_error(self): + # Backward accumulation can drift for repeated products as well. This + # mirrors the forward stability test but compares input gradients against + # a dense fp64 reference graph. torch.manual_seed(79) reference = CliffordAlgebra(8, 0, 0, device=DEVICE, dtype=torch.float64) standard = PartitionedCliffordAlgebra( @@ -400,6 +462,9 @@ def test_stable_accumulation_reduces_cumulative_backward_error(self): assert stable_error < standard_error * 0.7 def test_recursive_product_chunked_pair_merge_matches_core_kernel(self): + # Chunking processes only a slice of right-basis pairs at a time. This + # product-level check ensures chunk boundaries do not change the merged + # result or batch broadcasting behavior. torch.manual_seed(29) reference, algebra = _make_pair(5, 1, 0, leaf_n=2, product_chunk_size=3) A = torch.randn(2, 1, algebra.dim, dtype=torch.float64) @@ -412,6 +477,10 @@ def test_recursive_product_chunked_pair_merge_matches_core_kernel(self): @pytest.mark.parametrize("pair_range", ["full", "chunk"]) def test_indexed_right_interaction_merge_matches_reference(self, pair_range): + # Private merge helpers are tested directly because they are the critical + # memory-saving replacement for materialized right-interaction tensors. + # Both full-range and chunk-range calls must produce identical values and + # gradients. torch.manual_seed(31) product_chunk_size = None if pair_range == "full" else 3 algebra = PartitionedCliffordAlgebra( @@ -447,6 +516,9 @@ def test_indexed_right_interaction_merge_matches_reference(self, pair_range): @pytest.mark.parametrize(("p", "q", "r"), [(5, 1, 0), (1, 1, 4)]) def test_vectorized_full_pair_product_matches_chunked_with_gradients(self, p, q, r): + # Vectorized and chunked pair accumulation should be algebraically + # identical. The degenerate signature case verifies that compact + # surviving-pair routing for null dimensions also matches. torch.manual_seed(83 + p * 13 + q * 7 + r) reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=torch.float64) vectorized = PartitionedCliffordAlgebra(p, q, r, device=DEVICE, dtype=torch.float64, leaf_n=3) @@ -490,6 +562,9 @@ def test_vectorized_full_pair_product_matches_chunked_with_gradients(self, p, q, assert torch.allclose(B_chunked.grad, B_ref.grad, atol=1e-10, rtol=1e-10) def test_unit_rotor_chain_maintains_normalization_beyond_depth_threshold(self): + # Rotor chains are a realistic high-depth workload. The small bivector + # step should preserve unit norm over many recursive products when + # accumulation is promoted. algebra = PartitionedCliffordAlgebra( 8, 0, @@ -518,6 +593,9 @@ def test_unit_rotor_chain_maintains_normalization_beyond_depth_threshold(self): assert max_error < 5e-5 def test_bridge_sign_for_high_times_low_vector(self): + # The bridge sign is easiest to see with one vector from the high child + # multiplied by one vector from the low child. e5 * e1 must be -e15 + # because the high vector crosses one low vector. reference = CliffordAlgebra(5, 0, 0, device=DEVICE, dtype=torch.float64) algebra = PartitionedCliffordAlgebra(5, 0, 0, device=DEVICE, dtype=torch.float64, leaf_n=4) @@ -533,12 +611,18 @@ def test_bridge_sign_for_high_times_low_vector(self): assert torch.equal(actual, expected) def test_null_cross_split_matches_core_kernel(self): + # Degenerate dimensions can live on either side of a split. This covers + # null-annihilation behavior when null bits cross child boundaries. _assert_matches_monolithic(4, 2, 2, leaf_n=4, shape=(2,)) def test_minkowski_signature_matches_core_kernel(self): + # Lorentzian signs are a common model target and should match the dense + # kernel under forced recursion. _assert_matches_monolithic(1, 3, 0, leaf_n=2, shape=(3,)) def test_degenerate_signature_matches_core_kernel(self): + # Small degenerate mixed signature used as a dense-reference smoke test + # before the broader parameterized sweep. _assert_matches_monolithic(2, 1, 2, leaf_n=2, shape=(2,)) @pytest.mark.parametrize( @@ -552,6 +636,10 @@ def test_degenerate_signature_matches_core_kernel(self): ], ) def test_general_signature_sweep_matches_core_kernel(self, p, q, r): + # Compact sweep over pure negative, pure null, mixed positive/null, + # negative/null, and fully mixed signatures. Binary and unary operations + # are checked together so structural sign buffers stay aligned with the + # product implementation. torch.manual_seed(19 + p * 11 + q * 7 + r) reference, algebra = _make_pair(p, q, r, leaf_n=2, product_chunk_size=3) A = torch.randn(2, algebra.dim, dtype=torch.float64) @@ -582,6 +670,9 @@ def test_general_signature_sweep_matches_core_kernel(self, p, q, r): assert torch.allclose(actual, expected, atol=1e-10, rtol=1e-10), method_name def test_recursive_node_does_not_allocate_global_cayley_table(self): + # A recursive root must not expose dense Cayley buffers. If this fails, + # high-dimensional partitioned construction has likely regressed toward + # dense memory behavior. algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=6) assert not hasattr(algebra, "cayley_indices") @@ -590,6 +681,9 @@ def test_recursive_node_does_not_allocate_global_cayley_table(self): assert algebra.right_sub.dim == 16 def test_static_structural_sign_buffers_match_core_kernel(self): + # These buffers are derived analytically in the partitioned kernel but + # should match the dense core on small signatures. They feed unary + # operations, norms, pseudoscalar products, exp, and right contraction. reference, algebra = _make_pair(3, 1, 1, leaf_n=2) for name in [ @@ -614,6 +708,8 @@ def test_static_structural_sign_buffers_match_core_kernel(self): assert torch.equal(actual, expected) def test_unary_operations_match_core_kernel(self): + # Public unary APIs should not reveal whether the algebra is dense, + # leaf-backed, or recursively partitioned. torch.manual_seed(31) reference, algebra = _make_pair(3, 1, 0, leaf_n=2) mv = torch.randn(2, algebra.dim, dtype=torch.float64) @@ -637,6 +733,8 @@ def test_unary_operations_match_core_kernel(self): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) def test_binary_operations_match_core_kernel(self): + # Non-product binary APIs use combinations of geometric products and + # grade masks. Broadcasting across different leading axes is included. torch.manual_seed(37) reference, algebra = _make_pair(3, 1, 0, leaf_n=2, product_chunk_size=3) A = torch.randn(2, 1, algebra.dim, dtype=torch.float64) @@ -654,6 +752,8 @@ def test_binary_operations_match_core_kernel(self): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) def test_bivector_vector_right_contraction_matches_core_kernel(self): + # Right contraction has a specialized bivector-vector path. Projecting + # random inputs to grades 2 and 1 keeps the test focused on that path. torch.manual_seed(41) reference, algebra = _make_pair(4, 0, 0, leaf_n=2) A = reference.grade_projection(torch.randn(3, algebra.dim, dtype=torch.float64), 2) @@ -665,6 +765,9 @@ def test_bivector_vector_right_contraction_matches_core_kernel(self): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) def test_blade_and_versor_operations_match_core_kernel(self): + # Higher-level blade and versor helpers compose several primitive + # operations. A dense reference here catches shape and promotion mistakes + # without needing large dimensions. torch.manual_seed(43) reference, algebra = _make_pair(3, 1, 0, leaf_n=2) mv = torch.randn(2, algebra.dim, dtype=torch.float64) @@ -688,6 +791,9 @@ def test_blade_and_versor_operations_match_core_kernel(self): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) def test_sandwich_variants_match_core_kernel(self): + # Sandwich variants have different broadcasting contracts: + # per-channel, same-batch multi-channel, and multi-rotor. All should use + # recursive products transparently. torch.manual_seed(47) reference, algebra = _make_pair(3, 0, 0, leaf_n=2) bivector = torch.zeros(4, algebra.dim, dtype=torch.float64) @@ -711,6 +817,9 @@ def test_sandwich_variants_match_core_kernel(self): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) def test_exp_paths_match_core_kernel(self): + # Exp has separate simple-bivector, compiled-safe, and Taylor helper + # paths. Small Cl4 keeps the dense reference cheap while still exercising + # the nontrivial bivector decomposition path. reference, algebra = _make_pair(4, 0, 0, leaf_n=2) B = torch.zeros(2, algebra.dim, dtype=torch.float64) B[:, 3] = torch.tensor([0.125, -0.25], dtype=torch.float64) @@ -730,6 +839,8 @@ def test_exp_paths_match_core_kernel(self): @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") def test_compile_geometric_product_matches_eager(self): + # ``aot_eager`` is used as a lightweight compile backend to ensure the + # recursive product graph avoids obvious compile breaks. torch.manual_seed(53) algebra = PartitionedCliffordAlgebra(4, 1, 0, device=DEVICE, dtype=torch.float32, leaf_n=2) A = torch.randn(3, algebra.dim) @@ -744,6 +855,8 @@ def product(x, y): @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") def test_compile_training_backward_matches_eager(self): + # Compilation must preserve both parameter and input gradients. The tiny + # layer keeps the graph small but includes an nn.Parameter broadcast. torch.manual_seed(59) eager_layer = _PartitionedProductLayer(4, 0, 0) compiled_layer = _PartitionedProductLayer(4, 0, 0) diff --git a/tests/test_partitioned_dense_reference.py b/tests/test_partitioned_dense_reference.py index 40f8222..1c48d55 100644 --- a/tests/test_partitioned_dense_reference.py +++ b/tests/test_partitioned_dense_reference.py @@ -11,6 +11,10 @@ up as direct numerical error instead of only algebraic identity failures. The slow Cl12 case is intentionally separated from the regular unit sweep because the dense reference allocates a monolithic Cayley table. + +Use this file when a future partitioned operation can still be compared against +``CliffordAlgebra``. Use ``test_partitioned_highdim.py`` when the dimension is +above the dense table limit or when the reference should be purely axiomatic. """ import pytest @@ -24,6 +28,7 @@ def _make_dense_reference_pair(p: int, q: int, r: int, leaf_n: int, product_chunk_size: int = 32): + """Create dense and partitioned algebras with matched signature and dtype.""" reference = make_algebra(p, q, r, kernel="dense", device=DEVICE, dtype=torch.float64) partitioned = make_algebra( p, @@ -38,6 +43,7 @@ def _make_dense_reference_pair(p: int, q: int, r: int, leaf_n: int, product_chun def _dense_inputs(dim: int, seed: int, batch_shape=(1,)): + """Build deterministic small-magnitude dense inputs for error checks.""" generator = torch.Generator(device=DEVICE).manual_seed(seed) scale = 0.125 A = torch.randn(*batch_shape, dim, dtype=torch.float64, generator=generator) * scale @@ -46,6 +52,7 @@ def _dense_inputs(dim: int, seed: int, batch_shape=(1,)): def _assert_bounded_error(actual: torch.Tensor, expected: torch.Tensor, label: str, *, atol=2e-9, rtol=2e-9): + """Assert both absolute and relative agreement with a diagnostic message.""" diff = actual - expected max_abs = diff.abs().max().item() denominator = expected.norm().clamp_min(torch.finfo(expected.dtype).eps) @@ -69,6 +76,9 @@ def _assert_bounded_error(actual: torch.Tensor, expected: torch.Tensor, label: s ], ) def test_dense_comparable_binary_operations_have_bounded_error(p, q, r, leaf_n): + # This is the fast dense-overlap sweep. Keep dimensions at or below Cl10 so + # the regular non-slow suite does not spend most of its time building dense + # Cayley tables. The separate slow test below covers Cl12 explicitly. reference, partitioned = _make_dense_reference_pair(p, q, r, leaf_n) A, B = _dense_inputs(partitioned.dim, seed=503 + p * 17 + q * 11 + r) @@ -93,6 +103,9 @@ def test_dense_comparable_binary_operations_have_bounded_error(p, q, r, leaf_n): ], ) def test_dense_comparable_unary_operations_have_bounded_error(p, q, r, leaf_n): + # Unary operations are mostly structural sign or mask operations. Comparing + # them against the dense kernel here catches public-basis permutation + # mistakes separately from the recursive product path. reference, partitioned = _make_dense_reference_pair(p, q, r, leaf_n) mv, _ = _dense_inputs(partitioned.dim, seed=719 + p * 17 + q * 11 + r, batch_shape=(2,)) @@ -118,6 +131,8 @@ def test_dense_comparable_unary_operations_have_bounded_error(p, q, r, leaf_n): def test_partitioned_8d_fixture_matches_dense_reference(partitioned_algebra_8d): + # Fixture-level coverage ensures conftest's shared high-dimensional algebra + # declarations stay aligned with the dense-overlap verification method. reference = make_algebra(8, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float64) A, B = _dense_inputs(partitioned_algebra_8d.dim, seed=1013, batch_shape=(2,)) @@ -129,6 +144,10 @@ def test_partitioned_8d_fixture_matches_dense_reference(partitioned_algebra_8d): @pytest.mark.slow def test_partitioned_12d_fixture_dense_reference_error_is_bounded(partitioned_algebra_12d): + # Cl12 is the highest dimension supported by the dense kernel. This test is + # slow because it allocates the monolithic dense Cayley table, but it is the + # strongest direct check that partitioned accumulation does not introduce + # excessive numerical error before we leave dense-reference territory. reference = make_algebra(12, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float64) A, B = _dense_inputs(partitioned_algebra_12d.dim, seed=1207) @@ -140,6 +159,9 @@ def test_partitioned_12d_fixture_dense_reference_error_is_bounded(partitioned_al @pytest.mark.slow def test_partitioned_12d_mixed_fixture_matches_bitmask_reference(partitioned_algebra_12d_mixed): + # Mixed signature Cl(8,3,1) checks negative metric signs and null + # annihilation with an axiomatic sparse reference. This complements the + # dense Cl12 test without requiring another dense mixed-signature table. entries_a = [(0, 0.25), (3, -1.5), (257, 0.75), (2049, 2.0)] entries_b = [(1, -0.5), (384, 1.25), (1025, -2.0), (4095, 0.5)] A = _make_sparse_multivector(partitioned_algebra_12d_mixed, entries_a, torch.float64) @@ -157,6 +179,9 @@ def test_partitioned_12d_mixed_fixture_matches_bitmask_reference(partitioned_alg @pytest.mark.slow def test_partitioned_16d_fixture_basis_product_matches_bitmask_reference(partitioned_algebra_16d): + # The 16D fixture is above the dense kernel limit, so the reference is a + # single basis product computed from bitmask rules. The binary literals make + # the active public dimensions visible when adding nearby cases. p, q, r = 10, 4, 2 index_a = 0b1001_0010_0110_1011 index_b = 0b0110_1101_1000_1110 @@ -175,6 +200,7 @@ def test_partitioned_16d_fixture_basis_product_matches_bitmask_reference(partiti def _make_sparse_multivector(algebra, entries, dtype: torch.dtype) -> torch.Tensor: + """Materialize sparse ``(bitmask_index, coefficient)`` entries.""" mv = torch.zeros(1, algebra.dim, dtype=dtype) for index, value in entries: mv[0, index] += value @@ -188,6 +214,7 @@ def _sparse_product_reference( q: int, r: int, ) -> dict[int, float]: + """Sparse multivector product using only bitmask basis rules.""" result = {} for index_a, value_a in entries_a: for index_b, value_b in entries_b: @@ -199,18 +226,29 @@ def _sparse_product_reference( def _basis_product_reference(index_a: int, index_b: int, p: int, q: int, r: int) -> tuple[int, float]: + """Reference basis product for ``e_index_a * e_index_b``. + + The result blade index is XOR. The sign is the parity of swaps between set + bits, adjusted for repeated negative basis vectors. A repeated null basis + vector makes the coefficient exactly zero. + """ n = p + q + r swap_count = 0 for bit in range(n): if index_a & (1 << bit): + # Count right-operand vectors that must move left across this + # left-operand vector to restore canonical increasing bit order. swap_count += (index_b & ((1 << bit) - 1)).bit_count() sign = -1.0 if swap_count % 2 else 1.0 + # Negative metric dimensions occupy [p, p + q). Repeating one contributes + # e_i^2 = -1. negative_mask = sum(1 << bit for bit in range(p, p + q)) if ((index_a & index_b & negative_mask).bit_count() % 2) == 1: sign = -sign + # Null dimensions occupy [p + q, n). Repeating any one contributes e_i^2 = 0. null_mask = sum(1 << bit for bit in range(p + q, n)) if (index_a & index_b & null_mask) != 0: sign = 0.0 diff --git a/tests/test_partitioned_highdim.py b/tests/test_partitioned_highdim.py index b24b2bf..321afce 100644 --- a/tests/test_partitioned_highdim.py +++ b/tests/test_partitioned_highdim.py @@ -10,6 +10,22 @@ These tests avoid monolithic Cayley-table references. For n >= 12, the reference is computed from axiomatic bitmask rules, sub-algebraic isomorphisms, algebraic identities, or closed forms inside known two-dimensional subalgebras. + +Bitmask convention used throughout this file: + +* Basis blade ``e_I`` is stored at integer index ``I``. +* Bit ``i`` in ``I`` means vector basis element ``e_i`` participates in the + blade. For example, ``0b0101`` represents ``e_0 e_2``. +* The geometric product result blade is ``index_a ^ index_b`` unless a repeated + null basis vector annihilates the term. +* The product sign is the parity of swaps needed to move the right blade's + vectors past the left blade's vectors, followed by metric signs from repeated + negative basis vectors. + +When adding high-dimensional tests, prefer constructing sparse multivectors as +``[(bitmask_index, coefficient), ...]`` and comparing against +``_sparse_product_reference``. This keeps the reference independent from both +the dense Cayley table and the partitioned implementation. """ import math @@ -26,6 +42,13 @@ def _signature_for_range_reference(p: int, q: int, r: int, start: int, width: int) -> tuple[int, int, int]: + """Return the local signature for a contiguous range of public dimensions. + + Dimensions are ordered by signature block: positive ``[0, p)``, negative + ``[p, p + q)``, then null ``[p + q, p + q + r)``. This helper is used when a + test embeds a lower-dimensional algebra inside a contiguous slice of a + larger algebra and needs to build the matching local reference algebra. + """ end = start + width p_count = max(0, min(end, p) - start) q_count = max(0, min(end, p + q) - max(start, p)) @@ -35,6 +58,12 @@ def _signature_for_range_reference(p: int, q: int, r: int, start: int, width: in def _shift_index(index: int, offset: int) -> int: + """Shift every set basis bit in ``index`` by ``offset`` public dimensions. + + Example: local blade ``0b101`` at offset 3 becomes global blade + ``0b101000``. This is the bitmask equivalent of embedding a local blade + into a higher-dimensional subspace without changing the local blade order. + """ shifted = 0 bit = 0 while index: @@ -49,7 +78,14 @@ def _make_orthonormal_subspace_basis( algebra: PartitionedCliffordAlgebra, frame: torch.Tensor, ) -> torch.Tensor: - """Return embedded basis blades for an orthonormal frame.""" + """Return embedded basis blades for an orthonormal frame. + + ``frame`` contains ``width`` orthonormal vectors in the ambient algebra's + vector space. The returned matrix has one row for every local bitmask + ``0..2**width-1``. Row ``basis_index`` is built by multiplying the selected + frame vectors in increasing bit order, matching the canonical bitmask basis + order used by ``CliffordAlgebra``. + """ width = frame.shape[-1] vector_indices = (1 << torch.arange(algebra.n, dtype=torch.long, device=frame.device)).long() vectors = torch.zeros(width, algebra.dim, dtype=frame.dtype, device=frame.device) @@ -68,18 +104,38 @@ def _make_orthonormal_subspace_basis( def _basis_product_reference(index_a: int, index_b: int, p: int, q: int, r: int) -> tuple[int, float]: + """Reference product for two canonical basis blades using bit operations. + + This is the smallest trusted oracle in the partitioned tests. It intentionally + does not call either dense or partitioned algebra code. + + The algorithm mirrors the Clifford product rules: + + 1. The result blade contains basis vectors that appear in exactly one input, + so its index is ``index_a ^ index_b``. + 2. Every vector in ``index_a`` must move past lower-numbered vectors in + ``index_b``. The parity of those swaps gives the exterior sign. + 3. Repeated negative basis vectors contribute an extra ``-1`` metric sign. + 4. Repeated null basis vectors make the entire product zero. + """ n = p + q + r swap_count = 0 for bit in range(n): if index_a & (1 << bit): + # Count vectors from B with lower public index than the current A + # vector. Each such pair must be swapped once. swap_count += (index_b & ((1 << bit) - 1)).bit_count() sign = -1.0 if swap_count % 2 else 1.0 + # Negative dimensions occupy [p, p + q). Repeating one of those vectors + # squares it to -1, so an odd number of repeated negative bits flips sign. negative_mask = sum(1 << bit for bit in range(p, p + q)) if ((index_a & index_b & negative_mask).bit_count() % 2) == 1: sign = -sign + # Null dimensions occupy [p + q, n). Repeating a null basis vector squares + # it to zero, annihilating that basis-pair contribution. null_mask = sum(1 << bit for bit in range(p + q, n)) if (index_a & index_b & null_mask) != 0: sign = 0.0 @@ -88,13 +144,28 @@ def _basis_product_reference(index_a: int, index_b: int, p: int, q: int, r: int) def _partitioned_basis_product(algebra, index_a: int, index_b: int) -> tuple[int, float]: + """Evaluate a basis product by walking the partition tree directly. + + This helper verifies the recursive routing logic without constructing full + multivectors. At each internal node, public indices are first converted into + split-local order if the node uses a repeated-tile permutation. The low + ``right_n`` bits are handled by the right child, the remaining high bits by + the left child, and the bridge sign accounts for commuting right-child + factors across left-child factors. + """ if algebra.core is not None: + # Leaves are intentionally backed by the dense local kernel. Reading the + # leaf Cayley table here checks the partition traversal and merge signs, + # not the local dense product itself. result_index = int(algebra.core.cayley_indices[index_a, index_b].item()) sign = float(algebra.core.cayley_signs[index_a, index_b].item()) return result_index, sign input_sign = 1.0 if algebra.basis_permutation.uses_permutation: + # Some signatures are internally reordered to share identical child + # subalgebras. The permutation carries signs because changing vector + # order changes canonical blade orientation. split_a = int(algebra.basis_permutation.public_to_split[index_a].item()) split_b = int(algebra.basis_permutation.public_to_split[index_b].item()) input_sign *= float(algebra.basis_permutation.split_signs[split_a].item()) @@ -106,14 +177,19 @@ def _partitioned_basis_product(algebra, index_a: int, index_b: int) -> tuple[int left_a, right_a = index_a >> algebra.right_n, index_a & right_mask left_b, right_b = index_b >> algebra.right_n, index_b & right_mask + # Recurse independently in the child algebras. The child result indices are + # then packed back into the split-local index layout. left_result, left_sign = _partitioned_basis_product(algebra.left_sub, left_a, left_b) right_result, right_sign = _partitioned_basis_product(algebra.right_sub, right_a, right_b) right_b_index = torch.tensor([right_b], dtype=torch.long, device=algebra.device) + # Bridge sign = (-1) ** (grade(left_A) * grade(right_B)). It is the only + # sign that couples the two child products at an internal node. bridge_sign = float(algebra._bridge_signs_for_right_b(right_b_index, torch.float64)[0, left_a].item()) result_index = (left_result << algebra.right_n) | right_result sign = input_sign * left_sign * right_sign * bridge_sign if algebra.basis_permutation.uses_permutation: + # Convert split-local result orientation back to public canonical order. sign *= float(algebra.basis_permutation.split_signs[result_index].item()) result_index = int(algebra.basis_permutation.split_to_public[result_index].item()) @@ -127,6 +203,7 @@ def _sparse_product_reference( q: int, r: int, ) -> dict[int, float]: + """Sparse multivector product using only the bitmask basis oracle.""" result = {} for index_a, value_a in entries_a: for index_b, value_b in entries_b: @@ -138,11 +215,24 @@ def _sparse_product_reference( def _signature_sweep_entries(p: int, q: int, r: int) -> tuple[list[tuple[int, float]], list[tuple[int, float]]]: + """Build sparse inputs that exercise low, middle, high, and null bits. + + The exact coefficients are arbitrary but non-symmetric. The bit positions + are chosen to cross split boundaries for balanced trees and to hit positive, + negative, and null signature blocks when present. Reuse this helper for + broad signature sweeps; add explicit hand-picked entries when a test needs a + very specific blade interaction. + """ n = p + q + r entries_a = [ + # Scalar term: verifies scalar multiplication and accumulation into an + # already-populated output blade. (0, 0.375), + # Low plus middle bit: likely crosses left/right children in Cl12+. ((1 << 0) | (1 << (n // 2)), -0.5), + # Two high bits: catches sign handling away from the low-order block. ((1 << (n - 3)) | (1 << (n - 1)), 0.875), + # Low plus high bit: exercises bridge signs across the partition split. ((1 << 1) | (1 << (n - 2)), -1.125), ] entries_b = [ @@ -153,6 +243,8 @@ def _signature_sweep_entries(p: int, q: int, r: int) -> tuple[list[tuple[int, fl ] if r > 0: + # Include repeated null-bit products. Any pair that repeats ``null_bit`` + # should vanish in the reference and in the partitioned kernel. null_bit = p + q entries_a.append(((1 << null_bit) | (1 << 1), 0.25)) entries_b.append(((1 << null_bit) | (1 << 2), -1.5)) @@ -161,6 +253,7 @@ def _signature_sweep_entries(p: int, q: int, r: int) -> tuple[list[tuple[int, fl def _make_sparse_multivector(algebra: PartitionedCliffordAlgebra, entries, dtype: torch.dtype) -> torch.Tensor: + """Materialize ``[(bitmask, coeff), ...]`` entries into a dense tensor.""" mv = torch.zeros(1, algebra.dim, dtype=dtype) for index, value in entries: mv[0, index] += value @@ -168,6 +261,7 @@ def _make_sparse_multivector(algebra: PartitionedCliffordAlgebra, entries, dtype def _make_expected_multivector(algebra: PartitionedCliffordAlgebra, entries, dtype: torch.dtype) -> torch.Tensor: + """Materialize a sparse dictionary reference into a dense output tensor.""" expected = torch.zeros(1, algebra.dim, dtype=dtype) for index, value in entries.items(): expected[0, index] = value @@ -175,6 +269,7 @@ def _make_expected_multivector(algebra: PartitionedCliffordAlgebra, entries, dty def _long_taylor_simple_bivector(theta: float, square: float, order: int = 80) -> tuple[float, float]: + """Reference exp(theta B) for a simple bivector with known ``B**2`` scalar.""" scalar = 0.0 bivector = 0.0 power = 1.0 @@ -189,6 +284,7 @@ def _long_taylor_simple_bivector(theta: float, square: float, order: int = 80) - def _canonical_basis_term(index: int, coefficient: float) -> tuple[int, float]: + """Normalize zero coefficients so identity checks compare exact tuples.""" if coefficient == 0.0: return 0, 0.0 return index, coefficient @@ -199,6 +295,7 @@ def _multiply_signed_basis( left: tuple[int, float], right: tuple[int, float], ) -> tuple[int, float]: + """Multiply two signed basis terms through the partition-tree basis oracle.""" left_index, left_coeff = left right_index, right_coeff = right if left_coeff == 0.0 or right_coeff == 0.0: @@ -208,16 +305,22 @@ def _multiply_signed_basis( def _reverse_sign(index: int) -> float: + """Sign applied by reversion to a basis blade of grade ``popcount(index)``.""" grade = index.bit_count() return -1.0 if (grade * (grade - 1) // 2) % 2 else 1.0 def _grade_involution_sign(index: int) -> float: + """Sign applied by grade involution to a basis blade.""" return -1.0 if index.bit_count() % 2 else 1.0 class TestPartitionedHighDimensionalVerification: def test_cl12_sparse_multivector_product_matches_direct_bitmask_reference(self): + # Cl(8,3,1) is dense enough to exercise recursive routing, includes a + # negative block and a null block, and still keeps the sparse reference + # human-readable. The selected indices include low, middle, high, and + # all-bits-set blades so sign and null handling are both visible. p, q, r = 8, 3, 1 algebra = PartitionedCliffordAlgebra( p, @@ -266,6 +369,10 @@ def test_sparse_multivector_products_match_bitmask_rules_across_highdim_signatur dtype, atol, ): + # This sweep is the broad regression surface for metric signatures: + # pure positive, pure negative, mixed nondegenerate, mixed degenerate, + # and the current 16D ceiling. Each case uses the same sparse pattern so + # differences come from signature signs and null annihilation only. algebra = PartitionedCliffordAlgebra( p, q, @@ -286,6 +393,10 @@ def test_sparse_multivector_products_match_bitmask_rules_across_highdim_signatur assert torch.allclose(actual, expected, atol=atol, rtol=atol) def test_automatic_tiled_cl12_product_matches_bitmask_reference(self): + # Cl(6,3,3) has signature gcd 3, so the automatic planner can reorder + # dimensions into repeated Cl(2,1,1) tiles and share child modules. The + # bitmask reference stays in public canonical order, making this a direct + # check that permutation signs are restored correctly at the boundary. p, q, r = 6, 3, 3 dtype = torch.float64 algebra = PartitionedCliffordAlgebra( @@ -310,6 +421,11 @@ def test_automatic_tiled_cl12_product_matches_bitmask_reference(self): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) def test_cl12_sparse_multivectors_satisfy_numerical_identities(self): + # This test avoids any dense reference. Instead, it checks identities + # that must hold for the product regardless of partition shape: + # distributivity, associativity, reverse anti-automorphism, grade + # involution automorphism, scalar multiplication, and vector + # anticommutation. p, q, r = 7, 3, 2 dtype = torch.float64 algebra = PartitionedCliffordAlgebra( @@ -378,6 +494,10 @@ def test_cl12_sparse_multivectors_satisfy_numerical_identities(self): assert not torch.allclose(e0e2, e2e0, atol=1e-12, rtol=1e-12) def test_cl12_backward_matches_finite_difference_directional_derivative(self): + # Autograd is checked against a directional finite difference using + # sparse perturbations. Keeping A, B, dA, dB, and the loss weight sparse + # makes the reference direction easy to inspect while still exercising + # the recursive product backward path. p, q, r = 7, 3, 2 dtype = torch.float64 algebra = PartitionedCliffordAlgebra( @@ -414,6 +534,9 @@ def test_cl12_backward_matches_finite_difference_directional_derivative(self): assert torch.allclose(directional_grad, finite_difference, atol=1e-9, rtol=1e-9) def test_cl16_dense_basis_product_matches_direct_bitmask_reference(self): + # A single basis-product case at the 16D cap catches indexing mistakes + # that are invisible in smaller dimensions. The binary literals make it + # easy to see which public bits are active in each operand. p, q, r = 10, 4, 2 algebra = PartitionedCliffordAlgebra( p, @@ -440,6 +563,10 @@ def test_cl16_dense_basis_product_matches_direct_bitmask_reference(self): assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) def test_cl12_embedded_subalgebra_product_matches_local_isomorphism(self): + # A contiguous 6D public slice should behave exactly like a standalone + # local algebra with the induced signature. ``_shift_index`` embeds each + # local basis blade into the global bit positions, then the global output + # is compared against the shifted local product. p, q, r = 6, 4, 2 offset, width = 3, 6 local_p, local_q, local_r = _signature_for_range_reference(p, q, r, offset, width) @@ -490,6 +617,10 @@ def test_cl12_embedded_subalgebra_product_matches_local_isomorphism(self): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) def test_cl12_random_three_dimensional_subspace_projects_to_n3_engine(self): + # This is a basis-independent isomorphism check. A random orthonormal + # 3-frame defines an embedded Cl(3,0) subalgebra inside Cl(12,0). After + # multiplying embedded multivectors globally, projecting back onto the + # constructed basis must recover the local Cl(3,0) product. dtype = torch.float64 global_algebra = PartitionedCliffordAlgebra( 12, @@ -523,6 +654,9 @@ def test_cl12_random_three_dimensional_subspace_projects_to_n3_engine(self): assert torch.allclose(global_product, embedded_expected, atol=1e-10, rtol=1e-10) def test_cl16_recursive_sign_merge_matches_direct_bitmask_reference(self): + # This helper-level test walks the partition tree one basis product at a + # time. It is faster than materializing full multivectors and directly + # targets the recursive split/bridge/permutation sign merge. p, q, r = 10, 4, 2 algebra = PartitionedCliffordAlgebra( p, @@ -549,6 +683,10 @@ def test_cl16_recursive_sign_merge_matches_direct_bitmask_reference(self): assert actual == expected def test_cl16_basis_products_satisfy_algebraic_identities(self): + # Tuple-valued basis products make exact identity checks possible at the + # 16D cap. Associativity validates recursive merge consistency; reverse + # and grade-involution identities validate the sign formulas used by the + # structural buffers. p, q, r = 10, 4, 2 algebra = PartitionedCliffordAlgebra( p, @@ -604,6 +742,10 @@ def test_cl16_basis_products_satisfy_algebraic_identities(self): assert involution_ab == involution_product def test_cl16_simple_bivector_exp_matches_long_taylor_reference(self): + # Simple bivector exponentials have a closed two-term form. The long + # Taylor reference is intentionally scalar-only and independent of the + # algebra implementation, so it catches coefficient/sign regressions in + # the high-dimensional closed-form path. p, q, r = 16, 0, 0 algebra = PartitionedCliffordAlgebra( p, @@ -651,6 +793,10 @@ def test_cl16_simple_bivector_exp_matches_closed_form( scalar_ref, bivector_ref, ): + # Parameterized closed-form cases cover the three regimes: + # Euclidean-like elliptic, Lorentzian hyperbolic, and degenerate + # parabolic. The selected bivector index always contains exactly two + # bits so the expected output has only scalar and bivector components. algebra = PartitionedCliffordAlgebra( p, q, @@ -675,6 +821,9 @@ def test_cl16_simple_bivector_exp_matches_closed_form( assert torch.count_nonzero(actual).item() == 2 def test_cl16_lorentzian_bivector_exp_matches_long_taylor_reference(self): + # Separate long-Taylor check for the hyperbolic sign regime. In + # Cl(1,15), e0 has positive square and e1 has negative square, so + # (e0e1)^2 = +1 and exp(theta e0e1) uses cosh/sinh. p, q, r = 1, 15, 0 algebra = PartitionedCliffordAlgebra( p, @@ -705,6 +854,10 @@ def test_cl16_lorentzian_bivector_exp_matches_long_taylor_reference(self): assert torch.count_nonzero(actual).item() == 2 def test_cl16_degenerate_repeated_null_factor_annihilates_product(self): + # In a degenerate signature, any product that repeats the same null + # basis vector must vanish. These cases keep additional non-null bits in + # each operand to ensure the annihilation is detected before other sign + # details can mask the bug. p, q, r = 10, 4, 2 algebra = PartitionedCliffordAlgebra( p, From 788c564bd273a63500e75a79a8133bbd4fc589ea Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 6 May 2026 20:18:51 +0900 Subject: [PATCH 11/45] test: add test case about tree-rejection and more signature --- tests/test_partitioned_algebra.py | 57 +++++++++++++++++++++++++ tests/test_partitioned_highdim.py | 71 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/tests/test_partitioned_algebra.py b/tests/test_partitioned_algebra.py index 5906673..386c830 100644 --- a/tests/test_partitioned_algebra.py +++ b/tests/test_partitioned_algebra.py @@ -45,6 +45,49 @@ def test_partitioned_algebra_rejects_above_supported_dimension(): PartitionedCliffordAlgebra(MAX_PARTITIONED_DIMENSIONS + 1, 0, 0, device=DEVICE) +@pytest.mark.parametrize("n", range(8, MAX_PARTITIONED_DIMENSIONS + 1)) +def test_default_partitioned_structure_covers_8_to_16_dimensions(n): + # This is a lightweight structural sweep across the intended operating + # range. It does not multiply full multivectors; it checks that construction + # stays recursive, bounded by leaf_n, and free of global Cayley tables. + algebra = PartitionedCliffordAlgebra(n, 0, 0, device=DEVICE, dtype=torch.float32) + + assert algebra.dim == 2**n + assert algebra.core is None + assert not hasattr(algebra, "cayley_indices") + assert not hasattr(algebra, "cayley_signs") + + for node in _walk_unique_partition_nodes(algebra): + if node.core is None: + assert node.left_n + node.right_n == node.n + assert node.left_sub.n == node.left_n + assert node.right_sub.n == node.right_n + assert node.left_sub.n > 0 + assert node.right_sub.n > 0 + assert not hasattr(node, "cayley_indices") + assert not hasattr(node, "cayley_signs") + else: + assert node.n <= DEFAULT_PARTITION_LEAF_N + + +@pytest.mark.parametrize( + ("partition_tree", "match"), + [ + pytest.param("R=0-3; L=4-6", "cover every dimension", id="missing-dimension"), + pytest.param("R=0-3; L=3-7", "appears in both", id="overlapping-dimension"), + pytest.param("R=0-3; X=4-7", "Invalid partition path", id="invalid-path"), + pytest.param("R=0-3; L=8", "outside", id="out-of-range"), + pytest.param("R=3-0; L=4-7", "descending", id="descending-range"), + pytest.param("R=0,0; L=1-7", "duplicates", id="duplicate-within-node"), + ], +) +def test_partition_tree_expression_rejects_invalid_edge_cases(partition_tree, match): + # Explicit tree declarations are user-facing, so parser failures should be + # caught before construction reaches recursive internals. + with pytest.raises(ValueError, match=match): + PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, partition_tree=partition_tree) + + def _dtype_tolerance(dtype: torch.dtype) -> float: if dtype == torch.float16: return 5e-3 @@ -86,6 +129,20 @@ def _assert_matches_monolithic(p, q=0, r=0, *, leaf_n=6, shape=(3,), dtype=torch assert torch.allclose(actual, expected, atol=1e-9, rtol=1e-9) +def _walk_unique_partition_nodes(algebra: PartitionedCliffordAlgebra): + """Yield every unique node in a partition tree, handling shared children.""" + stack = [algebra] + seen = set() + while stack: + node = stack.pop() + if id(node) in seen: + continue + seen.add(id(node)) + yield node + if node.core is None: + stack.extend([node.left_sub, node.right_sub]) + + class _PartitionedProductLayer(nn.Module): """Tiny module used to check ``torch.compile`` forward and backward paths.""" diff --git a/tests/test_partitioned_highdim.py b/tests/test_partitioned_highdim.py index 321afce..34f4846 100644 --- a/tests/test_partitioned_highdim.py +++ b/tests/test_partitioned_highdim.py @@ -392,6 +392,45 @@ def test_sparse_multivector_products_match_bitmask_rules_across_highdim_signatur assert torch.allclose(actual, expected, atol=atol, rtol=atol) + @pytest.mark.parametrize( + ("p", "q", "r", "dtype", "atol"), + [ + pytest.param(8, 0, 0, torch.float64, 1e-12, id="n8-positive"), + pytest.param(0, 9, 0, torch.float64, 1e-12, id="n9-negative"), + pytest.param(0, 0, 10, torch.float64, 1e-12, id="n10-null"), + pytest.param(6, 3, 2, torch.float64, 1e-12, id="n11-mixed-null"), + pytest.param(8, 3, 1, torch.float64, 1e-12, id="n12-mixed-null"), + pytest.param(7, 4, 2, torch.float64, 1e-12, id="n13-mixed-null"), + pytest.param(8, 4, 2, torch.float32, 1e-6, id="n14-mixed-null"), + pytest.param(9, 4, 2, torch.float32, 1e-6, id="n15-mixed-null"), + pytest.param(10, 4, 2, torch.float32, 1e-6, id="n16-mixed-null"), + ], + ) + def test_sparse_multivector_products_cover_every_dimension_from_8_to_16(self, p, q, r, dtype, atol): + # This is the main 8D-16D product sweep. It deliberately uses one + # signature per dimension so changes in split depth, odd/even splits, + # null compaction, and the 16D cap are all exercised by a single test + # family. The bitmask oracle makes this independent from dense support. + n = p + q + r + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=dtype, + leaf_n=6, + product_chunk_size=8 if n >= 14 else 16, + ) + entries_a, entries_b = _signature_sweep_entries(p, q, r) + A = _make_sparse_multivector(algebra, entries_a, dtype) + B = _make_sparse_multivector(algebra, entries_b, dtype) + + actual = algebra.geometric_product(A, B) + expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) + expected = _make_expected_multivector(algebra, expected_sparse, dtype) + + assert torch.allclose(actual, expected, atol=atol, rtol=atol) + def test_automatic_tiled_cl12_product_matches_bitmask_reference(self): # Cl(6,3,3) has signature gcd 3, so the automatic planner can reorder # dimensions into repeated Cl(2,1,1) tiles and share child modules. The @@ -682,6 +721,38 @@ def test_cl16_recursive_sign_merge_matches_direct_bitmask_reference(self): actual = _partitioned_basis_product(algebra, index_a, index_b) assert actual == expected + def test_cl16_forced_unbalanced_tree_matches_bitmask_reference(self): + # Explicit tree expressions are how callers force a non-default split. + # This tree peels off four low dimensions, then recursively splits the + # remaining high dimensions. It crosses positive, negative, and null + # signature blocks, so both public-basis permutation and bridge signs + # must be correct. + p, q, r = 10, 4, 2 + dtype = torch.float32 + partition_tree = "R=0-3; L.R=4-7; L.L.R=8-11; L.L.L=12-15" + algebra = PartitionedCliffordAlgebra( + p, + q, + r, + device=DEVICE, + dtype=dtype, + leaf_n=6, + product_chunk_size=4, + partition_tree=partition_tree, + ) + entries_a, entries_b = _signature_sweep_entries(p, q, r) + A = _make_sparse_multivector(algebra, entries_a, dtype) + B = _make_sparse_multivector(algebra, entries_b, dtype) + + actual = algebra.geometric_product(A, B) + expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) + expected = _make_expected_multivector(algebra, expected_sparse, dtype) + + tree = algebra.describe_tree() + assert "root: Cl(10,4,2)" in tree + assert "root.L.L" in tree + assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) + def test_cl16_basis_products_satisfy_algebraic_identities(self): # Tuple-valued basis products make exact identity checks possible at the # 16D cap. Associativity validates recursive merge consistency; reverse From 70b9c3ea4d11689aaec3c271d513105631eb05fb Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 11:27:55 +0900 Subject: [PATCH 12/45] refactor: restructure core planning architecture --- README.md | 2 +- __init__.py | 13 +- benchmarks/benchmark_core.py | 27 +- conf/config.yaml | 10 +- core/__init__.py | 66 +- core/analysis/commutator.py | 2 +- core/analysis/dimension.py | 2 +- core/analysis/geodesic.py | 2 +- core/analysis/pipeline.py | 2 +- core/analysis/signature.py | 2 +- core/analysis/spectral.py | 6 +- core/analysis/symmetry.py | 2 +- core/config.py | 123 +- core/foundation/__init__.py | 47 + core/foundation/basis.py | 124 ++ core/{ => foundation}/device.py | 0 core/foundation/layout.py | 99 + core/{ => foundation}/module.py | 2 +- core/{ => foundation}/validation.py | 0 core/partitioned_algebra.py | 2003 --------------------- core/planning/__init__.py | 33 + core/planning/flow.py | 97 + core/planning/grade_plan.py | 335 ++++ core/planning/request.py | 221 +++ core/planning/translator.py | 229 +++ core/planning/tree.py | 136 ++ core/planning/unary.py | 252 +++ core/runtime/__init__.py | 20 + core/{ => runtime}/algebra.py | 26 +- core/runtime/context.py | 176 ++ core/{ => runtime}/decomposition.py | 0 core/{ => runtime}/metric.py | 2 +- core/{ => runtime}/multivector.py | 142 +- core/runtime/projected.py | 80 + core/visualizer.py | 2 +- docs/api/core.md | 10 +- docs/design_guide.md | 6 +- docs/index.md | 2 +- docs/tutorial.md | 4 +- examples/datasets/amass.py | 2 +- examples/datasets/synthetic.py | 2 +- examples/tasks/cgenn.py | 2 +- examples/tasks/clifford_pde.py | 2 +- examples/tasks/hyperbolic.py | 2 +- examples/tasks/manifold.py | 2 +- examples/tasks/sanity_check.py | 2 +- experiments/_gdo/benchmarks.py | 2 +- experiments/_gdo/controller.py | 2 +- experiments/_gdo/harness.py | 2 +- experiments/_gdo/optimizer.py | 2 +- experiments/_gdo/parameter_groups.py | 2 +- experiments/_gdo/pre_exploration.py | 2 +- experiments/_lib.py | 21 +- experiments/_templates/inc_template.py | 2 +- experiments/dbg_linear_basis_mixing.py | 4 +- experiments/dbg_lorentz.py | 4 +- experiments/dbg_maxwell_equations.py | 4 +- experiments/dbg_navier_stokes.py | 4 +- experiments/dbg_yang_mills.py | 6 +- experiments/inc_embed_compress.py | 4 +- experiments/inc_lattice_morph.py | 8 +- experiments/inc_pendulum_dynamics.py | 6 +- experiments/inc_sta_trajectory.py | 6 +- functional/activation.py | 2 +- functional/loss.py | 4 +- functional/orthogonality.py | 2 +- layers/__init__.py | 2 +- layers/adapters/conformal.py | 4 +- layers/adapters/embedding.py | 4 +- layers/adapters/gnn.py | 4 +- layers/adapters/mother.py | 4 +- layers/adapters/projective.py | 4 +- layers/blocks/attention.py | 214 ++- layers/blocks/multi_rotor_ffn.py | 4 +- layers/blocks/transformer.py | 4 +- layers/primitives/linear.py | 6 +- layers/primitives/multi_rotor.py | 6 +- layers/primitives/normalization.py | 4 +- layers/primitives/projection.py | 4 +- layers/primitives/reflection.py | 6 +- layers/primitives/rotor.py | 8 +- layers/primitives/rotor_gadget.py | 6 +- models/blocks/gbn.py | 4 +- models/blocks/multi_rotor.py | 4 +- models/blocks/time_series.py | 4 +- models/deap/eeg_net.py | 2 +- models/lqa/glr_net.py | 4 +- models/lqa/heads.py | 4 +- models/md17/forcenet.py | 4 +- models/sr/estimator.py | 2 +- models/sr/grouper.py | 4 +- models/sr/net.py | 4 +- models/sr/translator.py | 2 +- tasks/base.py | 4 +- tasks/md17.py | 2 +- tasks/symbolic_regression.py | 2 +- tests/conftest.py | 58 +- tests/test_analysis.py | 2 +- tests/test_attention.py | 101 ++ tests/test_core.py | 2 +- tests/test_decomposition.py | 4 +- tests/test_degenerate_algebra.py | 4 +- tests/test_exp_signatures.py | 14 +- tests/test_extensions.py | 2 +- tests/test_geodesic.py | 2 +- tests/test_grade_plan.py | 450 +++++ tests/test_hermitian_metrics.py | 4 +- tests/test_iterative_unbender.py | 2 +- tests/test_layer_optimization.py | 2 +- tests/test_layers.py | 16 +- tests/test_md17_complete.py | 8 +- tests/test_metric_search.py | 2 +- tests/test_multivector.py | 4 +- tests/test_partitioned_algebra.py | 934 ---------- tests/test_partitioned_dense_reference.py | 256 --- tests/test_partitioned_highdim.py | 952 ---------- tests/test_properties.py | 2 +- tests/test_riemannian_optimizer.py | 2 +- tests/test_rotor_gadget.py | 6 +- tests/test_rotor_translate.py | 2 +- tests/test_symbolic_regression.py | 11 +- 121 files changed, 2975 insertions(+), 4607 deletions(-) create mode 100644 core/foundation/__init__.py create mode 100644 core/foundation/basis.py rename core/{ => foundation}/device.py (100%) create mode 100644 core/foundation/layout.py rename core/{ => foundation}/module.py (96%) rename core/{ => foundation}/validation.py (100%) delete mode 100644 core/partitioned_algebra.py create mode 100644 core/planning/__init__.py create mode 100644 core/planning/flow.py create mode 100644 core/planning/grade_plan.py create mode 100644 core/planning/request.py create mode 100644 core/planning/translator.py create mode 100644 core/planning/tree.py create mode 100644 core/planning/unary.py create mode 100644 core/runtime/__init__.py rename core/{ => runtime}/algebra.py (97%) create mode 100644 core/runtime/context.py rename core/{ => runtime}/decomposition.py (100%) rename core/{ => runtime}/metric.py (99%) rename core/{ => runtime}/multivector.py (64%) create mode 100644 core/runtime/projected.py create mode 100644 tests/test_attention.py create mode 100644 tests/test_grade_plan.py delete mode 100644 tests/test_partitioned_algebra.py delete mode 100644 tests/test_partitioned_dense_reference.py delete mode 100644 tests/test_partitioned_highdim.py diff --git a/README.md b/README.md index e38e3eb..f22bd0e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ uv sync --extra all # everything ```python import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers.primitives.rotor import RotorLayer from layers.linear import CliffordLinear from functional.activation import GeometricGELU diff --git a/__init__.py b/__init__.py index 292c470..abe7e8b 100644 --- a/__init__.py +++ b/__init__.py @@ -2,21 +2,18 @@ __version__ = "1.0.0" -from core.algebra import CliffordAlgebra -from core.config import DEFAULT_PARTITION_LEAF_N, AlgebraConfig, PartitionConfig, make_algebra, make_algebra_from_config -from core.module import CliffordModule -from core.partitioned_algebra import MAX_PARTITIONED_DIMENSIONS, PartitionedCliffordAlgebra +from core.config import AlgebraConfig, make_algebra, make_algebra_from_config +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra +from core.runtime.context import AlgebraContext from layers import CliffordLinear, RotorLayer __all__ = [ "__version__", "AlgebraConfig", + "AlgebraContext", "CliffordAlgebra", "CliffordModule", - "DEFAULT_PARTITION_LEAF_N", - "MAX_PARTITIONED_DIMENSIONS", - "PartitionConfig", - "PartitionedCliffordAlgebra", "make_algebra", "make_algebra_from_config", "RotorLayer", diff --git a/benchmarks/benchmark_core.py b/benchmarks/benchmark_core.py index 29be49f..07f059b 100644 --- a/benchmarks/benchmark_core.py +++ b/benchmarks/benchmark_core.py @@ -55,11 +55,11 @@ if str(_REPO_ROOT) not in sys.path: sys.path.insert(0, str(_REPO_ROOT)) -from core.config import DEFAULT_PARTITION_LEAF_N, PartitionConfig, make_algebra -from core.decomposition import ExpPolicy, compiled_safe_decomposed_exp # noqa: E402 -from core.device import FLOAT_DTYPES, optional_dtype, resolve_device -from core.device import dtype_name as _format_dtype_name -from core.module import AlgebraLike +from core.config import make_algebra +from core.runtime.decomposition import ExpPolicy, compiled_safe_decomposed_exp # noqa: E402 +from core.foundation.device import FLOAT_DTYPES, resolve_device +from core.foundation.device import dtype_name as _format_dtype_name +from core.foundation.module import AlgebraLike DTYPES: dict[str, torch.dtype] = FLOAT_DTYPES @@ -182,19 +182,12 @@ def setup_algebra( args: argparse.Namespace | None = None, ) -> AlgebraLike: """Construct benchmark algebras through the shared core factory.""" - partition = PartitionConfig( - leaf_n=getattr(args, "partition_leaf_n", DEFAULT_PARTITION_LEAF_N), - product_chunk_size=getattr(args, "partition_product_chunk_size", None), - tree=getattr(args, "partition_tree", None), - accumulation_dtype=optional_dtype(getattr(args, "partition_accumulation_dtype", None)), - ) return make_algebra( p=p, q=q, r=r, kernel=getattr(args, "algebra_kernel", "auto"), - partition_threshold=getattr(args, "partition_threshold", 8), - partition=partition, + dense_threshold=getattr(args, "dense_threshold", 8), device=device, dtype=dtype, exp_policy=exp_policy, @@ -3491,12 +3484,8 @@ def _collect_runtime_metadata(device: str) -> dict[str, Any]: def make_argparser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Benchmark the Versor core package.") parser.add_argument("--device", default="auto", help="cpu, cuda, mps, or auto") - parser.add_argument("--algebra-kernel", default="auto", choices=("auto", "dense", "partitioned")) - parser.add_argument("--partition-threshold", type=int, default=8) - parser.add_argument("--partition-leaf-n", type=int, default=DEFAULT_PARTITION_LEAF_N) - parser.add_argument("--partition-product-chunk-size", type=int, default=None) - parser.add_argument("--partition-tree", default=None) - parser.add_argument("--partition-accumulation-dtype", default=None) + parser.add_argument("--algebra-kernel", default="auto", choices=("auto", "dense", "context")) + parser.add_argument("--dense-threshold", type=int, default=8) parser.add_argument("--out", default="benchmarks/results", help="artifact root") parser.add_argument("--sections", default="speed,backward,fusion,nonsimple,cumulative,convergence,stability") parser.add_argument("--n-values", type=_parse_int_csv, default=_parse_int_csv("2,3,4,5,6")) diff --git a/conf/config.yaml b/conf/config.yaml index f79e4fb..919f14c 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -6,16 +6,12 @@ algebra: p: 3 q: 0 r: 0 - kernel: "auto" # auto: dense for n<=partition_threshold, partitioned above it - partition_threshold: 8 + kernel: "auto" # auto: dense for n<=dense_threshold, AlgebraContext above it + dense_threshold: 8 device: "auto" # Auto-detect: cuda > mps > cpu dtype: "float32" exp_policy: "balanced" - partition: - leaf_n: null # null = partitioned kernel default - product_chunk_size: null - tree: null # Example: "R=0-3; L.R=4-7; L.L=8-11" - accumulation_dtype: null + default_grades: null # null = full layout fallback when no specific layout is declared training: epochs: 100 diff --git a/core/__init__.py b/core/__init__.py index d191566..d71f656 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -11,17 +11,37 @@ until first access, keeping ``import core`` lightweight. """ -from .algebra import CliffordAlgebra -from .config import DEFAULT_PARTITION_LEAF_N, AlgebraConfig, PartitionConfig, make_algebra, make_algebra_from_config -from .decomposition import ( +from .config import AlgebraConfig, make_algebra, make_algebra_from_config +from .foundation.basis import ( + GradeProductOp, + basis_indices_for_grades, + basis_product, + expand_output_grades, + geometric_product_output_grades, + normalize_grades, + operation_coefficient, + reverse_sign, +) +from .foundation.device import DeviceConfig, dtype_name, optional_dtype, resolve_device, resolve_dtype +from .foundation.layout import AlgebraSpec, GradeLayout +from .foundation.module import AlgebraLike, CliffordModule +from .foundation.validation import check_channels, check_multivector +from .planning.flow import GradeFlow +from .planning.grade_plan import GradeProductExecutor, GradeProductPlan, build_grade_product_plan +from .planning.request import ProductRequest, build_product_request +from .planning.translator import GradeTranslator +from .planning.tree import GradePathNode, GradePlanTree, build_grade_plan_tree +from .planning.unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request +from .runtime.algebra import CliffordAlgebra +from .runtime.context import AlgebraContext +from .runtime.decomposition import ( ExpPolicy, compiled_safe_decomposed_exp, differentiable_invariant_decomposition, exp_simple_bivector, ga_power_iteration, ) -from .device import DeviceConfig, dtype_name, optional_dtype, resolve_device, resolve_dtype -from .metric import ( +from .runtime.metric import ( clifford_conjugate, geometric_distance, grade_hermitian_norm, @@ -37,22 +57,19 @@ signature_norm_squared, signature_trace_form, ) -from .module import AlgebraLike, CliffordModule -from .multivector import Multivector -from .partitioned_algebra import MAX_PARTITIONED_DIMENSIONS, PartitionedCliffordAlgebra -from .validation import check_channels, check_multivector +from .runtime.multivector import Multivector __all__ = [ # algebra + "AlgebraContext", "CliffordAlgebra", "AlgebraConfig", "AlgebraLike", "CliffordModule", "Multivector", - "DEFAULT_PARTITION_LEAF_N", - "MAX_PARTITIONED_DIMENSIONS", - "PartitionConfig", - "PartitionedCliffordAlgebra", + "AlgebraSpec", + "GradeLayout", + "GradeTranslator", "make_algebra", "make_algebra_from_config", # device / validation @@ -84,6 +101,29 @@ "differentiable_invariant_decomposition", "exp_simple_bivector", "compiled_safe_decomposed_exp", + # static sparse grade planning + "GradeProductOp", + "GradeProductExecutor", + "GradeProductPlan", + "GradePathNode", + "GradePlanTree", + "GradeFlow", + "ProductRequest", + "GradeUnaryExecutor", + "GradeUnaryOp", + "GradeUnaryPlan", + "UnaryRequest", + "basis_indices_for_grades", + "basis_product", + "build_grade_product_plan", + "build_grade_plan_tree", + "build_product_request", + "build_unary_request", + "expand_output_grades", + "geometric_product_output_grades", + "normalize_grades", + "operation_coefficient", + "reverse_sign", # analysis (lazy) "MetricSearch", "GeodesicFlow", diff --git a/core/analysis/commutator.py b/core/analysis/commutator.py index 4c46dd3..ad4c22e 100644 --- a/core/analysis/commutator.py +++ b/core/analysis/commutator.py @@ -16,7 +16,7 @@ import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from ._types import CONSTANTS, CommutatorResult diff --git a/core/analysis/dimension.py b/core/analysis/dimension.py index c22c87e..628d9b3 100644 --- a/core/analysis/dimension.py +++ b/core/analysis/dimension.py @@ -18,7 +18,7 @@ import torch from core.config import make_algebra -from core.module import AlgebraLike +from core.foundation.module import AlgebraLike from ._types import CONSTANTS, DimensionResult diff --git a/core/analysis/geodesic.py b/core/analysis/geodesic.py index 6785a43..484dcdb 100644 --- a/core/analysis/geodesic.py +++ b/core/analysis/geodesic.py @@ -16,7 +16,7 @@ import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from ._types import CONSTANTS diff --git a/core/analysis/pipeline.py b/core/analysis/pipeline.py index 0761b40..f7ed812 100644 --- a/core/analysis/pipeline.py +++ b/core/analysis/pipeline.py @@ -14,7 +14,7 @@ import torch from core.config import make_algebra -from core.module import AlgebraLike +from core.foundation.module import AlgebraLike from ._types import AnalysisConfig, AnalysisReport, SamplingConfig from .commutator import CommutatorAnalyzer diff --git a/core/analysis/signature.py b/core/analysis/signature.py index a936769..a9874d5 100644 --- a/core/analysis/signature.py +++ b/core/analysis/signature.py @@ -22,7 +22,7 @@ import torch.nn as nn from core.config import make_algebra -from core.module import AlgebraLike +from core.foundation.module import AlgebraLike from layers import BladeSelector, CliffordLinear, RotorLayer from ._types import CONSTANTS, DimensionResult, SamplingConfig, SignatureResult diff --git a/core/analysis/spectral.py b/core/analysis/spectral.py index 4065c37..750971d 100644 --- a/core/analysis/spectral.py +++ b/core/analysis/spectral.py @@ -15,9 +15,9 @@ import torch -from core.algebra import CliffordAlgebra -from core.decomposition import differentiable_invariant_decomposition -from core.metric import hermitian_grade_spectrum +from core.runtime.algebra import CliffordAlgebra +from core.runtime.decomposition import differentiable_invariant_decomposition +from core.runtime.metric import hermitian_grade_spectrum from ._types import CONSTANTS, SpectralResult diff --git a/core/analysis/symmetry.py b/core/analysis/symmetry.py index df27776..a2171fe 100644 --- a/core/analysis/symmetry.py +++ b/core/analysis/symmetry.py @@ -15,7 +15,7 @@ import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from ._types import CONSTANTS, CommutatorResult, SymmetryResult diff --git a/core/config.py b/core/config.py index c1f2a97..79fa6ed 100644 --- a/core/config.py +++ b/core/config.py @@ -5,86 +5,56 @@ # you may not use this file except in compliance with the License. # -"""Algebra construction config and dense/partitioned kernel dispatch.""" +"""Algebra construction config and dense/planned context dispatch.""" from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any, Literal, Mapping, Optional +from dataclasses import dataclass +from typing import Any, Iterable, Literal, Mapping, Optional import torch -from core.algebra import CliffordAlgebra -from core.device import optional_dtype, resolve_device, resolve_dtype -from core.module import AlgebraLike -from core.partitioned_algebra import DEFAULT_PARTITION_LEAF_N, PartitionedCliffordAlgebra +from core.foundation.device import resolve_device, resolve_dtype +from core.foundation.module import AlgebraLike +from core.runtime.algebra import CliffordAlgebra +from core.runtime.context import AlgebraContext -AlgebraKernel = Literal["auto", "dense", "partitioned"] - - -@dataclass(frozen=True) -class PartitionConfig: - """Options specific to :class:`PartitionedCliffordAlgebra`.""" - - leaf_n: int = DEFAULT_PARTITION_LEAF_N - product_chunk_size: Optional[int] = None - tree: Optional[str] = None - accumulation_dtype: Optional[torch.dtype] = None - - def __post_init__(self) -> None: - object.__setattr__(self, "accumulation_dtype", optional_dtype(self.accumulation_dtype)) - - @classmethod - def from_mapping(cls, config: Optional[Mapping[str, Any]]) -> "PartitionConfig": - """Build partition options from a Hydra/OmegaConf-compatible mapping.""" - if config is None: - return cls() - leaf_n = _mapping_get(config, "leaf_n", DEFAULT_PARTITION_LEAF_N) - return cls( - leaf_n=_int_or_default(leaf_n, DEFAULT_PARTITION_LEAF_N), - product_chunk_size=_optional_int(_mapping_get(config, "product_chunk_size", None)), - tree=_optional_str(_mapping_get(config, "tree", None)), - accumulation_dtype=optional_dtype(_mapping_get(config, "accumulation_dtype", None)), - ) +AlgebraKernel = Literal["auto", "dense", "context"] @dataclass(frozen=True) class AlgebraConfig: - """Dense/partitioned algebra declaration.""" + """Dense/context algebra declaration.""" p: int q: int = 0 r: int = 0 kernel: AlgebraKernel = "auto" - partition_threshold: int = 8 + dense_threshold: int = 8 device: str = "cuda" dtype: torch.dtype = torch.float32 exp_policy: str = "balanced" fixed_iterations: Optional[int] = None - partition: PartitionConfig = field(default_factory=PartitionConfig) + default_grades: Optional[tuple[int, ...]] = None + allow_full_layout_products: Optional[bool] = None @classmethod def from_mapping(cls, config: Mapping[str, Any], **overrides) -> "AlgebraConfig": """Build an algebra declaration from Hydra/OmegaConf config.""" - partition_mapping = _mapping_get(config, "partition", None) - if partition_mapping is None: - partition_mapping = _flat_partition_mapping(config) - values = { "p": int(_mapping_get(config, "p", 0)), "q": int(_mapping_get(config, "q", 0)), "r": int(_mapping_get(config, "r", 0)), "kernel": _mapping_get(config, "kernel", "auto"), - "partition_threshold": int(_mapping_get(config, "partition_threshold", 8)), + "dense_threshold": int(_mapping_get(config, "dense_threshold", 8)), "device": _mapping_get(config, "device", "cuda"), "dtype": resolve_dtype(_mapping_get(config, "dtype", torch.float32)), "exp_policy": _mapping_get(config, "exp_policy", "balanced"), "fixed_iterations": _optional_int(_mapping_get(config, "fixed_iterations", None)), - "partition": PartitionConfig.from_mapping(partition_mapping), + "default_grades": _optional_grades(_mapping_get(config, "default_grades", None)), + "allow_full_layout_products": _optional_bool(_mapping_get(config, "allow_full_layout_products", None)), } values.update({key: value for key, value in overrides.items() if value is not None}) - if not isinstance(values["partition"], PartitionConfig): - values["partition"] = PartitionConfig.from_mapping(values["partition"]) values["dtype"] = resolve_dtype(values["dtype"]) return cls(**values) @@ -95,17 +65,23 @@ def make_algebra( r: int = 0, *, kernel: AlgebraKernel = "auto", - partition_threshold: int = 8, - partition: Optional[PartitionConfig] = None, + dense_threshold: int = 8, device="cuda", dtype: torch.dtype = torch.float32, exp_policy: str = "balanced", fixed_iterations: Optional[int] = None, + default_grades: Optional[Iterable[int]] = None, + allow_full_layout_products: Optional[bool] = None, + **deprecated_options, ) -> AlgebraLike: - """Construct a dense or partitioned algebra according to a kernel policy.""" + """Construct a dense low-dimensional algebra or high-dimensional planning context.""" kernel = _normalize_kernel(kernel) n = p + q + r - selected_kernel = "partitioned" if kernel == "auto" and n > partition_threshold else kernel + if deprecated_options: + unknown = ", ".join(sorted(deprecated_options)) + raise TypeError(f"Unsupported algebra construction option(s): {unknown}") + + selected_kernel = "context" if kernel == "auto" and n > dense_threshold else kernel if selected_kernel == "auto": selected_kernel = "dense" @@ -121,21 +97,17 @@ def make_algebra( dtype=resolved_dtype, exp_policy=exp_policy, fixed_iterations=fixed_iterations, + allow_large_dense=kernel == "dense", ) - partition = PartitionConfig() if partition is None else partition - return PartitionedCliffordAlgebra( + return AlgebraContext( p, q, r, device=resolved_device, dtype=resolved_dtype, - leaf_n=partition.leaf_n, - product_chunk_size=partition.product_chunk_size, - exp_policy=exp_policy, - fixed_iterations=fixed_iterations, - accumulation_dtype=partition.accumulation_dtype, - partition_tree=partition.tree, + default_grades=default_grades, + allow_full_layout_products=allow_full_layout_products, ) @@ -147,12 +119,13 @@ def make_algebra_from_config(config: Mapping[str, Any], **overrides) -> AlgebraL algebra_config.q, algebra_config.r, kernel=algebra_config.kernel, - partition_threshold=algebra_config.partition_threshold, - partition=algebra_config.partition, + dense_threshold=algebra_config.dense_threshold, device=algebra_config.device, dtype=algebra_config.dtype, exp_policy=algebra_config.exp_policy, fixed_iterations=algebra_config.fixed_iterations, + default_grades=algebra_config.default_grades, + allow_full_layout_products=algebra_config.allow_full_layout_products, ) @@ -163,21 +136,11 @@ def _mapping_get(config: Mapping[str, Any], key: str, default): return config.get(key, default) -def _flat_partition_mapping(config: Mapping[str, Any]) -> dict[str, Any]: - """Return partition options from flat ``algebra.*`` aliases.""" - return { - "leaf_n": _mapping_get(config, "leaf_n", DEFAULT_PARTITION_LEAF_N), - "product_chunk_size": _mapping_get(config, "product_chunk_size", None), - "tree": _mapping_get(config, "partition_tree", _mapping_get(config, "tree", None)), - "accumulation_dtype": _mapping_get(config, "accumulation_dtype", None), - } - - def _normalize_kernel(kernel: str) -> AlgebraKernel: """Validate and normalize algebra kernel names.""" normalized = str(kernel).lower() - if normalized not in {"auto", "dense", "partitioned"}: - raise ValueError(f"Unknown algebra kernel {kernel!r}; expected 'auto', 'dense', or 'partitioned'") + if normalized not in {"auto", "dense", "context"}: + raise ValueError(f"Unknown algebra kernel {kernel!r}; expected 'auto', 'dense', or 'context'") return normalized # type: ignore[return-value] @@ -187,14 +150,20 @@ def _optional_int(value) -> Optional[int]: return int(value) -def _int_or_default(value, default: int) -> int: +def _optional_grades(value) -> Optional[tuple[int, ...]]: if value is None: - return default - return int(value) + return None + return tuple(int(grade) for grade in value) -def _optional_str(value) -> Optional[str]: +def _optional_bool(value) -> Optional[bool]: if value is None: return None - text = str(value).strip() - return text or None + if isinstance(value, str): + lowered = value.strip().lower() + if lowered in {"true", "1", "yes", "on"}: + return True + if lowered in {"false", "0", "no", "off"}: + return False + raise ValueError(f"Cannot parse boolean value {value!r}") + return bool(value) diff --git a/core/foundation/__init__.py b/core/foundation/__init__.py new file mode 100644 index 0000000..fdec0af --- /dev/null +++ b/core/foundation/__init__.py @@ -0,0 +1,47 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Foundation value objects, basis utilities, validation, and device helpers.""" + +from .basis import ( + GradeProductOp, + basis_index_tuple_for_grades, + basis_indices_for_grades, + basis_product, + expand_output_grades, + geometric_product_output_grades, + normalize_grades, + operation_coefficient, + reverse_sign, +) +from .device import DeviceConfig, dtype_name, optional_dtype, resolve_device, resolve_dtype +from .layout import AlgebraSpec, GradeLayout +from .module import AlgebraLike, CliffordModule +from .validation import check_channels, check_multivector + +__all__ = [ + "AlgebraLike", + "AlgebraSpec", + "CliffordModule", + "DeviceConfig", + "GradeLayout", + "GradeProductOp", + "basis_index_tuple_for_grades", + "basis_indices_for_grades", + "basis_product", + "check_channels", + "check_multivector", + "dtype_name", + "expand_output_grades", + "geometric_product_output_grades", + "normalize_grades", + "operation_coefficient", + "optional_dtype", + "reverse_sign", + "resolve_device", + "resolve_dtype", +] diff --git a/core/foundation/basis.py b/core/foundation/basis.py new file mode 100644 index 0000000..2ce976d --- /dev/null +++ b/core/foundation/basis.py @@ -0,0 +1,124 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Canonical bitmask-basis utilities for Clifford algebra planning.""" + +from __future__ import annotations + +from typing import Iterable, Literal, Optional + +import torch + +GradeProductOp = Literal["gp", "wedge", "inner", "commutator", "anti_commutator"] + + +def normalize_grades(grades: Iterable[int], n: int, *, name: str = "grades") -> tuple[int, ...]: + """Return sorted unique grades validated against ``0 <= grade <= n``.""" + normalized = tuple(sorted({int(grade) for grade in grades})) + if not normalized: + raise ValueError(f"{name} must contain at least one grade") + invalid = [grade for grade in normalized if grade < 0 or grade > n] + if invalid: + raise ValueError(f"{name} contains invalid grades for n={n}: {invalid}") + return normalized + + +def basis_index_tuple_for_grades(n: int, grades: Iterable[int]) -> tuple[int, ...]: + """Return canonical bitmask basis indices whose popcount is in ``grades``.""" + grade_set = set(normalize_grades(grades, n)) + return tuple(index for index in range(1 << n) if index.bit_count() in grade_set) + + +def basis_indices_for_grades(n: int, grades: Iterable[int], *, device=None) -> torch.Tensor: + """Return canonical bitmask basis indices as a tensor.""" + return torch.tensor(basis_index_tuple_for_grades(n, grades), dtype=torch.long, device=device) + + +def geometric_product_output_grades(left_grade: int, right_grade: int, n: int) -> tuple[int, ...]: + """Return the possible output grades of a homogeneous geometric product.""" + low = abs(int(left_grade) - int(right_grade)) + high = min(int(left_grade) + int(right_grade), 2 * n - int(left_grade) - int(right_grade)) + return tuple(range(low, high + 1, 2)) + + +def expand_output_grades( + left_grades: Iterable[int], + right_grades: Iterable[int], + n: int, + *, + op: GradeProductOp = "gp", + project_grades: Optional[Iterable[int]] = None, +) -> tuple[int, ...]: + """Expand input grade sets into output grades required by ``op``.""" + left = normalize_grades(left_grades, n, name="left_grades") + right = normalize_grades(right_grades, n, name="right_grades") + if op not in {"gp", "wedge", "inner", "commutator", "anti_commutator"}: + raise ValueError(f"Unsupported grade product op {op!r}") + + outputs: set[int] = set() + for left_grade in left: + for right_grade in right: + if op == "wedge": + grade = left_grade + right_grade + if grade <= n: + outputs.add(grade) + else: + outputs.update(geometric_product_output_grades(left_grade, right_grade, n)) + + if project_grades is not None: + outputs &= set(normalize_grades(project_grades, n, name="project_grades")) + if not outputs: + raise ValueError( + f"Grade expansion is empty for op={op!r}, left_grades={left}, right_grades={right}, " + f"project_grades={None if project_grades is None else tuple(project_grades)}" + ) + return tuple(sorted(outputs)) + + +def basis_product(index_a: int, index_b: int, p: int, q: int, r: int) -> tuple[int, float]: + """Return ``(index, sign)`` for two canonical basis blade products.""" + n = p + q + r + swap_count = 0 + for bit in range(n): + if index_a & (1 << bit): + swap_count += (index_b & ((1 << bit) - 1)).bit_count() + + sign = -1.0 if swap_count % 2 else 1.0 + + negative_mask = sum(1 << bit for bit in range(p, p + q)) + if ((index_a & index_b & negative_mask).bit_count() % 2) == 1: + sign = -sign + + null_mask = sum(1 << bit for bit in range(p + q, n)) + if (index_a & index_b & null_mask) != 0: + sign = 0.0 + + return index_a ^ index_b, sign + + +def reverse_sign(index: int) -> float: + """Return the reversion sign for a canonical basis blade.""" + grade = int(index).bit_count() + return -1.0 if ((grade * (grade - 1) // 2) % 2) else 1.0 + + +def operation_coefficient(index_a: int, index_b: int, p: int, q: int, r: int, op: GradeProductOp) -> float: + """Return the scalar coefficient multiplying ``A_i * B_j`` for ``op``.""" + _, sign_ab = basis_product(index_a, index_b, p, q, r) + if op == "gp": + return sign_ab + + _, sign_ba = basis_product(index_b, index_a, p, q, r) + if op == "wedge": + return 0.5 * (sign_ab - sign_ba) + if op == "inner": + return 0.5 * (sign_ab + sign_ba) + if op == "commutator": + return sign_ab - sign_ba + if op == "anti_commutator": + return sign_ab + sign_ba + raise ValueError(f"Unsupported grade product op {op!r}") diff --git a/core/device.py b/core/foundation/device.py similarity index 100% rename from core/device.py rename to core/foundation/device.py diff --git a/core/foundation/layout.py b/core/foundation/layout.py new file mode 100644 index 0000000..7a13fe9 --- /dev/null +++ b/core/foundation/layout.py @@ -0,0 +1,99 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Static algebra and compact grade-layout value objects.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Iterable + +import torch + +from core.foundation.basis import basis_index_tuple_for_grades, normalize_grades + + +@dataclass(frozen=True) +class AlgebraSpec: + """Immutable Clifford signature metadata used by grade planners.""" + + p: int + q: int = 0 + r: int = 0 + + def __post_init__(self) -> None: + if self.p < 0 or self.q < 0 or self.r < 0: + raise ValueError(f"signature counts must be non-negative, got Cl({self.p},{self.q},{self.r})") + + @classmethod + def from_algebra(cls, algebra) -> "AlgebraSpec": + """Build a spec from any algebra-like object with ``p``, ``q``, and ``r`` attributes.""" + return cls(int(algebra.p), int(algebra.q), int(algebra.r)) + + @property + def n(self) -> int: + """Number of basis vectors.""" + return self.p + self.q + self.r + + @property + def dim(self) -> int: + """Number of canonical basis blades.""" + return 1 << self.n + + def layout(self, grades: Iterable[int]) -> "GradeLayout": + """Return a compact layout for ``grades``.""" + return GradeLayout(self, normalize_grades(grades, self.n)) + + +@dataclass(frozen=True) +class GradeLayout: + """Compact basis-lane layout for a fixed grade set.""" + + spec: AlgebraSpec + grades: tuple[int, ...] + _basis_indices: tuple[int, ...] = field(init=False, repr=False) + + def __post_init__(self) -> None: + grades = normalize_grades(self.grades, self.spec.n) + object.__setattr__(self, "grades", grades) + object.__setattr__(self, "_basis_indices", basis_index_tuple_for_grades(self.spec.n, grades)) + + @property + def basis_indices(self) -> tuple[int, ...]: + """Canonical dense basis indices represented by this compact layout.""" + return self._basis_indices + + @property + def dim(self) -> int: + """Number of compact lanes.""" + return len(self.basis_indices) + + @property + def dense_dim(self) -> int: + """Full dense multivector dimension.""" + return self.spec.dim + + def contains_grade(self, grade: int) -> bool: + """Return whether ``grade`` is present in this layout.""" + return int(grade) in self.grades + + def indices_tensor(self, *, device=None) -> torch.Tensor: + """Return basis indices as a tensor on ``device``.""" + return torch.tensor(self.basis_indices, dtype=torch.long, device=device) + + def compact(self, dense: torch.Tensor) -> torch.Tensor: + """Gather compact lanes from a dense multivector tensor.""" + if dense.shape[-1] != self.dense_dim: + raise ValueError(f"dense last dimension must be {self.dense_dim}, got {dense.shape[-1]}") + return torch.index_select(dense, -1, self.indices_tensor(device=dense.device)) + + def dense(self, values: torch.Tensor) -> torch.Tensor: + """Materialize compact lane values into a dense multivector tensor.""" + if values.shape[-1] != self.dim: + raise ValueError(f"values last dimension must be {self.dim}, got {values.shape[-1]}") + output = values.new_zeros(*values.shape[:-1], self.dense_dim) + return output.index_copy(-1, self.indices_tensor(device=values.device), values) diff --git a/core/module.py b/core/foundation/module.py similarity index 96% rename from core/module.py rename to core/foundation/module.py index 73ee8b3..c87d5c7 100644 --- a/core/module.py +++ b/core/foundation/module.py @@ -15,7 +15,7 @@ @runtime_checkable class AlgebraLike(Protocol): - """Protocol implemented by dense and partitioned Clifford kernels.""" + """Protocol implemented by dense kernels and planned algebra contexts.""" p: int q: int diff --git a/core/validation.py b/core/foundation/validation.py similarity index 100% rename from core/validation.py rename to core/foundation/validation.py diff --git a/core/partitioned_algebra.py b/core/partitioned_algebra.py deleted file mode 100644 index 7c5768a..0000000 --- a/core/partitioned_algebra.py +++ /dev/null @@ -1,2003 +0,0 @@ -# Versor: Universal Geometric Algebra Neural Network -# Copyright (C) 2026 Eunkyum Kim -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# - -"""Recursive Clifford algebra kernel for high-dimensional products. - -The dense :class:`core.algebra.CliffordAlgebra` stores a full Cayley table and -is therefore practical only for small dimensions. ``PartitionedCliffordAlgebra`` -keeps that dense implementation as the leaf kernel, while internal nodes factor -the basis into left and right sub-algebras: - -``split_index = (left_index << right_n) | right_index``. - -The public basis order is always the canonical bitmask order used by -``CliffordAlgebra``. Some recursive splits use a different internal bit order so -that repeated signature tiles, such as two copies of ``Cl(2,1,1)``, share a -single sub-algebra object. ``_BasisPermutation`` is the only place that converts -between public coefficients and that split-local coefficient order. -""" - -import math -from dataclasses import dataclass -from math import gcd -from typing import Optional, Sequence - -import torch -import torch.nn as nn - -from core.algebra import CliffordAlgebra -from core.validation import check_multivector - -DEFAULT_PARTITION_LEAF_N = 6 -MAX_PARTITIONED_DIMENSIONS = 16 -_DEFAULT_PRODUCT_CHUNK_SIZE = 64 - - -@dataclass(frozen=True) -class _Signature: - """Small immutable signature value used by split planning helpers.""" - - p: int - q: int - r: int - - @property - def n(self) -> int: - return self.p + self.q + self.r - - def as_tuple(self) -> tuple[int, int, int]: - return self.p, self.q, self.r - - def subtract(self, other: "_Signature") -> "_Signature": - return _Signature(self.p - other.p, self.q - other.q, self.r - other.r) - - def scaled_tile(self, tile_count: int, selected_tiles: int) -> "_Signature": - return _Signature( - self.p // tile_count * selected_tiles, - self.q // tile_count * selected_tiles, - self.r // tile_count * selected_tiles, - ) - - -def _signature_for_range(p: int, q: int, r: int, start: int, width: int) -> tuple[int, int, int]: - """Return ``(p, q, r)`` counts covered by a contiguous public bit range.""" - end = start + width - p_count = max(0, min(end, p) - start) - q_count = max(0, min(end, p + q) - max(start, p)) - r_count = max(0, min(end, p + q + r) - max(start, p + q)) - assert p_count + q_count + r_count == width - return p_count, q_count, r_count - - -def _signature_gcd(p: int, q: int, r: int) -> int: - """Return the greatest common divisor across nonzero signature blocks. - - A value larger than one means the signature can be represented as repeated - copies of a smaller signature tile. Example: ``Cl(8,4,4)`` has gcd 4, so it - can be split into repeated ``Cl(2,1,1)`` tiles. - """ - counts = [count for count in (p, q, r) if count > 0] - if not counts: - return 1 - - result = counts[0] - for count in counts[1:]: - result = gcd(result, count) - return result - - -def _signature_prefix_dims_for_split( - signature: _Signature, - right_signature: _Signature, -) -> tuple[tuple[int, ...], tuple[int, ...]]: - """Return right/left public bit positions for a prefix-by-signature split.""" - right_dims, left_dims = _signature_prefix_dims( - signature.p, - signature.q, - signature.r, - right_signature.p, - right_signature.q, - right_signature.r, - ) - return tuple(right_dims), tuple(left_dims) - - -def _signature_prefix_dims( - p: int, - q: int, - r: int, - p_count: int, - q_count: int, - r_count: int, -) -> tuple[list[int], list[int]]: - """Return selected and remaining public bit positions by signature block. - - The selected bits are prefixes of each signature block: first positive - dimensions, then negative dimensions, then null dimensions. This preserves - the local ``(p, q, r)`` order inside a tiled child while still letting the - child draw dimensions from non-contiguous public bit positions. - """ - assert 0 <= p_count <= p - assert 0 <= q_count <= q - assert 0 <= r_count <= r - - selected = list(range(0, p_count)) + list(range(p, p + q_count)) + list(range(p + q, p + q + r_count)) - remaining = list(range(p_count, p)) + list(range(p + q_count, p + q)) + list(range(p + q + r_count, p + q + r)) - - return selected, remaining - - -@dataclass(frozen=True) -class _PartitionSplit: - """Child signatures and public bit positions for one recursive split. - - ``right_dims`` become the low split-local bits and ``left_dims`` become the - high split-local bits. The order is important because recursive products use - ``(left_index << right_n) | right_index`` after converting into split order. - """ - - right_signature: tuple[int, int, int] - left_signature: tuple[int, int, int] - right_dims: tuple[int, ...] - left_dims: tuple[int, ...] - - @property - def split_dims(self) -> tuple[int, ...]: - """Return public bit positions in split-order bit layout.""" - return self.right_dims + self.left_dims - - -@dataclass(frozen=True) -class _ExpSettings: - """Resolved exponential dispatch settings for one algebra signature.""" - - regime: str - policy: object - fixed_iterations: int - - -@dataclass(frozen=True) -class _StructuralBuffers: - """Purely generated buffers plus dtype-derived scalar tolerances.""" - - buffers: tuple[tuple[str, torch.Tensor], ...] - eps: float - eps_sq: float - - -@dataclass(frozen=True) -class _ProductPlan: - """Runtime product chunk plan for one recursive node.""" - - right_pair_count: int - chunk_size: int - - -@dataclass(frozen=True) -class _RightProductSlice: - """Runtime routing tensors for one range of right-basis products.""" - - right_a_indices: torch.Tensor - right_b_indices: torch.Tensor - right_result_indices: torch.Tensor - right_product_signs: torch.Tensor - - def __iter__(self): - """Preserve tuple-unpack compatibility for older private tests.""" - yield self.right_a_indices - yield self.right_b_indices - yield self.right_result_indices - yield self.right_product_signs - - -@dataclass(frozen=True) -class _PartitionTreeSpec: - """Validated relative split tree for one partitioned node.""" - - right_dims: tuple[int, ...] - left_dims: tuple[int, ...] - right: Optional["_PartitionTreeSpec"] = None - left: Optional["_PartitionTreeSpec"] = None - - @property - def split_dims(self) -> tuple[int, ...]: - return self.right_dims + self.left_dims - - def fingerprint(self) -> tuple: - """Return a relative structural fingerprint for safe subalgebra reuse.""" - return ( - self.right_dims, - None if self.right is None else self.right.fingerprint(), - self.left_dims, - None if self.left is None else self.left.fingerprint(), - ) - - -def _partition_split(p: int, q: int, r: int) -> _PartitionSplit: - """Return the single recursive split used by the partitioned algebra. - - Repeated signature tiles are grouped first so common sub-algebras have the - same local signature and can share one module instance. Signatures without - repeatable tiles fall back to a balanced contiguous split. - """ - signature = _Signature(p, q, r) - tiled_split = _tiled_signature_split(signature) - if tiled_split is not None: - return tiled_split - - return _balanced_contiguous_split(signature) - - -def _tiled_signature_split(signature: _Signature) -> Optional[_PartitionSplit]: - """Split repeated signature tiles so identical child trees can be shared.""" - tile_count = _signature_gcd(*signature.as_tuple()) - if tile_count <= 1: - return None - - right_tile_count = tile_count // 2 - right_signature = signature.scaled_tile(tile_count, right_tile_count) - left_signature = signature.subtract(right_signature) - right_dims, left_dims = _signature_prefix_dims_for_split(signature, right_signature) - - return _build_partition_split( - right_signature=right_signature, - left_signature=left_signature, - right_dims=right_dims, - left_dims=left_dims, - ) - - -def _balanced_contiguous_split(signature: _Signature) -> _PartitionSplit: - """Split a signature into balanced contiguous public bit ranges.""" - right_width = signature.n // 2 - left_width = signature.n - right_width - right_signature = _Signature(*_signature_for_range(*signature.as_tuple(), 0, right_width)) - left_signature = _Signature(*_signature_for_range(*signature.as_tuple(), right_width, left_width)) - right_dims = tuple(range(right_width)) - left_dims = tuple(range(right_width, signature.n)) - - return _build_partition_split( - right_signature=right_signature, - left_signature=left_signature, - right_dims=right_dims, - left_dims=left_dims, - ) - - -def _build_partition_split( - *, - right_signature: _Signature, - left_signature: _Signature, - right_dims: tuple[int, ...], - left_dims: tuple[int, ...], -) -> _PartitionSplit: - """Create a validated split plan.""" - assert right_signature.n == len(right_dims) - assert left_signature.n == len(left_dims) - return _PartitionSplit( - right_signature=right_signature.as_tuple(), - left_signature=left_signature.as_tuple(), - right_dims=right_dims, - left_dims=left_dims, - ) - - -def _partition_tree_fingerprint(partition_tree: Optional[_PartitionTreeSpec]) -> object: - """Return the cache fingerprint for a child partition tree.""" - return "auto" if partition_tree is None else partition_tree.fingerprint() - - -def _split_from_tree_spec(p: int, q: int, r: int, tree: _PartitionTreeSpec) -> _PartitionSplit: - """Create a concrete split from an explicit relative tree node.""" - assert sorted(tree.split_dims) == list(range(p + q + r)) - right_signature = _signature_for_dims(p, q, r, tree.right_dims) - left_signature = _signature_for_dims(p, q, r, tree.left_dims) - return _PartitionSplit( - right_signature=right_signature, - left_signature=left_signature, - right_dims=tree.right_dims, - left_dims=tree.left_dims, - ) - - -def _signature_for_dims(p: int, q: int, r: int, dims: Sequence[int]) -> tuple[int, int, int]: - """Return child signature counts for selected local basis-vector dims.""" - n = p + q + r - p_count = 0 - q_count = 0 - r_count = 0 - for dim in dims: - if dim < 0 or dim >= n: - raise ValueError(f"Partition dim {dim} is outside [0, {n})") - if dim < p: - p_count += 1 - elif dim < p + q: - q_count += 1 - else: - r_count += 1 - return p_count, q_count, r_count - - -def _normalize_partition_tree( - partition_tree, - p: int, - q: int, - r: int, -) -> Optional[_PartitionTreeSpec]: - """Resolve a public partition tree expression into a relative tree spec.""" - if partition_tree is None: - return None - if isinstance(partition_tree, _PartitionTreeSpec): - return partition_tree - if isinstance(partition_tree, str): - expression = partition_tree.strip() - if not expression or expression.lower() in {"auto", "none", "null"}: - return None - assignments = _parse_partition_tree_expression(expression, p + q + r) - root_dims = _canonical_global_dims(tuple(range(p + q + r)), p, q, r) - return _build_partition_tree_from_assignments(assignments, (), root_dims, p, q, r) - raise TypeError( - "partition_tree must be None, 'auto', a path expression string, " - f"or an internal _PartitionTreeSpec, got {type(partition_tree).__name__}" - ) - - -def _parse_partition_tree_expression(expression: str, n: int) -> dict[tuple[str, ...], tuple[int, ...]]: - """Parse ``R=0-3; L.R=4-7`` style partition expressions.""" - assignments: dict[tuple[str, ...], tuple[int, ...]] = {} - used_dims: dict[int, tuple[str, ...]] = {} - - for raw_entry in expression.split(";"): - entry = raw_entry.strip() - if not entry: - continue - if "=" not in entry: - raise ValueError(f"Invalid partition tree entry {entry!r}; expected PATH=DIMS") - raw_path, raw_dims = entry.split("=", 1) - path = _parse_partition_path(raw_path) - dims = _parse_dim_expression(raw_dims, n) - if path in assignments: - raise ValueError(f"Duplicate partition path {'.'.join(path)}") - for dim in dims: - if dim in used_dims: - previous = ".".join(used_dims[dim]) - current = ".".join(path) - raise ValueError(f"Partition dim {dim} appears in both {previous} and {current}") - used_dims[dim] = path - assignments[path] = dims - - if not assignments: - raise ValueError("Partition tree expression did not contain any PATH=DIMS entries") - - expected_dims = set(range(n)) - actual_dims = set(used_dims) - if actual_dims != expected_dims: - missing = sorted(expected_dims - actual_dims) - extra = sorted(actual_dims - expected_dims) - raise ValueError(f"Partition tree must cover every dimension exactly once; missing={missing}, extra={extra}") - - return assignments - - -def _parse_partition_path(raw_path: str) -> tuple[str, ...]: - """Parse a tree path made of ``L`` and ``R`` segments.""" - path = tuple(segment.strip().upper() for segment in raw_path.strip().split(".") if segment.strip()) - if not path: - raise ValueError("Partition path cannot be empty") - invalid = [segment for segment in path if segment not in {"L", "R"}] - if invalid: - raise ValueError(f"Invalid partition path segment(s): {invalid}; use only L and R") - return path - - -def _parse_dim_expression(raw_dims: str, n: int) -> tuple[int, ...]: - """Parse comma-separated dimensions and inclusive ranges.""" - dims: list[int] = [] - for raw_token in raw_dims.split(","): - token = raw_token.strip() - if not token: - continue - if "-" in token: - raw_start, raw_end = token.split("-", 1) - start = int(raw_start.strip()) - end = int(raw_end.strip()) - if end < start: - raise ValueError(f"Invalid descending partition range {token!r}") - dims.extend(range(start, end + 1)) - else: - dims.append(int(token)) - - if not dims: - raise ValueError("Partition dimension expression cannot be empty") - - unique_dims = tuple(dict.fromkeys(dims)) - if len(unique_dims) != len(dims): - raise ValueError(f"Partition dimension expression contains duplicates: {raw_dims!r}") - for dim in unique_dims: - if dim < 0 or dim >= n: - raise ValueError(f"Partition dim {dim} is outside [0, {n})") - return unique_dims - - -def _build_partition_tree_from_assignments( - assignments: dict[tuple[str, ...], tuple[int, ...]], - path: tuple[str, ...], - node_global_dims: tuple[int, ...], - p: int, - q: int, - r: int, -) -> _PartitionTreeSpec: - """Build a relative tree spec for one node from global path assignments.""" - right_global_dims = _assigned_dims_under(assignments, path + ("R",)) - left_global_dims = _assigned_dims_under(assignments, path + ("L",)) - - if not right_global_dims or not left_global_dims: - label = "root" if not path else ".".join(path) - raise ValueError(f"Partition node {label} must define both L and R children") - - node_dim_set = set(node_global_dims) - if set(right_global_dims) | set(left_global_dims) != node_dim_set: - label = "root" if not path else ".".join(path) - raise ValueError(f"Partition node {label} children must cover exactly its parent dimensions") - if set(right_global_dims) & set(left_global_dims): - label = "root" if not path else ".".join(path) - raise ValueError(f"Partition node {label} has overlapping L/R dimensions") - - right_child_global_dims = _canonical_global_dims(right_global_dims, p, q, r) - left_child_global_dims = _canonical_global_dims(left_global_dims, p, q, r) - local_index = {dim: index for index, dim in enumerate(node_global_dims)} - right_dims = tuple(local_index[dim] for dim in right_child_global_dims) - left_dims = tuple(local_index[dim] for dim in left_child_global_dims) - - right_tree = None - left_tree = None - if _has_descendant_assignment(assignments, path + ("R",)): - right_tree = _build_partition_tree_from_assignments( - assignments, - path + ("R",), - right_child_global_dims, - p, - q, - r, - ) - if _has_descendant_assignment(assignments, path + ("L",)): - left_tree = _build_partition_tree_from_assignments( - assignments, - path + ("L",), - left_child_global_dims, - p, - q, - r, - ) - - return _PartitionTreeSpec(right_dims=right_dims, left_dims=left_dims, right=right_tree, left=left_tree) - - -def _assigned_dims_under( - assignments: dict[tuple[str, ...], tuple[int, ...]], - path: tuple[str, ...], -) -> tuple[int, ...]: - """Return all globally assigned dims under a path.""" - dims: list[int] = [] - for assigned_path, assigned_dims in assignments.items(): - if assigned_path[: len(path)] == path: - dims.extend(assigned_dims) - return tuple(dims) - - -def _has_descendant_assignment( - assignments: dict[tuple[str, ...], tuple[int, ...]], - path: tuple[str, ...], -) -> bool: - """Whether a path has deeper assignments than itself.""" - return any(len(assigned_path) > len(path) and assigned_path[: len(path)] == path for assigned_path in assignments) - - -def _canonical_global_dims(dims: Sequence[int], p: int, q: int, r: int) -> tuple[int, ...]: - """Order global dimensions by local Clifford signature convention.""" - n = p + q + r - dim_set = set(dims) - return tuple(dim for dim in range(n) if dim in dim_set) - - -def _grade_index(n: int, device) -> torch.Tensor: - """Return the grade, i.e. popcount, for basis indices ``0..2**n-1``.""" - basis_indices = torch.arange(2**n, dtype=torch.long, device=device) - grades = torch.zeros_like(basis_indices) - remaining_bits = basis_indices - for _ in range(n): - grades += remaining_bits & 1 - remaining_bits = remaining_bits >> 1 - return grades - - -def _bit_range_mask(start: int, end: int) -> int: - """Return an integer bit mask covering ``[start, end)``.""" - mask = 0 - for bit in range(start, end): - mask |= 1 << bit - return mask - - -def _vector_square(bit: int, p: int, q: int) -> float: - """Return the metric square of one basis vector.""" - if bit < p: - return 1.0 - if bit < p + q: - return -1.0 - return 0.0 - - -def _resolve_exp_settings( - p: int, - q: int, - r: int, - dtype: torch.dtype, - exp_policy, - fixed_iterations: Optional[int], -) -> _ExpSettings: - """Resolve signature-wide exponential policy without mutating a module.""" - if p == 0 or q == 0: - regime = "elliptic" - elif p == 1 and q == 1 and r == 0: - regime = "hyperbolic" - else: - regime = "mixed" - - from core.decomposition import ExpPolicy, resolve_fixed_iterations - - policy = exp_policy if isinstance(exp_policy, ExpPolicy) else ExpPolicy(exp_policy) - iterations = ( - int(fixed_iterations) if fixed_iterations is not None else resolve_fixed_iterations(policy, dtype, p + q + r) - ) - return _ExpSettings(regime=regime, policy=policy, fixed_iterations=iterations) - - -def _default_product_chunk_size(pair_count: int) -> int: - """Choose a memory-conscious right-pair chunk size.""" - return max(1, min(pair_count, _DEFAULT_PRODUCT_CHUNK_SIZE)) - - -def _right_pair_count(right_n: int, right_r: int) -> int: - """Return the number of right basis pairs that survive the null metric.""" - non_null_n = right_n - right_r - return (4**non_null_n) * (3**right_r) - - -def _product_plan(right_n: int, right_r: int, requested_chunk_size: Optional[int]) -> _ProductPlan: - """Return the recursive product plan without allocating routing tables.""" - right_pair_count = _right_pair_count(right_n, right_r) - chunk_size = ( - _default_product_chunk_size(right_pair_count) - if requested_chunk_size is None - else max(1, int(requested_chunk_size)) - ) - return _ProductPlan(right_pair_count=right_pair_count, chunk_size=chunk_size) - - -def _product_pair_ranges(pair_count: int, chunk_size: int): - """Yield static right-pair ranges for recursive product chunks.""" - for start in range(0, pair_count, chunk_size): - yield start, min(start + chunk_size, pair_count) - - -def _compact_surviving_basis_pairs( - compact_pair_indices: torch.Tensor, - n: int, - p: int, - q: int, - r: int, -) -> tuple[torch.Tensor, torch.Tensor]: - """Map compact mixed-radix pair indices to surviving basis-product pairs. - - Non-null basis-vector bits have four states: absent, left only, right only, - or both. Null bits have only three active states because the ``both`` case - squares a null vector and annihilates the product. - """ - right_a_indices = torch.zeros_like(compact_pair_indices) - right_b_indices = torch.zeros_like(compact_pair_indices) - quotient = compact_pair_indices - non_null_n = p + q - - for bit in range(n): - if bit < non_null_n: - digit = quotient.remainder(4) - quotient = torch.div(quotient, 4, rounding_mode="floor") - right_a_indices = right_a_indices | ((digit & 1) << bit) - right_b_indices = right_b_indices | (((digit >> 1) & 1) << bit) - else: - digit = quotient.remainder(3) - quotient = torch.div(quotient, 3, rounding_mode="floor") - right_a_indices = right_a_indices | ((digit == 1).to(torch.long) << bit) - right_b_indices = right_b_indices | ((digit == 2).to(torch.long) << bit) - - assert r == n - non_null_n - return right_a_indices, right_b_indices - - -def _subalgebra_cache_key( - p: int, - q: int, - r: int, - device, - dtype: torch.dtype, - leaf_n: int, - product_chunk_size: Optional[int], - exp_policy, - fixed_iterations: int, - accumulation_dtype: Optional[torch.dtype], - partition_tree: Optional[_PartitionTreeSpec], -) -> tuple: - """Return the per-tree cache key for structurally identical sub-algebras.""" - return ( - p, - q, - r, - str(torch.device(device)), - str(dtype), - leaf_n, - product_chunk_size, - getattr(exp_policy, "value", str(exp_policy)), - fixed_iterations, - str(accumulation_dtype), - _partition_tree_fingerprint(partition_tree), - ) - - -def _basis_product_signs( - indices_a: torch.Tensor, - indices_b: torch.Tensor, - p: int, - q: int, - r: int, - dtype: torch.dtype, - popcount: Optional[torch.Tensor] = None, -) -> torch.Tensor: - """Return basis-product signs for equal-shaped bitmask index tensors. - - The output is the scalar coefficient of ``e_indices_a * e_indices_b`` before - the XOR result index is applied. Positive dimensions contribute ``+1``, - negative dimensions contribute ``-1`` when repeated, and null dimensions - annihilate products that repeat the same null basis vector. - """ - n = p + q + r - if popcount is None: - popcount = _grade_index(n, indices_a.device) - - # Reordering ``A`` basis vectors past lower-numbered ``B`` basis vectors - # gives the anticommutation sign. - swap_counts = torch.zeros_like(indices_a) - for bit in range(n): - a_bit = (indices_a >> bit) & 1 - lower_bits = indices_b & ((1 << bit) - 1) - swap_counts += a_bit * popcount[lower_bits] - - sign = torch.where( - swap_counts % 2 == 0, - torch.ones((), dtype=dtype, device=indices_a.device), - -torch.ones((), dtype=dtype, device=indices_a.device), - ) - - # Repeated negative basis vectors square to -1. - negative_mask = _bit_range_mask(p, p + q) - negative_intersection = indices_a & indices_b & negative_mask - negative_count = popcount[negative_intersection] - sign = torch.where(negative_count % 2 == 0, sign, -sign) - - if r > 0: - # Repeated null basis vectors square to 0, annihilating the term. - null_mask = _bit_range_mask(p + q, n) - sign = torch.where((indices_a & indices_b & null_mask) == 0, sign, torch.zeros_like(sign)) - - return sign - - -def _involution_buffers( - grade_index: torch.Tensor, - dtype: torch.dtype, - device, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - """Return reversion, grade-involution, and Clifford-conjugation signs.""" - rev_signs = ((-1.0) ** (grade_index * (grade_index - 1) // 2)).to(dtype=dtype) - involution_signs = torch.where( - grade_index % 2 == 0, - torch.ones((), dtype=dtype, device=device), - -torch.ones((), dtype=dtype, device=device), - ) - conj_signs = (involution_signs * rev_signs).to(dtype=dtype) - return rev_signs, involution_signs, conj_signs - - -def _basis_square_metric_signs( - basis_indices: torch.Tensor, - grade_index: torch.Tensor, - p: int, - q: int, - r: int, - dtype: torch.dtype, - device, -) -> torch.Tensor: - """Return the metric-only part of ``e_I * e_I`` for every basis blade.""" - negative_mask = _bit_range_mask(p, p + q) - negative_count = grade_index[basis_indices & negative_mask] - metric_signs = torch.where( - negative_count % 2 == 0, - torch.ones((), dtype=dtype, device=device), - -torch.ones((), dtype=dtype, device=device), - ) - - if r > 0: - null_mask = _bit_range_mask(p + q, p + q + r) - metric_signs = torch.where( - (basis_indices & null_mask) == 0, - metric_signs, - torch.zeros_like(metric_signs), - ) - - return metric_signs - - -def _pseudoscalar_buffers( - basis_indices: torch.Tensor, - p: int, - q: int, - r: int, - dtype: torch.dtype, - popcount: torch.Tensor, -) -> tuple[torch.Tensor, torch.Tensor]: - """Return source permutation and signs for right multiplication by ``I``.""" - pseudoscalar_index = basis_indices.numel() - 1 - ps_source = basis_indices ^ pseudoscalar_index - ps_target = torch.full_like(ps_source, pseudoscalar_index) - ps_signs = _basis_product_signs(ps_source, ps_target, p, q, r, dtype, popcount=popcount) - return ps_source, ps_signs - - -def _bivector_buffers( - n: int, - p: int, - q: int, - grade_index: torch.Tensor, - dtype: torch.dtype, - device, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - """Return bivector indices, squared scalars, and right-contraction action.""" - if n < 2: - return ( - torch.zeros(0, dtype=torch.long, device=device), - torch.zeros(0, dtype=dtype, device=device), - torch.zeros(0, n, n, dtype=dtype, device=device), - ) - - bivector_indices = [blade_index for blade_index in range(1 << n) if blade_index.bit_count() == 2] - bv_indices = torch.tensor(bivector_indices, dtype=torch.long, device=device) - bv_sq_scalar = torch.zeros(len(bv_indices), dtype=dtype, device=device) - rc_action = torch.zeros(len(bv_indices), n, n, dtype=dtype, device=device) - - for bivector_position, blade_index in enumerate(bv_indices.tolist()): - active_bits = [bit for bit in range(n) if blade_index & (1 << bit)] - if len(active_bits) != 2: - continue - first_bit, second_bit = active_bits - first_square = _vector_square(first_bit, p, q) - second_square = _vector_square(second_bit, p, q) - bv_sq_scalar[bivector_position] = -first_square * second_square - rc_action[bivector_position, first_bit, second_bit] = second_square - rc_action[bivector_position, second_bit, first_bit] = -first_square - - return bv_indices, bv_sq_scalar, rc_action - - -def _left_contraction_grade_buffers(n: int, device) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - """Return compact grade-pair dispatch vectors for left contraction.""" - grade_pairs = [ - (grade_a, grade_b, grade_b - grade_a) for grade_a in range(n + 1) for grade_b in range(grade_a, n + 1) - ] - lc_grade_a, lc_grade_b, lc_grade_result = zip(*grade_pairs) - return ( - torch.tensor(lc_grade_a, dtype=torch.long, device=device), - torch.tensor(lc_grade_b, dtype=torch.long, device=device), - torch.tensor(lc_grade_result, dtype=torch.long, device=device), - ) - - -def _product_pair_weights(dtype: torch.dtype, device) -> torch.Tensor: - """Return AB/BA linear-combination weights for common binary products.""" - return torch.tensor( - [ - [0.5, -0.5], - [0.5, 0.5], - [1.0, -1.0], - [1.0, 1.0], - ], - dtype=dtype, - device=device, - ) - - -def _structural_buffers(p: int, q: int, r: int, device, dtype: torch.dtype) -> _StructuralBuffers: - """Build all linear-size structural tensors without mutating a module.""" - n = p + q + r - dim = 2**n - basis_indices = torch.arange(dim, dtype=torch.long, device=device) - grade_index = _grade_index(n, device) - grade_values = torch.arange(n + 1, dtype=torch.long, device=device) - - rev_signs, involution_signs, conj_signs = _involution_buffers(grade_index, dtype, device) - metric_signs = _basis_square_metric_signs(basis_indices, grade_index, p, q, r, dtype, device) - cayley_diag = rev_signs * metric_signs - ps_source, ps_signs = _pseudoscalar_buffers(basis_indices, p, q, r, dtype, grade_index) - bv_indices, bv_sq_scalar, rc_action = _bivector_buffers(n, p, q, grade_index, dtype, device) - lc_grade_a, lc_grade_b, lc_grade_result = _left_contraction_grade_buffers(n, device) - g1_indices = (1 << torch.arange(n, device=device)).long() - - finfo = torch.finfo(dtype) - return _StructuralBuffers( - buffers=( - ("grade_index", grade_index), - ("_grade_values", grade_values), - ("rev_signs", rev_signs), - ("_involution_signs", involution_signs), - ("conj_signs", conj_signs), - ("_cayley_diag", cayley_diag), - ("_norm_sq_signs", (rev_signs * cayley_diag).clone()), - ("_hermitian_signs", (conj_signs * cayley_diag).clone()), - ("_ps_source", ps_source), - ("_ps_signs", ps_signs), - ("_bv_indices", bv_indices), - ("bv_sq_scalar", bv_sq_scalar), - ("rc_action", rc_action), - ("_g1_indices", g1_indices), - ("_lc_grade_a", lc_grade_a), - ("_lc_grade_b", lc_grade_b), - ("_lc_grade_result", lc_grade_result), - ("_product_pair_weights", _product_pair_weights(dtype, device)), - ), - eps=float(finfo.eps), - eps_sq=float(finfo.eps**2), - ) - - -class _BasisPermutation(nn.Module): - """Convert coefficients between public canonical order and split-local order. - - ``split_dims[split_bit]`` tells which public basis-vector bit occupies that - split-local bit position. For contiguous balanced splits this is the identity - mapping and all buffers stay empty. For tiled splits, bit positions are - permuted so identical child signatures can share sub-algebra modules. - - Coefficients need signs as well as index permutations. A basis blade stores - an ordered wedge of basis vectors; permuting vector dimensions changes that - orientation by ``(-1) ** inversion_count``. - """ - - def __init__(self, split_dims: Sequence[int], device): - super().__init__() - self.split_dims = tuple(split_dims) - self.n = len(self.split_dims) - self.dim = 2**self.n - self.uses_permutation = self.split_dims != tuple(range(self.n)) - - if not self.uses_permutation: - # Keep the identity case allocation-free in the hot path. Empty - # buffers preserve old introspection behavior and still move with - # ``module.to(device)``. - empty = torch.empty(0, dtype=torch.long, device=device) - self.register_buffer("split_to_public", empty, persistent=False) - self.register_buffer("public_to_split", empty, persistent=False) - self.register_buffer("split_signs", torch.empty(0, dtype=torch.int8, device=device), persistent=False) - return - - split_to_public_indices = [] - split_orientation_signs = [] - for split_index in range(self.dim): - public_index, orientation_sign = self._split_basis_term(split_index) - split_to_public_indices.append(public_index) - split_orientation_signs.append(orientation_sign) - - split_to_public = torch.tensor(split_to_public_indices, dtype=torch.long, device=device) - public_to_split = torch.empty_like(split_to_public) - public_to_split[split_to_public] = torch.arange(self.dim, dtype=torch.long, device=device) - split_signs = torch.tensor(split_orientation_signs, dtype=torch.int8, device=device) - - self.register_buffer("split_to_public", split_to_public, persistent=False) - self.register_buffer("public_to_split", public_to_split, persistent=False) - self.register_buffer("split_signs", split_signs, persistent=False) - - def _split_basis_term(self, split_index: int) -> tuple[int, int]: - """Return ``(public_index, orientation_sign)`` for one split-order blade.""" - public_index = 0 - public_bits = [] - for split_bit, public_bit in enumerate(self.split_dims): - if split_index & (1 << split_bit): - public_index |= 1 << public_bit - public_bits.append(public_bit) - - # ``public_bits`` are encountered in split-local order. Count how many - # swaps are needed to rewrite the same blade in canonical public order. - inversions = 0 - for i, public_i in enumerate(public_bits): - for public_j in public_bits[i + 1 :]: - if public_i > public_j: - inversions += 1 - - sign = -1 if inversions % 2 else 1 - return public_index, sign - - def to_split_order(self, mv: torch.Tensor) -> torch.Tensor: - """Convert public canonical coefficients to split-local coefficient order. - - ``split_to_public[k]`` is the public source index for split coefficient - ``k``. Multiplying by ``split_signs[k]`` accounts for blade orientation. - """ - if not self.uses_permutation: - return mv - signs = self.split_signs.to(dtype=mv.dtype) - return torch.index_select(mv, -1, self.split_to_public) * signs - - def to_public_order(self, mv: torch.Tensor) -> torch.Tensor: - """Convert split-local coefficients back to public canonical order.""" - if not self.uses_permutation: - return mv - signs = torch.index_select(self.split_signs, 0, self.public_to_split).to(dtype=mv.dtype) - return torch.index_select(mv, -1, self.public_to_split) * signs - - -class PartitionedCliffordAlgebra(nn.Module): - """Partitioned Clifford algebra kernel using recursive tensor products. - - The basis order matches :class:`core.algebra.CliffordAlgebra`: basis blades - are indexed by bitmasks, with lower-numbered vector dimensions in lower - bits. Recursive nodes use a binary split, with the low-order internal block - on the right and the high-order internal block on the left: - - ``global_index = (left_index << right_n) | right_index``. - - With this layout, the bridge sign for basis factors is - ``(-1) ** (grade(left_A) * grade(right_B))``. Local metric signs remain in - the left and right subalgebras. - - ``accumulation_dtype`` can promote recursive product accumulation, e.g. - fp32 inputs with fp64 intermediate sums, while returning the input dtype. - - The split algorithm keeps the public basis order canonical but permutes - coefficients internally when repeated signature tiles such as ``Cl(2,1,1)`` - inside ``Cl(8,4,4)`` can share subalgebra modules. - - Args: - p (int): Positive signature dimensions. - q (int, optional): Negative signature dimensions. Defaults to 0. - r (int, optional): Null signature dimensions. Defaults to 0. - device (str or torch.device, optional): Device for generated buffers. - dtype (torch.dtype, optional): Floating-point dtype for sign buffers and - dense leaf algebras. - leaf_n (int, optional): Maximum basis-vector count handled by local - leaves. The default targets ``2**leaf_n == 64`` coefficients so - deep-learning products use small dense kernels and indexed global - merge routing. - product_chunk_size (int, optional): Number of right-basis product pairs - processed per recursive chunk. ``None`` chooses a memory-conscious - default from the node shape. - exp_policy (str or ExpPolicy, optional): Bivector exponential policy. - fixed_iterations (int, optional): Fixed iteration budget for decomposed - exponential paths. ``None`` derives it from policy, dtype, and n. - accumulation_dtype (torch.dtype, optional): Optional promoted dtype for - recursive product accumulation. - partition_tree (str, optional): Explicit split expression such as - ``"R=0-3; L.R=4-7; L.L=8-11"``. ``None`` or ``"auto"`` uses the - automatic repeated-tile/balanced splitter. - """ - - def __init__( - self, - p: int, - q: int = 0, - r: int = 0, - device="cuda", - dtype: torch.dtype = torch.float32, - leaf_n: int = DEFAULT_PARTITION_LEAF_N, - product_chunk_size: Optional[int] = None, - exp_policy: str = "balanced", - fixed_iterations: Optional[int] = None, - accumulation_dtype: Optional[torch.dtype] = None, - partition_tree=None, - _subalgebra_cache: Optional[dict] = None, - ): - super().__init__() - - assert p >= 0, f"p must be non-negative, got {p}" - assert q >= 0, f"q must be non-negative, got {q}" - assert r >= 0, f"r must be non-negative, got {r}" - assert p + q + r <= MAX_PARTITIONED_DIMENSIONS, ( - f"p + q + r must be <= {MAX_PARTITIONED_DIMENSIONS}, got {p + q + r}" - ) - assert leaf_n >= 1, f"leaf_n must be >= 1, got {leaf_n}" - - self._init_signature(p, q, r, leaf_n, product_chunk_size, accumulation_dtype) - self._init_exp_settings(dtype, exp_policy, fixed_iterations) - self._init_structural_buffers(device, dtype) - self._partition_tree = _normalize_partition_tree(partition_tree, self.p, self.q, self.r) - - if self.n <= leaf_n and self._partition_tree is None: - self._init_leaf_node(device, dtype) - return - - subalgebra_cache = {} if _subalgebra_cache is None else _subalgebra_cache - self._init_recursive_node(device, dtype, subalgebra_cache) - - def _init_signature( - self, - p: int, - q: int, - r: int, - leaf_n: int, - product_chunk_size: Optional[int], - accumulation_dtype: Optional[torch.dtype], - ) -> None: - """Store constructor inputs that define this algebra node.""" - self.p, self.q, self.r = p, q, r - self.n = p + q + r - self.dim = 2**self.n - self.leaf_n = leaf_n - self.product_chunk_size = product_chunk_size - self.accumulation_dtype = accumulation_dtype - - def _init_exp_settings(self, dtype: torch.dtype, exp_policy, fixed_iterations: Optional[int]) -> None: - """Attach resolved exponential settings to this node.""" - settings = _resolve_exp_settings( - self.p, - self.q, - self.r, - dtype, - exp_policy, - fixed_iterations, - ) - self._exp_regime = settings.regime - self._exp_policy = settings.policy - self._exp_fixed_iterations = settings.fixed_iterations - - def _init_leaf_node(self, device, dtype: torch.dtype) -> None: - """Configure a leaf node backed by the dense local Clifford kernel.""" - self.basis_permutation = _BasisPermutation(tuple(range(self.n)), device) - self.core = CliffordAlgebra( - self.p, - self.q, - self.r, - device=device, - dtype=dtype, - exp_policy=self._exp_policy, - fixed_iterations=self._exp_fixed_iterations, - ) - self.left_sub = None - self.right_sub = None - self.left_n = 0 - self.right_n = 0 - self.left_dim = 0 - self.right_dim = 0 - self._right_pair_count = 0 - self._product_chunk_size = 0 - self._right_dims = () - self._left_dims = () - - def _init_recursive_node(self, device, dtype: torch.dtype, subalgebra_cache: dict) -> None: - """Configure split layout, child modules, and runtime product planning.""" - split = ( - _partition_split(self.p, self.q, self.r) - if self._partition_tree is None - else _split_from_tree_spec(self.p, self.q, self.r, self._partition_tree) - ) - self._init_split_layout(split, device) - self.core = None - self.left_sub, self.right_sub = self._create_child_subalgebras(split, device, dtype, subalgebra_cache) - self._init_product_plan() - - def _init_split_layout(self, split: _PartitionSplit, device) -> None: - """Store recursive split shape and basis permutation.""" - assert sorted(split.split_dims) == list(range(self.n)) - - self.left_n = len(split.left_dims) - self.right_n = len(split.right_dims) - self.left_dim = 2**self.left_n - self.right_dim = 2**self.right_n - self._right_dims = split.right_dims - self._left_dims = split.left_dims - self.basis_permutation = _BasisPermutation(split.split_dims, device) - - def _create_child_subalgebras( - self, - split: _PartitionSplit, - device, - dtype: torch.dtype, - subalgebra_cache: dict, - ) -> tuple["PartitionedCliffordAlgebra", "PartitionedCliffordAlgebra"]: - """Return cached left and right child modules for a recursive node.""" - child_kwargs = { - "device": device, - "dtype": dtype, - "leaf_n": self.leaf_n, - "product_chunk_size": self.product_chunk_size, - "exp_policy": self._exp_policy, - "fixed_iterations": self._exp_fixed_iterations, - "accumulation_dtype": self.accumulation_dtype, - "subalgebra_cache": subalgebra_cache, - } - left_tree = None if self._partition_tree is None else self._partition_tree.left - right_tree = None if self._partition_tree is None else self._partition_tree.right - left_sub = self._get_or_create_subalgebra(*split.left_signature, partition_tree=left_tree, **child_kwargs) - right_sub = self._get_or_create_subalgebra(*split.right_signature, partition_tree=right_tree, **child_kwargs) - return left_sub, right_sub - - @classmethod - def _get_or_create_subalgebra( - cls, - p: int, - q: int, - r: int, - *, - device, - dtype: torch.dtype, - leaf_n: int, - product_chunk_size: Optional[int], - exp_policy, - fixed_iterations: int, - accumulation_dtype: Optional[torch.dtype], - partition_tree: Optional[_PartitionTreeSpec], - subalgebra_cache: dict, - ) -> "PartitionedCliffordAlgebra": - """Create or reuse a child with the same algebraic structure. - - The cache is per root construction call, not global. That keeps module - ownership local to one tree while still ensuring repeated logical - sub-algebras, for example the left and right ``Cl(4,2,2)`` nodes inside - ``Cl(8,4,4)``, point at the same Python module object. - """ - cache_key = _subalgebra_cache_key( - p, - q, - r, - device, - dtype, - leaf_n, - product_chunk_size, - exp_policy, - fixed_iterations, - accumulation_dtype, - partition_tree, - ) - subalgebra = subalgebra_cache.get(cache_key) - if subalgebra is None: - subalgebra = cls( - p, - q, - r, - device=device, - dtype=dtype, - leaf_n=leaf_n, - product_chunk_size=product_chunk_size, - exp_policy=exp_policy, - fixed_iterations=fixed_iterations, - accumulation_dtype=accumulation_dtype, - partition_tree=partition_tree, - _subalgebra_cache=subalgebra_cache, - ) - subalgebra_cache[cache_key] = subalgebra - return subalgebra - - def _init_structural_buffers(self, device, dtype: torch.dtype) -> None: - """Register linear-size structural tensors generated by pure builders.""" - structural = _structural_buffers(self.p, self.q, self.r, device, dtype) - self._register_structural_buffers(structural.buffers) - self.eps = structural.eps - self.eps_sq = structural.eps_sq - - def _register_structural_buffers(self, buffers: tuple[tuple[str, torch.Tensor], ...]) -> None: - """Attach generated tensors as non-persistent buffers.""" - for name, tensor in buffers: - self.register_buffer(name, tensor, persistent=False) - - def _init_product_plan(self) -> None: - """Initialize recursive product shape planning without baked routing. - - Right-pair indices, metric signs, and bridge signs are derived per - product range at runtime. This keeps the - recursive kernel from carrying signature-specific pair tables. - """ - plan = _product_plan(self.right_n, self.right_sub.r, self.product_chunk_size) - self._right_pair_count = plan.right_pair_count - self._product_chunk_size = plan.chunk_size - - def _to_split_order(self, mv: torch.Tensor) -> torch.Tensor: - """Convert public canonical coefficients to this node's split order.""" - return self.basis_permutation.to_split_order(mv) - - def _to_public_order(self, mv: torch.Tensor) -> torch.Tensor: - """Convert split-order coefficients back to public canonical order.""" - return self.basis_permutation.to_public_order(mv) - - def _bridge_signs_for_right_b(self, right_b_indices: torch.Tensor, dtype: torch.dtype) -> torch.Tensor: - """Compute ``(-1) ** (grade(left_A) * grade(right_B))`` for a pair slice.""" - right_grades = torch.index_select(self.right_sub.grade_index, 0, right_b_indices).unsqueeze(1) - left_grades = self.left_sub.grade_index.unsqueeze(0) - signs = torch.where( - (right_grades * left_grades) % 2 == 0, - torch.ones((), dtype=dtype, device=right_b_indices.device), - -torch.ones((), dtype=dtype, device=right_b_indices.device), - ) - return signs - - def _right_product_signs( - self, - right_a_indices: torch.Tensor, - right_b_indices: torch.Tensor, - dtype: torch.dtype, - ) -> torch.Tensor: - """Compute right-child basis-product signs for one runtime pair slice.""" - return _basis_product_signs( - right_a_indices, - right_b_indices, - self.right_sub.p, - self.right_sub.q, - self.right_sub.r, - dtype, - popcount=self.right_sub.grade_index, - ) - - @property - def device(self): - """Return the device of the algebra buffers.""" - return self.grade_index.device - - @property - def dtype(self) -> torch.dtype: - """Return the floating-point dtype used by structural sign buffers.""" - return self.rev_signs.dtype - - def _apply(self, fn): - """Propagate device/dtype moves and keep eps tolerances in sync.""" - result = super()._apply(fn) - _finfo = torch.finfo(self.dtype) - self.eps = float(_finfo.eps) - self.eps_sq = float(_finfo.eps**2) - return result - - @property - def grade_masks(self): - """Grade masks indexed by grade: ``grade_masks[k]`` -> ``[dim]`` bool.""" - return self.grade_index.unsqueeze(0) == self._grade_values.unsqueeze(1) - - @property - def grade_masks_float(self): - """Float grade masks indexed by grade: ``grade_masks_float[k]`` -> ``[dim]`` float.""" - return self.grade_masks.to(dtype=self.dtype) - - @property - def is_leaf(self) -> bool: - """Whether this node delegates directly to the monolithic atomic kernel.""" - return self.core is not None - - def describe_tree(self) -> str: - """Return a readable split tree for debugging partition structure. - - The reported bit ranges use global basis-vector bit positions. Because - structurally identical subalgebras are shared, repeated module objects - are annotated with ``shared_with=`` while still being shown - at each logical tree position. - """ - lines: list[str] = [] - seen: dict[int, str] = {} - - self._describe_tree_node( - lines=lines, - path="root", - public_bits=tuple(range(self.n)), - depth=0, - seen=seen, - ) - - return "\n".join(lines) - - @staticmethod - def _format_public_bits(public_bits: tuple[int, ...]) -> str: - """Format global public bit positions compactly when contiguous.""" - if not public_bits: - return "[]" - - start = public_bits[0] - contiguous = tuple(range(start, start + len(public_bits))) - if public_bits == contiguous: - return f"[{start}, {start + len(public_bits)})" - - return "[" + ", ".join(str(bit) for bit in public_bits) + "]" - - def print_tree(self) -> None: - """Print ``describe_tree()`` for interactive debugging.""" - print(self.describe_tree()) - - def _describe_tree_node( - self, - *, - lines: list[str], - path: str, - public_bits: tuple[int, ...], - depth: int, - seen: dict[int, str], - ) -> None: - """Append this node and children to a tree description.""" - indent = " " * depth - signature = f"Cl({self.p},{self.q},{self.r})" - bits_text = self._format_public_bits(public_bits) - - node_id = id(self) - shared_suffix = "" - if node_id in seen: - shared_suffix = f", shared_with={seen[node_id]}" - else: - seen[node_id] = path - - if self.core is not None: - lines.append( - f"{indent}{path}: {signature}, n={self.n}, dim={self.dim}, bits={bits_text}, leaf_core{shared_suffix}" - ) - return - - right_bits = tuple(public_bits[bit] for bit in self._right_dims) - left_bits = tuple(public_bits[bit] for bit in self._left_dims) - right_bits_text = self._format_public_bits(right_bits) - left_bits_text = self._format_public_bits(left_bits) - - lines.append( - f"{indent}{path}: {signature}, n={self.n}, dim={self.dim}, " - f"bits={bits_text}, split left={self.left_n} bits={left_bits_text}, right={self.right_n} " - f"bits={right_bits_text}, pairs={self._right_pair_count}, " - f"chunk={self._product_chunk_size}{shared_suffix}" - ) - - self.left_sub._describe_tree_node( - lines=lines, - path=f"{path}.L", - public_bits=left_bits, - depth=depth + 1, - seen=seen, - ) - self.right_sub._describe_tree_node( - lines=lines, - path=f"{path}.R", - public_bits=right_bits, - depth=depth + 1, - seen=seen, - ) - - @property - def exp_policy(self): - """Active :class:`core.decomposition.ExpPolicy` controlling ``exp()`` dispatch.""" - return self._exp_policy - - @exp_policy.setter - def exp_policy(self, value): - from core.decomposition import ExpPolicy, resolve_fixed_iterations - - self._exp_policy = value if isinstance(value, ExpPolicy) else ExpPolicy(value) - self._exp_fixed_iterations = resolve_fixed_iterations(self._exp_policy, self.dtype, self.n) - if self.core is not None: - self.core.exp_policy = self._exp_policy - else: - self.left_sub.exp_policy = self._exp_policy - self.right_sub.exp_policy = self._exp_policy - - @property - def num_grades(self) -> int: - """Counts the number of grades.""" - return self.n + 1 - - def embed_vector(self, vectors: torch.Tensor) -> torch.Tensor: - """Inject vectors into the grade-1 subspace.""" - if self.core is not None: - return self.core.embed_vector(vectors) - mv = torch.zeros(*vectors.shape[:-1], self.dim, device=vectors.device, dtype=vectors.dtype) - mv.scatter_(-1, self._g1_indices.expand_as(vectors), vectors) - return mv - - def get_grade_norms(self, mv: torch.Tensor) -> torch.Tensor: - """Calculate per-grade Euclidean coefficient norms.""" - if self.core is not None: - return self.core.get_grade_norms(mv) - check_multivector(mv, self, "get_grade_norms(mv)") - batch_shape = mv.shape[:-1] - sq = mv.pow(2) - flat = sq.reshape(-1, self.dim) - grade_index = self.grade_index.unsqueeze(0).expand_as(flat) - result = torch.zeros(flat.shape[0], self.num_grades, device=mv.device, dtype=mv.dtype) - result.scatter_add_(1, grade_index, flat) - return result.reshape(*batch_shape, self.num_grades).clamp(min=self.eps).sqrt() - - def _combine_ab_ba(self, A: torch.Tensor, B: torch.Tensor, weight_index: int) -> torch.Tensor: - """Compute a weighted combination of ``AB`` and ``BA`` in one recursive pass.""" - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - A = A.to(dtype=output_dtype) - B = B.to(dtype=output_dtype) - - A_broadcast, B_broadcast = torch.broadcast_tensors(A, B) - left_operands = torch.stack((A_broadcast, B_broadcast), dim=-2) - right_operands = torch.stack((B_broadcast, A_broadcast), dim=-2) - products = self.geometric_product(left_operands, right_operands) - - weights = self._product_pair_weights[weight_index] - if weights.dtype != products.dtype: - weights = weights.to(dtype=products.dtype) - return torch.einsum("...pd,p->...d", products, weights) - - def _leaf_geometric_product( - self, - A: torch.Tensor, - B: torch.Tensor, - output_dtype: torch.dtype, - compute_dtype: torch.dtype, - ) -> torch.Tensor: - """Compute a dense leaf product with the local dense kernel.""" - A_compute = A.to(dtype=compute_dtype) - B_compute = B.to(dtype=compute_dtype) - - result = self.core.geometric_product(A_compute, B_compute) - - if result.dtype != output_dtype: - result = result.to(dtype=output_dtype) - return result - - def _right_blade_blocks(self, mv: torch.Tensor, compute_dtype: torch.dtype) -> torch.Tensor: - """Return split-order coefficients grouped as right-indexed left multivectors.""" - split_order = self._to_split_order(mv.to(dtype=compute_dtype)) - by_left_then_right = split_order.reshape(*mv.shape[:-1], self.left_dim, self.right_dim) - return by_left_then_right.transpose(-1, -2) - - def geometric_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: - """Compute ``A * B`` through recursive tensor-product partitioning. - - For recursive nodes the public coefficient vector is first converted to - split order, then reshaped into ``[..., right_dim, left_dim]``. Each - right basis-pair selects two left-subalgebra multivectors, multiplies - them recursively, and merges the result into the appropriate right block. - """ - check_multivector(A, self, "geometric_product(A)") - check_multivector(B, self, "geometric_product(B)") - - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - compute_dtype = self._geometric_product_compute_dtype(output_dtype) - - if self.core is not None: - return self._leaf_geometric_product(A, B, output_dtype, compute_dtype) - - return self._recursive_geometric_product(A, B, output_dtype, compute_dtype) - - def _recursive_geometric_product( - self, - A: torch.Tensor, - B: torch.Tensor, - output_dtype: torch.dtype, - compute_dtype: torch.dtype, - ) -> torch.Tensor: - """Compute a recursive node product after validation and dtype resolution.""" - A_by_right_blade = self._right_blade_blocks(A, compute_dtype) - B_by_right_blade = self._right_blade_blocks(B, compute_dtype) - result_blocks = self._accumulate_right_pair_chunks(A_by_right_blade, B_by_right_blade) - output_shape = result_blocks.shape[:-2] - result = result_blocks.reshape(*output_shape, self.dim) - return self._to_public_order(result).to(dtype=output_dtype) - - def _accumulate_right_pair_chunks( - self, - A_by_right_blade: torch.Tensor, - B_by_right_blade: torch.Tensor, - ) -> torch.Tensor: - """Accumulate sparse right-block interactions over static product chunks.""" - result_blocks = None - for start, end in _product_pair_ranges(self._right_pair_count, self._product_chunk_size): - chunk_blocks = self._geometric_product_pair_range(A_by_right_blade, B_by_right_blade, start, end) - if result_blocks is None: - result_blocks = chunk_blocks - else: - result_blocks = result_blocks + chunk_blocks - - assert result_blocks is not None - return result_blocks - - def _geometric_product_pair_range( - self, - A_by_right_blade: torch.Tensor, - B_by_right_blade: torch.Tensor, - start: int, - end: int, - ) -> torch.Tensor: - """Compute all contributions from a contiguous right-pair range.""" - product_slice = self._right_product_slice(start, end) - - if product_slice.right_product_signs.numel() == 0: - batch_shape = torch.broadcast_shapes(A_by_right_blade.shape[:-2], B_by_right_blade.shape[:-2]) - return A_by_right_blade.new_zeros(*batch_shape, self.left_dim, self.right_dim) - - A_terms = torch.index_select(A_by_right_blade, -2, product_slice.right_a_indices) - B_terms = torch.index_select(B_by_right_blade, -2, product_slice.right_b_indices) - - # ``bridge_signs[right_b, left_a]`` depends on the left basis index of - # each selected A term, so broadcasting over the final left_dim axis - # attaches the sign before the recursive left product. - bridge_signs = self._bridge_signs_for_right_b(product_slice.right_b_indices, A_terms.dtype) - if bridge_signs.dtype != A_terms.dtype: - bridge_signs = bridge_signs.to(dtype=A_terms.dtype) - A_terms = A_terms * bridge_signs - - left_products = self.left_sub.geometric_product(A_terms, B_terms) - - return self._merge_right_interactions( - left_products, - product_slice.right_result_indices, - product_slice.right_product_signs, - ) - - def _merge_right_interactions( - self, - left_products: torch.Tensor, - right_result_indices: torch.Tensor, - right_product_signs: torch.Tensor, - ) -> torch.Tensor: - """Merge left products into ``[..., left_dim, right_dim]`` result blocks.""" - if right_product_signs.numel() == 0: - return left_products.new_zeros( - *left_products.shape[:-2], - self.left_dim, - self.right_dim, - ) - - return self._merge_right_interactions_index_add(left_products, right_result_indices, right_product_signs) - - def _merge_right_interactions_index_add( - self, - left_products: torch.Tensor, - right_result_indices: torch.Tensor, - right_product_signs: torch.Tensor, - ) -> torch.Tensor: - """Merge right-pair contributions with direct indexed accumulation.""" - signed_products = left_products.transpose(-1, -2) * right_product_signs.to(dtype=left_products.dtype) - result_blocks = left_products.new_zeros( - *left_products.shape[:-2], - self.left_dim, - self.right_dim, - ) - result_blocks.index_add_(-1, right_result_indices, signed_products) - return result_blocks - - def _geometric_product_compute_dtype(self, output_dtype: torch.dtype) -> torch.dtype: - """Return the dtype used for product accumulation.""" - if self.accumulation_dtype is None or not output_dtype.is_floating_point: - return output_dtype - return torch.promote_types(output_dtype, self.accumulation_dtype) - - def _promote_with_algebra_dtype(self, *dtypes: torch.dtype) -> torch.dtype: - """Promote operand dtypes with the algebra's floating-point table dtype.""" - result = self.dtype - for dtype in dtypes: - result = torch.promote_types(result, dtype) - return result - - def _right_product_slice( - self, - start: int, - end: int, - ) -> _RightProductSlice: - """Derive right-block routing tensors for pair range ``[start, end)``. - - Returns: - _RightProductSlice: Each position describes one right basis-pair - contribution in the recursive product. The result still supports - tuple unpacking for older private tests. - """ - pair_indices = torch.arange(start, end, dtype=torch.long, device=self.device) - if self.right_sub.r == 0: - right_a_indices = torch.div(pair_indices, self.right_dim, rounding_mode="floor") - right_b_indices = pair_indices.remainder(self.right_dim) - else: - right_a_indices, right_b_indices = _compact_surviving_basis_pairs( - pair_indices, - self.right_n, - self.right_sub.p, - self.right_sub.q, - self.right_sub.r, - ) - - right_result_indices = right_a_indices ^ right_b_indices - right_product_signs = self._right_product_signs(right_a_indices, right_b_indices, torch.int8) - - return _RightProductSlice( - right_a_indices=right_a_indices, - right_b_indices=right_b_indices, - right_result_indices=right_result_indices, - right_product_signs=right_product_signs, - ) - - def grade_projection(self, mv: torch.Tensor, grade: int) -> torch.Tensor: - """Project a multivector onto a grade using the same mask contract as the core algebra.""" - if self.core is not None: - return self.core.grade_projection(mv, grade) - check_multivector(mv, self, "grade_projection(mv)") - return mv * (self.grade_index == grade).to(dtype=mv.dtype) - - def reverse(self, mv: torch.Tensor) -> torch.Tensor: - """Compute Clifford reversion.""" - if self.core is not None: - return self.core.reverse(mv) - check_multivector(mv, self, "reverse(mv)") - return mv * self.rev_signs.to(dtype=mv.dtype) - - def wedge(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: - """Compute ``(AB - BA) / 2`` through the partitioned product.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - return self.core.wedge(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) - return self._combine_ab_ba(A, B, 0) - - def right_contraction(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: - """Compute the bivector-vector right contraction used by decomposition.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - return self.core.right_contraction(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) - check_multivector(A, self, "right_contraction(A)") - check_multivector(B, self, "right_contraction(B)") - - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - bv_coeffs = torch.index_select(A, -1, self._bv_indices).to(dtype=output_dtype) - v_coeffs = torch.index_select(B, -1, self._g1_indices).to(dtype=output_dtype) - - rc = self.rc_action.to(dtype=output_dtype) - action = torch.einsum("...b, bij -> ...ij", bv_coeffs, rc) - result_v = torch.matmul(action, v_coeffs.unsqueeze(-1)).squeeze(-1) - - result = result_v.new_zeros(*result_v.shape[:-1], self.dim) - g1_idx_exp = self._g1_indices.expand(*result_v.shape[:-1], -1) - result.scatter_(-1, g1_idx_exp, result_v) - return result - - def inner_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: - """Compute ``(AB + BA) / 2`` through the partitioned product.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - return self.core.inner_product(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) - return self._combine_ab_ba(A, B, 1) - - def commutator(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: - """Compute the Lie bracket ``AB - BA``.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - return self.core.commutator(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) - return self._combine_ab_ba(A, B, 2) - - def anti_commutator(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: - """Compute the anti-commutator ``AB + BA``.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - return self.core.anti_commutator(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) - return self._combine_ab_ba(A, B, 3) - - def blade_inverse(self, blade: torch.Tensor) -> torch.Tensor: - """Compute the inverse of a non-degenerate blade.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(blade.dtype) - return self.core.blade_inverse(blade.to(dtype=output_dtype)) - blade_rev = self.reverse(blade) - blade_sq = self.geometric_product(blade, blade_rev) - scalar = blade_sq[..., 0:1].clamp(min=self.eps_sq) - return blade_rev / scalar - - def sandwich_product(self, R: torch.Tensor, x: torch.Tensor, R_rev: torch.Tensor = None) -> torch.Tensor: - """Compute ``R x R~`` using recursive products.""" - if self.core is not None: - dtypes = [R.dtype, x.dtype] - if R_rev is not None: - dtypes.append(R_rev.dtype) - output_dtype = self._promote_with_algebra_dtype(*dtypes) - R_rev = None if R_rev is None else R_rev.to(dtype=output_dtype) - return self.core.sandwich_product(R.to(dtype=output_dtype), x.to(dtype=output_dtype), R_rev) - if R_rev is None: - R_rev = self.reverse(R) - left = self.geometric_product(R.unsqueeze(-2), x) - return self.geometric_product(left, R_rev.unsqueeze(-2)) - - def per_channel_sandwich(self, R: torch.Tensor, x: torch.Tensor, R_rev: torch.Tensor = None) -> torch.Tensor: - """Compute per-channel sandwich products using recursive products.""" - if self.core is not None: - dtypes = [R.dtype, x.dtype] - if R_rev is not None: - dtypes.append(R_rev.dtype) - output_dtype = self._promote_with_algebra_dtype(*dtypes) - R_rev = None if R_rev is None else R_rev.to(dtype=output_dtype) - return self.core.per_channel_sandwich(R.to(dtype=output_dtype), x.to(dtype=output_dtype), R_rev) - if R_rev is None: - R_rev = self.reverse(R) - left = self.geometric_product(R.unsqueeze(0), x) - return self.geometric_product(left, R_rev.unsqueeze(0)) - - def multi_rotor_sandwich(self, R: torch.Tensor, x: torch.Tensor, R_rev: torch.Tensor = None) -> torch.Tensor: - """Apply K rotors to ``[B, C, D]`` inputs using recursive products.""" - if self.core is not None: - dtypes = [R.dtype, x.dtype] - if R_rev is not None: - dtypes.append(R_rev.dtype) - output_dtype = self._promote_with_algebra_dtype(*dtypes) - R_rev = None if R_rev is None else R_rev.to(dtype=output_dtype) - return self.core.multi_rotor_sandwich(R.to(dtype=output_dtype), x.to(dtype=output_dtype), R_rev) - if R_rev is None: - R_rev = self.reverse(R) - left = self.geometric_product(R.view(1, 1, *R.shape), x.unsqueeze(2)) - return self.geometric_product(left, R_rev.view(1, 1, *R_rev.shape)) - - def pseudoscalar_product(self, x: torch.Tensor) -> torch.Tensor: - """Multiply by the unit pseudoscalar using a static permutation/sign vector.""" - if self.core is not None: - return self.core.pseudoscalar_product(x) - check_multivector(x, self, "pseudoscalar_product(x)") - return x[..., self._ps_source] * self._ps_signs.to(dtype=x.dtype) - - def blade_project(self, mv: torch.Tensor, blade: torch.Tensor) -> torch.Tensor: - """Project a multivector onto a blade subspace.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(mv.dtype, blade.dtype) - return self.core.blade_project(mv.to(dtype=output_dtype), blade.to(dtype=output_dtype)) - inner = self.inner_product(mv, blade) - return self.geometric_product(inner, self.blade_inverse(blade)) - - def blade_reject(self, mv: torch.Tensor, blade: torch.Tensor) -> torch.Tensor: - """Reject a multivector from a blade subspace.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(mv.dtype, blade.dtype) - return self.core.blade_reject(mv.to(dtype=output_dtype), blade.to(dtype=output_dtype)) - return mv - self.blade_project(mv, blade) - - def grade_involution(self, mv: torch.Tensor) -> torch.Tensor: - """Apply the main involution.""" - if self.core is not None: - return self.core.grade_involution(mv) - check_multivector(mv, self, "grade_involution(mv)") - return mv * self._involution_signs.to(dtype=mv.dtype) - - def clifford_conjugation(self, mv: torch.Tensor) -> torch.Tensor: - """Apply Clifford conjugation.""" - if self.core is not None: - return self.core.clifford_conjugation(mv) - check_multivector(mv, self, "clifford_conjugation(mv)") - return mv * self.conj_signs.to(dtype=mv.dtype) - - def norm_sq(self, mv: torch.Tensor) -> torch.Tensor: - """Compute ``_0`` using pre-merged static signs.""" - if self.core is not None: - return self.core.norm_sq(mv) - check_multivector(mv, self, "norm_sq(mv)") - signs = self._norm_sq_signs - if signs.dtype != mv.dtype: - signs = signs.to(dtype=mv.dtype) - return torch.matmul(mv * mv, signs.unsqueeze(-1)) - - def left_contraction(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: - """Compute left contraction by static grade-pair dispatch.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - return self.core.left_contraction(A.to(dtype=output_dtype), B.to(dtype=output_dtype)) - check_multivector(A, self, "left_contraction(A)") - check_multivector(B, self, "left_contraction(B)") - - output_dtype = self._promote_with_algebra_dtype(A.dtype, B.dtype) - A = A.to(dtype=output_dtype) - B = B.to(dtype=output_dtype) - - A_b, B_b = torch.broadcast_tensors(A, B) - result = None - pair_count = int(self._lc_grade_a.numel()) - chunk_size = max( - 1, - min( - pair_count, - self._product_chunk_size if self._product_chunk_size > 0 else pair_count, - ), - ) - grade_index = self.grade_index.unsqueeze(0) - for start in range(0, pair_count, chunk_size): - end = min(start + chunk_size, pair_count) - a_masks = grade_index == self._lc_grade_a[start:end].unsqueeze(1) - b_masks = grade_index == self._lc_grade_b[start:end].unsqueeze(1) - result_masks = grade_index == self._lc_grade_result[start:end].unsqueeze(1) - a_masks = a_masks.to(dtype=A_b.dtype) - b_masks = b_masks.to(dtype=A_b.dtype) - result_masks = result_masks.to(dtype=A_b.dtype) - - A_terms = A_b.unsqueeze(-2) * a_masks - B_terms = B_b.unsqueeze(-2) * b_masks - products = self.geometric_product(A_terms, B_terms) - chunk = torch.einsum("...pd,pd->...d", products, result_masks) - result = chunk if result is None else result + chunk - return result - - def dual(self, mv: torch.Tensor) -> torch.Tensor: - """Hodge dual alias for pseudoscalar multiplication.""" - if self.core is not None: - return self.core.dual(mv) - return self.pseudoscalar_product(mv) - - def reflect(self, x: torch.Tensor, n: torch.Tensor) -> torch.Tensor: - """Reflect ``x`` through the hyperplane orthogonal to vector ``n``.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(x.dtype, n.dtype) - return self.core.reflect(x.to(dtype=output_dtype), n.to(dtype=output_dtype)) - n_hat = self.grade_involution(n) - n_inv = self.blade_inverse(n) - if x.dim() == 3 and n.dim() == 2 and x.shape[0] != n.shape[0]: - n_hat = n_hat.unsqueeze(0) - n_inv = n_inv.unsqueeze(0) - elif x.dim() == 3 and n.dim() == 2 and x.shape[0] == n.shape[0]: - n_hat = n_hat.unsqueeze(1) - n_inv = n_inv.unsqueeze(1) - return self.geometric_product(self.geometric_product(n_hat, x), n_inv) - - def versor_product(self, V: torch.Tensor, x: torch.Tensor) -> torch.Tensor: - """Apply the general versor transformation ``hat(V) x V^{-1}``.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(V.dtype, x.dtype) - return self.core.versor_product(V.to(dtype=output_dtype), x.to(dtype=output_dtype)) - V_inv = self.blade_inverse(V) - V_hat = self.grade_involution(V) - return self.geometric_product(self.geometric_product(V_hat, x), V_inv) - - def exp(self, mv: torch.Tensor) -> torch.Tensor: - """Exponentiates a bivector to produce a rotor. - - Dispatch mirrors :class:`core.algebra.CliffordAlgebra`: - - - ``n <= 3`` -- every bivector is simple; closed-form is exact. - - ``n >= 4`` -- compiled-safe decomposition; per-element selects - closed-form vs decomposed via ``torch.where(simple)``. - - Args: - mv (torch.Tensor): Pure bivector [..., dim]. - - Returns: - torch.Tensor: Rotor exp(mv) [..., dim]. - """ - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(mv.dtype) - return self.core.exp(mv.to(dtype=output_dtype)) - if self.n <= 3: - return self._exp_bivector_closed(mv) - return self._exp_compiled_safe(mv) - - def _exp_bivector_closed(self, B: torch.Tensor) -> torch.Tensor: - """Closed-form exponential for simple bivectors in arbitrary signature. - - Uses zero geometric products. Exact for simple bivectors in any - Clifford algebra Cl(p,q,r). - """ - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(B.dtype) - return self.core._exp_bivector_closed(B.to(dtype=output_dtype)) - - output_dtype = self._promote_with_algebra_dtype(B.dtype) - B = B.to(dtype=output_dtype) - - bv_coeffs = torch.index_select(B, -1, self._bv_indices) - bv_sq_scalar = self.bv_sq_scalar.to(dtype=output_dtype) - - # Signed squared norm: alpha = Sum_k b_k^2 . (e_k)^2 - # alpha < 0 -> elliptic (Euclidean-like), alpha > 0 -> hyperbolic - alpha = torch.matmul(bv_coeffs * bv_coeffs, bv_sq_scalar.unsqueeze(-1)) - - abs_alpha = alpha.abs().clamp(min=self.eps_sq) - theta = torch.sqrt(abs_alpha) - - g0_mask = self.grade_masks_float[0].to(dtype=output_dtype) - - # Dispatch by signature regime (Python branch, no graph break) - if self._exp_regime == "elliptic": - # Pure Euclidean: alpha is always negative, only cos/sinc needed - cos_theta = torch.cos(theta) - sinc_theta = torch.where( - theta > self.eps, - torch.sin(theta) / theta, - 1.0 - abs_alpha / 6.0, - ) - return cos_theta * g0_mask + sinc_theta * B - - if self._exp_regime == "hyperbolic": - # Pure negative: alpha is always positive, only cosh/sinhc needed - cosh_theta = torch.cosh(theta) - sinhc_theta = torch.where( - theta > self.eps, - torch.sinh(theta) / theta, - 1.0 + abs_alpha / 6.0, - ) - return cosh_theta * g0_mask + sinhc_theta * B - - # Mixed signature: need both branches + runtime select - cos_theta = torch.cos(theta) - sinc_theta = torch.where( - theta > self.eps, - torch.sin(theta) / theta, - 1.0 - abs_alpha / 6.0, - ) - cosh_theta = torch.cosh(theta) - sinhc_theta = torch.where( - theta > self.eps, - torch.sinh(theta) / theta, - 1.0 + abs_alpha / 6.0, - ) - - is_elliptic = alpha < -self.eps_sq - is_hyperbolic = alpha > self.eps_sq - - scalar_part = torch.where( - is_elliptic, - cos_theta, - torch.where(is_hyperbolic, cosh_theta, torch.ones_like(theta)), - ) - coeff_part = torch.where( - is_elliptic, - sinc_theta, - torch.where(is_hyperbolic, sinhc_theta, torch.ones_like(theta)), - ) - - return scalar_part * g0_mask + coeff_part * B - - def _exp_compiled_safe(self, B: torch.Tensor) -> torch.Tensor: - """Compiled-safe exponential using partitioned products. - - Runs both closed-form and decomposed paths, then selects per element - via ``torch.where`` based on simplicity. Both paths are computed - unconditionally so there is no data-dependent branching. - """ - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(B.dtype) - return self.core._exp_compiled_safe(B.to(dtype=output_dtype)) - from core.decomposition import compiled_safe_decomposed_exp - - R_closed = self._exp_bivector_closed(B) - R_decomposed = compiled_safe_decomposed_exp( - self, - B, - fixed_iterations=self._exp_fixed_iterations, - ) - - BB = self.geometric_product(B, B) - # Subtract scalar part, check if residual is negligible - scalar_part = self.grade_projection(BB, 0) - non_scalar_energy = (BB - scalar_part).norm(dim=-1, keepdim=True) - is_simple = non_scalar_energy < self.eps * 100 - - return torch.where(is_simple, R_closed, R_decomposed) - - def _exp_taylor(self, mv: torch.Tensor, order: int = 8) -> torch.Tensor: - """Taylor series exponential with scaling-and-squaring.""" - if self.core is not None: - output_dtype = self._promote_with_algebra_dtype(mv.dtype) - return self.core._exp_taylor(mv.to(dtype=output_dtype), order=order) - norm = mv.norm(dim=-1, keepdim=True) - k = torch.ceil(torch.log2(torch.clamp(norm, min=1.0))).int() - - max_k = k.max().item() - if max_k > 0: - mv_scaled = mv / (2.0**max_k) - else: - mv_scaled = mv - - res = torch.zeros_like(mv) - res[..., 0] = 1.0 - - term = torch.zeros_like(mv) - term[..., 0] = 1.0 - - for i in range(1, order + 1): - term = self.geometric_product(term, mv_scaled) - res = res + term / math.factorial(i) - - if max_k > 0: - for _ in range(int(max_k)): - res = self.geometric_product(res, res) - - return res diff --git a/core/planning/__init__.py b/core/planning/__init__.py new file mode 100644 index 0000000..19a28ff --- /dev/null +++ b/core/planning/__init__.py @@ -0,0 +1,33 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Grade-plan translation and Torch executor lowering.""" + +from .flow import GradeFlow +from .grade_plan import GradeProductExecutor, GradeProductPlan, build_grade_product_plan +from .request import ProductRequest, build_product_request +from .translator import GradeTranslator +from .tree import GradePathNode, GradePlanTree, build_grade_plan_tree +from .unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request + +__all__ = [ + "GradeFlow", + "GradePathNode", + "GradeProductExecutor", + "GradeProductPlan", + "GradePlanTree", + "GradeTranslator", + "GradeUnaryExecutor", + "GradeUnaryOp", + "GradeUnaryPlan", + "ProductRequest", + "UnaryRequest", + "build_grade_product_plan", + "build_grade_plan_tree", + "build_product_request", + "build_unary_request", +] diff --git a/core/planning/flow.py b/core/planning/flow.py new file mode 100644 index 0000000..e32c82b --- /dev/null +++ b/core/planning/flow.py @@ -0,0 +1,97 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Grade-flow metadata for AoT layout propagation.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable, Optional + +from core.foundation.basis import GradeProductOp, expand_output_grades, normalize_grades +from core.foundation.layout import AlgebraSpec, GradeLayout + + +@dataclass(frozen=True) +class GradeFlow: + """Static grade/layout metadata passed between planned operations.""" + + spec: AlgebraSpec + layout: GradeLayout + + @classmethod + def from_grades(cls, spec: AlgebraSpec, grades: Iterable[int]) -> "GradeFlow": + """Create a flow from active grades.""" + return cls(spec=spec, layout=spec.layout(grades)) + + @classmethod + def full(cls, spec: AlgebraSpec) -> "GradeFlow": + """Create a full-layout flow.""" + return cls.from_grades(spec, range(spec.n + 1)) + + @classmethod + def scalar(cls, spec: AlgebraSpec) -> "GradeFlow": + """Create a scalar-only flow.""" + return cls.from_grades(spec, (0,)) + + @classmethod + def vector(cls, spec: AlgebraSpec) -> "GradeFlow": + """Create a vector-only flow.""" + return cls.from_grades(spec, (1,)) + + @property + def grades(self) -> tuple[int, ...]: + """Active grades represented by this flow.""" + return self.layout.grades + + @property + def dim(self) -> int: + """Compact lane count represented by this flow.""" + return self.layout.dim + + def project(self, grades: Iterable[int]) -> "GradeFlow": + """Narrow the flow to a subset of active grades.""" + projected = normalize_grades(grades, self.spec.n, name="grades") + missing = tuple(grade for grade in projected if grade not in self.grades) + if missing: + raise ValueError(f"Cannot project missing grades {missing} from active grades {self.grades}") + return GradeFlow.from_grades(self.spec, projected) + + def unary(self, op: str, output_grades: Optional[Iterable[int]] = None) -> "GradeFlow": + """Propagate flow through a unary operation.""" + if output_grades is not None: + return self.project(output_grades) + if op in {"identity", "reverse", "grade_involution", "clifford_conjugation"}: + return self + if op == "grade_projection": + raise ValueError("grade_projection requires output_grades") + raise ValueError(f"Unsupported unary flow op {op!r}") + + def product( + self, + other: "GradeFlow", + *, + op: GradeProductOp = "gp", + output_grades: Optional[Iterable[int]] = None, + ) -> "GradeFlow": + """Propagate flow through a bilinear grade product.""" + self._check_spec(other) + grades = ( + expand_output_grades(self.grades, other.grades, self.spec.n, op=op) + if output_grades is None + else normalize_grades(output_grades, self.spec.n, name="output_grades") + ) + return GradeFlow.from_grades(self.spec, grades) + + def merge(self, other: "GradeFlow") -> "GradeFlow": + """Union two flows, e.g. for addition or concatenation.""" + self._check_spec(other) + return GradeFlow.from_grades(self.spec, (*self.grades, *other.grades)) + + def _check_spec(self, other: "GradeFlow") -> None: + if self.spec != other.spec: + raise ValueError(f"GradeFlow spec mismatch: {self.spec} vs {other.spec}") diff --git a/core/planning/grade_plan.py b/core/planning/grade_plan.py new file mode 100644 index 0000000..877ec5d --- /dev/null +++ b/core/planning/grade_plan.py @@ -0,0 +1,335 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Static grade-path plans for sparse high-dimensional Clifford products. + +This module describes the lower-level AoT shape needed by high-dimensional +sparse execution: input grades are declared or inferred at construction time, +all basis interactions are expanded once, and forward execution is only gather, +multiply, and indexed reduction over the required output grade lanes. +""" + +from __future__ import annotations + +from typing import Iterable, Optional + +import torch +import torch.nn as nn + +from core.foundation.basis import GradeProductOp, expand_output_grades, normalize_grades, operation_coefficient +from core.foundation.layout import AlgebraSpec, GradeLayout +from core.planning.request import ProductRequest +from core.planning.tree import GradePlanTree, build_grade_plan_tree + + +class GradeProductPlan: + """AoT basis interaction plan for one grade-restricted bilinear product.""" + + def __init__( + self, + *, + p: int, + q: int, + r: int, + op: GradeProductOp, + left_grades: tuple[int, ...], + right_grades: tuple[int, ...], + output_grades: tuple[int, ...], + left_indices: torch.Tensor, + right_indices: torch.Tensor, + output_indices: torch.Tensor, + output_positions: torch.Tensor, + coefficients: torch.Tensor, + active_output_indices: torch.Tensor, + tree: GradePlanTree, + ): + self.spec = AlgebraSpec(p, q, r) + self.op = op + self.left_layout = self.spec.layout(left_grades) + self.right_layout = self.spec.layout(right_grades) + self.output_layout = self.spec.layout(output_grades) + self.left_indices = left_indices + self.right_indices = right_indices + self.output_indices = output_indices + self.output_positions = output_positions + self.coefficients = coefficients + self.active_output_indices = active_output_indices + self.tree = tree + + @property + def p(self) -> int: + return self.spec.p + + @property + def q(self) -> int: + return self.spec.q + + @property + def r(self) -> int: + return self.spec.r + + @property + def left_grades(self) -> tuple[int, ...]: + return self.left_layout.grades + + @property + def right_grades(self) -> tuple[int, ...]: + return self.right_layout.grades + + @property + def output_grades(self) -> tuple[int, ...]: + return self.output_layout.grades + + @property + def n(self) -> int: + return self.p + self.q + self.r + + @property + def dim(self) -> int: + return 1 << self.n + + @property + def pair_count(self) -> int: + return int(self.left_indices.numel()) + + @property + def output_dim(self) -> int: + return int(self.active_output_indices.numel()) + + @property + def is_empty(self) -> bool: + return self.pair_count == 0 + + @property + def density(self) -> float: + if self.tree.estimated_pairs == 0: + return 0.0 + return self.pair_count / self.tree.estimated_pairs + + +def build_grade_product_plan( + p: int, + q: int = 0, + r: int = 0, + *, + left_grades: Iterable[int], + right_grades: Iterable[int], + output_grades: Optional[Iterable[int]] = None, + op: GradeProductOp = "gp", + device=None, + dtype: torch.dtype = torch.float32, +) -> GradeProductPlan: + """Build an exact static basis-pair plan for a grade-restricted operation.""" + spec = AlgebraSpec(int(p), int(q), int(r)) + tree = build_grade_plan_tree( + spec, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + op=op, + ) + return build_grade_product_plan_from_tree(tree, device=device, dtype=dtype) + + +def build_grade_product_plan_from_request( + request: ProductRequest, + *, + device=None, + dtype: Optional[torch.dtype] = None, +) -> GradeProductPlan: + """Build a plan from a normalized product request.""" + tree = build_grade_plan_tree( + request.spec, + left_grades=request.left_grades, + right_grades=request.right_grades, + output_grades=request.output_grades, + op=request.op, + ) + return build_grade_product_plan_from_tree( + tree, + device=request.device if device is None else device, + dtype=request.dtype if dtype is None else dtype, + ) + + +def build_grade_product_plan_from_tree( + tree: GradePlanTree, + *, + device=None, + dtype: torch.dtype = torch.float32, +) -> GradeProductPlan: + """Lower a planner tree into flat Torch gather/reduce buffers.""" + spec = tree.spec + p, q, r = spec.p, spec.q, spec.r + n = spec.n + left_grade_tuple = tree.left_grades + right_grade_tuple = tree.right_grades + output_grade_tuple = tree.output_grades + + left_basis_by_grade = { + grade: [index for index in range(1 << n) if index.bit_count() == grade] for grade in left_grade_tuple + } + right_basis_by_grade = { + grade: [index for index in range(1 << n) if index.bit_count() == grade] for grade in right_grade_tuple + } + active_outputs = [index for index in range(1 << n) if index.bit_count() in set(output_grade_tuple)] + output_position_by_index = {index: position for position, index in enumerate(active_outputs)} + + plan_left: list[int] = [] + plan_right: list[int] = [] + plan_output: list[int] = [] + plan_positions: list[int] = [] + plan_coefficients: list[float] = [] + + for path in tree.paths: + for left_index in left_basis_by_grade[path.left_grade]: + for right_index in right_basis_by_grade[path.right_grade]: + output_index = left_index ^ right_index + output_position = output_position_by_index.get(output_index) + if output_position is None: + continue + coefficient = operation_coefficient(left_index, right_index, p, q, r, tree.op) + if coefficient == 0.0: + continue + plan_left.append(left_index) + plan_right.append(right_index) + plan_output.append(output_index) + plan_positions.append(output_position) + plan_coefficients.append(coefficient) + + return GradeProductPlan( + p=p, + q=q, + r=r, + op=tree.op, + left_grades=left_grade_tuple, + right_grades=right_grade_tuple, + output_grades=output_grade_tuple, + left_indices=torch.tensor(plan_left, dtype=torch.long, device=device), + right_indices=torch.tensor(plan_right, dtype=torch.long, device=device), + output_indices=torch.tensor(plan_output, dtype=torch.long, device=device), + output_positions=torch.tensor(plan_positions, dtype=torch.long, device=device), + coefficients=torch.tensor(plan_coefficients, dtype=dtype, device=device), + active_output_indices=torch.tensor(active_outputs, dtype=torch.long, device=device), + tree=tree, + ) + + +class GradeProductExecutor(nn.Module): + """Compile-friendly grade-restricted product using a static interaction plan. + + ``forward`` returns compact output lanes ordered by ``active_output_indices``. + ``forward_dense`` is a compatibility helper for tests and dense callers. + """ + + def __init__(self, plan: GradeProductPlan): + super().__init__() + self.p = plan.p + self.q = plan.q + self.r = plan.r + self.n = plan.n + self.dim = plan.dim + self.op = plan.op + self.left_grades = plan.left_grades + self.right_grades = plan.right_grades + self.output_grades = plan.output_grades + self.left_layout = plan.left_layout + self.right_layout = plan.right_layout + self.output_layout = plan.output_layout + self.register_buffer("left_indices", plan.left_indices, persistent=False) + self.register_buffer("right_indices", plan.right_indices, persistent=False) + self.register_buffer("output_indices", plan.output_indices, persistent=False) + self.register_buffer("output_positions", plan.output_positions, persistent=False) + self.register_buffer("coefficients", plan.coefficients, persistent=False) + self.register_buffer("active_output_indices", plan.active_output_indices, persistent=False) + self.register_buffer( + "left_compact_positions", + self._dense_to_compact_positions(plan.left_layout, plan.left_indices), + persistent=False, + ) + self.register_buffer( + "right_compact_positions", + self._dense_to_compact_positions(plan.right_layout, plan.right_indices), + persistent=False, + ) + + @property + def output_dim(self) -> int: + return int(self.active_output_indices.numel()) + + @property + def pair_count(self) -> int: + return int(self.left_indices.numel()) + + def forward(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: + """Return compact grade-lane output for full dense input tensors.""" + if left.shape[-1] != self.dim: + raise ValueError(f"left last dimension must be {self.dim}, got {left.shape[-1]}") + if right.shape[-1] != self.dim: + raise ValueError(f"right last dimension must be {self.dim}, got {right.shape[-1]}") + + left_b, right_b = torch.broadcast_tensors(left, right) + left_terms = torch.index_select(left_b, -1, self.left_indices) + right_terms = torch.index_select(right_b, -1, self.right_indices) + coefficients = self.coefficients.to(dtype=torch.promote_types(left_terms.dtype, right_terms.dtype)) + terms = left_terms * right_terms * coefficients + + output = terms.new_zeros(*terms.shape[:-1], self.output_dim) + return output.index_add(-1, self.output_positions, terms) + + def forward_compact(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: + """Return compact output for inputs already stored in this plan's compact layouts.""" + if left.shape[-1] != self.left_layout.dim: + raise ValueError(f"left compact dimension must be {self.left_layout.dim}, got {left.shape[-1]}") + if right.shape[-1] != self.right_layout.dim: + raise ValueError(f"right compact dimension must be {self.right_layout.dim}, got {right.shape[-1]}") + + left_b, right_b = torch.broadcast_tensors(left, right) + left_terms = torch.index_select(left_b, -1, self.left_compact_positions) + right_terms = torch.index_select(right_b, -1, self.right_compact_positions) + coefficients = self.coefficients.to(dtype=torch.promote_types(left_terms.dtype, right_terms.dtype)) + terms = left_terms * right_terms * coefficients + + output = terms.new_zeros(*terms.shape[:-1], self.output_dim) + return output.index_add(-1, self.output_positions, terms) + + def forward_pairwise_compact(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: + """Pairwise compact product for sequence-style bilinear scoring. + + ``left`` is ``[..., left_items, left_layout.dim]`` and ``right`` is + ``[..., right_items, right_layout.dim]``. The result is + ``[..., left_items, right_items, output_layout.dim]``. + """ + if left.shape[-1] != self.left_layout.dim: + raise ValueError(f"left compact dimension must be {self.left_layout.dim}, got {left.shape[-1]}") + if right.shape[-1] != self.right_layout.dim: + raise ValueError(f"right compact dimension must be {self.right_layout.dim}, got {right.shape[-1]}") + + prefix = torch.broadcast_shapes(left.shape[:-2], right.shape[:-2]) + left = left.expand(*prefix, *left.shape[-2:]) + right = right.expand(*prefix, *right.shape[-2:]) + + left_terms = torch.index_select(left, -1, self.left_compact_positions) + right_terms = torch.index_select(right, -1, self.right_compact_positions) + coefficients = self.coefficients.to(dtype=torch.promote_types(left_terms.dtype, right_terms.dtype)) + terms = left_terms.unsqueeze(-2) * right_terms.unsqueeze(-3) * coefficients + + output = terms.new_zeros(*terms.shape[:-1], self.output_dim) + return output.index_add(-1, self.output_positions, terms) + + def forward_dense(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: + """Return a full ``[..., 2**n]`` dense tensor for dense-kernel parity checks.""" + compact = self.forward(left, right) + output = compact.new_zeros(*compact.shape[:-1], self.dim) + return output.index_copy(-1, self.active_output_indices, compact) + + @staticmethod + def _dense_to_compact_positions(layout: GradeLayout, dense_indices: torch.Tensor) -> torch.Tensor: + """Map dense basis indices used by a plan into compact lane positions.""" + positions = {index: position for position, index in enumerate(layout.basis_indices)} + compact_positions = [positions[int(index)] for index in dense_indices.detach().cpu().tolist()] + return torch.tensor(compact_positions, dtype=torch.long, device=dense_indices.device) diff --git a/core/planning/request.py b/core/planning/request.py new file mode 100644 index 0000000..e9aa349 --- /dev/null +++ b/core/planning/request.py @@ -0,0 +1,221 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Normalized product requests for grade-plan lowering.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +import torch + +from core.foundation.basis import GradeProductOp, expand_output_grades, normalize_grades +from core.foundation.layout import AlgebraSpec, GradeLayout + +_VALID_PRODUCT_OPS = {"gp", "wedge", "inner", "commutator", "anti_commutator"} + + +@dataclass(frozen=True) +class ProductRequest: + """Fully resolved static request for one bilinear product. + + The request is the translator's intermediate representation. It removes + ambiguity from caller input before any executor is built: layouts are + normalized, compact-vs-dense operand storage is known, and output grades are + inferred when callers do not explicitly project them. + """ + + spec: AlgebraSpec + op: GradeProductOp + left_layout: GradeLayout + right_layout: GradeLayout + output_layout: GradeLayout + left_compact: bool + right_compact: bool + dtype: torch.dtype + device: torch.device + + @property + def left_grades(self) -> tuple[int, ...]: + return self.left_layout.grades + + @property + def right_grades(self) -> tuple[int, ...]: + return self.right_layout.grades + + @property + def output_grades(self) -> tuple[int, ...]: + return self.output_layout.grades + + @property + def cache_key(self) -> tuple[object, ...]: + """Stable key for executor caching.""" + return ( + self.spec, + str(self.device), + str(self.dtype), + self.op, + self.left_grades, + self.right_grades, + self.output_grades, + ) + + +def build_product_request( + spec: AlgebraSpec, + left: torch.Tensor, + right: torch.Tensor, + *, + op: str = "gp", + left_grades=None, + right_grades=None, + output_grades=None, + left_layout: Optional[GradeLayout] = None, + right_layout: Optional[GradeLayout] = None, + output_layout: Optional[GradeLayout] = None, + left_compact: bool = False, + right_compact: bool = False, + full_layout_allowed: bool = True, +) -> ProductRequest: + """Resolve caller input into a static product request.""" + normalized_op = normalize_product_op(op) + left_layout = resolve_operand_layout( + spec, + left, + grades=left_grades, + layout=left_layout, + compact=left_compact, + side="left", + full_layout_allowed=full_layout_allowed, + ) + right_layout = resolve_operand_layout( + spec, + right, + grades=right_grades, + layout=right_layout, + compact=right_compact, + side="right", + full_layout_allowed=full_layout_allowed, + ) + output_layout = resolve_output_layout( + spec, + op=normalized_op, + left_layout=left_layout, + right_layout=right_layout, + output_grades=output_grades, + output_layout=output_layout, + ) + + left_compact = left_compact or is_compact_tensor(spec, left, left_layout) + right_compact = right_compact or is_compact_tensor(spec, right, right_layout) + + return ProductRequest( + spec=spec, + op=normalized_op, + left_layout=left_layout, + right_layout=right_layout, + output_layout=output_layout, + left_compact=left_compact, + right_compact=right_compact, + dtype=torch.promote_types(left.dtype, right.dtype), + device=left.device, + ) + + +def normalize_product_op(op: str) -> GradeProductOp: + """Validate and normalize a product operation name.""" + normalized = str(op) + if normalized not in _VALID_PRODUCT_OPS: + raise ValueError(f"Unsupported grade product op {op!r}") + return normalized # type: ignore[return-value] + + +def resolve_operand_layout( + spec: AlgebraSpec, + tensor: torch.Tensor, + *, + grades=None, + layout: Optional[GradeLayout] = None, + compact: bool = False, + side: str, + full_layout_allowed: bool = True, +) -> GradeLayout: + """Resolve one operand's grade layout from explicit metadata or tensor shape.""" + if layout is not None: + check_layout_spec(spec, layout, f"{side}_layout") + if grades is not None and layout.grades != normalize_grades(grades, spec.n, name=f"{side}_grades"): + raise ValueError(f"{side}_layout and {side}_grades disagree") + _check_operand_shape(spec, tensor, layout, compact=compact, side=side) + return layout + + if grades is not None: + layout = spec.layout(grades) + _check_operand_shape(spec, tensor, layout, compact=compact, side=side) + return layout + + if compact: + raise ValueError(f"{side}_layout or {side}_grades is required for compact {side} input") + if tensor.shape[-1] != spec.dim: + raise ValueError( + f"{side} input has last dimension {tensor.shape[-1]}; declare {side}_layout or " + f"{side}_grades for compact planned execution" + ) + if not full_layout_allowed: + raise ValueError( + f"{side} input would require a full Cl({spec.p},{spec.q},{spec.r}) layout. " + "Declare active grades or use a dense CliffordAlgebra reference kernel." + ) + return spec.layout(range(spec.n + 1)) + + +def resolve_output_layout( + spec: AlgebraSpec, + *, + op: GradeProductOp, + left_layout: GradeLayout, + right_layout: GradeLayout, + output_grades=None, + output_layout: Optional[GradeLayout] = None, +) -> GradeLayout: + """Resolve the output layout for a product request.""" + if output_layout is not None: + check_layout_spec(spec, output_layout, "output_layout") + if output_grades is not None and output_layout.grades != normalize_grades( + output_grades, spec.n, name="output_grades" + ): + raise ValueError("output_layout and output_grades disagree") + return output_layout + + if output_grades is None: + output_grades = expand_output_grades(left_layout.grades, right_layout.grades, spec.n, op=op) + return spec.layout(output_grades) + + +def check_layout_spec(spec: AlgebraSpec, layout: GradeLayout, name: str) -> None: + """Validate that a layout belongs to ``spec``.""" + if layout.spec != spec: + raise ValueError(f"{name} signature {layout.spec} does not match product spec {spec}") + + +def is_compact_tensor(spec: AlgebraSpec, tensor: torch.Tensor, layout: GradeLayout) -> bool: + """Return whether ``tensor`` already uses ``layout``'s compact lane count.""" + return layout.dim != spec.dim and tensor.shape[-1] == layout.dim + + +def _check_operand_shape( + spec: AlgebraSpec, + tensor: torch.Tensor, + layout: GradeLayout, + *, + compact: bool, + side: str, +) -> None: + expected = layout.dim if compact or is_compact_tensor(spec, tensor, layout) else spec.dim + if tensor.shape[-1] != expected: + storage = "compact" if expected == layout.dim else "dense" + raise ValueError(f"{side} {storage} last dimension must be {expected}, got {tensor.shape[-1]}") diff --git a/core/planning/translator.py b/core/planning/translator.py new file mode 100644 index 0000000..beef0d3 --- /dev/null +++ b/core/planning/translator.py @@ -0,0 +1,229 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Grade-aware translator from algebraic intent to static executors.""" + +from __future__ import annotations + +import torch + +from core.foundation.layout import AlgebraSpec, GradeLayout +from core.planning.grade_plan import GradeProductExecutor, build_grade_product_plan_from_request +from core.planning.request import ProductRequest, build_product_request, normalize_product_op +from core.planning.tree import build_grade_plan_tree +from core.planning.unary import ( + GradeUnaryExecutor, + UnaryRequest, + build_unary_plan_from_request, + build_unary_request, + normalize_unary_op, +) + + +class GradeTranslator: + """Owns layout and product-plan lowering for one algebra instance. + + The translator is deliberately not an ``nn.Module``. It builds static + executor modules keyed by signature, grades, dtype, and device, while the + algebra remains the source of truth for buffers and dense reference paths. + """ + + def __init__(self, algebra): + self.algebra = algebra + self.spec = AlgebraSpec.from_algebra(algebra) + self._product_executors = {} + self._unary_executors = {} + + def layout(self, grades): + """Return the compact layout for ``grades``.""" + return self.spec.layout(grades) + + def full_layout(self) -> GradeLayout: + """Return the full dense basis layout.""" + return self.layout(range(self.spec.n + 1)) + + def clear_cache(self) -> None: + """Drop cached executor modules.""" + self._product_executors.clear() + self._unary_executors.clear() + + def _apply(self, fn): + """Apply a PyTorch module-style transform to cached executor buffers.""" + for executor in self._product_executors.values(): + executor._apply(fn) + for executor in self._unary_executors.values(): + executor._apply(fn) + return self + + def product_executor( + self, + *, + op: str, + left_grades, + right_grades, + output_grades, + dtype, + device, + cache: bool = True, + ): + """Return a cached static executor for a projected bilinear product.""" + request = ProductRequest( + spec=self.spec, + op=normalize_product_op(op), + left_layout=self.layout(left_grades), + right_layout=self.layout(right_grades), + output_layout=self.layout(output_grades), + left_compact=False, + right_compact=False, + dtype=dtype, + device=torch.device(device), + ) + return self.executor_for_request(request, cache=cache) + + def executor_for_request(self, request: ProductRequest, *, cache: bool = True) -> GradeProductExecutor: + """Return an executor for an already normalized product request.""" + key = request.cache_key + executor = self._product_executors.get(key) if cache else None + if executor is None: + plan = build_grade_product_plan_from_request(request) + executor = GradeProductExecutor(plan) + if cache: + self._product_executors[key] = executor + return executor + + def product_tree(self, *, op: str, left_grades, right_grades, output_grades=None): + """Return planner-only grade tree metadata for a product route.""" + return build_grade_plan_tree( + self.spec, + op=op, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + ) + + def unary_executor( + self, + *, + op: str, + input_grades, + output_grades=None, + dtype, + device, + cache: bool = True, + ) -> GradeUnaryExecutor: + """Return a cached static executor for a unary operation.""" + op = normalize_unary_op(op) + if op == "grade_projection" and output_grades is None: + raise ValueError("output_grades is required for grade_projection") + input_layout = self.layout(input_grades) + output_layout = input_layout if output_grades is None else self.layout(output_grades) + request = UnaryRequest( + spec=self.spec, + op=op, + input_layout=input_layout, + output_layout=output_layout, + input_compact=False, + dtype=dtype, + device=torch.device(device), + ) + return self.unary_executor_for_request(request, cache=cache) + + def unary_executor_for_request(self, request: UnaryRequest, *, cache: bool = True) -> GradeUnaryExecutor: + """Return an executor for an already normalized unary request.""" + key = request.cache_key + executor = self._unary_executors.get(key) if cache else None + if executor is None: + plan = build_unary_plan_from_request(request) + executor = GradeUnaryExecutor(plan) + if cache: + self._unary_executors[key] = executor + return executor + + def planned_unary( + self, + values: torch.Tensor, + *, + op: str, + input_grades=None, + output_grades=None, + input_layout: GradeLayout = None, + output_layout: GradeLayout = None, + input_compact: bool = False, + compact_output: bool = False, + return_layout: bool = False, + ): + """Execute a unary operation using a static gather/sign plan.""" + request = build_unary_request( + self.spec, + values, + op=op, + input_grades=input_grades, + output_grades=output_grades, + input_layout=input_layout, + output_layout=output_layout, + input_compact=input_compact, + full_layout_allowed=self._full_layout_allowed(), + ) + executor = self.unary_executor_for_request(request) + output = executor.forward_compact(values) if request.input_compact else executor(values) + + if return_layout: + return output, executor.output_layout + if compact_output: + return output + return executor.output_layout.dense(output) + + def projected_product( + self, + A: torch.Tensor, + B: torch.Tensor, + *, + left_grades=None, + right_grades=None, + output_grades=None, + left_layout: GradeLayout = None, + right_layout: GradeLayout = None, + output_layout: GradeLayout = None, + op: str = "gp", + left_compact: bool = False, + right_compact: bool = False, + compact_output: bool = False, + return_layout: bool = False, + ): + """Execute a projected product using dense or compact input lanes.""" + request = build_product_request( + self.spec, + A, + B, + op=op, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + left_layout=left_layout, + right_layout=right_layout, + output_layout=output_layout, + left_compact=left_compact, + right_compact=right_compact, + full_layout_allowed=self._full_layout_allowed(), + ) + executor = self.executor_for_request(request) + + if request.left_compact or request.right_compact: + A_values = A if request.left_compact else executor.left_layout.compact(A) + B_values = B if request.right_compact else executor.right_layout.compact(B) + values = executor.forward_compact(A_values, B_values) + else: + values = executor(A, B) + + if return_layout: + return values, executor.output_layout + if compact_output: + return values + return executor.output_layout.dense(values) + + def _full_layout_allowed(self) -> bool: + return bool(getattr(self.algebra, "allow_full_layout_products", True)) diff --git a/core/planning/tree.py b/core/planning/tree.py new file mode 100644 index 0000000..48da362 --- /dev/null +++ b/core/planning/tree.py @@ -0,0 +1,136 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Planner-only grade tree metadata. + +The tree here is not a runtime backend. It groups declared grade routes before +they are lowered into flat Torch executor buffers. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from typing import Iterable, Optional + +from core.foundation.basis import GradeProductOp, expand_output_grades, normalize_grades +from core.foundation.layout import AlgebraSpec + + +@dataclass(frozen=True) +class GradePathNode: + """One homogeneous left-grade/right-grade route in a product plan.""" + + path_index: int + left_grade: int + right_grade: int + output_grades: tuple[int, ...] + left_dim: int + right_dim: int + + @property + def estimated_pairs(self) -> int: + """Upper-bound number of basis pairs before metric zero pruning.""" + return self.left_dim * self.right_dim + + +@dataclass(frozen=True) +class GradePlanTree: + """Planner-side grouping for a grade-restricted product.""" + + spec: AlgebraSpec + op: GradeProductOp + left_grades: tuple[int, ...] + right_grades: tuple[int, ...] + output_grades: tuple[int, ...] + paths: tuple[GradePathNode, ...] + chunk_pair_limit: Optional[int] = None + + @property + def path_count(self) -> int: + """Number of active homogeneous product routes.""" + return len(self.paths) + + @property + def estimated_pairs(self) -> int: + """Upper-bound number of basis pairs across all paths.""" + return sum(path.estimated_pairs for path in self.paths) + + @property + def estimated_chunks(self) -> int: + """Number of planner chunks implied by ``chunk_pair_limit``.""" + if self.chunk_pair_limit is None or self.chunk_pair_limit <= 0: + return 1 if self.paths else 0 + return sum(math.ceil(path.estimated_pairs / self.chunk_pair_limit) for path in self.paths) + + def path_for_grades(self, left_grade: int, right_grade: int) -> Optional[GradePathNode]: + """Return the active path for a homogeneous grade pair.""" + for path in self.paths: + if path.left_grade == left_grade and path.right_grade == right_grade: + return path + return None + + +def build_grade_plan_tree( + spec: AlgebraSpec, + *, + left_grades: Iterable[int], + right_grades: Iterable[int], + output_grades: Optional[Iterable[int]] = None, + op: GradeProductOp = "gp", + chunk_pair_limit: Optional[int] = None, +) -> GradePlanTree: + """Build planner metadata for grade route grouping.""" + left = normalize_grades(left_grades, spec.n, name="left_grades") + right = normalize_grades(right_grades, spec.n, name="right_grades") + output = ( + expand_output_grades(left, right, spec.n, op=op) + if output_grades is None + else normalize_grades(output_grades, spec.n, name="output_grades") + ) + output_set = set(output) + + paths = [] + for left_grade in left: + left_dim = _grade_dim(spec.n, left_grade) + for right_grade in right: + route_outputs = expand_output_grades((left_grade,), (right_grade,), spec.n, op=op) + route_outputs = tuple(grade for grade in route_outputs if grade in output_set) + if not route_outputs: + continue + paths.append( + GradePathNode( + path_index=len(paths), + left_grade=left_grade, + right_grade=right_grade, + output_grades=route_outputs, + left_dim=left_dim, + right_dim=_grade_dim(spec.n, right_grade), + ) + ) + + return GradePlanTree( + spec=spec, + op=op, + left_grades=left, + right_grades=right, + output_grades=output, + paths=tuple(paths), + chunk_pair_limit=chunk_pair_limit, + ) + + +def _grade_dim(n: int, grade: int) -> int: + if grade < 0 or grade > n: + return 0 + grade = min(grade, n - grade) + numerator = 1 + denominator = 1 + for i in range(1, grade + 1): + numerator *= n - grade + i + denominator *= i + return numerator // denominator diff --git a/core/planning/unary.py b/core/planning/unary.py new file mode 100644 index 0000000..7b70512 --- /dev/null +++ b/core/planning/unary.py @@ -0,0 +1,252 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Compile-friendly planned unary operators.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal, Optional + +import torch +import torch.nn as nn + +from core.foundation.basis import normalize_grades, reverse_sign +from core.foundation.layout import AlgebraSpec, GradeLayout +from core.planning.request import check_layout_spec, is_compact_tensor, resolve_operand_layout + +GradeUnaryOp = Literal["identity", "reverse", "grade_involution", "clifford_conjugation", "grade_projection"] +_VALID_UNARY_OPS = {"identity", "reverse", "grade_involution", "clifford_conjugation", "grade_projection"} + + +@dataclass(frozen=True) +class UnaryRequest: + """Fully resolved request for one unary planned operation.""" + + spec: AlgebraSpec + op: GradeUnaryOp + input_layout: GradeLayout + output_layout: GradeLayout + input_compact: bool + dtype: torch.dtype + device: torch.device + + @property + def input_grades(self) -> tuple[int, ...]: + return self.input_layout.grades + + @property + def output_grades(self) -> tuple[int, ...]: + return self.output_layout.grades + + @property + def cache_key(self) -> tuple[object, ...]: + return ( + self.spec, + str(self.device), + str(self.dtype), + self.op, + self.input_grades, + self.output_grades, + ) + + +class GradeUnaryPlan: + """Static gather/sign plan for one unary operation.""" + + def __init__( + self, + *, + spec: AlgebraSpec, + op: GradeUnaryOp, + input_grades: tuple[int, ...], + output_grades: tuple[int, ...], + input_positions: torch.Tensor, + output_indices: torch.Tensor, + signs: torch.Tensor, + ): + self.spec = spec + self.op = op + self.input_layout = spec.layout(input_grades) + self.output_layout = spec.layout(output_grades) + self.input_positions = input_positions + self.output_indices = output_indices + self.signs = signs + + @property + def dim(self) -> int: + return self.spec.dim + + @property + def output_dim(self) -> int: + return self.output_layout.dim + + +class GradeUnaryExecutor(nn.Module): + """Torch module for planned unary gather/sign execution.""" + + def __init__(self, plan: GradeUnaryPlan): + super().__init__() + self.spec = plan.spec + self.op = plan.op + self.input_layout = plan.input_layout + self.output_layout = plan.output_layout + self.dim = plan.dim + self.register_buffer("input_positions", plan.input_positions, persistent=False) + self.register_buffer("output_indices", plan.output_indices, persistent=False) + self.register_buffer("signs", plan.signs, persistent=False) + + @property + def output_dim(self) -> int: + return self.output_layout.dim + + def forward(self, values: torch.Tensor) -> torch.Tensor: + """Return compact output lanes for dense input coefficients.""" + if values.shape[-1] != self.dim: + raise ValueError(f"dense last dimension must be {self.dim}, got {values.shape[-1]}") + output = torch.index_select(values, -1, self.output_indices) + return output * self.signs.to(dtype=output.dtype) + + def forward_compact(self, values: torch.Tensor) -> torch.Tensor: + """Return compact output lanes for compact input coefficients.""" + if values.shape[-1] != self.input_layout.dim: + raise ValueError(f"compact last dimension must be {self.input_layout.dim}, got {values.shape[-1]}") + output = torch.index_select(values, -1, self.input_positions) + return output * self.signs.to(dtype=output.dtype) + + def forward_dense(self, values: torch.Tensor) -> torch.Tensor: + """Return dense output coefficients for dense input coefficients.""" + compact = self.forward(values) + return self.output_layout.dense(compact) + + +def build_unary_request( + spec: AlgebraSpec, + values: torch.Tensor, + *, + op: str, + input_grades=None, + output_grades=None, + input_layout: Optional[GradeLayout] = None, + output_layout: Optional[GradeLayout] = None, + input_compact: bool = False, + full_layout_allowed: bool = True, +) -> UnaryRequest: + """Resolve caller input into a static unary request.""" + op = normalize_unary_op(op) + if op == "grade_projection" and input_grades is None and input_layout is None and not input_compact: + if output_layout is not None: + input_layout = output_layout + elif output_grades is not None: + input_grades = output_grades + input_layout = resolve_operand_layout( + spec, + values, + grades=input_grades, + layout=input_layout, + compact=input_compact, + side="input", + full_layout_allowed=full_layout_allowed, + ) + output_layout = resolve_unary_output_layout( + spec, + op=op, + input_layout=input_layout, + output_grades=output_grades, + output_layout=output_layout, + ) + input_compact = input_compact or is_compact_tensor(spec, values, input_layout) + return UnaryRequest( + spec=spec, + op=op, + input_layout=input_layout, + output_layout=output_layout, + input_compact=input_compact, + dtype=values.dtype, + device=values.device, + ) + + +def build_unary_plan_from_request(request: UnaryRequest) -> GradeUnaryPlan: + """Lower a unary request into static gather/sign buffers.""" + input_position_by_index = {index: pos for pos, index in enumerate(request.input_layout.basis_indices)} + input_positions = [] + signs = [] + for index in request.output_layout.basis_indices: + position = input_position_by_index.get(index) + if position is None: + raise ValueError( + f"output basis index {index} is not available in input grades {request.input_layout.grades}" + ) + input_positions.append(position) + signs.append(_unary_sign(request.op, index)) + + return GradeUnaryPlan( + spec=request.spec, + op=request.op, + input_grades=request.input_grades, + output_grades=request.output_grades, + input_positions=torch.tensor(input_positions, dtype=torch.long, device=request.device), + output_indices=torch.tensor(request.output_layout.basis_indices, dtype=torch.long, device=request.device), + signs=torch.tensor(signs, dtype=request.dtype, device=request.device), + ) + + +def normalize_unary_op(op: str) -> GradeUnaryOp: + """Validate and normalize a unary operation name.""" + normalized = str(op) + if normalized not in _VALID_UNARY_OPS: + raise ValueError(f"Unsupported grade unary op {op!r}") + return normalized # type: ignore[return-value] + + +def resolve_unary_output_layout( + spec: AlgebraSpec, + *, + op: GradeUnaryOp, + input_layout: GradeLayout, + output_grades=None, + output_layout: Optional[GradeLayout] = None, +) -> GradeLayout: + """Resolve output layout for a planned unary operation.""" + if output_layout is not None: + check_layout_spec(spec, output_layout, "output_layout") + if output_grades is not None and output_layout.grades != normalize_grades( + output_grades, spec.n, name="output_grades" + ): + raise ValueError("output_layout and output_grades disagree") + return output_layout + + if op == "grade_projection": + if output_grades is None: + raise ValueError("output_grades is required for grade_projection") + projected = normalize_grades(output_grades, spec.n, name="output_grades") + missing = tuple(grade for grade in projected if grade not in input_layout.grades) + if missing: + raise ValueError(f"Cannot project missing grades {missing} from input grades {input_layout.grades}") + return spec.layout(projected) + + if output_grades is not None: + projected = normalize_grades(output_grades, spec.n, name="output_grades") + missing = tuple(grade for grade in projected if grade not in input_layout.grades) + if missing: + raise ValueError(f"Cannot project missing grades {missing} from input grades {input_layout.grades}") + return spec.layout(projected) + return input_layout + + +def _unary_sign(op: GradeUnaryOp, index: int) -> float: + grade = int(index).bit_count() + if op in {"identity", "grade_projection"}: + return 1.0 + if op == "reverse": + return reverse_sign(index) + if op == "grade_involution": + return -1.0 if grade % 2 else 1.0 + if op == "clifford_conjugation": + return (-1.0 if grade % 2 else 1.0) * reverse_sign(index) + raise ValueError(f"Unsupported grade unary op {op!r}") diff --git a/core/runtime/__init__.py b/core/runtime/__init__.py new file mode 100644 index 0000000..e510381 --- /dev/null +++ b/core/runtime/__init__.py @@ -0,0 +1,20 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Runtime algebra hosts and dense reference operations.""" + +from .algebra import CliffordAlgebra +from .context import AlgebraContext +from .multivector import Multivector +from .projected import ProjectedProductMixin + +__all__ = [ + "AlgebraContext", + "CliffordAlgebra", + "Multivector", + "ProjectedProductMixin", +] diff --git a/core/algebra.py b/core/runtime/algebra.py similarity index 97% rename from core/algebra.py rename to core/runtime/algebra.py index 56e749e..31260bc 100644 --- a/core/algebra.py +++ b/core/runtime/algebra.py @@ -17,10 +17,11 @@ import torch import torch.nn as nn -from core.validation import check_multivector +from core.foundation.validation import check_multivector +from core.runtime.projected import ProjectedProductMixin -class CliffordAlgebra(nn.Module): +class CliffordAlgebra(ProjectedProductMixin, nn.Module): """Differentiable Clifford algebra kernel with memory-optimized blocked accumulation. Extends ``nn.Module`` so that all Cayley tables are registered as @@ -50,6 +51,7 @@ def __init__( dtype: torch.dtype = torch.float32, exp_policy: str = "balanced", fixed_iterations: Optional[int] = None, + allow_large_dense: bool = False, ): """Initialize the algebra and cache the Cayley table. @@ -64,11 +66,11 @@ def __init__( precision (e.g. AMP on CUDA bfloat16 mode). exp_policy (str or ExpPolicy, optional): Bivector exp policy. ``'balanced'`` (default) or ``'precise'``. - See :class:`core.decomposition.ExpPolicy`. + See :class:`core.runtime.decomposition.ExpPolicy`. fixed_iterations (int, optional): Power-iteration step count for the compiled-safe decomposed exp path (used when n>=4). ``None`` (default) auto-derives from ``(exp_policy, dtype, n)`` - via :func:`core.decomposition.resolve_fixed_iterations`, + via :func:`core.runtime.decomposition.resolve_fixed_iterations`, pinned statically at init. """ super().__init__() @@ -76,7 +78,11 @@ def __init__( assert p >= 0, f"p must be non-negative, got {p}" assert q >= 0, f"q must be non-negative, got {q}" assert r >= 0, f"r must be non-negative, got {r}" - assert p + q + r <= 12, f"p + q + r must be <= 12, got {p + q + r}" + max_dense_n = 12 if allow_large_dense else 8 + assert p + q + r <= max_dense_n, ( + f"p + q + r must be <= {max_dense_n} for dense CliffordAlgebra, got {p + q + r}. " + "Use make_algebra(..., kernel='auto') for AlgebraContext or kernel='dense' to explicitly allow Cl9-Cl12." + ) self.p, self.q, self.r = p, q, r self.n = p + q + r @@ -91,7 +97,7 @@ def __init__( self._exp_regime = "mixed" # Exp policy: controls decomposition iteration budget - from core.decomposition import ExpPolicy, resolve_fixed_iterations + from core.runtime.decomposition import ExpPolicy, resolve_fixed_iterations self._exp_policy = exp_policy if isinstance(exp_policy, ExpPolicy) else ExpPolicy(exp_policy) @@ -163,6 +169,10 @@ def __init__( self.eps: float = float(_finfo.eps) self.eps_sq: float = float(_finfo.eps**2) + from core.planning.translator import GradeTranslator + + self.translator = GradeTranslator(self) + @property def device(self): """Return the device of the algebra tables.""" @@ -202,7 +212,7 @@ def exp_policy(self): @exp_policy.setter def exp_policy(self, value): - from core.decomposition import ExpPolicy, resolve_fixed_iterations + from core.runtime.decomposition import ExpPolicy, resolve_fixed_iterations self._exp_policy = value if isinstance(value, ExpPolicy) else ExpPolicy(value) self._exp_fixed_iterations = resolve_fixed_iterations(self._exp_policy, self.dtype, self.n) @@ -1057,7 +1067,7 @@ def _exp_compiled_safe(self, B: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Rotor exp(B) [..., dim]. """ - from core.decomposition import compiled_safe_decomposed_exp + from core.runtime.decomposition import compiled_safe_decomposed_exp R_closed = self._exp_bivector_closed(B) R_decomposed = compiled_safe_decomposed_exp(self, B, fixed_iterations=self._exp_fixed_iterations) diff --git a/core/runtime/context.py b/core/runtime/context.py new file mode 100644 index 0000000..099568f --- /dev/null +++ b/core/runtime/context.py @@ -0,0 +1,176 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Lightweight algebra context for planned high-dimensional execution.""" + +from __future__ import annotations + +from typing import Iterable, Optional + +import torch + +from core.foundation.basis import normalize_grades +from core.foundation.device import resolve_device, resolve_dtype +from core.foundation.layout import AlgebraSpec, GradeLayout +from core.planning.translator import GradeTranslator +from core.runtime.projected import ProjectedProductMixin + + +class AlgebraContext(ProjectedProductMixin): + """Signature and planning host without dense Cayley-table materialization.""" + + def __init__( + self, + p: int, + q: int = 0, + r: int = 0, + *, + device="cuda", + dtype: torch.dtype = torch.float32, + default_grades: Optional[Iterable[int]] = None, + allow_full_layout_products: Optional[bool] = None, + ): + if p < 0 or q < 0 or r < 0: + raise ValueError(f"signature counts must be non-negative, got Cl({p},{q},{r})") + + self.p = int(p) + self.q = int(q) + self.r = int(r) + self.n = self.p + self.q + self.r + self.dim = 1 << self.n + self.num_grades = self.n + 1 + self.spec = AlgebraSpec(self.p, self.q, self.r) + self._device = torch.device(resolve_device(device) if str(device) == "auto" else device) + self._dtype = resolve_dtype(dtype) + self.allow_full_layout_products = self.n <= 8 if allow_full_layout_products is None else bool( + allow_full_layout_products + ) + self._default_grades = None if default_grades is None else normalize_grades(default_grades, self.n) + self._default_layout: Optional[GradeLayout] = None + self.translator = GradeTranslator(self) + self._sync_eps() + + @property + def device(self): + """Return the context device used for planned executor buffers.""" + return self._device + + @property + def dtype(self) -> torch.dtype: + """Return the context floating-point dtype.""" + return self._dtype + + def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: + """Return a compact layout, or the full default layout when omitted.""" + if grades is None: + if self._default_layout is None: + if self._default_grades is None: + if not self.allow_full_layout_products: + raise ValueError( + "AlgebraContext has no default layout. Declare active grades for high-dimensional use." + ) + grades = range(self.num_grades) + else: + grades = self._default_grades + self._default_layout = self.translator.layout(grades) + return self._default_layout + return self.translator.layout(grades) + + def _apply(self, fn): + """Apply a PyTorch module-style device/dtype transform to cached executors.""" + probe = fn(torch.empty((), device=self.device, dtype=self.dtype)) + self._device = probe.device + if probe.dtype.is_floating_point: + self._dtype = probe.dtype + self._sync_eps() + self.translator._apply(fn) + return self + + def to(self, device=None, dtype=None): + """Move the context and cached executors.""" + if device is not None: + self._device = torch.device(resolve_device(device) if str(device) == "auto" else device) + if dtype is not None: + self._dtype = resolve_dtype(dtype) + self._sync_eps() + self.translator.clear_cache() + return self + + def geometric_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Plan and execute a geometric product.""" + return self.projected_product(A, B, op="gp", **kwargs) + + def wedge(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Plan and execute an exterior product.""" + return self.projected_product(A, B, op="wedge", **kwargs) + + def inner_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Plan and execute the symmetric inner product route.""" + return self.projected_product(A, B, op="inner", **kwargs) + + def commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Plan and execute a commutator product.""" + return self.projected_product(A, B, op="commutator", **kwargs) + + def anti_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Plan and execute an anti-commutator product.""" + return self.projected_product(A, B, op="anti_commutator", **kwargs) + + def grade_projection(self, mv: torch.Tensor, grade: int) -> torch.Tensor: + """Project a dense multivector tensor to one grade.""" + return self.planned_unary(mv, op="grade_projection", output_grades=(int(grade),)) + + def embed_vector(self, vectors: torch.Tensor) -> torch.Tensor: + """Embed grade-1 vector coordinates into dense multivector coefficients.""" + if vectors.shape[-1] != self.n: + raise ValueError(f"vectors last dimension must be {self.n}, got {vectors.shape[-1]}") + output = vectors.new_zeros(*vectors.shape[:-1], self.dim) + basis_indices = [1 << bit for bit in range(self.n)] + return output.index_copy(-1, torch.tensor(basis_indices, dtype=torch.long, device=vectors.device), vectors) + + def reverse(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: + """Reverse dense or compact multivector coefficients.""" + return self.planned_unary(mv, op="reverse", **kwargs) + + def grade_involution(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply grade involution to dense or compact multivector coefficients.""" + return self.planned_unary(mv, op="grade_involution", **kwargs) + + def clifford_conjugation(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply Clifford conjugation to dense or compact multivector coefficients.""" + return self.planned_unary(mv, op="clifford_conjugation", **kwargs) + + def planned_unary( + self, + values: torch.Tensor, + *, + op: str, + input_grades=None, + output_grades=None, + input_layout: Optional[GradeLayout] = None, + output_layout: Optional[GradeLayout] = None, + input_compact: bool = False, + compact_output: bool = False, + return_layout: bool = False, + ): + """Execute a unary planned operation.""" + return self.translator.planned_unary( + values, + op=op, + input_grades=input_grades, + output_grades=output_grades, + input_layout=input_layout, + output_layout=output_layout, + input_compact=input_compact, + compact_output=compact_output, + return_layout=return_layout, + ) + + def _sync_eps(self) -> None: + finfo = torch.finfo(self.dtype) + self.eps = float(finfo.eps) + self.eps_sq = float(finfo.eps**2) diff --git a/core/decomposition.py b/core/runtime/decomposition.py similarity index 100% rename from core/decomposition.py rename to core/runtime/decomposition.py diff --git a/core/metric.py b/core/runtime/metric.py similarity index 99% rename from core/metric.py rename to core/runtime/metric.py index c9c23f9..3aee0b4 100644 --- a/core/metric.py +++ b/core/runtime/metric.py @@ -13,7 +13,7 @@ import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra def _hermitian_signs(algebra: CliffordAlgebra) -> torch.Tensor: diff --git a/core/multivector.py b/core/runtime/multivector.py similarity index 64% rename from core/multivector.py rename to core/runtime/multivector.py index 1e6ecd8..51155bf 100644 --- a/core/multivector.py +++ b/core/runtime/multivector.py @@ -11,7 +11,8 @@ import torch -from core.module import AlgebraLike +from core.foundation.layout import GradeLayout +from core.foundation.module import AlgebraLike class Multivector: @@ -22,14 +23,38 @@ class Multivector: Attributes: algebra (AlgebraLike): The backend. - tensor (torch.Tensor): The raw data [..., Dim]. + tensor (torch.Tensor): Dense coefficients [..., Dim]. + values (torch.Tensor): Optional compact lane values [..., layout.dim]. + layout (GradeLayout): Optional compact layout for ``values``. """ - __slots__ = ("algebra", "tensor") + __slots__ = ("_tensor", "algebra", "layout", "values") - def __init__(self, algebra: AlgebraLike, tensor: torch.Tensor): + def __init__( + self, + algebra: AlgebraLike, + tensor: torch.Tensor = None, + *, + values: torch.Tensor = None, + layout: GradeLayout = None, + ): self.algebra = algebra - self.tensor = tensor + self.layout = layout + if layout is None: + if tensor is None: + raise ValueError("tensor is required when layout is not provided") + self._tensor = tensor + self.values = None + else: + self._check_layout(layout) + if values is None: + if tensor is None: + raise ValueError("values or tensor is required when layout is provided") + values = layout.compact(tensor) + if values.shape[-1] != layout.dim: + raise ValueError(f"compact values last dimension must be {layout.dim}, got {values.shape[-1]}") + self._tensor = None + self.values = values @classmethod def from_vectors(cls, algebra: AlgebraLike, vectors: torch.Tensor) -> Multivector: @@ -47,7 +72,57 @@ def scalar( return cls(algebra, t) def __repr__(self): - return f"Multivector(shape={self.tensor.shape}, algebra=Cl({self.algebra.p},{self.algebra.q},{self.algebra.r}))" + storage = "compact" if self.is_compact else "dense" + return ( + f"Multivector(shape={self.tensor.shape}, storage={storage}, " + f"algebra=Cl({self.algebra.p},{self.algebra.q},{self.algebra.r}))" + ) + + @property + def tensor(self) -> torch.Tensor: + """Dense coefficient tensor. + + This property remains dense for backward compatibility. Performance + paths that operate on compact data should use ``values`` directly. + """ + if self._tensor is not None: + return self._tensor + return self.layout.dense(self.values) + + @tensor.setter + def tensor(self, value: torch.Tensor) -> None: + self._tensor = value + self.values = None + self.layout = None + + @property + def is_compact(self) -> bool: + """Whether this multivector stores compact grade lanes.""" + return self.layout is not None + + def dense(self) -> Multivector: + """Return a dense-storage multivector.""" + return Multivector(self.algebra, self.tensor) + + def compact(self, grades) -> Multivector: + """Return a compact-storage multivector containing ``grades``.""" + layout = self.algebra.translator.layout(grades) + return self.with_layout(layout) + + def with_layout(self, layout: GradeLayout) -> Multivector: + """Return this multivector represented by ``layout``.""" + self._check_layout(layout) + if self.layout == layout: + return Multivector(self.algebra, values=self.values, layout=layout) + return Multivector(self.algebra, values=layout.compact(self.tensor), layout=layout) + + def _check_layout(self, layout: GradeLayout) -> None: + spec = layout.spec + if (spec.p, spec.q, spec.r) != (self.algebra.p, self.algebra.q, self.algebra.r): + raise ValueError( + f"Layout mismatch: Cl({spec.p},{spec.q},{spec.r}) vs " + f"Cl({self.algebra.p},{self.algebra.q},{self.algebra.r})" + ) def _check_algebra(self, other: Multivector) -> None: s, o = self.algebra, other.algebra @@ -57,6 +132,9 @@ def _check_algebra(self, other: Multivector) -> None: def _wrap(self, tensor: torch.Tensor) -> Multivector: return Multivector(self.algebra, tensor) + def _wrap_compact(self, values: torch.Tensor, layout: GradeLayout) -> Multivector: + return Multivector(self.algebra, values=values, layout=layout) + def __add__(self, other): if isinstance(other, Multivector): self._check_algebra(other) @@ -154,6 +232,37 @@ def geometric_product(self, other: Multivector) -> Multivector: self._check_algebra(other) return self._wrap(self.algebra.geometric_product(self.tensor, other.tensor)) + def projected_product( + self, + other: Multivector, + *, + output_grades=None, + op: str = "gp", + left_grades=None, + right_grades=None, + ) -> Multivector: + """Grade-projected product using compact layouts when available.""" + self._check_algebra(other) + left_layout = self.layout if self.is_compact else None + right_layout = other.layout if other.is_compact else None + left_grades = left_grades if left_grades is not None else _layout_grades(left_layout) + right_grades = right_grades if right_grades is not None else _layout_grades(right_layout) + + values, layout = self.algebra.projected_product( + self.values if self.is_compact else self.tensor, + other.values if other.is_compact else other.tensor, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + left_layout=left_layout, + right_layout=right_layout, + op=op, + left_compact=self.is_compact, + right_compact=other.is_compact, + return_layout=True, + ) + return self._wrap_compact(values, layout) + def wedge(self, other: Multivector) -> Multivector: """Wedge (outer) product (same as ``self ^ other``).""" self._check_algebra(other) @@ -186,7 +295,7 @@ def anti_commutator(self, other: Multivector) -> Multivector: def norm(self) -> torch.Tensor: """Induced metric norm (returns scalar tensor).""" - from core.metric import induced_norm + from core.runtime.metric import induced_norm return induced_norm(self.algebra, self.tensor) @@ -239,19 +348,28 @@ def blade_reject(self, blade: Multivector) -> Multivector: def to(self, *args, **kwargs) -> Multivector: """Move/cast the underlying tensor (same API as ``torch.Tensor.to``).""" + if self.is_compact: + return Multivector(self.algebra, values=self.values.to(*args, **kwargs), layout=self.layout) return self._wrap(self.tensor.to(*args, **kwargs)) def detach(self) -> Multivector: """Detach from computation graph.""" + if self.is_compact: + return Multivector(self.algebra, values=self.values.detach(), layout=self.layout) return self._wrap(self.tensor.detach()) def clone(self) -> Multivector: """Clone the underlying tensor.""" + if self.is_compact: + return Multivector(self.algebra, values=self.values.clone(), layout=self.layout) return self._wrap(self.tensor.clone()) def requires_grad_(self, requires_grad: bool = True) -> Multivector: """Set requires_grad in-place.""" - self.tensor.requires_grad_(requires_grad) + if self.is_compact: + self.values.requires_grad_(requires_grad) + else: + self.tensor.requires_grad_(requires_grad) return self @property @@ -260,8 +378,12 @@ def shape(self) -> torch.Size: @property def device(self) -> torch.device: - return self.tensor.device + return self.values.device if self.is_compact else self.tensor.device @property def dtype(self) -> torch.dtype: - return self.tensor.dtype + return self.values.dtype if self.is_compact else self.tensor.dtype + + +def _layout_grades(layout: GradeLayout) -> tuple[int, ...] | None: + return None if layout is None else layout.grades diff --git a/core/runtime/projected.py b/core/runtime/projected.py new file mode 100644 index 0000000..7f30e24 --- /dev/null +++ b/core/runtime/projected.py @@ -0,0 +1,80 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Shared projected-product facade for algebra hosts.""" + +from __future__ import annotations + +import torch + +from core.foundation.validation import check_multivector + + +class ProjectedProductMixin: + """Route declared grade products through an algebra's grade translator.""" + + def projected_product( + self, + A: torch.Tensor, + B: torch.Tensor, + *, + left_grades=None, + right_grades=None, + output_grades=None, + left_layout=None, + right_layout=None, + output_layout=None, + op: str = "gp", + left_compact: bool = False, + right_compact: bool = False, + compact_output: bool = False, + return_layout: bool = False, + ): + """Compute a declared grade-restricted product through the translator.""" + if not left_compact and left_layout is not None and A.shape[-1] == left_layout.dim: + left_compact = left_layout.dim != self.dim + if not right_compact and right_layout is not None and B.shape[-1] == right_layout.dim: + right_compact = right_layout.dim != self.dim + if not left_compact: + check_multivector(A, self, "projected_product(A)") + if not right_compact: + check_multivector(B, self, "projected_product(B)") + return self.translator.projected_product( + A, + B, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + left_layout=left_layout, + right_layout=right_layout, + output_layout=output_layout, + op=op, + left_compact=left_compact, + right_compact=right_compact, + compact_output=compact_output, + return_layout=return_layout, + ) + + def projected_geometric_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs): + """Projected geometric product convenience wrapper.""" + return self.projected_product(A, B, op="gp", **kwargs) + + def projected_wedge(self, A: torch.Tensor, B: torch.Tensor, **kwargs): + """Projected wedge product convenience wrapper.""" + return self.projected_product(A, B, op="wedge", **kwargs) + + def projected_inner_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs): + """Projected inner product convenience wrapper.""" + return self.projected_product(A, B, op="inner", **kwargs) + + def projected_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs): + """Projected commutator convenience wrapper.""" + return self.projected_product(A, B, op="commutator", **kwargs) + + def projected_anti_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs): + """Projected anti-commutator convenience wrapper.""" + return self.projected_product(A, B, op="anti_commutator", **kwargs) diff --git a/core/visualizer.py b/core/visualizer.py index 64d021d..10be229 100644 --- a/core/visualizer.py +++ b/core/visualizer.py @@ -12,7 +12,7 @@ from sklearn.decomposition import PCA from sklearn.manifold import TSNE -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra class GeneralVisualizer: diff --git a/docs/api/core.md b/docs/api/core.md index 8aa53db..3776c78 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -3,22 +3,22 @@ The mathematical kernel of Versor. ## Algebra -::: core.algebra.CliffordAlgebra +::: core.runtime.algebra.CliffordAlgebra ## Algebra Config ::: core.config ## Module -::: core.module.CliffordModule +::: core.foundation.module.CliffordModule ## Multivector -::: core.multivector.Multivector +::: core.runtime.multivector.Multivector ## Metric -::: core.metric +::: core.runtime.metric ## Decomposition -::: core.decomposition +::: core.runtime.decomposition ## Analysis ::: core.analysis.MetricSearch diff --git a/docs/design_guide.md b/docs/design_guide.md index b52c632..6926e0d 100644 --- a/docs/design_guide.md +++ b/docs/design_guide.md @@ -75,7 +75,7 @@ The canonical Geometric Blade Network block, annotated: ```python import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers.primitives.linear import CliffordLinear from layers.primitives.rotor import RotorLayer from layers.primitives.normalization import CliffordLayerNorm @@ -147,7 +147,7 @@ Versor models are intentionally hybrid. Standard `nn.Linear` and `CliffordLinear ```python import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers.primitives.linear import CliffordLinear from layers.primitives.rotor import RotorLayer from layers.primitives.normalization import CliffordLayerNorm @@ -237,7 +237,7 @@ End-to-end: choose algebra, build a 3-layer GBN, train, evaluate. Runs in under ```python import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers.primitives.linear import CliffordLinear from layers.primitives.rotor import RotorLayer from layers.primitives.normalization import CliffordLayerNorm diff --git a/docs/index.md b/docs/index.md index 74b8163..04bd3a9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -80,7 +80,7 @@ uv sync --extra all # everything ```python import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers.primitives.rotor import RotorLayer from layers.linear import CliffordLinear from functional.activation import GeometricGELU diff --git a/docs/tutorial.md b/docs/tutorial.md index 1e3e05b..1792103 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -7,7 +7,7 @@ A step-by-step guide to using Versor's geometric layers in your own models. Everything starts with a `CliffordAlgebra` instance. The signature $(p, q, r)$ determines the geometry: ```python -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra # 3D Euclidean (rotations in 3-space) algebra = CliffordAlgebra(p=3, q=0, r=0, device='cpu') @@ -183,7 +183,7 @@ All tasks inherit from `BaseTask` and implement 7 methods: ```python from tasks.base import BaseTask -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from functional.loss import GeometricMSELoss class MyTask(BaseTask): diff --git a/examples/datasets/amass.py b/examples/datasets/amass.py index 434b10e..81df07d 100644 --- a/examples/datasets/amass.py +++ b/examples/datasets/amass.py @@ -9,7 +9,7 @@ import torch from torch.utils.data import Dataset -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra class AMASSDataset(Dataset): diff --git a/examples/datasets/synthetic.py b/examples/datasets/synthetic.py index 84587c2..3ce5f14 100644 --- a/examples/datasets/synthetic.py +++ b/examples/datasets/synthetic.py @@ -9,7 +9,7 @@ import torch from torch.utils.data import Dataset -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra class Figure8Dataset(Dataset): diff --git a/examples/tasks/cgenn.py b/examples/tasks/cgenn.py index b840856..7be6ef0 100644 --- a/examples/tasks/cgenn.py +++ b/examples/tasks/cgenn.py @@ -61,7 +61,7 @@ from torch.utils.data import DataLoader, TensorDataset from core.config import make_algebra_from_config -from core.module import CliffordModule +from core.foundation.module import CliffordModule from functional.activation import GeometricSquare from layers import ( BladeSelector, diff --git a/examples/tasks/clifford_pde.py b/examples/tasks/clifford_pde.py index 0581dce..9c9379e 100644 --- a/examples/tasks/clifford_pde.py +++ b/examples/tasks/clifford_pde.py @@ -67,7 +67,7 @@ from torch.utils.data import DataLoader, TensorDataset from core.config import make_algebra_from_config -from core.module import CliffordModule +from core.foundation.module import CliffordModule from functional.activation import GeometricGELU from layers import ( CliffordLayerNorm, diff --git a/examples/tasks/hyperbolic.py b/examples/tasks/hyperbolic.py index 11843b4..34e4059 100644 --- a/examples/tasks/hyperbolic.py +++ b/examples/tasks/hyperbolic.py @@ -10,7 +10,7 @@ import torch.nn as nn from core.config import make_algebra_from_config -from core.module import CliffordModule +from core.foundation.module import CliffordModule from core.visualizer import GeneralVisualizer from functional.loss import GeometricMSELoss from layers import RotorLayer diff --git a/examples/tasks/manifold.py b/examples/tasks/manifold.py index b46c398..e3a4ce1 100644 --- a/examples/tasks/manifold.py +++ b/examples/tasks/manifold.py @@ -10,7 +10,7 @@ from torch.utils.data import DataLoader from core.config import make_algebra_from_config -from core.module import CliffordModule +from core.foundation.module import CliffordModule from core.visualizer import GeneralVisualizer from examples.datasets.synthetic import Figure8Dataset from functional.loss import SubspaceLoss diff --git a/examples/tasks/sanity_check.py b/examples/tasks/sanity_check.py index 11c084a..394fc30 100644 --- a/examples/tasks/sanity_check.py +++ b/examples/tasks/sanity_check.py @@ -9,7 +9,7 @@ import torch.nn as nn from core.config import make_algebra_from_config -from core.module import CliffordModule +from core.foundation.module import CliffordModule from core.visualizer import GeneralVisualizer from functional.loss import GeometricMSELoss from layers import RotorLayer diff --git a/experiments/_gdo/benchmarks.py b/experiments/_gdo/benchmarks.py index 4dffeb7..76c6ef6 100644 --- a/experiments/_gdo/benchmarks.py +++ b/experiments/_gdo/benchmarks.py @@ -31,7 +31,7 @@ import torch.nn as nn import torch.nn.functional as F -from core.module import CliffordModule +from core.foundation.module import CliffordModule from experiments._lib import setup_algebra from functional.activation import GeometricGELU from layers import ( diff --git a/experiments/_gdo/controller.py b/experiments/_gdo/controller.py index 4773066..3c06593 100644 --- a/experiments/_gdo/controller.py +++ b/experiments/_gdo/controller.py @@ -9,7 +9,7 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from optimizers.riemannian import MANIFOLD_EUCLIDEAN from .config import GDOConfig diff --git a/experiments/_gdo/harness.py b/experiments/_gdo/harness.py index 17db9e2..affdcc8 100644 --- a/experiments/_gdo/harness.py +++ b/experiments/_gdo/harness.py @@ -9,7 +9,7 @@ import torch.nn as nn from torch.optim import Optimizer -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers import MultiRotorLayer, RotorLayer from optimizers.riemannian import ExponentialSGD, RiemannianAdam diff --git a/experiments/_gdo/optimizer.py b/experiments/_gdo/optimizer.py index 4412466..1bceb7f 100644 --- a/experiments/_gdo/optimizer.py +++ b/experiments/_gdo/optimizer.py @@ -8,7 +8,7 @@ import torch.nn as nn from torch.optim import Optimizer -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from optimizers.riemannian import ( MANIFOLD_EUCLIDEAN, MANIFOLD_SPHERE, diff --git a/experiments/_gdo/parameter_groups.py b/experiments/_gdo/parameter_groups.py index 2686079..12fb99f 100644 --- a/experiments/_gdo/parameter_groups.py +++ b/experiments/_gdo/parameter_groups.py @@ -8,7 +8,6 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra from core.analysis import ( CommutatorAnalyzer as CoreCommutatorAnalyzer, ) @@ -16,6 +15,7 @@ GeodesicFlow, SpectralAnalyzer, ) +from core.runtime.algebra import CliffordAlgebra from layers import MultiRotorLayer, RotorLayer from .config import GDOConfig diff --git a/experiments/_gdo/pre_exploration.py b/experiments/_gdo/pre_exploration.py index facbb24..fece19b 100644 --- a/experiments/_gdo/pre_exploration.py +++ b/experiments/_gdo/pre_exploration.py @@ -42,7 +42,7 @@ SpectralResult, SymmetryResult, ) -from core.module import AlgebraLike +from core.foundation.module import AlgebraLike from experiments._lib import setup_algebra from .config import GDOConfig diff --git a/experiments/_lib.py b/experiments/_lib.py index 6873ca7..b6470f8 100644 --- a/experiments/_lib.py +++ b/experiments/_lib.py @@ -40,9 +40,9 @@ import torch import torch.nn as nn -from core.config import DEFAULT_PARTITION_LEAF_N, PartitionConfig, make_algebra -from core.metric import hermitian_grade_spectrum -from core.module import AlgebraLike +from core.config import make_algebra +from core.foundation.module import AlgebraLike +from core.runtime.metric import hermitian_grade_spectrum from functional.activation import GeometricGELU from layers import CliffordLayerNorm, CliffordLinear, RotorLayer @@ -76,28 +76,17 @@ def setup_algebra( *, dtype: torch.dtype | str = torch.float32, kernel: str = "auto", - partition_threshold: int = 8, - leaf_n: int = DEFAULT_PARTITION_LEAF_N, - product_chunk_size: Optional[int] = None, - partition_tree: Optional[str] = None, - accumulation_dtype: torch.dtype | str | None = None, + dense_threshold: int = 8, exp_policy: str = "balanced", fixed_iterations: Optional[int] = None, ) -> AlgebraLike: """Construct the shared experiment algebra through the core factory.""" - partition = PartitionConfig( - leaf_n=leaf_n, - product_chunk_size=product_chunk_size, - tree=partition_tree, - accumulation_dtype=accumulation_dtype, - ) return make_algebra( p=p, q=q, r=r, kernel=kernel, - partition_threshold=partition_threshold, - partition=partition, + dense_threshold=dense_threshold, device=device, dtype=dtype, exp_policy=exp_policy, diff --git a/experiments/_templates/inc_template.py b/experiments/_templates/inc_template.py index 10d2876..5ffd1b4 100644 --- a/experiments/_templates/inc_template.py +++ b/experiments/_templates/inc_template.py @@ -50,7 +50,7 @@ # Bootstrap project root so the file runs both via ``-m`` and as a bare script. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))) -from core.module import CliffordModule +from core.foundation.module import CliffordModule from experiments._lib import ( count_parameters, ensure_output_dir, diff --git a/experiments/dbg_linear_basis_mixing.py b/experiments/dbg_linear_basis_mixing.py index 55f1fb5..2074890 100644 --- a/experiments/dbg_linear_basis_mixing.py +++ b/experiments/dbg_linear_basis_mixing.py @@ -55,8 +55,8 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from experiments._lib import ( RawDefaultsHelpFormatter, build_visualization_metadata, diff --git a/experiments/dbg_lorentz.py b/experiments/dbg_lorentz.py index 7cc554a..b117626 100644 --- a/experiments/dbg_lorentz.py +++ b/experiments/dbg_lorentz.py @@ -56,12 +56,12 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.metric import ( +from core.foundation.module import CliffordModule +from core.runtime.metric import ( hermitian_grade_spectrum, signature_norm_squared, signature_trace_form, ) -from core.module import CliffordModule from experiments._lib import ( build_visualization_metadata, ensure_output_dir, diff --git a/experiments/dbg_maxwell_equations.py b/experiments/dbg_maxwell_equations.py index eff8a56..391203e 100644 --- a/experiments/dbg_maxwell_equations.py +++ b/experiments/dbg_maxwell_equations.py @@ -57,8 +57,8 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from experiments._lib import ( build_visualization_metadata, ensure_output_dir, diff --git a/experiments/dbg_navier_stokes.py b/experiments/dbg_navier_stokes.py index 1aa86b3..42f6eec 100644 --- a/experiments/dbg_navier_stokes.py +++ b/experiments/dbg_navier_stokes.py @@ -56,8 +56,8 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.metric import hermitian_grade_spectrum, hermitian_inner_product -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.metric import hermitian_grade_spectrum, hermitian_inner_product from experiments._lib import ( build_visualization_metadata, count_parameters, diff --git a/experiments/dbg_yang_mills.py b/experiments/dbg_yang_mills.py index cf56a71..4d55eab 100644 --- a/experiments/dbg_yang_mills.py +++ b/experiments/dbg_yang_mills.py @@ -54,9 +54,9 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.algebra import CliffordAlgebra -from core.metric import hermitian_grade_spectrum, hermitian_inner_product -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra +from core.runtime.metric import hermitian_grade_spectrum, hermitian_inner_product from experiments._lib import ( build_visualization_metadata, count_parameters, diff --git a/experiments/inc_embed_compress.py b/experiments/inc_embed_compress.py index dce84c8..4474dd4 100644 --- a/experiments/inc_embed_compress.py +++ b/experiments/inc_embed_compress.py @@ -61,11 +61,11 @@ from datasets import load_dataset from sentence_transformers import SentenceTransformer -from core.algebra import CliffordAlgebra from core.analysis._types import DimensionResult from core.analysis.dimension import DimensionLifter, EffectiveDimensionAnalyzer from core.analysis.spectral import SpectralAnalyzer -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from experiments._lib import ( build_visualization_metadata, ensure_output_dir, diff --git a/experiments/inc_lattice_morph.py b/experiments/inc_lattice_morph.py index 0270cfd..1706df0 100644 --- a/experiments/inc_lattice_morph.py +++ b/experiments/inc_lattice_morph.py @@ -48,9 +48,9 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.decomposition import ExpPolicy -from core.metric import induced_norm -from core.module import AlgebraLike, CliffordModule +from core.foundation.module import AlgebraLike, CliffordModule +from core.runtime.decomposition import ExpPolicy +from core.runtime.metric import induced_norm from experiments._lib import ( build_visualization_metadata, ensure_output_dir, @@ -212,7 +212,7 @@ class MorphStage(CliffordModule): When ``compound_blades >= 2`` and ``n >= 4`` the global and twist rotors learn a *sum* of independent simple bivectors per slot, producing non-simple bivectors that are handled by ``algebra.exp()`` via the active - :class:`~core.decomposition.ExpPolicy`. + :class:`~core.runtime.decomposition.ExpPolicy`. """ def __init__(self, algebra: AlgebraLike, n: int, compound_blades: int = 1): diff --git a/experiments/inc_pendulum_dynamics.py b/experiments/inc_pendulum_dynamics.py index 5770c87..94178d3 100644 --- a/experiments/inc_pendulum_dynamics.py +++ b/experiments/inc_pendulum_dynamics.py @@ -52,9 +52,9 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.algebra import CliffordAlgebra -from core.metric import hermitian_norm -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra +from core.runtime.metric import hermitian_norm from experiments._lib import ( apply_residual_block, build_visualization_metadata, diff --git a/experiments/inc_sta_trajectory.py b/experiments/inc_sta_trajectory.py index 4bda41e..9011840 100644 --- a/experiments/inc_sta_trajectory.py +++ b/experiments/inc_sta_trajectory.py @@ -56,9 +56,9 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) -from core.algebra import CliffordAlgebra -from core.metric import signature_norm_squared -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra +from core.runtime.metric import signature_norm_squared from experiments._lib import ( build_visualization_metadata, count_parameters, diff --git a/functional/activation.py b/functional/activation.py index c66d84b..92510c2 100644 --- a/functional/activation.py +++ b/functional/activation.py @@ -14,7 +14,7 @@ import torch.nn as nn import torch.nn.functional as F -from core.module import CliffordModule +from core.foundation.module import CliffordModule class GeometricGELU(CliffordModule): diff --git a/functional/loss.py b/functional/loss.py index a14a9fd..1a8f365 100644 --- a/functional/loss.py +++ b/functional/loss.py @@ -9,8 +9,8 @@ import torch.nn as nn import torch.nn.functional as F -from core.metric import hermitian_grade_spectrum -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.metric import hermitian_grade_spectrum class GeometricMSELoss(CliffordModule): diff --git a/functional/orthogonality.py b/functional/orthogonality.py index 6730f4c..c906015 100644 --- a/functional/orthogonality.py +++ b/functional/orthogonality.py @@ -52,7 +52,7 @@ import torch import torch.nn as nn -from core.module import CliffordModule +from core.foundation.module import CliffordModule @dataclass diff --git a/layers/__init__.py b/layers/__init__.py index 2e2061c..85a81ac 100644 --- a/layers/__init__.py +++ b/layers/__init__.py @@ -3,7 +3,7 @@ Organized into Primitives, Canonical Blocks, and Task-Specific Adapters. """ -from core.module import CliffordModule +from core.foundation.module import CliffordModule from .adapters.embedding import MultivectorEmbedding, RotaryBivectorPE from .adapters.mother import EntropyGatedAttention, MotherEmbedding, PhaseShiftHead diff --git a/layers/adapters/conformal.py b/layers/adapters/conformal.py index cc48056..dd6f958 100644 --- a/layers/adapters/conformal.py +++ b/layers/adapters/conformal.py @@ -7,8 +7,8 @@ import torch -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra class ConformalEmbedding(CliffordModule): diff --git a/layers/adapters/embedding.py b/layers/adapters/embedding.py index beb6300..ac4e7f1 100644 --- a/layers/adapters/embedding.py +++ b/layers/adapters/embedding.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra class MultivectorEmbedding(CliffordModule): diff --git a/layers/adapters/gnn.py b/layers/adapters/gnn.py index 71b6591..de87277 100644 --- a/layers/adapters/gnn.py +++ b/layers/adapters/gnn.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from ..primitives.linear import CliffordLinear diff --git a/layers/adapters/mother.py b/layers/adapters/mother.py index 29079a0..883fb5a 100644 --- a/layers/adapters/mother.py +++ b/layers/adapters/mother.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from ..blocks.attention import GeometricProductAttention from ..primitives.normalization import CliffordLayerNorm diff --git a/layers/adapters/projective.py b/layers/adapters/projective.py index 4058915..200ca9a 100644 --- a/layers/adapters/projective.py +++ b/layers/adapters/projective.py @@ -7,8 +7,8 @@ import torch -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra class ProjectiveEmbedding(CliffordModule): diff --git a/layers/blocks/attention.py b/layers/blocks/attention.py index 3f54985..d81e5d4 100644 --- a/layers/blocks/attention.py +++ b/layers/blocks/attention.py @@ -11,13 +11,16 @@ import torch.nn as nn import torch.nn.functional as F -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.basis import normalize_grades, reverse_sign +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from ..primitives.linear import CliffordLinear # Memory-bounded block size for chunked attention computation _BLOCK_SIZE = 64 +_G2_BLADE_CHUNK_SIZE = 16 +_SCORE_PRECOMPUTE_LIMIT = 8_000_000 class GeometricProductAttention(CliffordModule): @@ -50,6 +53,9 @@ def __init__( causal: bool = True, bivector_weight: float = 0.5, dropout: float = 0.0, + score_grades=None, + score_blade_chunk_size: int = _G2_BLADE_CHUNK_SIZE, + score_precompute_limit: int = _SCORE_PRECOMPUTE_LIMIT, ): """Sets up geometric product attention. @@ -60,6 +66,12 @@ def __init__( causal: Apply causal mask for autoregressive generation. bivector_weight: lambda_ weight on bivector score component. dropout: Dropout rate on attention weights. + score_grades: Optional declared grades for compact planned scoring. + ``None`` preserves exact dense scoring over all basis lanes. + score_blade_chunk_size: Grade-2 output blades processed per dense + chunk when exact dense scoring is used. + score_precompute_limit: Maximum temporary ``K_g2`` elements allowed + before exact dense scoring switches to chunked grade-2 blades. """ super().__init__(algebra) assert channels % num_heads == 0, f"channels ({channels}) must be divisible by num_heads ({num_heads})" @@ -69,6 +81,9 @@ def __init__( self.head_channels = channels // num_heads self.causal = causal self.bivector_weight = bivector_weight + self.score_grades = None if score_grades is None else normalize_grades(score_grades, algebra.n) + self.score_blade_chunk_size = max(1, int(score_blade_chunk_size)) + self.score_precompute_limit = max(0, int(score_precompute_limit)) # Q, K, V projections operate on [B*L, channels, dim] self.q_proj = CliffordLinear(algebra, channels, channels) @@ -78,83 +93,82 @@ def __init__( self.attn_dropout = nn.Dropout(dropout) if dropout > 0.0 else None - # Precompute bilinear score tables (replaces pairwise geometric product) + # Precompute bilinear score routes (replaces pairwise geometric product) self._precompute_score_tables() def _precompute_score_tables(self): - """Precomputes lookup tables for efficient attention scoring. + """Precompute exact dense or compact-planned attention score routes. - Replaces the O(L**2) full pairwise geometric product with direct bilinear - forms for grade-0 and grade-2 components of Q * reverse(K): - - Grade-0: _0 = Sum_a Q[a] * K[a] * metric_rev[a] - -> simple weighted dot product, no pairwise expansion needed. - - Grade-2: _r = Sum_a Q[a] * K[a^r] * g2_sign[r, a] - -> precompute K_g2 once, then batched matmul. - - Memory: ~4 MB peak vs ~256 MB for the naive B_gathered approach. + Dense scoring is exact for existing callers and chunks grade-2 blades + instead of materializing ``[B, H, L, Hc, n_g2, D]``. Compact scoring + uses static grade product plans when ``score_grades`` declares a layout. """ alg = self.algebra D = alg.dim + self._score_layout = None + self._score_scalar_product = None + self._score_bivector_product = None + if self.score_grades is not None: + self.n_g2 = alg.n * (alg.n - 1) // 2 + self._score_layout = alg.translator.layout(self.score_grades) + layout_indices = self._score_layout.indices_tensor(device=alg.device) + rev_signs = torch.tensor( + [reverse_sign(index) for index in self._score_layout.basis_indices], + dtype=torch.float32, + device=alg.device, + ) + self.register_buffer("_score_layout_indices", layout_indices) + self.register_buffer("_score_rev_signs", rev_signs) + self._score_scalar_product = alg.translator.product_executor( + op="gp", + left_grades=self.score_grades, + right_grades=self.score_grades, + output_grades=(0,), + device=alg.device, + dtype=alg.dtype, + cache=False, + ) + if self.n_g2 > 0: + self._score_bivector_product = alg.translator.product_executor( + op="gp", + left_grades=self.score_grades, + right_grades=self.score_grades, + output_grades=(2,), + device=alg.device, + dtype=alg.dtype, + cache=False, + ) + return + + if not hasattr(alg, "gp_signs") or not hasattr(alg, "rev_signs"): + raise ValueError("Dense attention scoring requires CliffordAlgebra; pass score_grades for AlgebraContext.") + # Grade-0 metric: metric_rev[a] = gp_signs[a, 0] * rev_signs[a] # gp_signs[a, 0] is the sign when A[a] * B[a] contributes to output blade 0 metric_rev = alg.gp_signs[:, 0].float() * alg.rev_signs.float() self.register_buffer("_metric_rev", metric_rev) # [D] - # Grade-2 tables: for each grade-2 blade r, for each A-blade a: - # B-blade = a XOR r - # sign = rev_sign[a^r] * gp_signs[a, r] g2_blades = [i for i in range(D) if bin(i).count("1") == 2] - n_g2 = len(g2_blades) - self.n_g2 = n_g2 + self.n_g2 = len(g2_blades) + self.register_buffer("_g2_blades", torch.tensor(g2_blades, dtype=torch.long, device=alg.device)) + self.register_buffer("_basis_indices", torch.arange(D, dtype=torch.long, device=alg.device)) - if n_g2 > 0: - a_idx = torch.arange(D, device=alg.device) - r_vals = torch.tensor(g2_blades, dtype=torch.long, device=alg.device) # [n_g2] - - # b_idx[r, a] = a XOR r_vals[r] - b_idx = a_idx.unsqueeze(0) ^ r_vals.unsqueeze(1) # [n_g2, D] - - # rev_sign at the B-blade position - rev_b = alg.rev_signs.float()[b_idx] # [n_g2, D] - - # gp_signs[a, r_val]: sign when A[a] pairs with B[a^r] to give output r - # alg.gp_signs[:, r_vals] -> [D, n_g2]; transpose -> [n_g2, D] - gp_ar = alg.gp_signs[:, r_vals].float().T # [n_g2, D] - - g2_sign = rev_b * gp_ar # [n_g2, D] - else: - b_idx = torch.zeros(0, D, dtype=torch.long, device=alg.device) - g2_sign = torch.zeros(0, D, device=alg.device) - - self.register_buffer("_g2_b_idx", b_idx) # [n_g2, D] long - self.register_buffer("_g2_sign", g2_sign) # [n_g2, D] float + self.register_buffer("_score_layout_indices", torch.zeros(0, dtype=torch.long, device=alg.device)) + self.register_buffer("_score_rev_signs", torch.zeros(0, device=alg.device)) def _compute_score( self, q_head: torch.Tensor, k_head: torch.Tensor, - k_g2: torch.Tensor, ) -> torch.Tensor: - """Computes GA attention score using precomputed bilinear form tables. - - Avoids the O(B.H.Lq.Lk.Hc.D.BLOCK) memory of the full pairwise - geometric product. Instead: - - Grade-0: score_g0 = Q_weighted @ K^T (weighted dot product, peak ~1 MB) - Grade-2: batched matmul via precomputed k_g2 (peak ~4 MB) + """Compute GA attention scores for one query block.""" + if self._score_layout is not None: + return self._compute_score_compact(q_head, k_head) + return self._compute_score_dense(q_head, k_head) - Args: - q_head: Query block [B, H, Lq, Hc, D] - k_head: Keys [B, H, Lk, Hc, D] - k_g2: Precomputed [B, H, Lk, Hc, n_g2, D] - k_g2[b,h,j,c,r,d] = K[b,h,j,c, d^r] * g2_sign[r, d] - - Returns: - scores: [B, H, Lq, Lk] - """ + def _compute_score_dense(self, q_head: torch.Tensor, k_head: torch.Tensor) -> torch.Tensor: + """Exact dense score with automatic full/prechunked grade-2 routing.""" B, H, Lq, Hc, D = q_head.shape Lk = k_head.shape[2] n_g2 = self.n_g2 @@ -169,21 +183,15 @@ def _compute_score( # == Grade-2 score ==================================================== # ||_2||_F = sqrt(Sum_c Sum_r (Sum_d Q[c,d]*k_g2[j,c,r,d])^2) - # Batched matmul merging (B, H, Hc) into one batch dimension: - # q_2d: [B*H*Hc, Lq, D] - # k_g2_2d: [B*H*Hc, Lk*n_g2, D] (Lk and n_g2 merged, n_g2 varies fast) - # comp: [B*H*Hc, Lq, Lk*n_g2] - # Peak ~4 MB vs ~256 MB for the naive B_gathered approach. if n_g2 > 0: q_2d = q_head.permute(0, 1, 3, 2, 4).reshape(B * H * Hc, Lq, D) - # k_g2: [B, H, Lk, Hc, n_g2, D] -> permute to [B, H, Hc, Lk, n_g2, D] - k_g2_t = k_g2.permute(0, 1, 3, 2, 4, 5) - k_g2_2d = k_g2_t.reshape(B * H * Hc, Lk * n_g2, D) - # [B*H*Hc, Lq, D] @ [B*H*Hc, D, Lk*n_g2] -> [B*H*Hc, Lq, Lk*n_g2] - comp = torch.bmm(q_2d, k_g2_2d.transpose(-2, -1)) - # Sum squared components over n_g2, then sum over Hc -> [B, H, Lq, Lk] - comp_sq = comp.reshape(B * H * Hc, Lq, Lk, n_g2).pow(2).sum(-1) # [B*H*Hc, Lq, Lk] - score_g2_sq = comp_sq.reshape(B, H, Hc, Lq, Lk).sum(2) # [B, H, Lq, Lk] + + full_k_g2_elements = B * H * Lk * Hc * n_g2 * D + if full_k_g2_elements <= self.score_precompute_limit: + score_g2_sq = self._dense_score_g2_precomputed(q_2d, k_head, B, H, Hc, Lq, Lk, D, n_g2) + else: + k_2d = k_head.permute(0, 1, 3, 2, 4).reshape(B * H * Hc, Lk, D) + score_g2_sq = self._dense_score_g2_chunked(q_2d, k_2d, B, H, Hc, Lq, Lk, D, n_g2) score_g2 = score_g2_sq.sqrt() else: score_g2 = torch.zeros_like(score_g0) @@ -192,6 +200,65 @@ def _compute_score( scale = math.sqrt(self.head_channels * self.algebra.dim) return (score_g0 + self.bivector_weight * score_g2) / scale + def _dense_score_g2_precomputed(self, q_2d, k_head, B, H, Hc, Lq, Lk, D, n_g2): + """Dense grade-2 score using one full shifted-key materialization.""" + r_vals = self._g2_blades + b_idx = self._basis_indices.unsqueeze(0) ^ r_vals.unsqueeze(1) + rev_b = self.algebra.rev_signs[b_idx].to(dtype=k_head.dtype) + gp_ar = self.algebra.gp_signs[:, r_vals].T.to(dtype=k_head.dtype) + g2_sign = rev_b * gp_ar + + k_g2 = k_head[..., b_idx] * g2_sign + k_g2_2d = k_g2.permute(0, 1, 3, 2, 4, 5).reshape(B * H * Hc, Lk * n_g2, D) + comp = torch.bmm(q_2d, k_g2_2d.transpose(-2, -1)) + comp_sq = comp.reshape(B * H * Hc, Lq, Lk, n_g2).pow(2).sum(-1) + return comp_sq.reshape(B, H, Hc, Lq, Lk).sum(2) + + def _dense_score_g2_chunked(self, q_2d, k_2d, B, H, Hc, Lq, Lk, D, n_g2): + """Dense grade-2 score using bounded output-blade chunks.""" + score_g2_sq = q_2d.new_zeros(B, H, Lq, Lk) + for start in range(0, n_g2, self.score_blade_chunk_size): + end = min(start + self.score_blade_chunk_size, n_g2) + r_vals = self._g2_blades[start:end] + b_idx = self._basis_indices.unsqueeze(0) ^ r_vals.unsqueeze(1) + rev_b = self.algebra.rev_signs[b_idx].to(dtype=k_2d.dtype) + gp_ar = self.algebra.gp_signs[:, r_vals].T.to(dtype=k_2d.dtype) + g2_sign = rev_b * gp_ar + + k_shifted = torch.index_select(k_2d, -1, b_idx.reshape(-1)) + k_shifted = k_shifted * g2_sign.reshape(-1) + k_g2_2d = k_shifted.reshape(B * H * Hc, Lk * (end - start), D) + comp = torch.bmm(q_2d, k_g2_2d.transpose(-2, -1)) + comp_sq = comp.reshape(B * H * Hc, Lq, Lk, end - start).pow(2).sum(-1) + score_g2_sq = score_g2_sq + comp_sq.reshape(B, H, Hc, Lq, Lk).sum(2) + return score_g2_sq + + def _compute_score_compact(self, q_head: torch.Tensor, k_head: torch.Tensor) -> torch.Tensor: + """Declared-grade score using static compact product plans.""" + B, H, Lq, Hc, _ = q_head.shape + Lk = k_head.shape[2] + + q_values = torch.index_select(q_head, -1, self._score_layout_indices) + k_values = torch.index_select(k_head, -1, self._score_layout_indices) * self._score_rev_signs.to( + dtype=k_head.dtype + ) + + q_2d = q_values.permute(0, 1, 3, 2, 4).reshape(B * H * Hc, Lq, self._score_layout.dim) + k_2d = k_values.permute(0, 1, 3, 2, 4).reshape(B * H * Hc, Lk, self._score_layout.dim) + + scalar = self._score_scalar_product.forward_pairwise_compact(q_2d, k_2d).squeeze(-1) + score_g0 = scalar.reshape(B, H, Hc, Lq, Lk).sum(2) + + if self._score_bivector_product is not None: + bivectors = self._score_bivector_product.forward_pairwise_compact(q_2d, k_2d) + score_g2_sq = bivectors.pow(2).sum(-1).reshape(B, H, Hc, Lq, Lk).sum(2) + score_g2 = score_g2_sq.sqrt() + else: + score_g2 = torch.zeros_like(score_g0) + + scale = math.sqrt(self.head_channels * self.algebra.dim) + return (score_g0 + self.bivector_weight * score_g2) / scale + def forward(self, x: torch.Tensor, key_padding_mask: torch.Tensor = None) -> torch.Tensor: """Computes geometric product attention. @@ -226,11 +293,6 @@ def forward(self, x: torch.Tensor, key_padding_mask: torch.Tensor = None) -> tor else: causal_mask = None - # Precompute K_g2 once for all query blocks - much cheaper than recomputing - # k_g2[b,h,j,c,r,d] = K[b,h,j,c, d^r_val] * g2_sign[r, d] - # Shape: [B, H, L, Hc, n_g2, D] ~= 768 KB for the small MPS config - K_g2 = K[..., self._g2_b_idx] * self._g2_sign # [B, H, L, Hc, n_g2, D] - # Chunked attention over query positions to bound memory output_chunks = [] for q_start in range(0, L, _BLOCK_SIZE): @@ -239,7 +301,7 @@ def forward(self, x: torch.Tensor, key_padding_mask: torch.Tensor = None) -> tor Q_block = Q[:, :, q_start:q_end] # [B, H, Lq, Hc, D] # Compute scores: [B, H, Lq, L] - scores = self._compute_score(Q_block, K, K_g2) + scores = self._compute_score(Q_block, K) # Apply causal mask if causal_mask is not None: diff --git a/layers/blocks/multi_rotor_ffn.py b/layers/blocks/multi_rotor_ffn.py index fbc6891..1feb76f 100644 --- a/layers/blocks/multi_rotor_ffn.py +++ b/layers/blocks/multi_rotor_ffn.py @@ -7,8 +7,8 @@ import torch -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from functional.activation import GeometricGELU from ..primitives.linear import CliffordLinear diff --git a/layers/blocks/transformer.py b/layers/blocks/transformer.py index e750083..0c5459f 100644 --- a/layers/blocks/transformer.py +++ b/layers/blocks/transformer.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from ..adapters.mother import EntropyGatedAttention from ..primitives.normalization import CliffordLayerNorm diff --git a/layers/primitives/linear.py b/layers/primitives/linear.py index 6cd5003..a678506 100644 --- a/layers/primitives/linear.py +++ b/layers/primitives/linear.py @@ -15,9 +15,9 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule -from core.validation import check_channels, check_multivector +from core.foundation.module import CliffordModule +from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class CliffordLinear(CliffordModule): diff --git a/layers/primitives/multi_rotor.py b/layers/primitives/multi_rotor.py index d51d385..16addb9 100644 --- a/layers/primitives/multi_rotor.py +++ b/layers/primitives/multi_rotor.py @@ -13,9 +13,9 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule -from core.validation import check_channels, check_multivector +from core.foundation.module import CliffordModule +from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class MultiRotorLayer(CliffordModule): diff --git a/layers/primitives/normalization.py b/layers/primitives/normalization.py index c10cf2c..149566b 100644 --- a/layers/primitives/normalization.py +++ b/layers/primitives/normalization.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra class CliffordLayerNorm(CliffordModule): diff --git a/layers/primitives/projection.py b/layers/primitives/projection.py index 4034292..4df6aa3 100644 --- a/layers/primitives/projection.py +++ b/layers/primitives/projection.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from utils.compat import safe_linalg_solve diff --git a/layers/primitives/reflection.py b/layers/primitives/reflection.py index b37f252..ac3d388 100644 --- a/layers/primitives/reflection.py +++ b/layers/primitives/reflection.py @@ -8,9 +8,9 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule -from core.validation import check_channels, check_multivector +from core.foundation.module import CliffordModule +from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class ReflectionLayer(CliffordModule): diff --git a/layers/primitives/rotor.py b/layers/primitives/rotor.py index 51732ef..32d126d 100644 --- a/layers/primitives/rotor.py +++ b/layers/primitives/rotor.py @@ -8,9 +8,9 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule -from core.validation import check_channels, check_multivector +from core.foundation.module import CliffordModule +from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class RotorLayer(CliffordModule): @@ -23,7 +23,7 @@ class RotorLayer(CliffordModule): Preserves origin. For grade=2, also preserves lengths and angles (isometry). The exp strategy (closed-form vs decomposition) is controlled by - ``algebra.exp_policy`` -- see :class:`core.decomposition.ExpPolicy`. + ``algebra.exp_policy`` -- see :class:`core.runtime.decomposition.ExpPolicy`. Attributes: channels (int): Number of versors. diff --git a/layers/primitives/rotor_gadget.py b/layers/primitives/rotor_gadget.py index 80b653e..708aeea 100644 --- a/layers/primitives/rotor_gadget.py +++ b/layers/primitives/rotor_gadget.py @@ -14,9 +14,9 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule -from core.validation import check_channels, check_multivector +from core.foundation.module import CliffordModule +from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class RotorGadget(CliffordModule): diff --git a/models/blocks/gbn.py b/models/blocks/gbn.py index 79db066..2000f22 100644 --- a/models/blocks/gbn.py +++ b/models/blocks/gbn.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from functional.activation import GeometricGELU from layers import BladeSelector, CliffordLinear, RotorLayer diff --git a/models/blocks/multi_rotor.py b/models/blocks/multi_rotor.py index 60fb452..145afab 100644 --- a/models/blocks/multi_rotor.py +++ b/models/blocks/multi_rotor.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from functional.activation import GeometricGELU from layers import CliffordLinear, MultiRotorLayer diff --git a/models/blocks/time_series.py b/models/blocks/time_series.py index f301e9d..f674eaf 100644 --- a/models/blocks/time_series.py +++ b/models/blocks/time_series.py @@ -8,8 +8,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from layers import CliffordLinear, RotorLayer diff --git a/models/deap/eeg_net.py b/models/deap/eeg_net.py index eae4a34..bf4de26 100644 --- a/models/deap/eeg_net.py +++ b/models/deap/eeg_net.py @@ -24,7 +24,7 @@ import torch.nn as nn from core.config import make_algebra, make_algebra_from_config -from core.module import AlgebraLike, CliffordModule +from core.foundation.module import AlgebraLike, CliffordModule from layers import ( CliffordLayerNorm, GeometricNeutralizer, diff --git a/models/lqa/glr_net.py b/models/lqa/glr_net.py index 74969a3..f91e075 100644 --- a/models/lqa/glr_net.py +++ b/models/lqa/glr_net.py @@ -24,8 +24,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from layers.adapters.embedding import RotaryBivectorPE from layers.adapters.mother import MotherEmbedding from layers.blocks.transformer import GeometricTransformerBlock diff --git a/models/lqa/heads.py b/models/lqa/heads.py index 7eb985d..13bdc3f 100644 --- a/models/lqa/heads.py +++ b/models/lqa/heads.py @@ -17,8 +17,8 @@ import torch.nn as nn import torch.nn.functional as F -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from layers.primitives.projection import GeometricNeutralizer from layers.primitives.rotor import RotorLayer diff --git a/models/md17/forcenet.py b/models/md17/forcenet.py index 8c630f9..c96bffa 100644 --- a/models/md17/forcenet.py +++ b/models/md17/forcenet.py @@ -9,8 +9,8 @@ import torch.nn as nn import torch.nn.functional as F -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from functional.activation import GeometricGELU, GeometricSquare from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, MultiRotorLayer diff --git a/models/sr/estimator.py b/models/sr/estimator.py index d07dcda..b802786 100644 --- a/models/sr/estimator.py +++ b/models/sr/estimator.py @@ -17,7 +17,7 @@ from sklearn.base import BaseEstimator, RegressorMixin from core.config import make_algebra -from core.decomposition import ExpPolicy +from core.runtime.decomposition import ExpPolicy from models.sr.net import SRGBN from models.sr.utils import make_lambdify_fn from optimizers.riemannian import RiemannianAdam diff --git a/models/sr/grouper.py b/models/sr/grouper.py index 467f556..67a02f9 100644 --- a/models/sr/grouper.py +++ b/models/sr/grouper.py @@ -21,7 +21,7 @@ import torch from core.config import make_algebra -from core.module import AlgebraLike +from core.foundation.module import AlgebraLike from models.sr.utils import safe_svd, standardize, subsample logger = logging.getLogger(__name__) @@ -35,7 +35,7 @@ class VariableGroup: var_indices: Indices into original X columns. var_names: Human-readable variable names. signature: (p, q, r) from MetricSearch. - algebra: Shared dense or partitioned algebra for this group. + algebra: Shared dense algebra or planning context for this group. svd_Vt: SVD right-singular vectors for this group (or None). mother_offset: Bit offset in mother algebra basis. internal_edges: VariableEdge list within this group. diff --git a/models/sr/net.py b/models/sr/net.py index a9aa722..8901003 100644 --- a/models/sr/net.py +++ b/models/sr/net.py @@ -17,8 +17,8 @@ import torch import torch.nn as nn -from core.algebra import CliffordAlgebra -from core.module import CliffordModule +from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from functional.activation import GeometricGELU, GeometricSquare from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, RotorLayer diff --git a/models/sr/translator.py b/models/sr/translator.py index 890ced0..8833177 100644 --- a/models/sr/translator.py +++ b/models/sr/translator.py @@ -30,7 +30,7 @@ import sympy import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from models.sr.utils import LAMBDIFY_MODULES, make_lambdify_fn logger = logging.getLogger(__name__) diff --git a/tasks/base.py b/tasks/base.py index 29addc9..9d4c591 100644 --- a/tasks/base.py +++ b/tasks/base.py @@ -13,7 +13,7 @@ from omegaconf import DictConfig from tqdm import tqdm -from core.device import DeviceConfig, resolve_device +from core.foundation.device import DeviceConfig, resolve_device from log import get_logger logger = get_logger(__name__) @@ -83,7 +83,7 @@ def _resolve_device(device: str) -> str: Priority: cuda > mps > cpu. .. deprecated:: - Use :func:`core.device.resolve_device` instead. + Use :func:`core.foundation.device.resolve_device` instead. """ return resolve_device(device) diff --git a/tasks/md17.py b/tasks/md17.py index 99e7f81..7184a17 100644 --- a/tasks/md17.py +++ b/tasks/md17.py @@ -9,7 +9,7 @@ import torch.nn as nn from core.config import make_algebra_from_config -from core.metric import hermitian_grade_spectrum, hermitian_norm +from core.runtime.metric import hermitian_grade_spectrum, hermitian_norm from datalib.md17 import get_md17_loaders from functional.loss import ConservativeLoss, HermitianGradeRegularization from log import get_logger diff --git a/tasks/symbolic_regression.py b/tasks/symbolic_regression.py index a669e64..a7c0f9c 100644 --- a/tasks/symbolic_regression.py +++ b/tasks/symbolic_regression.py @@ -21,7 +21,7 @@ from omegaconf import DictConfig from core.config import make_algebra_from_config -from core.module import AlgebraLike +from core.foundation.module import AlgebraLike from datalib.symbolic_regression import _fetch_pmlb_data, get_dataset_ids, get_sr_loaders, get_sr_raw_splits from log import get_logger from models.sr import SRGBN diff --git a/tests/conftest.py b/tests/conftest.py index b7fa42a..4642036 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,6 @@ import pytest -import torch -from core.algebra import CliffordAlgebra -from core.config import PartitionConfig, make_algebra +from core.runtime.algebra import CliffordAlgebra DEVICE = "cpu" @@ -37,60 +35,6 @@ def algebra_minkowski(): def algebra_conformal(): return CliffordAlgebra(p=4, q=1, device=DEVICE) - -# -- High-dimensional partitioned algebras ------------------------------ -@pytest.fixture -def partitioned_algebra_8d(): - return make_algebra( - p=8, - q=0, - r=0, - kernel="partitioned", - device=DEVICE, - dtype=torch.float64, - partition=PartitionConfig(leaf_n=6, product_chunk_size=32), - ) - - -@pytest.fixture -def partitioned_algebra_12d(): - return make_algebra( - p=12, - q=0, - r=0, - kernel="partitioned", - device=DEVICE, - dtype=torch.float64, - partition=PartitionConfig(leaf_n=6, product_chunk_size=64), - ) - - -@pytest.fixture -def partitioned_algebra_12d_mixed(): - return make_algebra( - p=8, - q=3, - r=1, - kernel="partitioned", - device=DEVICE, - dtype=torch.float64, - partition=PartitionConfig(leaf_n=6, product_chunk_size=32), - ) - - -@pytest.fixture -def partitioned_algebra_16d(): - return make_algebra( - p=10, - q=4, - r=2, - kernel="partitioned", - device=DEVICE, - dtype=torch.float32, - partition=PartitionConfig(leaf_n=6, product_chunk_size=8), - ) - - # -- Module-scoped (used by test_geodesic.py - exact name match) ---------- @pytest.fixture(scope="module") def alg2(): diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 9330b04..6e29e63 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -16,7 +16,6 @@ import pytest import torch -from core.algebra import CliffordAlgebra from core.analysis import ( AnalysisConfig, AnalysisReport, @@ -33,6 +32,7 @@ SymmetryResult, compute_uncertainty_and_alignment, ) +from core.runtime.algebra import CliffordAlgebra DEVICE = "cpu" diff --git a/tests/test_attention.py b/tests/test_attention.py new file mode 100644 index 0000000..ddb5364 --- /dev/null +++ b/tests/test_attention.py @@ -0,0 +1,101 @@ +import math + +import pytest +import torch + +from core.runtime.algebra import CliffordAlgebra +from layers.blocks.attention import GeometricProductAttention + +pytestmark = pytest.mark.unit + +DEVICE = "cpu" + + +def _reference_attention_score(algebra, q_head, k_head, bivector_weight): + product = algebra.geometric_product(q_head.unsqueeze(3), algebra.reverse(k_head).unsqueeze(2)) + score_g0 = product[..., 0].sum(-1) + + g2_idx = algebra.grade_masks[2].nonzero(as_tuple=False).squeeze(-1) + if g2_idx.numel() > 0: + g2 = torch.index_select(product, -1, g2_idx) + score_g2 = g2.pow(2).sum(dim=(-1, -2)).sqrt() + else: + score_g2 = torch.zeros_like(score_g0) + + scale = math.sqrt(q_head.shape[3] * algebra.dim) + return (score_g0 + bivector_weight * score_g2) / scale + + +def _grade_only(algebra, x, grades): + result = torch.zeros_like(x) + for grade in grades: + result = result + algebra.grade_projection(x, grade) + return result + + +def test_attention_dense_chunked_score_matches_direct_product(): + algebra = CliffordAlgebra(3, 0, 0, device=DEVICE, dtype=torch.float64) + attn = GeometricProductAttention( + algebra, + channels=4, + num_heads=2, + causal=False, + bivector_weight=0.25, + score_blade_chunk_size=1, + score_precompute_limit=0, + ) + q_head = torch.randn(2, 2, 3, 2, algebra.dim, dtype=torch.float64) + k_head = torch.randn(2, 2, 4, 2, algebra.dim, dtype=torch.float64) + + actual = attn._compute_score(q_head, k_head) + expected = _reference_attention_score(algebra, q_head, k_head, attn.bivector_weight) + + assert not hasattr(attn, "_g2_b_idx") + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + +def test_attention_compact_declared_grade_score_matches_projected_reference(): + algebra = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) + attn = GeometricProductAttention( + algebra, + channels=4, + num_heads=2, + causal=False, + bivector_weight=0.5, + score_grades=(1,), + ) + q_head = _grade_only(algebra, torch.randn(1, 2, 3, 2, algebra.dim, dtype=torch.float64), (1,)) + k_head = _grade_only(algebra, torch.randn(1, 2, 4, 2, algebra.dim, dtype=torch.float64), (1,)) + + actual = attn._compute_score(q_head, k_head) + expected = _reference_attention_score(algebra, q_head, k_head, attn.bivector_weight) + + assert attn._score_layout is not None + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + +def test_attention_forward_shape_after_score_refactor(): + algebra = CliffordAlgebra(3, 0, 0, device=DEVICE, dtype=torch.float32) + attn = GeometricProductAttention(algebra, channels=4, num_heads=2, causal=False) + x = torch.randn(2, 5, 4, algebra.dim) + + y = attn(x) + + assert y.shape == x.shape + + +@pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") +def test_attention_compact_score_compiles_fullgraph(): + algebra = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float32) + attn = GeometricProductAttention(algebra, channels=4, num_heads=2, causal=False, score_grades=(1,)) + q_head = _grade_only(algebra, torch.randn(1, 2, 3, 2, algebra.dim), (1,)) + k_head = _grade_only(algebra, torch.randn(1, 2, 4, 2, algebra.dim), (1,)) + + def score(q, k): + return attn._compute_score(q, k) + + expected = score(q_head, k_head) + compiled = torch.compile(score, backend="aot_eager", fullgraph=True) + actual = compiled(q_head, k_head) + + assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) diff --git a/tests/test_core.py b/tests/test_core.py index 2a51445..9296222 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -10,7 +10,7 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra pytestmark = pytest.mark.unit diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index c423e48..1e2225c 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -15,10 +15,10 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra pytestmark = pytest.mark.unit -from core.decomposition import ( +from core.runtime.decomposition import ( ExpPolicy, _power_iteration_compiled_safe, compiled_safe_decomposed_exp, diff --git a/tests/test_degenerate_algebra.py b/tests/test_degenerate_algebra.py index 608473f..8e8d7d7 100644 --- a/tests/test_degenerate_algebra.py +++ b/tests/test_degenerate_algebra.py @@ -17,7 +17,7 @@ pytestmark = pytest.mark.unit -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra DEVICE = "cpu" @@ -234,7 +234,7 @@ def test_non_simple_bivector_n4_decomposed_inference(self): B[0, bv_indices[0].item()] = 0.3 # e12 B[0, bv_indices[5].item()] = 0.4 # e34 - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy alg.exp_policy = ExpPolicy.PRECISE with torch.no_grad(): diff --git a/tests/test_exp_signatures.py b/tests/test_exp_signatures.py index 73a59e4..bbf45aa 100644 --- a/tests/test_exp_signatures.py +++ b/tests/test_exp_signatures.py @@ -11,7 +11,7 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra pytestmark = pytest.mark.unit @@ -443,7 +443,7 @@ class TestExpDecomposedGradient: def test_decomposed_gradient_cl40(self): """EXACT policy should produce finite gradients in Cl(4,0).""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy alg = CliffordAlgebra(4, 0, device=DEVICE) alg.exp_policy = ExpPolicy.PRECISE @@ -461,7 +461,7 @@ def test_decomposed_gradient_cl40(self): def test_decomposed_gradient_cl15(self): """EXACT policy should produce finite gradients in Cl(1,5).""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy alg = CliffordAlgebra(1, 5, device=DEVICE) alg.exp_policy = ExpPolicy.PRECISE @@ -481,7 +481,7 @@ def test_decomposed_gradient_cl15(self): def test_decomposed_matches_inference(self): """EXACT exp with grad should approximate inference result.""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy alg = CliffordAlgebra(4, 0, device=DEVICE) alg.exp_policy = ExpPolicy.PRECISE @@ -596,7 +596,7 @@ class TestDecompositionConvergence: def test_simple_bivector_converges_fast(self): """A simple bivector should decompose into 1 component.""" - from core.decomposition import differentiable_invariant_decomposition + from core.runtime.decomposition import differentiable_invariant_decomposition alg = CliffordAlgebra(4, 0, device=DEVICE) bv_mask = alg.grade_masks[2] @@ -615,7 +615,7 @@ def test_simple_bivector_converges_fast(self): def test_non_simple_needs_two_components(self): """e12 + e34 in Cl(4,0) should decompose into 2 components.""" - from core.decomposition import differentiable_invariant_decomposition + from core.runtime.decomposition import differentiable_invariant_decomposition alg = CliffordAlgebra(4, 0, device=DEVICE) bv_mask = alg.grade_masks[2] @@ -633,7 +633,7 @@ def test_non_simple_needs_two_components(self): def test_residual_check_limits_components(self): """With tight threshold, simple bivector should yield exactly 1 component.""" - from core.decomposition import differentiable_invariant_decomposition + from core.runtime.decomposition import differentiable_invariant_decomposition alg = CliffordAlgebra(4, 0, device=DEVICE) bv_mask = alg.grade_masks[2] diff --git a/tests/test_extensions.py b/tests/test_extensions.py index e3a6fd5..9803ef2 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -8,7 +8,7 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers import CliffordGraphConv from layers.adapters.conformal import ConformalEmbedding from layers.adapters.projective import ProjectiveEmbedding diff --git a/tests/test_geodesic.py b/tests/test_geodesic.py index d76df9b..b726e31 100644 --- a/tests/test_geodesic.py +++ b/tests/test_geodesic.py @@ -12,8 +12,8 @@ import pytest import torch -from core.algebra import CliffordAlgebra from core.analysis import DimensionLifter, GeodesicFlow, MetricSearch +from core.runtime.algebra import CliffordAlgebra pytestmark = pytest.mark.unit diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py new file mode 100644 index 0000000..d25849f --- /dev/null +++ b/tests/test_grade_plan.py @@ -0,0 +1,450 @@ +import pytest +import torch + +from core.config import make_algebra +from core.foundation.basis import basis_indices_for_grades, expand_output_grades, geometric_product_output_grades +from core.foundation.layout import AlgebraSpec +from core.planning.flow import GradeFlow +from core.planning.grade_plan import ( + GradeProductExecutor, + build_grade_product_plan, +) +from core.planning.request import build_product_request +from core.planning.translator import GradeTranslator +from core.planning.tree import build_grade_plan_tree +from core.planning.unary import build_unary_request +from core.runtime.algebra import CliffordAlgebra +from core.runtime.context import AlgebraContext +from core.runtime.multivector import Multivector + +pytestmark = pytest.mark.unit + +DEVICE = "cpu" + + +def _project_to_grades(algebra, mv: torch.Tensor, grades: tuple[int, ...]) -> torch.Tensor: + result = torch.zeros_like(mv) + for grade in grades: + result = result + algebra.grade_projection(mv, grade) + return result + + +def _grade_only_input(algebra, batch: int, grades: tuple[int, ...], seed: int) -> torch.Tensor: + generator = torch.Generator(device=DEVICE).manual_seed(seed) + mv = torch.zeros(batch, algebra.dim, dtype=torch.float64) + indices = basis_indices_for_grades(algebra.n, grades, device=DEVICE) + mv[:, indices] = torch.randn(batch, indices.numel(), dtype=torch.float64, generator=generator) * 0.1 + return mv + + +def test_grade_expansion_for_common_high_dim_paths(): + assert geometric_product_output_grades(1, 1, 16) == (0, 2) + assert geometric_product_output_grades(2, 1, 16) == (1, 3) + assert expand_output_grades((0, 2), (1,), 16, op="gp") == (1, 3) + assert expand_output_grades((1,), (1,), 16, op="wedge") == (2,) + assert expand_output_grades((1,), (1,), 16, op="gp", project_grades=(0,)) == (0,) + + +def test_grade_plan_tree_groups_routes_without_runtime_partition_backend(): + spec = AlgebraSpec(10, 4, 2) + tree = build_grade_plan_tree( + spec, + left_grades=(1, 2), + right_grades=(1,), + output_grades=(0, 2), + op="gp", + chunk_pair_limit=128, + ) + + assert tree.output_grades == (0, 2) + assert [(path.left_grade, path.right_grade, path.output_grades) for path in tree.paths] == [ + (1, 1, (0, 2)), + ] + assert tree.path_count == 1 + assert tree.estimated_pairs == 16 * 16 + assert tree.estimated_chunks == 2 + assert tree.path_for_grades(1, 1) is tree.paths[0] + assert tree.path_for_grades(2, 1) is None + + +def test_product_request_infers_declared_layouts_and_output_grades(): + spec = AlgebraSpec(10, 4, 2) + left = torch.zeros(2, spec.dim) + right = torch.zeros(2, spec.dim) + + request = build_product_request( + spec, + left, + right, + left_grades=(1,), + right_grades=(1,), + op="gp", + full_layout_allowed=False, + ) + + assert request.left_grades == (1,) + assert request.right_grades == (1,) + assert request.output_grades == (0, 2) + assert not request.left_compact + assert not request.right_compact + + +def test_product_request_detects_compact_tensors_from_layout_shape(): + spec = AlgebraSpec(6, 0, 0) + layout = spec.layout((1,)) + left = torch.zeros(2, layout.dim) + right = torch.zeros(2, layout.dim) + + request = build_product_request( + spec, + left, + right, + left_layout=layout, + right_layout=layout, + output_grades=(0, 2), + op="gp", + ) + + assert request.left_compact + assert request.right_compact + + +def test_unary_request_infers_projection_layout_without_full_layout(): + spec = AlgebraSpec(10, 4, 2) + values = torch.zeros(2, spec.dim) + + request = build_unary_request( + spec, + values, + op="grade_projection", + output_grades=(1,), + full_layout_allowed=False, + ) + + assert request.input_grades == (1,) + assert request.output_grades == (1,) + assert not request.input_compact + + +def test_grade_flow_propagates_embedding_unary_product_and_merge(): + spec = AlgebraSpec(8, 0, 0) + vector = GradeFlow.vector(spec) + scalar = GradeFlow.scalar(spec) + + product = vector.product(vector) + projected = product.project((2,)) + merged = scalar.merge(projected) + + assert vector.grades == (1,) + assert vector.unary("reverse").grades == (1,) + assert product.grades == (0, 2) + assert projected.grades == (2,) + assert merged.grades == (0, 2) + + +def test_grade_layout_compact_dense_round_trip(): + spec = AlgebraSpec(4, 1, 1) + layout = spec.layout((0, 2)) + dense = torch.randn(2, spec.dim, dtype=torch.float64, generator=torch.Generator().manual_seed(97)) + + values = layout.compact(dense) + materialized = layout.dense(values) + + assert values.shape[-1] == layout.dim + assert torch.allclose(materialized[..., layout.indices_tensor(device=dense.device)], values) + outside = torch.ones(spec.dim, dtype=torch.bool) + outside[layout.indices_tensor()] = False + assert materialized[..., outside].abs().sum().item() == 0.0 + + +@pytest.mark.parametrize("op", ["gp", "wedge", "inner", "commutator", "anti_commutator"]) +def test_static_grade_product_matches_dense_kernel_for_selected_grade_paths(op): + algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) + left_grades = (1,) + right_grades = (1, 2) + output_grades = expand_output_grades(left_grades, right_grades, algebra.n, op=op) + plan = build_grade_product_plan( + algebra.p, + algebra.q, + algebra.r, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + op=op, + device=DEVICE, + dtype=torch.float64, + ) + product = GradeProductExecutor(plan) + A = _grade_only_input(algebra, 3, left_grades, seed=101) + B = _grade_only_input(algebra, 3, right_grades, seed=103) + + expected = _project_to_grades(algebra, getattr(algebra, _dense_method_name(op))(A, B), output_grades) + actual = product.forward_dense(A, B) + + assert product.pair_count < algebra.dim * algebra.dim + assert actual.shape == expected.shape + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + +def test_algebra_projected_product_matches_dense_kernel_and_compact_output(): + algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) + A = _grade_only_input(algebra, 2, (1,), seed=113) + B = _grade_only_input(algebra, 2, (1,), seed=127) + + dense_expected = _project_to_grades(algebra, algebra.geometric_product(A, B), (0, 2)) + dense_actual = algebra.projected_geometric_product( + A, + B, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + ) + compact_actual = algebra.projected_geometric_product( + A, + B, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + compact_output=True, + ) + + assert torch.allclose(dense_actual, dense_expected, atol=1e-12, rtol=1e-12) + assert compact_actual.shape[-1] == AlgebraSpec.from_algebra(algebra).layout((0, 2)).dim + + +def test_grade_translator_reuses_projected_product_executor(): + algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) + translator = GradeTranslator(algebra) + + first = translator.product_executor( + op="gp", + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + dtype=torch.float64, + device=DEVICE, + ) + second = translator.product_executor( + op="gp", + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + dtype=torch.float64, + device=DEVICE, + ) + + assert first is second + + +def test_multivector_compact_projected_product_keeps_dense_tensor_compatibility(): + algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) + A = Multivector(algebra, _grade_only_input(algebra, 2, (1,), seed=131)).compact((1,)) + B = Multivector(algebra, _grade_only_input(algebra, 2, (1,), seed=137)).compact((1,)) + + result = A.projected_product(B, output_grades=(0, 2)) + expected = _project_to_grades(algebra, algebra.geometric_product(A.tensor, B.tensor), (0, 2)) + + assert result.is_compact + assert result.values.shape[-1] == result.layout.dim + assert result.tensor.shape[-1] == algebra.dim + assert torch.allclose(result.tensor, expected, atol=1e-12, rtol=1e-12) + + +def test_multivector_compact_projected_product_supports_mixed_dense_operand(): + algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) + A = Multivector(algebra, _grade_only_input(algebra, 2, (1,), seed=139)).compact((1,)) + B = Multivector(algebra, _grade_only_input(algebra, 2, (1,), seed=149)) + + result = A.projected_product(B, output_grades=(0, 2), right_grades=(1,)) + expected = _project_to_grades(algebra, algebra.geometric_product(A.tensor, B.tensor), (0, 2)) + + assert result.is_compact + assert torch.allclose(result.tensor, expected, atol=1e-12, rtol=1e-12) + + +def test_make_algebra_returns_context_above_dense_threshold(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + + assert isinstance(algebra, AlgebraContext) + assert algebra.n == 16 + assert not algebra.allow_full_layout_products + + +def test_dense_policy_uses_context_by_default_and_explicit_dense_up_to_twelve(): + auto_dense = make_algebra(8, 0, 0, device=DEVICE, dtype=torch.float32) + auto_context = make_algebra(9, 0, 0, device=DEVICE, dtype=torch.float32) + explicit_dense = make_algebra(9, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float32) + + assert isinstance(auto_dense, CliffordAlgebra) + assert isinstance(auto_context, AlgebraContext) + assert isinstance(explicit_dense, CliffordAlgebra) + with pytest.raises(AssertionError): + CliffordAlgebra(9, 0, 0, device=DEVICE, dtype=torch.float32) + with pytest.raises(AssertionError): + make_algebra(13, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float32) + + +def test_context_projected_product_handles_high_dim_vector_product(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + A = torch.zeros(1, algebra.dim) + B = torch.zeros(1, algebra.dim) + A[0, 1] = 1.0 + B[0, 1] = 1.0 + B[0, 2] = 1.0 + + values, layout = algebra.projected_geometric_product( + A, + B, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + compact_output=True, + return_layout=True, + ) + + scalar_pos = layout.basis_indices.index(0) + bivector_pos = layout.basis_indices.index(3) + assert values.shape[-1] == layout.dim + assert torch.allclose(values[0, scalar_pos], torch.tensor(1.0)) + assert torch.allclose(values[0, bivector_pos], torch.tensor(1.0)) + + +def test_context_planned_unary_projection_and_reverse_avoid_full_layout(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + mv = torch.zeros(1, algebra.dim) + mv[0, 1] = 2.0 + mv[0, 3] = 5.0 + + projected = algebra.grade_projection(mv, 1) + reversed_bivector = algebra.reverse( + mv, + input_grades=(2,), + compact_output=True, + ) + bivector_layout = algebra.layout((2,)) + bivector_pos = bivector_layout.basis_indices.index(3) + + assert torch.allclose(projected[0, 1], torch.tensor(2.0)) + assert torch.allclose(projected[0, 3], torch.tensor(0.0)) + assert torch.allclose(reversed_bivector[0, bivector_pos], torch.tensor(-5.0)) + + +def test_context_planned_unary_compact_reverse(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + layout = algebra.layout((2,)) + values = torch.arange(layout.dim, dtype=torch.float32).unsqueeze(0) + + actual, output_layout = algebra.reverse( + values, + input_layout=layout, + input_compact=True, + compact_output=True, + return_layout=True, + ) + + assert output_layout == layout + assert torch.allclose(actual, -values) + + +def test_high_dim_context_requires_declared_layout_for_products(): + algebra = make_algebra(9, 0, 0, device=DEVICE, dtype=torch.float32) + A = torch.zeros(1, algebra.dim) + B = torch.zeros(1, algebra.dim) + + with pytest.raises(ValueError, match="Declare active grades"): + algebra.geometric_product(A, B) + + with pytest.raises(ValueError, match="Declare active grades"): + algebra.reverse(A) + + +def test_low_dim_context_can_use_full_layout_fallback(): + context = make_algebra(4, 0, 0, kernel="context", device=DEVICE, dtype=torch.float64) + dense = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) + A = _grade_only_input(dense, 2, (1,), seed=163) + B = _grade_only_input(dense, 2, (1,), seed=167) + + actual = context.geometric_product(A, B) + expected = dense.geometric_product(A, B) + + assert context.allow_full_layout_products + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + +@pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") +def test_static_grade_product_compiles_fullgraph_with_aot_eager(): + algebra = CliffordAlgebra(5, 1, 0, device=DEVICE, dtype=torch.float32) + plan = build_grade_product_plan( + algebra.p, + algebra.q, + algebra.r, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + op="gp", + device=DEVICE, + dtype=torch.float32, + ) + product = GradeProductExecutor(plan) + A = _grade_only_input(algebra, 2, (1,), seed=107).to(dtype=torch.float32) + B = _grade_only_input(algebra, 2, (1,), seed=109).to(dtype=torch.float32) + + compiled = torch.compile(product, backend="aot_eager", fullgraph=True) + + expected = product(A, B) + actual = compiled(A, B) + + assert actual.shape[-1] == product.output_dim + assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) + + +@pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") +def test_planned_unary_compiles_fullgraph_with_aot_eager(): + algebra = make_algebra(6, 0, 0, kernel="context", device=DEVICE, dtype=torch.float32) + executor = algebra.translator.unary_executor( + op="reverse", + input_grades=(2,), + dtype=torch.float32, + device=DEVICE, + ) + values = _grade_only_input(CliffordAlgebra(6, 0, 0, device=DEVICE), 2, (2,), seed=173).to(dtype=torch.float32) + + compiled = torch.compile(executor, backend="aot_eager", fullgraph=True) + + expected = executor(values) + actual = compiled(values) + + assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) + + +@pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") +def test_algebra_projected_product_compiles_fullgraph_after_cache_warm(): + algebra = CliffordAlgebra(5, 1, 0, device=DEVICE, dtype=torch.float32) + A = _grade_only_input(algebra, 2, (1,), seed=151).to(dtype=torch.float32) + B = _grade_only_input(algebra, 2, (1,), seed=157).to(dtype=torch.float32) + + def product(x, y): + return algebra.projected_geometric_product( + x, + y, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + compact_output=True, + ) + + expected = product(A, B) + compiled = torch.compile(product, backend="aot_eager", fullgraph=True) + actual = compiled(A, B) + + assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) + + +def _dense_method_name(op: str) -> str: + if op == "gp": + return "geometric_product" + if op == "inner": + return "inner_product" + if op == "anti_commutator": + return "anti_commutator" + return op diff --git a/tests/test_hermitian_metrics.py b/tests/test_hermitian_metrics.py index bc01180..bfba6f1 100644 --- a/tests/test_hermitian_metrics.py +++ b/tests/test_hermitian_metrics.py @@ -3,10 +3,10 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra pytestmark = pytest.mark.unit -from core.metric import ( +from core.runtime.metric import ( _hermitian_signs, clifford_conjugate, geometric_distance, diff --git a/tests/test_iterative_unbender.py b/tests/test_iterative_unbender.py index da87f70..4071ce4 100644 --- a/tests/test_iterative_unbender.py +++ b/tests/test_iterative_unbender.py @@ -11,7 +11,7 @@ pytestmark = pytest.mark.slow -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from models.sr.unbender import ( IterativeUnbender, OrthogonalEliminationResult, diff --git a/tests/test_layer_optimization.py b/tests/test_layer_optimization.py index bbc573b..ab19ad5 100644 --- a/tests/test_layer_optimization.py +++ b/tests/test_layer_optimization.py @@ -8,7 +8,7 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers import RotorLayer pytestmark = pytest.mark.unit diff --git a/tests/test_layers.py b/tests/test_layers.py index ecdf0e8..3870628 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -8,8 +8,8 @@ import pytest import torch -from core.algebra import CliffordAlgebra -from core.decomposition import ExpPolicy +from core.runtime.algebra import CliffordAlgebra +from core.runtime.decomposition import ExpPolicy from layers import CliffordLinear, MultiRotorLayer, RotorLayer from layers.primitives.reflection import ReflectionLayer @@ -68,7 +68,7 @@ def test_multi_rotor_invariants(self, algebra_3d): def test_rotor_layer_exact_policy(self, algebra_3d): """Test RotorLayer with EXACT exp policy.""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy algebra_3d.exp_policy = ExpPolicy.PRECISE x = torch.randn(4, 5, 8) @@ -86,7 +86,7 @@ def test_rotor_layer_exact_policy(self, algebra_3d): def test_rotor_layer_policy_vs_standard(self, algebra_3d): """Compare RotorLayer with FAST vs EXACT policy.""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy layer_a = RotorLayer(algebra_3d, 3) layer_b = RotorLayer(algebra_3d, 3) @@ -107,7 +107,7 @@ def test_rotor_layer_policy_vs_standard(self, algebra_3d): def test_rotor_layer_backward_exact(self, algebra_3d): """Test gradient flow through RotorLayer with EXACT policy.""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy algebra_3d.exp_policy = ExpPolicy.PRECISE @@ -129,7 +129,7 @@ def test_rotor_layer_backward_exact(self, algebra_3d): def test_multi_rotor_layer_exact_policy(self, algebra_3d): """Test MultiRotorLayer with EXACT policy.""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy algebra_3d.exp_policy = ExpPolicy.PRECISE x = torch.randn(4, 5, 8) @@ -141,7 +141,7 @@ def test_multi_rotor_layer_exact_policy(self, algebra_3d): def test_multi_rotor_layer_backward_exact(self, algebra_3d): """Test gradient flow through MultiRotorLayer with EXACT policy.""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy algebra_3d.exp_policy = ExpPolicy.PRECISE @@ -164,7 +164,7 @@ def test_multi_rotor_layer_backward_exact(self, algebra_3d): def test_rotor_layer_rotor_property(self, algebra_3d): """Verify that exp-produced rotors satisfy R * ~R = 1.""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy algebra_3d.exp_policy = ExpPolicy.PRECISE diff --git a/tests/test_md17_complete.py b/tests/test_md17_complete.py index 9093f8a..3a2dfc9 100644 --- a/tests/test_md17_complete.py +++ b/tests/test_md17_complete.py @@ -3,11 +3,11 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra pytestmark = pytest.mark.unit -from core.decomposition import ExpPolicy -from core.metric import hermitian_grade_spectrum, hermitian_norm +from core.runtime.decomposition import ExpPolicy +from core.runtime.metric import hermitian_grade_spectrum, hermitian_norm from functional.loss import ConservativeLoss, HermitianGradeRegularization from models.md17 import DynamicRotorGenerator, GaussianRBF, MD17ForceNet, MD17InteractionBlock @@ -116,7 +116,7 @@ def test_forward_default(self, algebra): assert force.shape == (8, 3) def test_forward_with_exact_policy(self, algebra): - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy algebra.exp_policy = ExpPolicy.PRECISE model = MD17ForceNet( diff --git a/tests/test_metric_search.py b/tests/test_metric_search.py index 7bc53c8..0f445e9 100644 --- a/tests/test_metric_search.py +++ b/tests/test_metric_search.py @@ -10,7 +10,7 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra pytestmark = pytest.mark.slow from core.analysis import GeodesicFlow, MetricSearch diff --git a/tests/test_multivector.py b/tests/test_multivector.py index 3dd8a67..f3fbbf1 100644 --- a/tests/test_multivector.py +++ b/tests/test_multivector.py @@ -3,8 +3,8 @@ import pytest import torch -from core.algebra import CliffordAlgebra -from core.multivector import Multivector +from core.runtime.algebra import CliffordAlgebra +from core.runtime.multivector import Multivector @pytest.fixture diff --git a/tests/test_partitioned_algebra.py b/tests/test_partitioned_algebra.py deleted file mode 100644 index 386c830..0000000 --- a/tests/test_partitioned_algebra.py +++ /dev/null @@ -1,934 +0,0 @@ -# Versor: Universal Geometric Algebra Neural Network -# Copyright (C) 2026 Eunkyum Kim -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# - -"""Low-dimensional and structural unit coverage for partitioned Clifford algebra. - -Dense-reference sweeps for the Cl8-Cl12 overlap region live in -``test_partitioned_dense_reference.py``. Slow non-monolithic verification for -Cl12+ lives in ``test_partitioned_highdim.py``. - -This file is for small enough algebras where a dense ``CliffordAlgebra`` oracle -is cheap. It intentionally forces shallow recursive trees with small ``leaf_n`` -values so tests can inspect internal split structure, chunking, dtype handling, -and compile behavior without relying on large high-dimensional fixtures. -""" - -import pytest -import torch -import torch.nn as nn - -from core.algebra import CliffordAlgebra -from core.config import PartitionConfig -from core.partitioned_algebra import DEFAULT_PARTITION_LEAF_N, MAX_PARTITIONED_DIMENSIONS, PartitionedCliffordAlgebra - -pytestmark = pytest.mark.unit - -DEVICE = "cpu" - - -def test_partition_config_leaf_default_matches_kernel_default(): - # ``None`` is the Hydra/YAML spelling for "use the partitioned kernel - # default." Keep this tied to the constructor default so config and direct - # Python construction cannot silently diverge. - assert PartitionConfig().leaf_n == DEFAULT_PARTITION_LEAF_N - assert PartitionConfig.from_mapping({"leaf_n": None}).leaf_n == DEFAULT_PARTITION_LEAF_N - - -def test_partitioned_algebra_rejects_above_supported_dimension(): - # The partitioned kernel has an explicit physical memory limit. This test - # guards the public error boundary rather than a particular internal split. - with pytest.raises(AssertionError, match=f"p \\+ q \\+ r must be <= {MAX_PARTITIONED_DIMENSIONS}"): - PartitionedCliffordAlgebra(MAX_PARTITIONED_DIMENSIONS + 1, 0, 0, device=DEVICE) - - -@pytest.mark.parametrize("n", range(8, MAX_PARTITIONED_DIMENSIONS + 1)) -def test_default_partitioned_structure_covers_8_to_16_dimensions(n): - # This is a lightweight structural sweep across the intended operating - # range. It does not multiply full multivectors; it checks that construction - # stays recursive, bounded by leaf_n, and free of global Cayley tables. - algebra = PartitionedCliffordAlgebra(n, 0, 0, device=DEVICE, dtype=torch.float32) - - assert algebra.dim == 2**n - assert algebra.core is None - assert not hasattr(algebra, "cayley_indices") - assert not hasattr(algebra, "cayley_signs") - - for node in _walk_unique_partition_nodes(algebra): - if node.core is None: - assert node.left_n + node.right_n == node.n - assert node.left_sub.n == node.left_n - assert node.right_sub.n == node.right_n - assert node.left_sub.n > 0 - assert node.right_sub.n > 0 - assert not hasattr(node, "cayley_indices") - assert not hasattr(node, "cayley_signs") - else: - assert node.n <= DEFAULT_PARTITION_LEAF_N - - -@pytest.mark.parametrize( - ("partition_tree", "match"), - [ - pytest.param("R=0-3; L=4-6", "cover every dimension", id="missing-dimension"), - pytest.param("R=0-3; L=3-7", "appears in both", id="overlapping-dimension"), - pytest.param("R=0-3; X=4-7", "Invalid partition path", id="invalid-path"), - pytest.param("R=0-3; L=8", "outside", id="out-of-range"), - pytest.param("R=3-0; L=4-7", "descending", id="descending-range"), - pytest.param("R=0,0; L=1-7", "duplicates", id="duplicate-within-node"), - ], -) -def test_partition_tree_expression_rejects_invalid_edge_cases(partition_tree, match): - # Explicit tree declarations are user-facing, so parser failures should be - # caught before construction reaches recursive internals. - with pytest.raises(ValueError, match=match): - PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, partition_tree=partition_tree) - - -def _dtype_tolerance(dtype: torch.dtype) -> float: - if dtype == torch.float16: - return 5e-3 - if dtype == torch.bfloat16: - return 5e-2 - if dtype == torch.float32: - return 2e-5 - return 1e-10 - - -def _make_pair(p=3, q=1, r=0, *, leaf_n=2, product_chunk_size=None, dtype=torch.float64): - """Build matched dense and partitioned algebras for core-kernel comparisons.""" - reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype) - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=dtype, - leaf_n=leaf_n, - product_chunk_size=product_chunk_size, - ) - return reference, algebra - - -def _assert_matches_monolithic(p, q=0, r=0, *, leaf_n=6, shape=(3,), dtype=torch.float64): - """Compare a partitioned product against the dense monolithic Cayley kernel.""" - torch.manual_seed(17) - reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype) - algebra = PartitionedCliffordAlgebra(p, q, r, device=DEVICE, dtype=dtype, leaf_n=leaf_n) - - dim = 2 ** (p + q + r) - A = torch.randn(*shape, dim, dtype=dtype) - B = torch.randn(*shape, dim, dtype=dtype) - - expected = reference.geometric_product(A, B) - actual = algebra.geometric_product(A, B) - - assert torch.allclose(actual, expected, atol=1e-9, rtol=1e-9) - - -def _walk_unique_partition_nodes(algebra: PartitionedCliffordAlgebra): - """Yield every unique node in a partition tree, handling shared children.""" - stack = [algebra] - seen = set() - while stack: - node = stack.pop() - if id(node) in seen: - continue - seen.add(id(node)) - yield node - if node.core is None: - stack.extend([node.left_sub, node.right_sub]) - - -class _PartitionedProductLayer(nn.Module): - """Tiny module used to check ``torch.compile`` forward and backward paths.""" - - def __init__(self, p, q=0, r=0): - super().__init__() - self.algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float32, - leaf_n=2, - product_chunk_size=4, - ) - self.weight = nn.Parameter(torch.randn(self.algebra.dim)) - - def forward(self, x): - weight = self.weight.expand_as(x) - return self.algebra.geometric_product(x, weight) - - -class TestPartitionedCliffordAlgebra: - """Small-dimensional regression tests grouped by partitioned-kernel region.""" - - def test_leaf_matches_core_kernel(self): - # With n <= leaf_n, the partitioned algebra delegates to the dense leaf - # kernel. This test makes sure the leaf path has no wrapper-level dtype - # or shape differences. - _assert_matches_monolithic(3, 1, 0, leaf_n=6, shape=(2,)) - - def test_forced_recursive_euclidean_matches_core_kernel(self): - # ``leaf_n=2`` forces Cl(4,0) to split recursively even though the dense - # kernel could handle it directly. This is the smallest recursive - # product check with a Euclidean reference. - _assert_matches_monolithic(4, 0, 0, leaf_n=2, shape=(4,)) - - def test_default_recursive_cl8_matches_core_kernel(self): - # Cl8 is the first default-recursive Euclidean case now that the default - # leaf size is six dimensions. - _assert_matches_monolithic(8, 0, 0, leaf_n=6, shape=(2,)) - - def test_recursive_tree_uses_balanced_binary_splits(self): - # Balanced splits keep internal tensors bounded. Cl16 with leaf_n=6 - # should split 16 -> 8 + 8 -> 4 + 4 leaves instead of making one large - # dense child. - algebra = PartitionedCliffordAlgebra(16, 0, 0, device=DEVICE, leaf_n=6) - - assert algebra.left_n == 8 - assert algebra.right_n == 8 - assert algebra.left_sub.left_n == 4 - assert algebra.left_sub.right_n == 4 - assert algebra.right_sub.left_n == 4 - assert algebra.right_sub.right_n == 4 - - assert not hasattr(algebra.left_sub, "cayley_indices") - assert not hasattr(algebra.right_sub, "cayley_indices") - - def test_describe_tree_reports_split_layout_and_shared_nodes(self, capsys): - # ``describe_tree`` is the debugging view used when constructing custom - # partition trees. The exact root text documents public bit ranges, - # child sizes, pair count, chunk size, and shared-node annotations. - algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=2) - - tree = algebra.describe_tree() - lines = tree.splitlines() - - assert lines[0] == ( - "root: Cl(8,0,0), n=8, dim=256, bits=[0, 8), " - "split left=4 bits=[4, 8), right=4 bits=[0, 4), pairs=256, chunk=64" - ) - assert "root.L: Cl(4,0,0), n=4, dim=16, bits=[4, 8)" in lines[1] - assert "root.R: Cl(4,0,0), n=4, dim=16, bits=[0, 4)" in tree - assert "shared_with=root.L" in tree - - algebra.print_tree() - assert capsys.readouterr().out.strip() == tree - - def test_repeated_signature_tiles_share_subalgebras_automatically(self): - # Cl(8,4,4) can be tiled as repeated Cl(2,1,1) blocks. The planner is - # allowed to permute internal bit order to share identical child modules, - # but it must retain public canonical basis order at the API boundary. - algebra = PartitionedCliffordAlgebra( - 8, - 4, - 4, - device=DEVICE, - leaf_n=4, - ) - - assert (algebra.left_sub.p, algebra.left_sub.q, algebra.left_sub.r) == (4, 2, 2) - assert (algebra.right_sub.p, algebra.right_sub.q, algebra.right_sub.r) == (4, 2, 2) - assert algebra.left_sub is algebra.right_sub - assert (algebra.left_sub.left_sub.p, algebra.left_sub.left_sub.q, algebra.left_sub.left_sub.r) == (2, 1, 1) - assert algebra.left_sub.left_sub is algebra.left_sub.right_sub - assert algebra.basis_permutation.uses_permutation - - tree = algebra.describe_tree() - assert "root: Cl(8,4,4)" in tree - assert "root.L: Cl(4,2,2)" in tree - assert "root.L.L: Cl(2,1,1)" in tree - assert "shared_with=root.L" in tree - assert "shared_with=root.L.L" in tree - - def test_repeated_signature_tile_product_matches_core_kernel_with_basis_permutation(self): - # This catches sign mistakes introduced by the repeated-tile basis - # permutation. The dense reference uses public bitmask order, so any - # missing input or output permutation sign appears directly in the - # product comparison. - torch.manual_seed(107) - reference = CliffordAlgebra(4, 2, 2, device=DEVICE, dtype=torch.float64) - algebra = PartitionedCliffordAlgebra( - 4, - 2, - 2, - device=DEVICE, - dtype=torch.float64, - leaf_n=4, - ) - A = torch.randn(2, algebra.dim, dtype=torch.float64) - B = torch.randn(2, algebra.dim, dtype=torch.float64) - - assert algebra.basis_permutation.uses_permutation - assert torch.allclose( - algebra.geometric_product(A, B), reference.geometric_product(A, B), atol=1e-10, rtol=1e-10 - ) - - def test_repeated_signature_tile_product_gradients_match_core_kernel(self): - # The same repeated-tile permutation must also be transparent to - # autograd. Compare gradients against the dense kernel with cloned inputs - # so both graphs receive identical values. - torch.manual_seed(109) - reference = CliffordAlgebra(4, 2, 2, device=DEVICE, dtype=torch.float64) - algebra = PartitionedCliffordAlgebra( - 4, - 2, - 2, - device=DEVICE, - dtype=torch.float64, - leaf_n=4, - ) - - A_ref = torch.randn(2, algebra.dim, dtype=torch.float64, requires_grad=True) - B_ref = torch.randn(2, algebra.dim, dtype=torch.float64, requires_grad=True) - A_partitioned = A_ref.detach().clone().requires_grad_(True) - B_partitioned = B_ref.detach().clone().requires_grad_(True) - - reference.geometric_product(A_ref, B_ref).square().sum().backward() - algebra.geometric_product(A_partitioned, B_partitioned).square().sum().backward() - - assert torch.allclose(A_partitioned.grad, A_ref.grad, atol=1e-9, rtol=1e-9) - assert torch.allclose(B_partitioned.grad, B_ref.grad, atol=1e-9, rtol=1e-9) - - def test_identical_recursive_subalgebras_are_shared(self): - # Pure Euclidean repeated halves should reuse child objects. This reduces - # memory and ensures recursive caches key by structural signature rather - # than by object construction path. - algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=2) - - assert algebra.left_sub is algebra.right_sub - assert algebra.left_sub.left_sub is algebra.left_sub.right_sub - - def test_recursive_node_uses_compact_memory_layout(self): - # Recursive nodes should not store dense global Cayley-like routing - # tables. They keep structural sign vectors and derive right-pair slices - # lazily so memory scales with child size and chunk size. - algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, leaf_n=2) - - assert "_grade_masks_float" not in algebra._buffers - assert "_grade_masks_float_T" not in algebra._buffers - assert torch.equal(algebra.grade_masks[2], algebra.grade_index == 2) - assert algebra.grade_masks_float.dtype == algebra.dtype - - assert algebra._right_pair_count == algebra.right_dim * algebra.right_dim - assert not hasattr(algebra, "_right_pair_full") - assert not hasattr(algebra, "_right_pair_a") - assert not hasattr(algebra, "_right_pair_b") - assert not hasattr(algebra, "_right_pair_result") - assert not hasattr(algebra, "_right_pair_signs") - assert not hasattr(algebra, "_right_interaction") - assert not hasattr(algebra, "bridge_signs") - assert not hasattr(algebra, "_uses_basis_permutation") - assert not hasattr(algebra, "_to_split_basis") - assert not hasattr(algebra, "_to_public_basis") - assert not hasattr(algebra, "_split_basis_signs") - _, _, pair_result, pair_signs = algebra._right_product_slice(0, algebra._right_pair_count) - assert pair_result.shape == (algebra._right_pair_count,) - assert pair_signs.dtype == torch.int8 - assert not algebra.basis_permutation.uses_permutation - assert algebra.basis_permutation.split_to_public.numel() == 0 - assert algebra.basis_permutation.public_to_split.numel() == 0 - assert algebra.basis_permutation.split_signs.numel() == 0 - - def test_default_recursive_mixed_signature_matches_core_kernel(self): - # Mixed signatures stress metric signs across the split boundary. - _assert_matches_monolithic(5, 2, 1, leaf_n=6, shape=(2,)) - - def test_recursive_product_supports_extra_batch_axes(self): - # Recursive products must obey PyTorch broadcasting semantics on all - # leading dimensions, matching the dense kernel. - _assert_matches_monolithic(4, 1, 0, leaf_n=3, shape=(2, 3)) - - def test_recursive_product_gradients_match_core_kernel(self): - # Basic backward check for the recursive product path without repeated - # signature permutations. - torch.manual_seed(23) - reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) - algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64, leaf_n=2) - - A_ref = torch.randn(2, 16, dtype=torch.float64, requires_grad=True) - B_ref = torch.randn(2, 16, dtype=torch.float64, requires_grad=True) - A_partitioned = A_ref.detach().clone().requires_grad_(True) - B_partitioned = B_ref.detach().clone().requires_grad_(True) - - reference.geometric_product(A_ref, B_ref).square().sum().backward() - algebra.geometric_product(A_partitioned, B_partitioned).square().sum().backward() - - assert torch.allclose(A_partitioned.grad, A_ref.grad, atol=1e-9, rtol=1e-9) - assert torch.allclose(B_partitioned.grad, B_ref.grad, atol=1e-9, rtol=1e-9) - - @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32, torch.float64]) - def test_recursive_operations_support_generic_floating_dtypes(self, dtype): - # The partitioned kernel owns sign buffers in the algebra dtype but must - # still preserve the requested floating dtype for product, norm, and exp - # outputs. Tolerances widen for reduced precision. - torch.manual_seed(67) - reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=dtype) - algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=dtype, leaf_n=2) - atol = _dtype_tolerance(dtype) - - A = torch.randn(2, algebra.dim, dtype=dtype) * 0.25 - B = torch.randn(2, algebra.dim, dtype=dtype) * 0.25 - - for method_name in [ - "geometric_product", - "wedge", - "inner_product", - "commutator", - "anti_commutator", - "left_contraction", - ]: - expected = getattr(reference, method_name)(A, B) - actual = getattr(algebra, method_name)(A, B) - assert actual.dtype == dtype - assert torch.allclose(actual.float(), expected.float(), atol=atol, rtol=atol), method_name - - mv = torch.randn(2, algebra.dim, dtype=dtype) * 0.25 - assert algebra.norm_sq(mv).dtype == dtype - assert torch.allclose(algebra.norm_sq(mv).float(), reference.norm_sq(mv).float(), atol=atol, rtol=atol) - - bivector = torch.zeros(1, algebra.dim, dtype=dtype) - bivector[0, 3] = 0.125 - actual_exp = algebra.exp(bivector) - expected_exp = reference.exp(bivector) - assert actual_exp.dtype == dtype - assert torch.allclose(actual_exp.float(), expected_exp.float(), atol=atol, rtol=atol) - - @pytest.mark.parametrize( - ("algebra_dtype", "input_dtype", "expected_dtype"), - [ - (torch.float64, torch.float32, torch.float64), - (torch.float32, torch.float16, torch.float32), - (torch.float16, torch.bfloat16, torch.float32), - (torch.bfloat16, torch.float16, torch.float32), - ], - ) - @pytest.mark.parametrize("leaf_n", [2, 6]) - def test_geometric_product_promotes_inputs_with_algebra_dtype( - self, - algebra_dtype, - input_dtype, - expected_dtype, - leaf_n, - ): - # Mixed input dtypes should promote with the algebra's sign-buffer dtype. - # The test runs both a forced-recursive and leaf-backed path via leaf_n. - torch.manual_seed(71) - algebra = PartitionedCliffordAlgebra(4, 0, 0, device=DEVICE, dtype=algebra_dtype, leaf_n=leaf_n) - reference = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=expected_dtype) - atol = _dtype_tolerance(expected_dtype) - - A = torch.randn(2, algebra.dim, dtype=input_dtype) * 0.25 - B = torch.randn(2, algebra.dim, dtype=input_dtype) * 0.25 - - actual = algebra.geometric_product(A, B) - expected = reference.geometric_product(A.to(dtype=expected_dtype), B.to(dtype=expected_dtype)) - - assert actual.dtype == expected_dtype - assert torch.allclose(actual.float(), expected.float(), atol=atol, rtol=atol) - - bivector = torch.zeros(1, algebra.dim, dtype=input_dtype) - bivector[0, 3] = 0.125 - actual_exp = algebra.exp(bivector) - expected_exp = reference.exp(bivector.to(dtype=expected_dtype)) - - assert actual_exp.dtype == expected_dtype - assert torch.allclose(actual_exp.float(), expected_exp.float(), atol=atol, rtol=atol) - - def test_stable_accumulation_reduces_cumulative_forward_error(self): - # A long product chain amplifies fp32 accumulation error. Promoting only - # recursive accumulation to fp64 should improve agreement with a dense - # fp64 reference while returning fp32 outputs. - torch.manual_seed(73) - reference = CliffordAlgebra(8, 0, 0, device=DEVICE, dtype=torch.float64) - standard = PartitionedCliffordAlgebra( - 8, 0, 0, device=DEVICE, dtype=torch.float32, leaf_n=4, product_chunk_size=1 - ) - stable = PartitionedCliffordAlgebra( - 8, - 0, - 0, - device=DEVICE, - dtype=torch.float32, - leaf_n=4, - product_chunk_size=1, - accumulation_dtype=torch.float64, - ) - assert stable.left_sub.accumulation_dtype == torch.float64 - - factors = [torch.randn(1, standard.dim, dtype=torch.float32) * 0.02 for _ in range(5)] - expected = factors[0].double() - actual_standard = factors[0] - actual_stable = factors[0] - for factor in factors[1:]: - expected = reference.geometric_product(expected, factor.double()) - actual_standard = standard.geometric_product(actual_standard, factor) - actual_stable = stable.geometric_product(actual_stable, factor) - - standard_error = (actual_standard.double() - expected).norm() - stable_error = (actual_stable.double() - expected).norm() - - assert actual_stable.dtype == torch.float32 - assert stable_error < standard_error * 0.5 - - def test_stable_accumulation_reduces_cumulative_backward_error(self): - # Backward accumulation can drift for repeated products as well. This - # mirrors the forward stability test but compares input gradients against - # a dense fp64 reference graph. - torch.manual_seed(79) - reference = CliffordAlgebra(8, 0, 0, device=DEVICE, dtype=torch.float64) - standard = PartitionedCliffordAlgebra( - 8, 0, 0, device=DEVICE, dtype=torch.float32, leaf_n=4, product_chunk_size=1 - ) - stable = PartitionedCliffordAlgebra( - 8, - 0, - 0, - device=DEVICE, - dtype=torch.float32, - leaf_n=4, - product_chunk_size=1, - accumulation_dtype=torch.float64, - ) - - A = torch.randn(2, standard.dim, dtype=torch.float32) * 0.02 - B = torch.randn(2, standard.dim, dtype=torch.float32) * 0.02 - A_ref = A.double().requires_grad_(True) - B_ref = B.double().requires_grad_(True) - A_standard = A.detach().clone().requires_grad_(True) - B_standard = B.detach().clone().requires_grad_(True) - A_stable = A.detach().clone().requires_grad_(True) - B_stable = B.detach().clone().requires_grad_(True) - - reference.geometric_product(reference.geometric_product(A_ref, B_ref), B_ref).square().sum().backward() - standard.geometric_product( - standard.geometric_product(A_standard, B_standard), B_standard - ).square().sum().backward() - stable.geometric_product(stable.geometric_product(A_stable, B_stable), B_stable).square().sum().backward() - - standard_error = (A_standard.grad.double() - A_ref.grad).norm() + (B_standard.grad.double() - B_ref.grad).norm() - stable_error = (A_stable.grad.double() - A_ref.grad).norm() + (B_stable.grad.double() - B_ref.grad).norm() - - assert A_stable.grad.dtype == torch.float32 - assert B_stable.grad.dtype == torch.float32 - assert stable_error < standard_error * 0.7 - - def test_recursive_product_chunked_pair_merge_matches_core_kernel(self): - # Chunking processes only a slice of right-basis pairs at a time. This - # product-level check ensures chunk boundaries do not change the merged - # result or batch broadcasting behavior. - torch.manual_seed(29) - reference, algebra = _make_pair(5, 1, 0, leaf_n=2, product_chunk_size=3) - A = torch.randn(2, 1, algebra.dim, dtype=torch.float64) - B = torch.randn(1, 3, algebra.dim, dtype=torch.float64) - - expected = reference.geometric_product(A, B) - actual = algebra.geometric_product(A, B) - - assert torch.allclose(actual, expected, atol=1e-9, rtol=1e-9) - - @pytest.mark.parametrize("pair_range", ["full", "chunk"]) - def test_indexed_right_interaction_merge_matches_reference(self, pair_range): - # Private merge helpers are tested directly because they are the critical - # memory-saving replacement for materialized right-interaction tensors. - # Both full-range and chunk-range calls must produce identical values and - # gradients. - torch.manual_seed(31) - product_chunk_size = None if pair_range == "full" else 3 - algebra = PartitionedCliffordAlgebra( - 5, - 1, - 0, - device=DEVICE, - dtype=torch.float64, - leaf_n=2, - product_chunk_size=product_chunk_size, - ) - if pair_range == "full": - start, end = 0, algebra._right_pair_count - else: - start, end = algebra._product_chunk_size, 2 * algebra._product_chunk_size - - _, _, pair_result, pair_signs = algebra._right_product_slice(start, end) - pair_count = int(pair_signs.numel()) - left_products = torch.randn(2, pair_count, algebra.left_dim, dtype=torch.float64) - merged_terms = left_products.clone().requires_grad_(True) - index_terms = left_products.clone().requires_grad_(True) - - actual = algebra._merge_right_interactions(merged_terms, pair_result, pair_signs) - expected = algebra._merge_right_interactions_index_add(index_terms, pair_result, pair_signs) - - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - weight = torch.randn_like(actual) - (actual * weight).sum().backward() - (expected * weight).sum().backward() - - assert torch.allclose(merged_terms.grad, index_terms.grad, atol=1e-12, rtol=1e-12) - - @pytest.mark.parametrize(("p", "q", "r"), [(5, 1, 0), (1, 1, 4)]) - def test_vectorized_full_pair_product_matches_chunked_with_gradients(self, p, q, r): - # Vectorized and chunked pair accumulation should be algebraically - # identical. The degenerate signature case verifies that compact - # surviving-pair routing for null dimensions also matches. - torch.manual_seed(83 + p * 13 + q * 7 + r) - reference = CliffordAlgebra(p, q, r, device=DEVICE, dtype=torch.float64) - vectorized = PartitionedCliffordAlgebra(p, q, r, device=DEVICE, dtype=torch.float64, leaf_n=3) - chunked = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float64, - leaf_n=3, - product_chunk_size=5, - ) - - assert vectorized._product_chunk_size >= vectorized._right_pair_count - assert chunked._product_chunk_size < chunked._right_pair_count - - A = torch.randn(2, vectorized.dim, dtype=torch.float64) - B = torch.randn(2, vectorized.dim, dtype=torch.float64) - A_ref = A.clone().requires_grad_(True) - B_ref = B.clone().requires_grad_(True) - A_vectorized = A.clone().requires_grad_(True) - B_vectorized = B.clone().requires_grad_(True) - A_chunked = A.clone().requires_grad_(True) - B_chunked = B.clone().requires_grad_(True) - - expected = reference.geometric_product(A_ref, B_ref) - actual_vectorized = vectorized.geometric_product(A_vectorized, B_vectorized) - actual_chunked = chunked.geometric_product(A_chunked, B_chunked) - - assert torch.allclose(actual_vectorized, expected, atol=1e-10, rtol=1e-10) - assert torch.allclose(actual_chunked, expected, atol=1e-10, rtol=1e-10) - - weight = torch.linspace(-0.3, 0.4, vectorized.dim, dtype=torch.float64) - (expected * weight).sum().backward() - (actual_vectorized * weight).sum().backward() - (actual_chunked * weight).sum().backward() - - assert torch.allclose(A_vectorized.grad, A_ref.grad, atol=1e-10, rtol=1e-10) - assert torch.allclose(B_vectorized.grad, B_ref.grad, atol=1e-10, rtol=1e-10) - assert torch.allclose(A_chunked.grad, A_ref.grad, atol=1e-10, rtol=1e-10) - assert torch.allclose(B_chunked.grad, B_ref.grad, atol=1e-10, rtol=1e-10) - - def test_unit_rotor_chain_maintains_normalization_beyond_depth_threshold(self): - # Rotor chains are a realistic high-depth workload. The small bivector - # step should preserve unit norm over many recursive products when - # accumulation is promoted. - algebra = PartitionedCliffordAlgebra( - 8, - 0, - 0, - device=DEVICE, - dtype=torch.float32, - leaf_n=4, - product_chunk_size=16, - accumulation_dtype=torch.float64, - ) - bivector = torch.zeros(1, algebra.dim, dtype=torch.float32) - bivector[0, (1 << 0) | (1 << 6)] = 0.03125 - step = algebra._exp_bivector_closed(bivector) - - rotor = torch.zeros_like(step) - rotor[0, 0] = 1.0 - identity = rotor.clone() - - max_error = 0.0 - for depth in range(1, 129): - rotor = algebra.geometric_product(rotor, step) - if depth % 16 == 0: - rotor_norm = algebra.geometric_product(rotor, algebra.reverse(rotor)) - max_error = max(max_error, (rotor_norm - identity).abs().max().item()) - - assert max_error < 5e-5 - - def test_bridge_sign_for_high_times_low_vector(self): - # The bridge sign is easiest to see with one vector from the high child - # multiplied by one vector from the low child. e5 * e1 must be -e15 - # because the high vector crosses one low vector. - reference = CliffordAlgebra(5, 0, 0, device=DEVICE, dtype=torch.float64) - algebra = PartitionedCliffordAlgebra(5, 0, 0, device=DEVICE, dtype=torch.float64, leaf_n=4) - - A = torch.zeros(1, 32, dtype=torch.float64) - B = torch.zeros(1, 32, dtype=torch.float64) - A[0, 16] = 1.0 # e5, the high block's first vector after a 4D low split - B[0, 1] = 1.0 # e1, a low-block vector - - actual = algebra.geometric_product(A, B) - expected = reference.geometric_product(A, B) - - assert actual[0, 17].item() == -1.0 - assert torch.equal(actual, expected) - - def test_null_cross_split_matches_core_kernel(self): - # Degenerate dimensions can live on either side of a split. This covers - # null-annihilation behavior when null bits cross child boundaries. - _assert_matches_monolithic(4, 2, 2, leaf_n=4, shape=(2,)) - - def test_minkowski_signature_matches_core_kernel(self): - # Lorentzian signs are a common model target and should match the dense - # kernel under forced recursion. - _assert_matches_monolithic(1, 3, 0, leaf_n=2, shape=(3,)) - - def test_degenerate_signature_matches_core_kernel(self): - # Small degenerate mixed signature used as a dense-reference smoke test - # before the broader parameterized sweep. - _assert_matches_monolithic(2, 1, 2, leaf_n=2, shape=(2,)) - - @pytest.mark.parametrize( - ("p", "q", "r"), - [ - (0, 3, 0), - (0, 0, 4), - (2, 0, 2), - (0, 2, 2), - (2, 2, 1), - ], - ) - def test_general_signature_sweep_matches_core_kernel(self, p, q, r): - # Compact sweep over pure negative, pure null, mixed positive/null, - # negative/null, and fully mixed signatures. Binary and unary operations - # are checked together so structural sign buffers stay aligned with the - # product implementation. - torch.manual_seed(19 + p * 11 + q * 7 + r) - reference, algebra = _make_pair(p, q, r, leaf_n=2, product_chunk_size=3) - A = torch.randn(2, algebra.dim, dtype=torch.float64) - B = torch.randn(2, algebra.dim, dtype=torch.float64) - - for method_name in [ - "geometric_product", - "wedge", - "inner_product", - "commutator", - "anti_commutator", - "left_contraction", - ]: - expected = getattr(reference, method_name)(A, B) - actual = getattr(algebra, method_name)(A, B) - assert torch.allclose(actual, expected, atol=1e-10, rtol=1e-10), method_name - - for method_name in [ - "reverse", - "pseudoscalar_product", - "dual", - "grade_involution", - "clifford_conjugation", - "norm_sq", - ]: - expected = getattr(reference, method_name)(A) - actual = getattr(algebra, method_name)(A) - assert torch.allclose(actual, expected, atol=1e-10, rtol=1e-10), method_name - - def test_recursive_node_does_not_allocate_global_cayley_table(self): - # A recursive root must not expose dense Cayley buffers. If this fails, - # high-dimensional partitioned construction has likely regressed toward - # dense memory behavior. - algebra = PartitionedCliffordAlgebra(8, 0, 0, device=DEVICE, leaf_n=6) - - assert not hasattr(algebra, "cayley_indices") - assert not hasattr(algebra, "cayley_signs") - assert algebra.left_sub.dim == 16 - assert algebra.right_sub.dim == 16 - - def test_static_structural_sign_buffers_match_core_kernel(self): - # These buffers are derived analytically in the partitioned kernel but - # should match the dense core on small signatures. They feed unary - # operations, norms, pseudoscalar products, exp, and right contraction. - reference, algebra = _make_pair(3, 1, 1, leaf_n=2) - - for name in [ - "grade_index", - "rev_signs", - "_involution_signs", - "conj_signs", - "_cayley_diag", - "_norm_sq_signs", - "_hermitian_signs", - "_ps_source", - "_ps_signs", - "_bv_indices", - "bv_sq_scalar", - "rc_action", - ]: - expected = getattr(reference, name) - actual = getattr(algebra, name) - if expected.dtype.is_floating_point: - assert torch.allclose(actual, expected) - else: - assert torch.equal(actual, expected) - - def test_unary_operations_match_core_kernel(self): - # Public unary APIs should not reveal whether the algebra is dense, - # leaf-backed, or recursively partitioned. - torch.manual_seed(31) - reference, algebra = _make_pair(3, 1, 0, leaf_n=2) - mv = torch.randn(2, algebra.dim, dtype=torch.float64) - vectors = torch.randn(2, algebra.n, dtype=torch.float64) - - assert torch.allclose(algebra.embed_vector(vectors), reference.embed_vector(vectors)) - assert torch.allclose(algebra.get_grade_norms(mv), reference.get_grade_norms(mv), atol=1e-12, rtol=1e-12) - for grade in range(algebra.num_grades): - assert torch.allclose(algebra.grade_projection(mv, grade), reference.grade_projection(mv, grade)) - - for method_name in [ - "reverse", - "pseudoscalar_product", - "dual", - "grade_involution", - "clifford_conjugation", - "norm_sq", - ]: - expected = getattr(reference, method_name)(mv) - actual = getattr(algebra, method_name)(mv) - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - def test_binary_operations_match_core_kernel(self): - # Non-product binary APIs use combinations of geometric products and - # grade masks. Broadcasting across different leading axes is included. - torch.manual_seed(37) - reference, algebra = _make_pair(3, 1, 0, leaf_n=2, product_chunk_size=3) - A = torch.randn(2, 1, algebra.dim, dtype=torch.float64) - B = torch.randn(1, 3, algebra.dim, dtype=torch.float64) - - for method_name in [ - "wedge", - "inner_product", - "commutator", - "anti_commutator", - "left_contraction", - ]: - expected = getattr(reference, method_name)(A, B) - actual = getattr(algebra, method_name)(A, B) - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - def test_bivector_vector_right_contraction_matches_core_kernel(self): - # Right contraction has a specialized bivector-vector path. Projecting - # random inputs to grades 2 and 1 keeps the test focused on that path. - torch.manual_seed(41) - reference, algebra = _make_pair(4, 0, 0, leaf_n=2) - A = reference.grade_projection(torch.randn(3, algebra.dim, dtype=torch.float64), 2) - B = reference.grade_projection(torch.randn(3, algebra.dim, dtype=torch.float64), 1) - - expected = reference.right_contraction(A, B) - actual = algebra.right_contraction(A, B) - - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - def test_blade_and_versor_operations_match_core_kernel(self): - # Higher-level blade and versor helpers compose several primitive - # operations. A dense reference here catches shape and promotion mistakes - # without needing large dimensions. - torch.manual_seed(43) - reference, algebra = _make_pair(3, 1, 0, leaf_n=2) - mv = torch.randn(2, algebra.dim, dtype=torch.float64) - blade = torch.zeros(2, algebra.dim, dtype=torch.float64) - blade[:, 1] = 1.0 - blade[:, 2] = 0.25 - - for method_name in ["blade_inverse", "blade_project", "blade_reject", "reflect", "versor_product"]: - if method_name == "blade_inverse": - expected = reference.blade_inverse(blade) - actual = algebra.blade_inverse(blade) - elif method_name in {"blade_project", "blade_reject"}: - expected = getattr(reference, method_name)(mv, blade) - actual = getattr(algebra, method_name)(mv, blade) - elif method_name == "versor_product": - expected = reference.versor_product(blade, mv) - actual = algebra.versor_product(blade, mv) - else: - expected = getattr(reference, method_name)(mv, blade) - actual = getattr(algebra, method_name)(mv, blade) - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - def test_sandwich_variants_match_core_kernel(self): - # Sandwich variants have different broadcasting contracts: - # per-channel, same-batch multi-channel, and multi-rotor. All should use - # recursive products transparently. - torch.manual_seed(47) - reference, algebra = _make_pair(3, 0, 0, leaf_n=2) - bivector = torch.zeros(4, algebra.dim, dtype=torch.float64) - bivector[:, 3] = torch.linspace(0.05, 0.2, 4, dtype=torch.float64) - rotors = reference.exp(bivector) - - x_batch_channel = torch.randn(2, 4, algebra.dim, dtype=torch.float64) - x_same_batch = torch.randn(4, 3, algebra.dim, dtype=torch.float64) - x_multi = torch.randn(2, 3, algebra.dim, dtype=torch.float64) - - expected = reference.per_channel_sandwich(rotors, x_batch_channel) - actual = algebra.per_channel_sandwich(rotors, x_batch_channel) - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - expected = reference.sandwich_product(rotors, x_same_batch) - actual = algebra.sandwich_product(rotors, x_same_batch) - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - expected = reference.multi_rotor_sandwich(rotors, x_multi) - actual = algebra.multi_rotor_sandwich(rotors, x_multi) - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - def test_exp_paths_match_core_kernel(self): - # Exp has separate simple-bivector, compiled-safe, and Taylor helper - # paths. Small Cl4 keeps the dense reference cheap while still exercising - # the nontrivial bivector decomposition path. - reference, algebra = _make_pair(4, 0, 0, leaf_n=2) - B = torch.zeros(2, algebra.dim, dtype=torch.float64) - B[:, 3] = torch.tensor([0.125, -0.25], dtype=torch.float64) - - assert torch.allclose( - algebra._exp_bivector_closed(B), reference._exp_bivector_closed(B), atol=1e-12, rtol=1e-12 - ) - assert torch.allclose(algebra.exp(B), reference.exp(B), atol=1e-12, rtol=1e-12) - - mv = torch.zeros(2, algebra.dim, dtype=torch.float64) - mv[:, 0] = 0.1 - mv[:, 1] = torch.tensor([0.02, -0.03], dtype=torch.float64) - mv[:, 3] = torch.tensor([0.04, 0.05], dtype=torch.float64) - assert torch.allclose( - algebra._exp_taylor(mv, order=6), reference._exp_taylor(mv, order=6), atol=1e-12, rtol=1e-12 - ) - - @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") - def test_compile_geometric_product_matches_eager(self): - # ``aot_eager`` is used as a lightweight compile backend to ensure the - # recursive product graph avoids obvious compile breaks. - torch.manual_seed(53) - algebra = PartitionedCliffordAlgebra(4, 1, 0, device=DEVICE, dtype=torch.float32, leaf_n=2) - A = torch.randn(3, algebra.dim) - B = torch.randn(3, algebra.dim) - - def product(x, y): - return algebra.geometric_product(x, y) - - compiled_product = torch.compile(product, backend="aot_eager") - - assert torch.allclose(compiled_product(A, B), product(A, B), atol=1e-5, rtol=1e-5) - - @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") - def test_compile_training_backward_matches_eager(self): - # Compilation must preserve both parameter and input gradients. The tiny - # layer keeps the graph small but includes an nn.Parameter broadcast. - torch.manual_seed(59) - eager_layer = _PartitionedProductLayer(4, 0, 0) - compiled_layer = _PartitionedProductLayer(4, 0, 0) - compiled_layer.weight.data.copy_(eager_layer.weight.data) - - x_eager = torch.randn(3, eager_layer.algebra.dim, requires_grad=True) - x_compiled = x_eager.detach().clone().requires_grad_(True) - - eager_loss = eager_layer(x_eager).square().sum() - eager_loss.backward() - - compiled_forward = torch.compile(compiled_layer, backend="aot_eager") - compiled_loss = compiled_forward(x_compiled).square().sum() - compiled_loss.backward() - - assert torch.allclose(compiled_loss, eager_loss, atol=1e-5, rtol=1e-5) - assert torch.allclose(x_compiled.grad, x_eager.grad, atol=1e-4, rtol=1e-4) - assert torch.allclose(compiled_layer.weight.grad, eager_layer.weight.grad, atol=1e-4, rtol=1e-4) diff --git a/tests/test_partitioned_dense_reference.py b/tests/test_partitioned_dense_reference.py deleted file mode 100644 index 1c48d55..0000000 --- a/tests/test_partitioned_dense_reference.py +++ /dev/null @@ -1,256 +0,0 @@ -# Versor: Universal Geometric Algebra Neural Network -# Copyright (C) 2026 Eunkyum Kim -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# - -"""Dense-reference checks for partitioned Clifford algebra. - -This file covers the region where both kernels are valid, so regressions show -up as direct numerical error instead of only algebraic identity failures. The -slow Cl12 case is intentionally separated from the regular unit sweep because -the dense reference allocates a monolithic Cayley table. - -Use this file when a future partitioned operation can still be compared against -``CliffordAlgebra``. Use ``test_partitioned_highdim.py`` when the dimension is -above the dense table limit or when the reference should be purely axiomatic. -""" - -import pytest -import torch - -from core.config import PartitionConfig, make_algebra - -pytestmark = pytest.mark.unit - -DEVICE = "cpu" - - -def _make_dense_reference_pair(p: int, q: int, r: int, leaf_n: int, product_chunk_size: int = 32): - """Create dense and partitioned algebras with matched signature and dtype.""" - reference = make_algebra(p, q, r, kernel="dense", device=DEVICE, dtype=torch.float64) - partitioned = make_algebra( - p, - q, - r, - kernel="partitioned", - device=DEVICE, - dtype=torch.float64, - partition=PartitionConfig(leaf_n=leaf_n, product_chunk_size=product_chunk_size), - ) - return reference, partitioned - - -def _dense_inputs(dim: int, seed: int, batch_shape=(1,)): - """Build deterministic small-magnitude dense inputs for error checks.""" - generator = torch.Generator(device=DEVICE).manual_seed(seed) - scale = 0.125 - A = torch.randn(*batch_shape, dim, dtype=torch.float64, generator=generator) * scale - B = torch.randn(*batch_shape, dim, dtype=torch.float64, generator=generator) * scale - return A, B - - -def _assert_bounded_error(actual: torch.Tensor, expected: torch.Tensor, label: str, *, atol=2e-9, rtol=2e-9): - """Assert both absolute and relative agreement with a diagnostic message.""" - diff = actual - expected - max_abs = diff.abs().max().item() - denominator = expected.norm().clamp_min(torch.finfo(expected.dtype).eps) - relative = (diff.norm() / denominator).item() - assert torch.allclose(actual, expected, atol=atol, rtol=rtol), ( - f"{label} exceeded dense-reference error bounds: max_abs={max_abs:.3e}, relative={relative:.3e}" - ) - if rtol == 0.0: - assert max_abs == 0.0, f"{label} expected exact agreement, got max_abs={max_abs:.3e}" - else: - assert relative < rtol * 10.0, f"{label} relative error is excessive: {relative:.3e}" - - -@pytest.mark.parametrize( - ("p", "q", "r", "leaf_n"), - [ - pytest.param(4, 0, 0, 2, id="cl4_forced_recursive"), - pytest.param(5, 2, 1, 4, id="cl8_mixed"), - pytest.param(8, 0, 0, 4, id="cl8_euclidean"), - pytest.param(7, 2, 1, 5, id="cl10_mixed"), - ], -) -def test_dense_comparable_binary_operations_have_bounded_error(p, q, r, leaf_n): - # This is the fast dense-overlap sweep. Keep dimensions at or below Cl10 so - # the regular non-slow suite does not spend most of its time building dense - # Cayley tables. The separate slow test below covers Cl12 explicitly. - reference, partitioned = _make_dense_reference_pair(p, q, r, leaf_n) - A, B = _dense_inputs(partitioned.dim, seed=503 + p * 17 + q * 11 + r) - - for method_name in [ - "geometric_product", - "wedge", - "inner_product", - "commutator", - "anti_commutator", - "left_contraction", - ]: - expected = getattr(reference, method_name)(A, B) - actual = getattr(partitioned, method_name)(A, B) - _assert_bounded_error(actual, expected, f"Cl({p},{q},{r}).{method_name}") - - -@pytest.mark.parametrize( - ("p", "q", "r", "leaf_n"), - [ - pytest.param(8, 0, 0, 4, id="cl8_euclidean"), - pytest.param(7, 2, 1, 5, id="cl10_mixed"), - ], -) -def test_dense_comparable_unary_operations_have_bounded_error(p, q, r, leaf_n): - # Unary operations are mostly structural sign or mask operations. Comparing - # them against the dense kernel here catches public-basis permutation - # mistakes separately from the recursive product path. - reference, partitioned = _make_dense_reference_pair(p, q, r, leaf_n) - mv, _ = _dense_inputs(partitioned.dim, seed=719 + p * 17 + q * 11 + r, batch_shape=(2,)) - - for grade in range(partitioned.num_grades): - expected = reference.grade_projection(mv, grade) - actual = partitioned.grade_projection(mv, grade) - _assert_bounded_error(actual, expected, f"Cl({p},{q},{r}).grade_projection({grade})", atol=0.0, rtol=0.0) - - for method_name in [ - "reverse", - "pseudoscalar_product", - "dual", - "grade_involution", - "clifford_conjugation", - "norm_sq", - ]: - expected = getattr(reference, method_name)(mv) - actual = getattr(partitioned, method_name)(mv) - _assert_bounded_error(actual, expected, f"Cl({p},{q},{r}).{method_name}") - - vectors = torch.randn(2, partitioned.n, dtype=torch.float64, generator=torch.Generator().manual_seed(907)) - _assert_bounded_error(partitioned.embed_vector(vectors), reference.embed_vector(vectors), "embed_vector") - - -def test_partitioned_8d_fixture_matches_dense_reference(partitioned_algebra_8d): - # Fixture-level coverage ensures conftest's shared high-dimensional algebra - # declarations stay aligned with the dense-overlap verification method. - reference = make_algebra(8, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float64) - A, B = _dense_inputs(partitioned_algebra_8d.dim, seed=1013, batch_shape=(2,)) - - expected = reference.geometric_product(A, B) - actual = partitioned_algebra_8d.geometric_product(A, B) - - _assert_bounded_error(actual, expected, "partitioned_algebra_8d.geometric_product") - - -@pytest.mark.slow -def test_partitioned_12d_fixture_dense_reference_error_is_bounded(partitioned_algebra_12d): - # Cl12 is the highest dimension supported by the dense kernel. This test is - # slow because it allocates the monolithic dense Cayley table, but it is the - # strongest direct check that partitioned accumulation does not introduce - # excessive numerical error before we leave dense-reference territory. - reference = make_algebra(12, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float64) - A, B = _dense_inputs(partitioned_algebra_12d.dim, seed=1207) - - expected = reference.geometric_product(A, B) - actual = partitioned_algebra_12d.geometric_product(A, B) - - _assert_bounded_error(actual, expected, "partitioned_algebra_12d.geometric_product", atol=5e-9, rtol=5e-9) - - -@pytest.mark.slow -def test_partitioned_12d_mixed_fixture_matches_bitmask_reference(partitioned_algebra_12d_mixed): - # Mixed signature Cl(8,3,1) checks negative metric signs and null - # annihilation with an axiomatic sparse reference. This complements the - # dense Cl12 test without requiring another dense mixed-signature table. - entries_a = [(0, 0.25), (3, -1.5), (257, 0.75), (2049, 2.0)] - entries_b = [(1, -0.5), (384, 1.25), (1025, -2.0), (4095, 0.5)] - A = _make_sparse_multivector(partitioned_algebra_12d_mixed, entries_a, torch.float64) - B = _make_sparse_multivector(partitioned_algebra_12d_mixed, entries_b, torch.float64) - - expected = torch.zeros_like(A) - expected_sparse = _sparse_product_reference(entries_a, entries_b, 8, 3, 1) - for index, value in expected_sparse.items(): - expected[0, index] = value - - actual = partitioned_algebra_12d_mixed.geometric_product(A, B) - - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - -@pytest.mark.slow -def test_partitioned_16d_fixture_basis_product_matches_bitmask_reference(partitioned_algebra_16d): - # The 16D fixture is above the dense kernel limit, so the reference is a - # single basis product computed from bitmask rules. The binary literals make - # the active public dimensions visible when adding nearby cases. - p, q, r = 10, 4, 2 - index_a = 0b1001_0010_0110_1011 - index_b = 0b0110_1101_1000_1110 - result_index, sign = _basis_product_reference(index_a, index_b, p, q, r) - - A = torch.zeros(1, partitioned_algebra_16d.dim, dtype=torch.float32) - B = torch.zeros(1, partitioned_algebra_16d.dim, dtype=torch.float32) - A[0, index_a] = 1.25 - B[0, index_b] = -0.5 - - expected = torch.zeros_like(A) - expected[0, result_index] = 1.25 * -0.5 * sign - actual = partitioned_algebra_16d.geometric_product(A, B) - - assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) - - -def _make_sparse_multivector(algebra, entries, dtype: torch.dtype) -> torch.Tensor: - """Materialize sparse ``(bitmask_index, coefficient)`` entries.""" - mv = torch.zeros(1, algebra.dim, dtype=dtype) - for index, value in entries: - mv[0, index] += value - return mv - - -def _sparse_product_reference( - entries_a: list[tuple[int, float]], - entries_b: list[tuple[int, float]], - p: int, - q: int, - r: int, -) -> dict[int, float]: - """Sparse multivector product using only bitmask basis rules.""" - result = {} - for index_a, value_a in entries_a: - for index_b, value_b in entries_b: - result_index, sign = _basis_product_reference(index_a, index_b, p, q, r) - if sign == 0.0: - continue - result[result_index] = result.get(result_index, 0.0) + value_a * value_b * sign - return result - - -def _basis_product_reference(index_a: int, index_b: int, p: int, q: int, r: int) -> tuple[int, float]: - """Reference basis product for ``e_index_a * e_index_b``. - - The result blade index is XOR. The sign is the parity of swaps between set - bits, adjusted for repeated negative basis vectors. A repeated null basis - vector makes the coefficient exactly zero. - """ - n = p + q + r - swap_count = 0 - for bit in range(n): - if index_a & (1 << bit): - # Count right-operand vectors that must move left across this - # left-operand vector to restore canonical increasing bit order. - swap_count += (index_b & ((1 << bit) - 1)).bit_count() - - sign = -1.0 if swap_count % 2 else 1.0 - - # Negative metric dimensions occupy [p, p + q). Repeating one contributes - # e_i^2 = -1. - negative_mask = sum(1 << bit for bit in range(p, p + q)) - if ((index_a & index_b & negative_mask).bit_count() % 2) == 1: - sign = -sign - - # Null dimensions occupy [p + q, n). Repeating any one contributes e_i^2 = 0. - null_mask = sum(1 << bit for bit in range(p + q, n)) - if (index_a & index_b & null_mask) != 0: - sign = 0.0 - - return index_a ^ index_b, sign diff --git a/tests/test_partitioned_highdim.py b/tests/test_partitioned_highdim.py deleted file mode 100644 index 34f4846..0000000 --- a/tests/test_partitioned_highdim.py +++ /dev/null @@ -1,952 +0,0 @@ -# Versor: Universal Geometric Algebra Neural Network -# Copyright (C) 2026 Eunkyum Kim -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# - -"""High-dimensional verification for partitioned Clifford algebra. - -These tests avoid monolithic Cayley-table references. For n >= 12, the -reference is computed from axiomatic bitmask rules, sub-algebraic isomorphisms, -algebraic identities, or closed forms inside known two-dimensional subalgebras. - -Bitmask convention used throughout this file: - -* Basis blade ``e_I`` is stored at integer index ``I``. -* Bit ``i`` in ``I`` means vector basis element ``e_i`` participates in the - blade. For example, ``0b0101`` represents ``e_0 e_2``. -* The geometric product result blade is ``index_a ^ index_b`` unless a repeated - null basis vector annihilates the term. -* The product sign is the parity of swaps needed to move the right blade's - vectors past the left blade's vectors, followed by metric signs from repeated - negative basis vectors. - -When adding high-dimensional tests, prefer constructing sparse multivectors as -``[(bitmask_index, coefficient), ...]`` and comparing against -``_sparse_product_reference``. This keeps the reference independent from both -the dense Cayley table and the partitioned implementation. -""" - -import math - -import pytest -import torch - -from core.algebra import CliffordAlgebra -from core.partitioned_algebra import PartitionedCliffordAlgebra - -pytestmark = pytest.mark.slow - -DEVICE = "cpu" - - -def _signature_for_range_reference(p: int, q: int, r: int, start: int, width: int) -> tuple[int, int, int]: - """Return the local signature for a contiguous range of public dimensions. - - Dimensions are ordered by signature block: positive ``[0, p)``, negative - ``[p, p + q)``, then null ``[p + q, p + q + r)``. This helper is used when a - test embeds a lower-dimensional algebra inside a contiguous slice of a - larger algebra and needs to build the matching local reference algebra. - """ - end = start + width - p_count = max(0, min(end, p) - start) - q_count = max(0, min(end, p + q) - max(start, p)) - r_count = max(0, min(end, p + q + r) - max(start, p + q)) - assert p_count + q_count + r_count == width - return p_count, q_count, r_count - - -def _shift_index(index: int, offset: int) -> int: - """Shift every set basis bit in ``index`` by ``offset`` public dimensions. - - Example: local blade ``0b101`` at offset 3 becomes global blade - ``0b101000``. This is the bitmask equivalent of embedding a local blade - into a higher-dimensional subspace without changing the local blade order. - """ - shifted = 0 - bit = 0 - while index: - if index & 1: - shifted |= 1 << (bit + offset) - index >>= 1 - bit += 1 - return shifted - - -def _make_orthonormal_subspace_basis( - algebra: PartitionedCliffordAlgebra, - frame: torch.Tensor, -) -> torch.Tensor: - """Return embedded basis blades for an orthonormal frame. - - ``frame`` contains ``width`` orthonormal vectors in the ambient algebra's - vector space. The returned matrix has one row for every local bitmask - ``0..2**width-1``. Row ``basis_index`` is built by multiplying the selected - frame vectors in increasing bit order, matching the canonical bitmask basis - order used by ``CliffordAlgebra``. - """ - width = frame.shape[-1] - vector_indices = (1 << torch.arange(algebra.n, dtype=torch.long, device=frame.device)).long() - vectors = torch.zeros(width, algebra.dim, dtype=frame.dtype, device=frame.device) - vectors[:, vector_indices] = frame.transpose(0, 1) - - basis = torch.zeros(2**width, algebra.dim, dtype=frame.dtype, device=frame.device) - basis[0, 0] = 1.0 - for basis_index in range(1, basis.shape[0]): - blade = torch.zeros(1, algebra.dim, dtype=frame.dtype, device=frame.device) - blade[0, 0] = 1.0 - for bit in range(width): - if basis_index & (1 << bit): - blade = algebra.geometric_product(blade, vectors[bit : bit + 1]) - basis[basis_index] = blade[0] - return basis - - -def _basis_product_reference(index_a: int, index_b: int, p: int, q: int, r: int) -> tuple[int, float]: - """Reference product for two canonical basis blades using bit operations. - - This is the smallest trusted oracle in the partitioned tests. It intentionally - does not call either dense or partitioned algebra code. - - The algorithm mirrors the Clifford product rules: - - 1. The result blade contains basis vectors that appear in exactly one input, - so its index is ``index_a ^ index_b``. - 2. Every vector in ``index_a`` must move past lower-numbered vectors in - ``index_b``. The parity of those swaps gives the exterior sign. - 3. Repeated negative basis vectors contribute an extra ``-1`` metric sign. - 4. Repeated null basis vectors make the entire product zero. - """ - n = p + q + r - swap_count = 0 - for bit in range(n): - if index_a & (1 << bit): - # Count vectors from B with lower public index than the current A - # vector. Each such pair must be swapped once. - swap_count += (index_b & ((1 << bit) - 1)).bit_count() - - sign = -1.0 if swap_count % 2 else 1.0 - - # Negative dimensions occupy [p, p + q). Repeating one of those vectors - # squares it to -1, so an odd number of repeated negative bits flips sign. - negative_mask = sum(1 << bit for bit in range(p, p + q)) - if ((index_a & index_b & negative_mask).bit_count() % 2) == 1: - sign = -sign - - # Null dimensions occupy [p + q, n). Repeating a null basis vector squares - # it to zero, annihilating that basis-pair contribution. - null_mask = sum(1 << bit for bit in range(p + q, n)) - if (index_a & index_b & null_mask) != 0: - sign = 0.0 - - return index_a ^ index_b, sign - - -def _partitioned_basis_product(algebra, index_a: int, index_b: int) -> tuple[int, float]: - """Evaluate a basis product by walking the partition tree directly. - - This helper verifies the recursive routing logic without constructing full - multivectors. At each internal node, public indices are first converted into - split-local order if the node uses a repeated-tile permutation. The low - ``right_n`` bits are handled by the right child, the remaining high bits by - the left child, and the bridge sign accounts for commuting right-child - factors across left-child factors. - """ - if algebra.core is not None: - # Leaves are intentionally backed by the dense local kernel. Reading the - # leaf Cayley table here checks the partition traversal and merge signs, - # not the local dense product itself. - result_index = int(algebra.core.cayley_indices[index_a, index_b].item()) - sign = float(algebra.core.cayley_signs[index_a, index_b].item()) - return result_index, sign - - input_sign = 1.0 - if algebra.basis_permutation.uses_permutation: - # Some signatures are internally reordered to share identical child - # subalgebras. The permutation carries signs because changing vector - # order changes canonical blade orientation. - split_a = int(algebra.basis_permutation.public_to_split[index_a].item()) - split_b = int(algebra.basis_permutation.public_to_split[index_b].item()) - input_sign *= float(algebra.basis_permutation.split_signs[split_a].item()) - input_sign *= float(algebra.basis_permutation.split_signs[split_b].item()) - index_a = split_a - index_b = split_b - - right_mask = algebra.right_dim - 1 - left_a, right_a = index_a >> algebra.right_n, index_a & right_mask - left_b, right_b = index_b >> algebra.right_n, index_b & right_mask - - # Recurse independently in the child algebras. The child result indices are - # then packed back into the split-local index layout. - left_result, left_sign = _partitioned_basis_product(algebra.left_sub, left_a, left_b) - right_result, right_sign = _partitioned_basis_product(algebra.right_sub, right_a, right_b) - right_b_index = torch.tensor([right_b], dtype=torch.long, device=algebra.device) - # Bridge sign = (-1) ** (grade(left_A) * grade(right_B)). It is the only - # sign that couples the two child products at an internal node. - bridge_sign = float(algebra._bridge_signs_for_right_b(right_b_index, torch.float64)[0, left_a].item()) - result_index = (left_result << algebra.right_n) | right_result - sign = input_sign * left_sign * right_sign * bridge_sign - - if algebra.basis_permutation.uses_permutation: - # Convert split-local result orientation back to public canonical order. - sign *= float(algebra.basis_permutation.split_signs[result_index].item()) - result_index = int(algebra.basis_permutation.split_to_public[result_index].item()) - - return result_index, sign - - -def _sparse_product_reference( - entries_a: list[tuple[int, float]], - entries_b: list[tuple[int, float]], - p: int, - q: int, - r: int, -) -> dict[int, float]: - """Sparse multivector product using only the bitmask basis oracle.""" - result = {} - for index_a, value_a in entries_a: - for index_b, value_b in entries_b: - result_index, sign = _basis_product_reference(index_a, index_b, p, q, r) - if sign == 0.0: - continue - result[result_index] = result.get(result_index, 0.0) + value_a * value_b * sign - return result - - -def _signature_sweep_entries(p: int, q: int, r: int) -> tuple[list[tuple[int, float]], list[tuple[int, float]]]: - """Build sparse inputs that exercise low, middle, high, and null bits. - - The exact coefficients are arbitrary but non-symmetric. The bit positions - are chosen to cross split boundaries for balanced trees and to hit positive, - negative, and null signature blocks when present. Reuse this helper for - broad signature sweeps; add explicit hand-picked entries when a test needs a - very specific blade interaction. - """ - n = p + q + r - entries_a = [ - # Scalar term: verifies scalar multiplication and accumulation into an - # already-populated output blade. - (0, 0.375), - # Low plus middle bit: likely crosses left/right children in Cl12+. - ((1 << 0) | (1 << (n // 2)), -0.5), - # Two high bits: catches sign handling away from the low-order block. - ((1 << (n - 3)) | (1 << (n - 1)), 0.875), - # Low plus high bit: exercises bridge signs across the partition split. - ((1 << 1) | (1 << (n - 2)), -1.125), - ] - entries_b = [ - (1 << 0, -0.75), - ((1 << (n // 2)) | (1 << (n - 4)), 1.25), - ((1 << (n - 1)) | (1 << 2), -0.625), - ((1 << 1) | (1 << (n - 5)) | (1 << (n - 2)), 0.5), - ] - - if r > 0: - # Include repeated null-bit products. Any pair that repeats ``null_bit`` - # should vanish in the reference and in the partitioned kernel. - null_bit = p + q - entries_a.append(((1 << null_bit) | (1 << 1), 0.25)) - entries_b.append(((1 << null_bit) | (1 << 2), -1.5)) - - return entries_a, entries_b - - -def _make_sparse_multivector(algebra: PartitionedCliffordAlgebra, entries, dtype: torch.dtype) -> torch.Tensor: - """Materialize ``[(bitmask, coeff), ...]`` entries into a dense tensor.""" - mv = torch.zeros(1, algebra.dim, dtype=dtype) - for index, value in entries: - mv[0, index] += value - return mv - - -def _make_expected_multivector(algebra: PartitionedCliffordAlgebra, entries, dtype: torch.dtype) -> torch.Tensor: - """Materialize a sparse dictionary reference into a dense output tensor.""" - expected = torch.zeros(1, algebra.dim, dtype=dtype) - for index, value in entries.items(): - expected[0, index] = value - return expected - - -def _long_taylor_simple_bivector(theta: float, square: float, order: int = 80) -> tuple[float, float]: - """Reference exp(theta B) for a simple bivector with known ``B**2`` scalar.""" - scalar = 0.0 - bivector = 0.0 - power = 1.0 - for k in range(order + 1): - if k > 0: - power *= theta - if k % 2 == 0: - scalar += power * (square ** (k // 2)) / math.factorial(k) - else: - bivector += power * (square ** ((k - 1) // 2)) / math.factorial(k) - return scalar, bivector - - -def _canonical_basis_term(index: int, coefficient: float) -> tuple[int, float]: - """Normalize zero coefficients so identity checks compare exact tuples.""" - if coefficient == 0.0: - return 0, 0.0 - return index, coefficient - - -def _multiply_signed_basis( - algebra: PartitionedCliffordAlgebra, - left: tuple[int, float], - right: tuple[int, float], -) -> tuple[int, float]: - """Multiply two signed basis terms through the partition-tree basis oracle.""" - left_index, left_coeff = left - right_index, right_coeff = right - if left_coeff == 0.0 or right_coeff == 0.0: - return 0, 0.0 - result_index, sign = _partitioned_basis_product(algebra, left_index, right_index) - return _canonical_basis_term(result_index, left_coeff * right_coeff * sign) - - -def _reverse_sign(index: int) -> float: - """Sign applied by reversion to a basis blade of grade ``popcount(index)``.""" - grade = index.bit_count() - return -1.0 if (grade * (grade - 1) // 2) % 2 else 1.0 - - -def _grade_involution_sign(index: int) -> float: - """Sign applied by grade involution to a basis blade.""" - return -1.0 if index.bit_count() % 2 else 1.0 - - -class TestPartitionedHighDimensionalVerification: - def test_cl12_sparse_multivector_product_matches_direct_bitmask_reference(self): - # Cl(8,3,1) is dense enough to exercise recursive routing, includes a - # negative block and a null block, and still keeps the sparse reference - # human-readable. The selected indices include low, middle, high, and - # all-bits-set blades so sign and null handling are both visible. - p, q, r = 8, 3, 1 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float64, - leaf_n=6, - product_chunk_size=32, - ) - entries_a = [(0, 0.25), (3, -1.5), (257, 0.75), (2049, 2.0)] - entries_b = [(1, -0.5), (384, 1.25), (1025, -2.0), (4095, 0.5)] - - A = torch.zeros(1, algebra.dim, dtype=torch.float64) - B = torch.zeros(1, algebra.dim, dtype=torch.float64) - for index, value in entries_a: - A[0, index] = value - for index, value in entries_b: - B[0, index] = value - - actual = algebra.geometric_product(A, B) - expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) - - expected = torch.zeros_like(actual) - for index, value in expected_sparse.items(): - expected[0, index] = value - - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - @pytest.mark.parametrize( - ("p", "q", "r", "dtype", "atol"), - [ - (12, 0, 0, torch.float64, 1e-12), - (0, 12, 0, torch.float64, 1e-12), - (6, 6, 0, torch.float64, 1e-12), - (8, 3, 1, torch.float64, 1e-12), - (4, 4, 4, torch.float64, 1e-12), - (10, 4, 2, torch.float32, 1e-6), - ], - ) - def test_sparse_multivector_products_match_bitmask_rules_across_highdim_signatures( - self, - p, - q, - r, - dtype, - atol, - ): - # This sweep is the broad regression surface for metric signatures: - # pure positive, pure negative, mixed nondegenerate, mixed degenerate, - # and the current 16D ceiling. Each case uses the same sparse pattern so - # differences come from signature signs and null annihilation only. - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=dtype, - leaf_n=6, - product_chunk_size=8 if p + q + r >= 16 else 16, - ) - entries_a, entries_b = _signature_sweep_entries(p, q, r) - A = _make_sparse_multivector(algebra, entries_a, dtype) - B = _make_sparse_multivector(algebra, entries_b, dtype) - - actual = algebra.geometric_product(A, B) - expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) - expected = _make_expected_multivector(algebra, expected_sparse, dtype) - - assert torch.allclose(actual, expected, atol=atol, rtol=atol) - - @pytest.mark.parametrize( - ("p", "q", "r", "dtype", "atol"), - [ - pytest.param(8, 0, 0, torch.float64, 1e-12, id="n8-positive"), - pytest.param(0, 9, 0, torch.float64, 1e-12, id="n9-negative"), - pytest.param(0, 0, 10, torch.float64, 1e-12, id="n10-null"), - pytest.param(6, 3, 2, torch.float64, 1e-12, id="n11-mixed-null"), - pytest.param(8, 3, 1, torch.float64, 1e-12, id="n12-mixed-null"), - pytest.param(7, 4, 2, torch.float64, 1e-12, id="n13-mixed-null"), - pytest.param(8, 4, 2, torch.float32, 1e-6, id="n14-mixed-null"), - pytest.param(9, 4, 2, torch.float32, 1e-6, id="n15-mixed-null"), - pytest.param(10, 4, 2, torch.float32, 1e-6, id="n16-mixed-null"), - ], - ) - def test_sparse_multivector_products_cover_every_dimension_from_8_to_16(self, p, q, r, dtype, atol): - # This is the main 8D-16D product sweep. It deliberately uses one - # signature per dimension so changes in split depth, odd/even splits, - # null compaction, and the 16D cap are all exercised by a single test - # family. The bitmask oracle makes this independent from dense support. - n = p + q + r - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=dtype, - leaf_n=6, - product_chunk_size=8 if n >= 14 else 16, - ) - entries_a, entries_b = _signature_sweep_entries(p, q, r) - A = _make_sparse_multivector(algebra, entries_a, dtype) - B = _make_sparse_multivector(algebra, entries_b, dtype) - - actual = algebra.geometric_product(A, B) - expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) - expected = _make_expected_multivector(algebra, expected_sparse, dtype) - - assert torch.allclose(actual, expected, atol=atol, rtol=atol) - - def test_automatic_tiled_cl12_product_matches_bitmask_reference(self): - # Cl(6,3,3) has signature gcd 3, so the automatic planner can reorder - # dimensions into repeated Cl(2,1,1) tiles and share child modules. The - # bitmask reference stays in public canonical order, making this a direct - # check that permutation signs are restored correctly at the boundary. - p, q, r = 6, 3, 3 - dtype = torch.float64 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=dtype, - leaf_n=4, - product_chunk_size=16, - ) - entries_a, entries_b = _signature_sweep_entries(p, q, r) - A = _make_sparse_multivector(algebra, entries_a, dtype) - B = _make_sparse_multivector(algebra, entries_b, dtype) - - actual = algebra.geometric_product(A, B) - expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) - expected = _make_expected_multivector(algebra, expected_sparse, dtype) - - assert algebra.basis_permutation.uses_permutation - assert algebra.left_sub.left_sub is algebra.right_sub - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - def test_cl12_sparse_multivectors_satisfy_numerical_identities(self): - # This test avoids any dense reference. Instead, it checks identities - # that must hold for the product regardless of partition shape: - # distributivity, associativity, reverse anti-automorphism, grade - # involution automorphism, scalar multiplication, and vector - # anticommutation. - p, q, r = 7, 3, 2 - dtype = torch.float64 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=dtype, - leaf_n=6, - product_chunk_size=64, - ) - deep_algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=dtype, - leaf_n=3, - product_chunk_size=17, - ) - entries_a, entries_b = _signature_sweep_entries(p, q, r) - entries_c = [(0, -0.25), (5, 0.5), ((1 << 4) | (1 << 9), -0.75), ((1 << 10) | 3, 1.125)] - - A = _make_sparse_multivector(algebra, entries_a, dtype) - B = _make_sparse_multivector(algebra, entries_b, dtype) - C = _make_sparse_multivector(algebra, entries_c, dtype) - - AB = algebra.geometric_product(A, B) - BC = algebra.geometric_product(B, C) - AC = algebra.geometric_product(A, C) - - assert torch.allclose(AB, deep_algebra.geometric_product(A, B), atol=1e-10, rtol=1e-10) - assert torch.allclose(A + B, B + A, atol=0.0, rtol=0.0) - assert torch.allclose(algebra.geometric_product(A, B + C), AB + AC, atol=1e-10, rtol=1e-10) - assert torch.allclose(algebra.geometric_product(A + B, C), AC + BC, atol=1e-10, rtol=1e-10) - - assert torch.allclose( - algebra.geometric_product(AB, C), - algebra.geometric_product(A, BC), - atol=1e-10, - rtol=1e-10, - ) - assert torch.allclose( - algebra.reverse(AB), - algebra.geometric_product(algebra.reverse(B), algebra.reverse(A)), - atol=1e-10, - rtol=1e-10, - ) - assert torch.allclose( - algebra.grade_involution(AB), - algebra.geometric_product(algebra.grade_involution(A), algebra.grade_involution(B)), - atol=1e-10, - rtol=1e-10, - ) - - scalar = torch.zeros_like(A) - scalar[..., 0] = -1.75 - assert torch.allclose(algebra.geometric_product(scalar, A), -1.75 * A, atol=1e-12, rtol=1e-12) - assert torch.allclose(algebra.geometric_product(A, scalar), -1.75 * A, atol=1e-12, rtol=1e-12) - - e0 = _make_sparse_multivector(algebra, [(1 << 0, 1.0)], dtype) - e2 = _make_sparse_multivector(algebra, [(1 << 2, 1.0)], dtype) - e0e2 = algebra.geometric_product(e0, e2) - e2e0 = algebra.geometric_product(e2, e0) - assert torch.allclose(e0e2, -e2e0, atol=1e-12, rtol=1e-12) - assert not torch.allclose(e0e2, e2e0, atol=1e-12, rtol=1e-12) - - def test_cl12_backward_matches_finite_difference_directional_derivative(self): - # Autograd is checked against a directional finite difference using - # sparse perturbations. Keeping A, B, dA, dB, and the loss weight sparse - # makes the reference direction easy to inspect while still exercising - # the recursive product backward path. - p, q, r = 7, 3, 2 - dtype = torch.float64 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=dtype, - leaf_n=6, - product_chunk_size=16, - accumulation_dtype=torch.float64, - ) - - entries_a, entries_b = _signature_sweep_entries(p, q, r) - entries_da = [(1, 0.125), ((1 << 5) | (1 << 8), -0.25), ((1 << 10) | 3, 0.375)] - entries_db = [(2, -0.2), ((1 << 6) | (1 << 11), 0.15), ((1 << 7) | (1 << 9), -0.3)] - entries_w = [(0, -0.5), ((1 << 1) | (1 << 7), 0.75), ((1 << 8) | (1 << 11), -1.25)] - - A = _make_sparse_multivector(algebra, entries_a, dtype).requires_grad_(True) - B = _make_sparse_multivector(algebra, entries_b, dtype).requires_grad_(True) - dA = _make_sparse_multivector(algebra, entries_da, dtype) - dB = _make_sparse_multivector(algebra, entries_db, dtype) - weight = _make_sparse_multivector(algebra, entries_w, dtype) - - loss = (algebra.geometric_product(A, B) * weight).sum() - loss.backward() - directional_grad = (A.grad * dA).sum() + (B.grad * dB).sum() - - eps = 1e-6 - loss_plus = (algebra.geometric_product(A.detach() + eps * dA, B.detach() + eps * dB) * weight).sum() - loss_minus = (algebra.geometric_product(A.detach() - eps * dA, B.detach() - eps * dB) * weight).sum() - finite_difference = (loss_plus - loss_minus) / (2.0 * eps) - - assert torch.allclose(directional_grad, finite_difference, atol=1e-9, rtol=1e-9) - - def test_cl16_dense_basis_product_matches_direct_bitmask_reference(self): - # A single basis-product case at the 16D cap catches indexing mistakes - # that are invisible in smaller dimensions. The binary literals make it - # easy to see which public bits are active in each operand. - p, q, r = 10, 4, 2 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float32, - leaf_n=6, - product_chunk_size=8, - ) - index_a = 0b1001_0010_0110_1011 - index_b = 0b0110_1101_1000_1110 - result_index, sign = _basis_product_reference(index_a, index_b, p, q, r) - - A = torch.zeros(1, algebra.dim) - B = torch.zeros(1, algebra.dim) - A[0, index_a] = 1.25 - B[0, index_b] = -0.5 - - actual = algebra.geometric_product(A, B) - expected = torch.zeros_like(actual) - expected[0, result_index] = 1.25 * -0.5 * sign - - assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) - - def test_cl12_embedded_subalgebra_product_matches_local_isomorphism(self): - # A contiguous 6D public slice should behave exactly like a standalone - # local algebra with the induced signature. ``_shift_index`` embeds each - # local basis blade into the global bit positions, then the global output - # is compared against the shifted local product. - p, q, r = 6, 4, 2 - offset, width = 3, 6 - local_p, local_q, local_r = _signature_for_range_reference(p, q, r, offset, width) - - global_algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float64, - leaf_n=6, - product_chunk_size=16, - ) - local_algebra = PartitionedCliffordAlgebra( - local_p, - local_q, - local_r, - device=DEVICE, - dtype=torch.float64, - leaf_n=4, - product_chunk_size=16, - ) - - entries_a = [(0, 0.5), (3, -1.25), (17, 0.75), (63, -0.2)] - entries_b = [(1, 2.0), (10, -0.5), (48, 1.5)] - local_a = torch.zeros(1, local_algebra.dim, dtype=torch.float64) - local_b = torch.zeros(1, local_algebra.dim, dtype=torch.float64) - global_a = torch.zeros(1, global_algebra.dim, dtype=torch.float64) - global_b = torch.zeros(1, global_algebra.dim, dtype=torch.float64) - - for index, value in entries_a: - local_a[0, index] = value - global_a[0, _shift_index(index, offset)] = value - for index, value in entries_b: - local_b[0, index] = value - global_b[0, _shift_index(index, offset)] = value - - local_product = local_algebra.geometric_product(local_a, local_b) - actual = global_algebra.geometric_product(global_a, global_b) - - expected = torch.zeros_like(actual) - shifted_indices = torch.tensor( - [_shift_index(index, offset) for index in range(local_algebra.dim)], - dtype=torch.long, - ) - expected[..., shifted_indices] = local_product - - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - def test_cl12_random_three_dimensional_subspace_projects_to_n3_engine(self): - # This is a basis-independent isomorphism check. A random orthonormal - # 3-frame defines an embedded Cl(3,0) subalgebra inside Cl(12,0). After - # multiplying embedded multivectors globally, projecting back onto the - # constructed basis must recover the local Cl(3,0) product. - dtype = torch.float64 - global_algebra = PartitionedCliffordAlgebra( - 12, - 0, - 0, - device=DEVICE, - dtype=dtype, - leaf_n=6, - product_chunk_size=64, - ) - local_algebra = CliffordAlgebra(3, 0, 0, device=DEVICE, dtype=dtype) - - generator = torch.Generator(device=DEVICE).manual_seed(211) - frame, _ = torch.linalg.qr(torch.randn(global_algebra.n, 3, dtype=dtype, generator=generator)) - basis = _make_orthonormal_subspace_basis(global_algebra, frame.contiguous()) - - gram = basis @ basis.transpose(0, 1) - assert torch.allclose(gram, torch.eye(local_algebra.dim, dtype=dtype), atol=1e-12, rtol=1e-12) - - local_a = torch.randn(2, local_algebra.dim, dtype=dtype, generator=generator) - local_b = torch.randn(2, local_algebra.dim, dtype=dtype, generator=generator) - global_a = local_a @ basis - global_b = local_b @ basis - - local_expected = local_algebra.geometric_product(local_a, local_b) - global_product = global_algebra.geometric_product(global_a, global_b) - projected = global_product @ basis.transpose(0, 1) - embedded_expected = local_expected @ basis - - assert torch.allclose(projected, local_expected, atol=1e-10, rtol=1e-10) - assert torch.allclose(global_product, embedded_expected, atol=1e-10, rtol=1e-10) - - def test_cl16_recursive_sign_merge_matches_direct_bitmask_reference(self): - # This helper-level test walks the partition tree one basis product at a - # time. It is faster than materializing full multivectors and directly - # targets the recursive split/bridge/permutation sign merge. - p, q, r = 10, 4, 2 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float32, - leaf_n=6, - product_chunk_size=4, - ) - pairs = [ - (0, 0), - (1, 1 << 15), - (0xABCD, 0x1357), - (0xFFFF, 0x0011), - (0x2222, 0xDDDD), - (0x7A5C, 0xC3A5), - ((1 << 14) | 7, (1 << 14) | 3), - ] - - for index_a, index_b in pairs: - expected = _basis_product_reference(index_a, index_b, p, q, r) - actual = _partitioned_basis_product(algebra, index_a, index_b) - assert actual == expected - - def test_cl16_forced_unbalanced_tree_matches_bitmask_reference(self): - # Explicit tree expressions are how callers force a non-default split. - # This tree peels off four low dimensions, then recursively splits the - # remaining high dimensions. It crosses positive, negative, and null - # signature blocks, so both public-basis permutation and bridge signs - # must be correct. - p, q, r = 10, 4, 2 - dtype = torch.float32 - partition_tree = "R=0-3; L.R=4-7; L.L.R=8-11; L.L.L=12-15" - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=dtype, - leaf_n=6, - product_chunk_size=4, - partition_tree=partition_tree, - ) - entries_a, entries_b = _signature_sweep_entries(p, q, r) - A = _make_sparse_multivector(algebra, entries_a, dtype) - B = _make_sparse_multivector(algebra, entries_b, dtype) - - actual = algebra.geometric_product(A, B) - expected_sparse = _sparse_product_reference(entries_a, entries_b, p, q, r) - expected = _make_expected_multivector(algebra, expected_sparse, dtype) - - tree = algebra.describe_tree() - assert "root: Cl(10,4,2)" in tree - assert "root.L.L" in tree - assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) - - def test_cl16_basis_products_satisfy_algebraic_identities(self): - # Tuple-valued basis products make exact identity checks possible at the - # 16D cap. Associativity validates recursive merge consistency; reverse - # and grade-involution identities validate the sign formulas used by the - # structural buffers. - p, q, r = 10, 4, 2 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float32, - leaf_n=6, - product_chunk_size=4, - ) - triples = [ - (0x1234, 0x00F0, 0xABCD), - (0x7001, 0x02A8, 0x1111), - ((1 << 14) | 0x35, 0x4440, 0x2101), - (0x7A5C, (1 << 15) | 0x81, 0x0013), - ] - - for index_a, index_b, index_c in triples: - left = _multiply_signed_basis( - algebra, - _multiply_signed_basis(algebra, (index_a, 1.0), (index_b, 1.0)), - (index_c, 1.0), - ) - right = _multiply_signed_basis( - algebra, - (index_a, 1.0), - _multiply_signed_basis(algebra, (index_b, 1.0), (index_c, 1.0)), - ) - assert left == right - - pairs = [ - (0x1234, 0x00F0), - (0x7A5C, 0xC3A5), - ((1 << 14) | 0x101, (1 << 14) | 0x077), - ((1 << 15) | 0x222, 0x1357), - ] - for index_a, index_b in pairs: - ab = _multiply_signed_basis(algebra, (index_a, 1.0), (index_b, 1.0)) - reverse_ab = _canonical_basis_term(ab[0], ab[1] * _reverse_sign(ab[0])) - reverse_ba = _multiply_signed_basis( - algebra, - (index_b, _reverse_sign(index_b)), - (index_a, _reverse_sign(index_a)), - ) - assert reverse_ab == reverse_ba - - involution_ab = _canonical_basis_term(ab[0], ab[1] * _grade_involution_sign(ab[0])) - involution_product = _multiply_signed_basis( - algebra, - (index_a, _grade_involution_sign(index_a)), - (index_b, _grade_involution_sign(index_b)), - ) - assert involution_ab == involution_product - - def test_cl16_simple_bivector_exp_matches_long_taylor_reference(self): - # Simple bivector exponentials have a closed two-term form. The long - # Taylor reference is intentionally scalar-only and independent of the - # algebra implementation, so it catches coefficient/sign regressions in - # the high-dimensional closed-form path. - p, q, r = 16, 0, 0 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float64, - leaf_n=6, - product_chunk_size=4, - ) - bivector_index = (1 << 0) | (1 << 13) - theta = 0.375 - square = -1.0 - - B = torch.zeros(1, algebra.dim, dtype=torch.float64) - B[0, bivector_index] = theta - actual = algebra._exp_bivector_closed(B) - - scalar_ref, bivector_ref = _long_taylor_simple_bivector(theta, square, order=80) - - assert torch.allclose(actual[0, 0], torch.tensor(scalar_ref, dtype=torch.float64), atol=1e-14, rtol=1e-14) - assert torch.allclose( - actual[0, bivector_index], - torch.tensor(bivector_ref, dtype=torch.float64), - atol=1e-14, - rtol=1e-14, - ) - assert torch.count_nonzero(actual).item() == 2 - - @pytest.mark.parametrize( - ("p", "q", "r", "bivector_index", "theta", "scalar_ref", "bivector_ref"), - [ - (16, 0, 0, (1 << 0) | (1 << 13), 0.375, math.cos(0.375), math.sin(0.375)), - (1, 15, 0, (1 << 0) | (1 << 1), 0.25, math.cosh(0.25), math.sinh(0.25)), - (14, 0, 2, (1 << 0) | (1 << 14), 0.5, 1.0, 0.5), - ], - ) - def test_cl16_simple_bivector_exp_matches_closed_form( - self, - p, - q, - r, - bivector_index, - theta, - scalar_ref, - bivector_ref, - ): - # Parameterized closed-form cases cover the three regimes: - # Euclidean-like elliptic, Lorentzian hyperbolic, and degenerate - # parabolic. The selected bivector index always contains exactly two - # bits so the expected output has only scalar and bivector components. - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float64, - leaf_n=6, - product_chunk_size=4, - ) - - B = torch.zeros(1, algebra.dim, dtype=torch.float64) - B[0, bivector_index] = theta - actual = algebra._exp_bivector_closed(B) - - assert torch.allclose(actual[0, 0], torch.tensor(scalar_ref, dtype=torch.float64), atol=1e-14, rtol=1e-14) - assert torch.allclose( - actual[0, bivector_index], - torch.tensor(bivector_ref, dtype=torch.float64), - atol=1e-14, - rtol=1e-14, - ) - assert torch.count_nonzero(actual).item() == 2 - - def test_cl16_lorentzian_bivector_exp_matches_long_taylor_reference(self): - # Separate long-Taylor check for the hyperbolic sign regime. In - # Cl(1,15), e0 has positive square and e1 has negative square, so - # (e0e1)^2 = +1 and exp(theta e0e1) uses cosh/sinh. - p, q, r = 1, 15, 0 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float64, - leaf_n=6, - product_chunk_size=4, - ) - bivector_index = (1 << 0) | (1 << 1) - theta = 0.25 - square = 1.0 - - B = torch.zeros(1, algebra.dim, dtype=torch.float64) - B[0, bivector_index] = theta - actual = algebra._exp_bivector_closed(B) - - scalar_ref, bivector_ref = _long_taylor_simple_bivector(theta, square, order=80) - - assert torch.allclose(actual[0, 0], torch.tensor(scalar_ref, dtype=torch.float64), atol=1e-14, rtol=1e-14) - assert torch.allclose( - actual[0, bivector_index], - torch.tensor(bivector_ref, dtype=torch.float64), - atol=1e-14, - rtol=1e-14, - ) - assert torch.count_nonzero(actual).item() == 2 - - def test_cl16_degenerate_repeated_null_factor_annihilates_product(self): - # In a degenerate signature, any product that repeats the same null - # basis vector must vanish. These cases keep additional non-null bits in - # each operand to ensure the annihilation is detected before other sign - # details can mask the bug. - p, q, r = 10, 4, 2 - algebra = PartitionedCliffordAlgebra( - p, - q, - r, - device=DEVICE, - dtype=torch.float32, - leaf_n=6, - product_chunk_size=4, - ) - null_bit = p + q - pairs = [ - ((1 << null_bit) | 0x35, (1 << null_bit) | 0xC0), - ((1 << (null_bit + 1)) | 0x1A5, (1 << (null_bit + 1)) | 0x21), - ] - - for index_a, index_b in pairs: - expected = _basis_product_reference(index_a, index_b, p, q, r) - actual = _partitioned_basis_product(algebra, index_a, index_b) - assert expected[1] == 0.0 - assert actual == expected diff --git a/tests/test_properties.py b/tests/test_properties.py index 4486a42..3a6df8e 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -8,7 +8,7 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers import CliffordLayerNorm pytestmark = pytest.mark.unit diff --git a/tests/test_riemannian_optimizer.py b/tests/test_riemannian_optimizer.py index 2fdae02..5c56bd3 100644 --- a/tests/test_riemannian_optimizer.py +++ b/tests/test_riemannian_optimizer.py @@ -14,7 +14,7 @@ import torch.nn as nn import torch.nn.functional as F -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers import MultiRotorLayer, RotorGadget, RotorLayer from optimizers.riemannian import ( MANIFOLD_EUCLIDEAN, diff --git a/tests/test_rotor_gadget.py b/tests/test_rotor_gadget.py index b380028..d2a9ab9 100644 --- a/tests/test_rotor_gadget.py +++ b/tests/test_rotor_gadget.py @@ -12,7 +12,7 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from layers import CliffordLinear, RotorGadget pytestmark = pytest.mark.unit @@ -286,7 +286,7 @@ class TestExpPolicy: def test_with_exact_policy(self, algebra_3d): """Test layer with EXACT exp policy.""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy algebra_3d.exp_policy = ExpPolicy.PRECISE @@ -305,7 +305,7 @@ def test_with_exact_policy(self, algebra_3d): def test_policy_fast_vs_exact(self, algebra_3d): """Compare FAST and EXACT policies (n=3: should match).""" - from core.decomposition import ExpPolicy + from core.runtime.decomposition import ExpPolicy torch.manual_seed(42) layer_a = RotorGadget( diff --git a/tests/test_rotor_translate.py b/tests/test_rotor_translate.py index b64b4e8..780518a 100644 --- a/tests/test_rotor_translate.py +++ b/tests/test_rotor_translate.py @@ -7,7 +7,7 @@ import pytest import torch -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra pytestmark = pytest.mark.unit from models.sr.net import SRGBN diff --git a/tests/test_symbolic_regression.py b/tests/test_symbolic_regression.py index acd6df8..64afd1f 100644 --- a/tests/test_symbolic_regression.py +++ b/tests/test_symbolic_regression.py @@ -8,7 +8,7 @@ import torch from omegaconf import OmegaConf -from core.algebra import CliffordAlgebra +from core.runtime.algebra import CliffordAlgebra from datalib.symbolic_regression import ( BLACKBOX_DATASETS, FIRST_PRINCIPLES_DATASETS, @@ -54,13 +54,8 @@ def _make_cfg(dataset_name=_TEST_DATASET, hidden_channels=4, num_layers=1, n_sam "exp_policy": "balanced", "kernel": "auto", "metric_search": metric_search, - "partition_threshold": 8, - "partition": { - "leaf_n": None, - "product_chunk_size": None, - "tree": None, - "accumulation_dtype": None, - }, + "dense_threshold": 8, + "default_grades": None, }, "dataset": { "dataset_name": dataset_name, From 9e3e6319dcd18fc75b86facb7e29c283ec84a329 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 12:07:00 +0900 Subject: [PATCH 13/45] refactor: clean-up the core architecture --- core/__init__.py | 8 +- core/foundation/module.py | 1 + core/planning/__init__.py | 10 +- core/planning/{request.py => layouts.py} | 4 +- core/planning/{translator.py => planner.py} | 172 ++++++++++---------- core/planning/{grade_plan.py => product.py} | 2 +- core/planning/unary.py | 2 +- core/runtime/algebra.py | 5 +- core/runtime/context.py | 24 ++- core/runtime/multivector.py | 2 +- core/runtime/projected.py | 23 ++- layers/blocks/attention.py | 6 +- tests/test_grade_plan.py | 41 ++++- 13 files changed, 174 insertions(+), 126 deletions(-) rename core/planning/{request.py => layouts.py} (98%) rename core/planning/{translator.py => planner.py} (74%) rename core/planning/{grade_plan.py => product.py} (99%) diff --git a/core/__init__.py b/core/__init__.py index d71f656..629ab99 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -27,9 +27,9 @@ from .foundation.module import AlgebraLike, CliffordModule from .foundation.validation import check_channels, check_multivector from .planning.flow import GradeFlow -from .planning.grade_plan import GradeProductExecutor, GradeProductPlan, build_grade_product_plan -from .planning.request import ProductRequest, build_product_request -from .planning.translator import GradeTranslator +from .planning.layouts import ProductRequest, build_product_request +from .planning.planner import GradePlanner +from .planning.product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan from .planning.tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .planning.unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request from .runtime.algebra import CliffordAlgebra @@ -69,7 +69,7 @@ "Multivector", "AlgebraSpec", "GradeLayout", - "GradeTranslator", + "GradePlanner", "make_algebra", "make_algebra_from_config", # device / validation diff --git a/core/foundation/module.py b/core/foundation/module.py index c87d5c7..7e8ab13 100644 --- a/core/foundation/module.py +++ b/core/foundation/module.py @@ -24,6 +24,7 @@ class AlgebraLike(Protocol): dim: int eps: float eps_sq: float + planner: object @property def device(self): diff --git a/core/planning/__init__.py b/core/planning/__init__.py index 19a28ff..6077188 100644 --- a/core/planning/__init__.py +++ b/core/planning/__init__.py @@ -5,12 +5,12 @@ # you may not use this file except in compliance with the License. # -"""Grade-plan translation and Torch executor lowering.""" +"""Static grade planning and Torch executor lowering.""" from .flow import GradeFlow -from .grade_plan import GradeProductExecutor, GradeProductPlan, build_grade_product_plan -from .request import ProductRequest, build_product_request -from .translator import GradeTranslator +from .layouts import ProductRequest, build_product_request +from .planner import GradePlanner +from .product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan from .tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request @@ -20,7 +20,7 @@ "GradeProductExecutor", "GradeProductPlan", "GradePlanTree", - "GradeTranslator", + "GradePlanner", "GradeUnaryExecutor", "GradeUnaryOp", "GradeUnaryPlan", diff --git a/core/planning/request.py b/core/planning/layouts.py similarity index 98% rename from core/planning/request.py rename to core/planning/layouts.py index e9aa349..641250b 100644 --- a/core/planning/request.py +++ b/core/planning/layouts.py @@ -5,7 +5,7 @@ # you may not use this file except in compliance with the License. # -"""Normalized product requests for grade-plan lowering.""" +"""Layout and request normalization for static grade planning.""" from __future__ import annotations @@ -24,7 +24,7 @@ class ProductRequest: """Fully resolved static request for one bilinear product. - The request is the translator's intermediate representation. It removes + The request is the planner's intermediate representation. It removes ambiguity from caller input before any executor is built: layouts are normalized, compact-vs-dense operand storage is known, and output grades are inferred when callers do not explicitly project them. diff --git a/core/planning/translator.py b/core/planning/planner.py similarity index 74% rename from core/planning/translator.py rename to core/planning/planner.py index beef0d3..0c4455a 100644 --- a/core/planning/translator.py +++ b/core/planning/planner.py @@ -5,15 +5,15 @@ # you may not use this file except in compliance with the License. # -"""Grade-aware translator from algebraic intent to static executors.""" +"""Grade-aware planner from algebraic intent to static executors.""" from __future__ import annotations import torch from core.foundation.layout import AlgebraSpec, GradeLayout -from core.planning.grade_plan import GradeProductExecutor, build_grade_product_plan_from_request -from core.planning.request import ProductRequest, build_product_request, normalize_product_op +from core.planning.layouts import ProductRequest, build_product_request, normalize_product_op +from core.planning.product import GradeProductExecutor, build_grade_product_plan_from_request from core.planning.tree import build_grade_plan_tree from core.planning.unary import ( GradeUnaryExecutor, @@ -24,10 +24,10 @@ ) -class GradeTranslator: +class GradePlanner: """Owns layout and product-plan lowering for one algebra instance. - The translator is deliberately not an ``nn.Module``. It builds static + The planner is deliberately not an ``nn.Module``. It builds static executor modules keyed by signature, grades, dtype, and device, while the algebra remains the source of truth for buffers and dense reference paths. """ @@ -53,10 +53,17 @@ def clear_cache(self) -> None: def _apply(self, fn): """Apply a PyTorch module-style transform to cached executor buffers.""" - for executor in self._product_executors.values(): + product_executors = list(self._product_executors.values()) + self._product_executors.clear() + for executor in product_executors: executor._apply(fn) - for executor in self._unary_executors.values(): + self._product_executors[self._product_cache_key(executor)] = executor + + unary_executors = list(self._unary_executors.values()) + self._unary_executors.clear() + for executor in unary_executors: executor._apply(fn) + self._unary_executors[self._unary_cache_key(executor)] = executor return self def product_executor( @@ -82,9 +89,41 @@ def product_executor( dtype=dtype, device=torch.device(device), ) - return self.executor_for_request(request, cache=cache) + return self.product_executor_for_request(request, cache=cache) + + def product_request( + self, + left: torch.Tensor, + right: torch.Tensor, + *, + op: str = "gp", + left_grades=None, + right_grades=None, + output_grades=None, + left_layout: GradeLayout = None, + right_layout: GradeLayout = None, + output_layout: GradeLayout = None, + left_compact: bool = False, + right_compact: bool = False, + ) -> ProductRequest: + """Normalize product intent into a static request without executing tensors.""" + return build_product_request( + self.spec, + left, + right, + op=op, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + left_layout=left_layout, + right_layout=right_layout, + output_layout=output_layout, + left_compact=left_compact, + right_compact=right_compact, + full_layout_allowed=self._full_layout_allowed(), + ) - def executor_for_request(self, request: ProductRequest, *, cache: bool = True) -> GradeProductExecutor: + def product_executor_for_request(self, request: ProductRequest, *, cache: bool = True) -> GradeProductExecutor: """Return an executor for an already normalized product request.""" key = request.cache_key executor = self._product_executors.get(key) if cache else None @@ -105,6 +144,30 @@ def product_tree(self, *, op: str, left_grades, right_grades, output_grades=None output_grades=output_grades, ) + def unary_request( + self, + values: torch.Tensor, + *, + op: str, + input_grades=None, + output_grades=None, + input_layout: GradeLayout = None, + output_layout: GradeLayout = None, + input_compact: bool = False, + ) -> UnaryRequest: + """Normalize unary intent into a static request without executing tensors.""" + return build_unary_request( + self.spec, + values, + op=op, + input_grades=input_grades, + output_grades=output_grades, + input_layout=input_layout, + output_layout=output_layout, + input_compact=input_compact, + full_layout_allowed=self._full_layout_allowed(), + ) + def unary_executor( self, *, @@ -143,87 +206,26 @@ def unary_executor_for_request(self, request: UnaryRequest, *, cache: bool = Tru self._unary_executors[key] = executor return executor - def planned_unary( - self, - values: torch.Tensor, - *, - op: str, - input_grades=None, - output_grades=None, - input_layout: GradeLayout = None, - output_layout: GradeLayout = None, - input_compact: bool = False, - compact_output: bool = False, - return_layout: bool = False, - ): - """Execute a unary operation using a static gather/sign plan.""" - request = build_unary_request( + def _product_cache_key(self, executor: GradeProductExecutor) -> tuple[object, ...]: + return ( self.spec, - values, - op=op, - input_grades=input_grades, - output_grades=output_grades, - input_layout=input_layout, - output_layout=output_layout, - input_compact=input_compact, - full_layout_allowed=self._full_layout_allowed(), + str(executor.coefficients.device), + str(executor.coefficients.dtype), + executor.op, + executor.left_grades, + executor.right_grades, + executor.output_grades, ) - executor = self.unary_executor_for_request(request) - output = executor.forward_compact(values) if request.input_compact else executor(values) - - if return_layout: - return output, executor.output_layout - if compact_output: - return output - return executor.output_layout.dense(output) - def projected_product( - self, - A: torch.Tensor, - B: torch.Tensor, - *, - left_grades=None, - right_grades=None, - output_grades=None, - left_layout: GradeLayout = None, - right_layout: GradeLayout = None, - output_layout: GradeLayout = None, - op: str = "gp", - left_compact: bool = False, - right_compact: bool = False, - compact_output: bool = False, - return_layout: bool = False, - ): - """Execute a projected product using dense or compact input lanes.""" - request = build_product_request( + def _unary_cache_key(self, executor: GradeUnaryExecutor) -> tuple[object, ...]: + return ( self.spec, - A, - B, - op=op, - left_grades=left_grades, - right_grades=right_grades, - output_grades=output_grades, - left_layout=left_layout, - right_layout=right_layout, - output_layout=output_layout, - left_compact=left_compact, - right_compact=right_compact, - full_layout_allowed=self._full_layout_allowed(), + str(executor.signs.device), + str(executor.signs.dtype), + executor.op, + executor.input_layout.grades, + executor.output_layout.grades, ) - executor = self.executor_for_request(request) - - if request.left_compact or request.right_compact: - A_values = A if request.left_compact else executor.left_layout.compact(A) - B_values = B if request.right_compact else executor.right_layout.compact(B) - values = executor.forward_compact(A_values, B_values) - else: - values = executor(A, B) - - if return_layout: - return values, executor.output_layout - if compact_output: - return values - return executor.output_layout.dense(values) def _full_layout_allowed(self) -> bool: return bool(getattr(self.algebra, "allow_full_layout_products", True)) diff --git a/core/planning/grade_plan.py b/core/planning/product.py similarity index 99% rename from core/planning/grade_plan.py rename to core/planning/product.py index 877ec5d..42e14dc 100644 --- a/core/planning/grade_plan.py +++ b/core/planning/product.py @@ -22,7 +22,7 @@ from core.foundation.basis import GradeProductOp, expand_output_grades, normalize_grades, operation_coefficient from core.foundation.layout import AlgebraSpec, GradeLayout -from core.planning.request import ProductRequest +from core.planning.layouts import ProductRequest from core.planning.tree import GradePlanTree, build_grade_plan_tree diff --git a/core/planning/unary.py b/core/planning/unary.py index 7b70512..5906b97 100644 --- a/core/planning/unary.py +++ b/core/planning/unary.py @@ -17,7 +17,7 @@ from core.foundation.basis import normalize_grades, reverse_sign from core.foundation.layout import AlgebraSpec, GradeLayout -from core.planning.request import check_layout_spec, is_compact_tensor, resolve_operand_layout +from core.planning.layouts import check_layout_spec, is_compact_tensor, resolve_operand_layout GradeUnaryOp = Literal["identity", "reverse", "grade_involution", "clifford_conjugation", "grade_projection"] _VALID_UNARY_OPS = {"identity", "reverse", "grade_involution", "clifford_conjugation", "grade_projection"} diff --git a/core/runtime/algebra.py b/core/runtime/algebra.py index 31260bc..e404211 100644 --- a/core/runtime/algebra.py +++ b/core/runtime/algebra.py @@ -169,9 +169,9 @@ def __init__( self.eps: float = float(_finfo.eps) self.eps_sq: float = float(_finfo.eps**2) - from core.planning.translator import GradeTranslator + from core.planning.planner import GradePlanner - self.translator = GradeTranslator(self) + self.planner = GradePlanner(self) @property def device(self): @@ -193,6 +193,7 @@ def _apply(self, fn): _finfo = torch.finfo(self.cayley_signs.dtype) self.eps = float(_finfo.eps) self.eps_sq = float(_finfo.eps**2) + self.planner._apply(fn) return result @property diff --git a/core/runtime/context.py b/core/runtime/context.py index 099568f..13a0053 100644 --- a/core/runtime/context.py +++ b/core/runtime/context.py @@ -16,7 +16,7 @@ from core.foundation.basis import normalize_grades from core.foundation.device import resolve_device, resolve_dtype from core.foundation.layout import AlgebraSpec, GradeLayout -from core.planning.translator import GradeTranslator +from core.planning.planner import GradePlanner from core.runtime.projected import ProjectedProductMixin @@ -51,7 +51,7 @@ def __init__( ) self._default_grades = None if default_grades is None else normalize_grades(default_grades, self.n) self._default_layout: Optional[GradeLayout] = None - self.translator = GradeTranslator(self) + self.planner = GradePlanner(self) self._sync_eps() @property @@ -76,9 +76,9 @@ def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: grades = range(self.num_grades) else: grades = self._default_grades - self._default_layout = self.translator.layout(grades) + self._default_layout = self.planner.layout(grades) return self._default_layout - return self.translator.layout(grades) + return self.planner.layout(grades) def _apply(self, fn): """Apply a PyTorch module-style device/dtype transform to cached executors.""" @@ -87,7 +87,7 @@ def _apply(self, fn): if probe.dtype.is_floating_point: self._dtype = probe.dtype self._sync_eps() - self.translator._apply(fn) + self.planner._apply(fn) return self def to(self, device=None, dtype=None): @@ -97,7 +97,7 @@ def to(self, device=None, dtype=None): if dtype is not None: self._dtype = resolve_dtype(dtype) self._sync_eps() - self.translator.clear_cache() + self.planner.clear_cache() return self def geometric_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: @@ -158,7 +158,7 @@ def planned_unary( return_layout: bool = False, ): """Execute a unary planned operation.""" - return self.translator.planned_unary( + request = self.planner.unary_request( values, op=op, input_grades=input_grades, @@ -166,9 +166,15 @@ def planned_unary( input_layout=input_layout, output_layout=output_layout, input_compact=input_compact, - compact_output=compact_output, - return_layout=return_layout, ) + executor = self.planner.unary_executor_for_request(request) + output = executor.forward_compact(values) if request.input_compact else executor(values) + + if return_layout: + return output, executor.output_layout + if compact_output: + return output + return executor.output_layout.dense(output) def _sync_eps(self) -> None: finfo = torch.finfo(self.dtype) diff --git a/core/runtime/multivector.py b/core/runtime/multivector.py index 51155bf..1da86fa 100644 --- a/core/runtime/multivector.py +++ b/core/runtime/multivector.py @@ -106,7 +106,7 @@ def dense(self) -> Multivector: def compact(self, grades) -> Multivector: """Return a compact-storage multivector containing ``grades``.""" - layout = self.algebra.translator.layout(grades) + layout = self.algebra.planner.layout(grades) return self.with_layout(layout) def with_layout(self, layout: GradeLayout) -> Multivector: diff --git a/core/runtime/projected.py b/core/runtime/projected.py index 7f30e24..0725302 100644 --- a/core/runtime/projected.py +++ b/core/runtime/projected.py @@ -15,7 +15,7 @@ class ProjectedProductMixin: - """Route declared grade products through an algebra's grade translator.""" + """Execute declared grade products through an algebra's static planner.""" def projected_product( self, @@ -34,7 +34,7 @@ def projected_product( compact_output: bool = False, return_layout: bool = False, ): - """Compute a declared grade-restricted product through the translator.""" + """Compute a declared grade-restricted product through a static executor.""" if not left_compact and left_layout is not None and A.shape[-1] == left_layout.dim: left_compact = left_layout.dim != self.dim if not right_compact and right_layout is not None and B.shape[-1] == right_layout.dim: @@ -43,7 +43,8 @@ def projected_product( check_multivector(A, self, "projected_product(A)") if not right_compact: check_multivector(B, self, "projected_product(B)") - return self.translator.projected_product( + + request = self.planner.product_request( A, B, left_grades=left_grades, @@ -55,9 +56,21 @@ def projected_product( op=op, left_compact=left_compact, right_compact=right_compact, - compact_output=compact_output, - return_layout=return_layout, ) + executor = self.planner.product_executor_for_request(request) + + if request.left_compact or request.right_compact: + A_values = A if request.left_compact else executor.left_layout.compact(A) + B_values = B if request.right_compact else executor.right_layout.compact(B) + values = executor.forward_compact(A_values, B_values) + else: + values = executor(A, B) + + if return_layout: + return values, executor.output_layout + if compact_output: + return values + return executor.output_layout.dense(values) def projected_geometric_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs): """Projected geometric product convenience wrapper.""" diff --git a/layers/blocks/attention.py b/layers/blocks/attention.py index d81e5d4..55c1274 100644 --- a/layers/blocks/attention.py +++ b/layers/blocks/attention.py @@ -111,7 +111,7 @@ def _precompute_score_tables(self): self._score_bivector_product = None if self.score_grades is not None: self.n_g2 = alg.n * (alg.n - 1) // 2 - self._score_layout = alg.translator.layout(self.score_grades) + self._score_layout = alg.planner.layout(self.score_grades) layout_indices = self._score_layout.indices_tensor(device=alg.device) rev_signs = torch.tensor( [reverse_sign(index) for index in self._score_layout.basis_indices], @@ -120,7 +120,7 @@ def _precompute_score_tables(self): ) self.register_buffer("_score_layout_indices", layout_indices) self.register_buffer("_score_rev_signs", rev_signs) - self._score_scalar_product = alg.translator.product_executor( + self._score_scalar_product = alg.planner.product_executor( op="gp", left_grades=self.score_grades, right_grades=self.score_grades, @@ -130,7 +130,7 @@ def _precompute_score_tables(self): cache=False, ) if self.n_g2 > 0: - self._score_bivector_product = alg.translator.product_executor( + self._score_bivector_product = alg.planner.product_executor( op="gp", left_grades=self.score_grades, right_grades=self.score_grades, diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index d25849f..7f827a2 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -5,12 +5,12 @@ from core.foundation.basis import basis_indices_for_grades, expand_output_grades, geometric_product_output_grades from core.foundation.layout import AlgebraSpec from core.planning.flow import GradeFlow -from core.planning.grade_plan import ( +from core.planning.layouts import build_product_request +from core.planning.planner import GradePlanner +from core.planning.product import ( GradeProductExecutor, build_grade_product_plan, ) -from core.planning.request import build_product_request -from core.planning.translator import GradeTranslator from core.planning.tree import build_grade_plan_tree from core.planning.unary import build_unary_request from core.runtime.algebra import CliffordAlgebra @@ -212,11 +212,11 @@ def test_algebra_projected_product_matches_dense_kernel_and_compact_output(): assert compact_actual.shape[-1] == AlgebraSpec.from_algebra(algebra).layout((0, 2)).dim -def test_grade_translator_reuses_projected_product_executor(): +def test_grade_planner_reuses_projected_product_executor(): algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) - translator = GradeTranslator(algebra) + planner = GradePlanner(algebra) - first = translator.product_executor( + first = planner.product_executor( op="gp", left_grades=(1,), right_grades=(1,), @@ -224,7 +224,7 @@ def test_grade_translator_reuses_projected_product_executor(): dtype=torch.float64, device=DEVICE, ) - second = translator.product_executor( + second = planner.product_executor( op="gp", left_grades=(1,), right_grades=(1,), @@ -236,6 +236,31 @@ def test_grade_translator_reuses_projected_product_executor(): assert first is second +def test_grade_planner_rekeys_cached_executor_after_dtype_move(): + algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) + executor = algebra.planner.product_executor( + op="gp", + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + dtype=algebra.dtype, + device=DEVICE, + ) + + algebra.to(dtype=torch.float32) + moved = algebra.planner.product_executor( + op="gp", + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + dtype=algebra.dtype, + device=DEVICE, + ) + + assert moved is executor + assert moved.coefficients.dtype == torch.float32 + + def test_multivector_compact_projected_product_keeps_dense_tensor_compatibility(): algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) A = Multivector(algebra, _grade_only_input(algebra, 2, (1,), seed=131)).compact((1,)) @@ -401,7 +426,7 @@ def test_static_grade_product_compiles_fullgraph_with_aot_eager(): @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") def test_planned_unary_compiles_fullgraph_with_aot_eager(): algebra = make_algebra(6, 0, 0, kernel="context", device=DEVICE, dtype=torch.float32) - executor = algebra.translator.unary_executor( + executor = algebra.planner.unary_executor( op="reverse", input_grades=(2,), dtype=torch.float32, From 20e1bbba08d3cb8a7aa839a2aeae9ceffa6f5d52 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 14:55:39 +0900 Subject: [PATCH 14/45] feat: add layer-owned grade layouts to linear --- core/runtime/multivector.py | 2 ++ layers/grade.py | 49 +++++++++++++++++++++++++++++++++++++ layers/primitives/linear.py | 24 ++++++++++++------ tests/test_layers.py | 17 +++++++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 layers/grade.py diff --git a/core/runtime/multivector.py b/core/runtime/multivector.py index 1da86fa..b1187d9 100644 --- a/core/runtime/multivector.py +++ b/core/runtime/multivector.py @@ -87,6 +87,8 @@ def tensor(self) -> torch.Tensor: """ if self._tensor is not None: return self._tensor + # Dense materialization is a compatibility fallback. New Multivector + # operations should preserve compact ``values`` and ``layout`` instead. return self.layout.dense(self.values) @tensor.setter diff --git a/layers/grade.py b/layers/grade.py new file mode 100644 index 0000000..97ac556 --- /dev/null +++ b/layers/grade.py @@ -0,0 +1,49 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Layer-owned grade declaration helpers.""" + +from __future__ import annotations + +from typing import Iterable, Optional + +import torch + +from core.foundation.layout import GradeLayout +from core.foundation.module import AlgebraLike +from core.foundation.validation import VALIDATE, check_multivector + + +def resolve_layer_layout(algebra: AlgebraLike, grades: Optional[Iterable[int]]) -> Optional[GradeLayout]: + """Return a compact layout for declared grades, or ``None`` for dense lanes.""" + if grades is None: + return None + return algebra.planner.layout(grades) + + +def lane_count(algebra: AlgebraLike, layout: Optional[GradeLayout]) -> int: + """Return the active basis-lane count for a layer-owned layout.""" + return algebra.dim if layout is None else layout.dim + + +def check_multivector_lanes( + values: torch.Tensor, + algebra: AlgebraLike, + layout: Optional[GradeLayout], + name: str, +) -> None: + """Validate dense or declared compact multivector lanes.""" + if layout is None: + check_multivector(values, algebra, name) + return + if not VALIDATE: + return + assert values.ndim >= 1, f"{name}: expected ndim >= 1, got shape {tuple(values.shape)}" + assert values.shape[-1] == layout.dim, ( + f"{name}: last dim should be {layout.dim} for grades {layout.grades}, " + f"got {values.shape[-1]} (shape {tuple(values.shape)})" + ) diff --git a/layers/primitives/linear.py b/layers/primitives/linear.py index a678506..e51d120 100644 --- a/layers/primitives/linear.py +++ b/layers/primitives/linear.py @@ -10,14 +10,15 @@ Supports traditional matrix-based mixing and parameter-efficient rotor-based backends. """ -from typing import Literal, Optional +from typing import Iterable, Literal, Optional import torch import torch.nn as nn from core.foundation.module import CliffordModule -from core.foundation.validation import check_channels, check_multivector -from core.runtime.algebra import CliffordAlgebra +from core.foundation.validation import check_channels + +from ..grade import check_multivector_lanes, lane_count, resolve_layer_layout class CliffordLinear(CliffordModule): @@ -42,13 +43,14 @@ class CliffordLinear(CliffordModule): def __init__( self, - algebra: CliffordAlgebra, + algebra, in_channels: int, out_channels: int, backend: Literal["traditional", "rotor"] = "traditional", num_rotor_pairs: int = 4, aggregation: Literal["mean", "sum", "learned"] = "mean", shuffle: Literal["none", "fixed", "random"] = "none", + grades: Optional[Iterable[int]] = None, ): """Initialize Clifford Linear. @@ -64,19 +66,26 @@ def __init__( - 'none': No shuffle (default) - 'fixed': Fixed random permutation - 'random': Random permutation each forward pass + grades: Optional layer-owned active grades. When set, the traditional + backend operates on compact lanes for those grades instead of + requiring a full dense multivector width. """ super().__init__(algebra) self.in_channels = in_channels self.out_channels = out_channels self.backend = backend + self.layout = resolve_layer_layout(algebra, grades) + self.basis_dim = lane_count(algebra, self.layout) if backend == "traditional": self.weight = nn.Parameter(torch.Tensor(out_channels, in_channels)) - self.bias = nn.Parameter(torch.Tensor(out_channels, algebra.dim)) + self.bias = nn.Parameter(torch.Tensor(out_channels, self.basis_dim)) self.reset_parameters() self.gadget = None elif backend == "rotor": + if self.layout is not None: + raise ValueError("CliffordLinear rotor backend does not yet support compact grade declarations") from .rotor_gadget import RotorGadget self.gadget = RotorGadget( @@ -109,7 +118,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Output [Batch, Out, Dim]. """ - check_multivector(x, self.algebra, "CliffordLinear input") + check_multivector_lanes(x, self.algebra, self.layout, "CliffordLinear input") check_channels(x, self.in_channels, "CliffordLinear input") if self.backend == "traditional": @@ -131,6 +140,7 @@ def extra_repr(self) -> str: str: Layer parameters description """ if self.backend == "traditional": - return f"in_channels={self.in_channels}, out_channels={self.out_channels}, backend=traditional" + grades = "" if self.layout is None else f", grades={self.layout.grades}" + return f"in_channels={self.in_channels}, out_channels={self.out_channels}, backend=traditional{grades}" else: return f"in_channels={self.in_channels}, out_channels={self.out_channels}, backend=rotor" diff --git a/tests/test_layers.py b/tests/test_layers.py index 3870628..493d769 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -8,6 +8,7 @@ import pytest import torch +from core.config import make_algebra from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy from layers import CliffordLinear, MultiRotorLayer, RotorLayer @@ -25,6 +26,22 @@ def test_linear_shape(self, algebra_3d): y = layer(x) assert y.shape == (4, 3, 8) + def test_linear_declared_grades_use_compact_lanes_in_high_dimensions(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + layer = CliffordLinear(algebra, 2, 3, grades=(1,)) + x = torch.randn(4, 2, algebra.n) + + y = layer(x) + + assert layer.layout.grades == (1,) + assert layer.basis_dim == algebra.n + assert layer.bias.shape == (3, algebra.n) + assert y.shape == (4, 3, algebra.n) + + def test_linear_declared_grades_reject_rotor_backend_until_compact_sandwich_exists(self, algebra_3d): + with pytest.raises(ValueError, match="compact grade declarations"): + CliffordLinear(algebra_3d, 2, 3, backend="rotor", grades=(1,)) + def test_rotor_shape(self, algebra_3d): # Batch=4, Channels=5 x = torch.randn(4, 5, 8) From e82e0ea5625695827d6ea727d494e93713e0d252 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 14:57:05 +0900 Subject: [PATCH 15/45] feat: support compact grade layouts in layer norm --- layers/primitives/normalization.py | 36 ++++++++++++++++++++++++++---- tests/test_layers.py | 16 ++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/layers/primitives/normalization.py b/layers/primitives/normalization.py index 149566b..807c65c 100644 --- a/layers/primitives/normalization.py +++ b/layers/primitives/normalization.py @@ -5,11 +5,14 @@ # you may not use this file except in compliance with the License. # +from typing import Iterable, Optional + import torch import torch.nn as nn from core.foundation.module import CliffordModule -from core.runtime.algebra import CliffordAlgebra + +from ..grade import check_multivector_lanes, lane_count, resolve_layer_layout class CliffordLayerNorm(CliffordModule): @@ -27,7 +30,14 @@ class CliffordLayerNorm(CliffordModule): starts identical to the old (scale-discarding) behaviour. """ - def __init__(self, algebra: CliffordAlgebra, channels: int, eps: float = 1e-6, recover: bool = True): + def __init__( + self, + algebra, + channels: int, + eps: float = 1e-6, + recover: bool = True, + grades: Optional[Iterable[int]] = None, + ): """Sets up normalization. Args: @@ -35,13 +45,17 @@ def __init__(self, algebra: CliffordAlgebra, channels: int, eps: float = 1e-6, r channels (int): Features. eps (float): Stability term. recover (bool): Whether to inject original scale into the scalar part. + grades: Optional layer-owned active grades for compact lane execution. """ super().__init__(algebra) self.eps = eps self.recover = recover + self.layout = resolve_layer_layout(algebra, grades) + self.basis_dim = lane_count(algebra, self.layout) self.weight = nn.Parameter(torch.ones(channels)) self.bias = nn.Parameter(torch.zeros(channels)) + self.register_buffer("_scalar_lane_mask", self._build_scalar_lane_mask()) # Learnable gate: how much of the original log-magnitude to push # into the scalar part. Zero-init -> backward compatible at start. if recover: @@ -58,6 +72,8 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Normalized input. """ + check_multivector_lanes(x, self.algebra, self.layout, "CliffordLayerNorm input") + # Per-channel magnitude norm = x.norm(dim=-1, keepdim=True) # [B, C, 1] @@ -67,8 +83,8 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # Affine transform on direction out = x_normalized * self.weight.view(1, -1, 1) - # Add bias and optional log-magnitude to grade-0 via mask - g0 = self.algebra.grade_masks_float[0] # [D], 1.0 at index 0 + # Add bias and optional log-magnitude to the declared scalar lane. + g0 = self._scalar_lane_mask if g0.dtype != x.dtype: g0 = g0.to(dtype=x.dtype) out = out + self.bias.view(1, -1, 1) * g0 @@ -80,3 +96,15 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: out = out + self.norm_scale.view(1, -1, 1) * log_norm * g0 return out + + def _build_scalar_lane_mask(self) -> torch.Tensor: + """Return a lane mask with 1 at scalar basis position when present.""" + mask = torch.zeros(self.basis_dim, dtype=self.algebra.dtype, device=self.algebra.device) + if self.layout is None: + mask[0] = 1.0 + return mask + try: + mask[self.layout.basis_indices.index(0)] = 1.0 + except ValueError: + pass + return mask diff --git a/tests/test_layers.py b/tests/test_layers.py index 493d769..b9b7d11 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -11,7 +11,7 @@ from core.config import make_algebra from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy -from layers import CliffordLinear, MultiRotorLayer, RotorLayer +from layers import CliffordLayerNorm, CliffordLinear, MultiRotorLayer, RotorLayer from layers.primitives.reflection import ReflectionLayer pytestmark = pytest.mark.unit @@ -42,6 +42,20 @@ def test_linear_declared_grades_reject_rotor_backend_until_compact_sandwich_exis with pytest.raises(ValueError, match="compact grade declarations"): CliffordLinear(algebra_3d, 2, 3, backend="rotor", grades=(1,)) + def test_layer_norm_declared_grades_use_compact_lanes_in_high_dimensions(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + layer = CliffordLayerNorm(algebra, channels=2, grades=(0, 1)) + layout = algebra.layout((0, 1)) + x = torch.randn(3, 2, layout.dim) + + y = layer(x) + + scalar_pos = layout.basis_indices.index(0) + assert layer.layout == layout + assert y.shape == x.shape + assert layer._scalar_lane_mask.shape[-1] == layout.dim + assert layer._scalar_lane_mask[scalar_pos].item() == 1.0 + def test_rotor_shape(self, algebra_3d): # Batch=4, Channels=5 x = torch.randn(4, 5, 8) From c051115cf4766e3979c53a4e7faee90d9fda073f Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 14:58:46 +0900 Subject: [PATCH 16/45] feat: support compact grade layouts in blade selector --- layers/primitives/projection.py | 15 +++++++++++---- tests/test_layers.py | 14 +++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/layers/primitives/projection.py b/layers/primitives/projection.py index 4df6aa3..8e6b64d 100644 --- a/layers/primitives/projection.py +++ b/layers/primitives/projection.py @@ -5,13 +5,16 @@ # you may not use this file except in compliance with the License. # +from typing import Iterable, Optional + import torch import torch.nn as nn from core.foundation.module import CliffordModule -from core.runtime.algebra import CliffordAlgebra from utils.compat import safe_linalg_solve +from ..grade import check_multivector_lanes, lane_count, resolve_layer_layout + class BladeSelector(CliffordModule): """Blade Selector. Filters insignificant components. @@ -22,16 +25,19 @@ class BladeSelector(CliffordModule): weights (nn.Parameter): Soft gates [Channels, Dim]. """ - def __init__(self, algebra: CliffordAlgebra, channels: int): + def __init__(self, algebra, channels: int, grades: Optional[Iterable[int]] = None): """Sets up the selector. Args: algebra (CliffordAlgebra): The algebra instance. channels (int): Input features. + grades: Optional layer-owned active grades for compact lane execution. """ super().__init__(algebra) + self.layout = resolve_layer_layout(algebra, grades) + self.basis_dim = lane_count(algebra, self.layout) - self.weights = nn.Parameter(torch.Tensor(channels, algebra.dim)) + self.weights = nn.Parameter(torch.Tensor(channels, self.basis_dim)) self.reset_parameters() @@ -48,6 +54,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Filtered input. """ + check_multivector_lanes(x, self.algebra, self.layout, "BladeSelector input") # Sigmoid gate w = torch.sigmoid(self.weights).unsqueeze(0) return x * w @@ -67,7 +74,7 @@ class GeometricNeutralizer(CliffordModule): momentum (float): EMA momentum. """ - def __init__(self, algebra: CliffordAlgebra, channels: int, momentum: float = 0.1): + def __init__(self, algebra, channels: int, momentum: float = 0.1): """Initialize the neutralizer. Args: diff --git a/tests/test_layers.py b/tests/test_layers.py index b9b7d11..0eb61fe 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -11,7 +11,7 @@ from core.config import make_algebra from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy -from layers import CliffordLayerNorm, CliffordLinear, MultiRotorLayer, RotorLayer +from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, MultiRotorLayer, RotorLayer from layers.primitives.reflection import ReflectionLayer pytestmark = pytest.mark.unit @@ -56,6 +56,18 @@ def test_layer_norm_declared_grades_use_compact_lanes_in_high_dimensions(self): assert layer._scalar_lane_mask.shape[-1] == layout.dim assert layer._scalar_lane_mask[scalar_pos].item() == 1.0 + def test_blade_selector_declared_grades_use_compact_lanes_in_high_dimensions(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + layer = BladeSelector(algebra, channels=2, grades=(1, 2)) + layout = algebra.layout((1, 2)) + x = torch.randn(3, 2, layout.dim) + + y = layer(x) + + assert layer.layout == layout + assert layer.weights.shape == (2, layout.dim) + assert y.shape == x.shape + def test_rotor_shape(self, algebra_3d): # Batch=4, Channels=5 x = torch.randn(4, 5, 8) From 2428413a7ce51f06a0e9deb4874f568166a7e5d9 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 15:00:24 +0900 Subject: [PATCH 17/45] feat: route attention through layer-owned feature grades --- layers/blocks/attention.py | 42 +++++++++++++++++++++++++++++--------- tests/test_attention.py | 28 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/layers/blocks/attention.py b/layers/blocks/attention.py index 55c1274..36f94e3 100644 --- a/layers/blocks/attention.py +++ b/layers/blocks/attention.py @@ -13,8 +13,8 @@ from core.foundation.basis import normalize_grades, reverse_sign from core.foundation.module import CliffordModule -from core.runtime.algebra import CliffordAlgebra +from ..grade import check_multivector_lanes, lane_count, resolve_layer_layout from ..primitives.linear import CliffordLinear # Memory-bounded block size for chunked attention computation @@ -47,12 +47,13 @@ class GeometricProductAttention(CliffordModule): def __init__( self, - algebra: CliffordAlgebra, + algebra, channels: int, num_heads: int, causal: bool = True, bivector_weight: float = 0.5, dropout: float = 0.0, + feature_grades=None, score_grades=None, score_blade_chunk_size: int = _G2_BLADE_CHUNK_SIZE, score_precompute_limit: int = _SCORE_PRECOMPUTE_LIMIT, @@ -66,8 +67,12 @@ def __init__( causal: Apply causal mask for autoregressive generation. bivector_weight: lambda_ weight on bivector score component. dropout: Dropout rate on attention weights. + feature_grades: Optional active grades carried by this attention layer. + When set, projections and attention values use compact lanes. score_grades: Optional declared grades for compact planned scoring. - ``None`` preserves exact dense scoring over all basis lanes. + ``None`` preserves exact dense scoring over all basis lanes unless + ``feature_grades`` is set, in which case those feature grades are + also used for scoring. score_blade_chunk_size: Grade-2 output blades processed per dense chunk when exact dense scoring is used. score_precompute_limit: Maximum temporary ``K_g2`` elements allowed @@ -81,15 +86,19 @@ def __init__( self.head_channels = channels // num_heads self.causal = causal self.bivector_weight = bivector_weight + self.feature_layout = resolve_layer_layout(algebra, feature_grades) + self.feature_dim = lane_count(algebra, self.feature_layout) + if score_grades is None and feature_grades is not None: + score_grades = feature_grades self.score_grades = None if score_grades is None else normalize_grades(score_grades, algebra.n) self.score_blade_chunk_size = max(1, int(score_blade_chunk_size)) self.score_precompute_limit = max(0, int(score_precompute_limit)) # Q, K, V projections operate on [B*L, channels, dim] - self.q_proj = CliffordLinear(algebra, channels, channels) - self.k_proj = CliffordLinear(algebra, channels, channels) - self.v_proj = CliffordLinear(algebra, channels, channels) - self.out_proj = CliffordLinear(algebra, channels, channels) + self.q_proj = CliffordLinear(algebra, channels, channels, grades=feature_grades) + self.k_proj = CliffordLinear(algebra, channels, channels, grades=feature_grades) + self.v_proj = CliffordLinear(algebra, channels, channels, grades=feature_grades) + self.out_proj = CliffordLinear(algebra, channels, channels, grades=feature_grades) self.attn_dropout = nn.Dropout(dropout) if dropout > 0.0 else None @@ -109,10 +118,11 @@ def _precompute_score_tables(self): self._score_layout = None self._score_scalar_product = None self._score_bivector_product = None + self._score_scale_dim = self.feature_dim if self.feature_layout is not None else D if self.score_grades is not None: self.n_g2 = alg.n * (alg.n - 1) // 2 self._score_layout = alg.planner.layout(self.score_grades) - layout_indices = self._score_layout.indices_tensor(device=alg.device) + layout_indices = self._score_input_positions(device=alg.device) rev_signs = torch.tensor( [reverse_sign(index) for index in self._score_layout.basis_indices], dtype=torch.float32, @@ -197,7 +207,7 @@ def _compute_score_dense(self, q_head: torch.Tensor, k_head: torch.Tensor) -> to score_g2 = torch.zeros_like(score_g0) # Combined score - scale = math.sqrt(self.head_channels * self.algebra.dim) + scale = math.sqrt(self.head_channels * self._score_scale_dim) return (score_g0 + self.bivector_weight * score_g2) / scale def _dense_score_g2_precomputed(self, q_2d, k_head, B, H, Hc, Lq, Lk, D, n_g2): @@ -256,9 +266,20 @@ def _compute_score_compact(self, q_head: torch.Tensor, k_head: torch.Tensor) -> else: score_g2 = torch.zeros_like(score_g0) - scale = math.sqrt(self.head_channels * self.algebra.dim) + scale = math.sqrt(self.head_channels * self._score_scale_dim) return (score_g0 + self.bivector_weight * score_g2) / scale + def _score_input_positions(self, *, device) -> torch.Tensor: + """Return score-lane positions in dense or declared feature storage.""" + if self.feature_layout is None: + return self._score_layout.indices_tensor(device=device) + position_by_basis = {index: position for position, index in enumerate(self.feature_layout.basis_indices)} + missing = tuple(index for index in self._score_layout.basis_indices if index not in position_by_basis) + if missing: + raise ValueError("score_grades must be contained in feature_grades for compact attention") + positions = [position_by_basis[index] for index in self._score_layout.basis_indices] + return torch.tensor(positions, dtype=torch.long, device=device) + def forward(self, x: torch.Tensor, key_padding_mask: torch.Tensor = None) -> torch.Tensor: """Computes geometric product attention. @@ -269,6 +290,7 @@ def forward(self, x: torch.Tensor, key_padding_mask: torch.Tensor = None) -> tor Returns: Output multivectors [B, L, C, D]. """ + check_multivector_lanes(x, self.algebra, self.feature_layout, "GeometricProductAttention input") B, L, C, D = x.shape # Project Q, K, V (CliffordLinear expects [B, C, D]) diff --git a/tests/test_attention.py b/tests/test_attention.py index ddb5364..b8da6fd 100644 --- a/tests/test_attention.py +++ b/tests/test_attention.py @@ -3,6 +3,7 @@ import pytest import torch +from core.config import make_algebra from core.runtime.algebra import CliffordAlgebra from layers.blocks.attention import GeometricProductAttention @@ -84,6 +85,33 @@ def test_attention_forward_shape_after_score_refactor(): assert y.shape == x.shape +def test_attention_declared_feature_grades_run_compact_high_dim_context(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + attn = GeometricProductAttention(algebra, channels=4, num_heads=2, causal=False, feature_grades=(1,)) + x = torch.randn(2, 5, 4, algebra.n) + + y = attn(x) + + assert attn.feature_layout.grades == (1,) + assert attn._score_layout.grades == (1,) + assert attn.q_proj.basis_dim == algebra.n + assert y.shape == x.shape + + +def test_attention_rejects_score_grades_outside_compact_feature_grades(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + + with pytest.raises(ValueError, match="score_grades must be contained"): + GeometricProductAttention( + algebra, + channels=4, + num_heads=2, + causal=False, + feature_grades=(1,), + score_grades=(2,), + ) + + @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") def test_attention_compact_score_compiles_fullgraph(): algebra = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float32) From 62a0fc949983f95ab94cc1fc0f1c9f2309a1d856 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 15:02:27 +0900 Subject: [PATCH 18/45] feat: thread compact feature grades through transformer blocks --- layers/blocks/multi_rotor_ffn.py | 27 +++++++++++++++------- layers/blocks/transformer.py | 39 ++++++++++++++++++++++++++------ tests/test_layers.py | 33 +++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/layers/blocks/multi_rotor_ffn.py b/layers/blocks/multi_rotor_ffn.py index 1feb76f..819c38a 100644 --- a/layers/blocks/multi_rotor_ffn.py +++ b/layers/blocks/multi_rotor_ffn.py @@ -5,10 +5,12 @@ # you may not use this file except in compliance with the License. # +from typing import Optional + import torch +import torch.nn as nn from core.foundation.module import CliffordModule -from core.runtime.algebra import CliffordAlgebra from functional.activation import GeometricGELU from ..primitives.linear import CliffordLinear @@ -47,23 +49,32 @@ class MultiRotorFFN(CliffordModule): def __init__( self, - algebra: CliffordAlgebra, + algebra, channels: int, ffn_mult: int = 4, num_rotors: int = 8, use_rotor_backend: bool = False, + feature_grades=None, + use_rotor_toolbox: Optional[bool] = None, ): super().__init__(algebra) self.channels = channels ffn_channels = channels * ffn_mult backend = "rotor" if use_rotor_backend else "traditional" - - self.expand = CliffordLinear(algebra, channels, ffn_channels, backend=backend) - self.norm = CliffordLayerNorm(algebra, ffn_channels) - self.toolbox = MultiRotorLayer(algebra, ffn_channels, num_rotors) + if use_rotor_toolbox is None: + use_rotor_toolbox = feature_grades is None + if feature_grades is not None and use_rotor_toolbox: + raise ValueError("MultiRotorFFN rotor toolbox requires dense feature lanes") + self.use_rotor_toolbox = bool(use_rotor_toolbox) + + self.expand = CliffordLinear(algebra, channels, ffn_channels, backend=backend, grades=feature_grades) + self.norm = CliffordLayerNorm(algebra, ffn_channels, grades=feature_grades) + self.toolbox = ( + MultiRotorLayer(algebra, ffn_channels, num_rotors) if self.use_rotor_toolbox else nn.Identity() + ) self.act = GeometricGELU(algebra, channels=ffn_channels) - self.contract = CliffordLinear(algebra, ffn_channels, channels, backend=backend) - self.gate = BladeSelector(algebra, channels) + self.contract = CliffordLinear(algebra, ffn_channels, channels, backend=backend, grades=feature_grades) + self.gate = BladeSelector(algebra, channels, grades=feature_grades) def forward(self, x) -> torch.Tensor: """Applies the geometric toolbox FFN. diff --git a/layers/blocks/transformer.py b/layers/blocks/transformer.py index 0c5459f..9f753d9 100644 --- a/layers/blocks/transformer.py +++ b/layers/blocks/transformer.py @@ -5,11 +5,12 @@ # you may not use this file except in compliance with the License. # +from typing import Optional + import torch import torch.nn as nn from core.foundation.module import CliffordModule -from core.runtime.algebra import CliffordAlgebra from ..adapters.mother import EntropyGatedAttention from ..primitives.normalization import CliffordLayerNorm @@ -31,7 +32,7 @@ class GeometricTransformerBlock(CliffordModule): def __init__( self, - algebra: CliffordAlgebra, + algebra, channels: int, num_heads: int = 4, num_rotors: int = 8, @@ -39,6 +40,9 @@ def __init__( use_entropy_gating: bool = False, eta: float = 1.5, H_base: float = 0.5, + feature_grades=None, + attention_score_grades=None, + use_ffn_rotor_toolbox: Optional[bool] = None, ): """Initializes the Geometric Transformer Block. @@ -51,22 +55,43 @@ def __init__( use_entropy_gating: If True, uses EntropyGatedAttention. eta: Gating multiplier for entropy attention. H_base: Base entropy threshold. + feature_grades: Optional layer-owned active grades for compact execution. + attention_score_grades: Optional score grades for attention; defaults to + ``feature_grades`` when compact execution is used. + use_ffn_rotor_toolbox: Whether to use the dense rotor toolbox in the + FFN. Defaults to disabled when ``feature_grades`` is declared. """ super().__init__(algebra) + if use_entropy_gating and feature_grades is not None: + raise ValueError("Entropy-gated attention does not yet support compact feature grades") self.use_entropy_gating = use_entropy_gating - self.norm1 = CliffordLayerNorm(algebra, channels) + self.norm1 = CliffordLayerNorm(algebra, channels, grades=feature_grades) if use_entropy_gating: self.attn = EntropyGatedAttention(algebra, channels, num_heads, eta=eta, H_base=H_base) else: - self.attn = GeometricProductAttention(algebra, channels, num_heads, causal=False, dropout=dropout) - - self.norm2 = CliffordLayerNorm(algebra, channels) + self.attn = GeometricProductAttention( + algebra, + channels, + num_heads, + causal=False, + dropout=dropout, + feature_grades=feature_grades, + score_grades=attention_score_grades, + ) + + self.norm2 = CliffordLayerNorm(algebra, channels, grades=feature_grades) # Check MultiRotorFFN class name in multi_rotor_ffn.py from .multi_rotor_ffn import MultiRotorFFN - self.ffn = MultiRotorFFN(algebra, channels, num_rotors=num_rotors) + self.ffn = MultiRotorFFN( + algebra, + channels, + num_rotors=num_rotors, + feature_grades=feature_grades, + use_rotor_toolbox=use_ffn_rotor_toolbox, + ) def forward( self, x: torch.Tensor, key_padding_mask: torch.Tensor = None, return_state: bool = False diff --git a/tests/test_layers.py b/tests/test_layers.py index 0eb61fe..b9c452c 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -12,6 +12,8 @@ from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, MultiRotorLayer, RotorLayer +from layers.blocks.multi_rotor_ffn import MultiRotorFFN +from layers.blocks.transformer import GeometricTransformerBlock from layers.primitives.reflection import ReflectionLayer pytestmark = pytest.mark.unit @@ -68,6 +70,37 @@ def test_blade_selector_declared_grades_use_compact_lanes_in_high_dimensions(sel assert layer.weights.shape == (2, layout.dim) assert y.shape == x.shape + def test_compact_multirotor_ffn_uses_linear_toolbox_in_high_dimensions(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + layer = MultiRotorFFN(algebra, channels=4, ffn_mult=2, feature_grades=(1,), use_rotor_toolbox=False) + x = torch.randn(3, 4, algebra.n) + + y = layer(x) + + assert not layer.use_rotor_toolbox + assert y.shape == x.shape + + def test_compact_multirotor_ffn_rejects_dense_rotor_toolbox(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + + with pytest.raises(ValueError, match="dense feature lanes"): + MultiRotorFFN(algebra, channels=4, feature_grades=(1,), use_rotor_toolbox=True) + + def test_compact_transformer_block_runs_high_dim_pipeline(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + block = GeometricTransformerBlock( + algebra, + channels=4, + num_heads=2, + feature_grades=(1,), + use_ffn_rotor_toolbox=False, + ) + x = torch.randn(2, 5, 4, algebra.n) + + y = block(x) + + assert y.shape == x.shape + def test_rotor_shape(self, algebra_3d): # Batch=4, Channels=5 x = torch.randn(4, 5, 8) From 0c6d6651091d0879f7aec07614530308ce83ec4f Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 15:04:12 +0900 Subject: [PATCH 19/45] feat: start compact pipelines from embeddings --- layers/adapters/embedding.py | 33 +++++++++++++++++++++++---------- tests/test_layers.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/layers/adapters/embedding.py b/layers/adapters/embedding.py index ac4e7f1..35c23eb 100644 --- a/layers/adapters/embedding.py +++ b/layers/adapters/embedding.py @@ -5,12 +5,16 @@ # you may not use this file except in compliance with the License. # +from typing import Iterable, Optional + import torch import torch.nn as nn from core.foundation.module import CliffordModule from core.runtime.algebra import CliffordAlgebra +from ..grade import lane_count, resolve_layer_layout + class MultivectorEmbedding(CliffordModule): """Token embedding as multivectors. @@ -25,33 +29,42 @@ class MultivectorEmbedding(CliffordModule): embedding (nn.Embedding): Underlying embedding table. """ - def __init__(self, algebra: CliffordAlgebra, vocab_size: int, channels: int): + def __init__( + self, + algebra, + vocab_size: int, + channels: int, + grades: Optional[Iterable[int]] = None, + ): """Sets up the multivector embedding. Args: algebra: Clifford algebra instance. vocab_size: Vocabulary size. channels: Number of multivector channels per token. + grades: Optional layer-owned active output grades. When set, the + embedding table stores compact lanes only. """ super().__init__(algebra) self.vocab_size = vocab_size self.channels = channels + self.layout = resolve_layer_layout(algebra, grades) + self.basis_dim = lane_count(algebra, self.layout) - # Single flat embedding: vocab_size -> channels * dim - self.embedding = nn.Embedding(vocab_size, channels * algebra.dim) + # Single flat embedding: vocab_size -> channels * active basis lanes + self.embedding = nn.Embedding(vocab_size, channels * self.basis_dim) self._init_grade1() def _init_grade1(self): """Initializes only grade-1 components; zeros out all others.""" with torch.no_grad(): - dim = self.algebra.dim channels = self.channels + dim = self.basis_dim - # Build grade-1 mask (indices with exactly 1 bit set) - grade1_flat = [] - for i in range(dim): - if bin(i).count("1") == 1: - grade1_flat.append(i) + if self.layout is None: + grade1_flat = [i for i in range(dim) if bin(i).count("1") == 1] + else: + grade1_flat = [pos for pos, index in enumerate(self.layout.basis_indices) if bin(index).count("1") == 1] # Zero everything self.embedding.weight.zero_() @@ -73,7 +86,7 @@ def forward(self, token_ids: torch.Tensor) -> torch.Tensor: """ B, L = token_ids.shape flat = self.embedding(token_ids) # [B, L, channels * dim] - return flat.reshape(B, L, self.channels, self.algebra.dim) + return flat.reshape(B, L, self.channels, self.basis_dim) class RotaryBivectorPE(CliffordModule): diff --git a/tests/test_layers.py b/tests/test_layers.py index b9c452c..8013ba9 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -11,7 +11,7 @@ from core.config import make_algebra from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy -from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, MultiRotorLayer, RotorLayer +from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, MultiRotorLayer, MultivectorEmbedding, RotorLayer from layers.blocks.multi_rotor_ffn import MultiRotorFFN from layers.blocks.transformer import GeometricTransformerBlock from layers.primitives.reflection import ReflectionLayer @@ -101,6 +101,33 @@ def test_compact_transformer_block_runs_high_dim_pipeline(self): assert y.shape == x.shape + def test_multivector_embedding_declared_grades_start_compact_high_dim_pipeline(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + embedding = MultivectorEmbedding(algebra, vocab_size=11, channels=4, grades=(1,)) + token_ids = torch.randint(0, 11, (2, 5)) + + x = embedding(token_ids) + + assert embedding.layout.grades == (1,) + assert embedding.embedding.weight.shape == (11, 4 * algebra.n) + assert x.shape == (2, 5, 4, algebra.n) + + def test_compact_embedding_transformer_pipeline_runs_high_dim_context(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + embedding = MultivectorEmbedding(algebra, vocab_size=11, channels=4, grades=(1,)) + block = GeometricTransformerBlock( + algebra, + channels=4, + num_heads=2, + feature_grades=(1,), + use_ffn_rotor_toolbox=False, + ) + token_ids = torch.randint(0, 11, (2, 5)) + + output = block(embedding(token_ids)) + + assert output.shape == (2, 5, 4, algebra.n) + def test_rotor_shape(self, algebra_3d): # Batch=4, Channels=5 x = torch.randn(4, 5, 8) From 6c7babd0d922740cd6996ff0e0e0fe0801de415c Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 15:05:36 +0900 Subject: [PATCH 20/45] refactor: derive versor grade indices from planner layouts --- layers/adapters/embedding.py | 14 ++++++++------ layers/primitives/multi_rotor.py | 7 +++---- layers/primitives/reflection.py | 6 ++---- layers/primitives/rotor.py | 7 +++---- layers/primitives/rotor_gadget.py | 8 +++----- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/layers/adapters/embedding.py b/layers/adapters/embedding.py index 35c23eb..4170ba5 100644 --- a/layers/adapters/embedding.py +++ b/layers/adapters/embedding.py @@ -11,7 +11,6 @@ import torch.nn as nn from core.foundation.module import CliffordModule -from core.runtime.algebra import CliffordAlgebra from ..grade import lane_count, resolve_layer_layout @@ -108,7 +107,7 @@ class RotaryBivectorPE(CliffordModule): def __init__( self, - algebra: CliffordAlgebra, + algebra, channels: int, max_seq_len: int, learnable: bool = True, @@ -125,10 +124,13 @@ def __init__( self.max_seq_len = max_seq_len self.learnable = learnable - # Identify grade-2 basis elements - indices = [i for i in range(algebra.dim) if bin(i).count("1") == 2] - self.register_buffer("bivector_indices", torch.tensor(indices, dtype=torch.long)) - self.num_bivectors = len(indices) + # Identify grade-2 basis elements through the planner layout. + if algebra.n >= 2: + bivector_indices = algebra.planner.layout((2,)).indices_tensor(device=algebra.device) + else: + bivector_indices = torch.zeros(0, dtype=torch.long, device=algebra.device) + self.register_buffer("bivector_indices", bivector_indices) + self.num_bivectors = int(bivector_indices.numel()) # Sinusoidal initialization init = self._sinusoidal_init(max_seq_len, self.num_bivectors) diff --git a/layers/primitives/multi_rotor.py b/layers/primitives/multi_rotor.py index 16addb9..ba0ad0f 100644 --- a/layers/primitives/multi_rotor.py +++ b/layers/primitives/multi_rotor.py @@ -15,7 +15,6 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector -from core.runtime.algebra import CliffordAlgebra class MultiRotorLayer(CliffordModule): @@ -37,7 +36,7 @@ class MultiRotorLayer(CliffordModule): def __init__( self, - algebra: CliffordAlgebra, + algebra, channels: int, num_rotors: int = 8, grade: int = 2, @@ -57,8 +56,8 @@ def __init__( self.num_rotors = num_rotors self.grade = grade - grade_mask = algebra.grade_masks[grade] - self.register_buffer("grade_indices", grade_mask.nonzero(as_tuple=False).squeeze(-1)) + grade_layout = algebra.planner.layout((grade,)) + self.register_buffer("grade_indices", grade_layout.indices_tensor(device=algebra.device)) self.num_grade_elements = len(self.grade_indices) self.rotor_grade_weights = nn.Parameter(torch.Tensor(num_rotors, self.num_grade_elements)) diff --git a/layers/primitives/reflection.py b/layers/primitives/reflection.py index ac3d388..6cba910 100644 --- a/layers/primitives/reflection.py +++ b/layers/primitives/reflection.py @@ -10,7 +10,6 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector -from core.runtime.algebra import CliffordAlgebra class ReflectionLayer(CliffordModule): @@ -30,7 +29,7 @@ class ReflectionLayer(CliffordModule): vector_weights (nn.Parameter): Learnable grade-1 coefficients [C, n]. """ - def __init__(self, algebra: CliffordAlgebra, channels: int): + def __init__(self, algebra, channels: int): """Initialize the reflection layer. Args: @@ -41,8 +40,7 @@ def __init__(self, algebra: CliffordAlgebra, channels: int): self.channels = channels # Grade-1 indices: 2^0, 2^1, ..., 2^(n-1) - g1_mask = algebra.grade_masks[1] - self.register_buffer("vector_indices", g1_mask.nonzero(as_tuple=False).squeeze(-1)) + self.register_buffer("vector_indices", algebra.planner.layout((1,)).indices_tensor(device=algebra.device)) self.num_vectors = algebra.n self.vector_weights = nn.Parameter(torch.Tensor(channels, self.num_vectors)) diff --git a/layers/primitives/rotor.py b/layers/primitives/rotor.py index 32d126d..47a7d58 100644 --- a/layers/primitives/rotor.py +++ b/layers/primitives/rotor.py @@ -10,7 +10,6 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector -from core.runtime.algebra import CliffordAlgebra class RotorLayer(CliffordModule): @@ -33,7 +32,7 @@ class RotorLayer(CliffordModule): def __init__( self, - algebra: CliffordAlgebra, + algebra, channels: int, grade: int = 2, ): @@ -51,8 +50,8 @@ def __init__( self.channels = channels self.grade = grade - grade_mask = algebra.grade_masks[grade] - self.register_buffer("grade_indices", grade_mask.nonzero(as_tuple=False).squeeze(-1)) + grade_layout = algebra.planner.layout((grade,)) + self.register_buffer("grade_indices", grade_layout.indices_tensor(device=algebra.device)) self.num_grade_elements = len(self.grade_indices) self.grade_weights = nn.Parameter(torch.Tensor(channels, self.num_grade_elements)) diff --git a/layers/primitives/rotor_gadget.py b/layers/primitives/rotor_gadget.py index 708aeea..16534a7 100644 --- a/layers/primitives/rotor_gadget.py +++ b/layers/primitives/rotor_gadget.py @@ -16,7 +16,6 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector -from core.runtime.algebra import CliffordAlgebra class RotorGadget(CliffordModule): @@ -45,7 +44,7 @@ class RotorGadget(CliffordModule): def __init__( self, - algebra: CliffordAlgebra, + algebra, in_channels: int, out_channels: int, num_rotor_pairs: int = 4, @@ -75,10 +74,9 @@ def __init__( self.aggregation = aggregation self.shuffle = shuffle - # Use algebra's precomputed grade masks for bivector indices + # Use the algebra planner layout for layer-owned bivector indices. if algebra.num_grades > 2: - bv_mask = algebra.grade_masks[2] - self.register_buffer("bivector_indices", bv_mask.nonzero(as_tuple=False).squeeze(-1)) + self.register_buffer("bivector_indices", algebra.planner.layout((2,)).indices_tensor(device=algebra.device)) else: self.register_buffer("bivector_indices", torch.tensor([], dtype=torch.long, device=algebra.device)) self.num_bivectors = len(self.bivector_indices) From 2b2ab06d9546818e357f080ce61b01a3effc956b Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 15:08:02 +0900 Subject: [PATCH 21/45] feat: add compact grade layouts to mother adapters --- layers/adapters/mother.py | 94 ++++++++++++++++++++++++++++++--------- tests/test_layers.py | 53 +++++++++++++++++++++- 2 files changed, 126 insertions(+), 21 deletions(-) diff --git a/layers/adapters/mother.py b/layers/adapters/mother.py index 883fb5a..8162f5e 100644 --- a/layers/adapters/mother.py +++ b/layers/adapters/mother.py @@ -9,9 +9,9 @@ import torch.nn as nn from core.foundation.module import CliffordModule -from core.runtime.algebra import CliffordAlgebra from ..blocks.attention import GeometricProductAttention +from ..grade import lane_count, resolve_layer_layout from ..primitives.normalization import CliffordLayerNorm @@ -22,7 +22,15 @@ class MotherEmbedding(CliffordModule): reference frame, effectively aligning disparate geometric manifolds. """ - def __init__(self, algebra: CliffordAlgebra, input_dim: int, channels: int, U: float = 0.0, V: torch.Tensor = None): + def __init__( + self, + algebra, + input_dim: int, + channels: int, + U: float = 0.0, + V: torch.Tensor = None, + grades=None, + ): """Initializes the Mother Embedding. Args: @@ -31,9 +39,12 @@ def __init__(self, algebra: CliffordAlgebra, input_dim: int, channels: int, U: f channels: Number of multivector channels. U: Geometric uncertainty index for manifold suppression. V: Fixed rotor proxy for Procrustes alignment (input_dim x input_dim). + grades: Optional layer-owned active output grades for compact lanes. """ super().__init__(algebra) self.channels = channels + self.layout = resolve_layer_layout(algebra, grades) + self.basis_dim = lane_count(algebra, self.layout) # Procrustes Alignment Matrix (Fixed Rotor Proxy) if V is None: @@ -41,8 +52,8 @@ def __init__(self, algebra: CliffordAlgebra, input_dim: int, channels: int, U: f self.register_buffer("R_fixed", V) # Up-cast to Mother Algebra multivector channels - self.linear = nn.Linear(input_dim, channels * algebra.dim) - self.norm = CliffordLayerNorm(algebra, channels) + self.linear = nn.Linear(input_dim, channels * self.basis_dim) + self.norm = CliffordLayerNorm(algebra, channels, grades=grades) # Pre-condition LayerNorm scale with Uncertainty Index with torch.no_grad(): @@ -65,7 +76,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: x = x @ self.R_fixed.T # 2. Mother Projection - c = self.linear(x).view(-1, self.channels, self.algebra.dim) + c = self.linear(x).view(-1, self.channels, self.basis_dim) return self.norm(c) @@ -76,7 +87,16 @@ class EntropyGatedAttention(CliffordModule): or suppressed, allowing only coherent, synchronized states to propagate. """ - def __init__(self, algebra: CliffordAlgebra, channels: int, num_heads: int, eta: float = 1.0, H_base: float = 0.5): + def __init__( + self, + algebra, + channels: int, + num_heads: int, + eta: float = 1.0, + H_base: float = 0.5, + feature_grades=None, + score_grades=None, + ): """Initializes Entropy-Gated Attention. Args: @@ -85,17 +105,35 @@ def __init__(self, algebra: CliffordAlgebra, channels: int, num_heads: int, eta: num_heads: Number of attention heads. eta: Gating multiplier. H_base: Base entropy threshold. + feature_grades: Optional layer-owned active grades for compact lanes. + score_grades: Optional attention scoring grades. """ super().__init__(algebra) self.channels = channels self.eta = eta self.H_base = H_base - self.base_attention = GeometricProductAttention(algebra, channels, num_heads, causal=False) + self.feature_layout = resolve_layer_layout(algebra, feature_grades) + self.feature_dim = lane_count(algebra, self.feature_layout) + self.base_attention = GeometricProductAttention( + algebra, + channels, + num_heads, + causal=False, + feature_grades=feature_grades, + score_grades=score_grades, + ) # Cache bivector indices and float mask for compile-friendly gating - mask = self.algebra.grade_masks[2] - self.register_buffer("g2_idx", mask.nonzero(as_tuple=True)[0]) - self.register_buffer("_g2_float_mask", mask.float()) + g2_idx = ( + _grade_positions(algebra, (2,), self.feature_layout) + if algebra.n >= 2 + else torch.zeros(0, dtype=torch.long, device=algebra.device) + ) + g2_mask = torch.zeros(self.feature_dim, dtype=torch.float32, device=algebra.device) + if g2_idx.numel() > 0: + g2_mask.index_fill_(0, g2_idx, 1.0) + self.register_buffer("g2_idx", g2_idx) + self.register_buffer("_g2_float_mask", g2_mask) def forward( self, x: torch.Tensor, key_padding_mask: torch.Tensor = None, return_gating: bool = False @@ -150,25 +188,26 @@ class PhaseShiftHead(CliffordModule): high-grade component (G4) via a learned phase angle theta. """ - def __init__(self, algebra: CliffordAlgebra, channels: int): + def __init__(self, algebra, channels: int, feature_grades=None): """Initializes the Phase-Shift Head. Args: algebra: Clifford algebra instance. channels: Number of channels to mix. + feature_grades: Optional layer-owned active grades for compact lanes. """ super().__init__(algebra) self.channels = channels + self.feature_layout = resolve_layer_layout(algebra, feature_grades) + self.register_buffer("g0_idx", _grade_positions(algebra, (0,), self.feature_layout)) # Learned phase angle theta self.theta = nn.Parameter(torch.randn(1, channels, 1) * 0.1) # Identify grade-4 pseudoscalar in Cl(3,1) - mask_g4 = self.algebra.grade_masks[4] - if mask_g4.sum() > 0: - self.register_buffer("g4_idx", mask_g4.nonzero(as_tuple=True)[0]) + if algebra.n >= 4: + self.register_buffer("g4_idx", _grade_positions(algebra, (4,), self.feature_layout)) else: - # Fallback if algebra doesn't have grade 4 - self.g4_idx = None + self.register_buffer("g4_idx", torch.zeros(0, dtype=torch.long, device=algebra.device)) def forward(self, x: torch.Tensor) -> torch.Tensor: """Mixes grades using pseudoscalar rotation. @@ -183,12 +222,17 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: x_pool = x.mean(dim=1) # [B, C, D] # Grade-0 (Scalar) - G0 = x_pool[..., 0:1] + if len(self.g0_idx) > 0: + G0 = x_pool[..., self.g0_idx] + else: + G0 = x_pool.new_zeros(*x_pool.shape[:-1], 1) # Grade-4 (High-grade/Pseudoscalar) - if self.g4_idx is not None and len(self.g4_idx) > 0: - # For Cl(3,1), index 15 is typical - G4 = x_pool[..., self.g4_idx] + if len(self.g4_idx) > 0: + # For Cl(3,1), grade-4 has one lane. Higher-dimensional compact + # layouts can expose many grade-4 lanes, so reduce them to a scalar + # phase signal for this head. + G4 = x_pool[..., self.g4_idx].mean(dim=-1, keepdim=True) else: G4 = torch.zeros_like(G0) @@ -199,3 +243,13 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # Mean across channels for final scalar output return result.mean(dim=1) # [B, 1] + + +def _grade_positions(algebra, grades, source_layout) -> torch.Tensor: + """Return positions for ``grades`` in dense lanes or a compact source layout.""" + target_layout = algebra.planner.layout(grades) + if source_layout is None: + return target_layout.indices_tensor(device=algebra.device) + position_by_basis = {index: position for position, index in enumerate(source_layout.basis_indices)} + positions = [position_by_basis[index] for index in target_layout.basis_indices if index in position_by_basis] + return torch.tensor(positions, dtype=torch.long, device=algebra.device) diff --git a/tests/test_layers.py b/tests/test_layers.py index 8013ba9..6d0b27a 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -11,7 +11,17 @@ from core.config import make_algebra from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy -from layers import BladeSelector, CliffordLayerNorm, CliffordLinear, MultiRotorLayer, MultivectorEmbedding, RotorLayer +from layers import ( + BladeSelector, + CliffordLayerNorm, + CliffordLinear, + EntropyGatedAttention, + MotherEmbedding, + MultiRotorLayer, + MultivectorEmbedding, + PhaseShiftHead, + RotorLayer, +) from layers.blocks.multi_rotor_ffn import MultiRotorFFN from layers.blocks.transformer import GeometricTransformerBlock from layers.primitives.reflection import ReflectionLayer @@ -128,6 +138,47 @@ def test_compact_embedding_transformer_pipeline_runs_high_dim_context(self): assert output.shape == (2, 5, 4, algebra.n) + def test_mother_embedding_declared_grades_emit_compact_lanes(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + layer = MotherEmbedding(algebra, input_dim=6, channels=3, grades=(1,)) + x = torch.randn(2, 6) + + y = layer(x) + + assert layer.layout.grades == (1,) + assert y.shape == (2, 3, algebra.n) + + def test_entropy_gated_attention_declared_feature_grades_run_compact(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + layout = algebra.layout((1, 2)) + layer = EntropyGatedAttention( + algebra, + channels=4, + num_heads=2, + feature_grades=(1, 2), + score_grades=(1,), + ) + x = torch.randn(2, 5, 4, layout.dim) + + y, entropy, gate = layer(x, return_gating=True) + + assert layer.g2_idx.numel() == algebra.layout((2,)).dim + assert y.shape == x.shape + assert entropy.shape == (2,) + assert gate.shape == (2,) + + def test_phase_shift_head_declared_feature_grades_read_compact_lanes(self): + algebra = make_algebra(10, 0, 0, device="cpu", dtype=torch.float32) + layout = algebra.layout((0, 4)) + layer = PhaseShiftHead(algebra, channels=2, feature_grades=(0, 4)) + x = torch.randn(3, 5, 2, layout.dim) + + y = layer(x) + + assert layer.g0_idx.numel() == 1 + assert layer.g4_idx.numel() == algebra.layout((4,)).dim + assert y.shape == (3, 1) + def test_rotor_shape(self, algebra_3d): # Batch=4, Channels=5 x = torch.randn(4, 5, 8) From 20ffdc7eefd85b5cb1b7afabd75b692f2648e671 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 15:29:29 +0900 Subject: [PATCH 22/45] feat: expose static layer optimization plans --- layers/__init__.py | 4 + layers/adapters/embedding.py | 4 +- layers/adapters/mother.py | 8 +- layers/blocks/attention.py | 2 +- layers/blocks/transformer.py | 2 +- layers/grade.py | 49 ------ layers/planning.py | 246 +++++++++++++++++++++++++++++ layers/primitives/linear.py | 4 +- layers/primitives/normalization.py | 4 +- layers/primitives/projection.py | 14 +- layers/primitives/rotor_gadget.py | 2 +- tests/test_layers.py | 23 +++ 12 files changed, 295 insertions(+), 67 deletions(-) delete mode 100644 layers/grade.py create mode 100644 layers/planning.py diff --git a/layers/__init__.py b/layers/__init__.py index 85a81ac..dfc0fad 100644 --- a/layers/__init__.py +++ b/layers/__init__.py @@ -10,6 +10,7 @@ from .blocks.attention import GeometricProductAttention from .blocks.multi_rotor_ffn import MultiRotorFFN from .blocks.transformer import GeometricTransformerBlock +from .planning import LayerOptimizationPlan, collect_layer_optimization_plans, layer_optimization_plan from .primitives.linear import CliffordLinear from .primitives.multi_rotor import MultiRotorLayer from .primitives.normalization import CliffordLayerNorm @@ -41,4 +42,7 @@ "MultiRotorFFN", "GeometricTransformerBlock", "CliffordGraphConv", + "LayerOptimizationPlan", + "layer_optimization_plan", + "collect_layer_optimization_plans", ] diff --git a/layers/adapters/embedding.py b/layers/adapters/embedding.py index 4170ba5..1092d28 100644 --- a/layers/adapters/embedding.py +++ b/layers/adapters/embedding.py @@ -12,7 +12,7 @@ from core.foundation.module import CliffordModule -from ..grade import lane_count, resolve_layer_layout +from ..planning import lane_count, resolve_layer_layout class MultivectorEmbedding(CliffordModule): @@ -41,7 +41,7 @@ def __init__( algebra: Clifford algebra instance. vocab_size: Vocabulary size. channels: Number of multivector channels per token. - grades: Optional layer-owned active output grades. When set, the + grades: Optional declared output grades. When set, the embedding table stores compact lanes only. """ super().__init__(algebra) diff --git a/layers/adapters/mother.py b/layers/adapters/mother.py index 8162f5e..ad4628b 100644 --- a/layers/adapters/mother.py +++ b/layers/adapters/mother.py @@ -11,7 +11,7 @@ from core.foundation.module import CliffordModule from ..blocks.attention import GeometricProductAttention -from ..grade import lane_count, resolve_layer_layout +from ..planning import lane_count, resolve_layer_layout from ..primitives.normalization import CliffordLayerNorm @@ -39,7 +39,7 @@ def __init__( channels: Number of multivector channels. U: Geometric uncertainty index for manifold suppression. V: Fixed rotor proxy for Procrustes alignment (input_dim x input_dim). - grades: Optional layer-owned active output grades for compact lanes. + grades: Optional declared output grades for compact lanes. """ super().__init__(algebra) self.channels = channels @@ -105,7 +105,7 @@ def __init__( num_heads: Number of attention heads. eta: Gating multiplier. H_base: Base entropy threshold. - feature_grades: Optional layer-owned active grades for compact lanes. + feature_grades: Optional declared feature grades for compact lanes. score_grades: Optional attention scoring grades. """ super().__init__(algebra) @@ -194,7 +194,7 @@ def __init__(self, algebra, channels: int, feature_grades=None): Args: algebra: Clifford algebra instance. channels: Number of channels to mix. - feature_grades: Optional layer-owned active grades for compact lanes. + feature_grades: Optional declared feature grades for compact lanes. """ super().__init__(algebra) self.channels = channels diff --git a/layers/blocks/attention.py b/layers/blocks/attention.py index 36f94e3..ab0a6c9 100644 --- a/layers/blocks/attention.py +++ b/layers/blocks/attention.py @@ -14,7 +14,7 @@ from core.foundation.basis import normalize_grades, reverse_sign from core.foundation.module import CliffordModule -from ..grade import check_multivector_lanes, lane_count, resolve_layer_layout +from ..planning import check_multivector_lanes, lane_count, resolve_layer_layout from ..primitives.linear import CliffordLinear # Memory-bounded block size for chunked attention computation diff --git a/layers/blocks/transformer.py b/layers/blocks/transformer.py index 9f753d9..4096326 100644 --- a/layers/blocks/transformer.py +++ b/layers/blocks/transformer.py @@ -55,7 +55,7 @@ def __init__( use_entropy_gating: If True, uses EntropyGatedAttention. eta: Gating multiplier for entropy attention. H_base: Base entropy threshold. - feature_grades: Optional layer-owned active grades for compact execution. + feature_grades: Optional declared feature grades for compact execution. attention_score_grades: Optional score grades for attention; defaults to ``feature_grades`` when compact execution is used. use_ffn_rotor_toolbox: Whether to use the dense rotor toolbox in the diff --git a/layers/grade.py b/layers/grade.py deleted file mode 100644 index 97ac556..0000000 --- a/layers/grade.py +++ /dev/null @@ -1,49 +0,0 @@ -# Versor: Universal Geometric Algebra Neural Network -# Copyright (C) 2026 Eunkyum Kim -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# - -"""Layer-owned grade declaration helpers.""" - -from __future__ import annotations - -from typing import Iterable, Optional - -import torch - -from core.foundation.layout import GradeLayout -from core.foundation.module import AlgebraLike -from core.foundation.validation import VALIDATE, check_multivector - - -def resolve_layer_layout(algebra: AlgebraLike, grades: Optional[Iterable[int]]) -> Optional[GradeLayout]: - """Return a compact layout for declared grades, or ``None`` for dense lanes.""" - if grades is None: - return None - return algebra.planner.layout(grades) - - -def lane_count(algebra: AlgebraLike, layout: Optional[GradeLayout]) -> int: - """Return the active basis-lane count for a layer-owned layout.""" - return algebra.dim if layout is None else layout.dim - - -def check_multivector_lanes( - values: torch.Tensor, - algebra: AlgebraLike, - layout: Optional[GradeLayout], - name: str, -) -> None: - """Validate dense or declared compact multivector lanes.""" - if layout is None: - check_multivector(values, algebra, name) - return - if not VALIDATE: - return - assert values.ndim >= 1, f"{name}: expected ndim >= 1, got shape {tuple(values.shape)}" - assert values.shape[-1] == layout.dim, ( - f"{name}: last dim should be {layout.dim} for grades {layout.grades}, " - f"got {values.shape[-1]} (shape {tuple(values.shape)})" - ) diff --git a/layers/planning.py b/layers/planning.py new file mode 100644 index 0000000..5bc6ebf --- /dev/null +++ b/layers/planning.py @@ -0,0 +1,246 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Static optimization descriptors for composed layer graphs. + +The translator side is compile-time only: it inspects modules and returns +immutable route metadata before ``torch.compile`` captures tensor computation. +It must not execute algebra or branch on runtime tensor values. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable, Optional + +import torch +import torch.nn as nn + +from core.foundation.layout import GradeLayout +from core.foundation.module import AlgebraLike +from core.foundation.validation import VALIDATE, check_multivector + +__all__ = [ + "LayerOptimizationPlan", + "resolve_layer_layout", + "lane_count", + "check_multivector_lanes", + "layer_optimization_plan", + "collect_layer_optimization_plans", +] + + +@dataclass(frozen=True) +class LayerOptimizationPlan: + """Static metadata for one algebra-aware module in a composed model.""" + + path: str + module_type: str + operators: tuple[str, ...] + input_grades: Optional[tuple[int, ...]] + output_grades: Optional[tuple[int, ...]] + parameter_grades: Optional[tuple[int, ...]] + score_grades: Optional[tuple[int, ...]] + basis_dim: int + dense_dim: int + compact: bool + dense_only_reason: Optional[str] = None + + @property + def compression_ratio(self) -> float: + """Return active basis lanes divided by dense basis lanes.""" + if self.dense_dim == 0: + return 1.0 + return self.basis_dim / self.dense_dim + + def uses_grade(self, grade: int) -> bool: + """Return whether the plan mentions ``grade`` in any static grade slot.""" + grade = int(grade) + grade_sets = (self.input_grades, self.output_grades, self.parameter_grades, self.score_grades) + return any(grades is not None and grade in grades for grades in grade_sets) + + +def resolve_layer_layout(algebra: AlgebraLike, grades: Optional[Iterable[int]]) -> Optional[GradeLayout]: + """Return a compact layout for declared grades, or ``None`` for dense lanes.""" + if grades is None: + return None + return algebra.planner.layout(grades) + + +def lane_count(algebra: AlgebraLike, layout: Optional[GradeLayout]) -> int: + """Return the active basis-lane count for a declared layout.""" + return algebra.dim if layout is None else layout.dim + + +def check_multivector_lanes( + values: torch.Tensor, + algebra: AlgebraLike, + layout: Optional[GradeLayout], + name: str, +) -> None: + """Validate dense or declared compact multivector lanes.""" + if layout is None: + check_multivector(values, algebra, name) + return + if not VALIDATE: + return + assert values.ndim >= 1, f"{name}: expected ndim >= 1, got shape {tuple(values.shape)}" + assert values.shape[-1] == layout.dim, ( + f"{name}: last dim should be {layout.dim} for grades {layout.grades}, " + f"got {values.shape[-1]} (shape {tuple(values.shape)})" + ) + + +def layer_optimization_plan(module: nn.Module, *, path: str = "") -> Optional[LayerOptimizationPlan]: + """Return static optimization metadata for ``module`` when it exposes algebra routes.""" + custom_plan = _custom_plan(module, path) + if custom_plan is not None: + return custom_plan + + algebra = _module_algebra(module) + if algebra is None: + return None + + layout = _declared_layout(module) + score_grades = _score_grades(module) + parameter_grades = _parameter_grades(module) + operators = _operators(module) + if layout is None and score_grades is None and parameter_grades is None and not operators: + return None + + input_grades, output_grades = _io_grades(module, layout) + compact = layout is not None + dense_dim = int(algebra.dim) + basis_dim = lane_count(algebra, layout) + dense_only_reason = None if compact else _dense_only_reason(module) + return LayerOptimizationPlan( + path=path or "", + module_type=module.__class__.__name__, + operators=operators, + input_grades=input_grades, + output_grades=output_grades, + parameter_grades=parameter_grades, + score_grades=score_grades, + basis_dim=basis_dim, + dense_dim=dense_dim, + compact=compact, + dense_only_reason=dense_only_reason, + ) + + +def collect_layer_optimization_plans( + module: nn.Module, + *, + compact_only: bool = False, +) -> tuple[LayerOptimizationPlan, ...]: + """Collect static optimization metadata from a composed module tree.""" + plans = [] + for path, child in module.named_modules(): + plan = layer_optimization_plan(child, path=path) + if plan is None: + continue + if compact_only and not plan.compact: + continue + plans.append(plan) + return tuple(plans) + + +def _custom_plan(module: nn.Module, path: str) -> Optional[LayerOptimizationPlan]: + plan_fn = getattr(module, "optimization_plan", None) + if plan_fn is None or not callable(plan_fn): + return None + try: + plan = plan_fn(path=path or "") + except TypeError: + plan = plan_fn() + if plan is None: + return None + if not isinstance(plan, LayerOptimizationPlan): + raise TypeError(f"{module.__class__.__name__}.optimization_plan() must return LayerOptimizationPlan or None") + return plan + + +def _module_algebra(module: nn.Module) -> Optional[AlgebraLike]: + algebra = getattr(module, "algebra", None) + if algebra is None: + algebra = getattr(module, "_algebra", None) + if algebra is None or not hasattr(algebra, "planner") or not hasattr(algebra, "dim"): + return None + return algebra + + +def _declared_layout(module: nn.Module) -> Optional[GradeLayout]: + layout = getattr(module, "layout", None) + if layout is None: + layout = getattr(module, "feature_layout", None) + return layout + + +def _grades_from_layout(layout: Optional[GradeLayout]) -> Optional[tuple[int, ...]]: + if layout is None: + return None + return tuple(int(grade) for grade in layout.grades) + + +def _score_grades(module: nn.Module) -> Optional[tuple[int, ...]]: + grades = getattr(module, "score_grades", None) + if grades is None: + score_layout = getattr(module, "_score_layout", None) + return _grades_from_layout(score_layout) + return tuple(int(grade) for grade in grades) + + +def _parameter_grades(module: nn.Module) -> Optional[tuple[int, ...]]: + if hasattr(module, "grade"): + return (int(getattr(module, "grade")),) + return None + + +def _io_grades( + module: nn.Module, + layout: Optional[GradeLayout], +) -> tuple[Optional[tuple[int, ...]], Optional[tuple[int, ...]]]: + grades = _grades_from_layout(layout) + if module.__class__.__name__ in {"MultivectorEmbedding", "MotherEmbedding"}: + return None, grades + return grades, grades + + +def _operators(module: nn.Module) -> tuple[str, ...]: + module_type = module.__class__.__name__ + if module_type == "CliffordLinear": + return (f"linear:{getattr(module, 'backend', 'traditional')}",) + if module_type == "CliffordLayerNorm": + return ("normalize",) + if module_type == "BladeSelector": + return ("blade_gate",) + if module_type == "GeometricProductAttention": + return ("linear", "gp_score", "softmax", "linear") + if module_type == "EntropyGatedAttention": + return ("grade_energy", "gate", "attention") + if module_type in {"MultivectorEmbedding", "MotherEmbedding"}: + return ("embed",) + if module_type == "PhaseShiftHead": + return ("grade_readout",) + if module_type in {"RotorLayer", "MultiRotorLayer", "RotaryBivectorPE", "ReflectionLayer"}: + return ("dense_sandwich",) + if module_type == "RotorGadget": + return ("dense_rotor_toolbox",) + if module_type == "GeometricNeutralizer": + return ("grade_projection", "linear_solve") + return () + + +def _dense_only_reason(module: nn.Module) -> Optional[str]: + module_type = module.__class__.__name__ + if module_type in {"RotorLayer", "MultiRotorLayer", "RotaryBivectorPE", "ReflectionLayer", "RotorGadget"}: + return "sandwich path still materializes dense multivectors" + if module_type == "GeometricNeutralizer": + return "neutralizer reads fixed dense grade positions" + if module_type == "CliffordLinear" and getattr(module, "backend", "traditional") == "rotor": + return "rotor backend requires dense sandwich execution" + return "no compact grade layout declared" diff --git a/layers/primitives/linear.py b/layers/primitives/linear.py index e51d120..b6c07d1 100644 --- a/layers/primitives/linear.py +++ b/layers/primitives/linear.py @@ -18,7 +18,7 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels -from ..grade import check_multivector_lanes, lane_count, resolve_layer_layout +from ..planning import check_multivector_lanes, lane_count, resolve_layer_layout class CliffordLinear(CliffordModule): @@ -66,7 +66,7 @@ def __init__( - 'none': No shuffle (default) - 'fixed': Fixed random permutation - 'random': Random permutation each forward pass - grades: Optional layer-owned active grades. When set, the traditional + grades: Optional declared active grades. When set, the traditional backend operates on compact lanes for those grades instead of requiring a full dense multivector width. """ diff --git a/layers/primitives/normalization.py b/layers/primitives/normalization.py index 807c65c..d9e0db1 100644 --- a/layers/primitives/normalization.py +++ b/layers/primitives/normalization.py @@ -12,7 +12,7 @@ from core.foundation.module import CliffordModule -from ..grade import check_multivector_lanes, lane_count, resolve_layer_layout +from ..planning import check_multivector_lanes, lane_count, resolve_layer_layout class CliffordLayerNorm(CliffordModule): @@ -45,7 +45,7 @@ def __init__( channels (int): Features. eps (float): Stability term. recover (bool): Whether to inject original scale into the scalar part. - grades: Optional layer-owned active grades for compact lane execution. + grades: Optional declared grades for compact lane execution. """ super().__init__(algebra) self.eps = eps diff --git a/layers/primitives/projection.py b/layers/primitives/projection.py index 8e6b64d..a60c0c6 100644 --- a/layers/primitives/projection.py +++ b/layers/primitives/projection.py @@ -13,7 +13,7 @@ from core.foundation.module import CliffordModule from utils.compat import safe_linalg_solve -from ..grade import check_multivector_lanes, lane_count, resolve_layer_layout +from ..planning import check_multivector_lanes, lane_count, resolve_layer_layout class BladeSelector(CliffordModule): @@ -31,7 +31,7 @@ def __init__(self, algebra, channels: int, grades: Optional[Iterable[int]] = Non Args: algebra (CliffordAlgebra): The algebra instance. channels (int): Input features. - grades: Optional layer-owned active grades for compact lane execution. + grades: Optional declared grades for compact lane execution. """ super().__init__(algebra) self.layout = resolve_layer_layout(algebra, grades) @@ -86,9 +86,13 @@ def __init__(self, algebra, channels: int, momentum: float = 0.1): self.channels = channels self.momentum = momentum - # Get indices for Grade-0 and Grade-2 - self.register_buffer("g0_idx", algebra.grade_masks[0].nonzero(as_tuple=False).squeeze(-1)) - self.register_buffer("g2_idx", algebra.grade_masks[2].nonzero(as_tuple=False).squeeze(-1)) + # Get indices for Grade-0 and Grade-2 through planner layouts. + self.register_buffer("g0_idx", algebra.planner.layout((0,)).indices_tensor(device=algebra.device)) + if algebra.n >= 2: + g2_idx = algebra.planner.layout((2,)).indices_tensor(device=algebra.device) + else: + g2_idx = torch.zeros(0, dtype=torch.long, device=algebra.device) + self.register_buffer("g2_idx", g2_idx) # Dimensions for Cl(3,1): Grade-0 is 1, Grade-2 is 6 self.d0 = len(self.g0_idx) diff --git a/layers/primitives/rotor_gadget.py b/layers/primitives/rotor_gadget.py index 16534a7..366b8f3 100644 --- a/layers/primitives/rotor_gadget.py +++ b/layers/primitives/rotor_gadget.py @@ -74,7 +74,7 @@ def __init__( self.aggregation = aggregation self.shuffle = shuffle - # Use the algebra planner layout for layer-owned bivector indices. + # Use the algebra planner layout for static bivector indices. if algebra.num_grades > 2: self.register_buffer("bivector_indices", algebra.planner.layout((2,)).indices_tensor(device=algebra.device)) else: diff --git a/tests/test_layers.py b/tests/test_layers.py index 6d0b27a..687d717 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -21,6 +21,7 @@ MultivectorEmbedding, PhaseShiftHead, RotorLayer, + collect_layer_optimization_plans, ) from layers.blocks.multi_rotor_ffn import MultiRotorFFN from layers.blocks.transformer import GeometricTransformerBlock @@ -138,6 +139,28 @@ def test_compact_embedding_transformer_pipeline_runs_high_dim_context(self): assert output.shape == (2, 5, 4, algebra.n) + def test_compact_composed_layers_expose_static_optimization_plans(self): + algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) + block = GeometricTransformerBlock( + algebra, + channels=4, + num_heads=2, + feature_grades=(1,), + use_ffn_rotor_toolbox=False, + ) + + plans = collect_layer_optimization_plans(block, compact_only=True) + by_path = {plan.path: plan for plan in plans} + + assert "attn.q_proj" in by_path + assert "ffn.expand" in by_path + assert by_path["attn"].score_grades == (1,) + assert by_path["attn.q_proj"].operators == ("linear:traditional",) + assert all(plan.output_grades == (1,) for plan in plans) + assert all(plan.basis_dim == algebra.n for plan in plans) + assert all(plan.dense_dim == algebra.dim for plan in plans) + assert all(plan.compression_ratio < 0.001 for plan in plans) + def test_mother_embedding_declared_grades_emit_compact_lanes(self): algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) layer = MotherEmbedding(algebra, input_dim=6, channels=3, grades=(1,)) From ebe02ae356916873983f97d3e4fe53128d9c185b Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 15:37:50 +0900 Subject: [PATCH 23/45] refactor: move optimization route planning to core --- core/__init__.py | 4 + core/planning/__init__.py | 4 + core/planning/routes.py | 207 +++++++++++++++++++++++++++++ layers/__init__.py | 4 - layers/adapters/embedding.py | 7 + layers/adapters/mother.py | 8 ++ layers/blocks/attention.py | 2 + layers/planning.py | 193 +-------------------------- layers/primitives/linear.py | 2 + layers/primitives/multi_rotor.py | 3 + layers/primitives/normalization.py | 2 + layers/primitives/projection.py | 5 + layers/primitives/reflection.py | 4 + layers/primitives/rotor.py | 3 + layers/primitives/rotor_gadget.py | 4 + tests/test_layers.py | 16 ++- 16 files changed, 270 insertions(+), 198 deletions(-) create mode 100644 core/planning/routes.py diff --git a/core/__init__.py b/core/__init__.py index 629ab99..daf7c61 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -30,6 +30,7 @@ from .planning.layouts import ProductRequest, build_product_request from .planning.planner import GradePlanner from .planning.product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan +from .planning.routes import ModuleOptimizationPlan, collect_module_optimization_plans, module_optimization_plan from .planning.tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .planning.unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request from .runtime.algebra import CliffordAlgebra @@ -108,6 +109,7 @@ "GradePathNode", "GradePlanTree", "GradeFlow", + "ModuleOptimizationPlan", "ProductRequest", "GradeUnaryExecutor", "GradeUnaryOp", @@ -119,8 +121,10 @@ "build_grade_plan_tree", "build_product_request", "build_unary_request", + "collect_module_optimization_plans", "expand_output_grades", "geometric_product_output_grades", + "module_optimization_plan", "normalize_grades", "operation_coefficient", "reverse_sign", diff --git a/core/planning/__init__.py b/core/planning/__init__.py index 6077188..23763cb 100644 --- a/core/planning/__init__.py +++ b/core/planning/__init__.py @@ -11,6 +11,7 @@ from .layouts import ProductRequest, build_product_request from .planner import GradePlanner from .product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan +from .routes import ModuleOptimizationPlan, collect_module_optimization_plans, module_optimization_plan from .tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request @@ -21,6 +22,7 @@ "GradeProductPlan", "GradePlanTree", "GradePlanner", + "ModuleOptimizationPlan", "GradeUnaryExecutor", "GradeUnaryOp", "GradeUnaryPlan", @@ -30,4 +32,6 @@ "build_grade_plan_tree", "build_product_request", "build_unary_request", + "collect_module_optimization_plans", + "module_optimization_plan", ] diff --git a/core/planning/routes.py b/core/planning/routes.py new file mode 100644 index 0000000..2589545 --- /dev/null +++ b/core/planning/routes.py @@ -0,0 +1,207 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Static module-route descriptors for compile-time optimization.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable, Optional + +import torch.nn as nn + +from core.foundation.layout import GradeLayout +from core.foundation.module import AlgebraLike + +_MISSING = object() + + +@dataclass(frozen=True) +class ModuleOptimizationPlan: + """Static metadata for one algebra-aware module in a composed model.""" + + path: str + module_type: str + operators: tuple[str, ...] + input_grades: Optional[tuple[int, ...]] + output_grades: Optional[tuple[int, ...]] + parameter_grades: Optional[tuple[int, ...]] + score_grades: Optional[tuple[int, ...]] + basis_dim: int + dense_dim: int + compact: bool + dense_only_reason: Optional[str] = None + + @property + def compression_ratio(self) -> float: + """Return active basis lanes divided by dense basis lanes.""" + if self.dense_dim == 0: + return 1.0 + return self.basis_dim / self.dense_dim + + def uses_grade(self, grade: int) -> bool: + """Return whether the plan mentions ``grade`` in any static grade slot.""" + grade = int(grade) + grade_sets = (self.input_grades, self.output_grades, self.parameter_grades, self.score_grades) + return any(grades is not None and grade in grades for grades in grade_sets) + + +def module_optimization_plan(module: nn.Module, *, path: str = "") -> Optional[ModuleOptimizationPlan]: + """Return static optimization metadata for one module. + + The collector is layer-agnostic. Modules can either implement + ``optimization_plan(path=...)`` or expose simple static attributes such as + ``layout``, ``feature_layout``, ``score_grades``, and + ``optimization_operators``. + """ + custom_plan = _custom_plan(module, path) + if custom_plan is not None: + return custom_plan + + algebra = _module_algebra(module) + if algebra is None: + return None + + layout = _declared_layout(module) + operators = _operator_tuple(getattr(module, "optimization_operators", ())) + parameter_grades = _parameter_grades(module) + score_grades = _score_grades(module) + dense_only_reason = getattr(module, "optimization_dense_only_reason", None) + if layout is None and not operators and parameter_grades is None and score_grades is None and dense_only_reason is None: + return None + + default_grades = _grades_from_layout(layout) + input_grades = _grade_attr(module, "optimization_input_grades", default_grades) + output_grades = _grade_attr(module, "optimization_output_grades", default_grades) + compact = layout is not None + return ModuleOptimizationPlan( + path=path or "", + module_type=module.__class__.__name__, + operators=operators, + input_grades=input_grades, + output_grades=output_grades, + parameter_grades=parameter_grades, + score_grades=score_grades, + basis_dim=_basis_dim(algebra, layout), + dense_dim=int(algebra.dim), + compact=compact, + dense_only_reason=None if compact else dense_only_reason, + ) + + +def collect_module_optimization_plans( + module: nn.Module, + *, + compact_only: bool = False, +) -> tuple[ModuleOptimizationPlan, ...]: + """Collect static optimization metadata from a composed module tree.""" + plans = [] + for path, child in module.named_modules(): + plan = module_optimization_plan(child, path=path) + if plan is None: + continue + if compact_only and not plan.compact: + continue + plans.append(plan) + return tuple(plans) + + +def _custom_plan(module: nn.Module, path: str) -> Optional[ModuleOptimizationPlan]: + plan_fn = getattr(module, "optimization_plan", None) + if plan_fn is None or not callable(plan_fn): + return None + try: + plan = plan_fn(path=path or "") + except TypeError: + plan = plan_fn() + if plan is None: + return None + if not isinstance(plan, ModuleOptimizationPlan): + raise TypeError(f"{module.__class__.__name__}.optimization_plan() must return ModuleOptimizationPlan or None") + return plan + + +def _module_algebra(module: nn.Module) -> Optional[AlgebraLike]: + algebra = getattr(module, "algebra", None) + if algebra is None: + algebra = getattr(module, "_algebra", None) + if algebra is None or not hasattr(algebra, "planner") or not hasattr(algebra, "dim"): + return None + return algebra + + +def _declared_layout(module: nn.Module) -> Optional[GradeLayout]: + for attr in ("optimization_layout", "layout", "feature_layout"): + layout = getattr(module, attr, None) + if layout is not None: + return layout + return None + + +def _grades_from_layout(layout: Optional[GradeLayout]) -> Optional[tuple[int, ...]]: + if layout is None: + return None + return tuple(int(grade) for grade in layout.grades) + + +def _grade_attr( + module: nn.Module, + attr: str, + default: Optional[tuple[int, ...]], +) -> Optional[tuple[int, ...]]: + value = getattr(module, attr, _MISSING) + if value is _MISSING: + return default + return _grade_tuple(value) + + +def _score_grades(module: nn.Module) -> Optional[tuple[int, ...]]: + value = getattr(module, "optimization_score_grades", _MISSING) + if value is not _MISSING: + return _grade_tuple(value) + + grades = getattr(module, "score_grades", None) + if grades is not None: + return _grade_tuple(grades) + + score_layout = getattr(module, "_score_layout", None) + return _grades_from_layout(score_layout) + + +def _parameter_grades(module: nn.Module) -> Optional[tuple[int, ...]]: + value = getattr(module, "optimization_parameter_grades", _MISSING) + if value is not _MISSING: + return _grade_tuple(value) + if hasattr(module, "grade"): + return (int(getattr(module, "grade")),) + return None + + +def _grade_tuple(grades) -> Optional[tuple[int, ...]]: + if grades is None: + return None + if isinstance(grades, GradeLayout): + return _grades_from_layout(grades) + if isinstance(grades, int): + return (int(grades),) + if isinstance(grades, Iterable): + return tuple(int(grade) for grade in grades) + return (int(grades),) + + +def _operator_tuple(operators) -> tuple[str, ...]: + if operators is None: + return () + if isinstance(operators, str): + return (operators,) + return tuple(str(operator) for operator in operators) + + +def _basis_dim(algebra: AlgebraLike, layout: Optional[GradeLayout]) -> int: + if layout is not None: + return int(layout.dim) + return int(algebra.dim) diff --git a/layers/__init__.py b/layers/__init__.py index dfc0fad..85a81ac 100644 --- a/layers/__init__.py +++ b/layers/__init__.py @@ -10,7 +10,6 @@ from .blocks.attention import GeometricProductAttention from .blocks.multi_rotor_ffn import MultiRotorFFN from .blocks.transformer import GeometricTransformerBlock -from .planning import LayerOptimizationPlan, collect_layer_optimization_plans, layer_optimization_plan from .primitives.linear import CliffordLinear from .primitives.multi_rotor import MultiRotorLayer from .primitives.normalization import CliffordLayerNorm @@ -42,7 +41,4 @@ "MultiRotorFFN", "GeometricTransformerBlock", "CliffordGraphConv", - "LayerOptimizationPlan", - "layer_optimization_plan", - "collect_layer_optimization_plans", ] diff --git a/layers/adapters/embedding.py b/layers/adapters/embedding.py index 1092d28..0a7b390 100644 --- a/layers/adapters/embedding.py +++ b/layers/adapters/embedding.py @@ -28,6 +28,9 @@ class MultivectorEmbedding(CliffordModule): embedding (nn.Embedding): Underlying embedding table. """ + optimization_operators = ("embed",) + optimization_input_grades = None + def __init__( self, algebra, @@ -105,6 +108,10 @@ class RotaryBivectorPE(CliffordModule): bivector_indices (torch.Tensor): Indices of grade-2 basis elements. """ + optimization_operators = ("dense_sandwich",) + optimization_parameter_grades = (2,) + optimization_dense_only_reason = "positional rotor path still materializes dense multivectors" + def __init__( self, algebra, diff --git a/layers/adapters/mother.py b/layers/adapters/mother.py index ad4628b..70c65cb 100644 --- a/layers/adapters/mother.py +++ b/layers/adapters/mother.py @@ -22,6 +22,9 @@ class MotherEmbedding(CliffordModule): reference frame, effectively aligning disparate geometric manifolds. """ + optimization_operators = ("embed",) + optimization_input_grades = None + def __init__( self, algebra, @@ -87,6 +90,8 @@ class EntropyGatedAttention(CliffordModule): or suppressed, allowing only coherent, synchronized states to propagate. """ + optimization_operators = ("grade_energy", "gate", "attention") + def __init__( self, algebra, @@ -188,6 +193,9 @@ class PhaseShiftHead(CliffordModule): high-grade component (G4) via a learned phase angle theta. """ + optimization_operators = ("grade_readout",) + optimization_output_grades = None + def __init__(self, algebra, channels: int, feature_grades=None): """Initializes the Phase-Shift Head. diff --git a/layers/blocks/attention.py b/layers/blocks/attention.py index ab0a6c9..3aad773 100644 --- a/layers/blocks/attention.py +++ b/layers/blocks/attention.py @@ -45,6 +45,8 @@ class GeometricProductAttention(CliffordModule): bivector_weight (float): lambda_ - weight of bivector score component. """ + optimization_operators = ("linear", "gp_score", "softmax", "linear") + def __init__( self, algebra, diff --git a/layers/planning.py b/layers/planning.py index 5bc6ebf..4cea72c 100644 --- a/layers/planning.py +++ b/layers/planning.py @@ -5,65 +5,25 @@ # you may not use this file except in compliance with the License. # -"""Static optimization descriptors for composed layer graphs. - -The translator side is compile-time only: it inspects modules and returns -immutable route metadata before ``torch.compile`` captures tensor computation. -It must not execute algebra or branch on runtime tensor values. -""" +"""Layer helpers for declared compact grade layouts.""" from __future__ import annotations -from dataclasses import dataclass from typing import Iterable, Optional import torch -import torch.nn as nn from core.foundation.layout import GradeLayout from core.foundation.module import AlgebraLike from core.foundation.validation import VALIDATE, check_multivector __all__ = [ - "LayerOptimizationPlan", "resolve_layer_layout", "lane_count", "check_multivector_lanes", - "layer_optimization_plan", - "collect_layer_optimization_plans", ] -@dataclass(frozen=True) -class LayerOptimizationPlan: - """Static metadata for one algebra-aware module in a composed model.""" - - path: str - module_type: str - operators: tuple[str, ...] - input_grades: Optional[tuple[int, ...]] - output_grades: Optional[tuple[int, ...]] - parameter_grades: Optional[tuple[int, ...]] - score_grades: Optional[tuple[int, ...]] - basis_dim: int - dense_dim: int - compact: bool - dense_only_reason: Optional[str] = None - - @property - def compression_ratio(self) -> float: - """Return active basis lanes divided by dense basis lanes.""" - if self.dense_dim == 0: - return 1.0 - return self.basis_dim / self.dense_dim - - def uses_grade(self, grade: int) -> bool: - """Return whether the plan mentions ``grade`` in any static grade slot.""" - grade = int(grade) - grade_sets = (self.input_grades, self.output_grades, self.parameter_grades, self.score_grades) - return any(grades is not None and grade in grades for grades in grade_sets) - - def resolve_layer_layout(algebra: AlgebraLike, grades: Optional[Iterable[int]]) -> Optional[GradeLayout]: """Return a compact layout for declared grades, or ``None`` for dense lanes.""" if grades is None: @@ -93,154 +53,3 @@ def check_multivector_lanes( f"{name}: last dim should be {layout.dim} for grades {layout.grades}, " f"got {values.shape[-1]} (shape {tuple(values.shape)})" ) - - -def layer_optimization_plan(module: nn.Module, *, path: str = "") -> Optional[LayerOptimizationPlan]: - """Return static optimization metadata for ``module`` when it exposes algebra routes.""" - custom_plan = _custom_plan(module, path) - if custom_plan is not None: - return custom_plan - - algebra = _module_algebra(module) - if algebra is None: - return None - - layout = _declared_layout(module) - score_grades = _score_grades(module) - parameter_grades = _parameter_grades(module) - operators = _operators(module) - if layout is None and score_grades is None and parameter_grades is None and not operators: - return None - - input_grades, output_grades = _io_grades(module, layout) - compact = layout is not None - dense_dim = int(algebra.dim) - basis_dim = lane_count(algebra, layout) - dense_only_reason = None if compact else _dense_only_reason(module) - return LayerOptimizationPlan( - path=path or "", - module_type=module.__class__.__name__, - operators=operators, - input_grades=input_grades, - output_grades=output_grades, - parameter_grades=parameter_grades, - score_grades=score_grades, - basis_dim=basis_dim, - dense_dim=dense_dim, - compact=compact, - dense_only_reason=dense_only_reason, - ) - - -def collect_layer_optimization_plans( - module: nn.Module, - *, - compact_only: bool = False, -) -> tuple[LayerOptimizationPlan, ...]: - """Collect static optimization metadata from a composed module tree.""" - plans = [] - for path, child in module.named_modules(): - plan = layer_optimization_plan(child, path=path) - if plan is None: - continue - if compact_only and not plan.compact: - continue - plans.append(plan) - return tuple(plans) - - -def _custom_plan(module: nn.Module, path: str) -> Optional[LayerOptimizationPlan]: - plan_fn = getattr(module, "optimization_plan", None) - if plan_fn is None or not callable(plan_fn): - return None - try: - plan = plan_fn(path=path or "") - except TypeError: - plan = plan_fn() - if plan is None: - return None - if not isinstance(plan, LayerOptimizationPlan): - raise TypeError(f"{module.__class__.__name__}.optimization_plan() must return LayerOptimizationPlan or None") - return plan - - -def _module_algebra(module: nn.Module) -> Optional[AlgebraLike]: - algebra = getattr(module, "algebra", None) - if algebra is None: - algebra = getattr(module, "_algebra", None) - if algebra is None or not hasattr(algebra, "planner") or not hasattr(algebra, "dim"): - return None - return algebra - - -def _declared_layout(module: nn.Module) -> Optional[GradeLayout]: - layout = getattr(module, "layout", None) - if layout is None: - layout = getattr(module, "feature_layout", None) - return layout - - -def _grades_from_layout(layout: Optional[GradeLayout]) -> Optional[tuple[int, ...]]: - if layout is None: - return None - return tuple(int(grade) for grade in layout.grades) - - -def _score_grades(module: nn.Module) -> Optional[tuple[int, ...]]: - grades = getattr(module, "score_grades", None) - if grades is None: - score_layout = getattr(module, "_score_layout", None) - return _grades_from_layout(score_layout) - return tuple(int(grade) for grade in grades) - - -def _parameter_grades(module: nn.Module) -> Optional[tuple[int, ...]]: - if hasattr(module, "grade"): - return (int(getattr(module, "grade")),) - return None - - -def _io_grades( - module: nn.Module, - layout: Optional[GradeLayout], -) -> tuple[Optional[tuple[int, ...]], Optional[tuple[int, ...]]]: - grades = _grades_from_layout(layout) - if module.__class__.__name__ in {"MultivectorEmbedding", "MotherEmbedding"}: - return None, grades - return grades, grades - - -def _operators(module: nn.Module) -> tuple[str, ...]: - module_type = module.__class__.__name__ - if module_type == "CliffordLinear": - return (f"linear:{getattr(module, 'backend', 'traditional')}",) - if module_type == "CliffordLayerNorm": - return ("normalize",) - if module_type == "BladeSelector": - return ("blade_gate",) - if module_type == "GeometricProductAttention": - return ("linear", "gp_score", "softmax", "linear") - if module_type == "EntropyGatedAttention": - return ("grade_energy", "gate", "attention") - if module_type in {"MultivectorEmbedding", "MotherEmbedding"}: - return ("embed",) - if module_type == "PhaseShiftHead": - return ("grade_readout",) - if module_type in {"RotorLayer", "MultiRotorLayer", "RotaryBivectorPE", "ReflectionLayer"}: - return ("dense_sandwich",) - if module_type == "RotorGadget": - return ("dense_rotor_toolbox",) - if module_type == "GeometricNeutralizer": - return ("grade_projection", "linear_solve") - return () - - -def _dense_only_reason(module: nn.Module) -> Optional[str]: - module_type = module.__class__.__name__ - if module_type in {"RotorLayer", "MultiRotorLayer", "RotaryBivectorPE", "ReflectionLayer", "RotorGadget"}: - return "sandwich path still materializes dense multivectors" - if module_type == "GeometricNeutralizer": - return "neutralizer reads fixed dense grade positions" - if module_type == "CliffordLinear" and getattr(module, "backend", "traditional") == "rotor": - return "rotor backend requires dense sandwich execution" - return "no compact grade layout declared" diff --git a/layers/primitives/linear.py b/layers/primitives/linear.py index b6c07d1..5440a76 100644 --- a/layers/primitives/linear.py +++ b/layers/primitives/linear.py @@ -74,6 +74,7 @@ def __init__( self.in_channels = in_channels self.out_channels = out_channels self.backend = backend + self.optimization_operators = (f"linear:{backend}",) self.layout = resolve_layer_layout(algebra, grades) self.basis_dim = lane_count(algebra, self.layout) @@ -86,6 +87,7 @@ def __init__( elif backend == "rotor": if self.layout is not None: raise ValueError("CliffordLinear rotor backend does not yet support compact grade declarations") + self.optimization_dense_only_reason = "rotor backend requires dense sandwich execution" from .rotor_gadget import RotorGadget self.gadget = RotorGadget( diff --git a/layers/primitives/multi_rotor.py b/layers/primitives/multi_rotor.py index ba0ad0f..2dee568 100644 --- a/layers/primitives/multi_rotor.py +++ b/layers/primitives/multi_rotor.py @@ -34,6 +34,9 @@ class MultiRotorLayer(CliffordModule): weights (nn.Parameter): Mixing weights [channels, num_rotors]. """ + optimization_operators = ("dense_sandwich",) + optimization_dense_only_reason = "sandwich path still materializes dense multivectors" + def __init__( self, algebra, diff --git a/layers/primitives/normalization.py b/layers/primitives/normalization.py index d9e0db1..56f7d8e 100644 --- a/layers/primitives/normalization.py +++ b/layers/primitives/normalization.py @@ -30,6 +30,8 @@ class CliffordLayerNorm(CliffordModule): starts identical to the old (scale-discarding) behaviour. """ + optimization_operators = ("normalize",) + def __init__( self, algebra, diff --git a/layers/primitives/projection.py b/layers/primitives/projection.py index a60c0c6..eeea357 100644 --- a/layers/primitives/projection.py +++ b/layers/primitives/projection.py @@ -25,6 +25,8 @@ class BladeSelector(CliffordModule): weights (nn.Parameter): Soft gates [Channels, Dim]. """ + optimization_operators = ("blade_gate",) + def __init__(self, algebra, channels: int, grades: Optional[Iterable[int]] = None): """Sets up the selector. @@ -74,6 +76,9 @@ class GeometricNeutralizer(CliffordModule): momentum (float): EMA momentum. """ + optimization_operators = ("grade_projection", "linear_solve") + optimization_dense_only_reason = "neutralizer reads fixed dense grade positions" + def __init__(self, algebra, channels: int, momentum: float = 0.1): """Initialize the neutralizer. diff --git a/layers/primitives/reflection.py b/layers/primitives/reflection.py index 6cba910..ff15846 100644 --- a/layers/primitives/reflection.py +++ b/layers/primitives/reflection.py @@ -29,6 +29,10 @@ class ReflectionLayer(CliffordModule): vector_weights (nn.Parameter): Learnable grade-1 coefficients [C, n]. """ + optimization_operators = ("dense_sandwich",) + optimization_parameter_grades = (1,) + optimization_dense_only_reason = "reflection path still materializes dense multivectors" + def __init__(self, algebra, channels: int): """Initialize the reflection layer. diff --git a/layers/primitives/rotor.py b/layers/primitives/rotor.py index 47a7d58..6a49661 100644 --- a/layers/primitives/rotor.py +++ b/layers/primitives/rotor.py @@ -30,6 +30,9 @@ class RotorLayer(CliffordModule): grade_weights (nn.Parameter): Learnable grade-k coefficients [channels, num_grade_elements]. """ + optimization_operators = ("dense_sandwich",) + optimization_dense_only_reason = "sandwich path still materializes dense multivectors" + def __init__( self, algebra, diff --git a/layers/primitives/rotor_gadget.py b/layers/primitives/rotor_gadget.py index 366b8f3..4e075c6 100644 --- a/layers/primitives/rotor_gadget.py +++ b/layers/primitives/rotor_gadget.py @@ -42,6 +42,10 @@ class RotorGadget(CliffordModule): aggregation: Aggregation method ('mean', 'sum', or 'learned') """ + optimization_operators = ("dense_rotor_toolbox",) + optimization_parameter_grades = (2,) + optimization_dense_only_reason = "rotor gadget path still materializes dense multivectors" + def __init__( self, algebra, diff --git a/tests/test_layers.py b/tests/test_layers.py index 687d717..1dfb88e 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -9,6 +9,7 @@ import torch from core.config import make_algebra +from core.planning import collect_module_optimization_plans from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy from layers import ( @@ -21,7 +22,6 @@ MultivectorEmbedding, PhaseShiftHead, RotorLayer, - collect_layer_optimization_plans, ) from layers.blocks.multi_rotor_ffn import MultiRotorFFN from layers.blocks.transformer import GeometricTransformerBlock @@ -149,7 +149,7 @@ def test_compact_composed_layers_expose_static_optimization_plans(self): use_ffn_rotor_toolbox=False, ) - plans = collect_layer_optimization_plans(block, compact_only=True) + plans = collect_module_optimization_plans(block, compact_only=True) by_path = {plan.path: plan for plan in plans} assert "attn.q_proj" in by_path @@ -161,6 +161,18 @@ def test_compact_composed_layers_expose_static_optimization_plans(self): assert all(plan.dense_dim == algebra.dim for plan in plans) assert all(plan.compression_ratio < 0.001 for plan in plans) + def test_core_collector_reports_dense_only_declared_paths(self, algebra_3d): + layer = RotorLayer(algebra_3d, channels=4) + + plans = collect_module_optimization_plans(layer) + + assert len(plans) == 1 + assert plans[0].path == "" + assert plans[0].operators == ("dense_sandwich",) + assert plans[0].parameter_grades == (2,) + assert not plans[0].compact + assert plans[0].dense_only_reason == "sandwich path still materializes dense multivectors" + def test_mother_embedding_declared_grades_emit_compact_lanes(self): algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) layer = MotherEmbedding(algebra, input_dim=6, channels=3, grades=(1,)) From 399eea90754b285bca8bf9ab8bfb03374250ee43 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 15:52:33 +0900 Subject: [PATCH 24/45] feat: add optimization route coverage reports --- core/__init__.py | 12 ++++- core/planning/__init__.py | 12 ++++- core/planning/routes.py | 84 ++++++++++++++++++++++++++++++++ functional/activation.py | 36 +++++++++++++- functional/loss.py | 14 ++++++ functional/orthogonality.py | 3 ++ layers/adapters/conformal.py | 5 ++ layers/adapters/projective.py | 5 ++ layers/blocks/multi_rotor_ffn.py | 2 +- models/deap/eeg_net.py | 3 ++ models/lqa/heads.py | 9 ++++ models/md17/forcenet.py | 7 +++ models/sr/net.py | 5 ++ tasks/base.py | 15 ++++++ tests/test_layers.py | 20 +++++++- 15 files changed, 227 insertions(+), 5 deletions(-) diff --git a/core/__init__.py b/core/__init__.py index daf7c61..93cc7b6 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -30,7 +30,14 @@ from .planning.layouts import ProductRequest, build_product_request from .planning.planner import GradePlanner from .planning.product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan -from .planning.routes import ModuleOptimizationPlan, collect_module_optimization_plans, module_optimization_plan +from .planning.routes import ( + ModuleOptimizationIssue, + ModuleOptimizationPlan, + ModuleOptimizationReport, + collect_module_optimization_plans, + inspect_module_optimization, + module_optimization_plan, +) from .planning.tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .planning.unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request from .runtime.algebra import CliffordAlgebra @@ -109,7 +116,9 @@ "GradePathNode", "GradePlanTree", "GradeFlow", + "ModuleOptimizationIssue", "ModuleOptimizationPlan", + "ModuleOptimizationReport", "ProductRequest", "GradeUnaryExecutor", "GradeUnaryOp", @@ -124,6 +133,7 @@ "collect_module_optimization_plans", "expand_output_grades", "geometric_product_output_grades", + "inspect_module_optimization", "module_optimization_plan", "normalize_grades", "operation_coefficient", diff --git a/core/planning/__init__.py b/core/planning/__init__.py index 23763cb..d98a57c 100644 --- a/core/planning/__init__.py +++ b/core/planning/__init__.py @@ -11,7 +11,14 @@ from .layouts import ProductRequest, build_product_request from .planner import GradePlanner from .product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan -from .routes import ModuleOptimizationPlan, collect_module_optimization_plans, module_optimization_plan +from .routes import ( + ModuleOptimizationIssue, + ModuleOptimizationPlan, + ModuleOptimizationReport, + collect_module_optimization_plans, + inspect_module_optimization, + module_optimization_plan, +) from .tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request @@ -22,7 +29,9 @@ "GradeProductPlan", "GradePlanTree", "GradePlanner", + "ModuleOptimizationIssue", "ModuleOptimizationPlan", + "ModuleOptimizationReport", "GradeUnaryExecutor", "GradeUnaryOp", "GradeUnaryPlan", @@ -33,5 +42,6 @@ "build_product_request", "build_unary_request", "collect_module_optimization_plans", + "inspect_module_optimization", "module_optimization_plan", ] diff --git a/core/planning/routes.py b/core/planning/routes.py index 2589545..c673600 100644 --- a/core/planning/routes.py +++ b/core/planning/routes.py @@ -50,6 +50,47 @@ def uses_grade(self, grade: int) -> bool: return any(grades is not None and grade in grades for grades in grade_sets) +@dataclass(frozen=True) +class ModuleOptimizationIssue: + """Static coverage issue for one algebra-aware module.""" + + path: str + module_type: str + reason: str + has_planned_descendants: bool + + +@dataclass(frozen=True) +class ModuleOptimizationReport: + """Optimization-plan coverage for a composed module tree.""" + + plans: tuple[ModuleOptimizationPlan, ...] + issues: tuple[ModuleOptimizationIssue, ...] + + @property + def compact_plans(self) -> tuple[ModuleOptimizationPlan, ...]: + """Return plans that use compact basis lanes.""" + return tuple(plan for plan in self.plans if plan.compact) + + @property + def dense_only_plans(self) -> tuple[ModuleOptimizationPlan, ...]: + """Return plans that are explicitly dense-only.""" + return tuple(plan for plan in self.plans if not plan.compact and plan.dense_only_reason is not None) + + @property + def unplanned_leaf_modules(self) -> tuple[ModuleOptimizationIssue, ...]: + """Return algebra-aware modules that have no plan and no planned descendants.""" + return tuple(issue for issue in self.issues if not issue.has_planned_descendants) + + def assert_no_unplanned_leaves(self) -> None: + """Raise if any algebra-aware leaf module is invisible to the planner.""" + leaves = self.unplanned_leaf_modules + if not leaves: + return + details = ", ".join(f"{issue.path}:{issue.module_type}" for issue in leaves) + raise AssertionError(f"Unplanned algebra-aware leaf modules: {details}") + + def module_optimization_plan(module: nn.Module, *, path: str = "") -> Optional[ModuleOptimizationPlan]: """Return static optimization metadata for one module. @@ -110,6 +151,43 @@ def collect_module_optimization_plans( return tuple(plans) +def inspect_module_optimization(module: nn.Module) -> ModuleOptimizationReport: + """Return static optimization plans plus coverage issues for a module tree.""" + modules = tuple(module.named_modules()) + plans = [] + plan_by_path: dict[str, ModuleOptimizationPlan] = {} + + for path, child in modules: + normalized_path = path or "" + plan = module_optimization_plan(child, path=normalized_path) + if plan is None: + continue + plans.append(plan) + plan_by_path[normalized_path] = plan + + issues = [] + for path, child in modules: + normalized_path = path or "" + if normalized_path in plan_by_path or _module_algebra(child) is None: + continue + has_planned_descendants = any(_is_descendant(plan.path, normalized_path) for plan in plans) + reason = ( + "container has planned descendants but no direct route" + if has_planned_descendants + else "algebra-aware module exposes no static optimization metadata" + ) + issues.append( + ModuleOptimizationIssue( + path=normalized_path, + module_type=child.__class__.__name__, + reason=reason, + has_planned_descendants=has_planned_descendants, + ) + ) + + return ModuleOptimizationReport(plans=tuple(plans), issues=tuple(issues)) + + def _custom_plan(module: nn.Module, path: str) -> Optional[ModuleOptimizationPlan]: plan_fn = getattr(module, "optimization_plan", None) if plan_fn is None or not callable(plan_fn): @@ -205,3 +283,9 @@ def _basis_dim(algebra: AlgebraLike, layout: Optional[GradeLayout]) -> int: if layout is not None: return int(layout.dim) return int(algebra.dim) + + +def _is_descendant(path: str, parent_path: str) -> bool: + if parent_path == "": + return path != "" + return path.startswith(parent_path + ".") diff --git a/functional/activation.py b/functional/activation.py index 92510c2..57dc84c 100644 --- a/functional/activation.py +++ b/functional/activation.py @@ -10,11 +10,15 @@ Magnitude-scaling and grade-wise gating functions that preserve geometric structure. """ +from typing import Optional + import torch import torch.nn as nn import torch.nn.functional as F +from core.foundation.layout import GradeLayout from core.foundation.module import CliffordModule +from core.foundation.validation import VALIDATE, check_multivector class GeometricGELU(CliffordModule): @@ -27,14 +31,18 @@ class GeometricGELU(CliffordModule): bias (torch.nn.Parameter): Learnable bias added to norm. """ - def __init__(self, algebra, channels: int = 1): + optimization_operators = ("magnitude_gate",) + + def __init__(self, algebra, channels: int = 1, grades=None): """Initialize Geometric GELU. Args: algebra (CliffordAlgebra): The algebra instance. channels (int): Number of channels. + grades: Optional declared active grades for compact lane metadata. """ super().__init__(algebra) + self.layout = _resolve_layout(algebra, grades) self.bias = nn.Parameter(torch.zeros(channels)) def forward(self, x: torch.Tensor) -> torch.Tensor: @@ -46,6 +54,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Activated multivector. """ + _check_multivector_lanes(x, self.algebra, self.layout, "GeometricGELU input") norm = x.norm(dim=-1, keepdim=True) eps = 1e-6 @@ -62,6 +71,9 @@ class GeometricSquare(CliffordModule): rotors can then rotate into the output. """ + optimization_operators = ("gp_self", "residual_gate") + optimization_dense_only_reason = "geometric square uses dense self-product; compact output-grade expansion is pending" + def __init__(self, algebra, channels: int = 1): super().__init__(algebra) # sigmoid(-2) ~= 0.12 -- starts small so GP doesn't dominate @@ -85,6 +97,9 @@ class GradeSwish(CliffordModule): grade_biases (torch.nn.Parameter): Biases for each grade gate. """ + optimization_operators = ("grade_gate",) + optimization_dense_only_reason = "grade swish uses dense grade-index buffers" + def __init__(self, algebra, channels: int = 1): """Initialize Grade Swish. @@ -129,3 +144,22 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: per_component_gate = gates.gather(-1, grade_idx) # [..., D] return x * per_component_gate + + +def _resolve_layout(algebra, grades) -> Optional[GradeLayout]: + if grades is None: + return None + return algebra.planner.layout(grades) + + +def _check_multivector_lanes(values: torch.Tensor, algebra, layout: Optional[GradeLayout], name: str) -> None: + if layout is None: + check_multivector(values, algebra, name) + return + if not VALIDATE: + return + assert values.ndim >= 1, f"{name}: expected ndim >= 1, got shape {tuple(values.shape)}" + assert values.shape[-1] == layout.dim, ( + f"{name}: last dim should be {layout.dim} for grades {layout.grades}, " + f"got {values.shape[-1]} (shape {tuple(values.shape)})" + ) diff --git a/functional/loss.py b/functional/loss.py index 1a8f365..9d89b4b 100644 --- a/functional/loss.py +++ b/functional/loss.py @@ -19,6 +19,8 @@ class GeometricMSELoss(CliffordModule): Standard MSE on coefficients. """ + optimization_operators = ("mse_loss",) + def __init__(self, algebra): """Initialize the geometric MSE loss.""" super().__init__(algebra) @@ -34,6 +36,9 @@ class SubspaceLoss(CliffordModule): Penalizes energy in forbidden grades. """ + optimization_operators = ("grade_penalty",) + optimization_dense_only_reason = "subspace loss reads a dense coefficient mask" + def __init__(self, algebra, target_indices: list = None, exclude_indices: list = None): """Initialize grade constraint penalties.""" super().__init__(algebra) @@ -62,6 +67,9 @@ class IsometryLoss(CliffordModule): Ensures transformations preserve the metric norm. """ + optimization_operators = ("metric_norm_loss",) + optimization_dense_only_reason = "isometry loss uses a dense metric diagonal" + def __init__(self, algebra): """Initialize isometry loss with metric diagonal.""" super().__init__(algebra) @@ -92,6 +100,9 @@ class BivectorRegularization(CliffordModule): Penalizes energy outside the target grade (default: grade 2). """ + optimization_operators = ("grade_projection", "regularization") + optimization_dense_only_reason = "bivector regularization calls dense grade projection" + def __init__(self, algebra, grade=2): """Initialize bivector regularization.""" super().__init__(algebra) @@ -112,6 +123,9 @@ class HermitianGradeRegularization(CliffordModule): energy across grades in a physically meaningful way. """ + optimization_operators = ("grade_spectrum", "regularization") + optimization_dense_only_reason = "Hermitian grade spectrum currently expects dense multivectors" + def __init__(self, algebra, target_spectrum=None): """Initialize grade regularization. diff --git a/functional/orthogonality.py b/functional/orthogonality.py index c906015..d02d172 100644 --- a/functional/orthogonality.py +++ b/functional/orthogonality.py @@ -97,6 +97,9 @@ class StrictOrthogonality(CliffordModule): when calling .to(device). """ + optimization_operators = ("grade_projection", "orthogonality") + optimization_dense_only_reason = "strict orthogonality uses dense grade masks" + def __init__(self, algebra, settings: Optional[OrthogonalitySettings] = None): super().__init__(algebra) diff --git a/layers/adapters/conformal.py b/layers/adapters/conformal.py index dd6f958..d9d1a00 100644 --- a/layers/adapters/conformal.py +++ b/layers/adapters/conformal.py @@ -28,6 +28,11 @@ class ConformalEmbedding(CliffordModule): euclidean_dim (int): Physical dimension d. """ + optimization_operators = ("embed",) + optimization_input_grades = None + optimization_output_grades = (1,) + optimization_dense_only_reason = "conformal embedding currently emits dense grade-1 multivectors" + def __init__(self, algebra: CliffordAlgebra, euclidean_dim: int): """Sets up the conformal embedding. diff --git a/layers/adapters/projective.py b/layers/adapters/projective.py index 200ca9a..f87e7c0 100644 --- a/layers/adapters/projective.py +++ b/layers/adapters/projective.py @@ -33,6 +33,11 @@ class ProjectiveEmbedding(CliffordModule): euclidean_dim (int): Physical dimension d. """ + optimization_operators = ("embed",) + optimization_input_grades = None + optimization_output_grades = (1,) + optimization_dense_only_reason = "projective embedding currently emits dense grade-1 multivectors" + def __init__(self, algebra: CliffordAlgebra, euclidean_dim: int): """Sets up the projective embedding. diff --git a/layers/blocks/multi_rotor_ffn.py b/layers/blocks/multi_rotor_ffn.py index 819c38a..7ebf285 100644 --- a/layers/blocks/multi_rotor_ffn.py +++ b/layers/blocks/multi_rotor_ffn.py @@ -72,7 +72,7 @@ def __init__( self.toolbox = ( MultiRotorLayer(algebra, ffn_channels, num_rotors) if self.use_rotor_toolbox else nn.Identity() ) - self.act = GeometricGELU(algebra, channels=ffn_channels) + self.act = GeometricGELU(algebra, channels=ffn_channels, grades=feature_grades) self.contract = CliffordLinear(algebra, ffn_channels, channels, backend=backend, grades=feature_grades) self.gate = BladeSelector(algebra, channels, grades=feature_grades) diff --git a/models/deap/eeg_net.py b/models/deap/eeg_net.py index bf4de26..7fc51ed 100644 --- a/models/deap/eeg_net.py +++ b/models/deap/eeg_net.py @@ -41,6 +41,9 @@ class MultiTargetPhaseShiftHead(CliffordModule): each target can independently shift its prediction range. """ + optimization_operators = ("dense_readout",) + optimization_dense_only_reason = "DEAP phase-shift head flattens dense multivectors for readout" + def __init__(self, algebra: AlgebraLike, channels: int, num_targets: int = 4): super().__init__(algebra) self.channels = channels diff --git a/models/lqa/heads.py b/models/lqa/heads.py index 13bdc3f..87791d2 100644 --- a/models/lqa/heads.py +++ b/models/lqa/heads.py @@ -35,6 +35,9 @@ class ChainReasoningHead(CliffordModule): for relation composition, with soft gating selecting the composition. """ + optimization_operators = ("dense_rotor_bank", "grade_readout") + optimization_dense_only_reason = "chain head applies a dense relation-rotor bank" + def __init__(self, algebra: CliffordAlgebra, channels: int, num_relations: int = 18, hidden_dim: int = 64): super().__init__(algebra) self.channels = channels @@ -108,6 +111,9 @@ class EntailmentHead(CliffordModule): giving the model asymmetry for free from the algebra. """ + optimization_operators = ("gp", "grade_readout") + optimization_dense_only_reason = "entailment head computes dense product features" + def __init__(self, algebra: CliffordAlgebra, channels: int, hidden_dim: int = 64): super().__init__(algebra) self.channels = channels @@ -192,6 +198,9 @@ class NegationHead(CliffordModule): relational noise (grade-2). """ + optimization_operators = ("gp", "grade_involution", "grade_readout") + optimization_dense_only_reason = "negation head computes dense product and involution features" + def __init__(self, algebra: CliffordAlgebra, channels: int, hidden_dim: int = 64): super().__init__(algebra) self.channels = channels diff --git a/models/md17/forcenet.py b/models/md17/forcenet.py index c96bffa..730493f 100644 --- a/models/md17/forcenet.py +++ b/models/md17/forcenet.py @@ -47,6 +47,10 @@ class DynamicRotorGenerator(CliffordModule): as identity (exp(0) = 1). """ + optimization_operators = ("dense_rotor_generation",) + optimization_parameter_grades = (2,) + optimization_dense_only_reason = "dynamic rotor generator materializes dense rotor multivectors" + def __init__(self, algebra: CliffordAlgebra, input_dim: int, num_dynamic_rotors: int = 4): super().__init__(algebra) self.num_dynamic_rotors = num_dynamic_rotors @@ -104,6 +108,9 @@ class MD17InteractionBlock(CliffordModule): optionally uses GeometricSquare activation for algebraic cross-terms. """ + optimization_operators = ("gp_message", "dense_rotor_action", "scatter_message") + optimization_dense_only_reason = "MD17 interaction block builds dense action matrices" + def __init__( self, algebra: CliffordAlgebra, diff --git a/models/sr/net.py b/models/sr/net.py index 8901003..c45288a 100644 --- a/models/sr/net.py +++ b/models/sr/net.py @@ -109,6 +109,11 @@ class SRMultiGradeEmbedding(CliffordModule): grade1_proj (nn.Linear): k -> C*n_g1 projection. """ + optimization_operators = ("embed",) + optimization_input_grades = None + optimization_output_grades = (0, 1) + optimization_dense_only_reason = "SR embedding currently emits dense grade-0/grade-1 multivectors" + def __init__( self, algebra: CliffordAlgebra, diff --git a/tasks/base.py b/tasks/base.py index 9d4c591..d23489d 100644 --- a/tasks/base.py +++ b/tasks/base.py @@ -14,6 +14,7 @@ from tqdm import tqdm from core.foundation.device import DeviceConfig, resolve_device +from core.planning import inspect_module_optimization from log import get_logger logger = get_logger(__name__) @@ -58,6 +59,20 @@ def __init__(self, cfg: DictConfig): self.algebra = self.setup_algebra() self.model = self.setup_model().to(self.device) + self.optimization_report = inspect_module_optimization(self.model) + unplanned_leaves = self.optimization_report.unplanned_leaf_modules + logger.info( + "Optimization routes: %d planned (%d compact, %d dense-only), %d unplanned leaf modules", + len(self.optimization_report.plans), + len(self.optimization_report.compact_plans), + len(self.optimization_report.dense_only_plans), + len(unplanned_leaves), + ) + if unplanned_leaves: + logger.debug( + "Unplanned algebra-aware leaves: %s", + ", ".join(f"{issue.path}:{issue.module_type}" for issue in unplanned_leaves), + ) self.model = self.device_config.maybe_compile(self.model) self.criterion = self.setup_criterion() self.optimizer = self._setup_optimizer() diff --git a/tests/test_layers.py b/tests/test_layers.py index 1dfb88e..21198a2 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -9,7 +9,8 @@ import torch from core.config import make_algebra -from core.planning import collect_module_optimization_plans +from core.foundation.module import CliffordModule +from core.planning import collect_module_optimization_plans, inspect_module_optimization from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy from layers import ( @@ -149,13 +150,18 @@ def test_compact_composed_layers_expose_static_optimization_plans(self): use_ffn_rotor_toolbox=False, ) + report = inspect_module_optimization(block) + report.assert_no_unplanned_leaves() + plans = collect_module_optimization_plans(block, compact_only=True) by_path = {plan.path: plan for plan in plans} assert "attn.q_proj" in by_path + assert "ffn.act" in by_path assert "ffn.expand" in by_path assert by_path["attn"].score_grades == (1,) assert by_path["attn.q_proj"].operators == ("linear:traditional",) + assert by_path["ffn.act"].operators == ("magnitude_gate",) assert all(plan.output_grades == (1,) for plan in plans) assert all(plan.basis_dim == algebra.n for plan in plans) assert all(plan.dense_dim == algebra.dim for plan in plans) @@ -173,6 +179,18 @@ def test_core_collector_reports_dense_only_declared_paths(self, algebra_3d): assert not plans[0].compact assert plans[0].dense_only_reason == "sandwich path still materializes dense multivectors" + def test_core_report_flags_unplanned_algebra_aware_leaves(self, algebra_3d): + class UnplannedLeaf(CliffordModule): + def forward(self, x): + return x + + report = inspect_module_optimization(UnplannedLeaf(algebra_3d)) + + assert len(report.unplanned_leaf_modules) == 1 + assert report.unplanned_leaf_modules[0].module_type == "UnplannedLeaf" + with pytest.raises(AssertionError, match="Unplanned algebra-aware leaf modules"): + report.assert_no_unplanned_leaves() + def test_mother_embedding_declared_grades_emit_compact_lanes(self): algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) layer = MotherEmbedding(algebra, input_dim=6, channels=3, grades=(1,)) From 743fef3d9e894cef07f1f43f7cf75c01fa8f03d7 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 20:12:34 +0900 Subject: [PATCH 25/45] refactor: unify core grade planning interfaces --- core/analysis/commutator.py | 35 ++++++---- core/analysis/geodesic.py | 14 ++-- core/analysis/signature.py | 93 ++++++++++++++++++++----- core/analysis/spectral.py | 6 +- core/analysis/symmetry.py | 10 +-- core/foundation/layout.py | 34 +++++++++ core/foundation/module.py | 13 +++- core/planning/layouts.py | 2 +- core/planning/planner.py | 24 +++++++ core/planning/product.py | 3 +- core/planning/routes.py | 8 ++- core/runtime/algebra.py | 93 ++++++++++++++++++++++--- core/runtime/context.py | 15 +++- core/runtime/decomposition.py | 6 +- core/runtime/metric.py | 47 ++++++------- core/runtime/multivector.py | 125 +++++++++++++++++++++++++++++++--- tests/test_grade_plan.py | 86 +++++++++++++++++++++++ 17 files changed, 515 insertions(+), 99 deletions(-) diff --git a/core/analysis/commutator.py b/core/analysis/commutator.py index ad4c22e..5aca7ad 100644 --- a/core/analysis/commutator.py +++ b/core/analysis/commutator.py @@ -16,7 +16,7 @@ import torch -from core.runtime.algebra import CliffordAlgebra +from core.foundation.module import AlgebraLike from ._types import CONSTANTS, CommutatorResult @@ -29,14 +29,14 @@ class CommutatorAnalyzer: elements (bivectors), directly related to rotation planes. Args: - algebra: :class:`CliffordAlgebra` instance. + algebra: algebra kernel or planning context. max_bivectors: Maximum number of bivectors for Lie-bracket closure analysis (guards combinatorial cost). """ def __init__( self, - algebra: CliffordAlgebra, + algebra: AlgebraLike, max_bivectors: int = 15, ): self.algebra = algebra @@ -91,7 +91,7 @@ def commutativity_matrix(self, mv_data: torch.Tensor) -> torch.Tensor: dtype = mv_data.dtype # Build per-direction projections: keep only the e_i component - g1_idx = (1 << torch.arange(n, device=device)).long() + g1_idx = self.algebra.grade_indices((1,), device=device) N = mv_data.shape[0] @@ -107,7 +107,13 @@ def commutativity_matrix(self, mv_data: torch.Tensor) -> torch.Tensor: i_idx, j_idx = torch.triu_indices(n, n, offset=1, device=device) # Batched commutator: [n_pairs, N, dim] - comm = self.algebra.commutator(xi_all[i_idx], xi_all[j_idx]) + comm = self.algebra.commutator( + xi_all[i_idx], + xi_all[j_idx], + left_grades=(1,), + right_grades=(1,), + output_grades=(2,), + ) vals = comm.norm(dim=-1).mean(dim=-1) # [n_pairs] matrix = torch.zeros(n, n, device=device, dtype=dtype) @@ -194,9 +200,7 @@ def lie_bracket_closure(self, mv_data: torch.Tensor) -> Dict: bv_data = self.algebra.grade_projection(mv_data, 2) # [N, dim] mean_bv = bv_data.mean(dim=0) # [dim] - # Identify bivector basis indices - ii, jj = torch.triu_indices(n, n, offset=1) - bv_blade_indices = ((1 << ii) | (1 << jj)).tolist() + bv_blade_indices = self.algebra.grade_indices((2,), device=device).tolist() if not bv_blade_indices: return { @@ -222,8 +226,13 @@ def lie_bracket_closure(self, mv_data: torch.Tensor) -> Dict: a_idx, b_idx = torch.triu_indices(k, k, offset=1, device=device) # Batched commutator and grade-2 projection - brackets = self.algebra.commutator(B[a_idx], B[b_idx]) # [n_pairs, dim] - brackets_bv = self.algebra.grade_projection(brackets, 2) # [n_pairs, dim] + brackets_bv = self.algebra.commutator( + B[a_idx], + B[b_idx], + left_grades=(2,), + right_grades=(2,), + output_grades=(2,), + ) # [n_pairs, dim] # Project onto basis: coeffs[p, c] = coeffs = brackets_bv @ B.T # [n_pairs, k] @@ -251,14 +260,14 @@ def lie_bracket_closure(self, mv_data: torch.Tensor) -> Dict: } -def compute_uncertainty_and_alignment(algebra: CliffordAlgebra, data_tensor: torch.Tensor): +def compute_uncertainty_and_alignment(algebra: AlgebraLike, data_tensor: torch.Tensor): """Compute Geometric Uncertainty Index (U) and Procrustes Alignment (V). Used by :class:`~layers.adapters.mother.MotherEmbedding` to initialise per-group / per-subject alignment rotors. Args: - algebra: CliffordAlgebra instance. + algebra: algebra kernel or planning context. data_tensor: ``[N, D]`` tensor of raw features. Returns: @@ -279,7 +288,7 @@ def compute_uncertainty_and_alignment(algebra: CliffordAlgebra, data_tensor: tor # 2. Mean multivector and commutator [x_i, mu] mu = x.mean(dim=0, keepdim=True) # [1, dim] - comm = algebra.commutator(x, mu.expand_as(x)) + comm = algebra.commutator(x, mu.expand_as(x), left_grades=(1,), right_grades=(1,), output_grades=(2,)) U = torch.norm(comm, p=2, dim=-1).mean().item() diff --git a/core/analysis/geodesic.py b/core/analysis/geodesic.py index 484dcdb..4a90f8a 100644 --- a/core/analysis/geodesic.py +++ b/core/analysis/geodesic.py @@ -16,7 +16,7 @@ import torch -from core.runtime.algebra import CliffordAlgebra +from core.foundation.module import AlgebraLike from ._types import CONSTANTS @@ -44,7 +44,7 @@ class GeodesicFlow: collides with itself. The signal is dominated by noise. """ - def __init__(self, algebra: CliffordAlgebra, k: int = 8): + def __init__(self, algebra: AlgebraLike, k: int = 8): """Initialize Geodesic Flow. Args: @@ -112,10 +112,16 @@ def _connection_bivectors(self, mv: torch.Tensor) -> torch.Tensor: neighbors = mv[nn_idx] # [N, k, dim] xi = mv.unsqueeze(1).expand(N, k, D).reshape(N * k, D) - xj_rev = self.algebra.reverse(neighbors.reshape(N * k, D)) + xj_rev = self.algebra.reverse(neighbors.reshape(N * k, D), input_grades=(1,)) # For grade-1 inputs, _2 = wedge(xi, xj_rev) -- single pass - bv_raw = self.algebra.wedge(xi, xj_rev) # [N*k, dim] + bv_raw = self.algebra.wedge( + xi, + xj_rev, + left_grades=(1,), + right_grades=(1,), + output_grades=(2,), + ) # [N*k, dim] bv_norm = bv_raw.norm(dim=-1, keepdim=True).clamp(min=self.algebra.eps) return (bv_raw / bv_norm).reshape(N, k, D) # [N, k, dim] diff --git a/core/analysis/signature.py b/core/analysis/signature.py index a9874d5..60acc85 100644 --- a/core/analysis/signature.py +++ b/core/analysis/signature.py @@ -22,17 +22,73 @@ import torch.nn as nn from core.config import make_algebra -from core.foundation.module import AlgebraLike -from layers import BladeSelector, CliffordLinear, RotorLayer +from core.foundation.module import AlgebraLike, CliffordModule from ._types import CONSTANTS, DimensionResult, SamplingConfig, SignatureResult from .geodesic import GeodesicFlow +class _ProbeLinear(CliffordModule): + """Core-local channel mixer used by metric-search probes.""" + + def __init__(self, algebra: AlgebraLike, in_channels: int, out_channels: int): + super().__init__(algebra) + self.weight = nn.Parameter(torch.empty(out_channels, in_channels)) + self.bias = nn.Parameter(torch.empty(out_channels, algebra.dim)) + self.reset_parameters() + + def reset_parameters(self) -> None: + nn.init.xavier_uniform_(self.weight) + nn.init.zeros_(self.bias) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.einsum("oi,...id->...od", self.weight, x) + self.bias + + +class _ProbeRotor(CliffordModule): + """Core-local dense rotor used only for signature search analysis.""" + + def __init__(self, algebra: AlgebraLike, channels: int): + super().__init__(algebra) + self.channels = channels + self.register_buffer("bivector_indices", algebra.grade_indices((2,), device=algebra.device)) + self.bivector_weights = nn.Parameter(torch.empty(channels, self.bivector_indices.numel())) + self.bivector_weights._manifold = "spin" + self.reset_parameters() + + def reset_parameters(self) -> None: + nn.init.normal_(self.bivector_weights, std=0.01) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if not hasattr(self.algebra, "exp") or not hasattr(self.algebra, "per_channel_sandwich"): + raise ValueError( + "Signature probes require dense rotor execution; reduce dimensions or request dense kernel." + ) + bivectors = x.new_zeros(self.channels, self.algebra.dim) + indices = self.bivector_indices.unsqueeze(0).expand(self.channels, -1) + bivectors.scatter_(1, indices, self.bivector_weights.to(dtype=x.dtype)) + rotor = self.algebra.exp(-0.5 * bivectors) + return self.algebra.per_channel_sandwich(rotor, x, self.algebra.reverse(rotor)) + + def sparsity_loss(self) -> torch.Tensor: + return torch.norm(self.bivector_weights, p=1) + + +class _ProbeBladeSelector(CliffordModule): + """Core-local blade gate for metric-search probes.""" + + def __init__(self, algebra: AlgebraLike, channels: int): + super().__init__(algebra) + self.weights = nn.Parameter(torch.ones(channels, algebra.dim)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x * torch.sigmoid(self.weights).unsqueeze(0) + + class _SignatureProbe(nn.Module): """Minimal single-rotor probe for bivector energy analysis. - Architecture: CliffordLinear(1, C) -> RotorLayer(C) -> BladeSelector(C). + Architecture: channel mixer -> rotor -> channel mixer -> blade selector. Only one linear layer for channel expansion; the rotor bivector energy is the primary signal for signature discovery. """ @@ -40,10 +96,10 @@ class _SignatureProbe(nn.Module): def __init__(self, algebra: AlgebraLike, channels: int = 4): super().__init__() self.algebra = algebra - self.linear_in = CliffordLinear(algebra, 1, channels) - self.rotor = RotorLayer(algebra, channels) - self.linear_out = CliffordLinear(algebra, channels, 1) - self.selector = BladeSelector(algebra, 1) + self.linear_in = _ProbeLinear(algebra, 1, channels) + self.rotor = _ProbeRotor(algebra, channels) + self.linear_out = _ProbeLinear(algebra, channels, 1) + self.selector = _ProbeBladeSelector(algebra, 1) def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.linear_in(x) @@ -52,8 +108,8 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.selector(x) return x - def get_rotor_layers(self) -> List[RotorLayer]: - return [m for m in self.modules() if isinstance(m, RotorLayer)] + def get_rotor_layers(self) -> List[_ProbeRotor]: + return [m for m in self.modules() if isinstance(m, _ProbeRotor)] def _apply_biased_init( @@ -63,24 +119,24 @@ def _apply_biased_init( ) -> None: """Biases RotorLayer bivector weights based on signature type. - Uses ``algebra.bv_sq_scalar`` to classify each basis bivector: + Uses ``algebra.bivector_squared_signs()`` to classify each basis bivector: - bv_sq = -1: elliptic (positive-signature base vectors) - bv_sq = +1: hyperbolic (mixed-signature base vectors) - bv_sq = 0: null (degenerate base vectors) """ - bv_sq = algebra.bv_sq_scalar + bv_sq = algebra.bivector_squared_signs(device=algebra.device, dtype=algebra.dtype) ell = CONSTANTS.bv_sq_elliptic_bound hyp = CONSTANTS.bv_sq_hyperbolic_bound for rotor in probe.get_rotor_layers(): with torch.no_grad(): if bias_type == "euclidean": - weights = torch.where(bv_sq < ell, torch.tensor(1.0), torch.tensor(0.1)) + weights = torch.where(bv_sq < ell, torch.ones_like(bv_sq), torch.full_like(bv_sq, 0.1)) rotor.bivector_weights.copy_( weights.unsqueeze(0).expand_as(rotor.bivector_weights) + torch.randn_like(rotor.bivector_weights) * 0.05 ) elif bias_type == "minkowski": - weights = torch.where(bv_sq.abs() > hyp, torch.tensor(1.0), torch.tensor(0.1)) + weights = torch.where(bv_sq.abs() > hyp, torch.ones_like(bv_sq), torch.full_like(bv_sq, 0.1)) rotor.bivector_weights.copy_( weights.unsqueeze(0).expand_as(rotor.bivector_weights) + torch.randn_like(rotor.bivector_weights) * 0.05 @@ -138,14 +194,16 @@ def _lift_data(self, data: torch.Tensor) -> Tuple[torch.Tensor, AlgebraLike]: if X + 2 > 12: warnings.warn( f"Data dimension {X} yields algebra dim 2^{X + 2}={2 ** (X + 2)}. " - f"Consider PCA pre-reduction to X <= 10 for tractable computation." + "MetricSearch probes require dense rotor execution; use SignatureSearchAnalyzer PCA " + "pre-reduction to X <= 10." ) + raise ValueError("MetricSearch requires X <= 10 so the conformal probe algebra stays within Cl12.") norm_sq = 0.5 * (data**2).sum(dim=-1, keepdim=True) ones = torch.ones(N, 1, device=self.device, dtype=data.dtype) lifted = torch.cat([data, norm_sq, ones], dim=-1) - algebra = make_algebra(X + 1, 1, 0, device=self.device) + algebra = make_algebra(X + 1, 1, 0, kernel="dense", device=self.device) mv = algebra.embed_vector(lifted) mv = mv.unsqueeze(1) return mv, algebra @@ -222,9 +280,8 @@ def _analyze_bivector_energy( original_dim: int, ) -> Tuple[Tuple[int, int, int], Dict]: """Maps learned bivector energy to (p, q, r) signature.""" - bv_sq = algebra.bv_sq_scalar - bv_mask = algebra.grade_masks[2] - bv_indices = bv_mask.nonzero(as_tuple=False).squeeze(-1) + bv_sq = algebra.bivector_squared_signs(device=self.device, dtype=algebra.dtype) + bv_indices = algebra.grade_indices((2,), device=self.device) total_energy = torch.zeros(len(bv_indices), device=self.device) n_layers = 0 diff --git a/core/analysis/spectral.py b/core/analysis/spectral.py index 750971d..bc8d45b 100644 --- a/core/analysis/spectral.py +++ b/core/analysis/spectral.py @@ -15,7 +15,7 @@ import torch -from core.runtime.algebra import CliffordAlgebra +from core.foundation.module import AlgebraLike from core.runtime.decomposition import differentiable_invariant_decomposition from core.runtime.metric import hermitian_grade_spectrum @@ -35,14 +35,14 @@ class SpectralAnalyzer: operator :math:`L_x(y) = x \\cdot y` (only for small algebras). Args: - algebra: :class:`CliffordAlgebra` instance. + algebra: algebra kernel or planning context. max_simple_components: Maximum number of simple components to extract from the mean bivector. """ def __init__( self, - algebra: CliffordAlgebra, + algebra: AlgebraLike, max_simple_components: int = 5, ): self.algebra = algebra diff --git a/core/analysis/symmetry.py b/core/analysis/symmetry.py index a2171fe..8c01303 100644 --- a/core/analysis/symmetry.py +++ b/core/analysis/symmetry.py @@ -15,7 +15,7 @@ import torch -from core.runtime.algebra import CliffordAlgebra +from core.foundation.module import AlgebraLike from ._types import CONSTANTS, CommutatorResult, SymmetryResult @@ -24,14 +24,14 @@ class SymmetryDetector: """Detect symmetries, null directions, and invariances. Args: - algebra: :class:`CliffordAlgebra` instance. + algebra: algebra kernel or planning context. null_threshold: Energy threshold below which a direction is considered effectively null. """ def __init__( self, - algebra: CliffordAlgebra, + algebra: AlgebraLike, null_threshold: float = 0.01, ): self.algebra = algebra @@ -90,7 +90,7 @@ def detect_null_directions(self, mv_data: torch.Tensor) -> Tuple[List[int], torc *null_indices* lists those with score < threshold. """ n = self.algebra.n - g1_idx = (1 << torch.arange(n, device=mv_data.device)).long() + g1_idx = self.algebra.grade_indices((1,), device=mv_data.device) # Energy on each grade-1 component g1_coeffs = mv_data[:, g1_idx] # [N, n] @@ -141,7 +141,7 @@ def detect_reflection_symmetries(self, mv_data: torch.Tensor) -> List[Dict]: # Build all n basis vectors: [n, dim] basis_vecs = torch.zeros(n, dim, device=mv_data.device, dtype=mv_data.dtype) - blade_indices = (1 << torch.arange(n, device=mv_data.device)).long() + blade_indices = self.algebra.grade_indices((1,), device=mv_data.device) basis_vecs[torch.arange(n, device=mv_data.device), blade_indices] = 1.0 # Batch reflect: [n, N, dim] diff --git a/core/foundation/layout.py b/core/foundation/layout.py index 7a13fe9..b35d002 100644 --- a/core/foundation/layout.py +++ b/core/foundation/layout.py @@ -85,6 +85,40 @@ def indices_tensor(self, *, device=None) -> torch.Tensor: """Return basis indices as a tensor on ``device``.""" return torch.tensor(self.basis_indices, dtype=torch.long, device=device) + def convert(self, values: torch.Tensor, source: "GradeLayout") -> torch.Tensor: + """Convert compact values from ``source`` into this layout. + + Shared basis lanes are copied by canonical basis index. Lanes present in + this layout but absent from ``source`` are filled with zeros, which makes + the method usable for both projections and sparse layout unions without + materializing a full dense multivector. + """ + if source.spec != self.spec: + raise ValueError(f"source layout signature {source.spec} does not match target spec {self.spec}") + if values.shape[-1] != source.dim: + raise ValueError(f"source values last dimension must be {source.dim}, got {values.shape[-1]}") + if source == self: + return values + + source_positions = {index: position for position, index in enumerate(source.basis_indices)} + gather_positions: list[int] = [] + scatter_positions: list[int] = [] + for target_position, index in enumerate(self.basis_indices): + source_position = source_positions.get(index) + if source_position is None: + continue + gather_positions.append(source_position) + scatter_positions.append(target_position) + + output = values.new_zeros(*values.shape[:-1], self.dim) + if not gather_positions: + return output + + gather = torch.tensor(gather_positions, dtype=torch.long, device=values.device) + scatter = torch.tensor(scatter_positions, dtype=torch.long, device=values.device) + copied = torch.index_select(values, -1, gather) + return output.index_copy(-1, scatter, copied) + def compact(self, dense: torch.Tensor) -> torch.Tensor: """Gather compact lanes from a dense multivector tensor.""" if dense.shape[-1] != self.dense_dim: diff --git a/core/foundation/module.py b/core/foundation/module.py index 7e8ab13..af2062e 100644 --- a/core/foundation/module.py +++ b/core/foundation/module.py @@ -7,11 +7,13 @@ """Base PyTorch module for components that share a Clifford algebra.""" -from typing import Protocol, runtime_checkable +from typing import Iterable, Optional, Protocol, runtime_checkable import torch import torch.nn as nn +from core.foundation.layout import GradeLayout + @runtime_checkable class AlgebraLike(Protocol): @@ -22,6 +24,7 @@ class AlgebraLike(Protocol): r: int n: int dim: int + num_grades: int eps: float eps_sq: float planner: object @@ -40,6 +43,14 @@ def _apply(self, fn): """Move/cast algebra-owned buffers.""" ... + def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: + """Return a compact grade layout or the algebra's default layout.""" + ... + + def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: + """Return canonical dense basis indices for ``grades``.""" + ... + class CliffordModule(nn.Module): """Base module for Clifford algebra-aware components. diff --git a/core/planning/layouts.py b/core/planning/layouts.py index 641250b..bcfc364 100644 --- a/core/planning/layouts.py +++ b/core/planning/layouts.py @@ -168,7 +168,7 @@ def resolve_operand_layout( if not full_layout_allowed: raise ValueError( f"{side} input would require a full Cl({spec.p},{spec.q},{spec.r}) layout. " - "Declare active grades or use a dense CliffordAlgebra reference kernel." + "Declare active grades or enable an explicit low-dimensional full-layout fallback." ) return spec.layout(range(spec.n + 1)) diff --git a/core/planning/planner.py b/core/planning/planner.py index 0c4455a..5b027a7 100644 --- a/core/planning/planner.py +++ b/core/planning/planner.py @@ -11,6 +11,7 @@ import torch +from core.foundation.basis import operation_coefficient from core.foundation.layout import AlgebraSpec, GradeLayout from core.planning.layouts import ProductRequest, build_product_request, normalize_product_op from core.planning.product import GradeProductExecutor, build_grade_product_plan_from_request @@ -46,6 +47,29 @@ def full_layout(self) -> GradeLayout: """Return the full dense basis layout.""" return self.layout(range(self.spec.n + 1)) + def grade_indices(self, grades, *, device=None) -> torch.Tensor: + """Return canonical dense basis indices for ``grades``.""" + if device is None: + device = getattr(self.algebra, "device", None) + return self.layout(grades).indices_tensor(device=device) + + def convert_values(self, values: torch.Tensor, *, source_layout: GradeLayout, target_layout: GradeLayout): + """Convert compact values between layouts without full dense materialization.""" + return target_layout.convert(values, source_layout) + + def bivector_squared_signs(self, *, device=None, dtype: torch.dtype = None) -> torch.Tensor: + """Return ``(e_ab)^2`` signs in canonical grade-2 layout order.""" + if device is None: + device = getattr(self.algebra, "device", None) + if dtype is None: + dtype = getattr(self.algebra, "dtype", torch.float32) + layout = self.layout((2,)) + signs = [ + operation_coefficient(index, index, self.spec.p, self.spec.q, self.spec.r, "gp") + for index in layout.basis_indices + ] + return torch.tensor(signs, dtype=dtype, device=device) + def clear_cache(self) -> None: """Drop cached executor modules.""" self._product_executors.clear() diff --git a/core/planning/product.py b/core/planning/product.py index 42e14dc..8e4de72 100644 --- a/core/planning/product.py +++ b/core/planning/product.py @@ -223,7 +223,8 @@ class GradeProductExecutor(nn.Module): """Compile-friendly grade-restricted product using a static interaction plan. ``forward`` returns compact output lanes ordered by ``active_output_indices``. - ``forward_dense`` is a compatibility helper for tests and dense callers. + ``forward_dense`` is an explicit materialization helper for parity checks and + dense callers. """ def __init__(self, plan: GradeProductPlan): diff --git a/core/planning/routes.py b/core/planning/routes.py index c673600..19e53fe 100644 --- a/core/planning/routes.py +++ b/core/planning/routes.py @@ -112,7 +112,13 @@ def module_optimization_plan(module: nn.Module, *, path: str = "") -> Optional[M parameter_grades = _parameter_grades(module) score_grades = _score_grades(module) dense_only_reason = getattr(module, "optimization_dense_only_reason", None) - if layout is None and not operators and parameter_grades is None and score_grades is None and dense_only_reason is None: + if ( + layout is None + and not operators + and parameter_grades is None + and score_grades is None + and dense_only_reason is None + ): return None default_grades = _grades_from_layout(layout) diff --git a/core/runtime/algebra.py b/core/runtime/algebra.py index e404211..01758fa 100644 --- a/core/runtime/algebra.py +++ b/core/runtime/algebra.py @@ -12,11 +12,12 @@ """ import math -from typing import Optional +from typing import Iterable, Optional import torch import torch.nn as nn +from core.foundation.layout import GradeLayout from core.foundation.validation import check_multivector from core.runtime.projected import ProjectedProductMixin @@ -87,6 +88,7 @@ def __init__( self.p, self.q, self.r = p, q, r self.n = p + q + r self.dim = 2**self.n + self.allow_full_layout_products = True # Exp regime: dispatch at init if p == 0 or q == 0: @@ -187,6 +189,26 @@ def dtype(self) -> torch.dtype: """ return self.cayley_signs.dtype + def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: + """Return a compact grade layout, or the full dense layout when omitted.""" + if grades is None: + return self.planner.full_layout() + return self.planner.layout(grades) + + def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: + """Return canonical dense basis indices for ``grades``.""" + return self.planner.grade_indices(grades, device=self.device if device is None else device) + + def bivector_squared_signs(self, *, device=None, dtype: Optional[torch.dtype] = None) -> torch.Tensor: + """Return ``(e_ab)^2`` signs in canonical grade-2 layout order.""" + signs = self.bv_sq_scalar + if device is not None or dtype is not None: + signs = signs.to( + device=self.device if device is None else device, + dtype=self.dtype if dtype is None else dtype, + ) + return signs + def _apply(self, fn): """Propagate device/dtype moves and keep eps tolerances in sync.""" result = super()._apply(fn) @@ -477,7 +499,7 @@ def _compute_signs(self, indices: torch.Tensor, device, dtype: torch.dtype = tor return (commutator_sign * metric_sign).to(dtype=dtype) - def geometric_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + def geometric_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the Geometric Product. Uses vectorized gather + broadcast multiply + sum. @@ -489,6 +511,8 @@ def geometric_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: The product AB [..., Dim]. """ + if kwargs: + return self.projected_product(A, B, op="gp", **kwargs) check_multivector(A, self, "geometric_product(A)") check_multivector(B, self, "geometric_product(B)") @@ -498,7 +522,7 @@ def geometric_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: # result[..., k] = sum_i A[..., i] * B[..., cayley[i,k]] * signs[i,k] return torch.matmul(A.unsqueeze(-2), B_gathered * self.gp_signs).squeeze(-2) - def grade_projection(self, mv: torch.Tensor, grade: int) -> torch.Tensor: + def grade_projection(self, mv: torch.Tensor, grade: int, **kwargs) -> torch.Tensor: """Isolates a specific grade. Uses multiplicative masking (mv * float_mask) instead of boolean @@ -511,12 +535,15 @@ def grade_projection(self, mv: torch.Tensor, grade: int) -> torch.Tensor: Returns: torch.Tensor: Projected multivector [..., Dim]. """ + if kwargs: + kwargs.setdefault("output_grades", (int(grade),)) + return self.planned_unary(mv, op="grade_projection", **kwargs) mask = self.grade_masks_float[grade] if mask.dtype != mv.dtype: mask = mask.to(dtype=mv.dtype) return mv * mask - def reverse(self, mv: torch.Tensor) -> torch.Tensor: + def reverse(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the reversion. The Clifford conjugate. Args: @@ -525,12 +552,14 @@ def reverse(self, mv: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Reversed multivector [..., Dim]. """ + if kwargs: + return self.planned_unary(mv, op="reverse", **kwargs) rev = self.rev_signs if rev.dtype != mv.dtype: rev = rev.to(dtype=mv.dtype) return mv * rev - def wedge(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + def wedge(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the wedge (outer) product: A ^ B = (AB - BA)/2. Single-pass implementation using precomputed antisymmetric signs. @@ -546,6 +575,8 @@ def wedge(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Wedge product A ^ B [..., dim]. """ + if kwargs: + return self.projected_product(A, B, op="wedge", **kwargs) B_gathered = B[..., self.cayley_indices] return torch.matmul(A.unsqueeze(-2), B_gathered * self.wedge_gp_signs).squeeze(-2) @@ -588,7 +619,7 @@ def right_contraction(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: result.scatter_(-1, g1_idx_exp, result_v) return result - def inner_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + def inner_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the inner product: A . B = (AB + BA)/2. Single-pass implementation using precomputed symmetric signs. @@ -604,10 +635,12 @@ def inner_product(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Inner product A . B [..., dim]. """ + if kwargs: + return self.projected_product(A, B, op="inner", **kwargs) B_gathered = B[..., self.cayley_indices] return torch.matmul(A.unsqueeze(-2), B_gathered * self.inner_gp_signs).squeeze(-2) - def commutator(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + def commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the commutator (Lie bracket): [A, B] = AB - BA. Single-pass implementation using precomputed antisymmetric signs @@ -620,10 +653,12 @@ def commutator(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Commutator [A, B] [..., dim]. """ + if kwargs: + return self.projected_product(A, B, op="commutator", **kwargs) B_gathered = B[..., self.cayley_indices] return torch.matmul(A.unsqueeze(-2), B_gathered * self.comm_gp_signs).squeeze(-2) - def anti_commutator(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: + def anti_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the anti-commutator: {A, B} = AB + BA. Single-pass implementation using precomputed symmetric signs @@ -636,9 +671,43 @@ def anti_commutator(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Anti-commutator {A, B} [..., dim]. """ + if kwargs: + return self.projected_product(A, B, op="anti_commutator", **kwargs) B_gathered = B[..., self.cayley_indices] return torch.matmul(A.unsqueeze(-2), B_gathered * self.anti_comm_gp_signs).squeeze(-2) + def planned_unary( + self, + values: torch.Tensor, + *, + op: str, + input_grades=None, + output_grades=None, + input_layout: Optional[GradeLayout] = None, + output_layout: Optional[GradeLayout] = None, + input_compact: bool = False, + compact_output: bool = False, + return_layout: bool = False, + ): + """Execute a unary operation through the shared static grade planner.""" + request = self.planner.unary_request( + values, + op=op, + input_grades=input_grades, + output_grades=output_grades, + input_layout=input_layout, + output_layout=output_layout, + input_compact=input_compact, + ) + executor = self.planner.unary_executor_for_request(request) + output = executor.forward_compact(values) if request.input_compact else executor(values) + + if return_layout: + return output, executor.output_layout + if compact_output: + return output + return executor.output_layout.dense(output) + def blade_inverse(self, blade: torch.Tensor) -> torch.Tensor: """Compute the inverse of a blade: B^{-1} = B_rev / _0. @@ -810,7 +879,7 @@ def blade_reject(self, mv: torch.Tensor, blade: torch.Tensor) -> torch.Tensor: """ return mv - self.blade_project(mv, blade) - def grade_involution(self, mv: torch.Tensor) -> torch.Tensor: + def grade_involution(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: """Grade involution (main involution): x_hat = sum (-1)^k _k. Flips sign of all odd-grade components, preserves even-grade. @@ -822,12 +891,14 @@ def grade_involution(self, mv: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Involuted multivector [..., dim]. """ + if kwargs: + return self.planned_unary(mv, op="grade_involution", **kwargs) signs = self._involution_signs if signs.dtype != mv.dtype: signs = signs.to(dtype=mv.dtype) return mv * signs - def clifford_conjugation(self, mv: torch.Tensor) -> torch.Tensor: + def clifford_conjugation(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: """Clifford conjugation: bar{x} = grade_involution(reverse(x)). Combines reversion and grade involution. For a k-blade: @@ -841,6 +912,8 @@ def clifford_conjugation(self, mv: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Conjugated multivector [..., dim]. """ + if kwargs: + return self.planned_unary(mv, op="clifford_conjugation", **kwargs) cs = self.conj_signs if cs.dtype != mv.dtype: cs = cs.to(dtype=mv.dtype) diff --git a/core/runtime/context.py b/core/runtime/context.py index 13a0053..4bec1f4 100644 --- a/core/runtime/context.py +++ b/core/runtime/context.py @@ -46,8 +46,8 @@ def __init__( self.spec = AlgebraSpec(self.p, self.q, self.r) self._device = torch.device(resolve_device(device) if str(device) == "auto" else device) self._dtype = resolve_dtype(dtype) - self.allow_full_layout_products = self.n <= 8 if allow_full_layout_products is None else bool( - allow_full_layout_products + self.allow_full_layout_products = ( + self.n <= 8 if allow_full_layout_products is None else bool(allow_full_layout_products) ) self._default_grades = None if default_grades is None else normalize_grades(default_grades, self.n) self._default_layout: Optional[GradeLayout] = None @@ -80,6 +80,17 @@ def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: return self._default_layout return self.planner.layout(grades) + def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: + """Return canonical dense basis indices for ``grades`` without dense tables.""" + return self.planner.grade_indices(grades, device=self.device if device is None else device) + + def bivector_squared_signs(self, *, device=None, dtype: Optional[torch.dtype] = None) -> torch.Tensor: + """Return ``(e_ab)^2`` signs in canonical grade-2 layout order.""" + return self.planner.bivector_squared_signs( + device=self.device if device is None else device, + dtype=self.dtype if dtype is None else dtype, + ) + def _apply(self, fn): """Apply a PyTorch module-style device/dtype transform to cached executors.""" probe = fn(torch.empty((), device=self.device, dtype=self.dtype)) diff --git a/core/runtime/decomposition.py b/core/runtime/decomposition.py index a4880d4..d6257fc 100644 --- a/core/runtime/decomposition.py +++ b/core/runtime/decomposition.py @@ -313,7 +313,7 @@ def compiled_safe_decomposed_exp( with torch.no_grad(): decomp = _decompose_compiled_safe(algebra, b.detach(), k=k_actual, fixed_iterations=fixed_iterations) - bv_mask = algebra.grade_masks[2] + bv_indices = algebra.grade_indices((2,), device=b.device) # Re-project live bivector and compose rotors result = identity @@ -322,8 +322,8 @@ def compiled_safe_decomposed_exp( plane_norm = b_i_detached.norm(dim=-1, keepdim=True).clamp(min=algebra.eps_sq) plane_dir = b_i_detached / plane_norm - bv_live = residual[..., bv_mask] - plane_bv = plane_dir[..., bv_mask] + bv_live = torch.index_select(residual, -1, bv_indices) + plane_bv = torch.index_select(plane_dir, -1, bv_indices) coeff = (bv_live * plane_bv).sum(dim=-1, keepdim=True) b_i_live = coeff * plane_dir diff --git a/core/runtime/metric.py b/core/runtime/metric.py index 3aee0b4..2bfb384 100644 --- a/core/runtime/metric.py +++ b/core/runtime/metric.py @@ -13,10 +13,10 @@ import torch -from core.runtime.algebra import CliffordAlgebra +from core.foundation.module import AlgebraLike -def _hermitian_signs(algebra: CliffordAlgebra) -> torch.Tensor: +def _hermitian_signs(algebra: AlgebraLike) -> torch.Tensor: """Return the precomputed Hermitian sign tensor from the algebra. The Hermitian inner product on Cl(p,q) is: @@ -28,10 +28,12 @@ def _hermitian_signs(algebra: CliffordAlgebra) -> torch.Tensor: Returns: Sign tensor [Dim] with values +1, -1, or 0 (null blades). """ + if not hasattr(algebra, "_hermitian_signs"): + raise ValueError("Hermitian dense metrics require a dense algebra kernel.") return algebra._hermitian_signs -def inner_product(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def inner_product(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Compute the scalar product via projection onto grade 0. Computes _0. @@ -44,13 +46,10 @@ def inner_product(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tensor) -> Returns: torch.Tensor: Scalar part [Batch, 1]. """ - # Optimized: A * B then extract grade 0 - prod = algebra.geometric_product(A, B) - scalar_part = prod[..., 0:1] # Grade 0 - return scalar_part + return algebra.projected_geometric_product(A, B, output_grades=(0,), compact_output=True) -def induced_norm(algebra: CliffordAlgebra, A: torch.Tensor) -> torch.Tensor: +def induced_norm(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: """Compute the induced norm respecting the metric signature. Computes ||A|| = sqrt(|_0|). @@ -71,7 +70,7 @@ def induced_norm(algebra: CliffordAlgebra, A: torch.Tensor) -> torch.Tensor: return torch.sqrt(torch.abs(sq_norm)) -def geometric_distance(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def geometric_distance(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Computes geometric distance. dist(A, B) = ||A - B||. @@ -88,7 +87,7 @@ def geometric_distance(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tenso return induced_norm(algebra, diff) -def grade_purity(algebra: CliffordAlgebra, A: torch.Tensor, grade: int) -> torch.Tensor: +def grade_purity(algebra: AlgebraLike, A: torch.Tensor, grade: int) -> torch.Tensor: """Checks the purity of the grade by examining coefficient energy. Purity = ||_k||^2 / ||A||^2. @@ -111,7 +110,7 @@ def grade_purity(algebra: CliffordAlgebra, A: torch.Tensor, grade: int) -> torch return energy_k / energy_total -def mean_active_grade(algebra: CliffordAlgebra, A: torch.Tensor) -> torch.Tensor: +def mean_active_grade(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: """Average grade. Identifies the grade where the majority of the energy resides. Mean Grade = Sum(k * ||_k||^2) / ||A||^2. @@ -153,7 +152,7 @@ def mean_active_grade(algebra: CliffordAlgebra, A: torch.Tensor) -> torch.Tensor # and the signature-aware trace form for algebraic computations. -def clifford_conjugate(algebra: CliffordAlgebra, mv: torch.Tensor) -> torch.Tensor: +def clifford_conjugate(algebra: AlgebraLike, mv: torch.Tensor) -> torch.Tensor: """Clifford conjugation (bar involution). Combines reversion with grade involution: @@ -169,16 +168,10 @@ def clifford_conjugate(algebra: CliffordAlgebra, mv: torch.Tensor) -> torch.Tens Returns: Conjugated multivector [..., Dim]. """ - result = mv.clone() - for i in range(algebra.dim): - k = bin(i).count("1") - sign = ((-1) ** k) * ((-1) ** (k * (k - 1) // 2)) - if sign == -1: - result[..., i] = -result[..., i] - return result + return algebra.clifford_conjugation(mv) -def hermitian_inner_product(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def hermitian_inner_product(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Hermitian inner product on Cl(p,q): _0. _H = Sum_I (conj_sign_I * metric_sign_I) * a_I * b_I @@ -202,7 +195,7 @@ def hermitian_inner_product(algebra: CliffordAlgebra, A: torch.Tensor, B: torch. return (signs * A * B).sum(dim=-1, keepdim=True) -def hermitian_norm(algebra: CliffordAlgebra, A: torch.Tensor) -> torch.Tensor: +def hermitian_norm(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: """Hermitian norm: ||A||_H = sqrt(|_H|). Always real and non-negative for any signature. @@ -220,7 +213,7 @@ def hermitian_norm(algebra: CliffordAlgebra, A: torch.Tensor) -> torch.Tensor: return torch.sqrt(torch.abs(sq)) -def hermitian_distance(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def hermitian_distance(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Hermitian distance: d_H(A, B) = ||A - B||_H. Positive-definite metric distance for any signature. @@ -237,7 +230,7 @@ def hermitian_distance(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tenso return hermitian_norm(algebra, A - B) -def hermitian_angle(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def hermitian_angle(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Hermitian angle between multivectors. cos(theta) = _H / (||A||_H * ||B||_H) @@ -264,7 +257,7 @@ def hermitian_angle(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tensor) return torch.acos(cos_theta) -def grade_hermitian_norm(algebra: CliffordAlgebra, A: torch.Tensor, grade: int) -> torch.Tensor: +def grade_hermitian_norm(algebra: AlgebraLike, A: torch.Tensor, grade: int) -> torch.Tensor: """Hermitian norm restricted to a single grade. ||_k||_H = sqrt(Sum_{I: |I|=k} a_I**2) @@ -284,7 +277,7 @@ def grade_hermitian_norm(algebra: CliffordAlgebra, A: torch.Tensor, grade: int) return hermitian_norm(algebra, A_k) -def hermitian_grade_spectrum(algebra: CliffordAlgebra, A: torch.Tensor) -> torch.Tensor: +def hermitian_grade_spectrum(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: """Full Hermitian grade spectrum. Returns |_H| for each grade k = 0, ..., n. @@ -308,7 +301,7 @@ def hermitian_grade_spectrum(algebra: CliffordAlgebra, A: torch.Tensor) -> torch return torch.cat(spectrum, dim=-1) -def signature_trace_form(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def signature_trace_form(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Signature-aware trace form: <~A B>_0. The standard Clifford algebra scalar product. NOT positive-definite @@ -332,7 +325,7 @@ def signature_trace_form(algebra: CliffordAlgebra, A: torch.Tensor, B: torch.Ten return prod[..., 0:1] -def signature_norm_squared(algebra: CliffordAlgebra, A: torch.Tensor) -> torch.Tensor: +def signature_norm_squared(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: """Signature-aware squared norm: _0. Can be negative in mixed-signature algebras. Returns the raw value diff --git a/core/runtime/multivector.py b/core/runtime/multivector.py index b1187d9..fe885ec 100644 --- a/core/runtime/multivector.py +++ b/core/runtime/multivector.py @@ -74,7 +74,7 @@ def scalar( def __repr__(self): storage = "compact" if self.is_compact else "dense" return ( - f"Multivector(shape={self.tensor.shape}, storage={storage}, " + f"Multivector(shape={self.shape}, storage={storage}, " f"algebra=Cl({self.algebra.p},{self.algebra.q},{self.algebra.r}))" ) @@ -82,13 +82,13 @@ def __repr__(self): def tensor(self) -> torch.Tensor: """Dense coefficient tensor. - This property remains dense for backward compatibility. Performance - paths that operate on compact data should use ``values`` directly. + This property is an explicit dense boundary. Planned paths that operate + on compact data should use ``values`` or ``coefficients`` directly. """ if self._tensor is not None: return self._tensor - # Dense materialization is a compatibility fallback. New Multivector - # operations should preserve compact ``values`` and ``layout`` instead. + # Do not call this inside core operations that can preserve compact + # ``values`` and ``layout``; materialization belongs at API boundaries. return self.layout.dense(self.values) @tensor.setter @@ -102,6 +102,11 @@ def is_compact(self) -> bool: """Whether this multivector stores compact grade lanes.""" return self.layout is not None + @property + def coefficients(self) -> torch.Tensor: + """Return the active storage tensor without dense materialization.""" + return self.values if self.is_compact else self._tensor + def dense(self) -> Multivector: """Return a dense-storage multivector.""" return Multivector(self.algebra, self.tensor) @@ -116,6 +121,8 @@ def with_layout(self, layout: GradeLayout) -> Multivector: self._check_layout(layout) if self.layout == layout: return Multivector(self.algebra, values=self.values, layout=layout) + if self.is_compact: + return Multivector(self.algebra, values=layout.convert(self.values, self.layout), layout=layout) return Multivector(self.algebra, values=layout.compact(self.tensor), layout=layout) def _check_layout(self, layout: GradeLayout) -> None: @@ -137,11 +144,29 @@ def _wrap(self, tensor: torch.Tensor) -> Multivector: def _wrap_compact(self, values: torch.Tensor, layout: GradeLayout) -> Multivector: return Multivector(self.algebra, values=values, layout=layout) + def _values_for_layout(self, layout: GradeLayout) -> torch.Tensor: + self._check_layout(layout) + if self.is_compact: + return layout.convert(self.values, self.layout) + return layout.compact(self.tensor) + + def _combined_layout(self, other: Multivector) -> GradeLayout: + left = self.layout if self.is_compact else self.algebra.layout() + right = other.layout if other.is_compact else other.algebra.layout() + basis = set(left.basis_indices).union(right.basis_indices) + grades = sorted({index.bit_count() for index in basis}) + return self.algebra.layout(grades) + def __add__(self, other): if isinstance(other, Multivector): self._check_algebra(other) + if self.is_compact or other.is_compact: + layout = self._combined_layout(other) + return self._wrap_compact(self._values_for_layout(layout) + other._values_for_layout(layout), layout) return self._wrap(self.tensor + other.tensor) if isinstance(other, (int, float, torch.Tensor)): + if self.is_compact: + return self._wrap_compact(self.values + other, self.layout) return self._wrap(self.tensor + other) return NotImplemented @@ -151,39 +176,58 @@ def __radd__(self, other): def __sub__(self, other): if isinstance(other, Multivector): self._check_algebra(other) + if self.is_compact or other.is_compact: + layout = self._combined_layout(other) + return self._wrap_compact(self._values_for_layout(layout) - other._values_for_layout(layout), layout) return self._wrap(self.tensor - other.tensor) if isinstance(other, (int, float, torch.Tensor)): + if self.is_compact: + return self._wrap_compact(self.values - other, self.layout) return self._wrap(self.tensor - other) return NotImplemented def __rsub__(self, other): if isinstance(other, (int, float, torch.Tensor)): + if self.is_compact: + return self._wrap_compact(other - self.values, self.layout) return self._wrap(other - self.tensor) return NotImplemented def __neg__(self): + if self.is_compact: + return self._wrap_compact(-self.values, self.layout) return self._wrap(-self.tensor) def __mul__(self, other): """Geometric product ``A * B``, or scalar scaling.""" if isinstance(other, Multivector): self._check_algebra(other) - return self._wrap(self.algebra.geometric_product(self.tensor, other.tensor)) + return self.geometric_product(other) if isinstance(other, (int, float)): + if self.is_compact: + return self._wrap_compact(self.values * other, self.layout) return self._wrap(self.tensor * other) if isinstance(other, torch.Tensor): + if self.is_compact: + return self._wrap_compact(self.values * other, self.layout) return self._wrap(self.tensor * other) return NotImplemented def __rmul__(self, other): if isinstance(other, (int, float, torch.Tensor)): + if self.is_compact: + return self._wrap_compact(self.values * other, self.layout) return self._wrap(self.tensor * other) return NotImplemented def __truediv__(self, other): if isinstance(other, (int, float)): + if self.is_compact: + return self._wrap_compact(self.values / other, self.layout) return self._wrap(self.tensor / other) if isinstance(other, torch.Tensor): + if self.is_compact: + return self._wrap_compact(self.values / other, self.layout) return self._wrap(self.tensor / other) return NotImplemented @@ -191,34 +235,79 @@ def __xor__(self, other): """Wedge (outer) product ``A ^ B``.""" if isinstance(other, Multivector): self._check_algebra(other) - return self._wrap(self.algebra.wedge(self.tensor, other.tensor)) + return self.wedge(other) return NotImplemented def __or__(self, other): """Inner product ``A | B``.""" if isinstance(other, Multivector): self._check_algebra(other) - return self._wrap(self.algebra.inner_product(self.tensor, other.tensor)) + return self.inner(other) return NotImplemented def __invert__(self): """Reversion ``~A``.""" - return self._wrap(self.algebra.reverse(self.tensor)) + return self.reverse() def grade(self, k: int) -> Multivector: """Extract the grade-k component.""" + if self.is_compact: + layout = self.algebra.layout((int(k),)) + if not self.layout.contains_grade(k): + values = self.values.new_zeros(*self.values.shape[:-1], layout.dim) + return self._wrap_compact(values, layout) + values, output_layout = self.algebra.planned_unary( + self.values, + op="grade_projection", + input_layout=self.layout, + output_layout=layout, + input_compact=True, + compact_output=True, + return_layout=True, + ) + return self._wrap_compact(values, output_layout) return self._wrap(self.algebra.grade_projection(self.tensor, k)) def reverse(self) -> Multivector: """Reversion (same as ``~self``).""" + if self.is_compact: + values, layout = self.algebra.planned_unary( + self.values, + op="reverse", + input_layout=self.layout, + input_compact=True, + compact_output=True, + return_layout=True, + ) + return self._wrap_compact(values, layout) return self._wrap(self.algebra.reverse(self.tensor)) def grade_involution(self) -> Multivector: """Grade involution (main involution): flips odd-grade signs.""" + if self.is_compact: + values, layout = self.algebra.planned_unary( + self.values, + op="grade_involution", + input_layout=self.layout, + input_compact=True, + compact_output=True, + return_layout=True, + ) + return self._wrap_compact(values, layout) return self._wrap(self.algebra.grade_involution(self.tensor)) def clifford_conjugation(self) -> Multivector: """Clifford conjugation: grade_involution(reverse(x)).""" + if self.is_compact: + values, layout = self.algebra.planned_unary( + self.values, + op="clifford_conjugation", + input_layout=self.layout, + input_compact=True, + compact_output=True, + return_layout=True, + ) + return self._wrap_compact(values, layout) return self._wrap(self.algebra.clifford_conjugation(self.tensor)) def dual(self) -> Multivector: @@ -232,6 +321,8 @@ def inverse(self) -> Multivector: def geometric_product(self, other: Multivector) -> Multivector: """Explicit geometric product (same as ``self * other``).""" self._check_algebra(other) + if self.is_compact or other.is_compact: + return self.projected_product(other, op="gp") return self._wrap(self.algebra.geometric_product(self.tensor, other.tensor)) def projected_product( @@ -268,11 +359,15 @@ def projected_product( def wedge(self, other: Multivector) -> Multivector: """Wedge (outer) product (same as ``self ^ other``).""" self._check_algebra(other) + if self.is_compact or other.is_compact: + return self.projected_product(other, op="wedge") return self._wrap(self.algebra.wedge(self.tensor, other.tensor)) def inner(self, other: Multivector) -> Multivector: """Inner product (same as ``self | other``).""" self._check_algebra(other) + if self.is_compact or other.is_compact: + return self.projected_product(other, op="inner") return self._wrap(self.algebra.inner_product(self.tensor, other.tensor)) def left_contraction(self, other: Multivector) -> Multivector: @@ -288,11 +383,15 @@ def right_contraction(self, other: Multivector) -> Multivector: def commutator(self, other: Multivector) -> Multivector: """Commutator (Lie bracket): ``[self, other] = self*other - other*self``.""" self._check_algebra(other) + if self.is_compact or other.is_compact: + return self.projected_product(other, op="commutator") return self._wrap(self.algebra.commutator(self.tensor, other.tensor)) def anti_commutator(self, other: Multivector) -> Multivector: """Anti-commutator: ``{self, other} = self*other + other*self``.""" self._check_algebra(other) + if self.is_compact or other.is_compact: + return self.projected_product(other, op="anti_commutator") return self._wrap(self.algebra.anti_commutator(self.tensor, other.tensor)) def norm(self) -> torch.Tensor: @@ -307,6 +406,12 @@ def norm_sq(self) -> torch.Tensor: def get_grade_norms(self) -> torch.Tensor: """Per-grade L2 norms.""" + if self.is_compact: + result = self.values.new_zeros(*self.values.shape[:-1], self.algebra.num_grades) + for position, index in enumerate(self.layout.basis_indices): + grade = index.bit_count() + result[..., grade] = result[..., grade] + self.values[..., position].pow(2) + return result.clamp(min=self.algebra.eps).sqrt() return self.algebra.get_grade_norms(self.tensor) def exp(self) -> Multivector: @@ -376,7 +481,7 @@ def requires_grad_(self, requires_grad: bool = True) -> Multivector: @property def shape(self) -> torch.Size: - return self.tensor.shape + return self.values.shape if self.is_compact else self.tensor.shape @property def device(self) -> torch.device: diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index 7f827a2..22007af 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -309,6 +309,39 @@ def test_dense_policy_uses_context_by_default_and_explicit_dense_up_to_twelve(): make_algebra(13, 0, 0, kernel="dense", device=DEVICE, dtype=torch.float32) +def test_dense_kernel_accepts_shared_planned_operation_kwargs(): + algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) + A = _grade_only_input(algebra, 2, (1,), seed=191) + B = _grade_only_input(algebra, 2, (1,), seed=193) + + actual = algebra.geometric_product( + A, + B, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + compact_output=True, + ) + expected = algebra.projected_geometric_product( + A, + B, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + compact_output=True, + ) + + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + +def test_dense_and_context_share_layout_indices_and_bivector_metric_signs(): + dense = CliffordAlgebra(3, 1, 0, device=DEVICE, dtype=torch.float64) + context = make_algebra(3, 1, 0, kernel="context", device=DEVICE, dtype=torch.float64) + + assert torch.equal(dense.grade_indices((2,)), context.grade_indices((2,))) + assert torch.allclose(dense.bivector_squared_signs(), context.bivector_squared_signs()) + + def test_context_projected_product_handles_high_dim_vector_product(): algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) A = torch.zeros(1, algebra.dim) @@ -371,6 +404,59 @@ def test_context_planned_unary_compact_reverse(): assert torch.allclose(actual, -values) +def test_dense_kernel_planned_unary_handles_compact_layouts(): + algebra = CliffordAlgebra(6, 0, 0, device=DEVICE, dtype=torch.float32) + layout = algebra.layout((2,)) + values = torch.arange(layout.dim, dtype=torch.float32).unsqueeze(0) + + actual, output_layout = algebra.reverse( + values, + input_layout=layout, + input_compact=True, + compact_output=True, + return_layout=True, + ) + + assert output_layout == layout + assert torch.allclose(actual, -values) + + +def test_multivector_compact_geometric_product_stays_compact_in_high_dimensions(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + vector_layout = algebra.layout((1,)) + left = torch.zeros(1, vector_layout.dim) + right = torch.zeros(1, vector_layout.dim) + left[0, 0] = 1.0 + right[0, 0] = 1.0 + + result = Multivector(algebra, values=left, layout=vector_layout) * Multivector( + algebra, + values=right, + layout=vector_layout, + ) + + assert result.is_compact + assert result.layout.grades == (0, 2) + scalar_pos = result.layout.basis_indices.index(0) + assert torch.allclose(result.values[0, scalar_pos], torch.tensor(1.0)) + + +def test_multivector_compact_addition_merges_layouts_without_dense_materialization(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + vector_layout = algebra.layout((1,)) + bivector_layout = algebra.layout((2,)) + vector = Multivector(algebra, values=torch.ones(1, vector_layout.dim), layout=vector_layout) + bivector = Multivector(algebra, values=2.0 * torch.ones(1, bivector_layout.dim), layout=bivector_layout) + + result = vector + bivector + + assert result.is_compact + assert result.layout.grades == (1, 2) + vector_values = vector.with_layout(result.layout).values + bivector_values = bivector.with_layout(result.layout).values + assert torch.allclose(result.values, vector_values + bivector_values) + + def test_high_dim_context_requires_declared_layout_for_products(): algebra = make_algebra(9, 0, 0, device=DEVICE, dtype=torch.float32) A = torch.zeros(1, algebra.dim) From 901a138ae0bebfa4165b5819596f5792b330545e Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 21:07:15 +0900 Subject: [PATCH 26/45] feat: add compact core layout accessors --- core/__init__.py | 14 ++ core/foundation/module.py | 27 ++++ core/planning/planner.py | 27 +++- core/runtime/__init__.py | 7 + core/runtime/accessors.py | 237 ++++++++++++++++++++++++++++++++ core/runtime/algebra.py | 42 +++++- core/runtime/context.py | 58 ++++++-- core/runtime/metric.py | 213 +++++++++++++++++++++++----- core/runtime/multivector.py | 83 +++++++++-- core/runtime/projected.py | 12 ++ tests/test_grade_plan.py | 53 ++++++- tests/test_hermitian_metrics.py | 37 +++++ tests/test_multivector.py | 5 +- 13 files changed, 756 insertions(+), 59 deletions(-) create mode 100644 core/runtime/accessors.py diff --git a/core/__init__.py b/core/__init__.py index 93cc7b6..58f54b5 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -40,6 +40,14 @@ ) from .planning.tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .planning.unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request +from .runtime.accessors import ( + as_multivector, + compact_values, + grade_indices, + hermitian_signs, + materialize_dense, + resolve_layout, +) from .runtime.algebra import CliffordAlgebra from .runtime.context import AlgebraContext from .runtime.decomposition import ( @@ -103,6 +111,12 @@ "hermitian_grade_spectrum", "signature_trace_form", "signature_norm_squared", + "as_multivector", + "compact_values", + "grade_indices", + "hermitian_signs", + "materialize_dense", + "resolve_layout", # decomposition "ExpPolicy", "ga_power_iteration", diff --git a/core/foundation/module.py b/core/foundation/module.py index af2062e..4a310bf 100644 --- a/core/foundation/module.py +++ b/core/foundation/module.py @@ -47,10 +47,37 @@ def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: """Return a compact grade layout or the algebra's default layout.""" ... + def default_layout(self) -> GradeLayout: + """Return the algebra default layout.""" + ... + + def resolve_layout( + self, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + mv=None, + allow_full: bool = True, + warn_full: bool = True, + ) -> GradeLayout: + """Resolve static layout metadata for tensors or multivectors.""" + ... + def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: """Return canonical dense basis indices for ``grades``.""" ... + def hermitian_signs( + self, + layout: Optional[GradeLayout] = None, + *, + grades: Optional[Iterable[int]] = None, + device=None, + dtype: Optional[torch.dtype] = None, + ) -> torch.Tensor: + """Return Hermitian signs for a dense or compact layout.""" + ... + class CliffordModule(nn.Module): """Base module for Clifford algebra-aware components. diff --git a/core/planning/planner.py b/core/planning/planner.py index 5b027a7..d4661a2 100644 --- a/core/planning/planner.py +++ b/core/planning/planner.py @@ -23,6 +23,7 @@ build_unary_request, normalize_unary_op, ) +from core.runtime.accessors import FULL_LAYOUT_MAX_N, warn_full_layout_fallback class GradePlanner: @@ -131,6 +132,12 @@ def product_request( right_compact: bool = False, ) -> ProductRequest: """Normalize product intent into a static request without executing tensors.""" + left_grades = self._default_operand_grades(left_grades, left_layout) + right_grades = self._default_operand_grades(right_grades, right_layout) + if self._implicit_full_operand(left, grades=left_grades, layout=left_layout, compact=left_compact) or ( + self._implicit_full_operand(right, grades=right_grades, layout=right_layout, compact=right_compact) + ): + warn_full_layout_fallback(self.algebra) return build_product_request( self.spec, left, @@ -180,6 +187,10 @@ def unary_request( input_compact: bool = False, ) -> UnaryRequest: """Normalize unary intent into a static request without executing tensors.""" + if not (op == "grade_projection" and output_grades is not None): + input_grades = self._default_operand_grades(input_grades, input_layout) + if self._implicit_full_operand(values, grades=input_grades, layout=input_layout, compact=input_compact): + warn_full_layout_fallback(self.algebra) return build_unary_request( self.spec, values, @@ -252,4 +263,18 @@ def _unary_cache_key(self, executor: GradeUnaryExecutor) -> tuple[object, ...]: ) def _full_layout_allowed(self) -> bool: - return bool(getattr(self.algebra, "allow_full_layout_products", True)) + return bool(getattr(self.algebra, "allow_full_layout_products", True)) and self.spec.n <= FULL_LAYOUT_MAX_N + + def _implicit_full_operand(self, tensor: torch.Tensor, *, grades, layout, compact: bool) -> bool: + return ( + grades is None + and layout is None + and not compact + and self._full_layout_allowed() + and tensor.shape[-1] == self.spec.dim + ) + + def _default_operand_grades(self, grades, layout: GradeLayout = None): + if grades is not None or layout is not None: + return grades + return getattr(self.algebra, "_default_grades", None) diff --git a/core/runtime/__init__.py b/core/runtime/__init__.py index e510381..3aa28ba 100644 --- a/core/runtime/__init__.py +++ b/core/runtime/__init__.py @@ -7,6 +7,7 @@ """Runtime algebra hosts and dense reference operations.""" +from .accessors import as_multivector, compact_values, grade_indices, hermitian_signs, materialize_dense, resolve_layout from .algebra import CliffordAlgebra from .context import AlgebraContext from .multivector import Multivector @@ -17,4 +18,10 @@ "CliffordAlgebra", "Multivector", "ProjectedProductMixin", + "as_multivector", + "compact_values", + "grade_indices", + "hermitian_signs", + "materialize_dense", + "resolve_layout", ] diff --git a/core/runtime/accessors.py b/core/runtime/accessors.py new file mode 100644 index 0000000..c827e3c --- /dev/null +++ b/core/runtime/accessors.py @@ -0,0 +1,237 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Functional accessors for algebra layout, grade, and storage metadata.""" + +from __future__ import annotations + +import warnings +from typing import Iterable, Optional + +import torch + +from core.foundation.basis import normalize_grades, operation_coefficient, reverse_sign +from core.foundation.layout import AlgebraSpec, GradeLayout + +FULL_LAYOUT_WARN_N = 8 +FULL_LAYOUT_MAX_N = 12 +_FULL_LAYOUT_WARN_N = FULL_LAYOUT_WARN_N +_FULL_LAYOUT_MAX_N = FULL_LAYOUT_MAX_N +_WARNED_FULL_LAYOUT_SIGNATURES: set[tuple[int, int, int]] = set() + + +def resolve_layout( + algebra, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + mv=None, + allow_full: bool = True, + warn_full: bool = True, +) -> GradeLayout: + """Resolve static grade layout metadata without inspecting tensor values.""" + spec = AlgebraSpec.from_algebra(algebra) + if layout is not None: + _check_layout_spec(spec, layout, "layout") + if grades is not None and layout.grades != normalize_grades(grades, spec.n, name="grades"): + raise ValueError("layout and grades disagree") + return layout + + if grades is not None: + return spec.layout(grades) + + if _is_multivector(mv) and getattr(mv, "layout", None) is not None: + mv_layout = mv.layout + _check_layout_spec(spec, mv_layout, "mv.layout") + return mv_layout + + default_grades = getattr(algebra, "_default_grades", None) + if default_grades is not None: + cached = getattr(algebra, "_default_layout", None) + if cached is not None: + _check_layout_spec(spec, cached, "default_layout") + return cached + resolved = spec.layout(default_grades) + if hasattr(algebra, "_default_layout"): + algebra._default_layout = resolved + return resolved + + if not allow_full or not bool(getattr(algebra, "allow_full_layout_products", True)): + raise ValueError("No grade layout is available. Declare active grades or configure default_grades.") + if spec.n > _FULL_LAYOUT_MAX_N: + raise ValueError( + f"Implicit full Cl({spec.p},{spec.q},{spec.r}) layout is disabled for n>{_FULL_LAYOUT_MAX_N}. " + "Declare active grades or configure default_grades." + ) + if warn_full: + warn_full_layout_fallback(algebra) + return spec.layout(range(spec.n + 1)) + + +def default_layout(algebra) -> GradeLayout: + """Return the algebra default layout using the central fallback policy.""" + return resolve_layout(algebra) + + +def grade_indices(algebra, grades: Iterable[int], *, device=None) -> torch.Tensor: + """Return canonical dense basis indices for ``grades``.""" + if device is None: + device = getattr(algebra, "device", None) + return resolve_layout(algebra, grades=grades, warn_full=False).indices_tensor(device=device) + + +def hermitian_signs( + algebra, + layout: Optional[GradeLayout] = None, + *, + grades: Optional[Iterable[int]] = None, + device=None, + dtype: Optional[torch.dtype] = None, +) -> torch.Tensor: + """Return Hermitian metric signs for a dense or compact layout.""" + resolved = resolve_layout(algebra, layout=layout, grades=grades) + if device is None: + device = getattr(algebra, "device", None) + if dtype is None: + dtype = getattr(algebra, "dtype", torch.float32) + + dense_signs = getattr(algebra, "_hermitian_signs", None) + if dense_signs is not None: + indices = resolved.indices_tensor(device=dense_signs.device) + signs = torch.index_select(dense_signs, -1, indices) + return signs.to(device=device, dtype=dtype) + + values = [_hermitian_sign_for_index(algebra, index) for index in resolved.basis_indices] + return torch.tensor(values, dtype=dtype, device=device) + + +def compact_values( + algebra, + value, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, +) -> tuple[torch.Tensor, GradeLayout]: + """Return compact values plus layout for a tensor or ``Multivector``.""" + resolved = resolve_layout(algebra, layout=layout, grades=grades, mv=value) + if _is_multivector(value): + _check_algebra(algebra, value.algebra) + if value.is_compact: + return resolved.convert(value.values, value.layout), resolved + return resolved.compact(value.coefficients), resolved + + if not isinstance(value, torch.Tensor): + raise TypeError(f"Expected Tensor or Multivector-like value, got {type(value)!r}") + if value.shape[-1] == resolved.dim: + return value, resolved + if value.shape[-1] == resolved.dense_dim: + return resolved.compact(value), resolved + raise ValueError(f"value last dimension must be {resolved.dim} compact or {resolved.dense_dim} dense") + + +def materialize_dense( + algebra, + value, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, +) -> torch.Tensor: + """Return dense coefficients subject to the central full-layout policy.""" + if _is_multivector(value): + _check_algebra(algebra, value.algebra) + if not value.is_compact: + return value.coefficients + _check_dense_materialization_allowed(algebra) + return value.layout.dense(value.values) + + if not isinstance(value, torch.Tensor): + raise TypeError(f"Expected Tensor or Multivector-like value, got {type(value)!r}") + if value.shape[-1] == getattr(algebra, "dim"): + return value + resolved = resolve_layout(algebra, layout=layout, grades=grades) + if value.shape[-1] != resolved.dim: + raise ValueError(f"value compact last dimension must be {resolved.dim}, got {value.shape[-1]}") + _check_dense_materialization_allowed(algebra) + return resolved.dense(value) + + +def as_multivector( + algebra, + value, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, +): + """Wrap a tensor or return an existing ``Multivector``.""" + from core.runtime.multivector import Multivector + + if isinstance(value, Multivector): + _check_algebra(algebra, value.algebra) + if layout is None and grades is None: + return value + resolved = resolve_layout(algebra, layout=layout, grades=grades, mv=value) + return value.with_layout(resolved) + + if layout is None and grades is None: + if not isinstance(value, torch.Tensor): + raise TypeError(f"Expected Tensor or Multivector-like value, got {type(value)!r}") + return Multivector(algebra, value) + + resolved = resolve_layout(algebra, layout=layout, grades=grades) + if value.shape[-1] == resolved.dim: + return Multivector(algebra, values=value, layout=resolved) + return Multivector(algebra, tensor=value, layout=resolved) + + +def warn_full_layout_fallback(algebra) -> None: + """Warn once per signature when implicit full layout is used at n>=8.""" + if getattr(algebra, "n", 0) < _FULL_LAYOUT_WARN_N: + return + signature = (int(algebra.p), int(algebra.q), int(algebra.r)) + if signature in _WARNED_FULL_LAYOUT_SIGNATURES: + return + _WARNED_FULL_LAYOUT_SIGNATURES.add(signature) + warnings.warn( + f"Using implicit full Cl({algebra.p},{algebra.q},{algebra.r}) layout at n={algebra.n}. " + "Declare active grades or default_grades to avoid full-layout planning.", + RuntimeWarning, + stacklevel=3, + ) + + +def _hermitian_sign_for_index(algebra, index: int) -> float: + grade = int(index).bit_count() + grade_sign = -1.0 if grade % 2 else 1.0 + metric_sign = operation_coefficient(index, index, algebra.p, algebra.q, algebra.r, "gp") + return grade_sign * reverse_sign(index) * metric_sign + + +def _check_layout_spec(spec: AlgebraSpec, layout: GradeLayout, name: str) -> None: + if layout.spec != spec: + raise ValueError(f"{name} signature {layout.spec} does not match algebra signature {spec}") + + +def _check_algebra(expected, actual) -> None: + lhs = (expected.p, expected.q, expected.r) + rhs = (actual.p, actual.q, actual.r) + if lhs != rhs: + raise ValueError(f"Algebra mismatch: Cl{lhs} vs Cl{rhs}") + + +def _check_dense_materialization_allowed(algebra) -> None: + if not bool(getattr(algebra, "allow_full_layout_products", True)): + raise ValueError("Dense materialization is disabled for this algebra. Keep compact values.") + if getattr(algebra, "n", 0) > _FULL_LAYOUT_MAX_N: + raise ValueError( + f"Dense materialization is disabled for n>{_FULL_LAYOUT_MAX_N}. " + "Keep compact values or declare a smaller active layout." + ) + warn_full_layout_fallback(algebra) + + +def _is_multivector(value) -> bool: + return hasattr(value, "algebra") and hasattr(value, "layout") and hasattr(value, "is_compact") diff --git a/core/runtime/algebra.py b/core/runtime/algebra.py index 01758fa..5282530 100644 --- a/core/runtime/algebra.py +++ b/core/runtime/algebra.py @@ -19,6 +19,10 @@ from core.foundation.layout import GradeLayout from core.foundation.validation import check_multivector +from core.runtime.accessors import default_layout as _default_layout +from core.runtime.accessors import grade_indices as _grade_indices +from core.runtime.accessors import hermitian_signs as _hermitian_signs +from core.runtime.accessors import resolve_layout as _resolve_layout from core.runtime.projected import ProjectedProductMixin @@ -192,12 +196,46 @@ def dtype(self) -> torch.dtype: def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: """Return a compact grade layout, or the full dense layout when omitted.""" if grades is None: - return self.planner.full_layout() + return self.default_layout() return self.planner.layout(grades) + def default_layout(self) -> GradeLayout: + """Return the default layout using the central full-layout fallback policy.""" + return _default_layout(self) + + def resolve_layout( + self, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + mv=None, + allow_full: bool = True, + warn_full: bool = True, + ) -> GradeLayout: + """Resolve static layout metadata for tensors or multivectors.""" + return _resolve_layout( + self, + layout=layout, + grades=grades, + mv=mv, + allow_full=allow_full, + warn_full=warn_full, + ) + def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: """Return canonical dense basis indices for ``grades``.""" - return self.planner.grade_indices(grades, device=self.device if device is None else device) + return _grade_indices(self, grades, device=self.device if device is None else device) + + def hermitian_signs( + self, + layout: Optional[GradeLayout] = None, + *, + grades: Optional[Iterable[int]] = None, + device=None, + dtype: Optional[torch.dtype] = None, + ) -> torch.Tensor: + """Return Hermitian signs for a dense or compact layout.""" + return _hermitian_signs(self, layout=layout, grades=grades, device=device, dtype=dtype) def bivector_squared_signs(self, *, device=None, dtype: Optional[torch.dtype] = None) -> torch.Tensor: """Return ``(e_ab)^2`` signs in canonical grade-2 layout order.""" diff --git a/core/runtime/context.py b/core/runtime/context.py index 4bec1f4..7041e77 100644 --- a/core/runtime/context.py +++ b/core/runtime/context.py @@ -17,6 +17,11 @@ from core.foundation.device import resolve_device, resolve_dtype from core.foundation.layout import AlgebraSpec, GradeLayout from core.planning.planner import GradePlanner +from core.runtime.accessors import FULL_LAYOUT_MAX_N +from core.runtime.accessors import default_layout as _default_layout +from core.runtime.accessors import grade_indices as _grade_indices +from core.runtime.accessors import hermitian_signs as _hermitian_signs +from core.runtime.accessors import resolve_layout as _resolve_layout from core.runtime.projected import ProjectedProductMixin @@ -46,9 +51,10 @@ def __init__( self.spec = AlgebraSpec(self.p, self.q, self.r) self._device = torch.device(resolve_device(device) if str(device) == "auto" else device) self._dtype = resolve_dtype(dtype) - self.allow_full_layout_products = ( - self.n <= 8 if allow_full_layout_products is None else bool(allow_full_layout_products) + requested_full_layout = self.n <= FULL_LAYOUT_MAX_N if allow_full_layout_products is None else bool( + allow_full_layout_products ) + self.allow_full_layout_products = requested_full_layout and self.n <= FULL_LAYOUT_MAX_N self._default_grades = None if default_grades is None else normalize_grades(default_grades, self.n) self._default_layout: Optional[GradeLayout] = None self.planner = GradePlanner(self) @@ -67,22 +73,46 @@ def dtype(self) -> torch.dtype: def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: """Return a compact layout, or the full default layout when omitted.""" if grades is None: - if self._default_layout is None: - if self._default_grades is None: - if not self.allow_full_layout_products: - raise ValueError( - "AlgebraContext has no default layout. Declare active grades for high-dimensional use." - ) - grades = range(self.num_grades) - else: - grades = self._default_grades - self._default_layout = self.planner.layout(grades) - return self._default_layout + return self.default_layout() return self.planner.layout(grades) + def default_layout(self) -> GradeLayout: + """Return the default layout using the central full-layout fallback policy.""" + return _default_layout(self) + + def resolve_layout( + self, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + mv=None, + allow_full: bool = True, + warn_full: bool = True, + ) -> GradeLayout: + """Resolve static layout metadata for tensors or multivectors.""" + return _resolve_layout( + self, + layout=layout, + grades=grades, + mv=mv, + allow_full=allow_full, + warn_full=warn_full, + ) + def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: """Return canonical dense basis indices for ``grades`` without dense tables.""" - return self.planner.grade_indices(grades, device=self.device if device is None else device) + return _grade_indices(self, grades, device=self.device if device is None else device) + + def hermitian_signs( + self, + layout: Optional[GradeLayout] = None, + *, + grades: Optional[Iterable[int]] = None, + device=None, + dtype: Optional[torch.dtype] = None, + ) -> torch.Tensor: + """Return Hermitian signs for a dense or compact layout.""" + return _hermitian_signs(self, layout=layout, grades=grades, device=device, dtype=dtype) def bivector_squared_signs(self, *, device=None, dtype: Optional[torch.dtype] = None) -> torch.Tensor: """Return ``(e_ab)^2`` signs in canonical grade-2 layout order.""" diff --git a/core/runtime/metric.py b/core/runtime/metric.py index 2bfb384..b5375e7 100644 --- a/core/runtime/metric.py +++ b/core/runtime/metric.py @@ -11,13 +11,25 @@ the metric signature. """ +from typing import Iterable, Optional + import torch +from core.foundation.layout import GradeLayout from core.foundation.module import AlgebraLike +from core.runtime.accessors import compact_values +from core.runtime.accessors import hermitian_signs as _layout_hermitian_signs -def _hermitian_signs(algebra: AlgebraLike) -> torch.Tensor: - """Return the precomputed Hermitian sign tensor from the algebra. +def _hermitian_signs( + algebra: AlgebraLike, + layout: Optional[GradeLayout] = None, + *, + grades: Optional[Iterable[int]] = None, + device=None, + dtype: Optional[torch.dtype] = None, +) -> torch.Tensor: + """Return Hermitian sign tensor for a dense or compact layout. The Hermitian inner product on Cl(p,q) is: _H = sum_I (conj_sign_I * metric_sign_I) * a_I * b_I @@ -28,9 +40,7 @@ def _hermitian_signs(algebra: AlgebraLike) -> torch.Tensor: Returns: Sign tensor [Dim] with values +1, -1, or 0 (null blades). """ - if not hasattr(algebra, "_hermitian_signs"): - raise ValueError("Hermitian dense metrics require a dense algebra kernel.") - return algebra._hermitian_signs + return _layout_hermitian_signs(algebra, layout=layout, grades=grades, device=device, dtype=dtype) def inner_product(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: @@ -171,7 +181,18 @@ def clifford_conjugate(algebra: AlgebraLike, mv: torch.Tensor) -> torch.Tensor: return algebra.clifford_conjugation(mv) -def hermitian_inner_product(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def hermitian_inner_product( + algebra: AlgebraLike, + A: torch.Tensor, + B: torch.Tensor, + *, + layout: Optional[GradeLayout] = None, + left_layout: Optional[GradeLayout] = None, + right_layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + left_grades: Optional[Iterable[int]] = None, + right_grades: Optional[Iterable[int]] = None, +) -> torch.Tensor: """Hermitian inner product on Cl(p,q): _0. _H = Sum_I (conj_sign_I * metric_sign_I) * a_I * b_I @@ -189,13 +210,27 @@ def hermitian_inner_product(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tens Returns: Scalar inner product [..., 1]. """ - signs = _hermitian_signs(algebra) - if signs.dtype != A.dtype: - signs = signs.to(dtype=A.dtype) - return (signs * A * B).sum(dim=-1, keepdim=True) - - -def hermitian_norm(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: + A_values, B_values, resolved = _aligned_pair_values( + algebra, + A, + B, + layout=layout, + left_layout=left_layout, + right_layout=right_layout, + grades=grades, + left_grades=left_grades, + right_grades=right_grades, + ) + return _hermitian_inner_values(algebra, A_values, B_values, resolved) + + +def hermitian_norm( + algebra: AlgebraLike, + A: torch.Tensor, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, +) -> torch.Tensor: """Hermitian norm: ||A||_H = sqrt(|_H|). Always real and non-negative for any signature. @@ -209,11 +244,23 @@ def hermitian_norm(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: Returns: Norm [..., 1]. Always >= 0. """ - sq = hermitian_inner_product(algebra, A, A) + values, resolved = compact_values(algebra, A, layout=layout, grades=grades) + sq = _hermitian_inner_values(algebra, values, values, resolved) return torch.sqrt(torch.abs(sq)) -def hermitian_distance(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def hermitian_distance( + algebra: AlgebraLike, + A: torch.Tensor, + B: torch.Tensor, + *, + layout: Optional[GradeLayout] = None, + left_layout: Optional[GradeLayout] = None, + right_layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + left_grades: Optional[Iterable[int]] = None, + right_grades: Optional[Iterable[int]] = None, +) -> torch.Tensor: """Hermitian distance: d_H(A, B) = ||A - B||_H. Positive-definite metric distance for any signature. @@ -227,10 +274,34 @@ def hermitian_distance(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) - Returns: Distance [..., 1]. Always >= 0. """ - return hermitian_norm(algebra, A - B) + A_values, B_values, resolved = _aligned_pair_values( + algebra, + A, + B, + layout=layout, + left_layout=left_layout, + right_layout=right_layout, + grades=grades, + left_grades=left_grades, + right_grades=right_grades, + ) + diff = A_values - B_values + sq = _hermitian_inner_values(algebra, diff, diff, resolved) + return torch.sqrt(torch.abs(sq)) -def hermitian_angle(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: +def hermitian_angle( + algebra: AlgebraLike, + A: torch.Tensor, + B: torch.Tensor, + *, + layout: Optional[GradeLayout] = None, + left_layout: Optional[GradeLayout] = None, + right_layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + left_grades: Optional[Iterable[int]] = None, + right_grades: Optional[Iterable[int]] = None, +) -> torch.Tensor: """Hermitian angle between multivectors. cos(theta) = _H / (||A||_H * ||B||_H) @@ -243,12 +314,21 @@ def hermitian_angle(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> t Returns: Angle in radians [..., 1]. """ - signs = _hermitian_signs(algebra) - if signs.dtype != A.dtype: - signs = signs.to(dtype=A.dtype) - ip = (signs * A * B).sum(dim=-1, keepdim=True) - sq_a = (signs * A * A).sum(dim=-1, keepdim=True) - sq_b = (signs * B * B).sum(dim=-1, keepdim=True) + A_values, B_values, resolved = _aligned_pair_values( + algebra, + A, + B, + layout=layout, + left_layout=left_layout, + right_layout=right_layout, + grades=grades, + left_grades=left_grades, + right_grades=right_grades, + ) + signs = _signs_like(algebra, resolved, A_values, B_values) + ip = (signs * A_values * B_values).sum(dim=-1, keepdim=True) + sq_a = (signs * A_values * A_values).sum(dim=-1, keepdim=True) + sq_b = (signs * B_values * B_values).sum(dim=-1, keepdim=True) # Use sqrt(sq_a * sq_b) instead of sqrt(sq_a)*sqrt(sq_b) to avoid # float32 precision loss from two separate sqrt operations. denom = torch.sqrt(torch.abs(sq_a) * torch.abs(sq_b)).clamp(min=algebra.eps) @@ -257,7 +337,14 @@ def hermitian_angle(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> t return torch.acos(cos_theta) -def grade_hermitian_norm(algebra: AlgebraLike, A: torch.Tensor, grade: int) -> torch.Tensor: +def grade_hermitian_norm( + algebra: AlgebraLike, + A: torch.Tensor, + grade: int, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, +) -> torch.Tensor: """Hermitian norm restricted to a single grade. ||_k||_H = sqrt(Sum_{I: |I|=k} a_I**2) @@ -273,11 +360,20 @@ def grade_hermitian_norm(algebra: AlgebraLike, A: torch.Tensor, grade: int) -> t Returns: Grade-specific norm [..., 1]. """ - A_k = algebra.grade_projection(A, grade) - return hermitian_norm(algebra, A_k) + values, source_layout = compact_values(algebra, A, layout=layout, grades=grades) + grade_layout = algebra.layout((int(grade),)) + grade_values = grade_layout.convert(values, source_layout) + sq = _hermitian_inner_values(algebra, grade_values, grade_values, grade_layout) + return torch.sqrt(torch.abs(sq)) -def hermitian_grade_spectrum(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: +def hermitian_grade_spectrum( + algebra: AlgebraLike, + A: torch.Tensor, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, +) -> torch.Tensor: """Full Hermitian grade spectrum. Returns |_H| for each grade k = 0, ..., n. @@ -290,17 +386,70 @@ def hermitian_grade_spectrum(algebra: AlgebraLike, A: torch.Tensor) -> torch.Ten Returns: Grade energies [..., n+1]. Each entry >= 0. """ - signs = _hermitian_signs(algebra) - if signs.dtype != A.dtype: - signs = signs.to(dtype=A.dtype) + values, source_layout = compact_values(algebra, A, layout=layout, grades=grades) spectrum = [] for k in range(algebra.n + 1): - A_k = algebra.grade_projection(A, k) - sq = (signs * A_k * A_k).sum(dim=-1, keepdim=True) + grade_layout = algebra.layout((k,)) + grade_values = grade_layout.convert(values, source_layout) + sq = _hermitian_inner_values(algebra, grade_values, grade_values, grade_layout) spectrum.append(torch.abs(sq)) return torch.cat(spectrum, dim=-1) +def _aligned_pair_values( + algebra: AlgebraLike, + A, + B, + *, + layout: Optional[GradeLayout] = None, + left_layout: Optional[GradeLayout] = None, + right_layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + left_grades: Optional[Iterable[int]] = None, + right_grades: Optional[Iterable[int]] = None, +) -> tuple[torch.Tensor, torch.Tensor, GradeLayout]: + """Compact two values into one static layout without dense materialization.""" + shared_left_layout = left_layout if left_layout is not None else layout + shared_right_layout = right_layout if right_layout is not None else layout + shared_left_grades = left_grades if left_grades is not None else grades + shared_right_grades = right_grades if right_grades is not None else grades + + A_values, A_layout = compact_values(algebra, A, layout=shared_left_layout, grades=shared_left_grades) + B_values, B_layout = compact_values(algebra, B, layout=shared_right_layout, grades=shared_right_grades) + resolved = A_layout if A_layout == B_layout else _union_layout(algebra, A_layout, B_layout) + if A_layout != resolved: + A_values = resolved.convert(A_values, A_layout) + if B_layout != resolved: + B_values = resolved.convert(B_values, B_layout) + return A_values, B_values, resolved + + +def _union_layout(algebra: AlgebraLike, left: GradeLayout, right: GradeLayout) -> GradeLayout: + basis = set(left.basis_indices).union(right.basis_indices) + grades = sorted({index.bit_count() for index in basis}) + return algebra.layout(grades) + + +def _hermitian_inner_values( + algebra: AlgebraLike, + A_values: torch.Tensor, + B_values: torch.Tensor, + layout: GradeLayout, +) -> torch.Tensor: + signs = _signs_like(algebra, layout, A_values, B_values) + return (signs * A_values * B_values).sum(dim=-1, keepdim=True) + + +def _signs_like( + algebra: AlgebraLike, + layout: GradeLayout, + A_values: torch.Tensor, + B_values: torch.Tensor, +) -> torch.Tensor: + dtype = torch.promote_types(A_values.dtype, B_values.dtype) + return _hermitian_signs(algebra, layout=layout, device=A_values.device, dtype=dtype) + + def signature_trace_form(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Signature-aware trace form: <~A B>_0. diff --git a/core/runtime/multivector.py b/core/runtime/multivector.py index fe885ec..5ff6a46 100644 --- a/core/runtime/multivector.py +++ b/core/runtime/multivector.py @@ -13,6 +13,8 @@ from core.foundation.layout import GradeLayout from core.foundation.module import AlgebraLike +from core.runtime.accessors import as_multivector as _as_multivector +from core.runtime.accessors import materialize_dense class Multivector: @@ -56,20 +58,63 @@ def __init__( self._tensor = None self.values = values + @classmethod + def from_tensor( + cls, + algebra: AlgebraLike, + tensor: torch.Tensor, + *, + grades=None, + layout: GradeLayout = None, + ) -> Multivector: + """Wrap dense coefficients or compact coefficients with declared layout metadata.""" + if layout is None and grades is None: + return cls(algebra, tensor) + return _as_multivector(algebra, tensor, layout=layout, grades=grades) + + @classmethod + def from_compact( + cls, + algebra: AlgebraLike, + values: torch.Tensor, + *, + grades=None, + layout: GradeLayout = None, + ) -> Multivector: + """Create a compact multivector from active lane values.""" + if layout is None and grades is None: + raise ValueError("layout or grades is required for compact multivectors") + resolved = algebra.resolve_layout(layout=layout, grades=grades, warn_full=False) + return cls(algebra, values=values, layout=resolved) + @classmethod def from_vectors(cls, algebra: AlgebraLike, vectors: torch.Tensor) -> Multivector: - """Promotes vectors to multivectors (Grade 1).""" - return cls(algebra, algebra.embed_vector(vectors)) + """Promote vectors to compact grade-1 multivectors.""" + layout = algebra.layout((1,)) + if vectors.shape[-1] != layout.dim: + raise ValueError(f"vectors last dimension must be {layout.dim}, got {vectors.shape[-1]}") + return cls(algebra, values=vectors, layout=layout) @classmethod def scalar( cls, algebra: AlgebraLike, value: float | torch.Tensor, batch_shape: tuple[int, ...] = () ) -> Multivector: """Creates a scalar multivector (grade 0 only).""" - dim = 2**algebra.n - t = torch.zeros(*batch_shape, dim, device=algebra.device, dtype=algebra.dtype) - t[..., 0] = value - return cls(algebra, t) + layout = algebra.layout((0,)) + values = torch.as_tensor(value, device=algebra.device, dtype=algebra.dtype) + if batch_shape: + target_shape = torch.Size(batch_shape) + if values.ndim == 0: + values = values.expand(*batch_shape).clone() + elif values.shape == torch.Size((*batch_shape, 1)): + return cls(algebra, values=values, layout=layout) + elif values.shape != target_shape: + values = values.expand(*batch_shape).clone() + if values.ndim == 0: + values = values.reshape(1) + elif values.shape[-1] != 1: + values = values.unsqueeze(-1) + return cls(algebra, values=values, layout=layout) def __repr__(self): storage = "compact" if self.is_compact else "dense" @@ -89,7 +134,7 @@ def tensor(self) -> torch.Tensor: return self._tensor # Do not call this inside core operations that can preserve compact # ``values`` and ``layout``; materialization belongs at API boundaries. - return self.layout.dense(self.values) + return materialize_dense(self.algebra, self) @tensor.setter def tensor(self, value: torch.Tensor) -> None: @@ -102,6 +147,26 @@ def is_compact(self) -> bool: """Whether this multivector stores compact grade lanes.""" return self.layout is not None + @property + def is_dense(self) -> bool: + """Whether this multivector stores dense coefficients.""" + return not self.is_compact + + @property + def grades(self) -> tuple[int, ...] | None: + """Active grades when compact metadata is available.""" + return None if self.layout is None else self.layout.grades + + @property + def lane_count(self) -> int: + """Number of stored coefficient lanes.""" + return self.coefficients.shape[-1] + + @property + def storage(self) -> str: + """Storage mode name.""" + return "compact" if self.is_compact else "dense" + @property def coefficients(self) -> torch.Tensor: """Return the active storage tensor without dense materialization.""" @@ -113,7 +178,7 @@ def dense(self) -> Multivector: def compact(self, grades) -> Multivector: """Return a compact-storage multivector containing ``grades``.""" - layout = self.algebra.planner.layout(grades) + layout = self.algebra.layout(grades) return self.with_layout(layout) def with_layout(self, layout: GradeLayout) -> Multivector: @@ -312,6 +377,8 @@ def clifford_conjugation(self) -> Multivector: def dual(self) -> Multivector: """Hodge dual: maps grade-k to grade-(n-k).""" + # Dense-only algebra APIs below intentionally cross the dense boundary. + # Planned high-dimensional paths should use grade-declared primitives. return self._wrap(self.algebra.dual(self.tensor)) def inverse(self) -> Multivector: diff --git a/core/runtime/projected.py b/core/runtime/projected.py index 0725302..8947a97 100644 --- a/core/runtime/projected.py +++ b/core/runtime/projected.py @@ -35,6 +35,8 @@ def projected_product( return_layout: bool = False, ): """Compute a declared grade-restricted product through a static executor.""" + left_layout = self._declared_layout(left_grades, left_layout) + right_layout = self._declared_layout(right_grades, right_layout) if not left_compact and left_layout is not None and A.shape[-1] == left_layout.dim: left_compact = left_layout.dim != self.dim if not right_compact and right_layout is not None and B.shape[-1] == right_layout.dim: @@ -91,3 +93,13 @@ def projected_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs): def projected_anti_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs): """Projected anti-commutator convenience wrapper.""" return self.projected_product(A, B, op="anti_commutator", **kwargs) + + def _declared_layout(self, grades, layout): + if layout is not None: + return layout + if grades is not None: + return self.layout(grades) + default_grades = getattr(self, "_default_grades", None) + if default_grades is None: + return None + return self.layout(default_grades) diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index 22007af..539143c 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -457,8 +457,49 @@ def test_multivector_compact_addition_merges_layouts_without_dense_materializati assert torch.allclose(result.values, vector_values + bivector_values) +def test_context_default_grades_drive_compact_product_without_callsite_metadata(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32, default_grades=(1,)) + vector_layout = algebra.layout() + left = torch.zeros(1, vector_layout.dim) + right = torch.zeros(1, vector_layout.dim) + left[0, 0] = 1.0 + right[0, 0] = 1.0 + + values, output_layout = algebra.geometric_product( + left, + right, + compact_output=True, + return_layout=True, + ) + + assert vector_layout.grades == (1,) + assert output_layout.grades == (0, 2) + assert torch.allclose(values[0, output_layout.basis_indices.index(0)], torch.tensor(1.0)) + + +def test_context_declared_grades_infer_compact_operand_shapes(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + vector_layout = algebra.layout((1,)) + left = torch.zeros(1, vector_layout.dim) + right = torch.zeros(1, vector_layout.dim) + left[0, 0] = 1.0 + right[0, 0] = 1.0 + + values, output_layout = algebra.projected_geometric_product( + left, + right, + left_grades=(1,), + right_grades=(1,), + compact_output=True, + return_layout=True, + ) + + assert output_layout.grades == (0, 2) + assert torch.allclose(values[0, output_layout.basis_indices.index(0)], torch.tensor(1.0)) + + def test_high_dim_context_requires_declared_layout_for_products(): - algebra = make_algebra(9, 0, 0, device=DEVICE, dtype=torch.float32) + algebra = make_algebra(13, 0, 0, device=DEVICE, dtype=torch.float32) A = torch.zeros(1, algebra.dim) B = torch.zeros(1, algebra.dim) @@ -469,6 +510,16 @@ def test_high_dim_context_requires_declared_layout_for_products(): algebra.reverse(A) +def test_context_warns_for_implicit_full_layout_fallback_between_eight_and_twelve(): + context = make_algebra(9, 0, 0, kernel="context", device=DEVICE, dtype=torch.float32) + + with pytest.warns(RuntimeWarning, match="implicit full Cl\\(9,0,0\\) layout"): + layout = context.layout() + + assert layout.grades == tuple(range(context.n + 1)) + assert context.allow_full_layout_products + + def test_low_dim_context_can_use_full_layout_fallback(): context = make_algebra(4, 0, 0, kernel="context", device=DEVICE, dtype=torch.float64) dense = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) diff --git a/tests/test_hermitian_metrics.py b/tests/test_hermitian_metrics.py index bfba6f1..a85d58c 100644 --- a/tests/test_hermitian_metrics.py +++ b/tests/test_hermitian_metrics.py @@ -3,7 +3,9 @@ import pytest import torch +from core.config import make_algebra from core.runtime.algebra import CliffordAlgebra +from core.runtime.multivector import Multivector pytestmark = pytest.mark.unit from core.runtime.metric import ( @@ -143,6 +145,19 @@ def test_has_negative_signs(self, algebra_minkowski): has_negative = (signs < 0).any() assert has_negative, "Cl(2,1) should have negative signs" + def test_compact_context_matches_dense_active_lanes(self): + dense = CliffordAlgebra(3, 1, 0, device="cpu", dtype=torch.float64) + context = make_algebra(3, 1, 0, kernel="context", device="cpu", dtype=torch.float64) + layout = context.layout((1, 2)) + generator = torch.Generator(device="cpu").manual_seed(719) + A = torch.randn(5, layout.dim, dtype=torch.float64, generator=generator) + B = torch.randn(5, layout.dim, dtype=torch.float64, generator=generator) + + actual = hermitian_inner_product(context, A, B, layout=layout) + expected = hermitian_inner_product(dense, layout.dense(A), layout.dense(B)) + + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + class TestHermitianNorm: def test_non_negative(self, algebra_minkowski): @@ -279,6 +294,28 @@ def test_conformal_spectrum(self, algebra_conformal): assert spec.shape == (algebra_conformal.n + 1,) assert (spec >= -1e-6).all() + def test_compact_spectrum_fills_inactive_grades(self): + context = make_algebra(5, 0, 0, kernel="context", device="cpu", dtype=torch.float32) + layout = context.layout((1,)) + values = torch.ones(2, layout.dim) + + spec = hermitian_grade_spectrum(context, values, layout=layout) + + assert spec.shape == (2, context.n + 1) + assert torch.allclose(spec[:, 0], torch.zeros(2)) + assert torch.allclose(spec[:, 1], torch.full((2,), float(context.n))) + assert torch.allclose(spec[:, 2:], torch.zeros(2, context.n - 1)) + + def test_compact_multivector_norm_uses_layout_without_dense_materialization(self): + context = make_algebra(9, 0, 0, kernel="context", device="cpu", dtype=torch.float32) + mv = Multivector.from_vectors(context, torch.ones(3, context.n)) + + norm = hermitian_norm(context, mv) + + assert mv.is_compact + assert norm.shape == (3, 1) + assert torch.allclose(norm, torch.full((3, 1), context.n**0.5)) + class TestSignatureTraceForm: def test_matches_standard_for_euclidean(self, algebra_3d): diff --git a/tests/test_multivector.py b/tests/test_multivector.py index f3fbbf1..013630c 100644 --- a/tests/test_multivector.py +++ b/tests/test_multivector.py @@ -28,7 +28,10 @@ def rand_mv(alg, rng, batch=4): def test_from_vectors(alg): v = torch.randn(4, 3) mv = Multivector.from_vectors(alg, v) - assert mv.shape[-1] == 2**alg.n + assert mv.is_compact + assert mv.grades == (1,) + assert mv.shape[-1] == alg.n + assert mv.tensor.shape[-1] == 2**alg.n def test_scalar(alg): From 96e811617b89321b8ec0ae770ebb90fee47e192f Mon Sep 17 00:00:00 2001 From: Concode0 Date: Wed, 13 May 2026 21:27:30 +0900 Subject: [PATCH 27/45] refactor: enforce core grade planning policy --- core/__init__.py | 4 + core/config.py | 6 ++ core/foundation/module.py | 2 + core/planning/__init__.py | 4 + core/planning/planner.py | 20 +++-- core/planning/policy.py | 180 ++++++++++++++++++++++++++++++++++++++ core/runtime/__init__.py | 4 +- core/runtime/accessors.py | 44 +++------- core/runtime/algebra.py | 90 ++----------------- core/runtime/context.py | 97 ++------------------ core/runtime/projected.py | 90 ++++++++++++++++++- tests/test_grade_plan.py | 87 ++++++++++++++++-- 12 files changed, 407 insertions(+), 221 deletions(-) create mode 100644 core/planning/policy.py diff --git a/core/__init__.py b/core/__init__.py index 58f54b5..1361d69 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -29,6 +29,7 @@ from .planning.flow import GradeFlow from .planning.layouts import ProductRequest, build_product_request from .planning.planner import GradePlanner +from .planning.policy import DEFAULT_PLANNING_LIMITS, PlanCost, PlanningLimits from .planning.product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan from .planning.routes import ( ModuleOptimizationIssue, @@ -86,6 +87,9 @@ "AlgebraSpec", "GradeLayout", "GradePlanner", + "PlanningLimits", + "PlanCost", + "DEFAULT_PLANNING_LIMITS", "make_algebra", "make_algebra_from_config", # device / validation diff --git a/core/config.py b/core/config.py index 79fa6ed..b1d121f 100644 --- a/core/config.py +++ b/core/config.py @@ -16,6 +16,7 @@ from core.foundation.device import resolve_device, resolve_dtype from core.foundation.module import AlgebraLike +from core.planning.policy import PlanningLimits from core.runtime.algebra import CliffordAlgebra from core.runtime.context import AlgebraContext @@ -37,6 +38,7 @@ class AlgebraConfig: fixed_iterations: Optional[int] = None default_grades: Optional[tuple[int, ...]] = None allow_full_layout_products: Optional[bool] = None + planning_limits: Optional[PlanningLimits] = None @classmethod def from_mapping(cls, config: Mapping[str, Any], **overrides) -> "AlgebraConfig": @@ -72,6 +74,7 @@ def make_algebra( fixed_iterations: Optional[int] = None, default_grades: Optional[Iterable[int]] = None, allow_full_layout_products: Optional[bool] = None, + planning_limits: Optional[PlanningLimits] = None, **deprecated_options, ) -> AlgebraLike: """Construct a dense low-dimensional algebra or high-dimensional planning context.""" @@ -98,6 +101,7 @@ def make_algebra( exp_policy=exp_policy, fixed_iterations=fixed_iterations, allow_large_dense=kernel == "dense", + planning_limits=planning_limits, ) return AlgebraContext( @@ -108,6 +112,7 @@ def make_algebra( dtype=resolved_dtype, default_grades=default_grades, allow_full_layout_products=allow_full_layout_products, + planning_limits=planning_limits, ) @@ -126,6 +131,7 @@ def make_algebra_from_config(config: Mapping[str, Any], **overrides) -> AlgebraL fixed_iterations=algebra_config.fixed_iterations, default_grades=algebra_config.default_grades, allow_full_layout_products=algebra_config.allow_full_layout_products, + planning_limits=algebra_config.planning_limits, ) diff --git a/core/foundation/module.py b/core/foundation/module.py index 4a310bf..2dc46fc 100644 --- a/core/foundation/module.py +++ b/core/foundation/module.py @@ -28,6 +28,8 @@ class AlgebraLike(Protocol): eps: float eps_sq: float planner: object + planning_limits: object + allow_full_layout_products: bool @property def device(self): diff --git a/core/planning/__init__.py b/core/planning/__init__.py index d98a57c..5859ce1 100644 --- a/core/planning/__init__.py +++ b/core/planning/__init__.py @@ -10,6 +10,7 @@ from .flow import GradeFlow from .layouts import ProductRequest, build_product_request from .planner import GradePlanner +from .policy import DEFAULT_PLANNING_LIMITS, PlanCost, PlanningLimits from .product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan from .routes import ( ModuleOptimizationIssue, @@ -29,6 +30,9 @@ "GradeProductPlan", "GradePlanTree", "GradePlanner", + "PlanningLimits", + "PlanCost", + "DEFAULT_PLANNING_LIMITS", "ModuleOptimizationIssue", "ModuleOptimizationPlan", "ModuleOptimizationReport", diff --git a/core/planning/planner.py b/core/planning/planner.py index d4661a2..2e596d1 100644 --- a/core/planning/planner.py +++ b/core/planning/planner.py @@ -14,6 +14,13 @@ from core.foundation.basis import operation_coefficient from core.foundation.layout import AlgebraSpec, GradeLayout from core.planning.layouts import ProductRequest, build_product_request, normalize_product_op +from core.planning.policy import ( + full_layout_allowed, + validate_layout_cost, + validate_product_request, + validate_unary_request, + warn_full_layout_fallback, +) from core.planning.product import GradeProductExecutor, build_grade_product_plan_from_request from core.planning.tree import build_grade_plan_tree from core.planning.unary import ( @@ -23,7 +30,6 @@ build_unary_request, normalize_unary_op, ) -from core.runtime.accessors import FULL_LAYOUT_MAX_N, warn_full_layout_fallback class GradePlanner: @@ -42,7 +48,7 @@ def __init__(self, algebra): def layout(self, grades): """Return the compact layout for ``grades``.""" - return self.spec.layout(grades) + return validate_layout_cost(self.algebra, self.spec.layout(grades)) def full_layout(self) -> GradeLayout: """Return the full dense basis layout.""" @@ -138,7 +144,7 @@ def product_request( self._implicit_full_operand(right, grades=right_grades, layout=right_layout, compact=right_compact) ): warn_full_layout_fallback(self.algebra) - return build_product_request( + request = build_product_request( self.spec, left, right, @@ -153,6 +159,8 @@ def product_request( right_compact=right_compact, full_layout_allowed=self._full_layout_allowed(), ) + validate_product_request(self.algebra, request) + return request def product_executor_for_request(self, request: ProductRequest, *, cache: bool = True) -> GradeProductExecutor: """Return an executor for an already normalized product request.""" @@ -191,7 +199,7 @@ def unary_request( input_grades = self._default_operand_grades(input_grades, input_layout) if self._implicit_full_operand(values, grades=input_grades, layout=input_layout, compact=input_compact): warn_full_layout_fallback(self.algebra) - return build_unary_request( + request = build_unary_request( self.spec, values, op=op, @@ -202,6 +210,8 @@ def unary_request( input_compact=input_compact, full_layout_allowed=self._full_layout_allowed(), ) + validate_unary_request(self.algebra, request) + return request def unary_executor( self, @@ -263,7 +273,7 @@ def _unary_cache_key(self, executor: GradeUnaryExecutor) -> tuple[object, ...]: ) def _full_layout_allowed(self) -> bool: - return bool(getattr(self.algebra, "allow_full_layout_products", True)) and self.spec.n <= FULL_LAYOUT_MAX_N + return full_layout_allowed(self.algebra, self.spec) def _implicit_full_operand(self, tensor: torch.Tensor, *, grades, layout, compact: bool) -> bool: return ( diff --git a/core/planning/policy.py b/core/planning/policy.py new file mode 100644 index 0000000..00a821b --- /dev/null +++ b/core/planning/policy.py @@ -0,0 +1,180 @@ +# Versor: Universal Geometric Algebra Neural Network +# Copyright (C) 2026 Eunkyum Kim +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# + +"""Static planning policy for layout fallback and plan-cost diagnostics.""" + +from __future__ import annotations + +import warnings +from dataclasses import dataclass +from typing import Optional + +from core.foundation.layout import AlgebraSpec, GradeLayout + +FULL_LAYOUT_WARN_N = 8 +FULL_LAYOUT_MAX_N = 12 + + +@dataclass(frozen=True) +class PlanningLimits: + """Static cost limits for planned grade execution. + + ``max_lanes`` protects compact tensor width. ``max_pairs`` protects the + gather/reduce interaction count generated by a bilinear product plan. + """ + + warn_lanes: int = 2048 + max_lanes: int = 4096 + warn_pairs: int = 1_000_000 + max_pairs: int = 8_000_000 + + +@dataclass(frozen=True) +class PlanCost: + """Static cost summary for a layout or planned operation.""" + + spec: AlgebraSpec + kind: str + op: str + left_lanes: int = 0 + right_lanes: int = 0 + output_lanes: int = 0 + pair_count: int = 0 + input_grades: tuple[int, ...] = () + left_grades: tuple[int, ...] = () + right_grades: tuple[int, ...] = () + output_grades: tuple[int, ...] = () + + @property + def max_lanes(self) -> int: + return max(self.left_lanes, self.right_lanes, self.output_lanes) + + +DEFAULT_PLANNING_LIMITS = PlanningLimits() +_WARNED_FULL_LAYOUT_SIGNATURES: set[tuple[int, int, int]] = set() +_WARNED_PLAN_COSTS: set[tuple[object, ...]] = set() + + +def planning_limits_for(algebra) -> PlanningLimits: + """Return per-algebra planning limits, falling back to defaults.""" + return getattr(algebra, "planning_limits", DEFAULT_PLANNING_LIMITS) + + +def full_layout_allowed(algebra, spec: AlgebraSpec) -> bool: + """Return whether implicit full-layout planning is enabled for this algebra.""" + return bool(getattr(algebra, "allow_full_layout_products", False)) and spec.n <= FULL_LAYOUT_MAX_N + + +def warn_full_layout_fallback(algebra) -> None: + """Warn once per signature when implicit full layout is used at n>=8.""" + if getattr(algebra, "n", 0) < FULL_LAYOUT_WARN_N: + return + signature = (int(algebra.p), int(algebra.q), int(algebra.r)) + if signature in _WARNED_FULL_LAYOUT_SIGNATURES: + return + _WARNED_FULL_LAYOUT_SIGNATURES.add(signature) + warnings.warn( + f"Using implicit full Cl({algebra.p},{algebra.q},{algebra.r}) layout at n={algebra.n}. " + "Declare active grades or default_grades to avoid full-layout planning.", + RuntimeWarning, + stacklevel=3, + ) + + +def validate_layout_cost(algebra, layout: GradeLayout, *, role: str = "layout") -> GradeLayout: + """Validate one compact layout against static lane limits.""" + cost = PlanCost( + spec=layout.spec, + kind="layout", + op=role, + output_lanes=layout.dim, + output_grades=layout.grades, + ) + validate_plan_cost(algebra, cost) + return layout + + +def product_plan_cost(request) -> PlanCost: + """Return static cost summary for a product request.""" + return PlanCost( + spec=request.spec, + kind="product", + op=request.op, + left_lanes=request.left_layout.dim, + right_lanes=request.right_layout.dim, + output_lanes=request.output_layout.dim, + pair_count=request.left_layout.dim * request.right_layout.dim, + left_grades=request.left_grades, + right_grades=request.right_grades, + output_grades=request.output_grades, + ) + + +def unary_plan_cost(request) -> PlanCost: + """Return static cost summary for a unary request.""" + return PlanCost( + spec=request.spec, + kind="unary", + op=request.op, + left_lanes=request.input_layout.dim, + output_lanes=request.output_layout.dim, + input_grades=request.input_grades, + output_grades=request.output_grades, + ) + + +def validate_product_request(algebra, request) -> None: + """Validate product request static cost.""" + validate_plan_cost(algebra, product_plan_cost(request)) + + +def validate_unary_request(algebra, request) -> None: + """Validate unary request static cost.""" + validate_plan_cost(algebra, unary_plan_cost(request)) + + +def validate_plan_cost(algebra, cost: PlanCost, *, limits: Optional[PlanningLimits] = None) -> None: + """Raise on excessive static cost and warn once near configured limits.""" + limits = planning_limits_for(algebra) if limits is None else limits + errors = [] + if cost.max_lanes > limits.max_lanes: + errors.append(f"active lanes {cost.max_lanes} exceed max_lanes={limits.max_lanes}") + if cost.pair_count > limits.max_pairs: + errors.append(f"basis interactions {cost.pair_count} exceed max_pairs={limits.max_pairs}") + if errors: + detail = "; ".join(errors) + raise ValueError(f"Static {cost.kind} plan for {cost.op} is too large: {detail}. Declare fewer grades.") + + warnings_to_emit = [] + if cost.max_lanes >= limits.warn_lanes: + warnings_to_emit.append(f"active lanes {cost.max_lanes} are near max_lanes={limits.max_lanes}") + if cost.pair_count >= limits.warn_pairs: + warnings_to_emit.append(f"basis interactions {cost.pair_count} are near max_pairs={limits.max_pairs}") + if warnings_to_emit: + _warn_plan_cost_once(cost, "; ".join(warnings_to_emit)) + + +def _warn_plan_cost_once(cost: PlanCost, detail: str) -> None: + key = ( + cost.spec, + cost.kind, + cost.op, + cost.input_grades, + cost.left_grades, + cost.right_grades, + cost.output_grades, + cost.max_lanes, + cost.pair_count, + ) + if key in _WARNED_PLAN_COSTS: + return + _WARNED_PLAN_COSTS.add(key) + warnings.warn( + f"Static {cost.kind} plan for {cost.op} is large: {detail}.", + RuntimeWarning, + stacklevel=3, + ) diff --git a/core/runtime/__init__.py b/core/runtime/__init__.py index 3aa28ba..0c108e6 100644 --- a/core/runtime/__init__.py +++ b/core/runtime/__init__.py @@ -11,13 +11,13 @@ from .algebra import CliffordAlgebra from .context import AlgebraContext from .multivector import Multivector -from .projected import ProjectedProductMixin +from .projected import AlgebraRuntimeMixin __all__ = [ "AlgebraContext", + "AlgebraRuntimeMixin", "CliffordAlgebra", "Multivector", - "ProjectedProductMixin", "as_multivector", "compact_values", "grade_indices", diff --git a/core/runtime/accessors.py b/core/runtime/accessors.py index c827e3c..a70fa12 100644 --- a/core/runtime/accessors.py +++ b/core/runtime/accessors.py @@ -9,19 +9,13 @@ from __future__ import annotations -import warnings from typing import Iterable, Optional import torch from core.foundation.basis import normalize_grades, operation_coefficient, reverse_sign from core.foundation.layout import AlgebraSpec, GradeLayout - -FULL_LAYOUT_WARN_N = 8 -FULL_LAYOUT_MAX_N = 12 -_FULL_LAYOUT_WARN_N = FULL_LAYOUT_WARN_N -_FULL_LAYOUT_MAX_N = FULL_LAYOUT_MAX_N -_WARNED_FULL_LAYOUT_SIGNATURES: set[tuple[int, int, int]] = set() +from core.planning.policy import FULL_LAYOUT_MAX_N, validate_layout_cost, warn_full_layout_fallback def resolve_layout( @@ -39,37 +33,37 @@ def resolve_layout( _check_layout_spec(spec, layout, "layout") if grades is not None and layout.grades != normalize_grades(grades, spec.n, name="grades"): raise ValueError("layout and grades disagree") - return layout + return validate_layout_cost(algebra, layout) if grades is not None: - return spec.layout(grades) + return validate_layout_cost(algebra, spec.layout(grades)) if _is_multivector(mv) and getattr(mv, "layout", None) is not None: mv_layout = mv.layout _check_layout_spec(spec, mv_layout, "mv.layout") - return mv_layout + return validate_layout_cost(algebra, mv_layout) default_grades = getattr(algebra, "_default_grades", None) if default_grades is not None: cached = getattr(algebra, "_default_layout", None) if cached is not None: _check_layout_spec(spec, cached, "default_layout") - return cached + return validate_layout_cost(algebra, cached, role="default_layout") resolved = spec.layout(default_grades) if hasattr(algebra, "_default_layout"): algebra._default_layout = resolved - return resolved + return validate_layout_cost(algebra, resolved, role="default_layout") if not allow_full or not bool(getattr(algebra, "allow_full_layout_products", True)): raise ValueError("No grade layout is available. Declare active grades or configure default_grades.") - if spec.n > _FULL_LAYOUT_MAX_N: + if spec.n > FULL_LAYOUT_MAX_N: raise ValueError( - f"Implicit full Cl({spec.p},{spec.q},{spec.r}) layout is disabled for n>{_FULL_LAYOUT_MAX_N}. " + f"Implicit full Cl({spec.p},{spec.q},{spec.r}) layout is disabled for n>{FULL_LAYOUT_MAX_N}. " "Declare active grades or configure default_grades." ) if warn_full: warn_full_layout_fallback(algebra) - return spec.layout(range(spec.n + 1)) + return validate_layout_cost(algebra, spec.layout(range(spec.n + 1)), role="full_layout") def default_layout(algebra) -> GradeLayout: @@ -187,22 +181,6 @@ def as_multivector( return Multivector(algebra, tensor=value, layout=resolved) -def warn_full_layout_fallback(algebra) -> None: - """Warn once per signature when implicit full layout is used at n>=8.""" - if getattr(algebra, "n", 0) < _FULL_LAYOUT_WARN_N: - return - signature = (int(algebra.p), int(algebra.q), int(algebra.r)) - if signature in _WARNED_FULL_LAYOUT_SIGNATURES: - return - _WARNED_FULL_LAYOUT_SIGNATURES.add(signature) - warnings.warn( - f"Using implicit full Cl({algebra.p},{algebra.q},{algebra.r}) layout at n={algebra.n}. " - "Declare active grades or default_grades to avoid full-layout planning.", - RuntimeWarning, - stacklevel=3, - ) - - def _hermitian_sign_for_index(algebra, index: int) -> float: grade = int(index).bit_count() grade_sign = -1.0 if grade % 2 else 1.0 @@ -225,9 +203,9 @@ def _check_algebra(expected, actual) -> None: def _check_dense_materialization_allowed(algebra) -> None: if not bool(getattr(algebra, "allow_full_layout_products", True)): raise ValueError("Dense materialization is disabled for this algebra. Keep compact values.") - if getattr(algebra, "n", 0) > _FULL_LAYOUT_MAX_N: + if getattr(algebra, "n", 0) > FULL_LAYOUT_MAX_N: raise ValueError( - f"Dense materialization is disabled for n>{_FULL_LAYOUT_MAX_N}. " + f"Dense materialization is disabled for n>{FULL_LAYOUT_MAX_N}. " "Keep compact values or declare a smaller active layout." ) warn_full_layout_fallback(algebra) diff --git a/core/runtime/algebra.py b/core/runtime/algebra.py index 5282530..7cf9667 100644 --- a/core/runtime/algebra.py +++ b/core/runtime/algebra.py @@ -12,21 +12,17 @@ """ import math -from typing import Iterable, Optional +from typing import Optional import torch import torch.nn as nn -from core.foundation.layout import GradeLayout from core.foundation.validation import check_multivector -from core.runtime.accessors import default_layout as _default_layout -from core.runtime.accessors import grade_indices as _grade_indices -from core.runtime.accessors import hermitian_signs as _hermitian_signs -from core.runtime.accessors import resolve_layout as _resolve_layout -from core.runtime.projected import ProjectedProductMixin +from core.planning.policy import DEFAULT_PLANNING_LIMITS, PlanningLimits +from core.runtime.projected import AlgebraRuntimeMixin -class CliffordAlgebra(ProjectedProductMixin, nn.Module): +class CliffordAlgebra(AlgebraRuntimeMixin, nn.Module): """Differentiable Clifford algebra kernel with memory-optimized blocked accumulation. Extends ``nn.Module`` so that all Cayley tables are registered as @@ -57,6 +53,7 @@ def __init__( exp_policy: str = "balanced", fixed_iterations: Optional[int] = None, allow_large_dense: bool = False, + planning_limits: Optional[PlanningLimits] = None, ): """Initialize the algebra and cache the Cayley table. @@ -93,6 +90,7 @@ def __init__( self.n = p + q + r self.dim = 2**self.n self.allow_full_layout_products = True + self.planning_limits = DEFAULT_PLANNING_LIMITS if planning_limits is None else planning_limits # Exp regime: dispatch at init if p == 0 or q == 0: @@ -193,50 +191,6 @@ def dtype(self) -> torch.dtype: """ return self.cayley_signs.dtype - def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: - """Return a compact grade layout, or the full dense layout when omitted.""" - if grades is None: - return self.default_layout() - return self.planner.layout(grades) - - def default_layout(self) -> GradeLayout: - """Return the default layout using the central full-layout fallback policy.""" - return _default_layout(self) - - def resolve_layout( - self, - *, - layout: Optional[GradeLayout] = None, - grades: Optional[Iterable[int]] = None, - mv=None, - allow_full: bool = True, - warn_full: bool = True, - ) -> GradeLayout: - """Resolve static layout metadata for tensors or multivectors.""" - return _resolve_layout( - self, - layout=layout, - grades=grades, - mv=mv, - allow_full=allow_full, - warn_full=warn_full, - ) - - def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: - """Return canonical dense basis indices for ``grades``.""" - return _grade_indices(self, grades, device=self.device if device is None else device) - - def hermitian_signs( - self, - layout: Optional[GradeLayout] = None, - *, - grades: Optional[Iterable[int]] = None, - device=None, - dtype: Optional[torch.dtype] = None, - ) -> torch.Tensor: - """Return Hermitian signs for a dense or compact layout.""" - return _hermitian_signs(self, layout=layout, grades=grades, device=device, dtype=dtype) - def bivector_squared_signs(self, *, device=None, dtype: Optional[torch.dtype] = None) -> torch.Tensor: """Return ``(e_ab)^2`` signs in canonical grade-2 layout order.""" signs = self.bv_sq_scalar @@ -714,38 +668,6 @@ def anti_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.T B_gathered = B[..., self.cayley_indices] return torch.matmul(A.unsqueeze(-2), B_gathered * self.anti_comm_gp_signs).squeeze(-2) - def planned_unary( - self, - values: torch.Tensor, - *, - op: str, - input_grades=None, - output_grades=None, - input_layout: Optional[GradeLayout] = None, - output_layout: Optional[GradeLayout] = None, - input_compact: bool = False, - compact_output: bool = False, - return_layout: bool = False, - ): - """Execute a unary operation through the shared static grade planner.""" - request = self.planner.unary_request( - values, - op=op, - input_grades=input_grades, - output_grades=output_grades, - input_layout=input_layout, - output_layout=output_layout, - input_compact=input_compact, - ) - executor = self.planner.unary_executor_for_request(request) - output = executor.forward_compact(values) if request.input_compact else executor(values) - - if return_layout: - return output, executor.output_layout - if compact_output: - return output - return executor.output_layout.dense(output) - def blade_inverse(self, blade: torch.Tensor) -> torch.Tensor: """Compute the inverse of a blade: B^{-1} = B_rev / _0. diff --git a/core/runtime/context.py b/core/runtime/context.py index 7041e77..04e8dcd 100644 --- a/core/runtime/context.py +++ b/core/runtime/context.py @@ -17,15 +17,11 @@ from core.foundation.device import resolve_device, resolve_dtype from core.foundation.layout import AlgebraSpec, GradeLayout from core.planning.planner import GradePlanner -from core.runtime.accessors import FULL_LAYOUT_MAX_N -from core.runtime.accessors import default_layout as _default_layout -from core.runtime.accessors import grade_indices as _grade_indices -from core.runtime.accessors import hermitian_signs as _hermitian_signs -from core.runtime.accessors import resolve_layout as _resolve_layout -from core.runtime.projected import ProjectedProductMixin +from core.planning.policy import DEFAULT_PLANNING_LIMITS, FULL_LAYOUT_MAX_N, PlanningLimits +from core.runtime.projected import AlgebraRuntimeMixin -class AlgebraContext(ProjectedProductMixin): +class AlgebraContext(AlgebraRuntimeMixin): """Signature and planning host without dense Cayley-table materialization.""" def __init__( @@ -38,6 +34,7 @@ def __init__( dtype: torch.dtype = torch.float32, default_grades: Optional[Iterable[int]] = None, allow_full_layout_products: Optional[bool] = None, + planning_limits: Optional[PlanningLimits] = None, ): if p < 0 or q < 0 or r < 0: raise ValueError(f"signature counts must be non-negative, got Cl({p},{q},{r})") @@ -51,10 +48,9 @@ def __init__( self.spec = AlgebraSpec(self.p, self.q, self.r) self._device = torch.device(resolve_device(device) if str(device) == "auto" else device) self._dtype = resolve_dtype(dtype) - requested_full_layout = self.n <= FULL_LAYOUT_MAX_N if allow_full_layout_products is None else bool( - allow_full_layout_products - ) + requested_full_layout = False if allow_full_layout_products is None else bool(allow_full_layout_products) self.allow_full_layout_products = requested_full_layout and self.n <= FULL_LAYOUT_MAX_N + self.planning_limits = DEFAULT_PLANNING_LIMITS if planning_limits is None else planning_limits self._default_grades = None if default_grades is None else normalize_grades(default_grades, self.n) self._default_layout: Optional[GradeLayout] = None self.planner = GradePlanner(self) @@ -70,50 +66,6 @@ def dtype(self) -> torch.dtype: """Return the context floating-point dtype.""" return self._dtype - def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: - """Return a compact layout, or the full default layout when omitted.""" - if grades is None: - return self.default_layout() - return self.planner.layout(grades) - - def default_layout(self) -> GradeLayout: - """Return the default layout using the central full-layout fallback policy.""" - return _default_layout(self) - - def resolve_layout( - self, - *, - layout: Optional[GradeLayout] = None, - grades: Optional[Iterable[int]] = None, - mv=None, - allow_full: bool = True, - warn_full: bool = True, - ) -> GradeLayout: - """Resolve static layout metadata for tensors or multivectors.""" - return _resolve_layout( - self, - layout=layout, - grades=grades, - mv=mv, - allow_full=allow_full, - warn_full=warn_full, - ) - - def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: - """Return canonical dense basis indices for ``grades`` without dense tables.""" - return _grade_indices(self, grades, device=self.device if device is None else device) - - def hermitian_signs( - self, - layout: Optional[GradeLayout] = None, - *, - grades: Optional[Iterable[int]] = None, - device=None, - dtype: Optional[torch.dtype] = None, - ) -> torch.Tensor: - """Return Hermitian signs for a dense or compact layout.""" - return _hermitian_signs(self, layout=layout, grades=grades, device=device, dtype=dtype) - def bivector_squared_signs(self, *, device=None, dtype: Optional[torch.dtype] = None) -> torch.Tensor: """Return ``(e_ab)^2`` signs in canonical grade-2 layout order.""" return self.planner.bivector_squared_signs( @@ -161,9 +113,10 @@ def anti_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.T """Plan and execute an anti-commutator product.""" return self.projected_product(A, B, op="anti_commutator", **kwargs) - def grade_projection(self, mv: torch.Tensor, grade: int) -> torch.Tensor: + def grade_projection(self, mv: torch.Tensor, grade: int, **kwargs) -> torch.Tensor: """Project a dense multivector tensor to one grade.""" - return self.planned_unary(mv, op="grade_projection", output_grades=(int(grade),)) + kwargs.setdefault("output_grades", (int(grade),)) + return self.planned_unary(mv, op="grade_projection", **kwargs) def embed_vector(self, vectors: torch.Tensor) -> torch.Tensor: """Embed grade-1 vector coordinates into dense multivector coefficients.""" @@ -185,38 +138,6 @@ def clifford_conjugation(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: """Apply Clifford conjugation to dense or compact multivector coefficients.""" return self.planned_unary(mv, op="clifford_conjugation", **kwargs) - def planned_unary( - self, - values: torch.Tensor, - *, - op: str, - input_grades=None, - output_grades=None, - input_layout: Optional[GradeLayout] = None, - output_layout: Optional[GradeLayout] = None, - input_compact: bool = False, - compact_output: bool = False, - return_layout: bool = False, - ): - """Execute a unary planned operation.""" - request = self.planner.unary_request( - values, - op=op, - input_grades=input_grades, - output_grades=output_grades, - input_layout=input_layout, - output_layout=output_layout, - input_compact=input_compact, - ) - executor = self.planner.unary_executor_for_request(request) - output = executor.forward_compact(values) if request.input_compact else executor(values) - - if return_layout: - return output, executor.output_layout - if compact_output: - return output - return executor.output_layout.dense(output) - def _sync_eps(self) -> None: finfo = torch.finfo(self.dtype) self.eps = float(finfo.eps) diff --git a/core/runtime/projected.py b/core/runtime/projected.py index 8947a97..c157b86 100644 --- a/core/runtime/projected.py +++ b/core/runtime/projected.py @@ -9,13 +9,65 @@ from __future__ import annotations +from typing import Iterable, Optional + import torch +from core.foundation.layout import GradeLayout from core.foundation.validation import check_multivector +from core.runtime.accessors import default_layout as _default_layout +from core.runtime.accessors import grade_indices as _grade_indices +from core.runtime.accessors import hermitian_signs as _hermitian_signs +from core.runtime.accessors import materialize_dense +from core.runtime.accessors import resolve_layout as _resolve_layout + + +class AlgebraRuntimeMixin: + """Shared runtime protocol for dense kernels and planned contexts.""" + + def layout(self, grades: Optional[Iterable[int]] = None) -> GradeLayout: + """Return a compact grade layout or the algebra's default layout.""" + if grades is None: + return self.default_layout() + return self.planner.layout(grades) + def default_layout(self) -> GradeLayout: + """Return the default layout using the central fallback policy.""" + return _default_layout(self) -class ProjectedProductMixin: - """Execute declared grade products through an algebra's static planner.""" + def resolve_layout( + self, + *, + layout: Optional[GradeLayout] = None, + grades: Optional[Iterable[int]] = None, + mv=None, + allow_full: bool = True, + warn_full: bool = True, + ) -> GradeLayout: + """Resolve static layout metadata for tensors or multivectors.""" + return _resolve_layout( + self, + layout=layout, + grades=grades, + mv=mv, + allow_full=allow_full, + warn_full=warn_full, + ) + + def grade_indices(self, grades: Iterable[int], *, device=None) -> torch.Tensor: + """Return canonical dense basis indices for ``grades``.""" + return _grade_indices(self, grades, device=self.device if device is None else device) + + def hermitian_signs( + self, + layout: Optional[GradeLayout] = None, + *, + grades: Optional[Iterable[int]] = None, + device=None, + dtype: Optional[torch.dtype] = None, + ) -> torch.Tensor: + """Return Hermitian signs for a dense or compact layout.""" + return _hermitian_signs(self, layout=layout, grades=grades, device=device, dtype=dtype) def projected_product( self, @@ -72,7 +124,7 @@ def projected_product( return values, executor.output_layout if compact_output: return values - return executor.output_layout.dense(values) + return materialize_dense(self, values, layout=executor.output_layout) def projected_geometric_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs): """Projected geometric product convenience wrapper.""" @@ -94,6 +146,38 @@ def projected_anti_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs): """Projected anti-commutator convenience wrapper.""" return self.projected_product(A, B, op="anti_commutator", **kwargs) + def planned_unary( + self, + values: torch.Tensor, + *, + op: str, + input_grades=None, + output_grades=None, + input_layout: Optional[GradeLayout] = None, + output_layout: Optional[GradeLayout] = None, + input_compact: bool = False, + compact_output: bool = False, + return_layout: bool = False, + ): + """Execute a unary operation through the shared static grade planner.""" + request = self.planner.unary_request( + values, + op=op, + input_grades=input_grades, + output_grades=output_grades, + input_layout=input_layout, + output_layout=output_layout, + input_compact=input_compact, + ) + executor = self.planner.unary_executor_for_request(request) + output = executor.forward_compact(values) if request.input_compact else executor(values) + + if return_layout: + return output, executor.output_layout + if compact_output: + return output + return materialize_dense(self, output, layout=executor.output_layout) + def _declared_layout(self, grades, layout): if layout is not None: return layout diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index 539143c..bce7cfb 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -7,6 +7,7 @@ from core.planning.flow import GradeFlow from core.planning.layouts import build_product_request from core.planning.planner import GradePlanner +from core.planning.policy import PlanningLimits from core.planning.product import ( GradeProductExecutor, build_grade_product_plan, @@ -373,17 +374,17 @@ def test_context_planned_unary_projection_and_reverse_avoid_full_layout(): mv[0, 1] = 2.0 mv[0, 3] = 5.0 - projected = algebra.grade_projection(mv, 1) + projected, projected_layout = algebra.grade_projection(mv, 1, compact_output=True, return_layout=True) reversed_bivector = algebra.reverse( mv, input_grades=(2,), compact_output=True, ) + vector_pos = projected_layout.basis_indices.index(1) bivector_layout = algebra.layout((2,)) bivector_pos = bivector_layout.basis_indices.index(3) - assert torch.allclose(projected[0, 1], torch.tensor(2.0)) - assert torch.allclose(projected[0, 3], torch.tensor(0.0)) + assert torch.allclose(projected[0, vector_pos], torch.tensor(2.0)) assert torch.allclose(reversed_bivector[0, bivector_pos], torch.tensor(-5.0)) @@ -498,6 +499,21 @@ def test_context_declared_grades_infer_compact_operand_shapes(): assert torch.allclose(values[0, output_layout.basis_indices.index(0)], torch.tensor(1.0)) +def test_context_declared_product_requires_compact_output_without_dense_materialization(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + vector_layout = algebra.layout((1,)) + left = torch.zeros(1, vector_layout.dim) + right = torch.zeros(1, vector_layout.dim) + + with pytest.raises(ValueError, match="Dense materialization is disabled"): + algebra.projected_geometric_product( + left, + right, + left_layout=vector_layout, + right_layout=vector_layout, + ) + + def test_high_dim_context_requires_declared_layout_for_products(): algebra = make_algebra(13, 0, 0, device=DEVICE, dtype=torch.float32) A = torch.zeros(1, algebra.dim) @@ -510,8 +526,23 @@ def test_high_dim_context_requires_declared_layout_for_products(): algebra.reverse(A) -def test_context_warns_for_implicit_full_layout_fallback_between_eight_and_twelve(): - context = make_algebra(9, 0, 0, kernel="context", device=DEVICE, dtype=torch.float32) +def test_context_requires_declared_grades_by_default_even_low_dimensional(): + context = make_algebra(4, 0, 0, kernel="context", device=DEVICE, dtype=torch.float64) + + with pytest.raises(ValueError, match="Declare active grades"): + context.layout() + + +def test_context_warns_for_explicit_implicit_full_layout_fallback_between_eight_and_twelve(): + context = make_algebra( + 9, + 0, + 0, + kernel="context", + device=DEVICE, + dtype=torch.float32, + allow_full_layout_products=True, + ) with pytest.warns(RuntimeWarning, match="implicit full Cl\\(9,0,0\\) layout"): layout = context.layout() @@ -521,7 +552,15 @@ def test_context_warns_for_implicit_full_layout_fallback_between_eight_and_twelv def test_low_dim_context_can_use_full_layout_fallback(): - context = make_algebra(4, 0, 0, kernel="context", device=DEVICE, dtype=torch.float64) + context = make_algebra( + 4, + 0, + 0, + kernel="context", + device=DEVICE, + dtype=torch.float64, + allow_full_layout_products=True, + ) dense = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) A = _grade_only_input(dense, 2, (1,), seed=163) B = _grade_only_input(dense, 2, (1,), seed=167) @@ -533,6 +572,42 @@ def test_low_dim_context_can_use_full_layout_fallback(): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) +def test_context_static_product_cost_limits_raise_before_executor_build(): + limits = PlanningLimits(warn_lanes=512, max_lanes=512, warn_pairs=512, max_pairs=64) + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32, planning_limits=limits) + layout = algebra.layout((1,)) + left = torch.zeros(1, layout.dim) + right = torch.zeros(1, layout.dim) + + with pytest.raises(ValueError, match="basis interactions"): + algebra.projected_geometric_product( + left, + right, + left_layout=layout, + right_layout=layout, + compact_output=True, + ) + + +def test_context_static_product_cost_warns_near_configured_limits(): + limits = PlanningLimits(warn_lanes=512, max_lanes=512, warn_pairs=128, max_pairs=512) + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32, planning_limits=limits) + layout = algebra.layout((1,)) + left = torch.zeros(1, layout.dim) + right = torch.zeros(1, layout.dim) + + with pytest.warns(RuntimeWarning, match="basis interactions"): + values = algebra.projected_geometric_product( + left, + right, + left_layout=layout, + right_layout=layout, + compact_output=True, + ) + + assert values.shape[-1] == algebra.layout((0, 2)).dim + + @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") def test_static_grade_product_compiles_fullgraph_with_aot_eager(): algebra = CliffordAlgebra(5, 1, 0, device=DEVICE, dtype=torch.float32) From 380f9c7d61e9946e558f309f49ada94b5b67a386 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 08:56:46 +0900 Subject: [PATCH 28/45] fix: avoid dense basis enumeration in planner --- core/foundation/__init__.py | 2 + core/foundation/basis.py | 50 ++++++++++++++++++-- core/foundation/layout.py | 4 +- core/planning/planner.py | 47 ++++++++++++++++++- core/planning/policy.py | 53 +++++++++++++++++++++ core/planning/product.py | 27 ++++++----- core/runtime/accessors.py | 13 ++++-- tests/test_grade_plan.py | 91 ++++++++++++++++++++++++++++++++++++- 8 files changed, 264 insertions(+), 23 deletions(-) diff --git a/core/foundation/__init__.py b/core/foundation/__init__.py index fdec0af..130f764 100644 --- a/core/foundation/__init__.py +++ b/core/foundation/__init__.py @@ -9,6 +9,7 @@ from .basis import ( GradeProductOp, + basis_count_for_grades, basis_index_tuple_for_grades, basis_indices_for_grades, basis_product, @@ -30,6 +31,7 @@ "DeviceConfig", "GradeLayout", "GradeProductOp", + "basis_count_for_grades", "basis_index_tuple_for_grades", "basis_indices_for_grades", "basis_product", diff --git a/core/foundation/basis.py b/core/foundation/basis.py index 2ce976d..db7cd13 100644 --- a/core/foundation/basis.py +++ b/core/foundation/basis.py @@ -9,11 +9,15 @@ from __future__ import annotations +from itertools import combinations +from math import comb from typing import Iterable, Literal, Optional import torch GradeProductOp = Literal["gp", "wedge", "inner", "commutator", "anti_commutator"] +TORCH_LONG_BASIS_MAX_N = 63 +_TORCH_LONG_MAX = (1 << TORCH_LONG_BASIS_MAX_N) - 1 def normalize_grades(grades: Iterable[int], n: int, *, name: str = "grades") -> tuple[int, ...]: @@ -29,13 +33,53 @@ def normalize_grades(grades: Iterable[int], n: int, *, name: str = "grades") -> def basis_index_tuple_for_grades(n: int, grades: Iterable[int]) -> tuple[int, ...]: """Return canonical bitmask basis indices whose popcount is in ``grades``.""" - grade_set = set(normalize_grades(grades, n)) - return tuple(index for index in range(1 << n) if index.bit_count() in grade_set) + indices: list[int] = [] + for grade in normalize_grades(grades, n): + indices.extend(_basis_indices_for_grade(n, grade)) + return tuple(sorted(indices)) + + +def basis_count_for_grades(n: int, grades: Iterable[int]) -> int: + """Return the number of basis blades represented by ``grades``.""" + return sum(comb(n, grade) for grade in normalize_grades(grades, n)) def basis_indices_for_grades(n: int, grades: Iterable[int], *, device=None) -> torch.Tensor: """Return canonical bitmask basis indices as a tensor.""" - return torch.tensor(basis_index_tuple_for_grades(n, grades), dtype=torch.long, device=device) + return basis_indices_tensor(basis_index_tuple_for_grades(n, grades), n=n, device=device) + + +def basis_indices_tensor( + indices: Iterable[int], + *, + n: Optional[int] = None, + role: str = "basis indices", + device=None, +) -> torch.Tensor: + """Tensorize canonical basis bitmasks with a clear signed-int64 boundary.""" + values = tuple(int(index) for index in indices) + _validate_torch_long_basis_indices(values, n=n, role=role) + return torch.tensor(values, dtype=torch.long, device=device) + + +def _basis_indices_for_grade(n: int, grade: int) -> tuple[int, ...]: + if grade == 0: + return (0,) + if grade == n: + return ((1 << n) - 1,) + return tuple(sum(1 << bit for bit in bits) for bits in combinations(range(n), grade)) + + +def _validate_torch_long_basis_indices(indices: tuple[int, ...], *, n: Optional[int], role: str) -> None: + if not indices: + return + if max(indices) <= _TORCH_LONG_MAX: + return + dimension = "" if n is None else f" for n={n}" + raise ValueError( + f"{role}{dimension} cannot be represented as torch.long basis bitmasks. " + f"Current Torch-backed executors support bitmask tensorization up to n={TORCH_LONG_BASIS_MAX_N}." + ) def geometric_product_output_grades(left_grade: int, right_grade: int, n: int) -> tuple[int, ...]: diff --git a/core/foundation/layout.py b/core/foundation/layout.py index b35d002..49f0520 100644 --- a/core/foundation/layout.py +++ b/core/foundation/layout.py @@ -14,7 +14,7 @@ import torch -from core.foundation.basis import basis_index_tuple_for_grades, normalize_grades +from core.foundation.basis import basis_index_tuple_for_grades, basis_indices_tensor, normalize_grades @dataclass(frozen=True) @@ -83,7 +83,7 @@ def contains_grade(self, grade: int) -> bool: def indices_tensor(self, *, device=None) -> torch.Tensor: """Return basis indices as a tensor on ``device``.""" - return torch.tensor(self.basis_indices, dtype=torch.long, device=device) + return basis_indices_tensor(self.basis_indices, n=self.spec.n, role="layout basis indices", device=device) def convert(self, values: torch.Tensor, source: "GradeLayout") -> torch.Tensor: """Convert compact values from ``source`` into this layout. diff --git a/core/planning/planner.py b/core/planning/planner.py index 2e596d1..978111b 100644 --- a/core/planning/planner.py +++ b/core/planning/planner.py @@ -16,7 +16,9 @@ from core.planning.layouts import ProductRequest, build_product_request, normalize_product_op from core.planning.policy import ( full_layout_allowed, + validate_grades_cost, validate_layout_cost, + validate_product_grades_cost, validate_product_request, validate_unary_request, warn_full_layout_fallback, @@ -48,7 +50,7 @@ def __init__(self, algebra): def layout(self, grades): """Return the compact layout for ``grades``.""" - return validate_layout_cost(self.algebra, self.spec.layout(grades)) + return self.spec.layout(validate_grades_cost(self.algebra, self.spec, grades)) def full_layout(self) -> GradeLayout: """Return the full dense basis layout.""" @@ -109,6 +111,14 @@ def product_executor( cache: bool = True, ): """Return a cached static executor for a projected bilinear product.""" + left_grades, right_grades, output_grades = validate_product_grades_cost( + self.algebra, + self.spec, + op=op, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + ) request = ProductRequest( spec=self.spec, op=normalize_product_op(op), @@ -144,6 +154,15 @@ def product_request( self._implicit_full_operand(right, grades=right_grades, layout=right_layout, compact=right_compact) ): warn_full_layout_fallback(self.algebra) + self._validate_product_grade_cost_before_layouts( + op=op, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + left_layout=left_layout, + right_layout=right_layout, + output_layout=output_layout, + ) request = build_product_request( self.spec, left, @@ -164,6 +183,7 @@ def product_request( def product_executor_for_request(self, request: ProductRequest, *, cache: bool = True) -> GradeProductExecutor: """Return an executor for an already normalized product request.""" + validate_product_request(self.algebra, request) key = request.cache_key executor = self._product_executors.get(key) if cache else None if executor is None: @@ -288,3 +308,28 @@ def _default_operand_grades(self, grades, layout: GradeLayout = None): if grades is not None or layout is not None: return grades return getattr(self.algebra, "_default_grades", None) + + def _validate_product_grade_cost_before_layouts( + self, + *, + op: str, + left_grades, + right_grades, + output_grades, + left_layout: GradeLayout = None, + right_layout: GradeLayout = None, + output_layout: GradeLayout = None, + ) -> None: + left = left_layout.grades if left_layout is not None else left_grades + right = right_layout.grades if right_layout is not None else right_grades + if left is None or right is None: + return + output = output_layout.grades if output_layout is not None else output_grades + validate_product_grades_cost( + self.algebra, + self.spec, + op=op, + left_grades=left, + right_grades=right, + output_grades=output, + ) diff --git a/core/planning/policy.py b/core/planning/policy.py index 00a821b..fb49967 100644 --- a/core/planning/policy.py +++ b/core/planning/policy.py @@ -13,6 +13,7 @@ from dataclasses import dataclass from typing import Optional +from core.foundation.basis import basis_count_for_grades, expand_output_grades, normalize_grades from core.foundation.layout import AlgebraSpec, GradeLayout FULL_LAYOUT_WARN_N = 8 @@ -98,6 +99,58 @@ def validate_layout_cost(algebra, layout: GradeLayout, *, role: str = "layout") return layout +def validate_grades_cost(algebra, spec: AlgebraSpec, grades, *, role: str = "layout") -> tuple[int, ...]: + """Validate a grade set before materializing its basis indices.""" + normalized = normalize_grades(grades, spec.n) + cost = PlanCost( + spec=spec, + kind="layout", + op=role, + output_lanes=basis_count_for_grades(spec.n, normalized), + output_grades=normalized, + ) + validate_plan_cost(algebra, cost) + return normalized + + +def validate_product_grades_cost( + algebra, + spec: AlgebraSpec, + *, + op: str, + left_grades, + right_grades, + output_grades=None, +) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]: + """Validate product grade sets before materializing layouts.""" + left = normalize_grades(left_grades, spec.n, name="left_grades") + right = normalize_grades(right_grades, spec.n, name="right_grades") + output = ( + expand_output_grades(left, right, spec.n, op=op) + if output_grades is None + else normalize_grades(output_grades, spec.n, name="output_grades") + ) + left_lanes = basis_count_for_grades(spec.n, left) + right_lanes = basis_count_for_grades(spec.n, right) + output_lanes = basis_count_for_grades(spec.n, output) + validate_plan_cost( + algebra, + PlanCost( + spec=spec, + kind="product", + op=str(op), + left_lanes=left_lanes, + right_lanes=right_lanes, + output_lanes=output_lanes, + pair_count=left_lanes * right_lanes, + left_grades=left, + right_grades=right, + output_grades=output, + ), + ) + return left, right, output + + def product_plan_cost(request) -> PlanCost: """Return static cost summary for a product request.""" return PlanCost( diff --git a/core/planning/product.py b/core/planning/product.py index 8e4de72..74f2ff2 100644 --- a/core/planning/product.py +++ b/core/planning/product.py @@ -20,7 +20,14 @@ import torch import torch.nn as nn -from core.foundation.basis import GradeProductOp, expand_output_grades, normalize_grades, operation_coefficient +from core.foundation.basis import ( + GradeProductOp, + basis_index_tuple_for_grades, + basis_indices_tensor, + expand_output_grades, + normalize_grades, + operation_coefficient, +) from core.foundation.layout import AlgebraSpec, GradeLayout from core.planning.layouts import ProductRequest from core.planning.tree import GradePlanTree, build_grade_plan_tree @@ -170,13 +177,9 @@ def build_grade_product_plan_from_tree( right_grade_tuple = tree.right_grades output_grade_tuple = tree.output_grades - left_basis_by_grade = { - grade: [index for index in range(1 << n) if index.bit_count() == grade] for grade in left_grade_tuple - } - right_basis_by_grade = { - grade: [index for index in range(1 << n) if index.bit_count() == grade] for grade in right_grade_tuple - } - active_outputs = [index for index in range(1 << n) if index.bit_count() in set(output_grade_tuple)] + left_basis_by_grade = {grade: basis_index_tuple_for_grades(n, (grade,)) for grade in left_grade_tuple} + right_basis_by_grade = {grade: basis_index_tuple_for_grades(n, (grade,)) for grade in right_grade_tuple} + active_outputs = basis_index_tuple_for_grades(n, output_grade_tuple) output_position_by_index = {index: position for position, index in enumerate(active_outputs)} plan_left: list[int] = [] @@ -209,12 +212,12 @@ def build_grade_product_plan_from_tree( left_grades=left_grade_tuple, right_grades=right_grade_tuple, output_grades=output_grade_tuple, - left_indices=torch.tensor(plan_left, dtype=torch.long, device=device), - right_indices=torch.tensor(plan_right, dtype=torch.long, device=device), - output_indices=torch.tensor(plan_output, dtype=torch.long, device=device), + left_indices=basis_indices_tensor(plan_left, n=n, role="left product basis indices", device=device), + right_indices=basis_indices_tensor(plan_right, n=n, role="right product basis indices", device=device), + output_indices=basis_indices_tensor(plan_output, n=n, role="output product basis indices", device=device), output_positions=torch.tensor(plan_positions, dtype=torch.long, device=device), coefficients=torch.tensor(plan_coefficients, dtype=dtype, device=device), - active_output_indices=torch.tensor(active_outputs, dtype=torch.long, device=device), + active_output_indices=basis_indices_tensor(active_outputs, n=n, role="active output basis indices", device=device), tree=tree, ) diff --git a/core/runtime/accessors.py b/core/runtime/accessors.py index a70fa12..7cbd44d 100644 --- a/core/runtime/accessors.py +++ b/core/runtime/accessors.py @@ -15,7 +15,12 @@ from core.foundation.basis import normalize_grades, operation_coefficient, reverse_sign from core.foundation.layout import AlgebraSpec, GradeLayout -from core.planning.policy import FULL_LAYOUT_MAX_N, validate_layout_cost, warn_full_layout_fallback +from core.planning.policy import ( + FULL_LAYOUT_MAX_N, + validate_grades_cost, + validate_layout_cost, + warn_full_layout_fallback, +) def resolve_layout( @@ -36,7 +41,7 @@ def resolve_layout( return validate_layout_cost(algebra, layout) if grades is not None: - return validate_layout_cost(algebra, spec.layout(grades)) + return spec.layout(validate_grades_cost(algebra, spec, grades)) if _is_multivector(mv) and getattr(mv, "layout", None) is not None: mv_layout = mv.layout @@ -49,7 +54,7 @@ def resolve_layout( if cached is not None: _check_layout_spec(spec, cached, "default_layout") return validate_layout_cost(algebra, cached, role="default_layout") - resolved = spec.layout(default_grades) + resolved = spec.layout(validate_grades_cost(algebra, spec, default_grades, role="default_layout")) if hasattr(algebra, "_default_layout"): algebra._default_layout = resolved return validate_layout_cost(algebra, resolved, role="default_layout") @@ -63,7 +68,7 @@ def resolve_layout( ) if warn_full: warn_full_layout_fallback(algebra) - return validate_layout_cost(algebra, spec.layout(range(spec.n + 1)), role="full_layout") + return spec.layout(validate_grades_cost(algebra, spec, range(spec.n + 1), role="full_layout")) def default_layout(algebra) -> GradeLayout: diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index bce7cfb..79ce79f 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -2,7 +2,13 @@ import torch from core.config import make_algebra -from core.foundation.basis import basis_indices_for_grades, expand_output_grades, geometric_product_output_grades +from core.foundation.basis import ( + basis_count_for_grades, + basis_index_tuple_for_grades, + basis_indices_for_grades, + expand_output_grades, + geometric_product_output_grades, +) from core.foundation.layout import AlgebraSpec from core.planning.flow import GradeFlow from core.planning.layouts import build_product_request @@ -46,6 +52,21 @@ def test_grade_expansion_for_common_high_dim_paths(): assert expand_output_grades((1,), (1,), 16, op="gp", project_grades=(0,)) == (0,) +def test_basis_indices_for_grades_are_combinatorial_and_high_dimensional(): + assert basis_index_tuple_for_grades(4, (1, 2)) == tuple( + index for index in range(1 << 4) if index.bit_count() in {1, 2} + ) + high = basis_index_tuple_for_grades(32, (1, 2)) + assert len(high) == basis_count_for_grades(32, (1, 2)) + assert high[0] == 1 + assert high[-1] == (1 << 31) | (1 << 30) + + +def test_basis_tensorization_reports_int64_bitmask_boundary(): + with pytest.raises(ValueError, match="torch.long basis bitmasks"): + basis_indices_for_grades(64, (1,)) + + def test_grade_plan_tree_groups_routes_without_runtime_partition_backend(): spec = AlgebraSpec(10, 4, 2) tree = build_grade_plan_tree( @@ -589,6 +610,74 @@ def test_context_static_product_cost_limits_raise_before_executor_build(): ) +def test_direct_product_executor_obeys_static_pair_limits(): + limits = PlanningLimits(warn_lanes=512, max_lanes=512, warn_pairs=512, max_pairs=64) + algebra = make_algebra(16, 0, 0, device=DEVICE, dtype=torch.float32, planning_limits=limits) + + with pytest.raises(ValueError, match="basis interactions"): + algebra.planner.product_executor( + op="gp", + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + dtype=torch.float32, + device=DEVICE, + ) + + +def test_context_static_layout_cost_limit_raises_before_basis_materialization(): + limits = PlanningLimits(warn_lanes=32, max_lanes=64, warn_pairs=512, max_pairs=1024) + algebra = make_algebra(32, 0, 0, device=DEVICE, dtype=torch.float32, planning_limits=limits) + + with pytest.raises(ValueError, match="active lanes"): + algebra.layout((1, 2)) + + +def test_high_dimensional_vector_product_plan_avoids_full_basis_enumeration(): + algebra = make_algebra( + 32, + 0, + 0, + device=DEVICE, + dtype=torch.float32, + planning_limits=PlanningLimits(max_lanes=4096, max_pairs=100_000), + ) + vector_layout = algebra.layout((1,)) + executor = algebra.planner.product_executor( + op="gp", + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + dtype=torch.float32, + device=DEVICE, + ) + + assert vector_layout.dim == 32 + assert executor.output_dim == 1 + 32 * 31 // 2 + assert executor.pair_count == 32 * 32 + + +def test_high_dimensional_product_plan_reports_int64_bitmask_boundary(): + algebra = make_algebra( + 64, + 0, + 0, + device=DEVICE, + dtype=torch.float32, + planning_limits=PlanningLimits(max_lanes=4096, max_pairs=100_000), + ) + + with pytest.raises(ValueError, match="Current Torch-backed executors support bitmask tensorization up to n=63"): + algebra.planner.product_executor( + op="gp", + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + dtype=torch.float32, + device=DEVICE, + ) + + def test_context_static_product_cost_warns_near_configured_limits(): limits = PlanningLimits(warn_lanes=512, max_lanes=512, warn_pairs=128, max_pairs=512) algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32, planning_limits=limits) From 70809bd96ab43f9732571a97694bd6b94dcc7e5d Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 09:06:25 +0900 Subject: [PATCH 29/45] docs: clarify torch bitmask dimension limit --- core/foundation/basis.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/foundation/basis.py b/core/foundation/basis.py index db7cd13..cec3a55 100644 --- a/core/foundation/basis.py +++ b/core/foundation/basis.py @@ -16,6 +16,13 @@ import torch GradeProductOp = Literal["gp", "wedge", "inner", "commutator", "anti_commutator"] + +# NOTE: Torch-backed executors currently store canonical basis blades as signed +# int64 bitmasks. That makes n=63 the largest supported dimension: the highest +# usable basis bit is 1 << 62, while n=64 would require 1 << 63, which is outside +# torch.long's positive range. Supporting n>=64 requires kernel-level storage +# engineering, for example compact-position-only kernels, declared blade objects, +# multi-limb or variable-length bitsets, or another non-int64 basis identifier. TORCH_LONG_BASIS_MAX_N = 63 _TORCH_LONG_MAX = (1 << TORCH_LONG_BASIS_MAX_N) - 1 From fb46bb26c02146eed639c81e24ab873a8ff00084 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 09:19:47 +0900 Subject: [PATCH 30/45] refactor: remove premature layer optimization coupling --- core/__init__.py | 14 -- core/planning/__init__.py | 14 -- core/planning/routes.py | 297 ----------------------------- functional/activation.py | 36 +--- functional/loss.py | 14 -- functional/orthogonality.py | 3 - layers/adapters/conformal.py | 5 - layers/adapters/embedding.py | 54 ++---- layers/adapters/mother.py | 102 ++-------- layers/adapters/projective.py | 5 - layers/blocks/attention.py | 118 +----------- layers/blocks/multi_rotor_ffn.py | 29 +-- layers/blocks/transformer.py | 39 +--- layers/planning.py | 55 ------ layers/primitives/linear.py | 26 +-- layers/primitives/multi_rotor.py | 10 +- layers/primitives/normalization.py | 38 +--- layers/primitives/projection.py | 30 +-- layers/primitives/reflection.py | 10 +- layers/primitives/rotor.py | 10 +- layers/primitives/rotor_gadget.py | 12 +- models/deap/eeg_net.py | 3 - models/lqa/heads.py | 9 - models/md17/forcenet.py | 7 - models/sr/net.py | 5 - tasks/base.py | 15 -- tests/test_attention.py | 63 +----- tests/test_layers.py | 209 +------------------- 28 files changed, 102 insertions(+), 1130 deletions(-) delete mode 100644 core/planning/routes.py delete mode 100644 layers/planning.py diff --git a/core/__init__.py b/core/__init__.py index 1361d69..07b9123 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -31,14 +31,6 @@ from .planning.planner import GradePlanner from .planning.policy import DEFAULT_PLANNING_LIMITS, PlanCost, PlanningLimits from .planning.product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan -from .planning.routes import ( - ModuleOptimizationIssue, - ModuleOptimizationPlan, - ModuleOptimizationReport, - collect_module_optimization_plans, - inspect_module_optimization, - module_optimization_plan, -) from .planning.tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .planning.unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request from .runtime.accessors import ( @@ -134,9 +126,6 @@ "GradePathNode", "GradePlanTree", "GradeFlow", - "ModuleOptimizationIssue", - "ModuleOptimizationPlan", - "ModuleOptimizationReport", "ProductRequest", "GradeUnaryExecutor", "GradeUnaryOp", @@ -148,11 +137,8 @@ "build_grade_plan_tree", "build_product_request", "build_unary_request", - "collect_module_optimization_plans", "expand_output_grades", "geometric_product_output_grades", - "inspect_module_optimization", - "module_optimization_plan", "normalize_grades", "operation_coefficient", "reverse_sign", diff --git a/core/planning/__init__.py b/core/planning/__init__.py index 5859ce1..570aa81 100644 --- a/core/planning/__init__.py +++ b/core/planning/__init__.py @@ -12,14 +12,6 @@ from .planner import GradePlanner from .policy import DEFAULT_PLANNING_LIMITS, PlanCost, PlanningLimits from .product import GradeProductExecutor, GradeProductPlan, build_grade_product_plan -from .routes import ( - ModuleOptimizationIssue, - ModuleOptimizationPlan, - ModuleOptimizationReport, - collect_module_optimization_plans, - inspect_module_optimization, - module_optimization_plan, -) from .tree import GradePathNode, GradePlanTree, build_grade_plan_tree from .unary import GradeUnaryExecutor, GradeUnaryOp, GradeUnaryPlan, UnaryRequest, build_unary_request @@ -33,9 +25,6 @@ "PlanningLimits", "PlanCost", "DEFAULT_PLANNING_LIMITS", - "ModuleOptimizationIssue", - "ModuleOptimizationPlan", - "ModuleOptimizationReport", "GradeUnaryExecutor", "GradeUnaryOp", "GradeUnaryPlan", @@ -45,7 +34,4 @@ "build_grade_plan_tree", "build_product_request", "build_unary_request", - "collect_module_optimization_plans", - "inspect_module_optimization", - "module_optimization_plan", ] diff --git a/core/planning/routes.py b/core/planning/routes.py deleted file mode 100644 index 19e53fe..0000000 --- a/core/planning/routes.py +++ /dev/null @@ -1,297 +0,0 @@ -# Versor: Universal Geometric Algebra Neural Network -# Copyright (C) 2026 Eunkyum Kim -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# - -"""Static module-route descriptors for compile-time optimization.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Iterable, Optional - -import torch.nn as nn - -from core.foundation.layout import GradeLayout -from core.foundation.module import AlgebraLike - -_MISSING = object() - - -@dataclass(frozen=True) -class ModuleOptimizationPlan: - """Static metadata for one algebra-aware module in a composed model.""" - - path: str - module_type: str - operators: tuple[str, ...] - input_grades: Optional[tuple[int, ...]] - output_grades: Optional[tuple[int, ...]] - parameter_grades: Optional[tuple[int, ...]] - score_grades: Optional[tuple[int, ...]] - basis_dim: int - dense_dim: int - compact: bool - dense_only_reason: Optional[str] = None - - @property - def compression_ratio(self) -> float: - """Return active basis lanes divided by dense basis lanes.""" - if self.dense_dim == 0: - return 1.0 - return self.basis_dim / self.dense_dim - - def uses_grade(self, grade: int) -> bool: - """Return whether the plan mentions ``grade`` in any static grade slot.""" - grade = int(grade) - grade_sets = (self.input_grades, self.output_grades, self.parameter_grades, self.score_grades) - return any(grades is not None and grade in grades for grades in grade_sets) - - -@dataclass(frozen=True) -class ModuleOptimizationIssue: - """Static coverage issue for one algebra-aware module.""" - - path: str - module_type: str - reason: str - has_planned_descendants: bool - - -@dataclass(frozen=True) -class ModuleOptimizationReport: - """Optimization-plan coverage for a composed module tree.""" - - plans: tuple[ModuleOptimizationPlan, ...] - issues: tuple[ModuleOptimizationIssue, ...] - - @property - def compact_plans(self) -> tuple[ModuleOptimizationPlan, ...]: - """Return plans that use compact basis lanes.""" - return tuple(plan for plan in self.plans if plan.compact) - - @property - def dense_only_plans(self) -> tuple[ModuleOptimizationPlan, ...]: - """Return plans that are explicitly dense-only.""" - return tuple(plan for plan in self.plans if not plan.compact and plan.dense_only_reason is not None) - - @property - def unplanned_leaf_modules(self) -> tuple[ModuleOptimizationIssue, ...]: - """Return algebra-aware modules that have no plan and no planned descendants.""" - return tuple(issue for issue in self.issues if not issue.has_planned_descendants) - - def assert_no_unplanned_leaves(self) -> None: - """Raise if any algebra-aware leaf module is invisible to the planner.""" - leaves = self.unplanned_leaf_modules - if not leaves: - return - details = ", ".join(f"{issue.path}:{issue.module_type}" for issue in leaves) - raise AssertionError(f"Unplanned algebra-aware leaf modules: {details}") - - -def module_optimization_plan(module: nn.Module, *, path: str = "") -> Optional[ModuleOptimizationPlan]: - """Return static optimization metadata for one module. - - The collector is layer-agnostic. Modules can either implement - ``optimization_plan(path=...)`` or expose simple static attributes such as - ``layout``, ``feature_layout``, ``score_grades``, and - ``optimization_operators``. - """ - custom_plan = _custom_plan(module, path) - if custom_plan is not None: - return custom_plan - - algebra = _module_algebra(module) - if algebra is None: - return None - - layout = _declared_layout(module) - operators = _operator_tuple(getattr(module, "optimization_operators", ())) - parameter_grades = _parameter_grades(module) - score_grades = _score_grades(module) - dense_only_reason = getattr(module, "optimization_dense_only_reason", None) - if ( - layout is None - and not operators - and parameter_grades is None - and score_grades is None - and dense_only_reason is None - ): - return None - - default_grades = _grades_from_layout(layout) - input_grades = _grade_attr(module, "optimization_input_grades", default_grades) - output_grades = _grade_attr(module, "optimization_output_grades", default_grades) - compact = layout is not None - return ModuleOptimizationPlan( - path=path or "", - module_type=module.__class__.__name__, - operators=operators, - input_grades=input_grades, - output_grades=output_grades, - parameter_grades=parameter_grades, - score_grades=score_grades, - basis_dim=_basis_dim(algebra, layout), - dense_dim=int(algebra.dim), - compact=compact, - dense_only_reason=None if compact else dense_only_reason, - ) - - -def collect_module_optimization_plans( - module: nn.Module, - *, - compact_only: bool = False, -) -> tuple[ModuleOptimizationPlan, ...]: - """Collect static optimization metadata from a composed module tree.""" - plans = [] - for path, child in module.named_modules(): - plan = module_optimization_plan(child, path=path) - if plan is None: - continue - if compact_only and not plan.compact: - continue - plans.append(plan) - return tuple(plans) - - -def inspect_module_optimization(module: nn.Module) -> ModuleOptimizationReport: - """Return static optimization plans plus coverage issues for a module tree.""" - modules = tuple(module.named_modules()) - plans = [] - plan_by_path: dict[str, ModuleOptimizationPlan] = {} - - for path, child in modules: - normalized_path = path or "" - plan = module_optimization_plan(child, path=normalized_path) - if plan is None: - continue - plans.append(plan) - plan_by_path[normalized_path] = plan - - issues = [] - for path, child in modules: - normalized_path = path or "" - if normalized_path in plan_by_path or _module_algebra(child) is None: - continue - has_planned_descendants = any(_is_descendant(plan.path, normalized_path) for plan in plans) - reason = ( - "container has planned descendants but no direct route" - if has_planned_descendants - else "algebra-aware module exposes no static optimization metadata" - ) - issues.append( - ModuleOptimizationIssue( - path=normalized_path, - module_type=child.__class__.__name__, - reason=reason, - has_planned_descendants=has_planned_descendants, - ) - ) - - return ModuleOptimizationReport(plans=tuple(plans), issues=tuple(issues)) - - -def _custom_plan(module: nn.Module, path: str) -> Optional[ModuleOptimizationPlan]: - plan_fn = getattr(module, "optimization_plan", None) - if plan_fn is None or not callable(plan_fn): - return None - try: - plan = plan_fn(path=path or "") - except TypeError: - plan = plan_fn() - if plan is None: - return None - if not isinstance(plan, ModuleOptimizationPlan): - raise TypeError(f"{module.__class__.__name__}.optimization_plan() must return ModuleOptimizationPlan or None") - return plan - - -def _module_algebra(module: nn.Module) -> Optional[AlgebraLike]: - algebra = getattr(module, "algebra", None) - if algebra is None: - algebra = getattr(module, "_algebra", None) - if algebra is None or not hasattr(algebra, "planner") or not hasattr(algebra, "dim"): - return None - return algebra - - -def _declared_layout(module: nn.Module) -> Optional[GradeLayout]: - for attr in ("optimization_layout", "layout", "feature_layout"): - layout = getattr(module, attr, None) - if layout is not None: - return layout - return None - - -def _grades_from_layout(layout: Optional[GradeLayout]) -> Optional[tuple[int, ...]]: - if layout is None: - return None - return tuple(int(grade) for grade in layout.grades) - - -def _grade_attr( - module: nn.Module, - attr: str, - default: Optional[tuple[int, ...]], -) -> Optional[tuple[int, ...]]: - value = getattr(module, attr, _MISSING) - if value is _MISSING: - return default - return _grade_tuple(value) - - -def _score_grades(module: nn.Module) -> Optional[tuple[int, ...]]: - value = getattr(module, "optimization_score_grades", _MISSING) - if value is not _MISSING: - return _grade_tuple(value) - - grades = getattr(module, "score_grades", None) - if grades is not None: - return _grade_tuple(grades) - - score_layout = getattr(module, "_score_layout", None) - return _grades_from_layout(score_layout) - - -def _parameter_grades(module: nn.Module) -> Optional[tuple[int, ...]]: - value = getattr(module, "optimization_parameter_grades", _MISSING) - if value is not _MISSING: - return _grade_tuple(value) - if hasattr(module, "grade"): - return (int(getattr(module, "grade")),) - return None - - -def _grade_tuple(grades) -> Optional[tuple[int, ...]]: - if grades is None: - return None - if isinstance(grades, GradeLayout): - return _grades_from_layout(grades) - if isinstance(grades, int): - return (int(grades),) - if isinstance(grades, Iterable): - return tuple(int(grade) for grade in grades) - return (int(grades),) - - -def _operator_tuple(operators) -> tuple[str, ...]: - if operators is None: - return () - if isinstance(operators, str): - return (operators,) - return tuple(str(operator) for operator in operators) - - -def _basis_dim(algebra: AlgebraLike, layout: Optional[GradeLayout]) -> int: - if layout is not None: - return int(layout.dim) - return int(algebra.dim) - - -def _is_descendant(path: str, parent_path: str) -> bool: - if parent_path == "": - return path != "" - return path.startswith(parent_path + ".") diff --git a/functional/activation.py b/functional/activation.py index 57dc84c..92510c2 100644 --- a/functional/activation.py +++ b/functional/activation.py @@ -10,15 +10,11 @@ Magnitude-scaling and grade-wise gating functions that preserve geometric structure. """ -from typing import Optional - import torch import torch.nn as nn import torch.nn.functional as F -from core.foundation.layout import GradeLayout from core.foundation.module import CliffordModule -from core.foundation.validation import VALIDATE, check_multivector class GeometricGELU(CliffordModule): @@ -31,18 +27,14 @@ class GeometricGELU(CliffordModule): bias (torch.nn.Parameter): Learnable bias added to norm. """ - optimization_operators = ("magnitude_gate",) - - def __init__(self, algebra, channels: int = 1, grades=None): + def __init__(self, algebra, channels: int = 1): """Initialize Geometric GELU. Args: algebra (CliffordAlgebra): The algebra instance. channels (int): Number of channels. - grades: Optional declared active grades for compact lane metadata. """ super().__init__(algebra) - self.layout = _resolve_layout(algebra, grades) self.bias = nn.Parameter(torch.zeros(channels)) def forward(self, x: torch.Tensor) -> torch.Tensor: @@ -54,7 +46,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Activated multivector. """ - _check_multivector_lanes(x, self.algebra, self.layout, "GeometricGELU input") norm = x.norm(dim=-1, keepdim=True) eps = 1e-6 @@ -71,9 +62,6 @@ class GeometricSquare(CliffordModule): rotors can then rotate into the output. """ - optimization_operators = ("gp_self", "residual_gate") - optimization_dense_only_reason = "geometric square uses dense self-product; compact output-grade expansion is pending" - def __init__(self, algebra, channels: int = 1): super().__init__(algebra) # sigmoid(-2) ~= 0.12 -- starts small so GP doesn't dominate @@ -97,9 +85,6 @@ class GradeSwish(CliffordModule): grade_biases (torch.nn.Parameter): Biases for each grade gate. """ - optimization_operators = ("grade_gate",) - optimization_dense_only_reason = "grade swish uses dense grade-index buffers" - def __init__(self, algebra, channels: int = 1): """Initialize Grade Swish. @@ -144,22 +129,3 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: per_component_gate = gates.gather(-1, grade_idx) # [..., D] return x * per_component_gate - - -def _resolve_layout(algebra, grades) -> Optional[GradeLayout]: - if grades is None: - return None - return algebra.planner.layout(grades) - - -def _check_multivector_lanes(values: torch.Tensor, algebra, layout: Optional[GradeLayout], name: str) -> None: - if layout is None: - check_multivector(values, algebra, name) - return - if not VALIDATE: - return - assert values.ndim >= 1, f"{name}: expected ndim >= 1, got shape {tuple(values.shape)}" - assert values.shape[-1] == layout.dim, ( - f"{name}: last dim should be {layout.dim} for grades {layout.grades}, " - f"got {values.shape[-1]} (shape {tuple(values.shape)})" - ) diff --git a/functional/loss.py b/functional/loss.py index 9d89b4b..1a8f365 100644 --- a/functional/loss.py +++ b/functional/loss.py @@ -19,8 +19,6 @@ class GeometricMSELoss(CliffordModule): Standard MSE on coefficients. """ - optimization_operators = ("mse_loss",) - def __init__(self, algebra): """Initialize the geometric MSE loss.""" super().__init__(algebra) @@ -36,9 +34,6 @@ class SubspaceLoss(CliffordModule): Penalizes energy in forbidden grades. """ - optimization_operators = ("grade_penalty",) - optimization_dense_only_reason = "subspace loss reads a dense coefficient mask" - def __init__(self, algebra, target_indices: list = None, exclude_indices: list = None): """Initialize grade constraint penalties.""" super().__init__(algebra) @@ -67,9 +62,6 @@ class IsometryLoss(CliffordModule): Ensures transformations preserve the metric norm. """ - optimization_operators = ("metric_norm_loss",) - optimization_dense_only_reason = "isometry loss uses a dense metric diagonal" - def __init__(self, algebra): """Initialize isometry loss with metric diagonal.""" super().__init__(algebra) @@ -100,9 +92,6 @@ class BivectorRegularization(CliffordModule): Penalizes energy outside the target grade (default: grade 2). """ - optimization_operators = ("grade_projection", "regularization") - optimization_dense_only_reason = "bivector regularization calls dense grade projection" - def __init__(self, algebra, grade=2): """Initialize bivector regularization.""" super().__init__(algebra) @@ -123,9 +112,6 @@ class HermitianGradeRegularization(CliffordModule): energy across grades in a physically meaningful way. """ - optimization_operators = ("grade_spectrum", "regularization") - optimization_dense_only_reason = "Hermitian grade spectrum currently expects dense multivectors" - def __init__(self, algebra, target_spectrum=None): """Initialize grade regularization. diff --git a/functional/orthogonality.py b/functional/orthogonality.py index d02d172..c906015 100644 --- a/functional/orthogonality.py +++ b/functional/orthogonality.py @@ -97,9 +97,6 @@ class StrictOrthogonality(CliffordModule): when calling .to(device). """ - optimization_operators = ("grade_projection", "orthogonality") - optimization_dense_only_reason = "strict orthogonality uses dense grade masks" - def __init__(self, algebra, settings: Optional[OrthogonalitySettings] = None): super().__init__(algebra) diff --git a/layers/adapters/conformal.py b/layers/adapters/conformal.py index d9d1a00..dd6f958 100644 --- a/layers/adapters/conformal.py +++ b/layers/adapters/conformal.py @@ -28,11 +28,6 @@ class ConformalEmbedding(CliffordModule): euclidean_dim (int): Physical dimension d. """ - optimization_operators = ("embed",) - optimization_input_grades = None - optimization_output_grades = (1,) - optimization_dense_only_reason = "conformal embedding currently emits dense grade-1 multivectors" - def __init__(self, algebra: CliffordAlgebra, euclidean_dim: int): """Sets up the conformal embedding. diff --git a/layers/adapters/embedding.py b/layers/adapters/embedding.py index 0a7b390..ac4e7f1 100644 --- a/layers/adapters/embedding.py +++ b/layers/adapters/embedding.py @@ -5,14 +5,11 @@ # you may not use this file except in compliance with the License. # -from typing import Iterable, Optional - import torch import torch.nn as nn from core.foundation.module import CliffordModule - -from ..planning import lane_count, resolve_layer_layout +from core.runtime.algebra import CliffordAlgebra class MultivectorEmbedding(CliffordModule): @@ -28,45 +25,33 @@ class MultivectorEmbedding(CliffordModule): embedding (nn.Embedding): Underlying embedding table. """ - optimization_operators = ("embed",) - optimization_input_grades = None - - def __init__( - self, - algebra, - vocab_size: int, - channels: int, - grades: Optional[Iterable[int]] = None, - ): + def __init__(self, algebra: CliffordAlgebra, vocab_size: int, channels: int): """Sets up the multivector embedding. Args: algebra: Clifford algebra instance. vocab_size: Vocabulary size. channels: Number of multivector channels per token. - grades: Optional declared output grades. When set, the - embedding table stores compact lanes only. """ super().__init__(algebra) self.vocab_size = vocab_size self.channels = channels - self.layout = resolve_layer_layout(algebra, grades) - self.basis_dim = lane_count(algebra, self.layout) - # Single flat embedding: vocab_size -> channels * active basis lanes - self.embedding = nn.Embedding(vocab_size, channels * self.basis_dim) + # Single flat embedding: vocab_size -> channels * dim + self.embedding = nn.Embedding(vocab_size, channels * algebra.dim) self._init_grade1() def _init_grade1(self): """Initializes only grade-1 components; zeros out all others.""" with torch.no_grad(): + dim = self.algebra.dim channels = self.channels - dim = self.basis_dim - if self.layout is None: - grade1_flat = [i for i in range(dim) if bin(i).count("1") == 1] - else: - grade1_flat = [pos for pos, index in enumerate(self.layout.basis_indices) if bin(index).count("1") == 1] + # Build grade-1 mask (indices with exactly 1 bit set) + grade1_flat = [] + for i in range(dim): + if bin(i).count("1") == 1: + grade1_flat.append(i) # Zero everything self.embedding.weight.zero_() @@ -88,7 +73,7 @@ def forward(self, token_ids: torch.Tensor) -> torch.Tensor: """ B, L = token_ids.shape flat = self.embedding(token_ids) # [B, L, channels * dim] - return flat.reshape(B, L, self.channels, self.basis_dim) + return flat.reshape(B, L, self.channels, self.algebra.dim) class RotaryBivectorPE(CliffordModule): @@ -108,13 +93,9 @@ class RotaryBivectorPE(CliffordModule): bivector_indices (torch.Tensor): Indices of grade-2 basis elements. """ - optimization_operators = ("dense_sandwich",) - optimization_parameter_grades = (2,) - optimization_dense_only_reason = "positional rotor path still materializes dense multivectors" - def __init__( self, - algebra, + algebra: CliffordAlgebra, channels: int, max_seq_len: int, learnable: bool = True, @@ -131,13 +112,10 @@ def __init__( self.max_seq_len = max_seq_len self.learnable = learnable - # Identify grade-2 basis elements through the planner layout. - if algebra.n >= 2: - bivector_indices = algebra.planner.layout((2,)).indices_tensor(device=algebra.device) - else: - bivector_indices = torch.zeros(0, dtype=torch.long, device=algebra.device) - self.register_buffer("bivector_indices", bivector_indices) - self.num_bivectors = int(bivector_indices.numel()) + # Identify grade-2 basis elements + indices = [i for i in range(algebra.dim) if bin(i).count("1") == 2] + self.register_buffer("bivector_indices", torch.tensor(indices, dtype=torch.long)) + self.num_bivectors = len(indices) # Sinusoidal initialization init = self._sinusoidal_init(max_seq_len, self.num_bivectors) diff --git a/layers/adapters/mother.py b/layers/adapters/mother.py index 70c65cb..883fb5a 100644 --- a/layers/adapters/mother.py +++ b/layers/adapters/mother.py @@ -9,9 +9,9 @@ import torch.nn as nn from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from ..blocks.attention import GeometricProductAttention -from ..planning import lane_count, resolve_layer_layout from ..primitives.normalization import CliffordLayerNorm @@ -22,18 +22,7 @@ class MotherEmbedding(CliffordModule): reference frame, effectively aligning disparate geometric manifolds. """ - optimization_operators = ("embed",) - optimization_input_grades = None - - def __init__( - self, - algebra, - input_dim: int, - channels: int, - U: float = 0.0, - V: torch.Tensor = None, - grades=None, - ): + def __init__(self, algebra: CliffordAlgebra, input_dim: int, channels: int, U: float = 0.0, V: torch.Tensor = None): """Initializes the Mother Embedding. Args: @@ -42,12 +31,9 @@ def __init__( channels: Number of multivector channels. U: Geometric uncertainty index for manifold suppression. V: Fixed rotor proxy for Procrustes alignment (input_dim x input_dim). - grades: Optional declared output grades for compact lanes. """ super().__init__(algebra) self.channels = channels - self.layout = resolve_layer_layout(algebra, grades) - self.basis_dim = lane_count(algebra, self.layout) # Procrustes Alignment Matrix (Fixed Rotor Proxy) if V is None: @@ -55,8 +41,8 @@ def __init__( self.register_buffer("R_fixed", V) # Up-cast to Mother Algebra multivector channels - self.linear = nn.Linear(input_dim, channels * self.basis_dim) - self.norm = CliffordLayerNorm(algebra, channels, grades=grades) + self.linear = nn.Linear(input_dim, channels * algebra.dim) + self.norm = CliffordLayerNorm(algebra, channels) # Pre-condition LayerNorm scale with Uncertainty Index with torch.no_grad(): @@ -79,7 +65,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: x = x @ self.R_fixed.T # 2. Mother Projection - c = self.linear(x).view(-1, self.channels, self.basis_dim) + c = self.linear(x).view(-1, self.channels, self.algebra.dim) return self.norm(c) @@ -90,18 +76,7 @@ class EntropyGatedAttention(CliffordModule): or suppressed, allowing only coherent, synchronized states to propagate. """ - optimization_operators = ("grade_energy", "gate", "attention") - - def __init__( - self, - algebra, - channels: int, - num_heads: int, - eta: float = 1.0, - H_base: float = 0.5, - feature_grades=None, - score_grades=None, - ): + def __init__(self, algebra: CliffordAlgebra, channels: int, num_heads: int, eta: float = 1.0, H_base: float = 0.5): """Initializes Entropy-Gated Attention. Args: @@ -110,35 +85,17 @@ def __init__( num_heads: Number of attention heads. eta: Gating multiplier. H_base: Base entropy threshold. - feature_grades: Optional declared feature grades for compact lanes. - score_grades: Optional attention scoring grades. """ super().__init__(algebra) self.channels = channels self.eta = eta self.H_base = H_base - self.feature_layout = resolve_layer_layout(algebra, feature_grades) - self.feature_dim = lane_count(algebra, self.feature_layout) - self.base_attention = GeometricProductAttention( - algebra, - channels, - num_heads, - causal=False, - feature_grades=feature_grades, - score_grades=score_grades, - ) + self.base_attention = GeometricProductAttention(algebra, channels, num_heads, causal=False) # Cache bivector indices and float mask for compile-friendly gating - g2_idx = ( - _grade_positions(algebra, (2,), self.feature_layout) - if algebra.n >= 2 - else torch.zeros(0, dtype=torch.long, device=algebra.device) - ) - g2_mask = torch.zeros(self.feature_dim, dtype=torch.float32, device=algebra.device) - if g2_idx.numel() > 0: - g2_mask.index_fill_(0, g2_idx, 1.0) - self.register_buffer("g2_idx", g2_idx) - self.register_buffer("_g2_float_mask", g2_mask) + mask = self.algebra.grade_masks[2] + self.register_buffer("g2_idx", mask.nonzero(as_tuple=True)[0]) + self.register_buffer("_g2_float_mask", mask.float()) def forward( self, x: torch.Tensor, key_padding_mask: torch.Tensor = None, return_gating: bool = False @@ -193,29 +150,25 @@ class PhaseShiftHead(CliffordModule): high-grade component (G4) via a learned phase angle theta. """ - optimization_operators = ("grade_readout",) - optimization_output_grades = None - - def __init__(self, algebra, channels: int, feature_grades=None): + def __init__(self, algebra: CliffordAlgebra, channels: int): """Initializes the Phase-Shift Head. Args: algebra: Clifford algebra instance. channels: Number of channels to mix. - feature_grades: Optional declared feature grades for compact lanes. """ super().__init__(algebra) self.channels = channels - self.feature_layout = resolve_layer_layout(algebra, feature_grades) - self.register_buffer("g0_idx", _grade_positions(algebra, (0,), self.feature_layout)) # Learned phase angle theta self.theta = nn.Parameter(torch.randn(1, channels, 1) * 0.1) # Identify grade-4 pseudoscalar in Cl(3,1) - if algebra.n >= 4: - self.register_buffer("g4_idx", _grade_positions(algebra, (4,), self.feature_layout)) + mask_g4 = self.algebra.grade_masks[4] + if mask_g4.sum() > 0: + self.register_buffer("g4_idx", mask_g4.nonzero(as_tuple=True)[0]) else: - self.register_buffer("g4_idx", torch.zeros(0, dtype=torch.long, device=algebra.device)) + # Fallback if algebra doesn't have grade 4 + self.g4_idx = None def forward(self, x: torch.Tensor) -> torch.Tensor: """Mixes grades using pseudoscalar rotation. @@ -230,17 +183,12 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: x_pool = x.mean(dim=1) # [B, C, D] # Grade-0 (Scalar) - if len(self.g0_idx) > 0: - G0 = x_pool[..., self.g0_idx] - else: - G0 = x_pool.new_zeros(*x_pool.shape[:-1], 1) + G0 = x_pool[..., 0:1] # Grade-4 (High-grade/Pseudoscalar) - if len(self.g4_idx) > 0: - # For Cl(3,1), grade-4 has one lane. Higher-dimensional compact - # layouts can expose many grade-4 lanes, so reduce them to a scalar - # phase signal for this head. - G4 = x_pool[..., self.g4_idx].mean(dim=-1, keepdim=True) + if self.g4_idx is not None and len(self.g4_idx) > 0: + # For Cl(3,1), index 15 is typical + G4 = x_pool[..., self.g4_idx] else: G4 = torch.zeros_like(G0) @@ -251,13 +199,3 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # Mean across channels for final scalar output return result.mean(dim=1) # [B, 1] - - -def _grade_positions(algebra, grades, source_layout) -> torch.Tensor: - """Return positions for ``grades`` in dense lanes or a compact source layout.""" - target_layout = algebra.planner.layout(grades) - if source_layout is None: - return target_layout.indices_tensor(device=algebra.device) - position_by_basis = {index: position for position, index in enumerate(source_layout.basis_indices)} - positions = [position_by_basis[index] for index in target_layout.basis_indices if index in position_by_basis] - return torch.tensor(positions, dtype=torch.long, device=algebra.device) diff --git a/layers/adapters/projective.py b/layers/adapters/projective.py index f87e7c0..200ca9a 100644 --- a/layers/adapters/projective.py +++ b/layers/adapters/projective.py @@ -33,11 +33,6 @@ class ProjectiveEmbedding(CliffordModule): euclidean_dim (int): Physical dimension d. """ - optimization_operators = ("embed",) - optimization_input_grades = None - optimization_output_grades = (1,) - optimization_dense_only_reason = "projective embedding currently emits dense grade-1 multivectors" - def __init__(self, algebra: CliffordAlgebra, euclidean_dim: int): """Sets up the projective embedding. diff --git a/layers/blocks/attention.py b/layers/blocks/attention.py index 3aad773..2caebbe 100644 --- a/layers/blocks/attention.py +++ b/layers/blocks/attention.py @@ -11,10 +11,9 @@ import torch.nn as nn import torch.nn.functional as F -from core.foundation.basis import normalize_grades, reverse_sign from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra -from ..planning import check_multivector_lanes, lane_count, resolve_layer_layout from ..primitives.linear import CliffordLinear # Memory-bounded block size for chunked attention computation @@ -45,18 +44,14 @@ class GeometricProductAttention(CliffordModule): bivector_weight (float): lambda_ - weight of bivector score component. """ - optimization_operators = ("linear", "gp_score", "softmax", "linear") - def __init__( self, - algebra, + algebra: CliffordAlgebra, channels: int, num_heads: int, causal: bool = True, bivector_weight: float = 0.5, dropout: float = 0.0, - feature_grades=None, - score_grades=None, score_blade_chunk_size: int = _G2_BLADE_CHUNK_SIZE, score_precompute_limit: int = _SCORE_PRECOMPUTE_LIMIT, ): @@ -69,12 +64,6 @@ def __init__( causal: Apply causal mask for autoregressive generation. bivector_weight: lambda_ weight on bivector score component. dropout: Dropout rate on attention weights. - feature_grades: Optional active grades carried by this attention layer. - When set, projections and attention values use compact lanes. - score_grades: Optional declared grades for compact planned scoring. - ``None`` preserves exact dense scoring over all basis lanes unless - ``feature_grades`` is set, in which case those feature grades are - also used for scoring. score_blade_chunk_size: Grade-2 output blades processed per dense chunk when exact dense scoring is used. score_precompute_limit: Maximum temporary ``K_g2`` elements allowed @@ -88,19 +77,14 @@ def __init__( self.head_channels = channels // num_heads self.causal = causal self.bivector_weight = bivector_weight - self.feature_layout = resolve_layer_layout(algebra, feature_grades) - self.feature_dim = lane_count(algebra, self.feature_layout) - if score_grades is None and feature_grades is not None: - score_grades = feature_grades - self.score_grades = None if score_grades is None else normalize_grades(score_grades, algebra.n) self.score_blade_chunk_size = max(1, int(score_blade_chunk_size)) self.score_precompute_limit = max(0, int(score_precompute_limit)) # Q, K, V projections operate on [B*L, channels, dim] - self.q_proj = CliffordLinear(algebra, channels, channels, grades=feature_grades) - self.k_proj = CliffordLinear(algebra, channels, channels, grades=feature_grades) - self.v_proj = CliffordLinear(algebra, channels, channels, grades=feature_grades) - self.out_proj = CliffordLinear(algebra, channels, channels, grades=feature_grades) + self.q_proj = CliffordLinear(algebra, channels, channels) + self.k_proj = CliffordLinear(algebra, channels, channels) + self.v_proj = CliffordLinear(algebra, channels, channels) + self.out_proj = CliffordLinear(algebra, channels, channels) self.attn_dropout = nn.Dropout(dropout) if dropout > 0.0 else None @@ -108,53 +92,12 @@ def __init__( self._precompute_score_tables() def _precompute_score_tables(self): - """Precompute exact dense or compact-planned attention score routes. - - Dense scoring is exact for existing callers and chunks grade-2 blades - instead of materializing ``[B, H, L, Hc, n_g2, D]``. Compact scoring - uses static grade product plans when ``score_grades`` declares a layout. - """ + """Precompute exact dense attention score routes.""" alg = self.algebra D = alg.dim - self._score_layout = None - self._score_scalar_product = None - self._score_bivector_product = None - self._score_scale_dim = self.feature_dim if self.feature_layout is not None else D - if self.score_grades is not None: - self.n_g2 = alg.n * (alg.n - 1) // 2 - self._score_layout = alg.planner.layout(self.score_grades) - layout_indices = self._score_input_positions(device=alg.device) - rev_signs = torch.tensor( - [reverse_sign(index) for index in self._score_layout.basis_indices], - dtype=torch.float32, - device=alg.device, - ) - self.register_buffer("_score_layout_indices", layout_indices) - self.register_buffer("_score_rev_signs", rev_signs) - self._score_scalar_product = alg.planner.product_executor( - op="gp", - left_grades=self.score_grades, - right_grades=self.score_grades, - output_grades=(0,), - device=alg.device, - dtype=alg.dtype, - cache=False, - ) - if self.n_g2 > 0: - self._score_bivector_product = alg.planner.product_executor( - op="gp", - left_grades=self.score_grades, - right_grades=self.score_grades, - output_grades=(2,), - device=alg.device, - dtype=alg.dtype, - cache=False, - ) - return - if not hasattr(alg, "gp_signs") or not hasattr(alg, "rev_signs"): - raise ValueError("Dense attention scoring requires CliffordAlgebra; pass score_grades for AlgebraContext.") + raise ValueError("GeometricProductAttention currently requires dense CliffordAlgebra inputs.") # Grade-0 metric: metric_rev[a] = gp_signs[a, 0] * rev_signs[a] # gp_signs[a, 0] is the sign when A[a] * B[a] contributes to output blade 0 @@ -166,17 +109,12 @@ def _precompute_score_tables(self): self.register_buffer("_g2_blades", torch.tensor(g2_blades, dtype=torch.long, device=alg.device)) self.register_buffer("_basis_indices", torch.arange(D, dtype=torch.long, device=alg.device)) - self.register_buffer("_score_layout_indices", torch.zeros(0, dtype=torch.long, device=alg.device)) - self.register_buffer("_score_rev_signs", torch.zeros(0, device=alg.device)) - def _compute_score( self, q_head: torch.Tensor, k_head: torch.Tensor, ) -> torch.Tensor: """Compute GA attention scores for one query block.""" - if self._score_layout is not None: - return self._compute_score_compact(q_head, k_head) return self._compute_score_dense(q_head, k_head) def _compute_score_dense(self, q_head: torch.Tensor, k_head: torch.Tensor) -> torch.Tensor: @@ -209,7 +147,7 @@ def _compute_score_dense(self, q_head: torch.Tensor, k_head: torch.Tensor) -> to score_g2 = torch.zeros_like(score_g0) # Combined score - scale = math.sqrt(self.head_channels * self._score_scale_dim) + scale = math.sqrt(self.head_channels * self.algebra.dim) return (score_g0 + self.bivector_weight * score_g2) / scale def _dense_score_g2_precomputed(self, q_2d, k_head, B, H, Hc, Lq, Lk, D, n_g2): @@ -245,43 +183,6 @@ def _dense_score_g2_chunked(self, q_2d, k_2d, B, H, Hc, Lq, Lk, D, n_g2): score_g2_sq = score_g2_sq + comp_sq.reshape(B, H, Hc, Lq, Lk).sum(2) return score_g2_sq - def _compute_score_compact(self, q_head: torch.Tensor, k_head: torch.Tensor) -> torch.Tensor: - """Declared-grade score using static compact product plans.""" - B, H, Lq, Hc, _ = q_head.shape - Lk = k_head.shape[2] - - q_values = torch.index_select(q_head, -1, self._score_layout_indices) - k_values = torch.index_select(k_head, -1, self._score_layout_indices) * self._score_rev_signs.to( - dtype=k_head.dtype - ) - - q_2d = q_values.permute(0, 1, 3, 2, 4).reshape(B * H * Hc, Lq, self._score_layout.dim) - k_2d = k_values.permute(0, 1, 3, 2, 4).reshape(B * H * Hc, Lk, self._score_layout.dim) - - scalar = self._score_scalar_product.forward_pairwise_compact(q_2d, k_2d).squeeze(-1) - score_g0 = scalar.reshape(B, H, Hc, Lq, Lk).sum(2) - - if self._score_bivector_product is not None: - bivectors = self._score_bivector_product.forward_pairwise_compact(q_2d, k_2d) - score_g2_sq = bivectors.pow(2).sum(-1).reshape(B, H, Hc, Lq, Lk).sum(2) - score_g2 = score_g2_sq.sqrt() - else: - score_g2 = torch.zeros_like(score_g0) - - scale = math.sqrt(self.head_channels * self._score_scale_dim) - return (score_g0 + self.bivector_weight * score_g2) / scale - - def _score_input_positions(self, *, device) -> torch.Tensor: - """Return score-lane positions in dense or declared feature storage.""" - if self.feature_layout is None: - return self._score_layout.indices_tensor(device=device) - position_by_basis = {index: position for position, index in enumerate(self.feature_layout.basis_indices)} - missing = tuple(index for index in self._score_layout.basis_indices if index not in position_by_basis) - if missing: - raise ValueError("score_grades must be contained in feature_grades for compact attention") - positions = [position_by_basis[index] for index in self._score_layout.basis_indices] - return torch.tensor(positions, dtype=torch.long, device=device) - def forward(self, x: torch.Tensor, key_padding_mask: torch.Tensor = None) -> torch.Tensor: """Computes geometric product attention. @@ -292,7 +193,6 @@ def forward(self, x: torch.Tensor, key_padding_mask: torch.Tensor = None) -> tor Returns: Output multivectors [B, L, C, D]. """ - check_multivector_lanes(x, self.algebra, self.feature_layout, "GeometricProductAttention input") B, L, C, D = x.shape # Project Q, K, V (CliffordLinear expects [B, C, D]) diff --git a/layers/blocks/multi_rotor_ffn.py b/layers/blocks/multi_rotor_ffn.py index 7ebf285..1feb76f 100644 --- a/layers/blocks/multi_rotor_ffn.py +++ b/layers/blocks/multi_rotor_ffn.py @@ -5,12 +5,10 @@ # you may not use this file except in compliance with the License. # -from typing import Optional - import torch -import torch.nn as nn from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from functional.activation import GeometricGELU from ..primitives.linear import CliffordLinear @@ -49,32 +47,23 @@ class MultiRotorFFN(CliffordModule): def __init__( self, - algebra, + algebra: CliffordAlgebra, channels: int, ffn_mult: int = 4, num_rotors: int = 8, use_rotor_backend: bool = False, - feature_grades=None, - use_rotor_toolbox: Optional[bool] = None, ): super().__init__(algebra) self.channels = channels ffn_channels = channels * ffn_mult backend = "rotor" if use_rotor_backend else "traditional" - if use_rotor_toolbox is None: - use_rotor_toolbox = feature_grades is None - if feature_grades is not None and use_rotor_toolbox: - raise ValueError("MultiRotorFFN rotor toolbox requires dense feature lanes") - self.use_rotor_toolbox = bool(use_rotor_toolbox) - - self.expand = CliffordLinear(algebra, channels, ffn_channels, backend=backend, grades=feature_grades) - self.norm = CliffordLayerNorm(algebra, ffn_channels, grades=feature_grades) - self.toolbox = ( - MultiRotorLayer(algebra, ffn_channels, num_rotors) if self.use_rotor_toolbox else nn.Identity() - ) - self.act = GeometricGELU(algebra, channels=ffn_channels, grades=feature_grades) - self.contract = CliffordLinear(algebra, ffn_channels, channels, backend=backend, grades=feature_grades) - self.gate = BladeSelector(algebra, channels, grades=feature_grades) + + self.expand = CliffordLinear(algebra, channels, ffn_channels, backend=backend) + self.norm = CliffordLayerNorm(algebra, ffn_channels) + self.toolbox = MultiRotorLayer(algebra, ffn_channels, num_rotors) + self.act = GeometricGELU(algebra, channels=ffn_channels) + self.contract = CliffordLinear(algebra, ffn_channels, channels, backend=backend) + self.gate = BladeSelector(algebra, channels) def forward(self, x) -> torch.Tensor: """Applies the geometric toolbox FFN. diff --git a/layers/blocks/transformer.py b/layers/blocks/transformer.py index 4096326..0c5459f 100644 --- a/layers/blocks/transformer.py +++ b/layers/blocks/transformer.py @@ -5,12 +5,11 @@ # you may not use this file except in compliance with the License. # -from typing import Optional - import torch import torch.nn as nn from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from ..adapters.mother import EntropyGatedAttention from ..primitives.normalization import CliffordLayerNorm @@ -32,7 +31,7 @@ class GeometricTransformerBlock(CliffordModule): def __init__( self, - algebra, + algebra: CliffordAlgebra, channels: int, num_heads: int = 4, num_rotors: int = 8, @@ -40,9 +39,6 @@ def __init__( use_entropy_gating: bool = False, eta: float = 1.5, H_base: float = 0.5, - feature_grades=None, - attention_score_grades=None, - use_ffn_rotor_toolbox: Optional[bool] = None, ): """Initializes the Geometric Transformer Block. @@ -55,43 +51,22 @@ def __init__( use_entropy_gating: If True, uses EntropyGatedAttention. eta: Gating multiplier for entropy attention. H_base: Base entropy threshold. - feature_grades: Optional declared feature grades for compact execution. - attention_score_grades: Optional score grades for attention; defaults to - ``feature_grades`` when compact execution is used. - use_ffn_rotor_toolbox: Whether to use the dense rotor toolbox in the - FFN. Defaults to disabled when ``feature_grades`` is declared. """ super().__init__(algebra) - if use_entropy_gating and feature_grades is not None: - raise ValueError("Entropy-gated attention does not yet support compact feature grades") self.use_entropy_gating = use_entropy_gating - self.norm1 = CliffordLayerNorm(algebra, channels, grades=feature_grades) + self.norm1 = CliffordLayerNorm(algebra, channels) if use_entropy_gating: self.attn = EntropyGatedAttention(algebra, channels, num_heads, eta=eta, H_base=H_base) else: - self.attn = GeometricProductAttention( - algebra, - channels, - num_heads, - causal=False, - dropout=dropout, - feature_grades=feature_grades, - score_grades=attention_score_grades, - ) - - self.norm2 = CliffordLayerNorm(algebra, channels, grades=feature_grades) + self.attn = GeometricProductAttention(algebra, channels, num_heads, causal=False, dropout=dropout) + + self.norm2 = CliffordLayerNorm(algebra, channels) # Check MultiRotorFFN class name in multi_rotor_ffn.py from .multi_rotor_ffn import MultiRotorFFN - self.ffn = MultiRotorFFN( - algebra, - channels, - num_rotors=num_rotors, - feature_grades=feature_grades, - use_rotor_toolbox=use_ffn_rotor_toolbox, - ) + self.ffn = MultiRotorFFN(algebra, channels, num_rotors=num_rotors) def forward( self, x: torch.Tensor, key_padding_mask: torch.Tensor = None, return_state: bool = False diff --git a/layers/planning.py b/layers/planning.py deleted file mode 100644 index 4cea72c..0000000 --- a/layers/planning.py +++ /dev/null @@ -1,55 +0,0 @@ -# Versor: Universal Geometric Algebra Neural Network -# Copyright (C) 2026 Eunkyum Kim -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# - -"""Layer helpers for declared compact grade layouts.""" - -from __future__ import annotations - -from typing import Iterable, Optional - -import torch - -from core.foundation.layout import GradeLayout -from core.foundation.module import AlgebraLike -from core.foundation.validation import VALIDATE, check_multivector - -__all__ = [ - "resolve_layer_layout", - "lane_count", - "check_multivector_lanes", -] - - -def resolve_layer_layout(algebra: AlgebraLike, grades: Optional[Iterable[int]]) -> Optional[GradeLayout]: - """Return a compact layout for declared grades, or ``None`` for dense lanes.""" - if grades is None: - return None - return algebra.planner.layout(grades) - - -def lane_count(algebra: AlgebraLike, layout: Optional[GradeLayout]) -> int: - """Return the active basis-lane count for a declared layout.""" - return algebra.dim if layout is None else layout.dim - - -def check_multivector_lanes( - values: torch.Tensor, - algebra: AlgebraLike, - layout: Optional[GradeLayout], - name: str, -) -> None: - """Validate dense or declared compact multivector lanes.""" - if layout is None: - check_multivector(values, algebra, name) - return - if not VALIDATE: - return - assert values.ndim >= 1, f"{name}: expected ndim >= 1, got shape {tuple(values.shape)}" - assert values.shape[-1] == layout.dim, ( - f"{name}: last dim should be {layout.dim} for grades {layout.grades}, " - f"got {values.shape[-1]} (shape {tuple(values.shape)})" - ) diff --git a/layers/primitives/linear.py b/layers/primitives/linear.py index 5440a76..a678506 100644 --- a/layers/primitives/linear.py +++ b/layers/primitives/linear.py @@ -10,15 +10,14 @@ Supports traditional matrix-based mixing and parameter-efficient rotor-based backends. """ -from typing import Iterable, Literal, Optional +from typing import Literal, Optional import torch import torch.nn as nn from core.foundation.module import CliffordModule -from core.foundation.validation import check_channels - -from ..planning import check_multivector_lanes, lane_count, resolve_layer_layout +from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class CliffordLinear(CliffordModule): @@ -43,14 +42,13 @@ class CliffordLinear(CliffordModule): def __init__( self, - algebra, + algebra: CliffordAlgebra, in_channels: int, out_channels: int, backend: Literal["traditional", "rotor"] = "traditional", num_rotor_pairs: int = 4, aggregation: Literal["mean", "sum", "learned"] = "mean", shuffle: Literal["none", "fixed", "random"] = "none", - grades: Optional[Iterable[int]] = None, ): """Initialize Clifford Linear. @@ -66,28 +64,19 @@ def __init__( - 'none': No shuffle (default) - 'fixed': Fixed random permutation - 'random': Random permutation each forward pass - grades: Optional declared active grades. When set, the traditional - backend operates on compact lanes for those grades instead of - requiring a full dense multivector width. """ super().__init__(algebra) self.in_channels = in_channels self.out_channels = out_channels self.backend = backend - self.optimization_operators = (f"linear:{backend}",) - self.layout = resolve_layer_layout(algebra, grades) - self.basis_dim = lane_count(algebra, self.layout) if backend == "traditional": self.weight = nn.Parameter(torch.Tensor(out_channels, in_channels)) - self.bias = nn.Parameter(torch.Tensor(out_channels, self.basis_dim)) + self.bias = nn.Parameter(torch.Tensor(out_channels, algebra.dim)) self.reset_parameters() self.gadget = None elif backend == "rotor": - if self.layout is not None: - raise ValueError("CliffordLinear rotor backend does not yet support compact grade declarations") - self.optimization_dense_only_reason = "rotor backend requires dense sandwich execution" from .rotor_gadget import RotorGadget self.gadget = RotorGadget( @@ -120,7 +109,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Output [Batch, Out, Dim]. """ - check_multivector_lanes(x, self.algebra, self.layout, "CliffordLinear input") + check_multivector(x, self.algebra, "CliffordLinear input") check_channels(x, self.in_channels, "CliffordLinear input") if self.backend == "traditional": @@ -142,7 +131,6 @@ def extra_repr(self) -> str: str: Layer parameters description """ if self.backend == "traditional": - grades = "" if self.layout is None else f", grades={self.layout.grades}" - return f"in_channels={self.in_channels}, out_channels={self.out_channels}, backend=traditional{grades}" + return f"in_channels={self.in_channels}, out_channels={self.out_channels}, backend=traditional" else: return f"in_channels={self.in_channels}, out_channels={self.out_channels}, backend=rotor" diff --git a/layers/primitives/multi_rotor.py b/layers/primitives/multi_rotor.py index 2dee568..16addb9 100644 --- a/layers/primitives/multi_rotor.py +++ b/layers/primitives/multi_rotor.py @@ -15,6 +15,7 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class MultiRotorLayer(CliffordModule): @@ -34,12 +35,9 @@ class MultiRotorLayer(CliffordModule): weights (nn.Parameter): Mixing weights [channels, num_rotors]. """ - optimization_operators = ("dense_sandwich",) - optimization_dense_only_reason = "sandwich path still materializes dense multivectors" - def __init__( self, - algebra, + algebra: CliffordAlgebra, channels: int, num_rotors: int = 8, grade: int = 2, @@ -59,8 +57,8 @@ def __init__( self.num_rotors = num_rotors self.grade = grade - grade_layout = algebra.planner.layout((grade,)) - self.register_buffer("grade_indices", grade_layout.indices_tensor(device=algebra.device)) + grade_mask = algebra.grade_masks[grade] + self.register_buffer("grade_indices", grade_mask.nonzero(as_tuple=False).squeeze(-1)) self.num_grade_elements = len(self.grade_indices) self.rotor_grade_weights = nn.Parameter(torch.Tensor(num_rotors, self.num_grade_elements)) diff --git a/layers/primitives/normalization.py b/layers/primitives/normalization.py index 56f7d8e..149566b 100644 --- a/layers/primitives/normalization.py +++ b/layers/primitives/normalization.py @@ -5,14 +5,11 @@ # you may not use this file except in compliance with the License. # -from typing import Iterable, Optional - import torch import torch.nn as nn from core.foundation.module import CliffordModule - -from ..planning import check_multivector_lanes, lane_count, resolve_layer_layout +from core.runtime.algebra import CliffordAlgebra class CliffordLayerNorm(CliffordModule): @@ -30,16 +27,7 @@ class CliffordLayerNorm(CliffordModule): starts identical to the old (scale-discarding) behaviour. """ - optimization_operators = ("normalize",) - - def __init__( - self, - algebra, - channels: int, - eps: float = 1e-6, - recover: bool = True, - grades: Optional[Iterable[int]] = None, - ): + def __init__(self, algebra: CliffordAlgebra, channels: int, eps: float = 1e-6, recover: bool = True): """Sets up normalization. Args: @@ -47,17 +35,13 @@ def __init__( channels (int): Features. eps (float): Stability term. recover (bool): Whether to inject original scale into the scalar part. - grades: Optional declared grades for compact lane execution. """ super().__init__(algebra) self.eps = eps self.recover = recover - self.layout = resolve_layer_layout(algebra, grades) - self.basis_dim = lane_count(algebra, self.layout) self.weight = nn.Parameter(torch.ones(channels)) self.bias = nn.Parameter(torch.zeros(channels)) - self.register_buffer("_scalar_lane_mask", self._build_scalar_lane_mask()) # Learnable gate: how much of the original log-magnitude to push # into the scalar part. Zero-init -> backward compatible at start. if recover: @@ -74,8 +58,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Normalized input. """ - check_multivector_lanes(x, self.algebra, self.layout, "CliffordLayerNorm input") - # Per-channel magnitude norm = x.norm(dim=-1, keepdim=True) # [B, C, 1] @@ -85,8 +67,8 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # Affine transform on direction out = x_normalized * self.weight.view(1, -1, 1) - # Add bias and optional log-magnitude to the declared scalar lane. - g0 = self._scalar_lane_mask + # Add bias and optional log-magnitude to grade-0 via mask + g0 = self.algebra.grade_masks_float[0] # [D], 1.0 at index 0 if g0.dtype != x.dtype: g0 = g0.to(dtype=x.dtype) out = out + self.bias.view(1, -1, 1) * g0 @@ -98,15 +80,3 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: out = out + self.norm_scale.view(1, -1, 1) * log_norm * g0 return out - - def _build_scalar_lane_mask(self) -> torch.Tensor: - """Return a lane mask with 1 at scalar basis position when present.""" - mask = torch.zeros(self.basis_dim, dtype=self.algebra.dtype, device=self.algebra.device) - if self.layout is None: - mask[0] = 1.0 - return mask - try: - mask[self.layout.basis_indices.index(0)] = 1.0 - except ValueError: - pass - return mask diff --git a/layers/primitives/projection.py b/layers/primitives/projection.py index eeea357..4df6aa3 100644 --- a/layers/primitives/projection.py +++ b/layers/primitives/projection.py @@ -5,16 +5,13 @@ # you may not use this file except in compliance with the License. # -from typing import Iterable, Optional - import torch import torch.nn as nn from core.foundation.module import CliffordModule +from core.runtime.algebra import CliffordAlgebra from utils.compat import safe_linalg_solve -from ..planning import check_multivector_lanes, lane_count, resolve_layer_layout - class BladeSelector(CliffordModule): """Blade Selector. Filters insignificant components. @@ -25,21 +22,16 @@ class BladeSelector(CliffordModule): weights (nn.Parameter): Soft gates [Channels, Dim]. """ - optimization_operators = ("blade_gate",) - - def __init__(self, algebra, channels: int, grades: Optional[Iterable[int]] = None): + def __init__(self, algebra: CliffordAlgebra, channels: int): """Sets up the selector. Args: algebra (CliffordAlgebra): The algebra instance. channels (int): Input features. - grades: Optional declared grades for compact lane execution. """ super().__init__(algebra) - self.layout = resolve_layer_layout(algebra, grades) - self.basis_dim = lane_count(algebra, self.layout) - self.weights = nn.Parameter(torch.Tensor(channels, self.basis_dim)) + self.weights = nn.Parameter(torch.Tensor(channels, algebra.dim)) self.reset_parameters() @@ -56,7 +48,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Filtered input. """ - check_multivector_lanes(x, self.algebra, self.layout, "BladeSelector input") # Sigmoid gate w = torch.sigmoid(self.weights).unsqueeze(0) return x * w @@ -76,10 +67,7 @@ class GeometricNeutralizer(CliffordModule): momentum (float): EMA momentum. """ - optimization_operators = ("grade_projection", "linear_solve") - optimization_dense_only_reason = "neutralizer reads fixed dense grade positions" - - def __init__(self, algebra, channels: int, momentum: float = 0.1): + def __init__(self, algebra: CliffordAlgebra, channels: int, momentum: float = 0.1): """Initialize the neutralizer. Args: @@ -91,13 +79,9 @@ def __init__(self, algebra, channels: int, momentum: float = 0.1): self.channels = channels self.momentum = momentum - # Get indices for Grade-0 and Grade-2 through planner layouts. - self.register_buffer("g0_idx", algebra.planner.layout((0,)).indices_tensor(device=algebra.device)) - if algebra.n >= 2: - g2_idx = algebra.planner.layout((2,)).indices_tensor(device=algebra.device) - else: - g2_idx = torch.zeros(0, dtype=torch.long, device=algebra.device) - self.register_buffer("g2_idx", g2_idx) + # Get indices for Grade-0 and Grade-2 + self.register_buffer("g0_idx", algebra.grade_masks[0].nonzero(as_tuple=False).squeeze(-1)) + self.register_buffer("g2_idx", algebra.grade_masks[2].nonzero(as_tuple=False).squeeze(-1)) # Dimensions for Cl(3,1): Grade-0 is 1, Grade-2 is 6 self.d0 = len(self.g0_idx) diff --git a/layers/primitives/reflection.py b/layers/primitives/reflection.py index ff15846..ac3d388 100644 --- a/layers/primitives/reflection.py +++ b/layers/primitives/reflection.py @@ -10,6 +10,7 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class ReflectionLayer(CliffordModule): @@ -29,11 +30,7 @@ class ReflectionLayer(CliffordModule): vector_weights (nn.Parameter): Learnable grade-1 coefficients [C, n]. """ - optimization_operators = ("dense_sandwich",) - optimization_parameter_grades = (1,) - optimization_dense_only_reason = "reflection path still materializes dense multivectors" - - def __init__(self, algebra, channels: int): + def __init__(self, algebra: CliffordAlgebra, channels: int): """Initialize the reflection layer. Args: @@ -44,7 +41,8 @@ def __init__(self, algebra, channels: int): self.channels = channels # Grade-1 indices: 2^0, 2^1, ..., 2^(n-1) - self.register_buffer("vector_indices", algebra.planner.layout((1,)).indices_tensor(device=algebra.device)) + g1_mask = algebra.grade_masks[1] + self.register_buffer("vector_indices", g1_mask.nonzero(as_tuple=False).squeeze(-1)) self.num_vectors = algebra.n self.vector_weights = nn.Parameter(torch.Tensor(channels, self.num_vectors)) diff --git a/layers/primitives/rotor.py b/layers/primitives/rotor.py index 6a49661..32d126d 100644 --- a/layers/primitives/rotor.py +++ b/layers/primitives/rotor.py @@ -10,6 +10,7 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class RotorLayer(CliffordModule): @@ -30,12 +31,9 @@ class RotorLayer(CliffordModule): grade_weights (nn.Parameter): Learnable grade-k coefficients [channels, num_grade_elements]. """ - optimization_operators = ("dense_sandwich",) - optimization_dense_only_reason = "sandwich path still materializes dense multivectors" - def __init__( self, - algebra, + algebra: CliffordAlgebra, channels: int, grade: int = 2, ): @@ -53,8 +51,8 @@ def __init__( self.channels = channels self.grade = grade - grade_layout = algebra.planner.layout((grade,)) - self.register_buffer("grade_indices", grade_layout.indices_tensor(device=algebra.device)) + grade_mask = algebra.grade_masks[grade] + self.register_buffer("grade_indices", grade_mask.nonzero(as_tuple=False).squeeze(-1)) self.num_grade_elements = len(self.grade_indices) self.grade_weights = nn.Parameter(torch.Tensor(channels, self.num_grade_elements)) diff --git a/layers/primitives/rotor_gadget.py b/layers/primitives/rotor_gadget.py index 4e075c6..708aeea 100644 --- a/layers/primitives/rotor_gadget.py +++ b/layers/primitives/rotor_gadget.py @@ -16,6 +16,7 @@ from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector +from core.runtime.algebra import CliffordAlgebra class RotorGadget(CliffordModule): @@ -42,13 +43,9 @@ class RotorGadget(CliffordModule): aggregation: Aggregation method ('mean', 'sum', or 'learned') """ - optimization_operators = ("dense_rotor_toolbox",) - optimization_parameter_grades = (2,) - optimization_dense_only_reason = "rotor gadget path still materializes dense multivectors" - def __init__( self, - algebra, + algebra: CliffordAlgebra, in_channels: int, out_channels: int, num_rotor_pairs: int = 4, @@ -78,9 +75,10 @@ def __init__( self.aggregation = aggregation self.shuffle = shuffle - # Use the algebra planner layout for static bivector indices. + # Use algebra's precomputed grade masks for bivector indices if algebra.num_grades > 2: - self.register_buffer("bivector_indices", algebra.planner.layout((2,)).indices_tensor(device=algebra.device)) + bv_mask = algebra.grade_masks[2] + self.register_buffer("bivector_indices", bv_mask.nonzero(as_tuple=False).squeeze(-1)) else: self.register_buffer("bivector_indices", torch.tensor([], dtype=torch.long, device=algebra.device)) self.num_bivectors = len(self.bivector_indices) diff --git a/models/deap/eeg_net.py b/models/deap/eeg_net.py index 7fc51ed..bf4de26 100644 --- a/models/deap/eeg_net.py +++ b/models/deap/eeg_net.py @@ -41,9 +41,6 @@ class MultiTargetPhaseShiftHead(CliffordModule): each target can independently shift its prediction range. """ - optimization_operators = ("dense_readout",) - optimization_dense_only_reason = "DEAP phase-shift head flattens dense multivectors for readout" - def __init__(self, algebra: AlgebraLike, channels: int, num_targets: int = 4): super().__init__(algebra) self.channels = channels diff --git a/models/lqa/heads.py b/models/lqa/heads.py index 87791d2..13bdc3f 100644 --- a/models/lqa/heads.py +++ b/models/lqa/heads.py @@ -35,9 +35,6 @@ class ChainReasoningHead(CliffordModule): for relation composition, with soft gating selecting the composition. """ - optimization_operators = ("dense_rotor_bank", "grade_readout") - optimization_dense_only_reason = "chain head applies a dense relation-rotor bank" - def __init__(self, algebra: CliffordAlgebra, channels: int, num_relations: int = 18, hidden_dim: int = 64): super().__init__(algebra) self.channels = channels @@ -111,9 +108,6 @@ class EntailmentHead(CliffordModule): giving the model asymmetry for free from the algebra. """ - optimization_operators = ("gp", "grade_readout") - optimization_dense_only_reason = "entailment head computes dense product features" - def __init__(self, algebra: CliffordAlgebra, channels: int, hidden_dim: int = 64): super().__init__(algebra) self.channels = channels @@ -198,9 +192,6 @@ class NegationHead(CliffordModule): relational noise (grade-2). """ - optimization_operators = ("gp", "grade_involution", "grade_readout") - optimization_dense_only_reason = "negation head computes dense product and involution features" - def __init__(self, algebra: CliffordAlgebra, channels: int, hidden_dim: int = 64): super().__init__(algebra) self.channels = channels diff --git a/models/md17/forcenet.py b/models/md17/forcenet.py index 730493f..c96bffa 100644 --- a/models/md17/forcenet.py +++ b/models/md17/forcenet.py @@ -47,10 +47,6 @@ class DynamicRotorGenerator(CliffordModule): as identity (exp(0) = 1). """ - optimization_operators = ("dense_rotor_generation",) - optimization_parameter_grades = (2,) - optimization_dense_only_reason = "dynamic rotor generator materializes dense rotor multivectors" - def __init__(self, algebra: CliffordAlgebra, input_dim: int, num_dynamic_rotors: int = 4): super().__init__(algebra) self.num_dynamic_rotors = num_dynamic_rotors @@ -108,9 +104,6 @@ class MD17InteractionBlock(CliffordModule): optionally uses GeometricSquare activation for algebraic cross-terms. """ - optimization_operators = ("gp_message", "dense_rotor_action", "scatter_message") - optimization_dense_only_reason = "MD17 interaction block builds dense action matrices" - def __init__( self, algebra: CliffordAlgebra, diff --git a/models/sr/net.py b/models/sr/net.py index c45288a..8901003 100644 --- a/models/sr/net.py +++ b/models/sr/net.py @@ -109,11 +109,6 @@ class SRMultiGradeEmbedding(CliffordModule): grade1_proj (nn.Linear): k -> C*n_g1 projection. """ - optimization_operators = ("embed",) - optimization_input_grades = None - optimization_output_grades = (0, 1) - optimization_dense_only_reason = "SR embedding currently emits dense grade-0/grade-1 multivectors" - def __init__( self, algebra: CliffordAlgebra, diff --git a/tasks/base.py b/tasks/base.py index d23489d..9d4c591 100644 --- a/tasks/base.py +++ b/tasks/base.py @@ -14,7 +14,6 @@ from tqdm import tqdm from core.foundation.device import DeviceConfig, resolve_device -from core.planning import inspect_module_optimization from log import get_logger logger = get_logger(__name__) @@ -59,20 +58,6 @@ def __init__(self, cfg: DictConfig): self.algebra = self.setup_algebra() self.model = self.setup_model().to(self.device) - self.optimization_report = inspect_module_optimization(self.model) - unplanned_leaves = self.optimization_report.unplanned_leaf_modules - logger.info( - "Optimization routes: %d planned (%d compact, %d dense-only), %d unplanned leaf modules", - len(self.optimization_report.plans), - len(self.optimization_report.compact_plans), - len(self.optimization_report.dense_only_plans), - len(unplanned_leaves), - ) - if unplanned_leaves: - logger.debug( - "Unplanned algebra-aware leaves: %s", - ", ".join(f"{issue.path}:{issue.module_type}" for issue in unplanned_leaves), - ) self.model = self.device_config.maybe_compile(self.model) self.criterion = self.setup_criterion() self.optimizer = self._setup_optimizer() diff --git a/tests/test_attention.py b/tests/test_attention.py index b8da6fd..d3138ef 100644 --- a/tests/test_attention.py +++ b/tests/test_attention.py @@ -3,7 +3,6 @@ import pytest import torch -from core.config import make_algebra from core.runtime.algebra import CliffordAlgebra from layers.blocks.attention import GeometricProductAttention @@ -27,13 +26,6 @@ def _reference_attention_score(algebra, q_head, k_head, bivector_weight): return (score_g0 + bivector_weight * score_g2) / scale -def _grade_only(algebra, x, grades): - result = torch.zeros_like(x) - for grade in grades: - result = result + algebra.grade_projection(x, grade) - return result - - def test_attention_dense_chunked_score_matches_direct_product(): algebra = CliffordAlgebra(3, 0, 0, device=DEVICE, dtype=torch.float64) attn = GeometricProductAttention( @@ -55,26 +47,6 @@ def test_attention_dense_chunked_score_matches_direct_product(): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) -def test_attention_compact_declared_grade_score_matches_projected_reference(): - algebra = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) - attn = GeometricProductAttention( - algebra, - channels=4, - num_heads=2, - causal=False, - bivector_weight=0.5, - score_grades=(1,), - ) - q_head = _grade_only(algebra, torch.randn(1, 2, 3, 2, algebra.dim, dtype=torch.float64), (1,)) - k_head = _grade_only(algebra, torch.randn(1, 2, 4, 2, algebra.dim, dtype=torch.float64), (1,)) - - actual = attn._compute_score(q_head, k_head) - expected = _reference_attention_score(algebra, q_head, k_head, attn.bivector_weight) - - assert attn._score_layout is not None - assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) - - def test_attention_forward_shape_after_score_refactor(): algebra = CliffordAlgebra(3, 0, 0, device=DEVICE, dtype=torch.float32) attn = GeometricProductAttention(algebra, channels=4, num_heads=2, causal=False) @@ -85,39 +57,12 @@ def test_attention_forward_shape_after_score_refactor(): assert y.shape == x.shape -def test_attention_declared_feature_grades_run_compact_high_dim_context(): - algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) - attn = GeometricProductAttention(algebra, channels=4, num_heads=2, causal=False, feature_grades=(1,)) - x = torch.randn(2, 5, 4, algebra.n) - - y = attn(x) - - assert attn.feature_layout.grades == (1,) - assert attn._score_layout.grades == (1,) - assert attn.q_proj.basis_dim == algebra.n - assert y.shape == x.shape - - -def test_attention_rejects_score_grades_outside_compact_feature_grades(): - algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) - - with pytest.raises(ValueError, match="score_grades must be contained"): - GeometricProductAttention( - algebra, - channels=4, - num_heads=2, - causal=False, - feature_grades=(1,), - score_grades=(2,), - ) - - @pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") -def test_attention_compact_score_compiles_fullgraph(): +def test_attention_dense_score_compiles_fullgraph(): algebra = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float32) - attn = GeometricProductAttention(algebra, channels=4, num_heads=2, causal=False, score_grades=(1,)) - q_head = _grade_only(algebra, torch.randn(1, 2, 3, 2, algebra.dim), (1,)) - k_head = _grade_only(algebra, torch.randn(1, 2, 4, 2, algebra.dim), (1,)) + attn = GeometricProductAttention(algebra, channels=4, num_heads=2, causal=False) + q_head = torch.randn(1, 2, 3, 2, algebra.dim) + k_head = torch.randn(1, 2, 4, 2, algebra.dim) def score(q, k): return attn._compute_score(q, k) diff --git a/tests/test_layers.py b/tests/test_layers.py index 21198a2..3870628 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -8,24 +8,9 @@ import pytest import torch -from core.config import make_algebra -from core.foundation.module import CliffordModule -from core.planning import collect_module_optimization_plans, inspect_module_optimization from core.runtime.algebra import CliffordAlgebra from core.runtime.decomposition import ExpPolicy -from layers import ( - BladeSelector, - CliffordLayerNorm, - CliffordLinear, - EntropyGatedAttention, - MotherEmbedding, - MultiRotorLayer, - MultivectorEmbedding, - PhaseShiftHead, - RotorLayer, -) -from layers.blocks.multi_rotor_ffn import MultiRotorFFN -from layers.blocks.transformer import GeometricTransformerBlock +from layers import CliffordLinear, MultiRotorLayer, RotorLayer from layers.primitives.reflection import ReflectionLayer pytestmark = pytest.mark.unit @@ -40,198 +25,6 @@ def test_linear_shape(self, algebra_3d): y = layer(x) assert y.shape == (4, 3, 8) - def test_linear_declared_grades_use_compact_lanes_in_high_dimensions(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - layer = CliffordLinear(algebra, 2, 3, grades=(1,)) - x = torch.randn(4, 2, algebra.n) - - y = layer(x) - - assert layer.layout.grades == (1,) - assert layer.basis_dim == algebra.n - assert layer.bias.shape == (3, algebra.n) - assert y.shape == (4, 3, algebra.n) - - def test_linear_declared_grades_reject_rotor_backend_until_compact_sandwich_exists(self, algebra_3d): - with pytest.raises(ValueError, match="compact grade declarations"): - CliffordLinear(algebra_3d, 2, 3, backend="rotor", grades=(1,)) - - def test_layer_norm_declared_grades_use_compact_lanes_in_high_dimensions(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - layer = CliffordLayerNorm(algebra, channels=2, grades=(0, 1)) - layout = algebra.layout((0, 1)) - x = torch.randn(3, 2, layout.dim) - - y = layer(x) - - scalar_pos = layout.basis_indices.index(0) - assert layer.layout == layout - assert y.shape == x.shape - assert layer._scalar_lane_mask.shape[-1] == layout.dim - assert layer._scalar_lane_mask[scalar_pos].item() == 1.0 - - def test_blade_selector_declared_grades_use_compact_lanes_in_high_dimensions(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - layer = BladeSelector(algebra, channels=2, grades=(1, 2)) - layout = algebra.layout((1, 2)) - x = torch.randn(3, 2, layout.dim) - - y = layer(x) - - assert layer.layout == layout - assert layer.weights.shape == (2, layout.dim) - assert y.shape == x.shape - - def test_compact_multirotor_ffn_uses_linear_toolbox_in_high_dimensions(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - layer = MultiRotorFFN(algebra, channels=4, ffn_mult=2, feature_grades=(1,), use_rotor_toolbox=False) - x = torch.randn(3, 4, algebra.n) - - y = layer(x) - - assert not layer.use_rotor_toolbox - assert y.shape == x.shape - - def test_compact_multirotor_ffn_rejects_dense_rotor_toolbox(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - - with pytest.raises(ValueError, match="dense feature lanes"): - MultiRotorFFN(algebra, channels=4, feature_grades=(1,), use_rotor_toolbox=True) - - def test_compact_transformer_block_runs_high_dim_pipeline(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - block = GeometricTransformerBlock( - algebra, - channels=4, - num_heads=2, - feature_grades=(1,), - use_ffn_rotor_toolbox=False, - ) - x = torch.randn(2, 5, 4, algebra.n) - - y = block(x) - - assert y.shape == x.shape - - def test_multivector_embedding_declared_grades_start_compact_high_dim_pipeline(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - embedding = MultivectorEmbedding(algebra, vocab_size=11, channels=4, grades=(1,)) - token_ids = torch.randint(0, 11, (2, 5)) - - x = embedding(token_ids) - - assert embedding.layout.grades == (1,) - assert embedding.embedding.weight.shape == (11, 4 * algebra.n) - assert x.shape == (2, 5, 4, algebra.n) - - def test_compact_embedding_transformer_pipeline_runs_high_dim_context(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - embedding = MultivectorEmbedding(algebra, vocab_size=11, channels=4, grades=(1,)) - block = GeometricTransformerBlock( - algebra, - channels=4, - num_heads=2, - feature_grades=(1,), - use_ffn_rotor_toolbox=False, - ) - token_ids = torch.randint(0, 11, (2, 5)) - - output = block(embedding(token_ids)) - - assert output.shape == (2, 5, 4, algebra.n) - - def test_compact_composed_layers_expose_static_optimization_plans(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - block = GeometricTransformerBlock( - algebra, - channels=4, - num_heads=2, - feature_grades=(1,), - use_ffn_rotor_toolbox=False, - ) - - report = inspect_module_optimization(block) - report.assert_no_unplanned_leaves() - - plans = collect_module_optimization_plans(block, compact_only=True) - by_path = {plan.path: plan for plan in plans} - - assert "attn.q_proj" in by_path - assert "ffn.act" in by_path - assert "ffn.expand" in by_path - assert by_path["attn"].score_grades == (1,) - assert by_path["attn.q_proj"].operators == ("linear:traditional",) - assert by_path["ffn.act"].operators == ("magnitude_gate",) - assert all(plan.output_grades == (1,) for plan in plans) - assert all(plan.basis_dim == algebra.n for plan in plans) - assert all(plan.dense_dim == algebra.dim for plan in plans) - assert all(plan.compression_ratio < 0.001 for plan in plans) - - def test_core_collector_reports_dense_only_declared_paths(self, algebra_3d): - layer = RotorLayer(algebra_3d, channels=4) - - plans = collect_module_optimization_plans(layer) - - assert len(plans) == 1 - assert plans[0].path == "" - assert plans[0].operators == ("dense_sandwich",) - assert plans[0].parameter_grades == (2,) - assert not plans[0].compact - assert plans[0].dense_only_reason == "sandwich path still materializes dense multivectors" - - def test_core_report_flags_unplanned_algebra_aware_leaves(self, algebra_3d): - class UnplannedLeaf(CliffordModule): - def forward(self, x): - return x - - report = inspect_module_optimization(UnplannedLeaf(algebra_3d)) - - assert len(report.unplanned_leaf_modules) == 1 - assert report.unplanned_leaf_modules[0].module_type == "UnplannedLeaf" - with pytest.raises(AssertionError, match="Unplanned algebra-aware leaf modules"): - report.assert_no_unplanned_leaves() - - def test_mother_embedding_declared_grades_emit_compact_lanes(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - layer = MotherEmbedding(algebra, input_dim=6, channels=3, grades=(1,)) - x = torch.randn(2, 6) - - y = layer(x) - - assert layer.layout.grades == (1,) - assert y.shape == (2, 3, algebra.n) - - def test_entropy_gated_attention_declared_feature_grades_run_compact(self): - algebra = make_algebra(10, 4, 2, device="cpu", dtype=torch.float32) - layout = algebra.layout((1, 2)) - layer = EntropyGatedAttention( - algebra, - channels=4, - num_heads=2, - feature_grades=(1, 2), - score_grades=(1,), - ) - x = torch.randn(2, 5, 4, layout.dim) - - y, entropy, gate = layer(x, return_gating=True) - - assert layer.g2_idx.numel() == algebra.layout((2,)).dim - assert y.shape == x.shape - assert entropy.shape == (2,) - assert gate.shape == (2,) - - def test_phase_shift_head_declared_feature_grades_read_compact_lanes(self): - algebra = make_algebra(10, 0, 0, device="cpu", dtype=torch.float32) - layout = algebra.layout((0, 4)) - layer = PhaseShiftHead(algebra, channels=2, feature_grades=(0, 4)) - x = torch.randn(3, 5, 2, layout.dim) - - y = layer(x) - - assert layer.g0_idx.numel() == 1 - assert layer.g4_idx.numel() == algebra.layout((4,)).dim - assert y.shape == (3, 1) - def test_rotor_shape(self, algebra_3d): # Batch=4, Channels=5 x = torch.randn(4, 5, 8) From 5a6f9ba335cbb7eb7910e58bc86c803cb67346dd Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 17:21:46 +0900 Subject: [PATCH 31/45] chore: add setuptools for compile --- pyproject.toml | 1 + uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 40c6321..ffda5b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "numpy>=1.26.0", "hydra-core>=1.3.2", "tqdm>=4.67.3", + "setuptools>=80.10.2", ] [project.urls] diff --git a/uv.lock b/uv.lock index 1cd172e..4475c2e 100644 --- a/uv.lock +++ b/uv.lock @@ -5400,6 +5400,7 @@ dependencies = [ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "setuptools" }, { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "tqdm" }, @@ -5496,6 +5497,7 @@ requires-dist = [ { name = "scikit-learn", marker = "extra == 'sr'", specifier = ">=1.3.0" }, { name = "seaborn", marker = "extra == 'viz'", specifier = ">=0.13.0" }, { name = "sentence-transformers", marker = "extra == 'lqa'", specifier = ">=5.1.2" }, + { name = "setuptools", specifier = ">=80.10.2" }, { name = "streamlit", marker = "extra == 'demo'", specifier = ">=1.50.0" }, { name = "sympy", marker = "extra == 'sr'", specifier = ">=1.12" }, { name = "torch", specifier = ">=2.0.0" }, From a8b2a4dd1b352c96c0abd6d5a68c0322473fcdfb Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 17:23:32 +0900 Subject: [PATCH 32/45] fix: separate product planning from runtime buffers --- core/planning/product.py | 43 ++++++++++---------- core/runtime/projected.py | 26 ++++++++++++ tests/test_grade_plan.py | 83 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 20 deletions(-) diff --git a/core/planning/product.py b/core/planning/product.py index 74f2ff2..d678f60 100644 --- a/core/planning/product.py +++ b/core/planning/product.py @@ -28,7 +28,7 @@ normalize_grades, operation_coefficient, ) -from core.foundation.layout import AlgebraSpec, GradeLayout +from core.foundation.layout import AlgebraSpec from core.planning.layouts import ProductRequest from core.planning.tree import GradePlanTree, build_grade_plan_tree @@ -50,6 +50,8 @@ def __init__( right_indices: torch.Tensor, output_indices: torch.Tensor, output_positions: torch.Tensor, + left_compact_positions: torch.Tensor, + right_compact_positions: torch.Tensor, coefficients: torch.Tensor, active_output_indices: torch.Tensor, tree: GradePlanTree, @@ -63,6 +65,8 @@ def __init__( self.right_indices = right_indices self.output_indices = output_indices self.output_positions = output_positions + self.left_compact_positions = left_compact_positions + self.right_compact_positions = right_compact_positions self.coefficients = coefficients self.active_output_indices = active_output_indices self.tree = tree @@ -179,6 +183,12 @@ def build_grade_product_plan_from_tree( left_basis_by_grade = {grade: basis_index_tuple_for_grades(n, (grade,)) for grade in left_grade_tuple} right_basis_by_grade = {grade: basis_index_tuple_for_grades(n, (grade,)) for grade in right_grade_tuple} + left_position_by_index = { + index: position for position, index in enumerate(spec.layout(left_grade_tuple).basis_indices) + } + right_position_by_index = { + index: position for position, index in enumerate(spec.layout(right_grade_tuple).basis_indices) + } active_outputs = basis_index_tuple_for_grades(n, output_grade_tuple) output_position_by_index = {index: position for position, index in enumerate(active_outputs)} @@ -186,6 +196,8 @@ def build_grade_product_plan_from_tree( plan_right: list[int] = [] plan_output: list[int] = [] plan_positions: list[int] = [] + plan_left_compact_positions: list[int] = [] + plan_right_compact_positions: list[int] = [] plan_coefficients: list[float] = [] for path in tree.paths: @@ -202,6 +214,8 @@ def build_grade_product_plan_from_tree( plan_right.append(right_index) plan_output.append(output_index) plan_positions.append(output_position) + plan_left_compact_positions.append(left_position_by_index[left_index]) + plan_right_compact_positions.append(right_position_by_index[right_index]) plan_coefficients.append(coefficient) return GradeProductPlan( @@ -216,6 +230,8 @@ def build_grade_product_plan_from_tree( right_indices=basis_indices_tensor(plan_right, n=n, role="right product basis indices", device=device), output_indices=basis_indices_tensor(plan_output, n=n, role="output product basis indices", device=device), output_positions=torch.tensor(plan_positions, dtype=torch.long, device=device), + left_compact_positions=torch.tensor(plan_left_compact_positions, dtype=torch.long, device=device), + right_compact_positions=torch.tensor(plan_right_compact_positions, dtype=torch.long, device=device), coefficients=torch.tensor(plan_coefficients, dtype=dtype, device=device), active_output_indices=basis_indices_tensor(active_outputs, n=n, role="active output basis indices", device=device), tree=tree, @@ -244,30 +260,24 @@ def __init__(self, plan: GradeProductPlan): self.left_layout = plan.left_layout self.right_layout = plan.right_layout self.output_layout = plan.output_layout + self._output_dim = plan.output_dim + self._pair_count = plan.pair_count self.register_buffer("left_indices", plan.left_indices, persistent=False) self.register_buffer("right_indices", plan.right_indices, persistent=False) self.register_buffer("output_indices", plan.output_indices, persistent=False) self.register_buffer("output_positions", plan.output_positions, persistent=False) self.register_buffer("coefficients", plan.coefficients, persistent=False) self.register_buffer("active_output_indices", plan.active_output_indices, persistent=False) - self.register_buffer( - "left_compact_positions", - self._dense_to_compact_positions(plan.left_layout, plan.left_indices), - persistent=False, - ) - self.register_buffer( - "right_compact_positions", - self._dense_to_compact_positions(plan.right_layout, plan.right_indices), - persistent=False, - ) + self.register_buffer("left_compact_positions", plan.left_compact_positions, persistent=False) + self.register_buffer("right_compact_positions", plan.right_compact_positions, persistent=False) @property def output_dim(self) -> int: - return int(self.active_output_indices.numel()) + return self._output_dim @property def pair_count(self) -> int: - return int(self.left_indices.numel()) + return self._pair_count def forward(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: """Return compact grade-lane output for full dense input tensors.""" @@ -330,10 +340,3 @@ def forward_dense(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor compact = self.forward(left, right) output = compact.new_zeros(*compact.shape[:-1], self.dim) return output.index_copy(-1, self.active_output_indices, compact) - - @staticmethod - def _dense_to_compact_positions(layout: GradeLayout, dense_indices: torch.Tensor) -> torch.Tensor: - """Map dense basis indices used by a plan into compact lane positions.""" - positions = {index: position for position, index in enumerate(layout.basis_indices)} - compact_positions = [positions[int(index)] for index in dense_indices.detach().cpu().tolist()] - return torch.tensor(compact_positions, dtype=torch.long, device=dense_indices.device) diff --git a/core/runtime/projected.py b/core/runtime/projected.py index c157b86..5ed71a8 100644 --- a/core/runtime/projected.py +++ b/core/runtime/projected.py @@ -35,6 +35,32 @@ def default_layout(self) -> GradeLayout: """Return the default layout using the central fallback policy.""" return _default_layout(self) + def product_executor( + self, + *, + left_grades, + right_grades, + op: str = "gp", + output_grades=None, + dtype: Optional[torch.dtype] = None, + device=None, + cache: bool = True, + ): + """Return a preplanned product executor suitable for ``torch.compile``.""" + if dtype is None: + dtype = getattr(self, "dtype", torch.float32) + if device is None: + device = getattr(self, "device", None) + return self.planner.product_executor( + op=op, + left_grades=left_grades, + right_grades=right_grades, + output_grades=output_grades, + dtype=dtype, + device=device, + cache=cache, + ) + def resolve_layout( self, *, diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index 79ce79f..8ec4264 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -208,6 +208,38 @@ def test_static_grade_product_matches_dense_kernel_for_selected_grade_paths(op): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) +def test_product_plan_owns_compact_position_buffers(): + algebra = CliffordAlgebra(4, 1, 0, device=DEVICE, dtype=torch.float64) + plan = build_grade_product_plan( + algebra.p, + algebra.q, + algebra.r, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + op="gp", + device=DEVICE, + dtype=torch.float64, + ) + product = GradeProductExecutor(plan) + A = _grade_only_input(algebra, 2, (1,), seed=109) + B = _grade_only_input(algebra, 2, (1,), seed=111) + + left_positions = {index: position for position, index in enumerate(plan.left_layout.basis_indices)} + right_positions = {index: position for position, index in enumerate(plan.right_layout.basis_indices)} + expected_left = torch.tensor([left_positions[int(index)] for index in plan.left_indices], dtype=torch.long) + expected_right = torch.tensor([right_positions[int(index)] for index in plan.right_indices], dtype=torch.long) + + assert torch.equal(plan.left_compact_positions.cpu(), expected_left) + assert torch.equal(plan.right_compact_positions.cpu(), expected_right) + assert torch.allclose( + product.forward_compact(plan.left_layout.compact(A), plan.right_layout.compact(B)), + product(A, B), + atol=1e-12, + rtol=1e-12, + ) + + def test_algebra_projected_product_matches_dense_kernel_and_compact_output(): algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) A = _grade_only_input(algebra, 2, (1,), seed=113) @@ -258,6 +290,27 @@ def test_grade_planner_reuses_projected_product_executor(): assert first is second +def test_algebra_product_executor_returns_preplanned_runtime_handle(): + algebra = AlgebraContext(6, 0, device=DEVICE, dtype=torch.float32) + + first = algebra.product_executor( + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + ) + second = algebra.product_executor( + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + ) + + assert first is second + assert first.left_grades == (1,) + assert first.right_grades == (1,) + assert first.output_grades == (0, 2) + assert first.coefficients.dtype == algebra.dtype + + def test_grade_planner_rekeys_cached_executor_after_dtype_move(): algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) executor = algebra.planner.product_executor( @@ -766,6 +819,36 @@ def product(x, y): assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) +@pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile not available") +def test_context_projected_product_compiles_fullgraph_from_cold_planner_cache(): + if hasattr(torch, "_dynamo"): + torch._dynamo.reset() + algebra = AlgebraContext(6, 0, device=DEVICE, dtype=torch.float32) + generator = torch.Generator(device=DEVICE).manual_seed(181) + left = torch.randn(2, algebra.layout((1,)).dim, dtype=torch.float32, generator=generator) + right = torch.randn(2, algebra.layout((1,)).dim, dtype=torch.float32, generator=generator) + + def product(x, y): + return algebra.geometric_product( + x, + y, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + left_compact=True, + right_compact=True, + compact_output=True, + ) + + assert not algebra.planner._product_executors + compiled = torch.compile(product, backend="aot_eager", fullgraph=True) + actual = compiled(left, right) + expected = product(left, right) + + assert len(algebra.planner._product_executors) == 1 + assert torch.allclose(actual, expected, atol=1e-6, rtol=1e-6) + + def _dense_method_name(op: str) -> str: if op == "gp": return "geometric_product" From 48e6c08aac1a9da35e6522a5f141b6dffbf35599 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 17:34:01 +0900 Subject: [PATCH 33/45] perf: cache core layout indices --- core/foundation/layout.py | 68 +++++++++++++++++++++++++++++++-------- core/planning/product.py | 16 +++++---- core/runtime/algebra.py | 18 +++++++---- core/runtime/context.py | 15 +++++++-- 4 files changed, 88 insertions(+), 29 deletions(-) diff --git a/core/foundation/layout.py b/core/foundation/layout.py index 49f0520..254263e 100644 --- a/core/foundation/layout.py +++ b/core/foundation/layout.py @@ -56,6 +56,14 @@ class GradeLayout: spec: AlgebraSpec grades: tuple[int, ...] _basis_indices: tuple[int, ...] = field(init=False, repr=False) + _indices_cache: dict[str, torch.Tensor] = field(default_factory=dict, init=False, repr=False, compare=False) + _grade_index_cache: dict[str, torch.Tensor] = field(default_factory=dict, init=False, repr=False, compare=False) + _conversion_cache: dict[tuple[AlgebraSpec, tuple[int, ...], str], tuple[torch.Tensor, torch.Tensor]] = field( + default_factory=dict, + init=False, + repr=False, + compare=False, + ) def __post_init__(self) -> None: grades = normalize_grades(self.grades, self.spec.n) @@ -83,7 +91,22 @@ def contains_grade(self, grade: int) -> bool: def indices_tensor(self, *, device=None) -> torch.Tensor: """Return basis indices as a tensor on ``device``.""" - return basis_indices_tensor(self.basis_indices, n=self.spec.n, role="layout basis indices", device=device) + key = _device_cache_key(device) + cached = self._indices_cache.get(key) + if cached is None: + cached = basis_indices_tensor(self.basis_indices, n=self.spec.n, role="layout basis indices", device=device) + self._indices_cache[key] = cached + return cached + + def grade_indices_tensor(self, *, device=None) -> torch.Tensor: + """Return per-lane grade ids on ``device``.""" + key = _device_cache_key(device) + cached = self._grade_index_cache.get(key) + if cached is None: + grades = tuple(index.bit_count() for index in self.basis_indices) + cached = torch.tensor(grades, dtype=torch.long, device=device) + self._grade_index_cache[key] = cached + return cached def convert(self, values: torch.Tensor, source: "GradeLayout") -> torch.Tensor: """Convert compact values from ``source`` into this layout. @@ -100,22 +123,11 @@ def convert(self, values: torch.Tensor, source: "GradeLayout") -> torch.Tensor: if source == self: return values - source_positions = {index: position for position, index in enumerate(source.basis_indices)} - gather_positions: list[int] = [] - scatter_positions: list[int] = [] - for target_position, index in enumerate(self.basis_indices): - source_position = source_positions.get(index) - if source_position is None: - continue - gather_positions.append(source_position) - scatter_positions.append(target_position) - output = values.new_zeros(*values.shape[:-1], self.dim) - if not gather_positions: + gather, scatter = self._conversion_tensors(source, device=values.device) + if gather.numel() == 0: return output - gather = torch.tensor(gather_positions, dtype=torch.long, device=values.device) - scatter = torch.tensor(scatter_positions, dtype=torch.long, device=values.device) copied = torch.index_select(values, -1, gather) return output.index_copy(-1, scatter, copied) @@ -131,3 +143,31 @@ def dense(self, values: torch.Tensor) -> torch.Tensor: raise ValueError(f"values last dimension must be {self.dim}, got {values.shape[-1]}") output = values.new_zeros(*values.shape[:-1], self.dense_dim) return output.index_copy(-1, self.indices_tensor(device=values.device), values) + + def _conversion_tensors(self, source: "GradeLayout", *, device=None) -> tuple[torch.Tensor, torch.Tensor]: + key = (source.spec, source.grades, _device_cache_key(device)) + cached = self._conversion_cache.get(key) + if cached is not None: + return cached + + source_positions = {index: position for position, index in enumerate(source.basis_indices)} + gather_positions: list[int] = [] + scatter_positions: list[int] = [] + for target_position, index in enumerate(self.basis_indices): + source_position = source_positions.get(index) + if source_position is None: + continue + gather_positions.append(source_position) + scatter_positions.append(target_position) + + gather = torch.tensor(gather_positions, dtype=torch.long, device=device) + scatter = torch.tensor(scatter_positions, dtype=torch.long, device=device) + cached = (gather, scatter) + self._conversion_cache[key] = cached + return cached + + +def _device_cache_key(device) -> str: + if device is None: + return "cpu" + return str(torch.device(device)) diff --git a/core/planning/product.py b/core/planning/product.py index d678f60..48157cd 100644 --- a/core/planning/product.py +++ b/core/planning/product.py @@ -289,8 +289,7 @@ def forward(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: left_b, right_b = torch.broadcast_tensors(left, right) left_terms = torch.index_select(left_b, -1, self.left_indices) right_terms = torch.index_select(right_b, -1, self.right_indices) - coefficients = self.coefficients.to(dtype=torch.promote_types(left_terms.dtype, right_terms.dtype)) - terms = left_terms * right_terms * coefficients + terms = left_terms * right_terms * self._coefficients_for(left_terms, right_terms) output = terms.new_zeros(*terms.shape[:-1], self.output_dim) return output.index_add(-1, self.output_positions, terms) @@ -305,8 +304,7 @@ def forward_compact(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tens left_b, right_b = torch.broadcast_tensors(left, right) left_terms = torch.index_select(left_b, -1, self.left_compact_positions) right_terms = torch.index_select(right_b, -1, self.right_compact_positions) - coefficients = self.coefficients.to(dtype=torch.promote_types(left_terms.dtype, right_terms.dtype)) - terms = left_terms * right_terms * coefficients + terms = left_terms * right_terms * self._coefficients_for(left_terms, right_terms) output = terms.new_zeros(*terms.shape[:-1], self.output_dim) return output.index_add(-1, self.output_positions, terms) @@ -329,8 +327,7 @@ def forward_pairwise_compact(self, left: torch.Tensor, right: torch.Tensor) -> t left_terms = torch.index_select(left, -1, self.left_compact_positions) right_terms = torch.index_select(right, -1, self.right_compact_positions) - coefficients = self.coefficients.to(dtype=torch.promote_types(left_terms.dtype, right_terms.dtype)) - terms = left_terms.unsqueeze(-2) * right_terms.unsqueeze(-3) * coefficients + terms = left_terms.unsqueeze(-2) * right_terms.unsqueeze(-3) * self._coefficients_for(left_terms, right_terms) output = terms.new_zeros(*terms.shape[:-1], self.output_dim) return output.index_add(-1, self.output_positions, terms) @@ -340,3 +337,10 @@ def forward_dense(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor compact = self.forward(left, right) output = compact.new_zeros(*compact.shape[:-1], self.dim) return output.index_copy(-1, self.active_output_indices, compact) + + def _coefficients_for(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: + dtype = torch.promote_types(left.dtype, right.dtype) + coefficients = self.coefficients + if coefficients.dtype == dtype and coefficients.device == left.device: + return coefficients + return coefficients.to(device=left.device, dtype=dtype) diff --git a/core/runtime/algebra.py b/core/runtime/algebra.py index 7cf9667..8fcbef4 100644 --- a/core/runtime/algebra.py +++ b/core/runtime/algebra.py @@ -156,6 +156,7 @@ def __init__( stacked = torch.stack(grade_masks_list) # [n+1, dim] self.register_buffer("_grade_masks", stacked, persistent=False) self.register_buffer("_grade_masks_float", stacked.to(dtype=cayley_signs.dtype), persistent=False) + self.register_buffer("_g1_indices", stacked[1].nonzero(as_tuple=False).squeeze(-1), persistent=False) # Bivector indices if self.n >= 2: @@ -279,10 +280,9 @@ def embed_vector(self, vectors: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Multivector coefficients [..., dim]. """ - g1_idx = (1 << torch.arange(self.n, device=vectors.device)).long() - mv = torch.zeros(*vectors.shape[:-1], self.dim, device=vectors.device, dtype=vectors.dtype) - mv.scatter_(-1, g1_idx.expand_as(vectors), vectors) - return mv + g1_idx = self._basis_vector_indices(vectors.device) + mv = vectors.new_zeros(*vectors.shape[:-1], self.dim) + return mv.index_copy(-1, g1_idx, vectors) def get_grade_norms(self, mv: torch.Tensor) -> torch.Tensor: """Calculates norms per grade. Useful for invariant features. @@ -593,9 +593,7 @@ def right_contraction(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: bv_idx_exp = self._bv_indices.expand(*A.shape[:-1], -1) bv_coeffs = torch.gather(A, -1, bv_idx_exp) # [..., num_bv] - # Grade-1 indices: powers of 2 for basis vectors - g1_idx = torch.arange(self.n, device=A.device) - g1_idx = (1 << g1_idx).long() # [n] + g1_idx = self._basis_vector_indices(A.device) g1_idx_exp = g1_idx.expand(*B.shape[:-1], -1) v_coeffs = torch.gather(B, -1, g1_idx_exp) # [..., n] @@ -611,6 +609,12 @@ def right_contraction(self, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: result.scatter_(-1, g1_idx_exp, result_v) return result + def _basis_vector_indices(self, device) -> torch.Tensor: + indices = self._g1_indices + if indices.device != torch.device(device): + indices = indices.to(device=device) + return indices + def inner_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the inner product: A . B = (AB + BA)/2. diff --git a/core/runtime/context.py b/core/runtime/context.py index 04e8dcd..44c1630 100644 --- a/core/runtime/context.py +++ b/core/runtime/context.py @@ -53,6 +53,7 @@ def __init__( self.planning_limits = DEFAULT_PLANNING_LIMITS if planning_limits is None else planning_limits self._default_grades = None if default_grades is None else normalize_grades(default_grades, self.n) self._default_layout: Optional[GradeLayout] = None + self._g1_indices_cache: dict[str, torch.Tensor] = {} self.planner = GradePlanner(self) self._sync_eps() @@ -80,6 +81,7 @@ def _apply(self, fn): if probe.dtype.is_floating_point: self._dtype = probe.dtype self._sync_eps() + self._g1_indices_cache.clear() self.planner._apply(fn) return self @@ -90,6 +92,7 @@ def to(self, device=None, dtype=None): if dtype is not None: self._dtype = resolve_dtype(dtype) self._sync_eps() + self._g1_indices_cache.clear() self.planner.clear_cache() return self @@ -123,8 +126,16 @@ def embed_vector(self, vectors: torch.Tensor) -> torch.Tensor: if vectors.shape[-1] != self.n: raise ValueError(f"vectors last dimension must be {self.n}, got {vectors.shape[-1]}") output = vectors.new_zeros(*vectors.shape[:-1], self.dim) - basis_indices = [1 << bit for bit in range(self.n)] - return output.index_copy(-1, torch.tensor(basis_indices, dtype=torch.long, device=vectors.device), vectors) + return output.index_copy(-1, self._basis_vector_indices(vectors.device), vectors) + + def _basis_vector_indices(self, device) -> torch.Tensor: + resolved = torch.device(device) + key = str(resolved) + cached = self._g1_indices_cache.get(key) + if cached is None: + cached = torch.tensor([1 << bit for bit in range(self.n)], dtype=torch.long, device=resolved) + self._g1_indices_cache[key] = cached + return cached def reverse(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: """Reverse dense or compact multivector coefficients.""" From 40d19c49e24937537d293acaa2a2796b88a0e41c Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 17:36:27 +0900 Subject: [PATCH 34/45] perf: narrow bivector exp simplicity checks --- core/runtime/algebra.py | 22 ++++++++++++++++++---- core/runtime/decomposition.py | 13 ++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/runtime/algebra.py b/core/runtime/algebra.py index 8fcbef4..65a2623 100644 --- a/core/runtime/algebra.py +++ b/core/runtime/algebra.py @@ -615,6 +615,12 @@ def _basis_vector_indices(self, device) -> torch.Tensor: indices = indices.to(device=device) return indices + def _bivector_indices_for(self, device) -> torch.Tensor: + indices = self._bv_indices + if indices.device != torch.device(device): + indices = indices.to(device=device) + return indices + def inner_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the inner product: A . B = (AB + BA)/2. @@ -1110,10 +1116,18 @@ def _exp_compiled_safe(self, B: torch.Tensor) -> torch.Tensor: R_closed = self._exp_bivector_closed(B) R_decomposed = compiled_safe_decomposed_exp(self, B, fixed_iterations=self._exp_fixed_iterations) - BB = self.geometric_product(B, B) - # Subtract scalar part, check if residual is negligible - scalar_part = self.grade_projection(BB, 0) - non_scalar_energy = (BB - scalar_part).norm(dim=-1, keepdim=True) + # For bivectors, B*B has only scalar and grade-4 components; the + # grade-4 energy is therefore the simplicity residual. + grade4 = self.projected_product( + B, + B, + op="gp", + left_grades=(2,), + right_grades=(2,), + output_grades=(4,), + compact_output=True, + ) + non_scalar_energy = grade4.norm(dim=-1, keepdim=True) is_simple = non_scalar_energy < self.eps * 100 return torch.where(is_simple, R_closed, R_decomposed) diff --git a/core/runtime/decomposition.py b/core/runtime/decomposition.py index d6257fc..9f57cd1 100644 --- a/core/runtime/decomposition.py +++ b/core/runtime/decomposition.py @@ -313,7 +313,7 @@ def compiled_safe_decomposed_exp( with torch.no_grad(): decomp = _decompose_compiled_safe(algebra, b.detach(), k=k_actual, fixed_iterations=fixed_iterations) - bv_indices = algebra.grade_indices((2,), device=b.device) + bv_indices = _bivector_indices(algebra, b.device) # Re-project live bivector and compose rotors result = identity @@ -333,3 +333,14 @@ def compiled_safe_decomposed_exp( result = algebra.geometric_product(result, R_i) return result + + +def _bivector_indices(algebra, device) -> torch.Tensor: + if hasattr(algebra, "_bivector_indices_for"): + return algebra._bivector_indices_for(device) + indices = getattr(algebra, "_bv_indices", None) + if indices is not None: + if indices.device != torch.device(device): + indices = indices.to(device=device) + return indices + return algebra.grade_indices((2,), device=device) From fb9733c2cb288ad19067585c3f804ec3e6a981a3 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 17:40:52 +0900 Subject: [PATCH 35/45] fix: gather product lanes before broadcasting --- core/planning/product.py | 12 ++++++------ tests/test_grade_plan.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/planning/product.py b/core/planning/product.py index 48157cd..eab5370 100644 --- a/core/planning/product.py +++ b/core/planning/product.py @@ -286,9 +286,9 @@ def forward(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: if right.shape[-1] != self.dim: raise ValueError(f"right last dimension must be {self.dim}, got {right.shape[-1]}") - left_b, right_b = torch.broadcast_tensors(left, right) - left_terms = torch.index_select(left_b, -1, self.left_indices) - right_terms = torch.index_select(right_b, -1, self.right_indices) + left_terms = torch.index_select(left, -1, self.left_indices) + right_terms = torch.index_select(right, -1, self.right_indices) + left_terms, right_terms = torch.broadcast_tensors(left_terms, right_terms) terms = left_terms * right_terms * self._coefficients_for(left_terms, right_terms) output = terms.new_zeros(*terms.shape[:-1], self.output_dim) @@ -301,9 +301,9 @@ def forward_compact(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tens if right.shape[-1] != self.right_layout.dim: raise ValueError(f"right compact dimension must be {self.right_layout.dim}, got {right.shape[-1]}") - left_b, right_b = torch.broadcast_tensors(left, right) - left_terms = torch.index_select(left_b, -1, self.left_compact_positions) - right_terms = torch.index_select(right_b, -1, self.right_compact_positions) + left_terms = torch.index_select(left, -1, self.left_compact_positions) + right_terms = torch.index_select(right, -1, self.right_compact_positions) + left_terms, right_terms = torch.broadcast_tensors(left_terms, right_terms) terms = left_terms * right_terms * self._coefficients_for(left_terms, right_terms) output = terms.new_zeros(*terms.shape[:-1], self.output_dim) diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index 8ec4264..44d9547 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -240,6 +240,30 @@ def test_product_plan_owns_compact_position_buffers(): ) +def test_product_executor_compact_forward_supports_different_layout_widths(): + algebra = CliffordAlgebra(4, 1, 0, device=DEVICE, dtype=torch.float64) + plan = build_grade_product_plan( + algebra.p, + algebra.q, + algebra.r, + left_grades=(1,), + right_grades=(1, 2), + output_grades=(0, 1, 2, 3), + op="gp", + device=DEVICE, + dtype=torch.float64, + ) + product = GradeProductExecutor(plan) + A = _grade_only_input(algebra, 2, (1,), seed=115) + B = _grade_only_input(algebra, 2, (1, 2), seed=117) + + compact = product.forward_compact(plan.left_layout.compact(A), plan.right_layout.compact(B)) + dense = product(A, B) + + assert plan.left_layout.dim != plan.right_layout.dim + assert torch.allclose(compact, dense, atol=1e-12, rtol=1e-12) + + def test_algebra_projected_product_matches_dense_kernel_and_compact_output(): algebra = CliffordAlgebra(4, 1, 1, device=DEVICE, dtype=torch.float64) A = _grade_only_input(algebra, 2, (1,), seed=113) From 2d041b1da0621bcacd4ec93451e8b6fbef255bd2 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 17:42:58 +0900 Subject: [PATCH 36/45] perf: vectorize grade energy metrics --- core/runtime/metric.py | 62 +++++++++++++++++++++++++------------ core/runtime/multivector.py | 10 +++--- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/core/runtime/metric.py b/core/runtime/metric.py index b5375e7..8d1c557 100644 --- a/core/runtime/metric.py +++ b/core/runtime/metric.py @@ -110,11 +110,16 @@ def grade_purity(algebra: AlgebraLike, A: torch.Tensor, grade: int) -> torch.Ten Returns: torch.Tensor: Purity score [0, 1]. """ - # Project to grade - A_k = algebra.grade_projection(A, grade) - # Compute energies (using standard squared norm of coefficients for stability) - energy_k = (A_k**2).sum(dim=-1) + grade_masks = getattr(algebra, "grade_masks_float", None) + if grade_masks is not None and A.shape[-1] == getattr(algebra, "dim"): + mask = grade_masks[int(grade)] + if mask.device != A.device or mask.dtype != A.dtype: + mask = mask.to(device=A.device, dtype=A.dtype) + energy_k = (A * A * mask).sum(dim=-1) + else: + A_k = algebra.grade_projection(A, grade) + energy_k = (A_k**2).sum(dim=-1) energy_total = (A**2).sum(dim=-1).clamp(min=algebra.eps) return energy_k / energy_total @@ -132,14 +137,19 @@ def mean_active_grade(algebra: AlgebraLike, A: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Average grade index. """ - energy_total = (A**2).sum(dim=-1).clamp(min=algebra.eps) - weighted_sum = torch.zeros_like(energy_total) - - for k in range(algebra.n + 1): - A_k = algebra.grade_projection(A, k) - energy_k = (A_k**2).sum(dim=-1) - weighted_sum += k * energy_k - + grade_energies = _dense_grade_energies(algebra, A) + if grade_energies is None: + energy_total = (A**2).sum(dim=-1).clamp(min=algebra.eps) + weighted_sum = torch.zeros_like(energy_total) + for k in range(algebra.n + 1): + A_k = algebra.grade_projection(A, k) + energy_k = (A_k**2).sum(dim=-1) + weighted_sum += k * energy_k + return weighted_sum / energy_total + + weights = torch.arange(algebra.n + 1, device=A.device, dtype=grade_energies.dtype) + weighted_sum = (grade_energies * weights).sum(dim=-1) + energy_total = grade_energies.sum(dim=-1).clamp(min=algebra.eps) return weighted_sum / energy_total @@ -387,13 +397,13 @@ def hermitian_grade_spectrum( Grade energies [..., n+1]. Each entry >= 0. """ values, source_layout = compact_values(algebra, A, layout=layout, grades=grades) - spectrum = [] - for k in range(algebra.n + 1): - grade_layout = algebra.layout((k,)) - grade_values = grade_layout.convert(values, source_layout) - sq = _hermitian_inner_values(algebra, grade_values, grade_values, grade_layout) - spectrum.append(torch.abs(sq)) - return torch.cat(spectrum, dim=-1) + signs = _signs_like(algebra, source_layout, values, values) + signed_energy = signs * values * values + flat = signed_energy.reshape(-1, source_layout.dim) + grade_ids = source_layout.grade_indices_tensor(device=values.device).unsqueeze(0).expand_as(flat) + spectrum = signed_energy.new_zeros(flat.shape[0], algebra.n + 1) + spectrum.scatter_add_(1, grade_ids, flat) + return spectrum.reshape(*values.shape[:-1], algebra.n + 1).abs() def _aligned_pair_values( @@ -450,6 +460,20 @@ def _signs_like( return _hermitian_signs(algebra, layout=layout, device=A_values.device, dtype=dtype) +def _dense_grade_energies(algebra: AlgebraLike, A: torch.Tensor) -> Optional[torch.Tensor]: + grade_index = getattr(algebra, "grade_index", None) + if grade_index is None or A.shape[-1] != getattr(algebra, "dim"): + return None + if grade_index.device != A.device: + grade_index = grade_index.to(device=A.device) + + flat = (A * A).reshape(-1, algebra.dim) + idx = grade_index.unsqueeze(0).expand_as(flat) + energies = flat.new_zeros(flat.shape[0], algebra.n + 1) + energies.scatter_add_(1, idx, flat) + return energies.reshape(*A.shape[:-1], algebra.n + 1) + + def signature_trace_form(algebra: AlgebraLike, A: torch.Tensor, B: torch.Tensor) -> torch.Tensor: """Signature-aware trace form: <~A B>_0. diff --git a/core/runtime/multivector.py b/core/runtime/multivector.py index 5ff6a46..4aa4e93 100644 --- a/core/runtime/multivector.py +++ b/core/runtime/multivector.py @@ -474,11 +474,11 @@ def norm_sq(self) -> torch.Tensor: def get_grade_norms(self) -> torch.Tensor: """Per-grade L2 norms.""" if self.is_compact: - result = self.values.new_zeros(*self.values.shape[:-1], self.algebra.num_grades) - for position, index in enumerate(self.layout.basis_indices): - grade = index.bit_count() - result[..., grade] = result[..., grade] + self.values[..., position].pow(2) - return result.clamp(min=self.algebra.eps).sqrt() + flat = self.values.pow(2).reshape(-1, self.layout.dim) + grade_ids = self.layout.grade_indices_tensor(device=self.values.device).unsqueeze(0).expand_as(flat) + result = flat.new_zeros(flat.shape[0], self.algebra.num_grades) + result.scatter_add_(1, grade_ids, flat) + return result.reshape(*self.values.shape[:-1], self.algebra.num_grades).clamp(min=self.algebra.eps).sqrt() return self.algebra.get_grade_norms(self.tensor) def exp(self) -> Multivector: From 97cea79797b6f692557ffa92f8377e7512fe00fd Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 17:45:50 +0900 Subject: [PATCH 37/45] perf: reduce dense analysis temporaries --- core/analysis/commutator.py | 82 ++++++++++++++++++++----------------- core/analysis/geodesic.py | 3 +- core/analysis/symmetry.py | 8 ++-- 3 files changed, 48 insertions(+), 45 deletions(-) diff --git a/core/analysis/commutator.py b/core/analysis/commutator.py index 5aca7ad..24e0119 100644 --- a/core/analysis/commutator.py +++ b/core/analysis/commutator.py @@ -86,33 +86,33 @@ def commutativity_matrix(self, mv_data: torch.Tensor) -> torch.Tensor: ``[n, n]`` symmetric matrix of commutativity indices. """ n = self.algebra.n - dim = self.algebra.dim device = mv_data.device dtype = mv_data.dtype - # Build per-direction projections: keep only the e_i component g1_idx = self.algebra.grade_indices((1,), device=device) - N = mv_data.shape[0] - # Build all n projected multivectors: [n, N, dim] - xi_all = torch.zeros(n, N, dim, device=device, dtype=dtype) - coeffs = mv_data[:, g1_idx] # [N, n] - row_idx = torch.arange(n, device=device).unsqueeze(1).expand(n, N) - batch_idx = torch.arange(N, device=device).unsqueeze(0).expand(n, N) - col_idx = g1_idx.unsqueeze(1).expand(n, N) - xi_all[row_idx, batch_idx, col_idx] = coeffs.T - # All (i, j) pairs with i < j i_idx, j_idx = torch.triu_indices(n, n, offset=1, device=device) + if i_idx.numel() == 0: + return torch.zeros(n, n, device=device, dtype=dtype) - # Batched commutator: [n_pairs, N, dim] + coeffs = mv_data[:, g1_idx] # [N, n] + left = torch.zeros(i_idx.numel(), N, n, device=device, dtype=dtype) + right = torch.zeros_like(left) + left.scatter_(-1, i_idx.view(-1, 1, 1).expand(-1, N, 1), coeffs[:, i_idx].T.unsqueeze(-1)) + right.scatter_(-1, j_idx.view(-1, 1, 1).expand(-1, N, 1), coeffs[:, j_idx].T.unsqueeze(-1)) + + # Batched compact commutator: [n_pairs, N, grade2_dim] comm = self.algebra.commutator( - xi_all[i_idx], - xi_all[j_idx], + left, + right, left_grades=(1,), right_grades=(1,), output_grades=(2,), + left_compact=True, + right_compact=True, + compact_output=True, ) vals = comm.norm(dim=-1).mean(dim=-1) # [n_pairs] @@ -184,7 +184,6 @@ def lie_bracket_closure(self, mv_data: torch.Tensor) -> Dict: ``"closure_error"`` (scalar), and ``"basis_indices"`` (list of multivector-coefficient indices of the chosen bivectors). """ - dim = self.algebra.dim n = self.algebra.n device = mv_data.device dtype = mv_data.dtype @@ -196,13 +195,13 @@ def lie_bracket_closure(self, mv_data: torch.Tensor) -> Dict: "basis_indices": [], } - # Extract grade-2 part of mean per-sample - bv_data = self.algebra.grade_projection(mv_data, 2) # [N, dim] - mean_bv = bv_data.mean(dim=0) # [dim] + # Extract compact grade-2 part of mean per-sample + bv_data = self.algebra.grade_projection(mv_data, 2, compact_output=True) # [N, grade2_dim] + mean_bv = bv_data.mean(dim=0) # [grade2_dim] - bv_blade_indices = self.algebra.grade_indices((2,), device=device).tolist() + bv_blade_indices = self.algebra.grade_indices((2,), device=device) - if not bv_blade_indices: + if bv_blade_indices.numel() == 0: return { "structure_constants": torch.zeros(0, 0, 0, device=device), "closure_error": 0.0, @@ -210,29 +209,29 @@ def lie_bracket_closure(self, mv_data: torch.Tensor) -> Dict: } # Pick top-k by energy in the mean bivector - bv_idx_tensor = torch.tensor(bv_blade_indices, dtype=torch.long, device=device) - energies = mean_bv[bv_idx_tensor].abs() - k = min(self.max_bivectors, len(bv_blade_indices)) - topk_pos = energies.topk(k).indices.tolist() + energies = mean_bv.abs() + k = min(self.max_bivectors, int(bv_blade_indices.numel())) + topk_pos = energies.topk(k).indices + selected_indices = bv_blade_indices[topk_pos].tolist() - selected_indices = [bv_blade_indices[p] for p in topk_pos] - - # Build basis bivector multivectors - B = torch.zeros(k, dim, device=device, dtype=dtype) - sel_tensor = torch.tensor(selected_indices, dtype=torch.long, device=device) - B[torch.arange(k, device=device), sel_tensor] = 1.0 + # Build compact basis bivectors. + B = torch.zeros(k, bv_blade_indices.numel(), device=device, dtype=dtype) + B[torch.arange(k, device=device), topk_pos] = 1.0 # Compute structure constants c_{a,b,c} such that [B_a, B_b] ~= Sum_c c_{abc} B_c a_idx, b_idx = torch.triu_indices(k, k, offset=1, device=device) - # Batched commutator and grade-2 projection + # Batched compact commutator and grade-2 projection. brackets_bv = self.algebra.commutator( B[a_idx], B[b_idx], left_grades=(2,), right_grades=(2,), output_grades=(2,), - ) # [n_pairs, dim] + left_compact=True, + right_compact=True, + compact_output=True, + ) # [n_pairs, grade2_dim] # Project onto basis: coeffs[p, c] = coeffs = brackets_bv @ B.T # [n_pairs, k] @@ -242,7 +241,7 @@ def lie_bracket_closure(self, mv_data: torch.Tensor) -> Dict: structure[b_idx, a_idx, :] = -coeffs # antisymmetry # Closure errors: residual norm / bracket norm - projected = coeffs @ B # [n_pairs, dim] + projected = coeffs @ B # [n_pairs, grade2_dim] residuals = brackets_bv - projected res_norms = residuals.norm(dim=-1) # [n_pairs] bracket_norms = brackets_bv.norm(dim=-1) # [n_pairs] @@ -284,11 +283,18 @@ def compute_uncertainty_and_alignment(algebra: AlgebraLike, data_tensor: torch.T else: x_n = data_tensor[:, :n] - x = algebra.embed_vector(x_n) # [N, dim] - - # 2. Mean multivector and commutator [x_i, mu] - mu = x.mean(dim=0, keepdim=True) # [1, dim] - comm = algebra.commutator(x, mu.expand_as(x), left_grades=(1,), right_grades=(1,), output_grades=(2,)) + # 2. Mean grade-1 vector and compact commutator [x_i, mu] + mu = x_n.mean(dim=0, keepdim=True) # [1, n] + comm = algebra.commutator( + x_n, + mu.expand_as(x_n), + left_grades=(1,), + right_grades=(1,), + output_grades=(2,), + left_compact=True, + right_compact=True, + compact_output=True, + ) U = torch.norm(comm, p=2, dim=-1).mean().item() diff --git a/core/analysis/geodesic.py b/core/analysis/geodesic.py index 4a90f8a..599e916 100644 --- a/core/analysis/geodesic.py +++ b/core/analysis/geodesic.py @@ -86,8 +86,7 @@ def _knn(self, mv: torch.Tensor) -> torch.Tensor: """ N = mv.shape[0] k = min(self.k, N - 1) - diff = mv.unsqueeze(1) - mv.unsqueeze(0) # [N, N, dim] - dists = diff.norm(dim=-1) # [N, N] + dists = torch.cdist(mv, mv) # [N, N] dists.fill_diagonal_(float("inf")) _, idx = dists.topk(k, dim=-1, largest=False) return idx # [N, k] diff --git a/core/analysis/symmetry.py b/core/analysis/symmetry.py index 8c01303..578ce78 100644 --- a/core/analysis/symmetry.py +++ b/core/analysis/symmetry.py @@ -198,16 +198,14 @@ def detect_continuous_symmetries( dim = self.algebra.dim N = mv_data.shape[0] - ii, jj = torch.triu_indices(n, n, offset=1) - bv_indices = ((1 << ii) | (1 << jj)).tolist() + bv_idx_tensor = self.algebra.grade_indices((2,), device=mv_data.device) - if not bv_indices: + if bv_idx_tensor.numel() == 0: return 0 - n_bv = len(bv_indices) + n_bv = int(bv_idx_tensor.numel()) # Build all bivector bases: [n_bv, dim] bv_bases = torch.zeros(n_bv, dim, device=mv_data.device, dtype=mv_data.dtype) - bv_idx_tensor = torch.tensor(bv_indices, dtype=torch.long, device=mv_data.device) bv_bases[torch.arange(n_bv, device=mv_data.device), bv_idx_tensor] = 1.0 # Batch commutator: [n_bv, N, dim] From 8557c21038bddf41f5987a8ecbd52a8843ab852d Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 18:36:58 +0900 Subject: [PATCH 38/45] fix: align wedge with exterior product --- core/__init__.py | 4 ++ core/foundation/__init__.py | 4 ++ core/foundation/basis.py | 76 ++++++++++++++++++++++++++++++------- core/planning/product.py | 3 ++ core/planning/tree.py | 4 +- core/runtime/algebra.py | 21 ++++++---- core/runtime/multivector.py | 4 +- tests/test_grade_plan.py | 72 +++++++++++++++++++++++++++++++++++ 8 files changed, 162 insertions(+), 26 deletions(-) diff --git a/core/__init__.py b/core/__init__.py index 07b9123..faf12f0 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -20,6 +20,8 @@ geometric_product_output_grades, normalize_grades, operation_coefficient, + operation_may_be_nonzero, + product_output_grades, reverse_sign, ) from .foundation.device import DeviceConfig, dtype_name, optional_dtype, resolve_device, resolve_dtype @@ -141,6 +143,8 @@ "geometric_product_output_grades", "normalize_grades", "operation_coefficient", + "operation_may_be_nonzero", + "product_output_grades", "reverse_sign", # analysis (lazy) "MetricSearch", diff --git a/core/foundation/__init__.py b/core/foundation/__init__.py index 130f764..2c8c726 100644 --- a/core/foundation/__init__.py +++ b/core/foundation/__init__.py @@ -17,6 +17,8 @@ geometric_product_output_grades, normalize_grades, operation_coefficient, + operation_may_be_nonzero, + product_output_grades, reverse_sign, ) from .device import DeviceConfig, dtype_name, optional_dtype, resolve_device, resolve_dtype @@ -42,7 +44,9 @@ "geometric_product_output_grades", "normalize_grades", "operation_coefficient", + "operation_may_be_nonzero", "optional_dtype", + "product_output_grades", "reverse_sign", "resolve_device", "resolve_dtype", diff --git a/core/foundation/basis.py b/core/foundation/basis.py index cec3a55..71a691e 100644 --- a/core/foundation/basis.py +++ b/core/foundation/basis.py @@ -113,12 +113,7 @@ def expand_output_grades( outputs: set[int] = set() for left_grade in left: for right_grade in right: - if op == "wedge": - grade = left_grade + right_grade - if grade <= n: - outputs.add(grade) - else: - outputs.update(geometric_product_output_grades(left_grade, right_grade, n)) + outputs.update(product_output_grades(left_grade, right_grade, n, op=op)) if project_grades is not None: outputs &= set(normalize_grades(project_grades, n, name="project_grades")) @@ -130,6 +125,33 @@ def expand_output_grades( return tuple(sorted(outputs)) +def product_output_grades(left_grade: int, right_grade: int, n: int, *, op: GradeProductOp = "gp") -> tuple[int, ...]: + """Return possible output grades for one homogeneous product route.""" + left_grade = int(left_grade) + right_grade = int(right_grade) + if op == "wedge": + grade = left_grade + right_grade + return (grade,) if grade <= n else () + + outputs = geometric_product_output_grades(left_grade, right_grade, n) + if op == "gp": + return outputs + if op not in {"inner", "commutator", "anti_commutator"}: + raise ValueError(f"Unsupported grade product op {op!r}") + + filtered = [] + for output_grade in outputs: + overlap = (left_grade + right_grade - output_grade) // 2 + parity_odd = ((left_grade * right_grade - overlap) % 2) == 1 + if op == "commutator": + keep = parity_odd + else: + keep = not parity_odd + if keep: + filtered.append(output_grade) + return tuple(filtered) + + def basis_product(index_a: int, index_b: int, p: int, q: int, r: int) -> tuple[int, float]: """Return ``(index, sign)`` for two canonical basis blade products.""" n = p + q + r @@ -159,17 +181,43 @@ def reverse_sign(index: int) -> float: def operation_coefficient(index_a: int, index_b: int, p: int, q: int, r: int, op: GradeProductOp) -> float: """Return the scalar coefficient multiplying ``A_i * B_j`` for ``op``.""" + if not operation_may_be_nonzero(index_a, index_b, p, q, r, op): + return 0.0 + _, sign_ab = basis_product(index_a, index_b, p, q, r) - if op == "gp": + if op in {"gp", "wedge", "inner"}: return sign_ab - _, sign_ba = basis_product(index_b, index_a, p, q, r) - if op == "wedge": - return 0.5 * (sign_ab - sign_ba) - if op == "inner": - return 0.5 * (sign_ab + sign_ba) if op == "commutator": - return sign_ab - sign_ba + return 2.0 * sign_ab if op == "anti_commutator": - return sign_ab + sign_ba + return 2.0 * sign_ab raise ValueError(f"Unsupported grade product op {op!r}") + + +def operation_may_be_nonzero(index_a: int, index_b: int, p: int, q: int, r: int, op: GradeProductOp) -> bool: + """Return whether an operator can have a non-zero coefficient for a basis pair.""" + overlap_mask = int(index_a) & int(index_b) + if op == "wedge": + return overlap_mask == 0 + + if r > 0: + null_mask = ((1 << r) - 1) << (p + q) + if overlap_mask & null_mask: + return False + if op == "gp": + return True + + parity_odd = _swap_parity_between_orders(index_a, index_b) == 1 + if op == "commutator": + return parity_odd + if op in {"inner", "anti_commutator"}: + return not parity_odd + raise ValueError(f"Unsupported grade product op {op!r}") + + +def _swap_parity_between_orders(index_a: int, index_b: int) -> int: + left_grade = int(index_a).bit_count() + right_grade = int(index_b).bit_count() + overlap = (int(index_a) & int(index_b)).bit_count() + return (left_grade * right_grade - overlap) % 2 diff --git a/core/planning/product.py b/core/planning/product.py index eab5370..e09afa6 100644 --- a/core/planning/product.py +++ b/core/planning/product.py @@ -201,9 +201,12 @@ def build_grade_product_plan_from_tree( plan_coefficients: list[float] = [] for path in tree.paths: + path_output_grades = set(path.output_grades) for left_index in left_basis_by_grade[path.left_grade]: for right_index in right_basis_by_grade[path.right_grade]: output_index = left_index ^ right_index + if output_index.bit_count() not in path_output_grades: + continue output_position = output_position_by_index.get(output_index) if output_position is None: continue diff --git a/core/planning/tree.py b/core/planning/tree.py index 48da362..c1312b9 100644 --- a/core/planning/tree.py +++ b/core/planning/tree.py @@ -17,7 +17,7 @@ from dataclasses import dataclass from typing import Iterable, Optional -from core.foundation.basis import GradeProductOp, expand_output_grades, normalize_grades +from core.foundation.basis import GradeProductOp, expand_output_grades, normalize_grades, product_output_grades from core.foundation.layout import AlgebraSpec @@ -98,7 +98,7 @@ def build_grade_plan_tree( for left_grade in left: left_dim = _grade_dim(spec.n, left_grade) for right_grade in right: - route_outputs = expand_output_grades((left_grade,), (right_grade,), spec.n, op=op) + route_outputs = product_output_grades(left_grade, right_grade, spec.n, op=op) route_outputs = tuple(grade for grade in route_outputs if grade in output_set) if not route_outputs: continue diff --git a/core/runtime/algebra.py b/core/runtime/algebra.py index 65a2623..8dad30e 100644 --- a/core/runtime/algebra.py +++ b/core/runtime/algebra.py @@ -354,12 +354,16 @@ def _generate_cayley_table(self, device, dtype: torch.dtype = torch.float32): else: bv_sq_scalar = torch.zeros(0, dtype=cayley_signs.dtype, device=device) - # Precomputed signs for single-pass wedge and inner product - # wedge(A,B) = (AB - BA)/2 uses antisymmetric part of signs + # Precomputed signs for single-pass exterior and symmetric products. + # wedge(A,B) is the exterior product: the grade-sum part of AB. + gi = grade_index.unsqueeze(1) # [D, 1] - left summation index grade + gj_for_result = grade_index[cayley_indices] # [D, D] - right index j = i^k grade + gk = grade_index.unsqueeze(0) # [1, D] - output index k grade + exterior_valid = gk == gi + gj_for_result + wedge_gp_signs = gp_signs * exterior_valid.to(dtype=gp_signs.dtype) + # inner(A,B) = (AB + BA)/2 uses symmetric part of signs - wedge_cayley_signs = (cayley_signs - cayley_signs.T) / 2.0 inner_cayley_signs = (cayley_signs + cayley_signs.T) / 2.0 - wedge_gp_signs = torch.gather(wedge_cayley_signs, 1, cayley_indices) inner_gp_signs = torch.gather(inner_cayley_signs, 1, cayley_indices) # Precomputed signs for commutator [A,B] = AB - BA and @@ -552,9 +556,11 @@ def reverse(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: return mv * rev def wedge(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: - """Computes the wedge (outer) product: A ^ B = (AB - BA)/2. + """Computes the wedge/exterior product ``A ^ B``. - Single-pass implementation using precomputed antisymmetric signs. + For homogeneous inputs this is the grade-sum part of the + geometric product, ``_{grade(A)+grade(B)}``. For vectors this + coincides with ``(AB - BA) / 2``. Reference: Pence, T., Yamada, D., & Singh, V. (2025). "Composing Linear Layers @@ -645,8 +651,7 @@ def inner_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Ten def commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: """Computes the commutator (Lie bracket): [A, B] = AB - BA. - Single-pass implementation using precomputed antisymmetric signs - (same structure as :meth:`wedge` but without the 1/2 factor). + Single-pass implementation using precomputed antisymmetric signs. Args: A (torch.Tensor): Left operand [..., dim]. diff --git a/core/runtime/multivector.py b/core/runtime/multivector.py index 4aa4e93..9fc2231 100644 --- a/core/runtime/multivector.py +++ b/core/runtime/multivector.py @@ -297,7 +297,7 @@ def __truediv__(self, other): return NotImplemented def __xor__(self, other): - """Wedge (outer) product ``A ^ B``.""" + """Wedge/exterior product ``A ^ B``.""" if isinstance(other, Multivector): self._check_algebra(other) return self.wedge(other) @@ -424,7 +424,7 @@ def projected_product( return self._wrap_compact(values, layout) def wedge(self, other: Multivector) -> Multivector: - """Wedge (outer) product (same as ``self ^ other``).""" + """Wedge/exterior product (same as ``self ^ other``).""" self._check_algebra(other) if self.is_compact or other.is_compact: return self.projected_product(other, op="wedge") diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index 44d9547..a828656 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -8,6 +8,8 @@ basis_indices_for_grades, expand_output_grades, geometric_product_output_grades, + operation_coefficient, + product_output_grades, ) from core.foundation.layout import AlgebraSpec from core.planning.flow import GradeFlow @@ -47,11 +49,22 @@ def _grade_only_input(algebra, batch: int, grades: tuple[int, ...], seed: int) - def test_grade_expansion_for_common_high_dim_paths(): assert geometric_product_output_grades(1, 1, 16) == (0, 2) assert geometric_product_output_grades(2, 1, 16) == (1, 3) + assert product_output_grades(2, 1, 16, op="wedge") == (3,) + assert product_output_grades(2, 1, 16, op="commutator") == (1,) + assert product_output_grades(2, 1, 16, op="anti_commutator") == (3,) assert expand_output_grades((0, 2), (1,), 16, op="gp") == (1, 3) assert expand_output_grades((1,), (1,), 16, op="wedge") == (2,) assert expand_output_grades((1,), (1,), 16, op="gp", project_grades=(0,)) == (0,) +def test_operation_coefficients_keep_wedge_as_exterior_product(): + # e12 and e3 commute, so the antisymmetric formula would vanish. The + # exterior product is instead the grade-sum part of the geometric product. + assert operation_coefficient(3, 4, 3, 0, 0, "wedge") == 1.0 + assert operation_coefficient(3, 4, 3, 0, 0, "commutator") == 0.0 + assert operation_coefficient(3, 4, 3, 0, 0, "anti_commutator") == 2.0 + + def test_basis_indices_for_grades_are_combinatorial_and_high_dimensional(): assert basis_index_tuple_for_grades(4, (1, 2)) == tuple( index for index in range(1 << 4) if index.bit_count() in {1, 2} @@ -208,6 +221,65 @@ def test_static_grade_product_matches_dense_kernel_for_selected_grade_paths(op): assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) +def test_wedge_dense_and_planned_paths_are_exterior_product_for_higher_grades(): + algebra = CliffordAlgebra(3, 0, 0, device=DEVICE, dtype=torch.float64) + spec = AlgebraSpec.from_algebra(algebra) + layout_2 = spec.layout((2,)) + layout_1 = spec.layout((1,)) + layout_3 = spec.layout((3,)) + + e12 = torch.zeros(1, algebra.dim, dtype=torch.float64) + e12[0, 3] = 1.0 + e3 = torch.zeros(1, algebra.dim, dtype=torch.float64) + e3[0, 4] = 1.0 + expected = torch.zeros_like(e12) + expected[0, 7] = 1.0 + + dense = algebra.wedge(e12, e3) + compact = algebra.wedge( + layout_2.compact(e12), + layout_1.compact(e3), + left_layout=layout_2, + right_layout=layout_1, + output_grades=(3,), + left_compact=True, + right_compact=True, + compact_output=True, + ) + + assert torch.allclose(dense, expected, atol=1e-12, rtol=1e-12) + assert torch.allclose(compact, layout_3.compact(expected), atol=1e-12, rtol=1e-12) + + +def test_wedge_plan_prunes_grade_route_pairs_before_coefficients(): + algebra = CliffordAlgebra(6, 0, 0, device=DEVICE, dtype=torch.float64) + broad = build_grade_product_plan( + algebra.p, + algebra.q, + algebra.r, + left_grades=(2,), + right_grades=(1,), + output_grades=(1, 3), + op="wedge", + device=DEVICE, + dtype=torch.float64, + ) + exterior_only = build_grade_product_plan( + algebra.p, + algebra.q, + algebra.r, + left_grades=(2,), + right_grades=(1,), + output_grades=(3,), + op="wedge", + device=DEVICE, + dtype=torch.float64, + ) + + assert broad.pair_count == exterior_only.pair_count + assert all(int(index).bit_count() == 3 for index in broad.output_indices.tolist()) + + def test_product_plan_owns_compact_position_buffers(): algebra = CliffordAlgebra(4, 1, 0, device=DEVICE, dtype=torch.float64) plan = build_grade_product_plan( From 859f2ccd9f305d9a7d94d4bbb1e1625dec4fec02 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Thu, 14 May 2026 21:24:16 +0900 Subject: [PATCH 39/45] fix: use exterior wedge in lattice volume --- experiments/inc_lattice_morph.py | 17 ++++++++++------- tests/test_grade_plan.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/experiments/inc_lattice_morph.py b/experiments/inc_lattice_morph.py index 1706df0..84b6fa1 100644 --- a/experiments/inc_lattice_morph.py +++ b/experiments/inc_lattice_morph.py @@ -122,10 +122,7 @@ def __init__(self, algebra: AlgebraLike): self.algebra = algebra def compute_volume(self, basis_mvs: torch.Tensor) -> torch.Tensor: - """det(L) = ||b_1 ^ b_2 ^ ... ^ b_n|| via iterative outer product. - - Uses grade_projection(GP(blade, b_i), k+1) instead of wedge(), - because wedge() is (AB-BA)/2 which only works for two vectors. + """det(L) = ||b_1 ^ b_2 ^ ... ^ b_n|| via iterative exterior product. Args: basis_mvs: [n, D] grade-1 multivectors. @@ -135,9 +132,15 @@ def compute_volume(self, basis_mvs: torch.Tensor) -> torch.Tensor: blade = basis_mvs[0] current_grade = 1 for i in range(1, basis_mvs.shape[0]): - product = self.algebra.geometric_product(blade, basis_mvs[i]) - current_grade += 1 - blade = self.algebra.grade_projection(product, current_grade) + next_grade = current_grade + 1 + blade = self.algebra.wedge( + blade, + basis_mvs[i], + left_grades=(current_grade,), + right_grades=(1,), + output_grades=(next_grade,), + ) + current_grade = next_grade return induced_norm(self.algebra, blade).squeeze(-1) def compute_gram_matrix(self, basis_mvs: torch.Tensor) -> torch.Tensor: diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index a828656..5098b24 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -251,6 +251,23 @@ def test_wedge_dense_and_planned_paths_are_exterior_product_for_higher_grades(): assert torch.allclose(compact, layout_3.compact(expected), atol=1e-12, rtol=1e-12) +def test_wedge_chains_as_iterative_exterior_product(): + algebra = CliffordAlgebra(4, 0, 0, device=DEVICE, dtype=torch.float64) + e1 = torch.zeros(1, algebra.dim, dtype=torch.float64) + e2 = torch.zeros(1, algebra.dim, dtype=torch.float64) + e3 = torch.zeros(1, algebra.dim, dtype=torch.float64) + e1[0, 1] = 1.0 + e2[0, 2] = 1.0 + e3[0, 4] = 1.0 + expected = torch.zeros_like(e1) + expected[0, 7] = 1.0 + + e12 = algebra.wedge(e1, e2, left_grades=(1,), right_grades=(1,), output_grades=(2,)) + actual = algebra.wedge(e12, e3, left_grades=(2,), right_grades=(1,), output_grades=(3,)) + + assert torch.allclose(actual, expected, atol=1e-12, rtol=1e-12) + + def test_wedge_plan_prunes_grade_route_pairs_before_coefficients(): algebra = CliffordAlgebra(6, 0, 0, device=DEVICE, dtype=torch.float64) broad = build_grade_product_plan( From a19a4b0eb9d08dd79f1a9cea72d2756123a398a2 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Fri, 15 May 2026 10:24:27 +0900 Subject: [PATCH 40/45] chore: raise python support floor to 3.10 --- .github/workflows/docs.yml | 2 +- .github/workflows/tests.yml | 8 +- README.md | 6 +- docs/index.md | 4 +- pyproject.toml | 5 +- uv.lock | 2329 +++-------------------------------- 6 files changed, 212 insertions(+), 2142 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5d39e3d..57ea95d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.10" - name: Install documentation dependencies run: uv sync --group docs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6808c2a..572644a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.10" - name: Install core + sr + md17 + dev extras run: uv sync --extra sr --extra md17 --extra dev @@ -62,7 +62,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.10" - name: Install all task extras + dev run: uv sync --extra sr --extra md17 --extra lqa --extra dev @@ -88,7 +88,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.10" - name: Install all task extras + dev run: uv sync --extra sr --extra md17 --extra lqa --extra dev @@ -114,7 +114,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.10" - name: Install all task extras + dev run: uv sync --extra sr --extra md17 --extra lqa --extra dev diff --git a/README.md b/README.md index f22bd0e..147bc34 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Versor: A PyTorch Framework for Geometric Algebra Deep Learning -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) [![PyTorch](https://img.shields.io/badge/PyTorch-2.0+-ee4c2c.svg)](https://pytorch.org/) [![Docs](https://img.shields.io/badge/docs-MkDocs-brightgreen)](https://concode0.github.io/Versor/) [![DOI](https://zenodo.org/badge/1149480519.svg)](https://doi.org/10.5281/zenodo.18939518) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![PyTorch](https://img.shields.io/badge/PyTorch-2.0+-ee4c2c.svg)](https://pytorch.org/) [![Docs](https://img.shields.io/badge/docs-MkDocs-brightgreen)](https://concode0.github.io/Versor/) [![DOI](https://zenodo.org/badge/1149480519.svg)](https://doi.org/10.5281/zenodo.18939518) > **"There is a ceiling above standard Deep Learning that no one saw. Versor opens the door above it."** @@ -59,7 +59,7 @@ For code examples of each innovation, see [docs/innovations.md](docs/innovations ## Installation -Versor requires Python 3.9+ and PyTorch. +Versor requires Python 3.10+ and PyTorch. ```bash # Clone the repository @@ -361,4 +361,4 @@ By releasing this under Apache 2.0, we provide a **perpetual, royalty-free paten ``` ## Reference: - * Pence, T., Yamada, D., & Singh, V. (2025). "Composing Linear Layers from Irreducibles." arXiv:2507.11688v1 [cs.LG] \ No newline at end of file + * Pence, T., Yamada, D., & Singh, V. (2025). "Composing Linear Layers from Irreducibles." arXiv:2507.11688v1 [cs.LG] diff --git a/docs/index.md b/docs/index.md index 04bd3a9..11f47b6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Versor: A PyTorch Framework for Geometric Algebra Deep Learning -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) [![PyTorch](https://img.shields.io/badge/PyTorch-2.0+-ee4c2c.svg)](https://pytorch.org/) [![Docs](https://img.shields.io/badge/docs-MkDocs-brightgreen)](https://concode0.github.io/Versor/) [![DOI](https://zenodo.org/badge/1149480519.svg)](https://doi.org/10.5281/zenodo.18939518) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![PyTorch](https://img.shields.io/badge/PyTorch-2.0+-ee4c2c.svg)](https://pytorch.org/) [![Docs](https://img.shields.io/badge/docs-MkDocs-brightgreen)](https://concode0.github.io/Versor/) [![DOI](https://zenodo.org/badge/1149480519.svg)](https://doi.org/10.5281/zenodo.18939518) > **"There is a ceiling above standard Deep Learning that no one saw. Versor opens the door above it."** @@ -53,7 +53,7 @@ For code examples of each innovation, see [Innovations](innovations.md). ## Installation -Versor requires Python 3.9+ and PyTorch. +Versor requires Python 3.10+ and PyTorch. ```bash # Clone the repository diff --git a/pyproject.toml b/pyproject.toml index ffda5b7..97fc84b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "versor" version = "1.0.0" description = "A PyTorch framework for Geometric Algebra Deep Learning" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" license = { file = "LICENSE" } authors = [ { name = "Eunkyum Kim", email = "nemonanconcode@gmail.com" }, @@ -24,7 +24,6 @@ classifiers = [ "Topic :: Scientific/Engineering :: Mathematics", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -78,7 +77,7 @@ all = [ include = ["core*", "layers*", "models*", "datalib*", "optimizers*", "functional*", "tasks*", "examples*", "experiments*"] [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 120 indent-width = 4 extend-exclude = [ diff --git a/uv.lock b/uv.lock index 4475c2e..07c1e35 100644 --- a/uv.lock +++ b/uv.lock @@ -1,22 +1,10 @@ version = 1 revision = 2 -requires-python = ">=3.9" +requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", - "python_full_version < '3.10'", + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", ] [[package]] @@ -146,23 +134,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, - { url = "https://files.pythonhosted.org/packages/bf/79/446655656861d3e7e2c32bfcf160c7aa9e9dc63776a691b124dba65cdd77/aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e", size = 741433, upload-time = "2026-01-03T17:32:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/cb/49/773c4b310b5140d2fb5e79bb0bf40b7b41dad80a288ca1a8759f5f72bda9/aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7", size = 497332, upload-time = "2026-01-03T17:32:28.37Z" }, - { url = "https://files.pythonhosted.org/packages/bc/31/1dcbc4b83a4e6f76a0ad883f07f21ffbfe29750c89db97381701508c9f45/aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02", size = 492365, upload-time = "2026-01-03T17:32:30.234Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b5/b50657496c8754482cd7964e50aaf3aa84b3db61ed45daec4c1aec5b94b4/aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43", size = 1660440, upload-time = "2026-01-03T17:32:32.586Z" }, - { url = "https://files.pythonhosted.org/packages/2a/73/9b69e5139d89d75127569298931444ad78ea86a5befd5599780b1e9a6880/aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6", size = 1632740, upload-time = "2026-01-03T17:32:34.793Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fe/3ea9b5af694b4e3aec0d0613a806132ca744747146fca68e96bf056f61a7/aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce", size = 1719782, upload-time = "2026-01-03T17:32:37.737Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c2/46b3b06e60851cbb71efb0f79a3267279cbef7b12c58e68a1e897f269cca/aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80", size = 1813527, upload-time = "2026-01-03T17:32:39.973Z" }, - { url = "https://files.pythonhosted.org/packages/36/23/71ceb78c769ed65fe4c697692de232b63dab399210678d2b00961ccb0619/aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a", size = 1661268, upload-time = "2026-01-03T17:32:42.082Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8d/86e929523d955e85ebab7c0e2b9e0cb63604cfc27dc3280e10d0063cf682/aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6", size = 1552742, upload-time = "2026-01-03T17:32:44.622Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/3f5987cba1bab6bd151f0d97aa60f0ce04d3c83316692a6bb6ba2fb69f92/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558", size = 1632918, upload-time = "2026-01-03T17:32:46.749Z" }, - { url = "https://files.pythonhosted.org/packages/be/2c/7e1e85121f2e31ee938cb83a8f32dfafd4908530c10fabd6d46761c12ac7/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7", size = 1644446, upload-time = "2026-01-03T17:32:49.063Z" }, - { url = "https://files.pythonhosted.org/packages/5d/35/ce6133d423ad0e8ca976a7c848f7146bca3520eea4ccf6b95e2d077c9d20/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877", size = 1689487, upload-time = "2026-01-03T17:32:51.113Z" }, - { url = "https://files.pythonhosted.org/packages/50/f7/ff7a27c15603d460fd1366b3c22054f7ae4fa9310aca40b43bde35867fcd/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3", size = 1540715, upload-time = "2026-01-03T17:32:53.38Z" }, - { url = "https://files.pythonhosted.org/packages/17/02/053f11346e5b962e6d8a1c4f8c70c29d5970a1b4b8e7894c68e12c27a57f/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704", size = 1711835, upload-time = "2026-01-03T17:32:56.088Z" }, - { url = "https://files.pythonhosted.org/packages/fb/71/9b9761ddf276fd6708d13720197cbac19b8d67ecfa9116777924056cfcaa/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f", size = 1649593, upload-time = "2026-01-03T17:32:58.181Z" }, - { url = "https://files.pythonhosted.org/packages/ae/72/5d817e9ea218acae12a5e3b9ad1178cf0c12fc3570c0b47eea2daf95f9ea/aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1", size = 434831, upload-time = "2026-01-03T17:33:00.577Z" }, - { url = "https://files.pythonhosted.org/packages/39/cb/22659d9bf3149b7a2927bc2769cc9c8f8f5a80eba098398e03c199a43a85/aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538", size = 457697, upload-time = "2026-01-03T17:33:03.167Z" }, ] [[package]] @@ -178,51 +149,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] -[[package]] -name = "altair" -version = "5.5.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "narwhals", marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305, upload-time = "2024-11-23T23:39:58.542Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200, upload-time = "2024-11-23T23:39:56.4Z" }, -] - [[package]] name = "altair" version = "6.0.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "jinja2", marker = "python_full_version >= '3.10'" }, - { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "narwhals", marker = "python_full_version >= '3.10'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.15'" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/c0/184a89bd5feba14ff3c41cfaf1dd8a82c05f5ceedbc92145e17042eb08a4/altair-6.0.0.tar.gz", hash = "sha256:614bf5ecbe2337347b590afb111929aa9c16c9527c4887d96c9bc7f6640756b4", size = 763834, upload-time = "2025-11-12T08:59:11.519Z" } wheels = [ @@ -240,9 +176,9 @@ name = "anyio" version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ @@ -403,62 +339,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, - { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, - { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, - { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, - { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, - { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, - { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, - { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, - { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ @@ -474,94 +363,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "contourpy" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, - { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, - { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, - { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, - { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, - { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, - { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, - { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, - { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, - { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, - { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, - { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, - { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, - { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, - { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, - { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879, upload-time = "2024-08-27T20:53:51.597Z" }, - { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573, upload-time = "2024-08-27T20:53:55.659Z" }, - { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184, upload-time = "2024-08-27T20:54:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262, upload-time = "2024-08-27T20:54:05.234Z" }, - { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806, upload-time = "2024-08-27T20:54:09.889Z" }, - { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710, upload-time = "2024-08-27T20:54:14.536Z" }, - { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107, upload-time = "2024-08-27T20:54:29.735Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458, upload-time = "2024-08-27T20:54:45.507Z" }, - { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643, upload-time = "2024-08-27T20:55:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301, upload-time = "2024-08-27T20:55:56.509Z" }, - { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972, upload-time = "2024-08-27T20:54:50.347Z" }, - { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375, upload-time = "2024-08-27T20:54:54.909Z" }, - { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188, upload-time = "2024-08-27T20:55:00.184Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644, upload-time = "2024-08-27T20:55:05.673Z" }, - { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141, upload-time = "2024-08-27T20:55:11.047Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469, upload-time = "2024-08-27T20:55:15.914Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894, upload-time = "2024-08-27T20:55:31.553Z" }, - { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829, upload-time = "2024-08-27T20:55:47.837Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, - { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, - { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, - { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, - { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, - { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, - { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, - { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, - { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, - { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, -] - [[package]] name = "contourpy" version = "1.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", + "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -628,18 +438,8 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", ] dependencies = [ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -724,7 +524,7 @@ name = "cuda-bindings" version = "12.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "(python_full_version == '3.10.*' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cuda-pathfinder" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, @@ -734,7 +534,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, - { url = "https://files.pythonhosted.org/packages/53/1d/f7f2bcffe788aebd4325a34d8a976b219a0751c06707aa89c9e70355ceae/cuda_bindings-12.9.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9866ceec83e39337d1a1d64837864c964ad902992478caa288a0bc1be95f21aa", size = 12152579, upload-time = "2025-10-21T14:52:16.731Z" }, ] [[package]] @@ -760,20 +559,15 @@ version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dill" }, - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "filelock" }, { name = "fsspec", extra = ["http"] }, - { name = "huggingface-hub", version = "0.36.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "huggingface-hub", version = "1.3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "huggingface-hub" }, { name = "multiprocess" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, { name = "pandas" }, - { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pyarrow", version = "23.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyarrow" }, { name = "pyyaml" }, { name = "requests" }, { name = "tqdm" }, @@ -814,131 +608,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] -[[package]] -name = "filelock" -version = "3.19.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, -] - [[package]] name = "filelock" version = "3.20.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] -[[package]] -name = "fonttools" -version = "4.60.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/db6a7b5eb0656534c3aa2596c2c5e18830d74f1b9aa5aa8a7dff63a0b11d/fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c", size = 3562254, upload-time = "2025-12-09T13:38:11.835Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/de/9e10a99fb3070accb8884886a41a4ce54e49bf2fa4fc63f48a6cf2061713/fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050", size = 2850403, upload-time = "2025-12-09T13:35:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/e4/40/d5b369d1073b134f600a94a287e13b5bdea2191ba6347d813fa3da00e94a/fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c", size = 2398629, upload-time = "2025-12-09T13:35:56.471Z" }, - { url = "https://files.pythonhosted.org/packages/7c/b5/123819369aaf99d1e4dc49f1de1925d4edc7379114d15a56a7dd2e9d56e6/fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34", size = 4893471, upload-time = "2025-12-09T13:35:58.927Z" }, - { url = "https://files.pythonhosted.org/packages/24/29/f8f8acccb9716b899be4be45e9ce770d6aa76327573863e68448183091b0/fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343", size = 4854686, upload-time = "2025-12-09T13:36:01.767Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0d/f3f51d7519f44f2dd5c9a60d7cd41185ebcee4348f073e515a3a93af15ff/fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334", size = 4871233, upload-time = "2025-12-09T13:36:06.094Z" }, - { url = "https://files.pythonhosted.org/packages/cc/3f/4d4fd47d3bc40ab4d76718555185f8adffb5602ea572eac4bbf200c47d22/fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a", size = 4988936, upload-time = "2025-12-09T13:36:08.42Z" }, - { url = "https://files.pythonhosted.org/packages/01/6f/83bbdefa43f2c3ae206fd8c4b9a481f3c913eef871b1ce9a453069239e39/fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96", size = 2278044, upload-time = "2025-12-09T13:36:10.641Z" }, - { url = "https://files.pythonhosted.org/packages/d4/04/7d9a137e919d6c9ef26704b7f7b2580d9cfc5139597588227aacebc0e3b7/fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45", size = 2326522, upload-time = "2025-12-09T13:36:12.981Z" }, - { url = "https://files.pythonhosted.org/packages/e0/80/b7693d37c02417e162cc83cdd0b19a4f58be82c638b5d4ce4de2dae050c4/fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae", size = 2847809, upload-time = "2025-12-09T13:36:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/f9/9a/9c2c13bf8a6496ac21607d704e74e9cc68ebf23892cf924c9a8b5c7566b9/fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1", size = 2397302, upload-time = "2025-12-09T13:36:17.151Z" }, - { url = "https://files.pythonhosted.org/packages/56/f6/ce38ff6b2d2d58f6fd981d32f3942365bfa30eadf2b47d93b2d48bf6097f/fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a", size = 5054418, upload-time = "2025-12-09T13:36:19.062Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/5353bea128ff39e857c31de3dd605725b4add956badae0b31bc9a50d4c8e/fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e", size = 5031652, upload-time = "2025-12-09T13:36:21.206Z" }, - { url = "https://files.pythonhosted.org/packages/71/05/ebca836437f6ebd57edd6428e7eff584e683ff0556ddb17d62e3b731f46c/fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779", size = 5030321, upload-time = "2025-12-09T13:36:23.515Z" }, - { url = "https://files.pythonhosted.org/packages/57/f9/eb9d2a2ce30c99f840c1cc3940729a970923cf39d770caf88909d98d516b/fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb", size = 5154255, upload-time = "2025-12-09T13:36:25.879Z" }, - { url = "https://files.pythonhosted.org/packages/08/a2/088b6ceba8272a9abb629d3c08f9c1e35e5ce42db0ccfe0c1f9f03e60d1d/fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a", size = 2276300, upload-time = "2025-12-09T13:36:27.772Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/8e4c3d908cc5dade7bb1316ce48589f6a24460c1056fd4b8db51f1fa309a/fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e", size = 2327574, upload-time = "2025-12-09T13:36:30.89Z" }, - { url = "https://files.pythonhosted.org/packages/c0/30/530c9eddcd1c39219dc0aaede2b5a4c8ab80e0bb88d1b3ffc12944c4aac3/fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2", size = 2847196, upload-time = "2025-12-09T13:36:33.262Z" }, - { url = "https://files.pythonhosted.org/packages/19/2f/4077a482836d5bbe3bc9dac1c004d02ee227cf04ed62b0a2dfc41d4f0dfd/fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033", size = 2395842, upload-time = "2025-12-09T13:36:35.47Z" }, - { url = "https://files.pythonhosted.org/packages/dd/05/aae5bb99c5398f8ed4a8b784f023fd9dd3568f0bd5d5b21e35b282550f11/fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917", size = 4949713, upload-time = "2025-12-09T13:36:37.874Z" }, - { url = "https://files.pythonhosted.org/packages/b4/37/49067349fc78ff0efbf09fadefe80ddf41473ca8f8a25400e3770da38328/fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd", size = 4999907, upload-time = "2025-12-09T13:36:39.853Z" }, - { url = "https://files.pythonhosted.org/packages/16/31/d0f11c758bd0db36b664c92a0f9dfdcc2d7313749aa7d6629805c6946f21/fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13", size = 4939717, upload-time = "2025-12-09T13:36:43.075Z" }, - { url = "https://files.pythonhosted.org/packages/d9/bc/1cff0d69522e561bf1b99bee7c3911c08c25e919584827c3454a64651ce9/fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153", size = 5089205, upload-time = "2025-12-09T13:36:45.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/e6/fb174f0069b7122e19828c551298bfd34fdf9480535d2a6ac2ed37afacd3/fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70", size = 2264674, upload-time = "2025-12-09T13:36:49.238Z" }, - { url = "https://files.pythonhosted.org/packages/75/57/6552ffd6b582d3e6a9f01780c5275e6dfff1e70ca146101733aa1c12a129/fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0", size = 2314701, upload-time = "2025-12-09T13:36:51.09Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e4/8381d0ca6b6c6c484660b03517ec5b5b81feeefca3808726dece36c652a9/fonttools-4.60.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bb5fd231e56ccd7403212636dcccffc96c5ae0d6f9e4721fa0a32cb2e3ca432", size = 2842063, upload-time = "2025-12-09T13:36:53.468Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2c/4367117ee8ff4f4374787a1222da0bd413d80cf3522111f727a7b8f80d1d/fonttools-4.60.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:536b5fab7b6fec78ccf59b5c59489189d9d0a8b0d3a77ed1858be59afb096696", size = 2393792, upload-time = "2025-12-09T13:36:55.742Z" }, - { url = "https://files.pythonhosted.org/packages/49/b7/a76b6dffa193869e54e32ca2f9abb0d0e66784bc8a24e6f86eb093015481/fonttools-4.60.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b9288fc38252ac86a9570f19313ecbc9ff678982e0f27c757a85f1f284d3400", size = 4924020, upload-time = "2025-12-09T13:36:58.229Z" }, - { url = "https://files.pythonhosted.org/packages/bd/4e/0078200e2259f0061c86a74075f507d64c43dd2ab38971956a5c0012d344/fonttools-4.60.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93fcb420791d839ef592eada2b69997c445d0ce9c969b5190f2e16828ec10607", size = 4980070, upload-time = "2025-12-09T13:37:00.311Z" }, - { url = "https://files.pythonhosted.org/packages/85/1f/d87c85a11cb84852c975251581862681e4a0c1c3bd456c648792203f311b/fonttools-4.60.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7916a381b094db4052ac284255186aebf74c5440248b78860cb41e300036f598", size = 4921411, upload-time = "2025-12-09T13:37:02.345Z" }, - { url = "https://files.pythonhosted.org/packages/75/c0/7efad650f5ed8e317c2633133ef3c64917e7adf2e4e2940c798f5d57ec6e/fonttools-4.60.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58c8c393d5e16b15662cfc2d988491940458aa87894c662154f50c7b49440bef", size = 5063465, upload-time = "2025-12-09T13:37:04.836Z" }, - { url = "https://files.pythonhosted.org/packages/18/a8/750518c4f8cdd79393b386bc81226047ade80239e58c6c9f5dbe1fdd8ea1/fonttools-4.60.2-cp313-cp313-win32.whl", hash = "sha256:19c6e0afd8b02008caa0aa08ab896dfce5d0bcb510c49b2c499541d5cb95a963", size = 2263443, upload-time = "2025-12-09T13:37:06.762Z" }, - { url = "https://files.pythonhosted.org/packages/b8/22/026c60376f165981f80a0e90bd98a79ae3334e9d89a3d046c4d2e265c724/fonttools-4.60.2-cp313-cp313-win_amd64.whl", hash = "sha256:6a500dc59e11b2338c2dba1f8cf11a4ae8be35ec24af8b2628b8759a61457b76", size = 2313800, upload-time = "2025-12-09T13:37:08.713Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ab/7cf1f5204e1366ddf9dc5cdc2789b571feb9eebcee0e3463c3f457df5f52/fonttools-4.60.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9387c532acbe323bbf2a920f132bce3c408a609d5f9dcfc6532fbc7e37f8ccbb", size = 2841690, upload-time = "2025-12-09T13:37:10.696Z" }, - { url = "https://files.pythonhosted.org/packages/00/3c/0bf83c6f863cc8b934952567fa2bf737cfcec8fc4ffb59b3f93820095f89/fonttools-4.60.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6f1c824185b5b8fb681297f315f26ae55abb0d560c2579242feea8236b1cfef", size = 2392191, upload-time = "2025-12-09T13:37:12.954Z" }, - { url = "https://files.pythonhosted.org/packages/00/f0/40090d148b8907fbea12e9bdf1ff149f30cdf1769e3b2c3e0dbf5106b88d/fonttools-4.60.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:55a3129d1e4030b1a30260f1b32fe76781b585fb2111d04a988e141c09eb6403", size = 4873503, upload-time = "2025-12-09T13:37:15.142Z" }, - { url = "https://files.pythonhosted.org/packages/dc/e0/d8b13f99e58b8c293781288ba62fe634f1f0697c9c4c0ae104d3215f3a10/fonttools-4.60.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b196e63753abc33b3b97a6fd6de4b7c4fef5552c0a5ba5e562be214d1e9668e0", size = 4968493, upload-time = "2025-12-09T13:37:18.272Z" }, - { url = "https://files.pythonhosted.org/packages/46/c5/960764d12c92bc225f02401d3067048cb7b282293d9e48e39fe2b0ec38a9/fonttools-4.60.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de76c8d740fb55745f3b154f0470c56db92ae3be27af8ad6c2e88f1458260c9a", size = 4920015, upload-time = "2025-12-09T13:37:20.334Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ab/839d8caf253d1eef3653ef4d34427d0326d17a53efaec9eb04056b670fff/fonttools-4.60.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ba6303225c95998c9fda2d410aa792c3d2c1390a09df58d194b03e17583fa25", size = 5031165, upload-time = "2025-12-09T13:37:23.57Z" }, - { url = "https://files.pythonhosted.org/packages/de/bf/3bc862796a6841cbe0725bb5512d272239b809dba631a4b0301df885e62d/fonttools-4.60.2-cp314-cp314-win32.whl", hash = "sha256:0a89728ce10d7c816fedaa5380c06d2793e7a8a634d7ce16810e536c22047384", size = 2267526, upload-time = "2025-12-09T13:37:25.821Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a1/c1909cacf00c76dc37b4743451561fbaaf7db4172c22a6d9394081d114c3/fonttools-4.60.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa8446e6ab8bd778b82cb1077058a2addba86f30de27ab9cc18ed32b34bc8667", size = 2319096, upload-time = "2025-12-09T13:37:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/29/b3/f66e71433f08e3a931b2b31a665aeed17fcc5e6911fc73529c70a232e421/fonttools-4.60.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4063bc81ac5a4137642865cb63dd270e37b3cd1f55a07c0d6e41d072699ccca2", size = 2925167, upload-time = "2025-12-09T13:37:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/2e/13/eeb491ff743594bbd0bee6e49422c03a59fe9c49002d3cc60eeb77414285/fonttools-4.60.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebfdb66fa69732ed604ab8e2a0431e6deff35e933a11d73418cbc7823d03b8e1", size = 2430923, upload-time = "2025-12-09T13:37:32.817Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/db609f785e460796e53c4dbc3874a5f4948477f27beceb5e2d24b2537666/fonttools-4.60.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50b10b3b1a72d1d54c61b0e59239e1a94c0958f4a06a1febf97ce75388dd91a4", size = 4877729, upload-time = "2025-12-09T13:37:35.858Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d6/85e4484dd4bfb03fee7bd370d65888cccbd3dee2681ee48c869dd5ccb23f/fonttools-4.60.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beae16891a13b4a2ddec9b39b4de76092a3025e4d1c82362e3042b62295d5e4d", size = 5096003, upload-time = "2025-12-09T13:37:37.862Z" }, - { url = "https://files.pythonhosted.org/packages/30/49/1a98e44b71030b83d2046f981373b80571868259d98e6dae7bc20099dac6/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522f017fdb3766fd5d2d321774ef351cc6ce88ad4e6ac9efe643e4a2b9d528db", size = 4974410, upload-time = "2025-12-09T13:37:40.166Z" }, - { url = "https://files.pythonhosted.org/packages/42/07/d6f775d950ee8a841012472c7303f8819423d8cc3b4530915de7265ebfa2/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82cceceaf9c09a965a75b84a4b240dd3768e596ffb65ef53852681606fe7c9ba", size = 5002036, upload-time = "2025-12-09T13:37:42.639Z" }, - { url = "https://files.pythonhosted.org/packages/73/f6/ba6458f83ce1a9f8c3b17bd8f7b8a2205a126aac1055796b7e7cfebbd38f/fonttools-4.60.2-cp314-cp314t-win32.whl", hash = "sha256:bbfbc918a75437fe7e6d64d1b1e1f713237df1cf00f3a36dedae910b2ba01cee", size = 2330985, upload-time = "2025-12-09T13:37:45.157Z" }, - { url = "https://files.pythonhosted.org/packages/91/24/fea0ba4d3a32d4ed1103a1098bfd99dc78b5fe3bb97202920744a37b73dc/fonttools-4.60.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0e5cd9b0830f6550d58c84f3ab151a9892b50c4f9d538c5603c0ce6fff2eb3f1", size = 2396226, upload-time = "2025-12-09T13:37:47.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/ae/a6d9446cb258d3fe87e311c2d7bacf8e8da3e5809fbdc3a8306db4f6b14e/fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008", size = 2857184, upload-time = "2025-12-09T13:37:49.96Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f3/1b41d0b6a8b908aa07f652111155dd653ebbf0b3385e66562556c5206685/fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc", size = 2401877, upload-time = "2025-12-09T13:37:52.307Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/048fd781680c38b05c5463657d0d95d5f2391a51972176e175c01de29d42/fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311", size = 4878073, upload-time = "2025-12-09T13:37:56.477Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/363364f052a893cebd3d449588b21244a9d873620fda03ad92702d2e1bc7/fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b", size = 4835385, upload-time = "2025-12-09T13:37:58.882Z" }, - { url = "https://files.pythonhosted.org/packages/1c/38/e392bb930b2436287e6021672345db26441bf1f85f1e98f8b9784334e41d/fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e", size = 4853084, upload-time = "2025-12-09T13:38:01.578Z" }, - { url = "https://files.pythonhosted.org/packages/65/60/0d77faeaecf7a3276a8a6dc49e2274357e6b3ed6a1774e2fdb2a7f142db0/fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5", size = 4971144, upload-time = "2025-12-09T13:38:03.748Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c7/6d3ac3afbcd598631bce24c3ecb919e7d0644a82fea8ddc4454312fc0be6/fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37", size = 1499411, upload-time = "2025-12-09T13:38:05.586Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/9dedf6420e23f9fa630bb97941839dddd2e1e57d1b2b85a902378dbe0bd2/fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501", size = 1547943, upload-time = "2025-12-09T13:38:07.604Z" }, - { url = "https://files.pythonhosted.org/packages/79/6c/10280af05b44fafd1dff69422805061fa1af29270bc52dce031ac69540bf/fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a", size = 1144610, upload-time = "2025-12-09T13:38:09.5Z" }, -] - [[package]] name = "fonttools" version = "4.61.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, @@ -1110,22 +792,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" }, - { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" }, - { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" }, - { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" }, - { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" }, - { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" }, - { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" }, - { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" }, - { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" }, - { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" }, - { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] @@ -1173,25 +839,12 @@ version = "3.1.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, ] -[[package]] -name = "griffe" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, -] - [[package]] name = "griffelib" version = "2.0.0" @@ -1244,8 +897,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.10'" }, - { name = "h11", marker = "python_full_version >= '3.10'" }, + { name = "certifi" }, + { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -1257,69 +910,31 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.10'" }, - { name = "certifi", marker = "python_full_version >= '3.10'" }, - { name = "httpcore", marker = "python_full_version >= '3.10'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[[package]] -name = "huggingface-hub" -version = "0.36.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "fsspec", marker = "python_full_version < '3.10'" }, - { name = "hf-xet", marker = "(python_full_version < '3.10' and platform_machine == 'aarch64') or (python_full_version < '3.10' and platform_machine == 'amd64') or (python_full_version < '3.10' and platform_machine == 'arm64') or (python_full_version < '3.10' and platform_machine == 'x86_64')" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pyyaml", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, - { name = "tqdm", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/54/096903f02ca14eb2670a4d11729da44a026c0bababec8c15f160441124c5/huggingface_hub-0.36.1.tar.gz", hash = "sha256:5a3b8bf87e182ad6f1692c196bb9ec9ade7755311d5d5e792dc45045f77283ad", size = 649681, upload-time = "2026-02-02T10:46:58.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/cb/8f5141b3c21d1ecdf87852506eb583fec497c7e9803a168fe4aec64252bb/huggingface_hub-0.36.1-py3-none-any.whl", hash = "sha256:c6fa8a8f7b8559bc624ebb7e218fb72171b30f6049ebe08f8bfc2a44b38ece50", size = 566283, upload-time = "2026-02-02T10:46:56.459Z" }, -] - [[package]] name = "huggingface-hub" version = "1.3.7" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "fsspec", marker = "python_full_version >= '3.10'" }, - { name = "hf-xet", marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'arm64') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, - { name = "httpx", marker = "python_full_version >= '3.10'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pyyaml", marker = "python_full_version >= '3.10'" }, - { name = "shellingham", marker = "python_full_version >= '3.10'" }, - { name = "tqdm", marker = "python_full_version >= '3.10'" }, - { name = "typer-slim", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/3f/352efd52136bfd8aa9280c6d4a445869226ae2ccd49ddad4f62e90cfd168/huggingface_hub-1.3.7.tar.gz", hash = "sha256:5f86cd48f27131cdbf2882699cbdf7a67dd4cbe89a81edfdc31211f42e4a5fd1", size = 627537, upload-time = "2026-02-02T10:40:10.61Z" } wheels = [ @@ -1333,8 +948,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "antlr4-python3-runtime" }, { name = "omegaconf" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } wheels = [ @@ -1350,62 +964,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, @@ -1432,49 +994,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] -[[package]] -name = "jsonschema" -version = "4.25.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "attrs", marker = "python_full_version < '3.10'" }, - { name = "jsonschema-specifications", marker = "python_full_version < '3.10'" }, - { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, -] - [[package]] name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, - { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1486,137 +1014,17 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] -[[package]] -name = "kiwisolver" -version = "1.4.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, - { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, - { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, - { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, - { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, - { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, - { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, - { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, - { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, - { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, - { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, - { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, - { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, - { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, - { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, - { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, - { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, - { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, - { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, - { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, - { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, - { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, - { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, - { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, - { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, - { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, - { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, - { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, - { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, - { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, - { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913, upload-time = "2024-09-04T09:05:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627, upload-time = "2024-09-04T09:05:05.119Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888, upload-time = "2024-09-04T09:05:06.191Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145, upload-time = "2024-09-04T09:05:07.919Z" }, - { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448, upload-time = "2024-09-04T09:05:10.01Z" }, - { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750, upload-time = "2024-09-04T09:05:11.598Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175, upload-time = "2024-09-04T09:05:13.22Z" }, - { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963, upload-time = "2024-09-04T09:05:15.925Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220, upload-time = "2024-09-04T09:05:17.434Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463, upload-time = "2024-09-04T09:05:18.997Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842, upload-time = "2024-09-04T09:05:21.299Z" }, - { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635, upload-time = "2024-09-04T09:05:23.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556, upload-time = "2024-09-04T09:05:25.907Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364, upload-time = "2024-09-04T09:05:27.184Z" }, - { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887, upload-time = "2024-09-04T09:05:28.372Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530, upload-time = "2024-09-04T09:05:30.225Z" }, - { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, - { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, - { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, - { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, - { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, - { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, - { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, - { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, - { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, - { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, - { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, - { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, - { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, - { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, - { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, - { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, - { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, - { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, -] - [[package]] name = "kiwisolver" version = "1.4.9" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, @@ -1721,41 +1129,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] -[[package]] -name = "markdown" -version = "3.9" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, -] - [[package]] name = "markdown" version = "3.10.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, @@ -1844,114 +1221,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, - { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, - { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, - { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, - { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, -] - -[[package]] -name = "matplotlib" -version = "3.9.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "cycler", marker = "python_full_version < '3.10'" }, - { name = "fonttools", version = "4.60.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "importlib-resources", marker = "python_full_version < '3.10'" }, - { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pyparsing", marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, - { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, - { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, - { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, - { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, - { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, - { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, - { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, - { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, - { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, - { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499, upload-time = "2024-12-13T05:55:22.142Z" }, - { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802, upload-time = "2024-12-13T05:55:25.947Z" }, - { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802, upload-time = "2024-12-13T05:55:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880, upload-time = "2024-12-13T05:55:30.965Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637, upload-time = "2024-12-13T05:55:33.701Z" }, - { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311, upload-time = "2024-12-13T05:55:36.737Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989, upload-time = "2024-12-13T05:55:39.024Z" }, - { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417, upload-time = "2024-12-13T05:55:42.412Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258, upload-time = "2024-12-13T05:55:47.259Z" }, - { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849, upload-time = "2024-12-13T05:55:49.763Z" }, - { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152, upload-time = "2024-12-13T05:55:51.997Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987, upload-time = "2024-12-13T05:55:55.941Z" }, - { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, - { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, - { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, - { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, - { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, ] [[package]] name = "matplotlib" version = "3.10.8" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler", marker = "python_full_version >= '3.10'" }, - { name = "fonttools", version = "4.61.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "kiwisolver", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pyparsing", marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ @@ -2025,19 +1312,15 @@ name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, - { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, @@ -2053,8 +1336,7 @@ name = "mkdocs-autorefs" version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] @@ -2068,10 +1350,8 @@ name = "mkdocs-get-deps" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "mergedeep" }, - { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "platformdirs", version = "4.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } @@ -2088,8 +1368,7 @@ dependencies = [ { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, - { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, @@ -2111,59 +1390,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] -[[package]] -name = "mkdocstrings" -version = "0.30.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markupsafe", marker = "python_full_version < '3.10'" }, - { name = "mkdocs", marker = "python_full_version < '3.10'" }, - { name = "mkdocs-autorefs", marker = "python_full_version < '3.10'" }, - { name = "pymdown-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, -] - -[package.optional-dependencies] -python = [ - { name = "mkdocstrings-python", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] - [[package]] name = "mkdocstrings" version = "1.0.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "jinja2", marker = "python_full_version >= '3.10'" }, - { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "markupsafe", marker = "python_full_version >= '3.10'" }, - { name = "mkdocs", marker = "python_full_version >= '3.10'" }, - { name = "mkdocs-autorefs", marker = "python_full_version >= '3.10'" }, - { name = "pymdown-extensions", marker = "python_full_version >= '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } wheels = [ @@ -2172,52 +1409,18 @@ wheels = [ [package.optional-dependencies] python = [ - { name = "mkdocstrings-python", version = "2.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, -] - -[[package]] -name = "mkdocstrings-python" -version = "1.18.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "griffe", marker = "python_full_version < '3.10'" }, - { name = "mkdocs-autorefs", marker = "python_full_version < '3.10'" }, - { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/8f/ce008599d9adebf33ed144e7736914385e8537f5fc686fdb7cceb8c22431/mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d", size = 138215, upload-time = "2025-08-28T16:11:18.176Z" }, + { name = "mkdocstrings-python" }, ] [[package]] name = "mkdocstrings-python" version = "2.0.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "griffelib", marker = "python_full_version >= '3.10'" }, - { name = "mkdocs-autorefs", marker = "python_full_version >= '3.10'" }, - { name = "mkdocstrings", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } wheels = [ @@ -2368,24 +1571,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/74525ebe3eb5fddcd6735fc03cbea3feeed4122b53bc798ac32d297ac9ae/multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", size = 77107, upload-time = "2026-01-26T02:46:12.608Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9a/ce8744e777a74b3050b1bf56be3eed1053b3457302ea055f1ea437200a23/multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", size = 44943, upload-time = "2026-01-26T02:46:14.016Z" }, - { url = "https://files.pythonhosted.org/packages/83/9c/1d2a283d9c6f31e260cb6c2fccadc3edcf6c4c14ee0929cd2af4d2606dd7/multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", size = 44603, upload-time = "2026-01-26T02:46:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/87/9d/3b186201671583d8e8d6d79c07481a5aafd0ba7575e3d8566baec80c1e82/multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", size = 240573, upload-time = "2026-01-26T02:46:16.783Z" }, - { url = "https://files.pythonhosted.org/packages/42/7d/a52f5d4d0754311d1ac78478e34dff88de71259a8585e05ee14e5f877caf/multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", size = 240106, upload-time = "2026-01-26T02:46:18.432Z" }, - { url = "https://files.pythonhosted.org/packages/84/9f/d80118e6c30ff55b7d171bdc5520aad4b9626e657520b8d7c8ca8c2fad12/multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", size = 219418, upload-time = "2026-01-26T02:46:20.526Z" }, - { url = "https://files.pythonhosted.org/packages/c7/bd/896e60b3457f194de77c7de64f9acce9f75da0518a5230ce1df534f6747b/multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", size = 252124, upload-time = "2026-01-26T02:46:22.157Z" }, - { url = "https://files.pythonhosted.org/packages/f4/de/ba6b30447c36a37078d0ba604aa12c1a52887af0c355236ca6e0a9d5286f/multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", size = 249402, upload-time = "2026-01-26T02:46:23.718Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b2/50a383c96230e432895a2fd3bcfe1b65785899598259d871d5de6b93180c/multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", size = 240346, upload-time = "2026-01-26T02:46:25.393Z" }, - { url = "https://files.pythonhosted.org/packages/89/37/16d391fd8da544b1489306e38a46785fa41dd0f0ef766837ed7d4676dde0/multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", size = 237010, upload-time = "2026-01-26T02:46:27.408Z" }, - { url = "https://files.pythonhosted.org/packages/b0/24/3152ee026eda86d5d3e3685182911e6951af7a016579da931080ce6ac9ad/multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", size = 232018, upload-time = "2026-01-26T02:46:29.941Z" }, - { url = "https://files.pythonhosted.org/packages/9c/1f/48d3c27a72be7fd23a55d8847193c459959bf35a5bb5844530dab00b739b/multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", size = 241498, upload-time = "2026-01-26T02:46:32.052Z" }, - { url = "https://files.pythonhosted.org/packages/1a/45/413643ae2952d0decdf6c1250f86d08a43e143271441e81027e38d598bd7/multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", size = 247957, upload-time = "2026-01-26T02:46:33.666Z" }, - { url = "https://files.pythonhosted.org/packages/50/f8/f1d0ac23df15e0470776388bdb261506f63af1f81d28bacb5e262d6e12b6/multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", size = 241651, upload-time = "2026-01-26T02:46:35.7Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c9/1a2a18f383cf129add66b6c36b75c3911a7ba95cf26cb141482de085cc12/multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", size = 236371, upload-time = "2026-01-26T02:46:37.37Z" }, - { url = "https://files.pythonhosted.org/packages/bb/aa/77d87e3fca31325b87e0eb72d5fe9a7472dcb51391a42df7ac1f3842f6c0/multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", size = 41426, upload-time = "2026-01-26T02:46:39.026Z" }, - { url = "https://files.pythonhosted.org/packages/e3/b3/e8863e6a2da15a9d7e98976ff402e871b7352c76566df6c18d0378e0d9cf/multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", size = 46180, upload-time = "2026-01-26T02:46:40.422Z" }, - { url = "https://files.pythonhosted.org/packages/93/d3/dd4fa951ad5b5fa216bf30054d705683d13405eea7459833d78f31b74c9c/multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", size = 43231, upload-time = "2026-01-26T02:46:41.945Z" }, { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] @@ -2400,8 +1585,6 @@ sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def032 wheels = [ { url = "https://files.pythonhosted.org/packages/ef/76/6e712a2623d146d314f17598df5de7224c85c0060ef63fd95cc15a25b3fa/multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee", size = 134980, upload-time = "2024-01-28T18:52:15.731Z" }, { url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec", size = 134982, upload-time = "2024-01-28T18:52:17.783Z" }, - { url = "https://files.pythonhosted.org/packages/d8/94/8638a89f93c80df329116e6781a060506c7e91e1f4370dc831e9d17a041d/multiprocess-0.70.16-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41", size = 133497, upload-time = "2024-01-28T18:52:22.644Z" }, - { url = "https://files.pythonhosted.org/packages/89/21/222066f6bb8d8af287923ae3bd26cf4699a9ce020228ac273caca1de8250/multiprocess-0.70.16-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a", size = 133498, upload-time = "2024-01-28T18:52:24.576Z" }, { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824, upload-time = "2024-01-28T18:52:26.062Z" }, { url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a", size = 143519, upload-time = "2024-01-28T18:52:28.115Z" }, { url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e", size = 146741, upload-time = "2024-01-28T18:52:29.395Z" }, @@ -2418,25 +1601,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, ] -[[package]] -name = "networkx" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928, upload-time = "2023-10-28T08:41:39.364Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772, upload-time = "2023-10-28T08:41:36.945Z" }, -] - [[package]] name = "networkx" version = "3.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", + "python_full_version < '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } wheels = [ @@ -2448,86 +1618,20 @@ name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] -[[package]] -name = "numpy" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, - { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, - { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, - { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, - { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, - { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, - { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, - { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, - { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, - { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, - { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, - { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, - { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, - { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, - { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, - { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, - { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, - { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, - { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, - { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, - { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, - { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, - { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, - { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, -] - [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", + "python_full_version < '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ @@ -2592,18 +1696,8 @@ name = "numpy" version = "2.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ @@ -2717,7 +1811,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.10' and platform_machine == 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cublas-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -2728,7 +1822,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.10' and platform_machine == 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -2755,9 +1849,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.10' and platform_machine == 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "nvidia-cusparse-cu12", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.10' and platform_machine == 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.10' and platform_machine == 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -2768,7 +1862,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.10' and platform_machine == 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -2782,27 +1876,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, ] -[[package]] -name = "nvidia-nccl-cu12" -version = "2.27.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" }, -] - [[package]] name = "nvidia-nccl-cu12" version = "2.27.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] wheels = [ { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, ] @@ -2844,38 +1921,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, ] -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, @@ -2895,8 +1944,7 @@ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dateutil" }, { name = "pytz" }, @@ -2951,13 +1999,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, - { url = "https://files.pythonhosted.org/packages/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" }, - { url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" }, - { url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" }, - { url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" }, - { url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" }, ] [[package]] @@ -2969,142 +2010,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] -[[package]] -name = "pillow" -version = "11.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, - { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, - { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, - { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, - { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, - { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, - { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, - { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, -] - [[package]] name = "pillow" version = "12.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, @@ -3199,38 +2108,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, ] -[[package]] -name = "platformdirs" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, -] - [[package]] name = "platformdirs" version = "4.9.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, @@ -3242,8 +2123,7 @@ version = "6.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/8a10a9b9f5192cb6fdef62f1d77fa7d834190b2c50c0cd256bd62879212b/plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393", size = 7015695, upload-time = "2026-01-14T21:26:51.222Z" } wheels = [ @@ -3384,21 +2264,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" }, - { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" }, - { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" }, - { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" }, - { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" }, - { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" }, - { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" }, - { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" }, - { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" }, - { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" }, - { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] @@ -3414,8 +2279,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/08/60/84d5f6dcda9165e4d6a56ac8433c9f40a8906bf2966150b8a0cfde097d78/protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c", size = 425892, upload-time = "2026-01-29T21:51:30.382Z" }, - { url = "https://files.pythonhosted.org/packages/68/19/33d7dc2dc84439587fa1e21e1c0026c01ad2af0a62f58fd54002a7546307/protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a", size = 437137, upload-time = "2026-01-29T21:51:31.456Z" }, { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] @@ -3447,79 +2310,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] -[[package]] -name = "pyarrow" -version = "21.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload-time = "2025-07-18T00:54:34.755Z" }, - { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload-time = "2025-07-18T00:54:38.329Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload-time = "2025-07-18T00:54:42.172Z" }, - { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload-time = "2025-07-18T00:54:47.132Z" }, - { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload-time = "2025-07-18T00:54:51.686Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload-time = "2025-07-18T00:54:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload-time = "2025-07-18T00:55:00.482Z" }, - { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload-time = "2025-07-18T00:55:03.812Z" }, - { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload-time = "2025-07-18T00:55:07.495Z" }, - { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload-time = "2025-07-18T00:55:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810, upload-time = "2025-07-18T00:55:16.301Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538, upload-time = "2025-07-18T00:55:23.82Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056, upload-time = "2025-07-18T00:55:28.231Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568, upload-time = "2025-07-18T00:55:32.122Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, - { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, - { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, - { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, - { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, - { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" }, - { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload-time = "2025-07-18T00:56:15.569Z" }, - { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload-time = "2025-07-18T00:56:19.531Z" }, - { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload-time = "2025-07-18T00:56:23.347Z" }, - { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload-time = "2025-07-18T00:56:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload-time = "2025-07-18T00:56:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload-time = "2025-07-18T00:56:33.935Z" }, - { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload-time = "2025-07-18T00:56:37.528Z" }, - { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload-time = "2025-07-18T00:56:41.483Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload-time = "2025-07-18T00:56:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload-time = "2025-07-18T00:56:52.568Z" }, - { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cc/ce4939f4b316457a083dc5718b3982801e8c33f921b3c98e7a93b7c7491f/pyarrow-21.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:a7f6524e3747e35f80744537c78e7302cd41deee8baa668d56d55f77d9c464b3", size = 31211248, upload-time = "2025-07-18T00:56:59.7Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c2/7a860931420d73985e2f340f06516b21740c15b28d24a0e99a900bb27d2b/pyarrow-21.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:203003786c9fd253ebcafa44b03c06983c9c8d06c3145e37f1b76a1f317aeae1", size = 32676896, upload-time = "2025-07-18T00:57:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/68/a8/197f989b9a75e59b4ca0db6a13c56f19a0ad8a298c68da9cc28145e0bb97/pyarrow-21.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b4d97e297741796fead24867a8dabf86c87e4584ccc03167e4a811f50fdf74d", size = 41067862, upload-time = "2025-07-18T00:57:07.587Z" }, - { url = "https://files.pythonhosted.org/packages/fa/82/6ecfa89487b35aa21accb014b64e0a6b814cc860d5e3170287bf5135c7d8/pyarrow-21.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:898afce396b80fdda05e3086b4256f8677c671f7b1d27a6976fa011d3fd0a86e", size = 42747508, upload-time = "2025-07-18T00:57:13.917Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b7/ba252f399bbf3addc731e8643c05532cf32e74cebb5e32f8f7409bc243cf/pyarrow-21.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:067c66ca29aaedae08218569a114e413b26e742171f526e828e1064fcdec13f4", size = 43345293, upload-time = "2025-07-18T00:57:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0a/a20819795bd702b9486f536a8eeb70a6aa64046fce32071c19ec8230dbaa/pyarrow-21.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0c4e75d13eb76295a49e0ea056eb18dbd87d81450bfeb8afa19a7e5a75ae2ad7", size = 45060670, upload-time = "2025-07-18T00:57:24.477Z" }, - { url = "https://files.pythonhosted.org/packages/10/15/6b30e77872012bbfe8265d42a01d5b3c17ef0ac0f2fae531ad91b6a6c02e/pyarrow-21.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f", size = 26227521, upload-time = "2025-07-18T00:57:29.119Z" }, -] - [[package]] name = "pyarrow" version = "23.0.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ae/2f/23e042a5aa99bcb15e794e14030e8d065e00827e846e53a66faec73c7cd6/pyarrow-23.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cbdc2bf5947aa4d462adcf8453cf04aee2f7932653cb67a27acd96e5e8528a67", size = 34281861, upload-time = "2026-01-18T16:13:34.332Z" }, @@ -3579,8 +2373,7 @@ version = "0.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240, upload-time = "2024-05-10T15:36:21.153Z" } @@ -3602,8 +2395,7 @@ name = "pymdown-extensions" version = "10.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } @@ -3620,55 +2412,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pluggy", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pluggy", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -3681,8 +2436,7 @@ version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ @@ -3772,15 +2526,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, - { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, - { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, - { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, - { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, - { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, ] [[package]] @@ -3795,47 +2540,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] -[[package]] -name = "referencing" -version = "0.36.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "attrs", marker = "python_full_version < '3.10'" }, - { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, -] - [[package]] name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -3961,23 +2673,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/a2/e7/0e1913dc52eee9c5cf8417c9813c4c55972a3f37d27cfa2e623b79b63dbc/regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5", size = 488185, upload-time = "2026-01-14T23:17:25.2Z" }, - { url = "https://files.pythonhosted.org/packages/78/df/c52c1ff4221529faad0953e197982fe9508c6dbb42327e31bf98ea07472a/regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815", size = 290628, upload-time = "2026-01-14T23:17:27.125Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/a2fef3717deaff647d7de2bccf899a576c7eaf042b6b271fc4474515fe97/regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b", size = 288509, upload-time = "2026-01-14T23:17:29.017Z" }, - { url = "https://files.pythonhosted.org/packages/70/89/faf5ee5c69168753c845a3d58b4683f61c899d162bfe1264fca88d5b3924/regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6", size = 781088, upload-time = "2026-01-14T23:17:30.961Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2c/707e5c380ad547c93686e21144e7e24dc2064dd84ec5b751b6dbdfc9be2b/regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b", size = 850516, upload-time = "2026-01-14T23:17:32.946Z" }, - { url = "https://files.pythonhosted.org/packages/5d/3b/baa816cdcad1c0f8195f9f40ab2b2a2246c8a2989dcd90641c0c6559e3fd/regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df", size = 898124, upload-time = "2026-01-14T23:17:36.019Z" }, - { url = "https://files.pythonhosted.org/packages/e7/74/1eb46bde30899825ed9fdf645eba16b7b97c49d12d300f5177989b9a09a4/regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e", size = 791290, upload-time = "2026-01-14T23:17:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5d/b72e176fb21e2ec248baed01151a342d1f44dd43c2b6bb6a41ad183b274e/regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c", size = 781996, upload-time = "2026-01-14T23:17:40.109Z" }, - { url = "https://files.pythonhosted.org/packages/61/0e/d3b3710eaafd994a4a71205d114abc38cda8691692a2ce2313abe68e7eb7/regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839", size = 767578, upload-time = "2026-01-14T23:17:42.134Z" }, - { url = "https://files.pythonhosted.org/packages/09/51/c6a6311833e040f95d229a34d82ac1cec2af8a5c00d58b244f2fceecef87/regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c", size = 774354, upload-time = "2026-01-14T23:17:44.392Z" }, - { url = "https://files.pythonhosted.org/packages/cc/97/c522d1f19fb2c549aaf680b115c110cd124c02062bc8c95f33db8583b4bb/regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077", size = 845297, upload-time = "2026-01-14T23:17:47.145Z" }, - { url = "https://files.pythonhosted.org/packages/99/a0/99468c386ab68a5e24c946c5c353c29c33a95523e275c17839f2446db15d/regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4", size = 755132, upload-time = "2026-01-14T23:17:49.796Z" }, - { url = "https://files.pythonhosted.org/packages/70/33/d5748c7b6c9d3621f12570583561ba529e2d1b12e4f70b8f17979b133e65/regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a", size = 835662, upload-time = "2026-01-14T23:17:52.559Z" }, - { url = "https://files.pythonhosted.org/packages/ad/15/1986972c276672505437f1ba3c9706c2d91f321cfb9b2f4d06e8bff1b999/regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3", size = 779513, upload-time = "2026-01-14T23:17:54.711Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f9/124f6a5cb3969d8e30471ed4f46cfc17c47aef1a9863ee8b4ba1d98b1bc4/regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a", size = 265923, upload-time = "2026-01-14T23:17:56.69Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c2/bb8fad7d27f1d71fc9772befd544bccd22eddc62a6735f57b003b4aff005/regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc", size = 277900, upload-time = "2026-01-14T23:17:58.72Z" }, - { url = "https://files.pythonhosted.org/packages/f7/fa/4e033327c1d8350bc812cac906d873984d3d4b39529252f392a47ccc356d/regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5", size = 270413, upload-time = "2026-01-14T23:18:00.764Z" }, ] [[package]] @@ -3995,191 +2690,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] -[[package]] -name = "rpds-py" -version = "0.27.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, - { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, - { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, - { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, - { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, - { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, - { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, - { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, - { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, - { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, - { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, - { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, - { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, - { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, - { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, - { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, - { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, - { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, - { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, - { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, - { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, - { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, - { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, - { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, - { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, - { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, - { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, - { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, - { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, - { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, - { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, - { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, - { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, - { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, - { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, - { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, - { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, - { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, - { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, - { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, - { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, - { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, - { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, - { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, - { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" }, - { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" }, - { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" }, - { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" }, - { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" }, - { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" }, - { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" }, - { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" }, - { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" }, - { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, - { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, - { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, - { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, - { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, - { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, - { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, - { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, - { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, - { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, - { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, - { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" }, - { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" }, - { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" }, - { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" }, - { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" }, - { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" }, - { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" }, -] - [[package]] name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, @@ -4347,56 +2861,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, - { url = "https://files.pythonhosted.org/packages/94/60/13ccb63ea85bfe2e4fe6af602cf1272155f048906556d5ec8509da9dba42/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b95a3fa7b3abb9b5b0e07668e808364d0d40f6bbbf9ae0faa8b5b210c97b140", size = 492627, upload-time = "2025-11-19T15:18:14.661Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2b/e2fde0d6334439908b0b0c4cba18b8ad76ea6a03b569d4a3388f423b4046/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cfdead2f57330d76aa7234051dadfa7d4eedc0e5a27fd08e6f96714a92b00f09", size = 503861, upload-time = "2025-11-19T15:18:19.418Z" }, - { url = "https://files.pythonhosted.org/packages/f0/71/566e3dd559a9cef1b4775c239daae09e6b6a32ca8b45eb1db9a4dfa1ba81/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc92bc2db7b45bda4510e4f51c59b00fe80b2d6be88928346e4294ce1c2abe7c", size = 623577, upload-time = "2025-11-19T15:18:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/82/fc/3035c5c30c8a5a82c31c6b2ad6f8bcd45ea2ddd9a8088840406bcf997413/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6999421eb8ba9df4450a16d9184fcb7bef26240b9f98e95401f17af6c2210b71", size = 532524, upload-time = "2025-11-19T15:18:29.334Z" }, -] - -[[package]] -name = "scikit-learn" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "joblib", marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702, upload-time = "2025-01-10T08:05:56.515Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765, upload-time = "2025-01-10T08:06:00.272Z" }, - { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991, upload-time = "2025-01-10T08:06:04.813Z" }, - { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182, upload-time = "2025-01-10T08:06:08.42Z" }, - { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517, upload-time = "2025-01-10T08:06:12.783Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620, upload-time = "2025-01-10T08:06:16.675Z" }, - { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234, upload-time = "2025-01-10T08:06:21.83Z" }, - { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155, upload-time = "2025-01-10T08:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069, upload-time = "2025-01-10T08:06:32.515Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809, upload-time = "2025-01-10T08:06:35.514Z" }, - { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516, upload-time = "2025-01-10T08:06:40.009Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837, upload-time = "2025-01-10T08:06:43.305Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728, upload-time = "2025-01-10T08:06:47.618Z" }, - { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700, upload-time = "2025-01-10T08:06:50.888Z" }, - { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613, upload-time = "2025-01-10T08:06:54.115Z" }, - { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001, upload-time = "2025-01-10T08:06:58.613Z" }, - { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360, upload-time = "2025-01-10T08:07:01.556Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004, upload-time = "2025-01-10T08:07:06.931Z" }, - { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776, upload-time = "2025-01-10T08:07:11.715Z" }, - { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865, upload-time = "2025-01-10T08:07:16.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804, upload-time = "2025-01-10T08:07:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530, upload-time = "2025-01-10T08:07:23.675Z" }, - { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852, upload-time = "2025-01-10T08:07:26.817Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256, upload-time = "2025-01-10T08:07:31.084Z" }, - { url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", size = 12101868, upload-time = "2025-01-10T08:07:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", size = 11144062, upload-time = "2025-01-10T08:07:37.67Z" }, - { url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", size = 12693173, upload-time = "2025-01-10T08:07:42.713Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", size = 13518605, upload-time = "2025-01-10T08:07:46.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", size = 11155078, upload-time = "2025-01-10T08:07:51.376Z" }, ] [[package]] @@ -4404,14 +2868,13 @@ name = "scikit-learn" version = "1.7.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", + "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib", marker = "python_full_version == '3.10.*'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "threadpoolctl", marker = "python_full_version == '3.10.*'" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -4452,18 +2915,8 @@ name = "scikit-learn" version = "1.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", ] dependencies = [ { name = "joblib", marker = "python_full_version >= '3.11'" }, @@ -4511,54 +2964,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, ] -[[package]] -name = "scipy" -version = "1.13.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, - { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, - { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, - { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, - { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, - { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, - { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, - { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, - { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, - { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, - { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, - { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, - { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, - { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, -] - [[package]] name = "scipy" version = "1.15.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", + "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -4614,18 +3028,8 @@ name = "scipy" version = "1.17.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", ] dependencies = [ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -4699,10 +3103,8 @@ name = "seaborn" version = "0.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas" }, ] @@ -4711,60 +3113,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, ] -[[package]] -name = "sentence-transformers" -version = "5.1.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "huggingface-hub", version = "0.36.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "tqdm", marker = "python_full_version < '3.10'" }, - { name = "transformers", version = "4.57.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/96/f3f3409179d14dbfdbea8622e2e9eaa3c8836ddcaecd2cd5ff0a11731d20/sentence_transformers-5.1.2.tar.gz", hash = "sha256:0f6c8bd916a78dc65b366feb8d22fd885efdb37432e7630020d113233af2b856", size = 375185, upload-time = "2025-10-22T12:47:55.019Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/a6/a607a737dc1a00b7afe267b9bfde101b8cee2529e197e57471d23137d4e5/sentence_transformers-5.1.2-py3-none-any.whl", hash = "sha256:724ce0ea62200f413f1a5059712aff66495bc4e815a1493f7f9bca242414c333", size = 488009, upload-time = "2025-10-22T12:47:53.433Z" }, -] - [[package]] name = "sentence-transformers" version = "5.2.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "huggingface-hub", version = "1.3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "tqdm", marker = "python_full_version >= '3.10'" }, - { name = "transformers", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/30/21664028fc0776eb1ca024879480bbbab36f02923a8ff9e4cae5a150fa35/sentence_transformers-5.2.3.tar.gz", hash = "sha256:3cd3044e1f3fe859b6a1b66336aac502eaae5d3dd7d5c8fc237f37fbf58137c7", size = 381623, upload-time = "2026-02-17T14:05:20.238Z" } wheels = [ @@ -4807,78 +3171,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] -[[package]] -name = "streamlit" -version = "1.50.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "altair", version = "5.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "blinker", marker = "python_full_version < '3.10'" }, - { name = "cachetools", marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "gitpython", marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pandas", marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "protobuf", marker = "python_full_version < '3.10'" }, - { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pydeck", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, - { name = "tenacity", marker = "python_full_version < '3.10'" }, - { name = "toml", marker = "python_full_version < '3.10'" }, - { name = "tornado", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, - { name = "watchdog", marker = "python_full_version < '3.10' and sys_platform != 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/f6/f7d3a0146577c1918439d3163707040f7111a7d2e7e2c73fa7adeb169c06/streamlit-1.50.0.tar.gz", hash = "sha256:87221d568aac585274a05ef18a378b03df332b93e08103fffcf3cd84d852af46", size = 9664808, upload-time = "2025-09-23T19:24:00.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/38/991bbf9fa3ed3d9c8e69265fc449bdaade8131c7f0f750dbd388c3c477dc/streamlit-1.50.0-py3-none-any.whl", hash = "sha256:9403b8f94c0a89f80cf679c2fcc803d9a6951e0fba542e7611995de3f67b4bb3", size = 10068477, upload-time = "2025-09-23T19:23:57.245Z" }, -] - [[package]] name = "streamlit" version = "1.53.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "altair", version = "6.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "blinker", marker = "python_full_version >= '3.10'" }, - { name = "cachetools", marker = "python_full_version >= '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "gitpython", marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "altair" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "click" }, + { name = "gitpython" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pandas", marker = "python_full_version >= '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "protobuf", marker = "python_full_version >= '3.10'" }, - { name = "pyarrow", version = "23.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pydeck", marker = "python_full_version >= '3.10'" }, - { name = "requests", marker = "python_full_version >= '3.10'" }, - { name = "tenacity", marker = "python_full_version >= '3.10'" }, - { name = "toml", marker = "python_full_version >= '3.10'" }, - { name = "tornado", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, - { name = "watchdog", marker = "python_full_version >= '3.10' and sys_platform != 'darwin'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "pydeck" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "tornado" }, + { name = "typing-extensions" }, + { name = "watchdog", marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/31/cc/347730d06e3950e426bf2ee06eaf9b281e18387bffa82bd69aa0f281eee3/streamlit-1.53.1.tar.gz", hash = "sha256:ae656af3b68b4bb2d669fa977606096f2021bcbaa14a454a290f8e0a37bab277", size = 8650843, upload-time = "2026-01-22T21:39:04.087Z" } wheels = [ @@ -4920,8 +3236,7 @@ name = "tokenizers" version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "huggingface-hub", version = "0.36.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "huggingface-hub", version = "1.3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "huggingface-hub" }, ] sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ @@ -4944,10 +3259,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, - { url = "https://files.pythonhosted.org/packages/27/46/8d7db1dff181be50b207ab0a7483a22d5c3a4f903a9afc7cf7e465ad8109/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012", size = 3287784, upload-time = "2026-01-05T10:40:37.108Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6e/3bc33cae8bf114afa5a98e35eb065c72b7c37d01d370906a893f33881767/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee", size = 3164301, upload-time = "2026-01-05T10:40:42.367Z" }, - { url = "https://files.pythonhosted.org/packages/91/fc/6aa749d7d443aab4daa6f8bc00338389149fd2534e25b772285c3301993e/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37", size = 3717771, upload-time = "2026-01-05T10:40:49.076Z" }, - { url = "https://files.pythonhosted.org/packages/fc/60/5b440d251863bd33f9b0a416c695b0309487b83abf6f2dafe9163a3aeac2/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113", size = 3377740, upload-time = "2026-01-05T10:40:54.859Z" }, ] [[package]] @@ -5013,109 +3324,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] -[[package]] -name = "torch" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "fsspec", marker = "python_full_version < '3.10'" }, - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "nvidia-cublas-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", version = "2.27.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "sympy", marker = "python_full_version < '3.10'" }, - { name = "triton", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/28/110f7274254f1b8476c561dada127173f994afa2b1ffc044efb773c15650/torch-2.8.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0be92c08b44009d4131d1ff7a8060d10bafdb7ddcb7359ef8d8c5169007ea905", size = 102052793, upload-time = "2025-08-06T14:53:15.852Z" }, - { url = "https://files.pythonhosted.org/packages/70/1c/58da560016f81c339ae14ab16c98153d51c941544ae568da3cb5b1ceb572/torch-2.8.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:89aa9ee820bb39d4d72b794345cccef106b574508dd17dbec457949678c76011", size = 888025420, upload-time = "2025-08-06T14:54:18.014Z" }, - { url = "https://files.pythonhosted.org/packages/70/87/f69752d0dd4ba8218c390f0438130c166fa264a33b7025adb5014b92192c/torch-2.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8e5bf982e87e2b59d932769938b698858c64cc53753894be25629bdf5cf2f46", size = 241363614, upload-time = "2025-08-06T14:53:31.496Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d6/e6d4c57e61c2b2175d3aafbfb779926a2cfd7c32eeda7c543925dceec923/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760", size = 73611154, upload-time = "2025-08-06T14:53:10.919Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c4/3e7a3887eba14e815e614db70b3b529112d1513d9dae6f4d43e373360b7f/torch-2.8.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:220a06fd7af8b653c35d359dfe1aaf32f65aa85befa342629f716acb134b9710", size = 102073391, upload-time = "2025-08-06T14:53:20.937Z" }, - { url = "https://files.pythonhosted.org/packages/5a/63/4fdc45a0304536e75a5e1b1bbfb1b56dd0e2743c48ee83ca729f7ce44162/torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b", size = 888063640, upload-time = "2025-08-06T14:55:05.325Z" }, - { url = "https://files.pythonhosted.org/packages/84/57/2f64161769610cf6b1c5ed782bd8a780e18a3c9d48931319f2887fa9d0b1/torch-2.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c7ef765e27551b2fbfc0f41bcf270e1292d9bf79f8e0724848b1682be6e80aa", size = 241366752, upload-time = "2025-08-06T14:53:38.692Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5e/05a5c46085d9b97e928f3f037081d3d2b87fb4b4195030fc099aaec5effc/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916", size = 73621174, upload-time = "2025-08-06T14:53:25.44Z" }, - { url = "https://files.pythonhosted.org/packages/49/0c/2fd4df0d83a495bb5e54dca4474c4ec5f9c62db185421563deeb5dabf609/torch-2.8.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e2fab4153768d433f8ed9279c8133a114a034a61e77a3a104dcdf54388838705", size = 101906089, upload-time = "2025-08-06T14:53:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/99/a8/6acf48d48838fb8fe480597d98a0668c2beb02ee4755cc136de92a0a956f/torch-2.8.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2aca0939fb7e4d842561febbd4ffda67a8e958ff725c1c27e244e85e982173c", size = 887913624, upload-time = "2025-08-06T14:56:44.33Z" }, - { url = "https://files.pythonhosted.org/packages/af/8a/5c87f08e3abd825c7dfecef5a0f1d9aa5df5dd0e3fd1fa2f490a8e512402/torch-2.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:2f4ac52f0130275d7517b03a33d2493bab3693c83dcfadf4f81688ea82147d2e", size = 241326087, upload-time = "2025-08-06T14:53:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/be/66/5c9a321b325aaecb92d4d1855421e3a055abd77903b7dab6575ca07796db/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0", size = 73630478, upload-time = "2025-08-06T14:53:57.144Z" }, - { url = "https://files.pythonhosted.org/packages/10/4e/469ced5a0603245d6a19a556e9053300033f9c5baccf43a3d25ba73e189e/torch-2.8.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2b2f96814e0345f5a5aed9bf9734efa913678ed19caf6dc2cddb7930672d6128", size = 101936856, upload-time = "2025-08-06T14:54:01.526Z" }, - { url = "https://files.pythonhosted.org/packages/16/82/3948e54c01b2109238357c6f86242e6ecbf0c63a1af46906772902f82057/torch-2.8.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:65616ca8ec6f43245e1f5f296603e33923f4c30f93d65e103d9e50c25b35150b", size = 887922844, upload-time = "2025-08-06T14:55:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/e3/54/941ea0a860f2717d86a811adf0c2cd01b3983bdd460d0803053c4e0b8649/torch-2.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:659df54119ae03e83a800addc125856effda88b016dfc54d9f65215c3975be16", size = 241330968, upload-time = "2025-08-06T14:54:45.293Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/8b7b13bba430f5e21d77708b616f767683629fc4f8037564a177d20f90ed/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:1a62a1ec4b0498930e2543535cf70b1bef8c777713de7ceb84cd79115f553767", size = 73915128, upload-time = "2025-08-06T14:54:34.769Z" }, - { url = "https://files.pythonhosted.org/packages/15/0e/8a800e093b7f7430dbaefa80075aee9158ec22e4c4fc3c1a66e4fb96cb4f/torch-2.8.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:83c13411a26fac3d101fe8035a6b0476ae606deb8688e904e796a3534c197def", size = 102020139, upload-time = "2025-08-06T14:54:39.047Z" }, - { url = "https://files.pythonhosted.org/packages/4a/15/5e488ca0bc6162c86a33b58642bc577c84ded17c7b72d97e49b5833e2d73/torch-2.8.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8f0a9d617a66509ded240add3754e462430a6c1fc5589f86c17b433dd808f97a", size = 887990692, upload-time = "2025-08-06T14:56:18.286Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a8/6a04e4b54472fc5dba7ca2341ab219e529f3c07b6941059fbf18dccac31f/torch-2.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a7242b86f42be98ac674b88a4988643b9bc6145437ec8f048fea23f72feb5eca", size = 241603453, upload-time = "2025-08-06T14:55:22.945Z" }, - { url = "https://files.pythonhosted.org/packages/04/6e/650bb7f28f771af0cb791b02348db8b7f5f64f40f6829ee82aa6ce99aabe/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:7b677e17f5a3e69fdef7eb3b9da72622f8d322692930297e4ccb52fefc6c8211", size = 73632395, upload-time = "2025-08-06T14:55:28.645Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b0/a321f27270049baa12f5c3fb0d6ceea005634787e3af9a8d75dce8306b0a/torch-2.8.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:da6afa31c13b669d4ba49d8a2169f0db2c3ec6bec4af898aa714f401d4c38904", size = 102059214, upload-time = "2025-08-06T14:55:33.433Z" }, - { url = "https://files.pythonhosted.org/packages/fd/dd/1630cb51b10d3d2e97db95e5a84c32def81fc26b005bce6fc880b0e6db81/torch-2.8.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:06fcee8000e5c62a9f3e52a688b9c5abb7c6228d0e56e3452983416025c41381", size = 888024302, upload-time = "2025-08-06T14:57:28.23Z" }, - { url = "https://files.pythonhosted.org/packages/b9/dc/1f1f621afe15e3c496e1e8f94f8903f75f87e7d642d5a985e92210cc208d/torch-2.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5128fe752a355d9308e56af1ad28b15266fe2da5948660fad44de9e3a9e36e8c", size = 241249338, upload-time = "2025-08-06T14:57:05.669Z" }, - { url = "https://files.pythonhosted.org/packages/ae/95/ae26263aceb3d57b821179f827d0e321373ed49423e603dd5906ab14a730/torch-2.8.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:e9f071f5b52a9f6970dc8a919694b27a91ae9dc08898b2b988abbef5eddfd1ae", size = 73610795, upload-time = "2025-08-06T14:57:11.513Z" }, -] - [[package]] name = "torch" version = "2.10.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "cuda-bindings", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "fsspec", marker = "python_full_version >= '3.10'" }, - { name = "jinja2", marker = "python_full_version >= '3.10'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", version = "2.27.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy", marker = "python_full_version >= '3.10'" }, - { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" }, @@ -5159,59 +3397,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, ] -[[package]] -name = "torch-geometric" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "aiohttp", marker = "python_full_version < '3.10'" }, - { name = "fsspec", marker = "python_full_version < '3.10'" }, - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "psutil", marker = "python_full_version < '3.10'" }, - { name = "pyparsing", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, - { name = "tqdm", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e8/81/e1b015494cb9e0bf4c47cc8426e49736120248733be0e22072a5628ae9ed/torch_geometric-2.6.1.tar.gz", hash = "sha256:1f18f9d0fc4d2239d526221e4f22606a4a3895b5d965a9856d27610a3df662c6", size = 771490, upload-time = "2024-09-26T08:11:30.25Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/9f/157e913626c1acfb3b19ce000b1a6e4e4fb177c0bc0ea0c67ca5bd714b5a/torch_geometric-2.6.1-py3-none-any.whl", hash = "sha256:8faeb353f9655f7dbec44c5e0b44c721773bdfb279994da96b9b8b12fd30f427", size = 1135632, upload-time = "2024-09-26T08:11:27.194Z" }, -] - [[package]] name = "torch-geometric" version = "2.7.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "aiohttp", marker = "python_full_version >= '3.10'" }, - { name = "fsspec", marker = "python_full_version >= '3.10'" }, - { name = "jinja2", marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "aiohttp" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "psutil", marker = "python_full_version >= '3.10'" }, - { name = "pyparsing", marker = "python_full_version >= '3.10'" }, - { name = "requests", marker = "python_full_version >= '3.10'" }, - { name = "tqdm", marker = "python_full_version >= '3.10'" }, - { name = "xxhash", marker = "python_full_version >= '3.10'" }, + { name = "psutil" }, + { name = "pyparsing" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/63/b210152635902da7fe79fcdd16517fae108f457a0ed22c737e702a9afbae/torch_geometric-2.7.0.tar.gz", hash = "sha256:f9099e4aece1a9f618c84dbaac33a77f43139736698c7e8bddf3301ef1f2e8d4", size = 876725, upload-time = "2025-10-15T20:48:03.443Z" } wheels = [ @@ -5249,98 +3449,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] -[[package]] -name = "transformers" -version = "4.57.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "huggingface-hub", version = "0.36.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pyyaml", marker = "python_full_version < '3.10'" }, - { name = "regex", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, - { name = "safetensors", marker = "python_full_version < '3.10'" }, - { name = "tokenizers", marker = "python_full_version < '3.10'" }, - { name = "tqdm", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, -] - [[package]] name = "transformers" version = "5.0.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.10.*' and platform_machine == 'ARM64' and sys_platform == 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] dependencies = [ - { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "huggingface-hub", version = "1.3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pyyaml", marker = "python_full_version >= '3.10'" }, - { name = "regex", marker = "python_full_version >= '3.10'" }, - { name = "safetensors", marker = "python_full_version >= '3.10'" }, - { name = "tokenizers", marker = "python_full_version >= '3.10'" }, - { name = "tqdm", marker = "python_full_version >= '3.10'" }, - { name = "typer-slim", marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer-slim" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/79/845941711811789c85fb7e2599cea425a14a07eda40f50896b9d3fda7492/transformers-5.0.0.tar.gz", hash = "sha256:5f5634efed6cf76ad068cc5834c7adbc32db78bbd6211fb70df2325a9c37dec8", size = 8424830, upload-time = "2026-01-26T10:46:46.813Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/f3/ac976fa8e305c9e49772527e09fbdc27cc6831b8a2f6b6063406626be5dd/transformers-5.0.0-py3-none-any.whl", hash = "sha256:587086f249ce64c817213cf36afdb318d087f790723e9b3d4500b97832afd52d", size = 10142091, upload-time = "2026-01-26T10:46:43.88Z" }, ] -[[package]] -name = "triton" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "setuptools", marker = "python_full_version < '3.10'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/ee/0ee5f64a87eeda19bbad9bc54ae5ca5b98186ed00055281fd40fb4beb10e/triton-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ff2785de9bc02f500e085420273bb5cc9c9bb767584a4aa28d6e360cec70128", size = 155430069, upload-time = "2025-07-30T19:58:21.715Z" }, - { url = "https://files.pythonhosted.org/packages/7d/39/43325b3b651d50187e591eefa22e236b2981afcebaefd4f2fc0ea99df191/triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b70f5e6a41e52e48cfc087436c8a28c17ff98db369447bcaff3b887a3ab4467", size = 155531138, upload-time = "2025-07-30T19:58:29.908Z" }, - { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" }, - { url = "https://files.pythonhosted.org/packages/30/7b/0a685684ed5322d2af0bddefed7906674f67974aa88b0fae6e82e3b766f6/triton-3.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00be2964616f4c619193cb0d1b29a99bd4b001d7dc333816073f92cf2a8ccdeb", size = 155569223, upload-time = "2025-07-30T19:58:44.017Z" }, - { url = "https://files.pythonhosted.org/packages/20/63/8cb444ad5cdb25d999b7d647abac25af0ee37d292afc009940c05b82dda0/triton-3.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7936b18a3499ed62059414d7df563e6c163c5e16c3773678a3ee3d417865035d", size = 155659780, upload-time = "2025-07-30T19:58:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/12/34/1251beb5a3cb93f3950ebe68732752014646003ef6eb11eb5f1a37ca78cd/triton-3.4.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e5c1442eaeabae2e2452ae765801bd53cd4ce873cab0d1bdd59a32ab2d9397", size = 155430799, upload-time = "2025-07-30T19:58:57.664Z" }, -] - [[package]] name = "triton" version = "3.6.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "(python_full_version == '3.10.*' and platform_machine != 'ARM64') or (python_full_version == '3.10.*' and sys_platform != 'win32')", -] wheels = [ { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, @@ -5356,8 +3490,8 @@ name = "typer-slim" version = "0.21.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "click" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } wheels = [ @@ -5397,80 +3531,63 @@ version = "1.0.0" source = { virtual = "." } dependencies = [ { name = "hydra-core" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "setuptools" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torch" }, { name = "tqdm" }, ] [package.optional-dependencies] all = [ { name = "datasets" }, - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib" }, { name = "plotly" }, { name = "pmlb" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, - { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "seaborn" }, - { name = "sentence-transformers", version = "5.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sentence-transformers", version = "5.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "streamlit", version = "1.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "streamlit", version = "1.53.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sentence-transformers" }, + { name = "streamlit" }, { name = "sympy" }, - { name = "torch-geometric", version = "2.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch-geometric", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torch-geometric" }, ] all-tasks = [ { name = "datasets" }, { name = "pmlb" }, - { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sentence-transformers", version = "5.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sentence-transformers", version = "5.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sentence-transformers" }, { name = "sympy" }, - { name = "torch-geometric", version = "2.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch-geometric", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torch-geometric" }, ] demo = [ { name = "plotly" }, - { name = "streamlit", version = "1.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "streamlit", version = "1.53.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "streamlit" }, ] dev = [ - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, ] lqa = [ { name = "datasets" }, - { name = "sentence-transformers", version = "5.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sentence-transformers", version = "5.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sentence-transformers" }, ] md17 = [ - { name = "torch-geometric", version = "2.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch-geometric", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torch-geometric" }, ] sr = [ { name = "pmlb" }, - { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sympy" }, ] viz = [ - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib" }, { name = "seaborn" }, ] @@ -5478,8 +3595,7 @@ viz = [ docs = [ { name = "mkdocs" }, { name = "mkdocs-material" }, - { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.10'" }, - { name = "mkdocstrings", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.10'" }, + { name = "mkdocstrings", extra = ["python"] }, { name = "pymdown-extensions" }, ] @@ -5534,13 +3650,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, - { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, @@ -5664,21 +3775,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/03/ff/1b4bb3f397552116c1df6266c1b83a21aeeb26061ab1f462984b499a3870/xxhash-3.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a", size = 32844, upload-time = "2025-10-02T14:36:39.157Z" }, - { url = "https://files.pythonhosted.org/packages/c1/db/27146d0bee4346a9a31f7b498a81fc02747f6f1e6c52a2e7989504278051/xxhash-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7", size = 30806, upload-time = "2025-10-02T14:36:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/4896188df564908817a75de19bf7f2384b99a75af2d528f9c49326f76458/xxhash-3.6.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2", size = 193448, upload-time = "2025-10-02T14:36:41.797Z" }, - { url = "https://files.pythonhosted.org/packages/51/c5/be8953f62e772340319a826ce1e07489935600089756cf83b628cd36ebe3/xxhash-3.6.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d", size = 212547, upload-time = "2025-10-02T14:36:43.581Z" }, - { url = "https://files.pythonhosted.org/packages/51/1a/1e9f0b911d1cf00dd537c074ae3fae15b535a7f0d9e7edd42a9d2c4f78ce/xxhash-3.6.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec", size = 211309, upload-time = "2025-10-02T14:36:45.307Z" }, - { url = "https://files.pythonhosted.org/packages/63/88/b284c6a128d88dc47f201957f926e707db79fb7415a87072e15c0e490de0/xxhash-3.6.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222", size = 444480, upload-time = "2025-10-02T14:36:47.226Z" }, - { url = "https://files.pythonhosted.org/packages/87/e4/798293a2bf9e4fac5f6d53ce59cba4739930778dfc6c7c73f40044ab0e6e/xxhash-3.6.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919", size = 192957, upload-time = "2025-10-02T14:36:48.968Z" }, - { url = "https://files.pythonhosted.org/packages/78/55/bfd0d7db447a927897469048b953caececa3532e743b940dd1f5c1032d24/xxhash-3.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6", size = 209850, upload-time = "2025-10-02T14:36:50.258Z" }, - { url = "https://files.pythonhosted.org/packages/31/06/d08ef9a792bfebfd2fb2bcbf04a541ad283bef74749ead6f089a0809d288/xxhash-3.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360", size = 197342, upload-time = "2025-10-02T14:36:51.651Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1a/aebf90797c94e9ca407c28e23f54d71f7149d91a93406a08a09e44d06994/xxhash-3.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4", size = 209757, upload-time = "2025-10-02T14:36:53.009Z" }, - { url = "https://files.pythonhosted.org/packages/3c/80/799eec3d0a144dc3edf8c19b4f139c27fb923c50b34352796089ca206429/xxhash-3.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86", size = 412773, upload-time = "2025-10-02T14:36:54.691Z" }, - { url = "https://files.pythonhosted.org/packages/6a/f9/09df7545699de09219a205123b8463ce9ea83f48acc7aeeba0269507f9d3/xxhash-3.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796", size = 190357, upload-time = "2025-10-02T14:36:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/07/40/2f8327f94e64a3f34d6ce3347c55207c322abbc80ae486ea45df4c62e7b3/xxhash-3.6.0-cp39-cp39-win32.whl", hash = "sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d", size = 30585, upload-time = "2025-10-02T14:36:57.664Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c8/2ecbc6799be9c02e8bf7b5a66cd94832b6ac13d59808746f0d402481c6ad/xxhash-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802", size = 31512, upload-time = "2025-10-02T14:36:58.837Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/1d5459a9c587c94d7b8bcc710bd08bbfa145cbd814ebde41b48494362a21/xxhash-3.6.0-cp39-cp39-win_arm64.whl", hash = "sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6", size = 27878, upload-time = "2025-10-02T14:37:00.201Z" }, { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, @@ -5809,30 +3905,5 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" }, - { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" }, - { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" }, - { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" }, - { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" }, - { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" }, - { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" }, - { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" }, - { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" }, { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] From 27ddd21608e5ed30833ac2769c6c1356e897daad Mon Sep 17 00:00:00 2001 From: Concode0 Date: Fri, 15 May 2026 10:24:38 +0900 Subject: [PATCH 41/45] fix: support pairwise projected products --- core/runtime/projected.py | 58 +++++++++++++++++++++++--- tests/test_grade_plan.py | 86 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 6 deletions(-) diff --git a/core/runtime/projected.py b/core/runtime/projected.py index 5ed71a8..da3e863 100644 --- a/core/runtime/projected.py +++ b/core/runtime/projected.py @@ -111,8 +111,14 @@ def projected_product( right_compact: bool = False, compact_output: bool = False, return_layout: bool = False, + pairwise: bool = False, ): - """Compute a declared grade-restricted product through a static executor.""" + """Compute a declared grade-restricted product through a static executor. + + By default, operands are multiplied elementwise over broadcastable + prefix dimensions. Set ``pairwise=True`` when the dimension before each + compact lane axis is an independent left/right item axis. + """ left_layout = self._declared_layout(left_grades, left_layout) right_layout = self._declared_layout(right_grades, right_layout) if not left_compact and left_layout is not None and A.shape[-1] == left_layout.dim: @@ -139,12 +145,10 @@ def projected_product( ) executor = self.planner.product_executor_for_request(request) - if request.left_compact or request.right_compact: - A_values = A if request.left_compact else executor.left_layout.compact(A) - B_values = B if request.right_compact else executor.right_layout.compact(B) - values = executor.forward_compact(A_values, B_values) + if pairwise: + values = self._execute_pairwise_product(A, B, request, executor) else: - values = executor(A, B) + values = self._execute_elementwise_product(A, B, request, executor) if return_layout: return values, executor.output_layout @@ -213,3 +217,45 @@ def _declared_layout(self, grades, layout): if default_grades is None: return None return self.layout(default_grades) + + def _execute_elementwise_product(self, left, right, request, executor): + if request.left_compact or request.right_compact: + left_values = left if request.left_compact else executor.left_layout.compact(left) + right_values = right if request.right_compact else executor.right_layout.compact(right) + self._check_elementwise_prefix(left_values, right_values) + return executor.forward_compact(left_values, right_values) + + self._check_elementwise_prefix(left, right) + return executor(left, right) + + def _execute_pairwise_product(self, left, right, request, executor): + left_values = left if request.left_compact else executor.left_layout.compact(left) + right_values = right if request.right_compact else executor.right_layout.compact(right) + self._check_pairwise_prefix(left_values, right_values) + return executor.forward_pairwise_compact(left_values, right_values) + + @staticmethod + def _check_elementwise_prefix(left: torch.Tensor, right: torch.Tensor) -> None: + try: + torch.broadcast_shapes(left.shape[:-1], right.shape[:-1]) + except RuntimeError as exc: + raise ValueError( + "projected_product elementwise prefixes must be broadcastable; " + f"got left prefix {tuple(left.shape[:-1])} and right prefix {tuple(right.shape[:-1])}. " + "Use pairwise=True when left and right have distinct item axes." + ) from exc + + @staticmethod + def _check_pairwise_prefix(left: torch.Tensor, right: torch.Tensor) -> None: + if left.ndim < 2 or right.ndim < 2: + raise ValueError( + "pairwise projected_product requires explicit item axes before the compact lane dimension; " + f"got left shape {tuple(left.shape)} and right shape {tuple(right.shape)}" + ) + try: + torch.broadcast_shapes(left.shape[:-2], right.shape[:-2]) + except RuntimeError as exc: + raise ValueError( + "pairwise projected_product batch prefixes must be broadcastable; " + f"got left prefix {tuple(left.shape[:-2])} and right prefix {tuple(right.shape[:-2])}" + ) from exc diff --git a/tests/test_grade_plan.py b/tests/test_grade_plan.py index 5098b24..cd4d801 100644 --- a/tests/test_grade_plan.py +++ b/tests/test_grade_plan.py @@ -629,6 +629,26 @@ def test_multivector_compact_geometric_product_stays_compact_in_high_dimensions( assert torch.allclose(result.values[0, scalar_pos], torch.tensor(1.0)) +def test_multivector_compact_binary_wrappers_do_not_unwrap_dense_tensors(): + algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) + bivector_layout = algebra.layout((2,)) + vector_layout = algebra.layout((1,)) + bivector = Multivector(algebra, values=torch.randn(2, bivector_layout.dim), layout=bivector_layout) + vector = Multivector(algebra, values=torch.randn(2, vector_layout.dim), layout=vector_layout) + + results = [ + (bivector.geometric_product(vector), (1, 3)), + (bivector.wedge(vector), (3,)), + (bivector.inner(vector), (3,)), + (bivector.commutator(vector), (1,)), + (bivector.anti_commutator(vector), (3,)), + ] + + for result, expected_grades in results: + assert result.is_compact + assert result.layout.grades == expected_grades + + def test_multivector_compact_addition_merges_layouts_without_dense_materialization(): algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) vector_layout = algebra.layout((1,)) @@ -686,6 +706,72 @@ def test_context_declared_grades_infer_compact_operand_shapes(): assert torch.allclose(values[0, output_layout.basis_indices.index(0)], torch.tensor(1.0)) +def test_context_projected_product_pairwise_mixed_compact_widths(): + algebra = make_algebra(16, 0, 0, kernel="context", device=DEVICE, dtype=torch.float32) + left_layout = algebra.layout((2,)) + right_layout = algebra.layout((1,)) + left = torch.randn(3, left_layout.dim) + right = torch.randn(4, right_layout.dim) + + values, output_layout = algebra.projected_wedge( + left, + right, + left_layout=left_layout, + right_layout=right_layout, + output_grades=(3,), + pairwise=True, + compact_output=True, + return_layout=True, + ) + executor = algebra.product_executor( + op="wedge", + left_grades=(2,), + right_grades=(1,), + output_grades=(3,), + dtype=torch.float32, + device=DEVICE, + ) + + assert output_layout.grades == (3,) + assert values.shape == (3, 4, output_layout.dim) + assert torch.allclose(values, executor.forward_pairwise_compact(left, right), atol=1e-6, rtol=1e-6) + + +def test_context_projected_product_suggests_pairwise_for_mismatched_item_axes(): + algebra = make_algebra(16, 0, 0, kernel="context", device=DEVICE, dtype=torch.float32) + left_layout = algebra.layout((2,)) + right_layout = algebra.layout((1,)) + left = torch.randn(3, left_layout.dim) + right = torch.randn(4, right_layout.dim) + + with pytest.raises(ValueError, match="Use pairwise=True"): + algebra.projected_wedge( + left, + right, + left_layout=left_layout, + right_layout=right_layout, + output_grades=(3,), + compact_output=True, + ) + + +def test_context_pairwise_projected_product_requires_item_axes(): + algebra = make_algebra(16, 0, 0, kernel="context", device=DEVICE, dtype=torch.float32) + left_layout = algebra.layout((2,)) + right_layout = algebra.layout((1,)) + + with pytest.raises(ValueError, match="explicit item axes"): + algebra.projected_wedge( + torch.randn(left_layout.dim), + torch.randn(right_layout.dim), + left_layout=left_layout, + right_layout=right_layout, + output_grades=(3,), + pairwise=True, + compact_output=True, + ) + + def test_context_declared_product_requires_compact_output_without_dense_materialization(): algebra = make_algebra(10, 4, 2, device=DEVICE, dtype=torch.float32) vector_layout = algebra.layout((1,)) From ed806e4b8a5fd86d7f1acd48f31ea811b95529de Mon Sep 17 00:00:00 2001 From: Concode0 Date: Fri, 15 May 2026 10:25:13 +0900 Subject: [PATCH 42/45] feat: add framework product pipeline surfaces --- core/foundation/__init__.py | 18 ++++ core/foundation/manifold.py | 30 +++++++ core/foundation/module.py | 40 +++++++++ functional/__init__.py | 34 ++++++- functional/products.py | 130 +++++++++++++++++++++++++++ layers/__init__.py | 16 ++++ layers/adapters/embedding.py | 3 +- layers/primitives/__init__.py | 34 +++++++ layers/primitives/multi_rotor.py | 3 +- layers/primitives/product.py | 138 +++++++++++++++++++++++++++++ layers/primitives/reflection.py | 3 +- layers/primitives/rotor.py | 3 +- layers/primitives/rotor_gadget.py | 5 +- optimizers/__init__.py | 2 + optimizers/riemannian.py | 92 ++++++++++--------- tests/test_functional_products.py | 60 +++++++++++++ tests/test_riemannian_optimizer.py | 35 ++++++-- 17 files changed, 592 insertions(+), 54 deletions(-) create mode 100644 core/foundation/manifold.py create mode 100644 functional/products.py create mode 100644 layers/primitives/product.py create mode 100644 tests/test_functional_products.py diff --git a/core/foundation/__init__.py b/core/foundation/__init__.py index 2c8c726..5af000d 100644 --- a/core/foundation/__init__.py +++ b/core/foundation/__init__.py @@ -23,6 +23,16 @@ ) from .device import DeviceConfig, dtype_name, optional_dtype, resolve_device, resolve_dtype from .layout import AlgebraSpec, GradeLayout +from .manifold import ( + MANIFOLD_EUCLIDEAN, + MANIFOLD_ORDER, + MANIFOLD_SPHERE, + MANIFOLD_SPIN, + VALID_MANIFOLDS, + format_valid_manifolds, + tag_manifold, + validate_manifold, +) from .module import AlgebraLike, CliffordModule from .validation import check_channels, check_multivector @@ -33,6 +43,11 @@ "DeviceConfig", "GradeLayout", "GradeProductOp", + "MANIFOLD_EUCLIDEAN", + "MANIFOLD_ORDER", + "MANIFOLD_SPHERE", + "MANIFOLD_SPIN", + "VALID_MANIFOLDS", "basis_count_for_grades", "basis_index_tuple_for_grades", "basis_indices_for_grades", @@ -41,6 +56,7 @@ "check_multivector", "dtype_name", "expand_output_grades", + "format_valid_manifolds", "geometric_product_output_grades", "normalize_grades", "operation_coefficient", @@ -50,4 +66,6 @@ "reverse_sign", "resolve_device", "resolve_dtype", + "tag_manifold", + "validate_manifold", ] diff --git a/core/foundation/manifold.py b/core/foundation/manifold.py new file mode 100644 index 0000000..efa0b3d --- /dev/null +++ b/core/foundation/manifold.py @@ -0,0 +1,30 @@ +"""Shared manifold metadata for framework parameters.""" + +from __future__ import annotations + +import torch.nn as nn + +MANIFOLD_SPIN = "spin" +MANIFOLD_SPHERE = "sphere" +MANIFOLD_EUCLIDEAN = "euclidean" + +MANIFOLD_ORDER = (MANIFOLD_SPIN, MANIFOLD_SPHERE, MANIFOLD_EUCLIDEAN) +VALID_MANIFOLDS = frozenset(MANIFOLD_ORDER) + + +def format_valid_manifolds() -> str: + """Return a stable, user-facing list of supported manifold names.""" + return ", ".join(repr(name) for name in MANIFOLD_ORDER) + + +def validate_manifold(manifold: str) -> str: + """Validate and return a manifold tag.""" + if manifold not in VALID_MANIFOLDS: + raise ValueError(f"Unknown manifold {manifold!r}. Must be one of {format_valid_manifolds()}") + return manifold + + +def tag_manifold(param: nn.Parameter, manifold: str) -> nn.Parameter: + """Tag a parameter with its manifold type and return the parameter.""" + param._manifold = validate_manifold(manifold) + return param diff --git a/core/foundation/module.py b/core/foundation/module.py index 2dc46fc..8139fc7 100644 --- a/core/foundation/module.py +++ b/core/foundation/module.py @@ -80,6 +80,46 @@ def hermitian_signs( """Return Hermitian signs for a dense or compact layout.""" ... + def projected_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply a declared grade-restricted binary product.""" + ... + + def geometric_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply the geometric product.""" + ... + + def wedge(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply the exterior product.""" + ... + + def inner_product(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply the inner product.""" + ... + + def commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply the commutator product.""" + ... + + def anti_commutator(self, A: torch.Tensor, B: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply the anti-commutator product.""" + ... + + def grade_projection(self, mv: torch.Tensor, grade: int, **kwargs) -> torch.Tensor: + """Project to a single grade.""" + ... + + def reverse(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply reversion.""" + ... + + def grade_involution(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply grade involution.""" + ... + + def clifford_conjugation(self, mv: torch.Tensor, **kwargs) -> torch.Tensor: + """Apply Clifford conjugation.""" + ... + class CliffordModule(nn.Module): """Base module for Clifford algebra-aware components. diff --git a/functional/__init__.py b/functional/__init__.py index 595a9b1..13c025c 100644 --- a/functional/__init__.py +++ b/functional/__init__.py @@ -3,7 +3,7 @@ Includes activation functions, loss functions, and orthogonality enforcement. """ -from .activation import GeometricGELU, GradeSwish +from .activation import GeometricGELU, GeometricSquare, GradeSwish from .loss import ( BivectorRegularization, ChamferDistance, @@ -15,11 +15,43 @@ SubspaceLoss, ) from .orthogonality import OrthogonalitySettings, StrictOrthogonality +from .products import ( + anti_commutator, + clifford_conjugation, + commutator, + dual, + embed_vector, + geometric_product, + grade_involution, + grade_projection, + inner_product, + norm_sq, + product, + projected_product, + reverse, + wedge, +) __all__ = [ # activations "GeometricGELU", + "GeometricSquare", "GradeSwish", + # products + "product", + "projected_product", + "geometric_product", + "wedge", + "inner_product", + "commutator", + "anti_commutator", + "grade_projection", + "reverse", + "grade_involution", + "clifford_conjugation", + "dual", + "norm_sq", + "embed_vector", # losses "GeometricMSELoss", "SubspaceLoss", diff --git a/functional/products.py b/functional/products.py new file mode 100644 index 0000000..bc3f254 --- /dev/null +++ b/functional/products.py @@ -0,0 +1,130 @@ +"""Stateless geometric algebra product helpers. + +These wrappers keep model code concise while preserving the algebra host as the +single execution authority. Dense kernels, compact planned kernels, and pairwise +planned kernels all flow through the same public calls. +""" + +from __future__ import annotations + +from typing import Any + +import torch + +_PRODUCT_METHODS = { + "gp": ("gp", "geometric_product"), + "geometric_product": ("gp", "geometric_product"), + "wedge": ("wedge", "wedge"), + "outer": ("wedge", "wedge"), + "inner": ("inner", "inner_product"), + "inner_product": ("inner", "inner_product"), + "commutator": ("commutator", "commutator"), + "anti_commutator": ("anti_commutator", "anti_commutator"), + "anticommutator": ("anti_commutator", "anti_commutator"), +} + + +def canonical_product_op(op: str) -> str: + """Return the planner op name for a supported product alias.""" + return _resolve_product_op(op)[0] + + +def _resolve_product_op(op: str) -> tuple[str, str]: + op_key = op.lower() + if op_key not in _PRODUCT_METHODS: + supported = ", ".join(sorted(_PRODUCT_METHODS)) + raise ValueError(f"Unsupported product op {op!r}. Supported ops: {supported}") + return _PRODUCT_METHODS[op_key] + + +def product(algebra, left: torch.Tensor, right: torch.Tensor, *, op: str = "gp", **kwargs: Any) -> torch.Tensor: + """Apply a binary geometric algebra product. + + Args: + algebra: Dense ``CliffordAlgebra`` or planned ``AlgebraContext``. + left: Left operand. + right: Right operand. + op: ``"gp"``, ``"wedge"``, ``"inner"``, ``"commutator"``, or + ``"anti_commutator"``. + **kwargs: Optional grade/layout declarations accepted by + ``algebra.projected_product``. + + Returns: + Product values in dense or compact form according to ``kwargs``. + """ + planned_op, method_name = _resolve_product_op(op) + if kwargs: + return algebra.projected_product(left, right, op=planned_op, **kwargs) + return getattr(algebra, method_name)(left, right) + + +def projected_product( + algebra, + left: torch.Tensor, + right: torch.Tensor, + *, + op: str = "gp", + **kwargs: Any, +) -> torch.Tensor: + """Apply a declared grade-restricted product through the planner.""" + return product(algebra, left, right, op=op, **kwargs) + + +def geometric_product(algebra, left: torch.Tensor, right: torch.Tensor, **kwargs: Any) -> torch.Tensor: + """Apply the geometric product.""" + return product(algebra, left, right, op="gp", **kwargs) + + +def wedge(algebra, left: torch.Tensor, right: torch.Tensor, **kwargs: Any) -> torch.Tensor: + """Apply the exterior product.""" + return product(algebra, left, right, op="wedge", **kwargs) + + +def inner_product(algebra, left: torch.Tensor, right: torch.Tensor, **kwargs: Any) -> torch.Tensor: + """Apply the inner product.""" + return product(algebra, left, right, op="inner", **kwargs) + + +def commutator(algebra, left: torch.Tensor, right: torch.Tensor, **kwargs: Any) -> torch.Tensor: + """Apply the commutator product.""" + return product(algebra, left, right, op="commutator", **kwargs) + + +def anti_commutator(algebra, left: torch.Tensor, right: torch.Tensor, **kwargs: Any) -> torch.Tensor: + """Apply the anti-commutator product.""" + return product(algebra, left, right, op="anti_commutator", **kwargs) + + +def grade_projection(algebra, values: torch.Tensor, grade: int, **kwargs: Any) -> torch.Tensor: + """Project multivectors to one grade.""" + return algebra.grade_projection(values, grade, **kwargs) + + +def reverse(algebra, values: torch.Tensor, **kwargs: Any) -> torch.Tensor: + """Apply reversion.""" + return algebra.reverse(values, **kwargs) + + +def grade_involution(algebra, values: torch.Tensor, **kwargs: Any) -> torch.Tensor: + """Apply grade involution.""" + return algebra.grade_involution(values, **kwargs) + + +def clifford_conjugation(algebra, values: torch.Tensor, **kwargs: Any) -> torch.Tensor: + """Apply Clifford conjugation.""" + return algebra.clifford_conjugation(values, **kwargs) + + +def dual(algebra, values: torch.Tensor) -> torch.Tensor: + """Apply the Hodge dual.""" + return algebra.dual(values) + + +def norm_sq(algebra, values: torch.Tensor) -> torch.Tensor: + """Return the algebraic squared norm.""" + return algebra.norm_sq(values) + + +def embed_vector(algebra, vectors: torch.Tensor) -> torch.Tensor: + """Embed coordinate vectors into the grade-1 subspace.""" + return algebra.embed_vector(vectors) diff --git a/layers/__init__.py b/layers/__init__.py index 85a81ac..457847b 100644 --- a/layers/__init__.py +++ b/layers/__init__.py @@ -13,7 +13,16 @@ from .primitives.linear import CliffordLinear from .primitives.multi_rotor import MultiRotorLayer from .primitives.normalization import CliffordLayerNorm +from .primitives.product import ( + AntiCommutatorLayer, + CommutatorLayer, + GeometricProductLayer, + InnerProductLayer, + ProductLayer, + WedgeLayer, +) from .primitives.projection import BladeSelector, GeometricNeutralizer +from .primitives.reflection import ReflectionLayer from .primitives.rotor import RotorLayer from .primitives.rotor_gadget import RotorGadget @@ -30,8 +39,15 @@ "CliffordLinear", "RotorGadget", "CliffordLayerNorm", + "ProductLayer", + "GeometricProductLayer", + "WedgeLayer", + "InnerProductLayer", + "CommutatorLayer", + "AntiCommutatorLayer", "BladeSelector", "GeometricNeutralizer", + "ReflectionLayer", "MultivectorEmbedding", "RotaryBivectorPE", "MotherEmbedding", diff --git a/layers/adapters/embedding.py b/layers/adapters/embedding.py index ac4e7f1..1b0bfee 100644 --- a/layers/adapters/embedding.py +++ b/layers/adapters/embedding.py @@ -8,6 +8,7 @@ import torch import torch.nn as nn +from core.foundation.manifold import MANIFOLD_SPIN, tag_manifold from core.foundation.module import CliffordModule from core.runtime.algebra import CliffordAlgebra @@ -122,7 +123,7 @@ def __init__( if learnable: self.bivector_weights = nn.Parameter(init) - self.bivector_weights._manifold = "spin" + tag_manifold(self.bivector_weights, MANIFOLD_SPIN) else: self.register_buffer("bivector_weights", init) diff --git a/layers/primitives/__init__.py b/layers/primitives/__init__.py index e69de29..451f879 100644 --- a/layers/primitives/__init__.py +++ b/layers/primitives/__init__.py @@ -0,0 +1,34 @@ +"""Primitive Clifford neural network layers.""" + +from .linear import CliffordLinear +from .multi_rotor import MultiRotorLayer +from .normalization import CliffordLayerNorm +from .product import ( + AntiCommutatorLayer, + CommutatorLayer, + GeometricProductLayer, + InnerProductLayer, + ProductLayer, + WedgeLayer, +) +from .projection import BladeSelector, GeometricNeutralizer +from .reflection import ReflectionLayer +from .rotor import RotorLayer +from .rotor_gadget import RotorGadget + +__all__ = [ + "CliffordLinear", + "MultiRotorLayer", + "CliffordLayerNorm", + "ProductLayer", + "GeometricProductLayer", + "WedgeLayer", + "InnerProductLayer", + "CommutatorLayer", + "AntiCommutatorLayer", + "BladeSelector", + "GeometricNeutralizer", + "ReflectionLayer", + "RotorLayer", + "RotorGadget", +] diff --git a/layers/primitives/multi_rotor.py b/layers/primitives/multi_rotor.py index 16addb9..0198f0b 100644 --- a/layers/primitives/multi_rotor.py +++ b/layers/primitives/multi_rotor.py @@ -13,6 +13,7 @@ import torch import torch.nn as nn +from core.foundation.manifold import MANIFOLD_SPIN, tag_manifold from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector from core.runtime.algebra import CliffordAlgebra @@ -63,7 +64,7 @@ def __init__( self.rotor_grade_weights = nn.Parameter(torch.Tensor(num_rotors, self.num_grade_elements)) if grade == 2: - self.rotor_grade_weights._manifold = "spin" + tag_manifold(self.rotor_grade_weights, MANIFOLD_SPIN) # Mixing weights (Euclidean — intentionally untagged) self.weights = nn.Parameter(torch.Tensor(channels, num_rotors)) diff --git a/layers/primitives/product.py b/layers/primitives/product.py new file mode 100644 index 0000000..90c9819 --- /dev/null +++ b/layers/primitives/product.py @@ -0,0 +1,138 @@ +"""Module wrappers around declared geometric algebra products.""" + +from __future__ import annotations + +from typing import Iterable, Optional + +import torch + +from core.foundation.basis import normalize_grades +from core.foundation.module import CliffordModule +from functional.products import canonical_product_op, product + + +def _normalize_optional_grades(grades: Optional[Iterable[int]], n: int, *, name: str) -> Optional[tuple[int, ...]]: + if grades is None: + return None + if isinstance(grades, int): + grades = (grades,) + return normalize_grades(grades, n, name=name) + + +class ProductLayer(CliffordModule): + """Apply a geometric algebra product inside ``nn.Module`` graphs. + + The layer is intentionally thin: grade declarations and compact/pairwise + behavior are forwarded to ``algebra.projected_product`` when supplied, + while the dense no-declaration path uses the algebra's direct kernels. + """ + + def __init__( + self, + algebra, + *, + op: str = "gp", + left_grades: Optional[Iterable[int]] = None, + right_grades: Optional[Iterable[int]] = None, + output_grades: Optional[Iterable[int]] = None, + left_compact: bool = False, + right_compact: bool = False, + compact_output: bool = False, + pairwise: bool = False, + ): + """Initialize a product layer. + + Args: + algebra: Dense ``CliffordAlgebra`` or planned ``AlgebraContext``. + op: Product route: ``"gp"``, ``"wedge"``, ``"inner"``, + ``"commutator"``, or ``"anti_commutator"``. + left_grades: Declared input grades for the left operand. + right_grades: Declared input grades for the right operand. + output_grades: Optional output grade projection. + left_compact: Whether the left operand is already compact. + right_compact: Whether the right operand is already compact. + compact_output: Return compact output instead of dense coefficients. + pairwise: Treat the penultimate dimension of each compact operand + as independent left/right item axes. + """ + super().__init__(algebra) + self.op = canonical_product_op(op) + self.left_grades = _normalize_optional_grades(left_grades, algebra.n, name="left_grades") + self.right_grades = _normalize_optional_grades(right_grades, algebra.n, name="right_grades") + self.output_grades = _normalize_optional_grades(output_grades, algebra.n, name="output_grades") + self.left_compact = bool(left_compact) + self.right_compact = bool(right_compact) + self.compact_output = bool(compact_output) + self.pairwise = bool(pairwise) + + def forward(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor: + """Apply the configured product to ``left`` and ``right``.""" + kwargs = self._product_kwargs() + return product(self.algebra, left, right, op=self.op, **kwargs) + + def _product_kwargs(self) -> dict: + kwargs = {} + if self.left_grades is not None: + kwargs["left_grades"] = self.left_grades + if self.right_grades is not None: + kwargs["right_grades"] = self.right_grades + if self.output_grades is not None: + kwargs["output_grades"] = self.output_grades + if self.left_compact: + kwargs["left_compact"] = True + if self.right_compact: + kwargs["right_compact"] = True + if self.compact_output: + kwargs["compact_output"] = True + if self.pairwise: + kwargs["pairwise"] = True + return kwargs + + def extra_repr(self) -> str: + parts = [f"op={self.op!r}"] + if self.left_grades is not None: + parts.append(f"left_grades={self.left_grades}") + if self.right_grades is not None: + parts.append(f"right_grades={self.right_grades}") + if self.output_grades is not None: + parts.append(f"output_grades={self.output_grades}") + if self.compact_output: + parts.append("compact_output=True") + if self.pairwise: + parts.append("pairwise=True") + return ", ".join(parts) + + +class GeometricProductLayer(ProductLayer): + """Layer form of the geometric product.""" + + def __init__(self, algebra, **kwargs): + super().__init__(algebra, op="gp", **kwargs) + + +class WedgeLayer(ProductLayer): + """Layer form of the exterior product.""" + + def __init__(self, algebra, **kwargs): + super().__init__(algebra, op="wedge", **kwargs) + + +class InnerProductLayer(ProductLayer): + """Layer form of the inner product.""" + + def __init__(self, algebra, **kwargs): + super().__init__(algebra, op="inner", **kwargs) + + +class CommutatorLayer(ProductLayer): + """Layer form of the commutator product.""" + + def __init__(self, algebra, **kwargs): + super().__init__(algebra, op="commutator", **kwargs) + + +class AntiCommutatorLayer(ProductLayer): + """Layer form of the anti-commutator product.""" + + def __init__(self, algebra, **kwargs): + super().__init__(algebra, op="anti_commutator", **kwargs) diff --git a/layers/primitives/reflection.py b/layers/primitives/reflection.py index ac3d388..0d83b47 100644 --- a/layers/primitives/reflection.py +++ b/layers/primitives/reflection.py @@ -8,6 +8,7 @@ import torch import torch.nn as nn +from core.foundation.manifold import MANIFOLD_SPHERE, tag_manifold from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector from core.runtime.algebra import CliffordAlgebra @@ -46,7 +47,7 @@ def __init__(self, algebra: CliffordAlgebra, channels: int): self.num_vectors = algebra.n self.vector_weights = nn.Parameter(torch.Tensor(channels, self.num_vectors)) - self.vector_weights._manifold = "sphere" + tag_manifold(self.vector_weights, MANIFOLD_SPHERE) # Cache for eval mode self._cached_n = None diff --git a/layers/primitives/rotor.py b/layers/primitives/rotor.py index 32d126d..2fc064c 100644 --- a/layers/primitives/rotor.py +++ b/layers/primitives/rotor.py @@ -8,6 +8,7 @@ import torch import torch.nn as nn +from core.foundation.manifold import MANIFOLD_SPIN, tag_manifold from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector from core.runtime.algebra import CliffordAlgebra @@ -57,7 +58,7 @@ def __init__( self.grade_weights = nn.Parameter(torch.Tensor(channels, self.num_grade_elements)) if grade == 2: - self.grade_weights._manifold = "spin" + tag_manifold(self.grade_weights, MANIFOLD_SPIN) # Versor cache for eval mode self._cached_V_left = None diff --git a/layers/primitives/rotor_gadget.py b/layers/primitives/rotor_gadget.py index 708aeea..bd283ba 100644 --- a/layers/primitives/rotor_gadget.py +++ b/layers/primitives/rotor_gadget.py @@ -14,6 +14,7 @@ import torch import torch.nn as nn +from core.foundation.manifold import MANIFOLD_SPIN, tag_manifold from core.foundation.module import CliffordModule from core.foundation.validation import check_channels, check_multivector from core.runtime.algebra import CliffordAlgebra @@ -89,10 +90,10 @@ def __init__( # Rotor parameters: bivector coefficients for exponential map # Left rotors: [num_rotor_pairs, num_bivectors] self.bivector_left = nn.Parameter(torch.randn(num_rotor_pairs, self.num_bivectors) * 0.1) - self.bivector_left._manifold = "spin" + tag_manifold(self.bivector_left, MANIFOLD_SPIN) # Right rotors: [num_rotor_pairs, num_bivectors] self.bivector_right = nn.Parameter(torch.randn(num_rotor_pairs, self.num_bivectors) * 0.1) - self.bivector_right._manifold = "spin" + tag_manifold(self.bivector_right, MANIFOLD_SPIN) # Channel routing: block diagonal partitioning (paper style) # Each rotor pair processes a subset of input channels diff --git a/optimizers/__init__.py b/optimizers/__init__.py index 4524aa9..2eea3d6 100644 --- a/optimizers/__init__.py +++ b/optimizers/__init__.py @@ -12,6 +12,7 @@ RiemannianAdam, exponential_retraction, group_parameters_by_manifold, + make_riemannian_optimizer, project_to_tangent_space, tag_manifold, ) @@ -23,6 +24,7 @@ "exponential_retraction", "tag_manifold", "group_parameters_by_manifold", + "make_riemannian_optimizer", "MANIFOLD_SPIN", "MANIFOLD_SPHERE", "MANIFOLD_EUCLIDEAN", diff --git a/optimizers/riemannian.py b/optimizers/riemannian.py index 474db4a..932ed78 100644 --- a/optimizers/riemannian.py +++ b/optimizers/riemannian.py @@ -24,31 +24,14 @@ import torch.nn as nn from torch.optim import Optimizer -MANIFOLD_SPIN = "spin" -MANIFOLD_SPHERE = "sphere" -MANIFOLD_EUCLIDEAN = "euclidean" - -_VALID_MANIFOLDS = {MANIFOLD_SPIN, MANIFOLD_SPHERE, MANIFOLD_EUCLIDEAN} - - -def tag_manifold(param: nn.Parameter, manifold: str) -> nn.Parameter: - """Tag a parameter with its Riemannian manifold type. - - Layers should call this (or set ``param._manifold`` directly) in their - ``__init__`` so that :meth:`RiemannianAdam.from_model` can auto-group - parameters for correct per-manifold retraction. - - Args: - param: The parameter to tag. - manifold: One of ``'spin'``, ``'sphere'``, ``'euclidean'``. - - Returns: - The same parameter (for chaining). - """ - if manifold not in _VALID_MANIFOLDS: - raise ValueError(f"Unknown manifold '{manifold}'. Must be one of {_VALID_MANIFOLDS}") - param._manifold = manifold - return param +from core.foundation.manifold import ( + MANIFOLD_EUCLIDEAN, + MANIFOLD_ORDER, + MANIFOLD_SPHERE, + MANIFOLD_SPIN, + tag_manifold, + validate_manifold, +) def group_parameters_by_manifold( @@ -71,11 +54,50 @@ def group_parameters_by_manifold( MANIFOLD_EUCLIDEAN: [], } for p in model.parameters(): - manifold = getattr(p, "_manifold", MANIFOLD_EUCLIDEAN) + manifold = validate_manifold(getattr(p, "_manifold", MANIFOLD_EUCLIDEAN)) groups[manifold].append(p) return groups +def _parameter_groups_for_model(model: nn.Module) -> list[dict]: + grouped = group_parameters_by_manifold(model) + param_groups = [] + for manifold in MANIFOLD_ORDER: + params = grouped[manifold] + if params: + param_groups.append({"params": params, "manifold": manifold}) + if not param_groups: + raise ValueError("Model has no parameters") + return param_groups + + +def make_riemannian_optimizer( + model: nn.Module, + algebra, + *, + optimizer: str = "adam", + **kwargs, +) -> Optimizer: + """Create a manifold-aware optimizer from a tagged model. + + Args: + model: Model whose parameters may be tagged with ``_manifold``. + algebra: Clifford algebra instance used by the optimizer. + optimizer: ``"adam"``/``"riemannian_adam"`` or + ``"sgd"``/``"exponential_sgd"``. + **kwargs: Optimizer-specific keyword arguments. + + Returns: + ``RiemannianAdam`` or ``ExponentialSGD`` with per-manifold groups. + """ + key = optimizer.lower().replace("-", "_") + if key in {"adam", "riemannian_adam"}: + return RiemannianAdam.from_model(model, algebra=algebra, **kwargs) + if key in {"sgd", "exponential_sgd"}: + return ExponentialSGD.from_model(model, algebra=algebra, **kwargs) + raise ValueError("optimizer must be one of 'adam', 'riemannian_adam', 'sgd', or 'exponential_sgd'") + + class ExponentialSGD(Optimizer): """SGD with exponential map retraction for rotor parameters. @@ -154,14 +176,7 @@ def from_model( Returns: ExponentialSGD instance with per-manifold parameter groups. """ - grouped = group_parameters_by_manifold(model) - param_groups = [] - for manifold in (MANIFOLD_SPIN, MANIFOLD_SPHERE, MANIFOLD_EUCLIDEAN): - params = grouped[manifold] - if params: - param_groups.append({"params": params, "manifold": manifold}) - if not param_groups: - raise ValueError("Model has no parameters") + param_groups = _parameter_groups_for_model(model) return cls(param_groups, lr=lr, momentum=momentum, algebra=algebra, max_bivector_norm=max_bivector_norm) @torch.no_grad() @@ -294,14 +309,7 @@ def from_model( Returns: RiemannianAdam instance with per-manifold parameter groups. """ - grouped = group_parameters_by_manifold(model) - param_groups = [] - for manifold in (MANIFOLD_SPIN, MANIFOLD_SPHERE, MANIFOLD_EUCLIDEAN): - params = grouped[manifold] - if params: - param_groups.append({"params": params, "manifold": manifold}) - if not param_groups: - raise ValueError("Model has no parameters") + param_groups = _parameter_groups_for_model(model) return cls(param_groups, lr=lr, betas=betas, eps=eps, algebra=algebra, max_bivector_norm=max_bivector_norm) @torch.no_grad() diff --git a/tests/test_functional_products.py b/tests/test_functional_products.py new file mode 100644 index 0000000..e2e5dbb --- /dev/null +++ b/tests/test_functional_products.py @@ -0,0 +1,60 @@ +import pytest +import torch + +from functional import ( + anti_commutator, + commutator, + geometric_product, + grade_projection, + inner_product, + product, + reverse, + wedge, +) + +pytestmark = pytest.mark.unit + + +def test_functional_products_match_algebra_dense(algebra_3d): + left = torch.randn(3, algebra_3d.dim) + right = torch.randn(3, algebra_3d.dim) + + assert torch.allclose(geometric_product(algebra_3d, left, right), algebra_3d.geometric_product(left, right)) + assert torch.allclose(wedge(algebra_3d, left, right), algebra_3d.wedge(left, right)) + assert torch.allclose(inner_product(algebra_3d, left, right), algebra_3d.inner_product(left, right)) + assert torch.allclose(commutator(algebra_3d, left, right), algebra_3d.commutator(left, right)) + assert torch.allclose(anti_commutator(algebra_3d, left, right), algebra_3d.anti_commutator(left, right)) + + +def test_functional_projected_product_compact_output(algebra_3d): + left = algebra_3d.embed_vector(torch.randn(4, algebra_3d.n)) + right = algebra_3d.embed_vector(torch.randn(4, algebra_3d.n)) + layout = algebra_3d.layout((2,)) + + actual = wedge( + algebra_3d, + left, + right, + left_grades=(1,), + right_grades=(1,), + output_grades=(2,), + compact_output=True, + ) + expected = layout.compact(algebra_3d.wedge(left, right)) + + assert actual.shape == (4, layout.dim) + assert torch.allclose(actual, expected) + + +def test_functional_unary_helpers(algebra_3d): + values = torch.randn(2, algebra_3d.dim) + + assert torch.allclose(reverse(algebra_3d, values), algebra_3d.reverse(values)) + assert torch.allclose(grade_projection(algebra_3d, values, 1), algebra_3d.grade_projection(values, 1)) + + +def test_functional_product_rejects_unknown_op(algebra_3d): + values = torch.randn(2, algebra_3d.dim) + + with pytest.raises(ValueError, match="Unsupported product op"): + product(algebra_3d, values, values, op="unknown") diff --git a/tests/test_riemannian_optimizer.py b/tests/test_riemannian_optimizer.py index 5c56bd3..6fe3903 100644 --- a/tests/test_riemannian_optimizer.py +++ b/tests/test_riemannian_optimizer.py @@ -23,6 +23,7 @@ ExponentialSGD, RiemannianAdam, group_parameters_by_manifold, + make_riemannian_optimizer, tag_manifold, ) @@ -556,18 +557,18 @@ def test_manifold_tagging(algebra_3d): from layers.primitives.reflection import ReflectionLayer rotor = RotorLayer(algebra_3d, channels=4) - assert getattr(rotor.bivector_weights, "_manifold", None) == "spin" + assert getattr(rotor.bivector_weights, "_manifold", None) == MANIFOLD_SPIN reflection = ReflectionLayer(algebra_3d, channels=4) - assert getattr(reflection.vector_weights, "_manifold", None) == "sphere" + assert getattr(reflection.vector_weights, "_manifold", None) == MANIFOLD_SPHERE multi = MultiRotorLayer(algebra_3d, channels=4, num_rotors=2) - assert getattr(multi.rotor_bivectors, "_manifold", None) == "spin" + assert getattr(multi.rotor_bivectors, "_manifold", None) == MANIFOLD_SPIN assert not hasattr(multi.weights, "_manifold") # Euclidean, untagged gadget = RotorGadget(algebra_3d, in_channels=4, out_channels=8) - assert getattr(gadget.bivector_left, "_manifold", None) == "spin" - assert getattr(gadget.bivector_right, "_manifold", None) == "spin" + assert getattr(gadget.bivector_left, "_manifold", None) == MANIFOLD_SPIN + assert getattr(gadget.bivector_right, "_manifold", None) == MANIFOLD_SPIN def test_tag_manifold_helper(): @@ -584,6 +585,17 @@ def test_tag_manifold_helper(): tag_manifold(p, "invalid") +def test_group_parameters_rejects_unknown_manifold(): + class BadModel(nn.Module): + def __init__(self): + super().__init__() + self.weight = nn.Parameter(torch.randn(2, 3)) + self.weight._manifold = "bad" + + with pytest.raises(ValueError, match="Unknown manifold"): + group_parameters_by_manifold(BadModel()) + + def test_from_model_groups(algebra_3d): """Verify from_model creates separate groups per manifold.""" from layers.primitives.reflection import ReflectionLayer @@ -612,6 +624,19 @@ def forward(self, x): assert "euclidean" in manifolds +def test_make_riemannian_optimizer_factory(algebra_3d): + layer = RotorLayer(algebra_3d, channels=4) + + adam = make_riemannian_optimizer(layer, algebra_3d, optimizer="adam", lr=0.001) + sgd = make_riemannian_optimizer(layer, algebra_3d, optimizer="exponential_sgd", lr=0.01) + + assert isinstance(adam, RiemannianAdam) + assert isinstance(sgd, ExponentialSGD) + + with pytest.raises(ValueError, match="optimizer must be"): + make_riemannian_optimizer(layer, algebra_3d, optimizer="rmsprop") + + def test_sphere_retraction(algebra_3d): """Verify sphere-tagged params are projected to unit sphere after step.""" from layers.primitives.reflection import ReflectionLayer From e9fd0a251ba6c74651bab07b2d75b193bcbd8c31 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Fri, 15 May 2026 10:26:06 +0900 Subject: [PATCH 43/45] test: organize framework pipeline coverage --- tests/test_framework_pipeline.py | 140 +++++++++++++++++++++++++++++++ tests/test_layers.py | 4 +- 2 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 tests/test_framework_pipeline.py diff --git a/tests/test_framework_pipeline.py b/tests/test_framework_pipeline.py new file mode 100644 index 0000000..a566a98 --- /dev/null +++ b/tests/test_framework_pipeline.py @@ -0,0 +1,140 @@ +import pytest +import torch +import torch.nn as nn + +from core.planning import PlanningLimits +from core.runtime.algebra import CliffordAlgebra +from core.runtime.context import AlgebraContext +from layers import ProductLayer, WedgeLayer +from optimizers import make_riemannian_optimizer + +pytestmark = pytest.mark.unit + + +def test_product_layer_dense_matches_algebra(algebra_3d): + left = torch.randn(4, 5, algebra_3d.dim) + right = torch.randn(4, 5, algebra_3d.dim) + layer = ProductLayer(algebra_3d) + + actual = layer(left, right) + expected = algebra_3d.geometric_product(left, right) + + assert torch.allclose(actual, expected) + + +def test_wedge_layer_declared_grades_match_planned_algebra(algebra_3d): + left = algebra_3d.embed_vector(torch.randn(4, 5, algebra_3d.n)) + right = algebra_3d.embed_vector(torch.randn(4, 5, algebra_3d.n)) + layer = WedgeLayer( + algebra_3d, + left_grades=(1,), + right_grades=(1,), + output_grades=(2,), + ) + + actual = layer(left, right) + expected = algebra_3d.wedge(left, right, left_grades=(1,), right_grades=(1,), output_grades=(2,)) + + assert torch.allclose(actual, expected) + + +def test_product_layer_pairwise_compact_widths_match_dense_reference(): + context = AlgebraContext(p=5, q=0, device="cpu") + dense = CliffordAlgebra(p=5, q=0, device="cpu") + left_layout = context.layout((2,)) + right_layout = context.layout((1,)) + output_layout = context.layout((3,)) + + left = torch.randn(2, 3, left_layout.dim) + right = torch.randn(2, 4, right_layout.dim) + layer = WedgeLayer( + context, + left_grades=(2,), + right_grades=(1,), + output_grades=(3,), + left_compact=True, + right_compact=True, + compact_output=True, + pairwise=True, + ) + + actual = layer(left, right) + expected_dense = dense.wedge( + left_layout.dense(left).unsqueeze(2), + right_layout.dense(right).unsqueeze(1), + left_grades=(2,), + right_grades=(1,), + output_grades=(3,), + ) + expected = output_layout.compact(expected_dense) + + assert actual.shape == (2, 3, 4, output_layout.dim) + assert torch.allclose(actual, expected) + + +def test_compact_layer_pipeline_trains_with_riemannian_optimizer_factory(): + context = AlgebraContext(p=6, q=0, device="cpu") + vector_layout = context.layout((1,)) + trivector_layout = context.layout((3,)) + + class CompactPipeline(nn.Module): + def __init__(self): + super().__init__() + self.wedge_vectors = WedgeLayer( + context, + left_grades=(1,), + right_grades=(1,), + output_grades=(2,), + compact_output=True, + ) + self.wedge_trivector = WedgeLayer( + context, + left_grades=(2,), + right_grades=(1,), + output_grades=(3,), + left_compact=True, + right_compact=True, + compact_output=True, + ) + self.scale = nn.Parameter(torch.ones(())) + + def forward(self, left_vector, right_vector, third_vector): + bivector = self.wedge_vectors(left_vector, right_vector) + return self.scale * self.wedge_trivector(bivector, third_vector) + + dense_left = context.embed_vector(torch.randn(8, context.n)) + dense_right = context.embed_vector(torch.randn(8, context.n)) + compact_third = vector_layout.compact(context.embed_vector(torch.randn(8, context.n))) + model = CompactPipeline() + optimizer = make_riemannian_optimizer(model, context, optimizer="adam", lr=0.01) + + output = model(dense_left, dense_right, compact_third) + assert output.shape == (8, trivector_layout.dim) + assert len(context.planner._product_executors) == 2 + + loss = output.square().mean() + loss.backward() + optimizer.step() + + assert model.scale.grad is not None + assert torch.isfinite(model.scale).all() + + +def test_product_layer_uses_context_planning_limits(): + limits = PlanningLimits(warn_lanes=32, max_lanes=512, warn_pairs=32, max_pairs=64) + context = AlgebraContext(p=16, q=0, device="cpu", planning_limits=limits) + vector_layout = context.layout((1,)) + left = torch.zeros(1, vector_layout.dim) + right = torch.zeros(1, vector_layout.dim) + layer = ProductLayer( + context, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + left_compact=True, + right_compact=True, + compact_output=True, + ) + + with pytest.raises(ValueError, match="basis interactions"): + layer(left, right) diff --git a/tests/test_layers.py b/tests/test_layers.py index 3870628..e701727 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -9,9 +9,7 @@ import torch from core.runtime.algebra import CliffordAlgebra -from core.runtime.decomposition import ExpPolicy -from layers import CliffordLinear, MultiRotorLayer, RotorLayer -from layers.primitives.reflection import ReflectionLayer +from layers import CliffordLinear, MultiRotorLayer, ReflectionLayer, RotorLayer pytestmark = pytest.mark.unit From 8453f2f71192566116a5fc378e27e7cf1d6a788f Mon Sep 17 00:00:00 2001 From: Concode0 Date: Fri, 15 May 2026 10:29:10 +0900 Subject: [PATCH 44/45] bench: add framework pipeline benchmark --- benchmarks/benchmark_framework_pipeline.py | 348 +++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 benchmarks/benchmark_framework_pipeline.py diff --git a/benchmarks/benchmark_framework_pipeline.py b/benchmarks/benchmark_framework_pipeline.py new file mode 100644 index 0000000..3b54e26 --- /dev/null +++ b/benchmarks/benchmark_framework_pipeline.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +"""Benchmark framework-level dense, compact, pairwise, and layer pipeline paths. + +The core benchmark suite measures many algebra kernels in detail. This script is +smaller and pipeline-oriented: it times the execution paths users hit when they +compose product layers, functional products, compact layouts, and planned +contexts. + +Examples: + uv run python benchmarks/benchmark_framework_pipeline.py --quick + uv run python benchmarks/benchmark_framework_pipeline.py --device cpu --n 8 --batch-size 1024 +""" + +from __future__ import annotations + +import argparse +import csv +import statistics +import sys +import time +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Callable + +import torch +import torch.nn as nn + +_REPO_ROOT = Path(__file__).resolve().parent.parent +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from core.foundation.device import FLOAT_DTYPES, resolve_device # noqa: E402 +from core.runtime.algebra import CliffordAlgebra # noqa: E402 +from core.runtime.context import AlgebraContext # noqa: E402 +from layers import ProductLayer, WedgeLayer # noqa: E402 + + +@dataclass(frozen=True) +class BenchmarkCase: + name: str + host: str + n: int + op: str + storage: str + batch_size: int + left_lanes: int + right_lanes: int + output_lanes: int + pairwise: bool + fn: Callable[[], torch.Tensor] + + +def _sync(device: str) -> None: + if device == "mps": + torch.mps.synchronize() + elif device.startswith("cuda"): + torch.cuda.synchronize() + + +def _time_case(case: BenchmarkCase, *, warmups: int, repeats: int, device: str) -> dict: + with torch.no_grad(): + start = time.perf_counter() + first_output = case.fn() + _sync(device) + first_ms = (time.perf_counter() - start) * 1000.0 + + for _ in range(warmups): + case.fn() + _sync(device) + + samples = [] + for _ in range(repeats): + start = time.perf_counter() + output = case.fn() + _sync(device) + samples.append((time.perf_counter() - start) * 1000.0) + + output_lanes = int(first_output.shape[-1]) + if output_lanes != case.output_lanes: + raise RuntimeError(f"{case.name} expected {case.output_lanes} output lanes, got {output_lanes}") + + median_ms = statistics.median(samples) + mean_ms = statistics.fmean(samples) + return { + "name": case.name, + "host": case.host, + "n": case.n, + "op": case.op, + "storage": case.storage, + "batch_size": case.batch_size, + "left_lanes": case.left_lanes, + "right_lanes": case.right_lanes, + "output_lanes": case.output_lanes, + "pairwise": case.pairwise, + "first_call_ms": first_ms, + "median_ms": median_ms, + "mean_ms": mean_ms, + "min_ms": min(samples), + "max_ms": max(samples), + "repeats": repeats, + "items_per_sec": case.batch_size * 1000.0 / median_ms if median_ms > 0 else float("inf"), + } + + +def _dense_product_case(args, dtype: torch.dtype, device: str) -> BenchmarkCase: + algebra = CliffordAlgebra(args.n, 0, device=device, dtype=dtype) + left = algebra.embed_vector(torch.randn(args.batch_size, algebra.n, device=device, dtype=dtype)) + right = algebra.embed_vector(torch.randn(args.batch_size, algebra.n, device=device, dtype=dtype)) + + return BenchmarkCase( + name="dense_full_gp_1x1", + host="CliffordAlgebra", + n=algebra.n, + op="gp", + storage="dense", + batch_size=args.batch_size, + left_lanes=algebra.dim, + right_lanes=algebra.dim, + output_lanes=algebra.dim, + pairwise=False, + fn=lambda: algebra.geometric_product(left, right), + ) + + +def _compact_product_case(args, dtype: torch.dtype, device: str) -> BenchmarkCase: + algebra = CliffordAlgebra(args.n, 0, device=device, dtype=dtype) + layout_1 = algebra.layout((1,)) + layout_02 = algebra.layout((0, 2)) + left = layout_1.compact(algebra.embed_vector(torch.randn(args.batch_size, algebra.n, device=device, dtype=dtype))) + right = layout_1.compact(algebra.embed_vector(torch.randn(args.batch_size, algebra.n, device=device, dtype=dtype))) + + layer = ProductLayer( + algebra, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + left_compact=True, + right_compact=True, + compact_output=True, + ) + + return BenchmarkCase( + name="compact_layer_gp_1x1_to_02", + host="CliffordAlgebra", + n=algebra.n, + op="gp", + storage="compact", + batch_size=args.batch_size, + left_lanes=layout_1.dim, + right_lanes=layout_1.dim, + output_lanes=layout_02.dim, + pairwise=False, + fn=lambda: layer(left, right), + ) + + +def _context_compact_case(args, dtype: torch.dtype, device: str) -> BenchmarkCase: + context = AlgebraContext(args.context_n, 0, device=device, dtype=dtype) + layout_1 = context.layout((1,)) + layout_02 = context.layout((0, 2)) + left = torch.randn(args.batch_size, layout_1.dim, device=device, dtype=dtype) + right = torch.randn(args.batch_size, layout_1.dim, device=device, dtype=dtype) + + return BenchmarkCase( + name="context_compact_gp_1x1_to_02", + host="AlgebraContext", + n=context.n, + op="gp", + storage="compact", + batch_size=args.batch_size, + left_lanes=layout_1.dim, + right_lanes=layout_1.dim, + output_lanes=layout_02.dim, + pairwise=False, + fn=lambda: context.geometric_product( + left, + right, + left_grades=(1,), + right_grades=(1,), + output_grades=(0, 2), + left_compact=True, + right_compact=True, + compact_output=True, + ), + ) + + +def _pairwise_context_case(args, dtype: torch.dtype, device: str) -> BenchmarkCase: + context = AlgebraContext(args.context_n, 0, device=device, dtype=dtype) + layout_2 = context.layout((2,)) + layout_1 = context.layout((1,)) + layout_3 = context.layout((3,)) + left = torch.randn(args.batch_size, args.left_items, layout_2.dim, device=device, dtype=dtype) + right = torch.randn(args.batch_size, args.right_items, layout_1.dim, device=device, dtype=dtype) + + return BenchmarkCase( + name="context_pairwise_wedge_2x1_to_3", + host="AlgebraContext", + n=context.n, + op="wedge", + storage="compact_pairwise", + batch_size=args.batch_size * args.left_items * args.right_items, + left_lanes=layout_2.dim, + right_lanes=layout_1.dim, + output_lanes=layout_3.dim, + pairwise=True, + fn=lambda: context.wedge( + left, + right, + left_grades=(2,), + right_grades=(1,), + output_grades=(3,), + left_compact=True, + right_compact=True, + compact_output=True, + pairwise=True, + ), + ) + + +def _layer_pipeline_case(args, dtype: torch.dtype, device: str) -> BenchmarkCase: + context = AlgebraContext(args.context_n, 0, device=device, dtype=dtype) + layout_1 = context.layout((1,)) + layout_3 = context.layout((3,)) + + class Pipeline(nn.Module): + def __init__(self): + super().__init__() + self.wedge_vectors = WedgeLayer( + context, + left_grades=(1,), + right_grades=(1,), + output_grades=(2,), + compact_output=True, + ) + self.wedge_trivector = WedgeLayer( + context, + left_grades=(2,), + right_grades=(1,), + output_grades=(3,), + left_compact=True, + right_compact=True, + compact_output=True, + ) + + def forward(self, left, right, third): + bivector = self.wedge_vectors(left, right) + return self.wedge_trivector(bivector, third) + + model = Pipeline().to(device=device) + left = context.embed_vector(torch.randn(args.batch_size, context.n, device=device, dtype=dtype)) + right = context.embed_vector(torch.randn(args.batch_size, context.n, device=device, dtype=dtype)) + third = layout_1.compact(context.embed_vector(torch.randn(args.batch_size, context.n, device=device, dtype=dtype))) + + return BenchmarkCase( + name="layer_pipeline_wedge_wedge", + host="AlgebraContext", + n=context.n, + op="wedge_pipeline", + storage="dense_to_compact", + batch_size=args.batch_size, + left_lanes=context.dim, + right_lanes=layout_1.dim, + output_lanes=layout_3.dim, + pairwise=False, + fn=lambda: model(left, right, third), + ) + + +def _write_results(rows: list[dict], output_dir: Path) -> None: + output_dir.mkdir(parents=True, exist_ok=True) + csv_path = output_dir / "framework_pipeline.csv" + with csv_path.open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) + + summary_path = output_dir / "summary.md" + with summary_path.open("w") as handle: + handle.write("# Framework Pipeline Benchmark\n\n") + handle.write("| case | host | storage | n | median ms | items/sec |\n") + handle.write("| --- | --- | --- | ---: | ---: | ---: |\n") + for row in rows: + handle.write( + f"| {row['name']} | {row['host']} | {row['storage']} | {row['n']} | " + f"{row['median_ms']:.4f} | {row['items_per_sec']:.2f} |\n" + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--device", default="cpu", help="cpu, cuda, cuda:0, mps, or auto") + parser.add_argument("--dtype", default="float32", choices=sorted(FLOAT_DTYPES)) + parser.add_argument("--n", type=int, default=6, help="Dense algebra dimension for dense-vs-compact comparison") + parser.add_argument("--context-n", type=int, default=12, help="Planned context dimension") + parser.add_argument("--batch-size", type=int, default=512) + parser.add_argument("--left-items", type=int, default=16) + parser.add_argument("--right-items", type=int, default=16) + parser.add_argument("--warmups", type=int, default=5) + parser.add_argument("--repeats", type=int, default=20) + parser.add_argument("--quick", action="store_true", help="Use smaller dimensions and repeat counts") + parser.add_argument("--no-save", action="store_true", help="Print rows without writing benchmark artifacts") + parser.add_argument("--output-dir", type=Path, default=None) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + if args.quick: + args.n = min(args.n, 5) + args.context_n = min(args.context_n, 8) + args.batch_size = min(args.batch_size, 64) + args.left_items = min(args.left_items, 8) + args.right_items = min(args.right_items, 8) + args.warmups = min(args.warmups, 2) + args.repeats = min(args.repeats, 5) + + device = resolve_device(args.device) if args.device == "auto" else args.device + dtype = FLOAT_DTYPES[args.dtype] + torch.manual_seed(2026) + + cases = [ + _dense_product_case(args, dtype, device), + _compact_product_case(args, dtype, device), + _context_compact_case(args, dtype, device), + _pairwise_context_case(args, dtype, device), + _layer_pipeline_case(args, dtype, device), + ] + rows = [_time_case(case, warmups=args.warmups, repeats=args.repeats, device=device) for case in cases] + + for row in rows: + print( + f"{row['name']}: median={row['median_ms']:.4f} ms, " + f"first={row['first_call_ms']:.4f} ms, items/sec={row['items_per_sec']:.2f}" + ) + + if not args.no_save: + output_dir = args.output_dir + if output_dir is None: + stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_dir = _REPO_ROOT / "benchmarks" / "results" / f"framework_pipeline_{stamp}" + _write_results(rows, output_dir) + print(f"Wrote benchmark artifacts to {output_dir}") + + +if __name__ == "__main__": + main() From 6b7ee64d9269e5e14448c530509c6d17d87d6e48 Mon Sep 17 00:00:00 2001 From: Concode0 Date: Fri, 15 May 2026 10:32:15 +0900 Subject: [PATCH 45/45] docs: document framework architecture --- docs/Architecture.md | 307 +++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 308 insertions(+) create mode 100644 docs/Architecture.md diff --git a/docs/Architecture.md b/docs/Architecture.md new file mode 100644 index 0000000..4ce674f --- /dev/null +++ b/docs/Architecture.md @@ -0,0 +1,307 @@ +# Architecture + +Versor is organized around one rule: algebraic identity, tensor layout, runtime +execution, and training behavior should stay separable. The core owns the +mathematics and the execution contracts; layers, functionals, and optimizers +expose those contracts in forms that are convenient for actual model code. + +## Framework Layers + +```mermaid +flowchart TB + User["User code
layers, functionals, optimizers"] + + subgraph Framework["Versor framework"] + Layers["layers/
nn.Module composition"] + Functional["functional/
stateless algebra helpers, losses, activations"] + Optimizers["optimizers/
manifold-aware parameter updates"] + end + + subgraph Core["core/"] + Foundation["foundation/
basis rules, layouts, validation, manifold tags"] + Planning["planning/
grade requests, lane limits, executors"] + Runtime["runtime/
dense algebra, planned context, multivectors"] + Config["config.py
algebra factory"] + Analysis["analysis/
metric/geodesic/symmetry tooling"] + end + + User --> Layers + User --> Functional + User --> Optimizers + Layers --> Runtime + Layers --> Functional + Layers --> Foundation + Functional --> Runtime + Optimizers --> Foundation + Optimizers --> Runtime + Config --> Runtime + Runtime --> Planning + Runtime --> Foundation + Planning --> Foundation + Analysis --> Runtime +``` + +`core/` is the authority for algebra and execution. `layers/` should be thin +module wrappers around core operations. `functional/` should expose stateless +helpers and losses without duplicating runtime logic. `optimizers/` should read +parameter manifold tags and apply updates; it should not decide product plans. + +## Algebra Host Selection + +```mermaid +flowchart LR + Factory["make_algebra(p, q, r, kernel)"] + Dense["CliffordAlgebra
dense tables, full dim = 2^n"] + Context["AlgebraContext
planned compact execution"] + Spec["AlgebraSpec
signature and dimension metadata"] + Planner["GradePlanner
cached layouts and executors"] + + Factory -->|"small n or explicit dense"| Dense + Factory -->|"large n or explicit context"| Context + Dense --> Spec + Context --> Spec + Dense --> Planner + Context --> Planner + Planner --> Spec +``` + +`CliffordAlgebra` owns dense Cayley-table buffers and fast full-layout kernels. +`AlgebraContext` exposes the same high-level product API but routes products +through static grade planning by default. Both hosts share the runtime facade, so +declared products use the same layout, cost, and executor path. + +## Tensor And Layout Flow + +```mermaid +flowchart LR + Grades["Grade set
for example (1, 2)"] + Basis["basis_index_tuple_for_grades(n, grades)"] + Layout["GradeLayout
grades, dense basis indices, lane count"] + Dense["Dense multivector
last dim = 2^n"] + Compact["Compact values
last dim = layout.dim"] + Materialized["Dense materialization
zeros outside layout"] + + Grades --> Basis --> Layout + Dense -->|"layout.compact"| Compact + Compact -->|"layout.dense"| Materialized + Layout --> Compact + Layout --> Materialized +``` + +A compact tensor is not just a shorter tensor. It is coefficient values plus a +`GradeLayout` identity that defines which dense basis blades the lanes +represent. Raw tensors do not carry that identity, so framework pipeline code +must declare `*_grades`, pass layouts, or use `Multivector` wrappers when layout +metadata needs to travel with values. + +## Product Execution Flow + +```mermaid +flowchart TB + A["A tensor
dense [*, 2^n] or compact [*, left_lanes]"] + B["B tensor
dense [*, 2^n] or compact [*, right_lanes]"] + Metadata["Declared metadata
left_grades, right_grades, output_grades
left_layout, right_layout, compact flags"] + + Request["ProductRequest
normalized op, layouts, dtype, device"] + Cost["PlanningLimits and cost checks
lanes, output width, estimated pairs"] + Tree["GradePlanTree
homogeneous route nodes"] + Plan["GradeProductPlan
left lanes, right lanes, output lanes, coefficients"] + Exec["GradeProductExecutor
forward, forward_compact, forward_pairwise_compact"] + Values["Compact output values
[*, output_lanes]"] + DenseOut["Dense materialized output
[*, 2^n]"] + + A --> Request + B --> Request + Metadata --> Request + Request --> Cost + Cost --> Tree + Tree --> Plan + Plan --> Exec + A --> Exec + B --> Exec + Exec --> Values + Values -->|"compact_output=True"| CompactReturn["Return compact values"] + Values -->|"compact_output=False"| DenseOut +``` + +Planning uses static grade metadata and tensor shapes. It does not inspect +runtime tensor values. This keeps compiled paths stable and avoids +data-dependent symbolic shape extraction. + +## Layer Pipeline Contract + +```mermaid +sequenceDiagram + participant Model as Model/Layers + participant ProductLayer as ProductLayer + participant Algebra as Algebra Runtime API + participant Planner as GradePlanner + participant Executor as GradeProductExecutor + participant Optimizer as Optimizer + + Model->>ProductLayer: forward(left, right) + ProductLayer->>Algebra: projected_product(...grades, compact flags) + Algebra->>Planner: product_request(...) + Planner->>Executor: cached or newly built executor + Algebra->>Executor: forward / forward_compact / forward_pairwise_compact + Executor-->>Model: dense or compact tensor + Model->>Optimizer: loss.backward(); step() + Optimizer->>Optimizer: update tagged parameters +``` + +Optimizers do not run planning. They update parameters after the forward pass. +Planning happens when the model calls algebra operations through direct runtime +APIs, functional helpers, `ProductLayer`, or `Multivector` methods. + +## Operator Rules + +```mermaid +flowchart TB + Route["Route
left grade r, right grade s"] + GP["geometric product
grades abs(r-s), abs(r-s)+2, ..."] + Wedge["wedge / exterior
grade r+s only"] + Symmetric["inner route
symmetric parity grades"] + Comm["commutator
odd swap-parity grades"] + Anti["anti-commutator
even swap-parity grades, doubled coefficient"] + Pair["Basis pair
left index i, right index j"] + Output["output index = i XOR j"] + Nonzero["operation_may_be_nonzero
wedge requires no shared basis bit
null self-overlap is zero"] + Coeff["operation_coefficient
metric sign and op scale"] + + Route --> GP + Route --> Wedge + Route --> Symmetric + Route --> Comm + Route --> Anti + GP --> Pair + Wedge --> Pair + Symmetric --> Pair + Comm --> Pair + Anti --> Pair + Pair --> Output + Output --> Nonzero + Nonzero --> Coeff +``` + +The wedge implementation is the exterior product. For homogeneous inputs: + +```text +A_r ^ B_s = _{r+s} +``` + +For vectors this coincides with `(AB - BA) / 2`, but higher-grade wedge routes +follow the grade-sum exterior definition. + +## Dense Runtime + +```mermaid +flowchart TB + Init["CliffordAlgebra init"] + Cayley["Cayley indices and signs
basis_product over all pairs"] + GP["gp_signs
single-pass geometric product"] + Wedge["wedge_gp_signs
grade(output) = grade(left) + grade(right)"] + Inner["inner_gp_signs
symmetric sign table"] + Comm["comm_gp_signs and anti_comm_gp_signs"] + Contractions["left and right contraction helpers"] + Product["Runtime product call"] + Matmul["gather B by Cayley indices
multiply signs
batched matmul over lanes"] + + Init --> Cayley + Cayley --> GP + Cayley --> Wedge + Cayley --> Inner + Cayley --> Comm + Cayley --> Contractions + GP --> Product + Wedge --> Product + Inner --> Product + Comm --> Product + Product --> Matmul +``` + +Dense products are table-driven. The input tensors carry coefficients; the +precomputed tables carry basis multiplication structure. + +## Planning Limits + +`PlanningLimits` centralizes static guardrails for compact planning: + +```python +from core.planning import PlanningLimits +from core.runtime.context import AlgebraContext + +limits = PlanningLimits(max_lanes=8192, max_pairs=16_000_000) +algebra = AlgebraContext(32, 0, device="cpu", planning_limits=limits) +``` + +`max_lanes` protects compact tensor width. `max_pairs` protects the +gather/reduce interaction count generated by product plans. Dense algebra hosts +and planned contexts both accept `planning_limits`, so the same policy object can +be used across framework construction. + +## Analysis Flow + +```mermaid +flowchart LR + Data["Input data
vectors or multivectors"] + Algebra["Algebra host
dense or context"] + Metric["Metric and dimension analysis
norms, distances, effective dimension"] + Geodesic["GeodesicFlow
grade-1 wedge to connection bivectors"] + Symmetry["SymmetryDetector
group and continuous symmetry"] + Comm["CommutatorAnalyzer
bracket spectra and exchange structure"] + Report["Analysis result dataclasses"] + + Data --> Algebra + Algebra --> Metric + Algebra --> Geodesic + Algebra --> Symmetry + Algebra --> Comm + Metric --> Report + Geodesic --> Report + Symmetry --> Report + Comm --> Report +``` + +Analysis code should call algebra APIs rather than reconstructing basis rules. +When active grades are known, analysis should pass `left_grades`, +`right_grades`, and `output_grades` so the planner can avoid full-layout work. + +## End-To-End Product Call + +```mermaid +sequenceDiagram + participant Caller as Caller + participant Algebra as Algebra Runtime API + participant Planner as GradePlanner + participant Executor as GradeProductExecutor + participant Layout as GradeLayout + + Caller->>Algebra: wedge(A, B, left_grades, right_grades, output_grades) + Algebra->>Planner: product_request(...) + Planner->>Layout: resolve operand and output layouts + Planner->>Planner: build or fetch cached executor + Planner->>Executor: return executor + Algebra->>Executor: forward, forward_compact, or forward_pairwise_compact + Executor-->>Algebra: compact output values + Algebra->>Layout: materialize dense unless compact_output=True + Algebra-->>Caller: tensor output +``` + +The same path applies to geometric product, wedge, inner route, commutator, and +anti-commutator. The operator name changes route grades and coefficients, not +the overall tensor plumbing. + +## Framework Verification Map + +Framework-level tests are grouped by the behavior they prove: + +- Core algebra and dense kernel identities: `tests/test_core.py` +- Static grade planning and compact execution: `tests/test_grade_plan.py` +- Multivector layout-preserving wrappers: `tests/test_multivector.py` +- Layer pipeline and optimizer integration: `tests/test_framework_pipeline.py` +- Functional product helpers: `tests/test_functional_products.py` +- Optimizer manifold grouping and factories: `tests/test_riemannian_optimizer.py` + +Performance checks live in `benchmarks/`. The framework pipeline benchmark +measures dense-vs-compact products, planned contexts, pairwise compact products, +and composed layer pipelines. diff --git a/mkdocs.yml b/mkdocs.yml index 8727d0f..d126966 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,7 @@ nav: - Home: index.md - Guide: - Design Guide: design_guide.md + - Architecture: Architecture.md - Philosophy: philosophy.md - Mathematics: mathematical.md - Innovations: innovations.md