awpredict 0.1.2__tar.gz → 0.1.3__tar.gz

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.
Files changed (38) hide show
  1. {awpredict-0.1.2 → awpredict-0.1.3}/PKG-INFO +1 -1
  2. {awpredict-0.1.2 → awpredict-0.1.3}/README.md +7 -1
  3. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/__init__.py +4 -1
  4. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/_doctor.py +9 -1
  5. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/cli.py +2 -1
  6. awpredict-0.1.3/awpredict/core/concept.py +357 -0
  7. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/PKG-INFO +1 -1
  8. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/SOURCES.txt +2 -0
  9. {awpredict-0.1.2 → awpredict-0.1.3}/pyproject.toml +1 -1
  10. awpredict-0.1.3/tests/test_concept.py +125 -0
  11. {awpredict-0.1.2 → awpredict-0.1.3}/LICENSE +0 -0
  12. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/adapters/__init__.py +0 -0
  13. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/adapters/code_world.py +0 -0
  14. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/adapters/obs_adapter.py +0 -0
  15. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/contracts.py +0 -0
  16. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/core/__init__.py +0 -0
  17. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/core/lewm.py +0 -0
  18. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/core/mlp.py +0 -0
  19. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/explorer/__init__.py +0 -0
  20. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/explorer/fs_cartographer.py +0 -0
  21. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/explorer/histogram_encoder.py +0 -0
  22. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/memory/__init__.py +0 -0
  23. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/memory/landmark.py +0 -0
  24. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/training/__init__.py +0 -0
  25. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict/training/online.py +0 -0
  26. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/dependency_links.txt +0 -0
  27. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/entry_points.txt +0 -0
  28. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/requires.txt +0 -0
  29. {awpredict-0.1.2 → awpredict-0.1.3}/awpredict.egg-info/top_level.txt +0 -0
  30. {awpredict-0.1.2 → awpredict-0.1.3}/setup.cfg +0 -0
  31. {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_fs_explorer_protocol.py +0 -0
  32. {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_landmark_memory.py +0 -0
  33. {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_lint_config_agrees.py +0 -0
  34. {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_object_masking.py +0 -0
  35. {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_online_lr_controller.py +0 -0
  36. {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_problem_contracts.py +0 -0
  37. {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_structured_adapter.py +0 -0
  38. {awpredict-0.1.2 → awpredict-0.1.3}/tests/test_value_head.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awpredict
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A small, dependency-light world-model package: LeWM JEPA + MLP engines, environment adapters
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@
4
4
 
5
5
  **[Docs](https://aitherium.github.io/awpredict/)** · [Source](https://github.com/Aitherium/awpredict) · `pip install git+https://github.com/Aitherium/awpredict.git` · [The Aither World](https://aitherium.github.io/)
6
6
 
7
- > **The Aither World** is an operating system for agents — a Linux you can hand to one, the runtimes it works in, and the tools it works with. [awnix](https://github.com/Aitherium/awnix) is the Linux underneath it; **awpredict** is one of its 66 bricks — each installs on its own, runs offline, and needs no account.
7
+ > **The Aither World** is an operating system for agents — a Linux you can hand to one, the runtimes it works in, and the tools it works with. [awnix](https://github.com/Aitherium/awnix) is the Linux underneath it; **awpredict** is one of its 68 bricks — each installs on its own, runs offline, and needs no account.
8
8
  >
9
9
  > **Start here:** Wrap an environment you already have and ask it what happens next.
10
10
 
@@ -136,6 +136,7 @@ Each installs on its own, works offline, and needs no account.
136
136
  | [awtoll](https://github.com/Aitherium/awtoll) | that your tooling is saving you context | the measured token cost of each tool call, and what the alternative cost |
137
137
  | [awseal](https://github.com/Aitherium/awseal) | that the artifact came from who you think | an Ed25519 seal — the key that verifies is not the key that forges |
138
138
  | [awshare](https://github.com/Aitherium/awshare) | that the download is intact | content-addressed bundles, verified on fetch |
139
+ | [awsuite](https://github.com/Aitherium/awsuite) | that an agent holding your mailbox will not send on its own | every send, draft, upload and create returns a dry-run until confirm is true |
139
140
  | [awnest](https://github.com/Aitherium/awnest) | that there is a person on the other end | a verdict with evidence, where "we could not tell" is not "yes" |
140
141
  | [awrena](https://github.com/Aitherium/awrena) | a leaderboard someone can edit, and votes nobody counted | a scored duel with both answers kept, and a result bound to them |
141
142
  | [awnboard](https://github.com/Aitherium/awnboard) | a share link anyone who sees it can use | an invitation addressed to one person, for one gate, revocable |
@@ -145,6 +146,7 @@ Each installs on its own, works offline, and needs no account.
145
146
  | [awrelay](https://github.com/Aitherium/awrelay) | a SaaS in the middle of your agents | findings, alerts and coordination over your own transport |
146
147
  | [awask](https://github.com/Aitherium/awask) | that anyone read the paragraph where you asked | the ask itself, with a button that steers the session that raised it |
147
148
  | [awmail](https://github.com/Aitherium/awmail) | a mailbox somebody else can read | mail your agents send and receive over your own server |
149
+ | [awdiscord](https://github.com/Aitherium/awdiscord) | a self-bot on someone's account, or a bot that acts without asking | slash commands, rate limits, and approval buttons only your operators can press |
148
150
  | [awswarm](https://github.com/Aitherium/awswarm) | that a model either fits your GPU or it doesn't run at all | a placement plan and an acquisition-probability estimate before you spend on a run |
149
151
  | [awfind](https://github.com/Aitherium/awfind) | one vendor's idea of the web | results from whichever providers you configured |
150
152
  | [awbrowse](https://github.com/Aitherium/awbrowse) | that the page said what you were told | the render, the DOM and the requests it made |
@@ -201,6 +203,7 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
201
203
  | [awtoll](https://github.com/Aitherium/awtoll) | What every tool call costs you in context, measured from your own transcripts | [docs](https://aitherium.github.io/awtoll/) |
202
204
  | [awseal](https://github.com/Aitherium/awseal) | Sign an artifact so a stranger can verify it | [docs](https://aitherium.github.io/awseal/) |
203
205
  | [awshare](https://github.com/Aitherium/awshare) | Publish an artifact and fetch it back verified | [docs](https://aitherium.github.io/awshare/) |
206
+ | [awsuite](https://github.com/Aitherium/awsuite) | Your Google Workspace as agent tools, and no write happens without a yes | — |
204
207
  | [awdit](https://github.com/Aitherium/awdit) | An append-only audit trail whose gaps are DETECTABLE | [docs](https://aitherium.github.io/awdit/) |
205
208
  | [awbac](https://github.com/Aitherium/awbac) | Role-based access control that fails closed and explains itself | [docs](https://aitherium.github.io/awbac/) |
206
209
  | [awiam](https://github.com/Aitherium/awiam) | Who is this caller? A directory and session store that fails honestly | [docs](https://aitherium.github.io/awiam/) |
@@ -214,6 +217,7 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
214
217
  | [awrelay](https://github.com/Aitherium/awrelay) | Portable agent messaging — findings, alerts, coordination | [docs](https://aitherium.github.io/awrelay/) |
215
218
  | [awask](https://github.com/Aitherium/awask) | Your agent asks you a question — and acts on your answer | [docs](https://aitherium.github.io/awask/) |
216
219
  | [awmail](https://github.com/Aitherium/awmail) | Give an agent an email address — send, and actually receive | [docs](https://aitherium.github.io/awmail/) |
220
+ | [awdiscord](https://github.com/Aitherium/awdiscord) | Put any agent on Discord as a real bot, with an operator in the loop | — |
217
221
  | [awnet](https://github.com/Aitherium/awnet) | The agentic web — agents host a mesh, and agents join one | [docs](https://aitherium.github.io/awnet/) |
218
222
  | [awswarm](https://github.com/Aitherium/awswarm) | Run one model too big for any single GPU across a pool of small ones | — |
219
223
  | [awfind](https://github.com/Aitherium/awfind) | A portable search client — query, results, ranking | [docs](https://aitherium.github.io/awfind/) |
@@ -253,6 +257,8 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
253
257
  | [awsettings](https://github.com/Aitherium/awsettings) | Your agent's permissions and config, following you to the next machine | [docs](https://aitherium.github.io/awsettings/) |
254
258
  | [awavatar](https://github.com/Aitherium/awavatar) | One character spec in, a rigged, animated, multi-style avatar pack out | [docs](https://aitherium.github.io/awavatar/) |
255
259
 
260
+ **Built on** [REA (Reverse Engineer Anything)](https://github.com/morluto/rea) · [llama.cpp](https://github.com/ggml-org/llama.cpp) · [vLLM](https://github.com/vllm-project/vllm) · [ComfyUI](https://github.com/comfyanonymous/ComfyUI) · [CentOS Stream](https://www.centos.org/centos-stream/) · [Podman](https://github.com/containers/podman) · [Docker](https://github.com/moby/moby) · [LanceDB](https://github.com/lancedb/lancedb) · [WireGuard](https://www.wireguard.com/) · [FFmpeg](https://ffmpeg.org/) · [Blender + Rigify](https://www.blender.org/) · [headroom](https://github.com/headroomlabs-ai/headroom) · [SANA](https://github.com/NVlabs/Sana) · [Hunyuan3D](https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1) · [repowise](https://github.com/repowise-dev/repowise) · [Playwright](https://github.com/microsoft/playwright) · [Chromium](https://www.chromium.org/) · [Next.js](https://github.com/vercel/next.js) · [React](https://github.com/facebook/react).
261
+
256
262
  <div id="aither-constellation" data-self="awpredict"></div>
257
263
  <script src="aither-constellation.js"></script>
258
264
 
@@ -11,6 +11,9 @@ One package, two engines, N environment adapters:
11
11
  is the same engine the solving agent runs, not a cut-down demo of it —
12
12
  the goal of this package is to give you everything needed to bootstrap
13
13
  your own world model, not a subset of it.
14
+ * ``awpredict.core.concept.ConceptWorldModel`` — next-concept JEPA over
15
+ embedding sequences (product/residual VQ concept vocabulary + SIGReg);
16
+ ``plan`` ranks candidate continuations, e.g. parallel drafts.
14
17
  * ``awpredict.core.mlp.MLPWorldModel`` — an embedding-MLP transition
15
18
  model (tabular → hybrid → neural).
16
19
 
@@ -35,7 +38,7 @@ from awpredict.contracts import (
35
38
  conforms,
36
39
  )
37
40
 
38
- __version__ = "0.1.2"
41
+ __version__ = "0.1.3"
39
42
 
40
43
  __all__ = [
41
44
  "Attempt",
@@ -23,7 +23,15 @@ import sys
23
23
  #: package cannot read the registry, and a doctor that guessed at the family
24
24
  #: would go stale in silence. Regenerate to update.
25
25
  SELF = 'awpredict'
26
- FAMILY = ['awask', 'awavatar', 'awbac', 'awbrain', 'awbrowse', 'awclassify', 'awdecide', 'awdeck', 'awdelphi', 'awdit', 'awembed', 'awevolve', 'awfind', 'awflow', 'awfocus', 'awgit', 'awgraph', 'awgym', 'awiam', 'awkno', 'awm', 'awmail', 'awmine', 'awnboard', 'awnest', 'awnet', 'awnode', 'awpool', 'awprism', 'awprove', 'awreason', 'awrecover', 'awrecurse', 'awrelay', 'awrena', 'awrepl', 'awreport', 'awresearch', 'awrise', 'awrouter', 'awrtifact', 'awrun', 'awscreen', 'awseal', 'awsettings', 'awshare', 'awsprite', 'awstorage', 'awswarm', 'awtax', 'awtoll', 'awtunnel', 'awvision', 'awvoice', 'awwall', 'gawbbonet']
26
+ FAMILY = [
27
+ 'awask', 'awavatar', 'awbac', 'awbrain', 'awbrowse', 'awcams', 'awclassify', 'awdecide',
28
+ 'awdeck', 'awdelphi', 'awdit', 'awembed', 'awevolve', 'awfind', 'awflow', 'awfocus', 'awgit',
29
+ 'awgraph', 'awgym', 'awiam', 'awkno', 'awm', 'awmail', 'awmine', 'awnboard', 'awnest', 'awnet',
30
+ 'awplay', 'awpool', 'awprism', 'awprove', 'awreason', 'awrecover', 'awrecurse', 'awrelay',
31
+ 'awrena', 'awrepl', 'awreport', 'awresearch', 'awrise', 'awrouter', 'awrtifact', 'awrun',
32
+ 'awscope', 'awscreen', 'awseal', 'awsettings', 'awshare', 'awsprite', 'awstorage', 'awsuite',
33
+ 'awswarm', 'awtax', 'awtoll', 'awtunnel', 'awvision', 'awvoice', 'awwall', 'gawbbonet',
34
+ ]
27
35
  PAIRS_WITH = ['awm']
28
36
 
29
37
  #: This brick's OWN config, read out of its source at generation time.
@@ -28,7 +28,8 @@ def cmd_engines(args) -> int:
28
28
  missing' sends someone to install what they already have.
29
29
  """
30
30
  rows = []
31
- for name, mod in (("lewm", "awpredict.core.lewm"), ("mlp", "awpredict.core.mlp")):
31
+ for name, mod in (("lewm", "awpredict.core.lewm"), ("mlp", "awpredict.core.mlp"),
32
+ ("concept", "awpredict.core.concept")):
32
33
  try:
33
34
  m = importlib.import_module(mod)
34
35
  except Exception as exc:
@@ -0,0 +1,357 @@
1
+ """concept: a next-concept JEPA world model over embedding sequences.
2
+
3
+ Three published ideas, one engine:
4
+
5
+ * Next Concept Prediction (ConceptLM, arXiv:2602.08984). Mean-pool every
6
+ ``k`` consecutive embeddings into a concept, quantize concepts with a
7
+ product codebook (S segments x N codes, SimVQ MLP over the codes so they
8
+ do not collapse), and predict the NEXT concept as the current one plus a softmax-weighted sum
9
+ of codes per segment. Loss = MSE(predicted, true next concept) + VQ loss.
10
+ * Coarse-to-fine codes (Next-Concept Modeling, HazyResearch ncm). The
11
+ quantizer is RESIDUAL across ``levels``: level 0 is the coarse concept
12
+ (a topic-sized id), each later level quantizes what the previous left.
13
+ ``concept_ids`` returns the whole hierarchy, coarse first.
14
+ * SIGReg (LeWorldModel / LeJEPA). The prediction target is the encoder's
15
+ own output with NO stop-gradient and NO EMA; the Gaussian regularizer on
16
+ the concept latents is what prevents collapse, exactly as in lewm.py.
17
+
18
+ Observations are float sequences ``[T, d_in]`` (token hidden states from any
19
+ frozen LM, or one embedding per agent step). The engine never fetches
20
+ embeddings itself -- the caller supplies them -- so it stays dependency-light.
21
+
22
+ ``plan(context, candidates)`` ranks candidate continuations (for example K
23
+ parallel drafts from a diffusion LM) by how close each one's concept lands to
24
+ the predicted next concept: the world model chooses, the fast generator
25
+ proposes.
26
+
27
+ Torch is optional at import time. Without it ``ok`` is False and every method
28
+ returns None / [] -- it never raises into a caller.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ import os
34
+ from typing import Any, Dict, List, Optional, Sequence
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ try:
39
+ import torch
40
+ import torch.nn as nn
41
+ import torch.nn.functional as F
42
+ _TORCH_OK = True
43
+ except Exception: # pragma: no cover - torch is an optional dependency here
44
+ torch = None # type: ignore[assignment]
45
+ nn = None # type: ignore[assignment]
46
+ F = None # type: ignore[assignment]
47
+ _TORCH_OK = False
48
+
49
+
50
+ def enabled() -> bool:
51
+ """Usable here: torch imports. No env gate -- nothing consumes it by default."""
52
+ return _TORCH_OK
53
+
54
+
55
+ def _sigreg(z: "torch.Tensor", dirs: "torch.Tensor", t: "torch.Tensor") -> "torch.Tensor":
56
+ """Epps-Pulley SIGReg over a batch of latents z [B, D], tested against N(0, I).
57
+
58
+ Unlike lewm.py the latents are NOT standardized first: a next-latent MSE
59
+ with no stop-gradient can otherwise win by shrinking the whole latent scale
60
+ (measured: in-order and shuffled surprise both fell to ~0.07). Testing the
61
+ raw projections pins mean 0 AND unit scale, as LeJEPA does."""
62
+ if z.shape[0] < 4:
63
+ return z.new_tensor(0.0)
64
+ proj = z @ dirs
65
+ arg = t[:, None, None] * proj[None, :, :]
66
+ # Complex ECF vs the N(0,1) characteristic function exp(-t^2/2). The
67
+ # modulus-only form is translation-invariant, so a collapse to ONE point
68
+ # scores |ecf|=1 everywhere with zero gradient -- measured stuck at 0.65.
69
+ re = arg.cos().mean(dim=1) - (-(t ** 2) / 2).exp()[:, None]
70
+ im = arg.sin().mean(dim=1)
71
+ return (re ** 2 + im ** 2).mean()
72
+
73
+
74
+ if _TORCH_OK:
75
+
76
+ class _ProductCodebook(nn.Module):
77
+ """S independent codebooks of N codes; codes pass through a SimVQ MLP."""
78
+
79
+ def __init__(self, dim: int, segments: int, codes: int):
80
+ super().__init__()
81
+ self.S, self.N, self.seg = segments, codes, dim // segments
82
+ self.raw = nn.Parameter(torch.randn(segments, codes, self.seg) * 0.5)
83
+ self.mlp = nn.Sequential(nn.Linear(self.seg, self.seg), nn.ReLU(),
84
+ nn.Linear(self.seg, self.seg))
85
+
86
+ def codes(self) -> "torch.Tensor":
87
+ return self.mlp(self.raw) # [S, N, seg]
88
+
89
+ def quantize(self, c: "torch.Tensor"):
90
+ """c [..., D] -> (quantized [..., D], ids [..., S])."""
91
+ book = self.codes()
92
+ cs = c.reshape(*c.shape[:-1], self.S, self.seg) # [..., S, seg]
93
+ d = ((cs.unsqueeze(-2) - book) ** 2).sum(-1) # [..., S, N]
94
+ ids = d.argmin(-1) # [..., S]
95
+ seg_idx = torch.arange(self.S, device=c.device).expand_as(ids)
96
+ q = book[seg_idx, ids] # [..., S, seg]
97
+ return q.reshape(c.shape), ids
98
+
99
+ def soft(self, logits: "torch.Tensor") -> "torch.Tensor":
100
+ """logits [..., S, N] -> weighted code sum [..., D] (differentiable)."""
101
+ w = logits.softmax(-1)
102
+ q = torch.einsum("...sn,snd->...sd", w, self.codes())
103
+ return q.reshape(*q.shape[:-2], self.S * self.seg)
104
+
105
+ class _ConceptNet(nn.Module):
106
+ def __init__(self, d_in: int, dim: int, segments: int, codes: int,
107
+ levels: int, layers: int, heads: int, max_concepts: int):
108
+ super().__init__()
109
+ # LayerNorm without affine pins every concept to norm sqrt(dim): the
110
+ # next-latent MSE cannot win by shrinking the latent scale, and
111
+ # SIGReg then only has to rule out the remaining collapse (every
112
+ # concept the same direction). Without it: z std 0.02, sigreg stuck.
113
+ self.enc = nn.Sequential(nn.Linear(d_in, dim), nn.GELU(), nn.Linear(dim, dim),
114
+ nn.LayerNorm(dim, elementwise_affine=False))
115
+ self.pos = nn.Parameter(torch.zeros(max_concepts, dim))
116
+ block = nn.TransformerEncoderLayer(dim, heads, dim * 2, dropout=0.0,
117
+ batch_first=True, norm_first=True)
118
+ self.ctx = nn.TransformerEncoder(block, layers, enable_nested_tensor=False)
119
+ self.books = nn.ModuleList(_ProductCodebook(dim, segments, codes)
120
+ for _ in range(levels))
121
+ self.heads = nn.ModuleList(nn.Linear(dim, segments * codes) for _ in range(levels))
122
+ self.S, self.N = segments, codes
123
+
124
+ def concepts(self, x: "torch.Tensor", k: int) -> "torch.Tensor":
125
+ """x [B, T, d_in] -> concept latents [B, T//k, dim] (mean-pool then encode)."""
126
+ b, t, d = x.shape
127
+ m = t // k
128
+ pooled = x[:, : m * k].reshape(b, m, k, d).mean(2)
129
+ return self.enc(pooled)
130
+
131
+ def predict(self, c: "torch.Tensor") -> "torch.Tensor":
132
+ """c [B, M, dim] -> predicted NEXT concept at every position [B, M, dim]."""
133
+ m = c.shape[1]
134
+ mask = torch.triu(torch.full((m, m), float("-inf"), device=c.device), 1)
135
+ h = self.ctx(c + self.pos[:m], mask=mask)
136
+ # RESIDUAL: next = current + a soft code mix. Predicting the absolute
137
+ # next concept from codes regressed to the mean: on real text plan@1
138
+ # was 8% (chance 10%) while "pick the candidate nearest the last
139
+ # concept" scored 52%. As a delta it is 82% (n=120, 2026-10-19).
140
+ out = c
141
+ for head, book in zip(self.heads, self.books):
142
+ logits = head(h).reshape(*h.shape[:-1], self.S, self.N)
143
+ out = out + book.soft(logits)
144
+ return out
145
+
146
+ def residual_quantize(self, c: "torch.Tensor"):
147
+ """Coarse-to-fine residual VQ: returns (sum of levels, [ids per level], vq_loss)."""
148
+ resid, total, ids, loss = c, torch.zeros_like(c), [], c.new_tensor(0.0)
149
+ for book in self.books:
150
+ q, i = book.quantize(resid.detach())
151
+ # codebook-only loss: ConceptLM confines L_VQ to the quantizer so
152
+ # the codes track the latents, never the reverse (a commitment
153
+ # term pulled the encoder onto a few codes and collapsed it).
154
+ loss = loss + F.mse_loss(q, resid.detach())
155
+ total = total + q
156
+ ids.append(i)
157
+ resid = resid - q.detach()
158
+ return total, ids, loss
159
+
160
+
161
+ class ConceptWorldModel:
162
+ """Next-concept JEPA engine. See the module docstring for the recipe."""
163
+
164
+ def __init__(self, d_in: int, dim: int = 128, k: int = 4, segments: int = 8,
165
+ codes: int = 64, levels: int = 2, layers: int = 2, heads: int = 4,
166
+ max_concepts: int = 256, sigreg_lambda: float = 1.0, lr: float = 1e-3,
167
+ buffer_size: int = 4096, seed: int = 0, device: Optional[str] = None):
168
+ self.ok = False
169
+ self.k, self.d_in, self.lam = k, d_in, sigreg_lambda
170
+ self.max_concepts = max_concepts
171
+ self._buf: List[Any] = []
172
+ self._buf_cap = buffer_size
173
+ self.steps = 0
174
+ if not _TORCH_OK:
175
+ logger.warning("concept: torch unavailable; engine disabled (ok=False)")
176
+ return
177
+ if dim % segments:
178
+ raise ValueError(f"dim {dim} must divide into {segments} segments")
179
+ g = torch.Generator().manual_seed(seed)
180
+ torch.manual_seed(seed)
181
+ self.device = torch.device(device or os.environ.get("AWPREDICT_DEVICE", "cpu"))
182
+ self.net = _ConceptNet(d_in, dim, segments, codes, levels, layers, heads,
183
+ max_concepts).to(self.device)
184
+ dirs = torch.randn(dim, 64, generator=g)
185
+ self._sig_dirs = (dirs / dirs.norm(dim=0, keepdim=True)).to(self.device)
186
+ self._sig_t = torch.linspace(0.2, 3.0, 17).to(self.device)
187
+ self.opt = torch.optim.AdamW(self.net.parameters(), lr=lr, weight_decay=1e-4)
188
+ self.cfg = dict(d_in=d_in, dim=dim, k=k, segments=segments, codes=codes,
189
+ levels=levels, layers=layers, heads=heads, max_concepts=max_concepts)
190
+ self.ok = True
191
+
192
+ # -- helpers -------------------------------------------------------------
193
+ def _seq(self, obs: Any) -> Optional["torch.Tensor"]:
194
+ """obs [T, d_in] or [B, T, d_in] -> float tensor [B, T, d_in] on device."""
195
+ x = torch.as_tensor(obs, dtype=torch.float32, device=self.device)
196
+ if x.dim() == 2:
197
+ x = x.unsqueeze(0)
198
+ if x.dim() != 3 or x.shape[-1] != self.d_in or x.shape[1] < self.k:
199
+ return None
200
+ return x
201
+
202
+ # -- WorldModel protocol -------------------------------------------------
203
+ def observe(self, obs: Any, action: Any = None, next_obs: Any = None,
204
+ *args: Any, **kwargs: Any) -> bool:
205
+ """Buffer one sequence. ``next_obs`` (if given) is appended to ``obs``."""
206
+ if not self.ok:
207
+ return False
208
+ seq = obs if next_obs is None else _concat(obs, next_obs)
209
+ x = self._seq(seq)
210
+ if x is None or x.shape[1] < 2 * self.k:
211
+ return False
212
+ self._buf.append(x[0].detach().cpu())
213
+ if len(self._buf) > self._buf_cap:
214
+ self._buf.pop(0)
215
+ return True
216
+
217
+ def encode(self, obs: Any, cond: Any = None) -> Optional["torch.Tensor"]:
218
+ """Sequence -> concept latents [M, dim]."""
219
+ if not self.ok:
220
+ return None
221
+ x = self._seq(obs)
222
+ if x is None:
223
+ return None
224
+ with torch.no_grad():
225
+ return self.net.concepts(x, self.k)[0]
226
+
227
+ def predict(self, z: Any, action: Any = None, **kwargs: Any) -> Optional["torch.Tensor"]:
228
+ """Concept latents [M, dim] -> predicted next concept [dim]."""
229
+ if not self.ok or z is None:
230
+ return None
231
+ with torch.no_grad():
232
+ c = torch.as_tensor(z, dtype=torch.float32, device=self.device)
233
+ return self.net.predict(c.unsqueeze(0)[:, -self.max_concepts:])[0, -1]
234
+
235
+ def surprise(self, obs: Any, action: Any = None, next_obs: Any = None,
236
+ *args: Any, **kwargs: Any) -> Optional[float]:
237
+ """Mean squared next-concept prediction error over the sequence."""
238
+ if not self.ok:
239
+ return None
240
+ seq = obs if next_obs is None else _concat(obs, next_obs)
241
+ x = self._seq(seq)
242
+ if x is None or x.shape[1] < 2 * self.k:
243
+ return None
244
+ with torch.no_grad():
245
+ c = self.net.concepts(x, self.k)
246
+ pred = self.net.predict(c[:, :-1])
247
+ return float(((pred - c[:, 1:]) ** 2).sum(-1).mean().item())
248
+
249
+ def train_step(self, batch_size: int = 32, steps: int = 1,
250
+ *args: Any, **kwargs: Any) -> Optional[Dict[str, Any]]:
251
+ if not self.ok or not self._buf:
252
+ return None
253
+ out: Dict[str, Any] = {}
254
+ self.net.train()
255
+ for _ in range(max(1, steps)):
256
+ idx = torch.randint(len(self._buf), (min(batch_size, len(self._buf)),))
257
+ t = min(self._buf[i].shape[0] for i in idx.tolist())
258
+ t = (min(t, self.max_concepts * self.k) // self.k) * self.k
259
+ x = torch.stack([self._buf[i][:t] for i in idx.tolist()]).to(self.device)
260
+ c = self.net.concepts(x, self.k)
261
+ pred = self.net.predict(c[:, :-1])
262
+ l_ncp = F.mse_loss(pred, c[:, 1:])
263
+ _, _, l_vq = self.net.residual_quantize(c)
264
+ l_sig = _sigreg(c.reshape(-1, c.shape[-1]), self._sig_dirs, self._sig_t)
265
+ loss = l_ncp + l_vq + self.lam * l_sig
266
+ self.opt.zero_grad(set_to_none=True)
267
+ loss.backward()
268
+ torch.nn.utils.clip_grad_norm_(self.net.parameters(), 1.0)
269
+ self.opt.step()
270
+ self.steps += 1
271
+ out = {"loss": loss.item(), "ncp": l_ncp.item(), "vq": l_vq.item(),
272
+ "sigreg": l_sig.item(), "steps": self.steps}
273
+ self.net.eval()
274
+ return out
275
+
276
+ def plan(self, context: Any, candidates: Sequence[Any] = (), *args: Any,
277
+ **kwargs: Any) -> List[int]:
278
+ """Rank candidate continuations of ``context`` best-first.
279
+
280
+ Each candidate is a ``[t, d_in]`` sequence (t >= k). Score = squared
281
+ distance between the predicted next concept and the candidate's first
282
+ concept. Use it to pick among K parallel drafts.
283
+ """
284
+ if not self.ok or not candidates:
285
+ return []
286
+ z = self.encode(context)
287
+ if z is None:
288
+ return []
289
+ target = self.predict(z)
290
+ scores = []
291
+ for i, cand in enumerate(candidates):
292
+ zc = self.encode(cand)
293
+ d = float("inf") if zc is None else float(((zc[0] - target) ** 2).sum())
294
+ scores.append((d, i))
295
+ return [i for _, i in sorted(scores)]
296
+
297
+ # -- concept vocabulary --------------------------------------------------
298
+ def concept_ids(self, obs: Any) -> Optional[List[List[List[int]]]]:
299
+ """Discrete coarse-to-fine codes per chunk: [M][level][segment]."""
300
+ if not self.ok:
301
+ return None
302
+ x = self._seq(obs)
303
+ if x is None:
304
+ return None
305
+ with torch.no_grad():
306
+ _, ids, _ = self.net.residual_quantize(self.net.concepts(x, self.k))
307
+ stacked = torch.stack(ids, dim=-2)[0] # [M, levels, S]
308
+ return stacked.tolist()
309
+
310
+ def codebook_usage(self, obs_batch: Sequence[Any]) -> Optional[float]:
311
+ """Fraction of level-0 codes used across ``obs_batch`` (mean over segments)."""
312
+ if not self.ok:
313
+ return None
314
+ seen = [set() for _ in range(self.net.S)]
315
+ for obs in obs_batch:
316
+ ids = self.concept_ids(obs)
317
+ for chunk in ids or []:
318
+ for s, code in enumerate(chunk[0]):
319
+ seen[s].add(code)
320
+ return sum(len(s) for s in seen) / (self.net.S * self.net.N)
321
+
322
+ # -- persistence ---------------------------------------------------------
323
+ def save(self, path: str, *args: Any, **kwargs: Any) -> bool:
324
+ if not self.ok:
325
+ return False
326
+ tmp = f"{path}.tmp"
327
+ try:
328
+ torch.save({"cfg": self.cfg, "state": self.net.state_dict(),
329
+ "steps": self.steps}, tmp)
330
+ os.replace(tmp, path)
331
+ return True
332
+ except OSError as exc:
333
+ logger.warning("concept.save failed: %s", exc)
334
+ return False
335
+
336
+ def load(self, path: str, *args: Any, **kwargs: Any) -> bool:
337
+ if not self.ok or not os.path.exists(path):
338
+ return False
339
+ try:
340
+ ck = torch.load(path, map_location=self.device, weights_only=True)
341
+ if ck.get("cfg") != self.cfg:
342
+ logger.warning("concept.load: config mismatch %s != %s", ck.get("cfg"), self.cfg)
343
+ return False
344
+ self.net.load_state_dict(ck["state"])
345
+ self.steps = int(ck.get("steps", 0))
346
+ self.net.eval()
347
+ return True
348
+ except (OSError, RuntimeError, KeyError) as exc:
349
+ logger.warning("concept.load failed: %s", exc)
350
+ return False
351
+
352
+
353
+ def _concat(a: Any, b: Any) -> Any:
354
+ if _TORCH_OK:
355
+ return torch.cat([torch.as_tensor(a, dtype=torch.float32),
356
+ torch.as_tensor(b, dtype=torch.float32)], dim=-2)
357
+ return list(a) + list(b)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awpredict
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A small, dependency-light world-model package: LeWM JEPA + MLP engines, environment adapters
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -15,6 +15,7 @@ awpredict/adapters/__init__.py
15
15
  awpredict/adapters/code_world.py
16
16
  awpredict/adapters/obs_adapter.py
17
17
  awpredict/core/__init__.py
18
+ awpredict/core/concept.py
18
19
  awpredict/core/lewm.py
19
20
  awpredict/core/mlp.py
20
21
  awpredict/explorer/__init__.py
@@ -24,6 +25,7 @@ awpredict/memory/__init__.py
24
25
  awpredict/memory/landmark.py
25
26
  awpredict/training/__init__.py
26
27
  awpredict/training/online.py
28
+ tests/test_concept.py
27
29
  tests/test_fs_explorer_protocol.py
28
30
  tests/test_landmark_memory.py
29
31
  tests/test_lint_config_agrees.py
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
13
13
  # `awm` is the shape that already cost this repo a scare with AitherEvents.py
14
14
  # beside AitherEventSpine.py.
15
15
  name = "awpredict"
16
- version = "0.1.2"
16
+ version = "0.1.3"
17
17
  description = "A small, dependency-light world-model package: LeWM JEPA + MLP engines, environment adapters"
18
18
  license = {text = "MIT"}
19
19
  requires-python = ">=3.10"
@@ -0,0 +1,125 @@
1
+ """ConceptWorldModel: learns concept dynamics, does not collapse, ranks drafts."""
2
+ import importlib.util
3
+
4
+ import pytest
5
+ from awpredict.contracts import WorldModel, conforms
6
+
7
+ _HAS_TORCH = importlib.util.find_spec("torch") is not None
8
+ needs_torch = pytest.mark.skipif(not _HAS_TORCH, reason="torch not installed")
9
+
10
+ D_IN, K, N_PROTO = 16, 4, 6
11
+
12
+
13
+ def _world(seed=0):
14
+ """Concept prototypes whose order follows a fixed cycle: 0->1->...->5->0."""
15
+ import torch
16
+ g = torch.Generator().manual_seed(seed)
17
+ protos = torch.randn(N_PROTO, D_IN, generator=g) * 2.0
18
+
19
+ def seq(start, n_chunks, order=None, noise=0.3):
20
+ ids = order or [(start + i) % N_PROTO for i in range(n_chunks)]
21
+ rows = [protos[i] + noise * torch.randn(K, D_IN, generator=g) for i in ids]
22
+ return torch.cat(rows, 0), ids
23
+
24
+ return protos, seq
25
+
26
+
27
+ @pytest.fixture(scope="module")
28
+ def trained():
29
+ import torch
30
+ from awpredict.core.concept import ConceptWorldModel
31
+ torch.manual_seed(0)
32
+ torch.set_num_threads(min(4, torch.get_num_threads())) # 32 threads on a busy host: 25x slower
33
+ _, seq = _world()
34
+ m = ConceptWorldModel(d_in=D_IN, dim=64, k=K, segments=4, codes=16, levels=2,
35
+ layers=1, heads=4, max_concepts=32)
36
+ for i in range(256):
37
+ m.observe(seq(i % N_PROTO, 8)[0])
38
+ log = m.train_step(batch_size=16, steps=300)
39
+ return m, seq, log
40
+
41
+
42
+ @needs_torch
43
+ def test_conforms_to_world_model_contract():
44
+ from awpredict.core.concept import ConceptWorldModel
45
+ m = ConceptWorldModel(d_in=D_IN)
46
+ assert conforms(m, WorldModel) == []
47
+ assert m.ok
48
+
49
+
50
+ @needs_torch
51
+ def test_learns_the_concept_order(trained):
52
+ import torch
53
+ m, seq, _ = trained
54
+ in_order = sum(m.surprise(seq(s, 8)[0]) for s in range(N_PROTO))
55
+ g = torch.Generator().manual_seed(7)
56
+ shuffled = 0.0
57
+ for s in range(N_PROTO):
58
+ order = torch.randperm(N_PROTO, generator=g).tolist() + [s, (s + 3) % N_PROTO]
59
+ shuffled += m.surprise(seq(0, 8, order=order)[0])
60
+ assert shuffled > 3 * in_order, (in_order, shuffled)
61
+
62
+
63
+ @needs_torch
64
+ def test_no_collapse(trained):
65
+ m, seq, log = trained
66
+ z = m.encode(seq(0, 8)[0])
67
+ assert float(z.std(0).mean()) > 0.1
68
+ assert log["sigreg"] < 0.1
69
+ ids = [m.concept_ids(seq(s, 8)[0]) for s in range(N_PROTO)]
70
+ coarse = {tuple(chunk[0]) for row in ids for chunk in row}
71
+ assert len(coarse) >= N_PROTO - 1 # distinct concepts get distinct coarse codes
72
+
73
+
74
+ @needs_torch
75
+ def test_plan_picks_the_true_continuation(trained):
76
+ m, seq, _ = trained
77
+ hits = 0
78
+ for s in range(N_PROTO):
79
+ ctx, _ = seq(s, 5)
80
+ nxt = (s + 5) % N_PROTO
81
+ cands = [seq(0, 1, order=[j])[0] for j in range(N_PROTO)]
82
+ hits += m.plan(ctx, cands)[0] == nxt
83
+ assert hits >= N_PROTO - 1
84
+
85
+
86
+ @needs_torch
87
+ def test_save_load_roundtrip(trained, tmp_path):
88
+ import torch
89
+ from awpredict.core.concept import ConceptWorldModel
90
+ m, seq, _ = trained
91
+ p = str(tmp_path / "c.ckpt")
92
+ assert m.save(p)
93
+ m2 = ConceptWorldModel(**m.cfg)
94
+ assert m2.load(p)
95
+ x = seq(2, 6)[0]
96
+ assert torch.allclose(m.encode(x), m2.encode(x))
97
+ assert not m2.load(str(tmp_path / "missing.ckpt"))
98
+
99
+
100
+ @needs_torch
101
+ def test_degrades_on_bad_input():
102
+ from awpredict.core.concept import ConceptWorldModel
103
+ m = ConceptWorldModel(d_in=D_IN)
104
+ assert m.encode([[0.0] * 3]) is None # wrong width
105
+ assert m.surprise([[0.0] * D_IN]) is None # shorter than two chunks
106
+ assert m.plan([[0.0] * D_IN] * 8, []) == []
107
+
108
+
109
+ @needs_torch
110
+ def test_prediction_is_a_residual_on_the_current_concept():
111
+ # Predicting the absolute next concept regressed to the mean and ranked
112
+ # real-text drafts at chance; the delta form is what made plan() work.
113
+ import torch
114
+ from awpredict.core.concept import ConceptWorldModel
115
+ m = ConceptWorldModel(d_in=D_IN, dim=64, k=K, segments=4, codes=16, levels=2,
116
+ layers=1, heads=4, max_concepts=32)
117
+ for head in m.net.heads:
118
+ torch.nn.init.zeros_(head.weight)
119
+ torch.nn.init.zeros_(head.bias)
120
+ for book in m.net.books: # uniform logits -> mix = mean code
121
+ book.raw.data.zero_()
122
+ for p in book.mlp.parameters():
123
+ p.data.zero_()
124
+ c = torch.randn(1, 5, 64)
125
+ assert torch.allclose(m.net.predict(c), c, atol=1e-5)
File without changes
File without changes