awpredict 0.1.2__tar.gz → 0.1.4__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 (41) hide show
  1. {awpredict-0.1.2 → awpredict-0.1.4}/PKG-INFO +1 -1
  2. {awpredict-0.1.2 → awpredict-0.1.4}/README.md +29 -1
  3. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/__init__.py +4 -1
  4. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/_doctor.py +9 -1
  5. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/cli.py +63 -2
  6. awpredict-0.1.4/awpredict/core/concept.py +357 -0
  7. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/core/mlp.py +27 -0
  8. awpredict-0.1.4/awpredict/viz.py +532 -0
  9. awpredict-0.1.4/awpredict/viz_demo.py +199 -0
  10. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/PKG-INFO +1 -1
  11. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/SOURCES.txt +6 -1
  12. {awpredict-0.1.2 → awpredict-0.1.4}/pyproject.toml +1 -1
  13. awpredict-0.1.4/tests/test_concept.py +125 -0
  14. awpredict-0.1.4/tests/test_viz.py +224 -0
  15. {awpredict-0.1.2 → awpredict-0.1.4}/LICENSE +0 -0
  16. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/adapters/__init__.py +0 -0
  17. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/adapters/code_world.py +0 -0
  18. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/adapters/obs_adapter.py +0 -0
  19. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/contracts.py +0 -0
  20. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/core/__init__.py +0 -0
  21. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/core/lewm.py +0 -0
  22. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/explorer/__init__.py +0 -0
  23. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/explorer/fs_cartographer.py +0 -0
  24. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/explorer/histogram_encoder.py +0 -0
  25. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/memory/__init__.py +0 -0
  26. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/memory/landmark.py +0 -0
  27. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/training/__init__.py +0 -0
  28. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/training/online.py +0 -0
  29. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/dependency_links.txt +0 -0
  30. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/entry_points.txt +0 -0
  31. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/requires.txt +0 -0
  32. {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/top_level.txt +0 -0
  33. {awpredict-0.1.2 → awpredict-0.1.4}/setup.cfg +0 -0
  34. {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_fs_explorer_protocol.py +0 -0
  35. {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_landmark_memory.py +0 -0
  36. {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_lint_config_agrees.py +0 -0
  37. {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_object_masking.py +0 -0
  38. {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_online_lr_controller.py +0 -0
  39. {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_problem_contracts.py +0 -0
  40. {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_structured_adapter.py +0 -0
  41. {awpredict-0.1.2 → awpredict-0.1.4}/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.4
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 69 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
 
@@ -63,6 +63,26 @@ pred = model.predict(z, action)
63
63
  print(model.surprise(state, action, next_state))
64
64
  ```
65
65
 
66
+ ## See what a model imagines
67
+
68
+ ```bash
69
+ awpredict viz --engine lewm --demo --out dream.html # also: mlp, concept
70
+ ```
71
+
72
+ `--demo` trains a small engine on a synthetic world (a moving block for `lewm`, a
73
+ drifting point or vector for `mlp`/`concept`) and writes one self-contained HTML
74
+ page: the open-loop dream next to the real sequence (a decoded filmstrip when the
75
+ engine can decode to a grid, a latent heatmap otherwise), the latent trajectory in
76
+ 2D, and every error beside the copy-last baseline (predict that nothing changes).
77
+ On your own trained engine:
78
+
79
+ ```python
80
+ from awpredict.viz import dream_report, render_html
81
+
82
+ report = dream_report(model, observations, actions, steps=6)
83
+ open("dream.html", "w", encoding="utf-8").write(render_html(report))
84
+ ```
85
+
66
86
  ## Why this exists
67
87
 
68
88
  The goal is to help someone bootstrap their own world model — a working
@@ -136,6 +156,8 @@ Each installs on its own, works offline, and needs no account.
136
156
  | [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
157
  | [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
158
  | [awshare](https://github.com/Aitherium/awshare) | that the download is intact | content-addressed bundles, verified on fetch |
159
+ | [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 |
160
+ | [awpermit](https://github.com/Aitherium/awpermit) | that an admitted agent will only do what its owner meant | an Ed25519 permit naming the agent, the principal, the door, the actions and the cap |
139
161
  | [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
162
  | [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
163
  | [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 +167,7 @@ Each installs on its own, works offline, and needs no account.
145
167
  | [awrelay](https://github.com/Aitherium/awrelay) | a SaaS in the middle of your agents | findings, alerts and coordination over your own transport |
146
168
  | [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
169
  | [awmail](https://github.com/Aitherium/awmail) | a mailbox somebody else can read | mail your agents send and receive over your own server |
170
+ | [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
171
  | [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
172
  | [awfind](https://github.com/Aitherium/awfind) | one vendor's idea of the web | results from whichever providers you configured |
150
173
  | [awbrowse](https://github.com/Aitherium/awbrowse) | that the page said what you were told | the render, the DOM and the requests it made |
@@ -201,10 +224,12 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
201
224
  | [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
225
  | [awseal](https://github.com/Aitherium/awseal) | Sign an artifact so a stranger can verify it | [docs](https://aitherium.github.io/awseal/) |
203
226
  | [awshare](https://github.com/Aitherium/awshare) | Publish an artifact and fetch it back verified | [docs](https://aitherium.github.io/awshare/) |
227
+ | [awsuite](https://github.com/Aitherium/awsuite) | Your Google Workspace as agent tools, and no write happens without a yes | — |
204
228
  | [awdit](https://github.com/Aitherium/awdit) | An append-only audit trail whose gaps are DETECTABLE | [docs](https://aitherium.github.io/awdit/) |
205
229
  | [awbac](https://github.com/Aitherium/awbac) | Role-based access control that fails closed and explains itself | [docs](https://aitherium.github.io/awbac/) |
206
230
  | [awiam](https://github.com/Aitherium/awiam) | Who is this caller? A directory and session store that fails honestly | [docs](https://aitherium.github.io/awiam/) |
207
231
  | [awtunnel](https://github.com/Aitherium/awtunnel) | Reach a service that has no public address | [docs](https://aitherium.github.io/awtunnel/) |
232
+ | [awpermit](https://github.com/Aitherium/awpermit) | Let an agent through your door with a scope, a spend cap, and someone accountable | — |
208
233
  | [awnest](https://github.com/Aitherium/awnest) | Prove there is a human before you let them into the nest | [docs](https://aitherium.github.io/awnest/) |
209
234
  | [awrena](https://github.com/Aitherium/awrena) | Put two agents head to head and get a verdict you can check | [docs](https://aitherium.github.io/awrena/) |
210
235
  | [awnboard](https://github.com/Aitherium/awnboard) | A front gate you can put in front of anything, and hand someone the key to | [docs](https://aitherium.github.io/awnboard/) |
@@ -214,6 +239,7 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
214
239
  | [awrelay](https://github.com/Aitherium/awrelay) | Portable agent messaging — findings, alerts, coordination | [docs](https://aitherium.github.io/awrelay/) |
215
240
  | [awask](https://github.com/Aitherium/awask) | Your agent asks you a question — and acts on your answer | [docs](https://aitherium.github.io/awask/) |
216
241
  | [awmail](https://github.com/Aitherium/awmail) | Give an agent an email address — send, and actually receive | [docs](https://aitherium.github.io/awmail/) |
242
+ | [awdiscord](https://github.com/Aitherium/awdiscord) | Put any agent on Discord as a real bot, with an operator in the loop | — |
217
243
  | [awnet](https://github.com/Aitherium/awnet) | The agentic web — agents host a mesh, and agents join one | [docs](https://aitherium.github.io/awnet/) |
218
244
  | [awswarm](https://github.com/Aitherium/awswarm) | Run one model too big for any single GPU across a pool of small ones | — |
219
245
  | [awfind](https://github.com/Aitherium/awfind) | A portable search client — query, results, ranking | [docs](https://aitherium.github.io/awfind/) |
@@ -253,6 +279,8 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
253
279
  | [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
280
  | [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
281
 
282
+ **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).
283
+
256
284
  <div id="aither-constellation" data-self="awpredict"></div>
257
285
  <script src="aither-constellation.js"></script>
258
286
 
@@ -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.4"
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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  engines which engines can actually construct here, and why not if they cannot
4
4
  conforms does a class satisfy WorldModel or EnvironmentAdapter, and what is missing
5
+ viz see what an engine imagines: dream filmstrip/heatmap + error vs copy-last
5
6
 
6
7
  --json for machine-readable output.
7
8
  """
@@ -11,6 +12,7 @@ from __future__ import annotations
11
12
  import argparse
12
13
  import importlib
13
14
  import json
15
+ from pathlib import Path
14
16
  from typing import List
15
17
 
16
18
  from . import contracts
@@ -28,7 +30,8 @@ def cmd_engines(args) -> int:
28
30
  missing' sends someone to install what they already have.
29
31
  """
30
32
  rows = []
31
- for name, mod in (("lewm", "awpredict.core.lewm"), ("mlp", "awpredict.core.mlp")):
33
+ for name, mod in (("lewm", "awpredict.core.lewm"), ("mlp", "awpredict.core.mlp"),
34
+ ("concept", "awpredict.core.concept")):
32
35
  try:
33
36
  m = importlib.import_module(mod)
34
37
  except Exception as exc:
@@ -81,6 +84,56 @@ def cmd_conforms(args) -> int:
81
84
  return 0 if not missing else 1
82
85
 
83
86
 
87
+ def cmd_viz(args) -> int:
88
+ """Write a dream-visualizer HTML page.
89
+
90
+ Exit 0 written, 1 the engine produced no rollout (nothing worth a page),
91
+ 2 could not run (torch/numpy missing, no data source) -- never 0 on nothing.
92
+ """
93
+ if not args.demo:
94
+ print("viz: only --demo is wired on the command line; from Python call "
95
+ "awpredict.viz.dream_report(model, observations, actions) and "
96
+ "awpredict.viz.render_html(report) on your own trained engine")
97
+ return 2
98
+ try:
99
+ from . import viz, viz_demo
100
+ except ImportError as exc:
101
+ print(f"viz: cannot import the visualizer: {exc}")
102
+ return 2
103
+ try:
104
+ model, obs, acts, meta = viz_demo.DEMOS[args.engine](steps=args.steps)
105
+ report = viz.dream_report(model, obs, acts, steps=args.steps)
106
+ except viz.VizUnavailableError as exc:
107
+ print(f"viz: cannot run the {args.engine} demo: {exc}")
108
+ return 2
109
+ report["meta"] = meta
110
+ for name in ("one_step", "rollout"):
111
+ b = report[name]
112
+ print(f" {name:<9} model {_num(b['model_mean'])} copy-last "
113
+ f"{_num(b['copy_last_mean'])} "
114
+ f"{'beats' if b['beats_copy_last'] else 'does NOT beat'} copy-last")
115
+ if not report["ok"]:
116
+ print(f"viz: {args.engine} produced no rollout: {report['reason']}")
117
+ return 1
118
+ out = Path(args.out or f"awpredict-dream-{args.engine}.html")
119
+ try:
120
+ out.parent.mkdir(parents=True, exist_ok=True)
121
+ out.write_text(viz.render_html(report), encoding="utf-8")
122
+ except OSError as exc:
123
+ print(f"viz: cannot write {out}: {exc}")
124
+ return 2
125
+ if args.json:
126
+ print(json.dumps({"out": str(out), "ok": True,
127
+ "one_step": report["one_step"], "rollout": report["rollout"],
128
+ "pixel": report["pixel"]}, indent=2))
129
+ print(f"wrote {out.resolve()}")
130
+ return 0
131
+
132
+
133
+ def _num(x) -> str:
134
+ return "n/a" if x is None else f"{x:.4g}"
135
+
136
+
84
137
  def build_parser() -> argparse.ArgumentParser:
85
138
  ap = argparse.ArgumentParser(prog="awpredict",
86
139
  description=__doc__.split("\n\n")[0])
@@ -93,6 +146,13 @@ def build_parser() -> argparse.ArgumentParser:
93
146
  p_c.add_argument("--module", required=True, help="importable module path")
94
147
  p_c.add_argument("--name", required=True, help="class or object in that module")
95
148
  p_c.add_argument("--protocol", choices=sorted(_PROTOCOLS), default="WorldModel")
149
+
150
+ p_v = sub.add_parser("viz", help="render what an engine imagines (HTML)")
151
+ p_v.add_argument("--engine", required=True, choices=["lewm", "mlp", "concept"])
152
+ p_v.add_argument("--demo", action="store_true",
153
+ help="train briefly on a synthetic world, then dream along it")
154
+ p_v.add_argument("--out", help="HTML path (default awpredict-dream-<engine>.html)")
155
+ p_v.add_argument("--steps", type=int, default=6, help="imagined steps (default 6)")
96
156
  return ap
97
157
 
98
158
 
@@ -115,7 +175,8 @@ def main(argv: "list[str] | None" = None) -> int:
115
175
  if not args.cmd:
116
176
  build_parser().print_help()
117
177
  return 2
118
- return {"engines": cmd_engines, "conforms": cmd_conforms}[args.cmd](args)
178
+ return {"engines": cmd_engines, "conforms": cmd_conforms,
179
+ "viz": cmd_viz}[args.cmd](args)
119
180
 
120
181
 
121
182
  if __name__ == "__main__":
@@ -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)
@@ -334,6 +334,33 @@ class MLPWorldModel:
334
334
  return None
335
335
  return self._hash_state_embedding(str(obs))
336
336
 
337
+ def predict_latent(self, z: Any, action: Any) -> Optional[List[float]]:
338
+ """Latent → predicted next latent through the neural transition net.
339
+
340
+ ``predict`` answers in state HASHES (the tabular contract), which cannot
341
+ be fed back into the model; this is the vector-space step a rollout or
342
+ a visualizer needs. ``z`` is an ``encode()`` output or a previous
343
+ ``predict_latent()`` output. None until the neural path has trained, or
344
+ when torch is missing — never a fabricated vector.
345
+ """
346
+ if z is None or not self._neural_available or not self._encoder.is_available:
347
+ return None
348
+ try:
349
+ if isinstance(z, torch.Tensor):
350
+ state = z.detach().float().reshape(-1)
351
+ else:
352
+ state = torch.tensor(list(z), dtype=torch.float32)
353
+ if state.numel() != self._state_dim:
354
+ return None
355
+ a = self._action_encoder.encode(action)
356
+ if not isinstance(a, torch.Tensor):
357
+ a = torch.tensor(a, dtype=torch.float32)
358
+ nxt, _, _ = self._encoder.predict(state, a.detach())
359
+ return nxt.tolist()
360
+ except (RuntimeError, TypeError, ValueError) as e:
361
+ log.debug(f"predict_latent failed: {e}")
362
+ return None
363
+
337
364
  def surprise(
338
365
  self,
339
366
  obs: int,