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.
- {awpredict-0.1.2 → awpredict-0.1.4}/PKG-INFO +1 -1
- {awpredict-0.1.2 → awpredict-0.1.4}/README.md +29 -1
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/__init__.py +4 -1
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/_doctor.py +9 -1
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/cli.py +63 -2
- awpredict-0.1.4/awpredict/core/concept.py +357 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/core/mlp.py +27 -0
- awpredict-0.1.4/awpredict/viz.py +532 -0
- awpredict-0.1.4/awpredict/viz_demo.py +199 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/PKG-INFO +1 -1
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/SOURCES.txt +6 -1
- {awpredict-0.1.2 → awpredict-0.1.4}/pyproject.toml +1 -1
- awpredict-0.1.4/tests/test_concept.py +125 -0
- awpredict-0.1.4/tests/test_viz.py +224 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/LICENSE +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/adapters/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/adapters/code_world.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/adapters/obs_adapter.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/contracts.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/core/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/core/lewm.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/explorer/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/explorer/fs_cartographer.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/explorer/histogram_encoder.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/memory/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/memory/landmark.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/training/__init__.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict/training/online.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/dependency_links.txt +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/entry_points.txt +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/requires.txt +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/awpredict.egg-info/top_level.txt +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/setup.cfg +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_fs_explorer_protocol.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_landmark_memory.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_lint_config_agrees.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_object_masking.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_online_lr_controller.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_problem_contracts.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_structured_adapter.py +0 -0
- {awpredict-0.1.2 → awpredict-0.1.4}/tests/test_value_head.py +0 -0
|
@@ -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
|
|
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.
|
|
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 = [
|
|
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
|
|
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,
|