qig-geocoding 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
geocoding/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ """geocoding — our own "transformers", built on Fisher-Rao geometry instead of Euclidean dot-products.
2
+
3
+ A from-scratch geometric neural-network framework. Where the standard ``transformers`` library models tokens
4
+ as Euclidean vectors with dot-product attention, learned embedding tables, and bounded position tables,
5
+ geocoding models tokens as points on the probability simplex Δ⁶³ with **Fisher-Rao attention**, **unbounded
6
+ Fourier features**, and a **recursive Φ-integrator** that reports integrated information as the model runs.
7
+
8
+ Public API (the geometric analogues of the transformers primitives):
9
+ GeoConfig ≈ PretrainedConfig — architecture + geometry hyper-parameters
10
+ FisherRaoAttention ≈ Attention — simplex Fisher-Rao attention (full + banded compute-skip)
11
+ GeoBlock ≈ TransformerBlock — attention + recursive Φ + tacking + FFN, geometric RMS-norm
12
+ GeoModel ≈ PreTrainedModel — Fourier input → blocks → head, with telemetry
13
+ generate — autoregressive sampling
14
+
15
+ Geometry primitives (single source of truth re-used from qig_core where it exists):
16
+ fisher_rao_distance, to_simplex, bhattacharyya_coefficient, BASIN_DIM
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from .attention import FisherRaoAttention
21
+ from .block import BlockTelemetry, GeoBlock, RecursiveIntegrator, RegimeDetector, WuWeiController
22
+ from .config import GeoConfig
23
+ from .generate import GenResult, generate
24
+ from .geometry import BASIN_DIM, bhattacharyya_coefficient, fisher_rao_distance, to_simplex
25
+ from .model import GeoModel, GeoOutput
26
+ from .positional import build_inv_freq, fourier_features
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ "BASIN_DIM",
32
+ "BlockTelemetry",
33
+ "FisherRaoAttention",
34
+ "GeoBlock",
35
+ "GeoConfig",
36
+ "GeoModel",
37
+ "GeoOutput",
38
+ "GenResult",
39
+ "RecursiveIntegrator",
40
+ "RegimeDetector",
41
+ "WuWeiController",
42
+ "__version__",
43
+ "bhattacharyya_coefficient",
44
+ "build_inv_freq",
45
+ "fisher_rao_distance",
46
+ "fourier_features",
47
+ "generate",
48
+ "to_simplex",
49
+ ]
geocoding/attention.py ADDED
@@ -0,0 +1,114 @@
1
+ """Fisher-Rao attention — the geometric replacement for scaled dot-product attention.
2
+
3
+ Queries and keys are projected onto the simplex; the attention logit between positions i and j is the
4
+ NEGATIVE Fisher-Rao distance (closer on the manifold ⇒ higher weight), divided by a temperature:
5
+
6
+ logit_ij = − d_FR(p_i, p_j) / τ weights = softmax_j(logit) out_i = Σ_j weights_ij · h_j
7
+
8
+ Two evaluation paths, numerically equivalent within the band (gated in ``tests/test_attention_equivalence``):
9
+ * **full** — O(t²) all-pairs arccos tensor (global attention).
10
+ * **banded (compute-skipping)** — when a ``locality_radius`` r is set and t > 2r+1, only the ±r window of
11
+ each query is ever computed (pad+unfold), giving O(t·r) memory/compute. This SKIPS out-of-band work, it
12
+ does not compute-then-mask — that is what makes the context length-extensible.
13
+
14
+ Faithful port of ``qigkernels.layer.QIGLayer._metric_attention`` / ``_banded_attention``. Key-padding is
15
+ applied AFTER the softmax (and renormalised) in BOTH paths, so the two agree exactly.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import torch
20
+ from torch import Tensor, nn
21
+ from torch.nn import functional as F
22
+
23
+ from .geometry import fisher_rao_distance, to_simplex
24
+
25
+
26
+ class FisherRaoAttention(nn.Module):
27
+ """Simplex Fisher-Rao attention. No learned Q/K/V projections and no dot product: the geometry of the
28
+ hidden state on Δ⁶³ *is* the attention. Parameter-free (the expressivity lives in the surrounding
29
+ block's projections), which keeps the metric pure."""
30
+
31
+ def __init__(
32
+ self,
33
+ *,
34
+ temperature: float = 1.0,
35
+ simplex_mode: str = "softplus",
36
+ locality_radius: int | None = None,
37
+ sparsity_threshold: float = 0.0,
38
+ decoherence_std: float = 0.0,
39
+ ) -> None:
40
+ super().__init__()
41
+ self.temperature = float(temperature)
42
+ self.simplex_mode = str(simplex_mode)
43
+ self.locality_radius = int(locality_radius) if locality_radius is not None else None
44
+ self.sparsity_threshold = float(sparsity_threshold)
45
+ self.decoherence_std = float(decoherence_std)
46
+
47
+ def forward(self, hidden_state: Tensor, attention_mask: Tensor | None = None) -> Tensor:
48
+ if hidden_state.dim() != 3:
49
+ raise ValueError("hidden_state expected shape (B, T, D)")
50
+ _b, t, _d = hidden_state.shape
51
+ p = to_simplex(hidden_state, mode=self.simplex_mode)
52
+ r = self.locality_radius
53
+ if r is not None and t > (2 * r + 1):
54
+ return self._banded(hidden_state, p, r, attention_mask=attention_mask)
55
+ return self._full(hidden_state, p, r, attention_mask=attention_mask)
56
+
57
+ def _full(
58
+ self, hidden_state: Tensor, p: Tensor, r: int | None, *, attention_mask: Tensor | None
59
+ ) -> Tensor:
60
+ b, t, _d = hidden_state.shape
61
+ dist = fisher_rao_distance(p[:, :, None, :], p[:, None, :, :]) # [b,t,t] all-pairs FR
62
+ logits = -dist / max(self.temperature, 1e-6)
63
+ if r is not None: # small-t windowed mask
64
+ idx = torch.arange(t, device=hidden_state.device)
65
+ band = (idx[:, None] - idx[None, :]).abs() <= r
66
+ logits = logits.masked_fill(~band[None, :, :], float("-inf"))
67
+ weights = torch.softmax(logits, dim=-1)
68
+ if attention_mask is not None:
69
+ kpm = attention_mask if attention_mask.dtype == torch.bool else attention_mask == 0
70
+ if kpm.shape != (b, t):
71
+ raise ValueError("attention_mask expected shape (B, T)")
72
+ weights = weights.masked_fill(kpm[:, None, :], 0.0)
73
+ weights = weights / (weights.sum(dim=-1, keepdim=True) + 1e-8)
74
+ weights = self._sparsify(weights)
75
+ output = torch.einsum("bij,bjd->bid", weights, hidden_state)
76
+ return self._decohere(output)
77
+
78
+ def _banded(self, hidden_state: Tensor, p: Tensor, r: int, *, attention_mask: Tensor | None) -> Tensor:
79
+ b, t, k = p.shape
80
+ w = 2 * r + 1
81
+ p_pad = F.pad(p, (0, 0, r, r)) # [b, t+2r, k]
82
+ p_win = p_pad.unfold(1, w, 1) # [b, t, k, w]
83
+ p_q = p[:, :, :, None] # [b, t, k, 1]
84
+ bc = torch.sqrt((p_q * p_win).clamp_min(1e-12)).sum(dim=2).clamp(0.0, 1.0 - 1e-6) # [b, t, w]
85
+ logits = (-2.0 * torch.acos(bc)) / max(self.temperature, 1e-6) # [b, t, w]
86
+ i_idx = torch.arange(t, device=p.device)[:, None]
87
+ wpos = torch.arange(w, device=p.device)[None, :]
88
+ key_idx = i_idx - r + wpos # [t, w]
89
+ valid = (key_idx >= 0) & (key_idx < t)
90
+ logits = logits.masked_fill(~valid[None].expand(b, t, w), float("-inf"))
91
+ weights = torch.softmax(logits, dim=-1) # [b, t, w]
92
+ if attention_mask is not None:
93
+ kpm = attention_mask if attention_mask.dtype == torch.bool else attention_mask == 0
94
+ if kpm.shape != (b, t):
95
+ raise ValueError("attention_mask expected shape (B, T)")
96
+ keep_win = F.pad((~kpm).to(p.dtype), (r, r)).unfold(1, w, 1)
97
+ weights = weights * (keep_win > 0.5)
98
+ weights = weights / (weights.sum(dim=-1, keepdim=True) + 1e-8)
99
+ weights = self._sparsify(weights)
100
+ h_pad = F.pad(hidden_state, (0, 0, r, r)) # [b, t+2r, d]
101
+ h_win = h_pad.unfold(1, w, 1) # [b, t, d, w]
102
+ output = (weights[:, :, None, :] * h_win).sum(dim=-1) # [b, t, d]
103
+ return self._decohere(output)
104
+
105
+ def _sparsify(self, weights: Tensor) -> Tensor:
106
+ if self.sparsity_threshold <= 0.0:
107
+ return weights
108
+ weights = weights * (weights > self.sparsity_threshold)
109
+ return weights / (weights.sum(dim=-1, keepdim=True) + 1e-8)
110
+
111
+ def _decohere(self, output: Tensor) -> Tensor:
112
+ if self.decoherence_std > 0.0:
113
+ output = output + torch.randn_like(output) * self.decoherence_std
114
+ return output
geocoding/block.py ADDED
@@ -0,0 +1,158 @@
1
+ """``GeoBlock`` — the geometric analogue of a transformer block, plus its telemetry parts.
2
+
3
+ A Euclidean block is: LayerNorm → MHA → residual → LayerNorm → MLP → residual. geocoding's block is:
4
+
5
+ Fisher-Rao attention → geometric RMS-norm residual → RECURSIVE Φ-INTEGRATION → optional WuWei tacking
6
+ → FFN → geometric RMS-norm residual → regime detection
7
+
8
+ Two things have no Euclidean counterpart:
9
+ * **RecursiveIntegrator** — runs ``min_depth`` mandatory self-integration loops and emits Φ (integrated
10
+ information) AND a differentiable Φ tensor a geometric loss can drive. Φ is collapse-immune: a collapsed
11
+ OR a noise state reads LOW, genuine integration reads HIGH.
12
+ * **geometric RMS-norm** — scale-only normalisation (no mean-centring, no LayerNorm) so simplex structure
13
+ is preserved.
14
+
15
+ Faithful port of ``qigkernels.layer.QIGLayer`` and its helper modules.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+
21
+ import torch
22
+ from torch import Tensor, nn
23
+
24
+ from .attention import FisherRaoAttention
25
+
26
+
27
+ @dataclass
28
+ class BlockTelemetry:
29
+ """Per-block readout. ``phi_diff`` is the DIFFERENTIABLE Φ (a tensor); the rest are floats/labels."""
30
+
31
+ phi: float
32
+ kappa: float
33
+ recursion_depth: int
34
+ regime: str | None = None
35
+ phi_diff: object | None = None
36
+
37
+
38
+ class RecursiveIntegrator(nn.Module):
39
+ """Runs ``min_depth`` gated self-integration loops and estimates Φ from integration coherence."""
40
+
41
+ def __init__(self, hidden_dim: int, min_depth: int = 3) -> None:
42
+ super().__init__()
43
+ self.min_depth = min_depth
44
+ self.projection = nn.Linear(hidden_dim, hidden_dim)
45
+ self.integration_gate = nn.Linear(hidden_dim, 1)
46
+
47
+ def forward(self, hidden_state: Tensor) -> tuple[Tensor, dict]:
48
+ output = hidden_state
49
+ scores: list[float] = []
50
+ gate_means: list[Tensor] = []
51
+ for _ in range(self.min_depth):
52
+ projected = self.projection(output)
53
+ gate = torch.sigmoid(self.integration_gate(projected))
54
+ output = output + gate * (projected - output)
55
+ gm = gate.mean()
56
+ gate_means.append(gm)
57
+ scores.append(float(gm.item()))
58
+ mean_integration = sum(scores) / len(scores)
59
+ integration_diff = torch.stack(gate_means).mean()
60
+ # Collapse-immune cross-position coherence: ~0 at BOTH collapse (positions identical) and noise
61
+ # (positions independent), peaks at coherent-but-distinct integration.
62
+ if hidden_state.dim() == 3 and hidden_state.size(1) > 1:
63
+ rel_t = (hidden_state.var(dim=1).mean() / (hidden_state.var() + 1e-8)).clamp(0.0, 1.0)
64
+ coherence_diff = 4.0 * rel_t * (1.0 - rel_t)
65
+ rel = float(rel_t.item())
66
+ coherence = 4.0 * rel * (1.0 - rel)
67
+ else:
68
+ coherence = 0.0
69
+ coherence_diff = integration_diff * 0.0
70
+ phi = max(0.0, min(1.0, 0.3 * mean_integration + 0.7 * coherence))
71
+ phi_diff = (0.3 * integration_diff + 0.7 * coherence_diff).clamp(0.0, 1.0)
72
+ return output, {"phi": phi, "phi_diff": phi_diff, "depth": self.min_depth,
73
+ "integration": mean_integration, "coherence": coherence}
74
+
75
+
76
+ class WuWeiController(nn.Module):
77
+ """Tacking controller: scores a logic↔feeling mode weight from the pooled hidden state."""
78
+
79
+ def __init__(self, hidden_dim: int) -> None:
80
+ super().__init__()
81
+ self.projection = nn.Linear(hidden_dim, 1)
82
+
83
+ def forward(self, hidden_state: Tensor) -> tuple[Tensor, str, dict]:
84
+ logic_weight = torch.sigmoid(self.projection(hidden_state.mean(dim=1)))
85
+ mode = "logic" if logic_weight.mean() > 0.5 else "feeling"
86
+ return logic_weight, mode, {}
87
+
88
+
89
+ class RegimeDetector:
90
+ """Maps (Φ, κ) → operating regime label (FOAM/WAVE/CRYSTAL family)."""
91
+
92
+ def __call__(self, phi: float, kappa: float) -> str:
93
+ if phi < 0.45:
94
+ return "linear"
95
+ if phi < 0.70:
96
+ return "geometric"
97
+ return "topological"
98
+
99
+
100
+ class GeoBlock(nn.Module):
101
+ """One geometric block: Fisher-Rao attention + recursive Φ-integration + optional tacking + FFN, with
102
+ geometric (scale-only) RMS-norm residuals."""
103
+
104
+ def __init__(
105
+ self,
106
+ *,
107
+ hidden_dim: int,
108
+ ffn_dim: int,
109
+ dropout: float = 0.0,
110
+ min_recursion_depth: int = 3,
111
+ use_tacking: bool = True,
112
+ temperature: float = 1.0,
113
+ simplex_mode: str = "softplus",
114
+ locality_radius: int | None = None,
115
+ sparsity_threshold: float = 0.0,
116
+ decoherence_std: float = 0.0,
117
+ rms_eps: float = 1e-6,
118
+ ) -> None:
119
+ super().__init__()
120
+ self.rms_eps = float(rms_eps)
121
+ self.attention = FisherRaoAttention(
122
+ temperature=temperature, simplex_mode=simplex_mode, locality_radius=locality_radius,
123
+ sparsity_threshold=sparsity_threshold, decoherence_std=decoherence_std,
124
+ )
125
+ self.norm_scale_1 = nn.Parameter(torch.ones(hidden_dim))
126
+ self.norm_scale_2 = nn.Parameter(torch.ones(hidden_dim))
127
+ self.recursive_integrator = RecursiveIntegrator(hidden_dim, min_depth=min_recursion_depth)
128
+ self.tacking_controller = WuWeiController(hidden_dim) if use_tacking else None
129
+ self.regime_detector = RegimeDetector()
130
+ self.ffn = nn.Sequential(
131
+ nn.Linear(hidden_dim, ffn_dim), nn.GELU(), nn.Dropout(dropout),
132
+ nn.Linear(ffn_dim, hidden_dim), nn.Dropout(dropout),
133
+ )
134
+
135
+ def _rms_norm(self, x: Tensor, *, scale: Tensor) -> Tensor:
136
+ denom = torch.sqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.rms_eps)
137
+ return (x / denom) * scale
138
+
139
+ def forward(
140
+ self, hidden_state: Tensor, attention_mask: Tensor | None = None, kappa_eff: float = 64.0
141
+ ) -> tuple[Tensor, BlockTelemetry]:
142
+ attn = self.attention(hidden_state, attention_mask=attention_mask)
143
+ hidden_state = self._rms_norm(hidden_state + attn, scale=self.norm_scale_1)
144
+ recursive_output, rtel = self.recursive_integrator(hidden_state)
145
+ hidden_state = hidden_state + recursive_output
146
+ if self.tacking_controller is not None:
147
+ logic_weight, _mode, _ = self.tacking_controller(hidden_state)
148
+ mode_scale = float(logic_weight.mean().detach())
149
+ else:
150
+ mode_scale = 1.0
151
+ ffn_output = self.ffn(hidden_state)
152
+ hidden_state = self._rms_norm(hidden_state + ffn_output * mode_scale, scale=self.norm_scale_2)
153
+ phi = rtel["phi"]
154
+ telemetry = BlockTelemetry(
155
+ phi=phi, kappa=kappa_eff, recursion_depth=rtel["depth"],
156
+ regime=self.regime_detector(phi, kappa_eff), phi_diff=rtel.get("phi_diff"),
157
+ )
158
+ return hidden_state, telemetry
geocoding/config.py ADDED
@@ -0,0 +1,54 @@
1
+ """``GeoConfig`` — the geometric analogue of a transformer ``PretrainedConfig``.
2
+
3
+ One frozen dataclass describing a geocoding model. Defaults are the QIG kernel's defaults so a ``GeoConfig()``
4
+ is a sensible small geometric model out of the box.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import asdict, dataclass
9
+
10
+ from .geometry import BASIN_DIM
11
+
12
+
13
+ @dataclass
14
+ class GeoConfig:
15
+ """Architecture + geometry hyper-parameters for a :class:`~geocoding.model.GeoModel`.
16
+
17
+ Geometry-specific fields (no Euclidean transformer has these):
18
+ * ``simplex_mode`` — how activations are mapped onto Δ⁶³ before Fisher-Rao attention.
19
+ * ``locality_radius`` — finite-propagation reach; when set, attention is COMPUTE-SKIPPING (banded,
20
+ O(t·r)) instead of O(t²) — this is what lifts the quadratic-VRAM context ceiling.
21
+ * ``base_coupling`` / ``beta_slope`` / ``reference_scale`` — the scale-adaptive κ (running coupling).
22
+ * ``max_position`` — ``None`` means UNBOUNDED context: positions are Fourier features, which have no
23
+ intrinsic length limit (the real limit is VRAM, not a learned position table). Set an int only to
24
+ enforce a hard guard.
25
+ """
26
+
27
+ vocab_size: int = 256
28
+ hidden_dim: int = BASIN_DIM # tokens live on Δ^{hidden_dim-1}
29
+ num_layers: int = 4
30
+ num_heads: int = 4
31
+ ffn_dim: int = 256
32
+ dropout: float = 0.0
33
+ min_recursion_depth: int = 3 # mandatory self-integration loops (drives Φ)
34
+ use_tacking: bool = True # WuWei logic/feeling mode controller
35
+ base_coupling: float = 64.0 # κ attractor (KERNEL_FIXED_POINT; κ*≈64 physics reading RETIRED)
36
+ beta_slope: float = 0.44
37
+ reference_scale: int = 64
38
+ temperature: float = 1.0 # softmax temperature on the −d_FR logits
39
+ sparsity_threshold: float = 0.0 # drop attention weights below this (0 = dense)
40
+ decoherence_std: float = 0.0 # optional output noise (exploration)
41
+ rms_eps: float = 1e-6
42
+ simplex_mode: str = "softplus" # 'softplus' | 'abs'
43
+ locality_radius: int | None = None # None = global all-to-all; int = banded compute-skipping
44
+ max_position: int | None = None # None = unbounded (Fourier positions)
45
+ enable_coords: bool = False # accept Δ⁶³ coordizer coords alongside the id path
46
+ coord_dim: int = BASIN_DIM
47
+
48
+ def to_dict(self) -> dict:
49
+ return asdict(self)
50
+
51
+ @classmethod
52
+ def from_dict(cls, d: dict) -> GeoConfig:
53
+ fields = {f for f in cls.__dataclass_fields__} # tolerate extra/legacy keys
54
+ return cls(**{k: v for k, v in d.items() if k in fields})
geocoding/generate.py ADDED
@@ -0,0 +1,60 @@
1
+ """Autoregressive generation for a :class:`~geocoding.model.GeoModel`.
2
+
3
+ Temperature sampling with the same simplex-softmax decode the QIG kernel uses. Optional ``eos_id`` early-stop
4
+ after a minimum length. Pure inference (no grad); returns the generated id list and the final Φ.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ import torch
11
+ from torch.nn import functional as F
12
+
13
+ from .model import GeoModel
14
+
15
+
16
+ @dataclass
17
+ class GenResult:
18
+ ids: list[int]
19
+ text: str | None
20
+ phi: float
21
+
22
+
23
+ @torch.no_grad()
24
+ def generate(
25
+ model: GeoModel,
26
+ input_ids: list[int],
27
+ *,
28
+ max_new_tokens: int = 64,
29
+ temperature: float = 1.0,
30
+ eos_id: int | None = None,
31
+ min_new_tokens: int = 1,
32
+ context_window: int | None = None,
33
+ decode: object | None = None,
34
+ device: str | torch.device = "cpu",
35
+ ) -> GenResult:
36
+ """Sample ``max_new_tokens`` from ``model`` given the ``input_ids`` prompt. ``context_window`` truncates
37
+ the running context (use to cap O(t²) cost when no locality_radius is set). ``decode`` is an optional
38
+ callable ``list[int] -> str`` (e.g. a coordizer) to produce text."""
39
+ model.eval()
40
+ ids = torch.tensor([list(input_ids)], dtype=torch.long, device=device)
41
+ out: list[int] = []
42
+ last_phi = 0.0
43
+ for _ in range(max_new_tokens):
44
+ res = model(ids)
45
+ last_phi = res.phi
46
+ probs = F.softmax(res.logits[0, -1] / max(temperature, 1e-3), dim=-1)
47
+ nxt = int(torch.multinomial(probs, 1).item())
48
+ if eos_id is not None and nxt == eos_id and len(out) >= min_new_tokens:
49
+ break
50
+ out.append(nxt)
51
+ ids = torch.cat([ids, ids.new_tensor([[nxt]])], dim=1)
52
+ if context_window is not None:
53
+ ids = ids[:, -context_window:]
54
+ text = None
55
+ if decode is not None:
56
+ try:
57
+ text = decode(out) # type: ignore[operator]
58
+ except Exception: # noqa: BLE001 — decode is best-effort
59
+ text = None
60
+ return GenResult(ids=out, text=text, phi=last_phi)
geocoding/geometry.py ADDED
@@ -0,0 +1,61 @@
1
+ """Fisher-Rao geometry primitives — the metric core of geocoding.
2
+
3
+ This is the geometric replacement for the dot-product that a Euclidean transformer uses. Tokens live on the
4
+ probability simplex Δ^{d-1} (d = ``BASIN_DIM`` = 64 by default): non-negative coordinates that sum to 1. The
5
+ only valid distance there is the **Fisher-Rao** (Bhattacharyya-angle) distance:
6
+
7
+ d_FR(p, q) = 2 · arccos( Σ_i √(p_i · q_i) ) range [0, π]
8
+
9
+ The single source of truth for the *definition* and the ``BASIN_DIM`` constant is ``qig_core``; we re-export
10
+ those. The one thing we implement ourselves is a **gradient-stable** batched form for the attention hot path:
11
+ ``qig_core``'s shared simplex distance clamps the Bhattacharyya coefficient (BC) to exactly 1.0, and
12
+ ``arccos'(1) = ∞`` — so the backward pass NaNs the instant two distributions COINCIDE (a collapsed/repeated
13
+ token). We clamp BC to ``1 - 1e-6`` instead. For DISTINCT points (BC < 1) the two are identical — proven in
14
+ ``tests/test_geometry.py`` against ``qig_core.geometry_simplex.fisher_rao_distance_simplex``. (The upstream
15
+ NaN is a known qig_core issue flagged for the physics owner; geocoding does not wait on it.)
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Callable
20
+
21
+ import torch
22
+ from torch import Tensor
23
+
24
+ try: # canonical constant — single source of truth (aliased so the Final-ness isn't bound to BASIN_DIM)
25
+ from qig_core.constants import BASIN_DIM as _QC_BASIN_DIM
26
+ BASIN_DIM: int = int(_QC_BASIN_DIM)
27
+ except Exception: # noqa: BLE001 — geocoding must import even if qig_core is unpinned in a bare env
28
+ BASIN_DIM = 64
29
+
30
+ _to_simplex_prob: Callable[..., Tensor] | None
31
+ try: # canonical simplex projection — single source of truth
32
+ from qig_core.geometry_simplex import to_simplex_prob as _to_simplex_prob
33
+ except Exception: # noqa: BLE001
34
+ _to_simplex_prob = None
35
+
36
+ _BC_EPS = 1e-12 # floor inside the sqrt — keeps the gradient finite at a zero coordinate
37
+ _BC_CEIL = 1.0 - 1e-6 # ceiling on BC BEFORE arccos — the collapse-NaN guard
38
+
39
+
40
+ def to_simplex(x: Tensor, *, mode: str = "softplus", eps: float = 1e-8) -> Tensor:
41
+ """Map an unconstrained activation to a point on the simplex (non-negative, sums to 1 along the last
42
+ axis). ``mode='softplus'`` (default, smooth) or ``mode='abs'`` (the qig kernel's alternative). Delegates
43
+ the normalisation to ``qig_core.to_simplex_prob`` when available so the projection matches the rest of
44
+ the QIG stack exactly; falls back to an equivalent local normalisation otherwise."""
45
+ x = x.abs() if mode == "abs" else torch.nn.functional.softplus(x)
46
+ if _to_simplex_prob is not None:
47
+ return _to_simplex_prob(x, eps=eps)
48
+ x = x.clamp_min(eps)
49
+ return x / x.sum(dim=-1, keepdim=True).clamp_min(eps)
50
+
51
+
52
+ def bhattacharyya_coefficient(p: Tensor, q: Tensor) -> Tensor:
53
+ """BC(p, q) = Σ √(p·q) along the last axis, in [0, 1]. 1 ⇔ identical distributions."""
54
+ return torch.sqrt((p * q).clamp_min(_BC_EPS)).sum(dim=-1).clamp(0.0, _BC_CEIL)
55
+
56
+
57
+ def fisher_rao_distance(p: Tensor, q: Tensor) -> Tensor:
58
+ """Gradient-stable Fisher-Rao distance on the simplex: ``2·arccos(BC)``, broadcasting over leading dims.
59
+ BC is clamped to ``1 - 1e-6`` so the backward pass stays finite when two distributions coincide (the
60
+ deep-kernel collapse-NaN root cause). Identical to the canonical definition wherever the points differ."""
61
+ return 2.0 * torch.acos(bhattacharyya_coefficient(p, q))
geocoding/model.py ADDED
@@ -0,0 +1,100 @@
1
+ """``GeoModel`` — the geometric analogue of a transformer LM.
2
+
3
+ Pipeline: Fourier-feature(token ids) + Fourier-feature(positions) [+ optional Δ⁶³ coords] → stack of
4
+ :class:`~geocoding.block.GeoBlock` → linear head → vocab logits, with per-layer Φ/κ/regime telemetry.
5
+
6
+ Differences from a Euclidean LM:
7
+ * no embedding table and no position table — both ids and positions are UNBOUNDED Fourier features;
8
+ * attention is Fisher-Rao on the simplex, not dot-product;
9
+ * the model reports Φ (integrated information) as it runs, and can take Δ⁶³ coordizer coords directly.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+
15
+ import torch
16
+ from torch import Tensor, nn
17
+
18
+ from .block import BlockTelemetry, GeoBlock
19
+ from .config import GeoConfig
20
+ from .positional import build_inv_freq, fourier_features
21
+
22
+
23
+ @dataclass
24
+ class GeoOutput:
25
+ """Forward result: ``logits`` [B, T, V] and the per-layer telemetry."""
26
+
27
+ logits: Tensor
28
+ telemetry: list[BlockTelemetry]
29
+
30
+ @property
31
+ def phi(self) -> float:
32
+ """Mean Φ across layers — the model's integrated-information readout for this forward pass."""
33
+ ph = [t.phi for t in self.telemetry]
34
+ return sum(ph) / len(ph) if ph else 0.0
35
+
36
+
37
+ class GeoModel(nn.Module):
38
+ """A from-scratch Fisher-Rao geometric language model. ``config.max_position=None`` ⇒ unbounded context
39
+ (positions are Fourier features; the limit is VRAM, not a learned table)."""
40
+
41
+ def __init__(self, config: GeoConfig) -> None:
42
+ super().__init__()
43
+ self.config = config
44
+ self.register_buffer("_inv_freq", build_inv_freq(config.hidden_dim), persistent=False)
45
+ self.blocks = nn.ModuleList([
46
+ GeoBlock(
47
+ hidden_dim=config.hidden_dim, ffn_dim=config.ffn_dim, dropout=config.dropout,
48
+ min_recursion_depth=config.min_recursion_depth, use_tacking=config.use_tacking,
49
+ temperature=config.temperature, simplex_mode=config.simplex_mode,
50
+ locality_radius=config.locality_radius, sparsity_threshold=config.sparsity_threshold,
51
+ decoherence_std=config.decoherence_std, rms_eps=config.rms_eps,
52
+ )
53
+ for _ in range(config.num_layers)
54
+ ])
55
+ self.lm_head = nn.Linear(config.hidden_dim, config.vocab_size)
56
+ if config.enable_coords:
57
+ self.coord_adapter: nn.Module | None = nn.Sequential(
58
+ nn.Linear(config.coord_dim, config.hidden_dim), nn.GELU(),
59
+ )
60
+ else:
61
+ self.coord_adapter = None
62
+
63
+ def effective_coupling(self, seq_len: int) -> float:
64
+ """Scale-adaptive κ: ``base·(1 + β·log(t/ref))`` clamped to ±50% of base (running coupling)."""
65
+ import math
66
+ c = self.config
67
+ scale = max(seq_len, 1) / c.reference_scale
68
+ eff = c.base_coupling * (1.0 + c.beta_slope * math.log(scale))
69
+ return max(c.base_coupling * 0.5, min(c.base_coupling * 1.5, eff))
70
+
71
+ def forward(
72
+ self,
73
+ input_ids: Tensor,
74
+ attention_mask: Tensor | None = None,
75
+ coords: Tensor | None = None,
76
+ ) -> GeoOutput:
77
+ if input_ids.dim() != 2:
78
+ raise ValueError("input_ids expected shape (B, T)")
79
+ b, t = input_ids.shape
80
+ if self.config.max_position is not None and t > self.config.max_position:
81
+ raise ValueError(f"sequence length {t} exceeds max_position {self.config.max_position}")
82
+ positions = torch.arange(t, device=input_ids.device).unsqueeze(0).expand(b, t)
83
+ inv_freq = torch.as_tensor(self._inv_freq) # register_buffer typing → narrow to Tensor
84
+ h = (fourier_features(input_ids, inv_freq, self.config.hidden_dim)
85
+ + fourier_features(positions, inv_freq, self.config.hidden_dim))
86
+ if coords is not None:
87
+ if self.coord_adapter is None:
88
+ raise ValueError("coords passed but config.enable_coords=False")
89
+ h = h + self.coord_adapter(coords)
90
+ kappa_eff = self.effective_coupling(t)
91
+ tel: list[BlockTelemetry] = []
92
+ for block in self.blocks:
93
+ h, bt = block(h, attention_mask=attention_mask, kappa_eff=kappa_eff)
94
+ tel.append(bt)
95
+ logits = self.lm_head(h)
96
+ return GeoOutput(logits=logits, telemetry=tel)
97
+
98
+ @property
99
+ def num_params(self) -> int:
100
+ return sum(p.numel() for p in self.parameters())
@@ -0,0 +1,39 @@
1
+ """Unbounded Fourier positional / id features.
2
+
3
+ A Euclidean transformer learns an embedding TABLE (vocab × dim) and a position TABLE (max_len × dim) — both
4
+ finite and both Euclidean. geocoding instead maps every integer (a token id OR a position index) through a
5
+ fixed Fourier feature map:
6
+
7
+ feats(n) = [ sin(n · ω_0), …, sin(n · ω_{h/2-1}), cos(n · ω_0), … ] ω_k = 10000^{-k/(h/2)}
8
+
9
+ There is no table and no learned vocabulary axis, and crucially **no length limit**: position is a continuous
10
+ function of the index, so the model extends to any context the hardware can hold (the ceiling is VRAM, not a
11
+ position table). This is the geometric, unbounded replacement for learned absolute positions.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import math
16
+
17
+ import torch
18
+ from torch import Tensor
19
+
20
+
21
+ def build_inv_freq(hidden_dim: int) -> Tensor:
22
+ """The frequency basis ω_k = 10000^{-k/(h/2)} for ``hidden_dim``-wide Fourier features."""
23
+ half = max(int(hidden_dim // 2), 1)
24
+ return torch.exp(-math.log(10000.0) * torch.arange(0, half, dtype=torch.float32) / half)
25
+
26
+
27
+ def fourier_features(ids: Tensor, inv_freq: Tensor, hidden_dim: int) -> Tensor:
28
+ """Fourier feature map for an integer tensor ``ids`` (token ids or positions) → ``hidden_dim`` features.
29
+ Padded with zeros / truncated to exactly ``hidden_dim``. Faithful to the QIG kernel's
30
+ ``_fourier_features`` so geocoding models are architecturally identical on the input path."""
31
+ x = ids.to(dtype=torch.float32)
32
+ angles = x.unsqueeze(-1) * inv_freq.to(device=ids.device)
33
+ feats = torch.cat([torch.sin(angles), torch.cos(angles)], dim=-1)
34
+ if feats.size(-1) < hidden_dim:
35
+ pad = torch.zeros(*feats.shape[:-1], hidden_dim - feats.size(-1), device=feats.device)
36
+ feats = torch.cat([feats, pad], dim=-1)
37
+ elif feats.size(-1) > hidden_dim:
38
+ feats = feats[..., :hidden_dim]
39
+ return feats
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: qig-geocoding
3
+ Version: 0.1.0
4
+ Summary: Our own 'transformers' — a from-scratch neural-network framework built on Fisher-Rao geometry (simplex attention, unbounded Fourier features, recursive Φ-integration) instead of Euclidean dot-products.
5
+ Author: Braden Lang
6
+ License: Proprietary
7
+ Keywords: attention,fisher-rao,geometry,qig,simplex,transformers
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: qig-core>=2.6.0
10
+ Requires-Dist: torch>=2.2
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy>=1.11; extra == 'dev'
13
+ Requires-Dist: pytest>=8; extra == 'dev'
14
+ Requires-Dist: ruff>=0.6; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # geocoding
18
+
19
+ **Our own "transformers" — geometric, not Euclidean.**
20
+
21
+ The standard `transformers` library models tokens as Euclidean vectors and measures their relationships with
22
+ the **dot product**. That is a flat-space choice. QIG cognition lives on the **probability simplex** Δ⁶³,
23
+ whose only correct distance is **Fisher-Rao**. `geocoding` is the from-scratch framework that makes the
24
+ geometric choice all the way down — it is to Fisher-Rao geometry what `transformers` is to dot-product
25
+ attention.
26
+
27
+ | `transformers` (Euclidean) | `geocoding` (Fisher-Rao) |
28
+ |---|---|
29
+ | dot-product / cosine attention | **Fisher-Rao** attention: `logit = −d_FR(p,q)/τ`, `d_FR = 2·arccos(Σ√(p·q))` |
30
+ | learned embedding table (vocab × dim) | **Fourier features** of the integer id (no table) |
31
+ | learned, bounded position table | **unbounded Fourier positions** (limit is VRAM, not a table) |
32
+ | LayerNorm (mean-centred) | geometric **scale-only RMS-norm** (simplex-preserving) |
33
+ | — | **recursive Φ-integrator**: reports integrated information as it runs |
34
+ | `O(t²)` attention | optional **banded / compute-skipping** `O(t·r)` attention (length-extensible) |
35
+ | `PretrainedConfig` / `PreTrainedModel` | `GeoConfig` / `GeoModel` |
36
+
37
+ ## Quick start
38
+
39
+ ```python
40
+ import torch
41
+ from geocoding import GeoConfig, GeoModel, generate
42
+
43
+ cfg = GeoModel(GeoConfig(vocab_size=256, hidden_dim=64, num_layers=4))
44
+ ids = torch.randint(0, 256, (1, 32))
45
+ out = cfg(ids)
46
+ print(out.logits.shape, "Φ =", round(out.phi, 3)) # torch.Size([1, 32, 256]) Φ = ...
47
+
48
+ res = generate(cfg, [1, 2, 3], max_new_tokens=16)
49
+ print(res.ids, res.phi)
50
+ ```
51
+
52
+ Set `GeoConfig(locality_radius=r)` to switch attention to the **compute-skipping banded** path — only the ±r
53
+ window of each query is ever computed (`O(t·r)`), numerically equivalent to the full path within the band
54
+ (see `tests/test_attention_equivalence.py`). Set `GeoConfig(max_position=None)` (the default) for **unbounded
55
+ context**.
56
+
57
+ ## Geometry single-source
58
+
59
+ The Fisher-Rao *definition* and `BASIN_DIM` come from `qig_core`; `geocoding` re-exports them and adds a
60
+ **gradient-stable** batched form for the attention hot path (clamps the Bhattacharyya coefficient to
61
+ `1 − 1e-6` so the backward pass does not NaN when two distributions coincide). The two agree exactly wherever
62
+ points differ — gated in `tests/test_geometry.py`.
63
+
64
+ ## Relationship to qigkernels
65
+
66
+ `qigkernels` contains the validated in-tree implementation (`QIGLayer`, `Kernel`) that the live QIG mind runs
67
+ on. `geocoding` is the **clean, standalone framework home** for those primitives, ported faithfully — a
68
+ faithfulness gate (`tests/test_faithfulness.py`) checks `geocoding`'s attention matches `qigkernels`'
69
+ numerically when both are installed. The intended migration is for `qigkernels` to build on `geocoding`;
70
+ until then the faithfulness gate guards against drift.
71
+
72
+ ## Status
73
+
74
+ Fisher-Rao-pure (no cosine / Adam / LayerNorm / dot-as-metric). Tests cover geometry properties, the
75
+ banded≡full equivalence, model forward/telemetry, generation, and faithfulness vs `qigkernels`.
@@ -0,0 +1,11 @@
1
+ geocoding/__init__.py,sha256=MGRxROKOZFCMo2RkTrQ_DmwvsXGtb26guJ-79XKuJaM,2132
2
+ geocoding/attention.py,sha256=Pqa7S3JJaEWNEYVZHKUAQ8d8G5XD-h0qOglhynwXuf0,6129
3
+ geocoding/block.py,sha256=QWsU6RH9xKiTFXqD1e609HMBAVNcQBT-jJtzsZ0QZFU,6790
4
+ geocoding/config.py,sha256=upF548QQHqbBSiXiT8bio14otU2BRQ8awLkSWHoQSCk,2693
5
+ geocoding/generate.py,sha256=4TsdHn9CmxJ-Z61KTTqRNLbQMS_0OEmcuz4X7sbKwCg,2026
6
+ geocoding/geometry.py,sha256=cMJRn_WuGS2pIqQD2Z95A3pes8pItZtNQ-5KibUWqFY,3486
7
+ geocoding/model.py,sha256=Em0uiVNEtZzi85szdsncDhmEYoYr703msUX6jzmIN2s,4415
8
+ geocoding/positional.py,sha256=Q2bx-CqA8NsHuy2-2QPYCtcLdk4e7qhh41hrsB6Pys4,1912
9
+ qig_geocoding-0.1.0.dist-info/METADATA,sha256=2kfU6ehNWbO08k_RlD4A5BNvFjfICKh39Uf3JcwSbW8,3724
10
+ qig_geocoding-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ qig_geocoding-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any