openworldformat 0.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,94 @@
1
+ """The soundtrack curves — the pure math half of the soundtrack block.
2
+
3
+ Spec: spec/world.md (the soundtrack section). A world's soundtrack is
4
+ curves over time: energy per second, beats at a bpm, sections as
5
+ fractions of the duration, modulations over a signal. Renderers and
6
+ audio engines consume them; here they are plain functions over plain
7
+ numbers — the notebook's half, no audio required.
8
+
9
+ Ported from the JS reference's pure-curve functions (``js/src/render.js``),
10
+ which is where the renderer keeps them because it is the only JS
11
+ consumer. Python's consumers are analysis, not playback.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import math
17
+
18
+ __all__ = [
19
+ "curve_at",
20
+ "beat_at",
21
+ "section_at",
22
+ "modulation_factor",
23
+ ]
24
+
25
+
26
+ def _clamp01(v: float) -> float:
27
+ return min(1.0, max(0.0, v))
28
+
29
+
30
+ def _mod(a: float, n: float) -> float:
31
+ return (a % n + n) % n
32
+
33
+
34
+ def _num(v) -> float | None:
35
+ """A JSON number, or None — JavaScript's ``x > 0`` on undefined."""
36
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
37
+ return v
38
+ return None
39
+
40
+
41
+ def curve_at(curve: list | None, t: float) -> float:
42
+ """``SoundtrackDef::energy_at`` / ``curve_at``: per-second curve, linear
43
+ interpolation, clamped to [0, 1]. Time outside the curve holds its
44
+ endpoints."""
45
+ if not curve:
46
+ return 0.0
47
+ n = len(curve)
48
+ if n == 1:
49
+ return _clamp01(curve[0])
50
+ tt = min(max(t, 0.0), n - 1)
51
+ i = math.floor(tt)
52
+ f = tt - i
53
+ if i + 1 >= n:
54
+ return _clamp01(curve[-1])
55
+ return _clamp01(curve[i] + (curve[i + 1] - curve[i]) * f)
56
+
57
+
58
+ def beat_at(soundtrack: dict | None, t: float) -> float:
59
+ """``SoundtrackDef::beat_at``: 1 on a beat, decaying linearly to 0 at
60
+ the next. 0 when there is no bpm."""
61
+ st = soundtrack if isinstance(soundtrack, dict) else {}
62
+ bpm = _num(st.get("bpm"))
63
+ if bpm is None or not bpm > 0:
64
+ return 0.0
65
+ period = 60 / bpm
66
+ offset = _num(st.get("beat_offset"))
67
+ since = _mod(t - (offset or 0.0), period)
68
+ return 1 - since / period
69
+
70
+
71
+ def section_at(soundtrack: dict | None, t: float) -> int:
72
+ """``SoundtrackDef::section_at``: the index of the section holding ``t``
73
+ (sections are start fractions of the duration). 0 when there is no
74
+ duration or no sections."""
75
+ st = soundtrack if isinstance(soundtrack, dict) else {}
76
+ sections = st.get("sections")
77
+ sections = sections if isinstance(sections, list) else []
78
+ duration = _num(st.get("duration"))
79
+ if duration is None or not duration > 0 or not sections:
80
+ return 0
81
+ frac = _clamp01(t / duration)
82
+ idx = 0
83
+ for i in range(len(sections)):
84
+ if sections[i] <= frac:
85
+ idx = i
86
+ return idx
87
+
88
+
89
+ def modulation_factor(definition: dict | None, signal: float) -> float:
90
+ """``ModulationDef::factor``: map a [0, 1] signal onto ``range``."""
91
+ d = definition if isinstance(definition, dict) else {}
92
+ rng = d.get("range") or [1.0, 1.0]
93
+ a, b = rng[0], rng[1]
94
+ return a + (b - a) * _clamp01(signal)
@@ -0,0 +1,278 @@
1
+ Metadata-Version: 2.4
2
+ Name: openworldformat
3
+ Version: 0.4.4
4
+ Summary: The Open World Format research package: parse and fold a .world in pure Python, no engine required.
5
+ Author: The Open World Format contributors
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Repository, https://github.com/openworldformat/openworldformat
8
+ Project-URL: Documentation, https://openworldformat.org
9
+ Project-URL: Homepage, https://openworldformat.org
10
+ Keywords: 3d,world,gltf,scene,format,session,crdt,replay,benchmark,dataset
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: File Formats
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+
25
+ # openworldformat (PyPI)
26
+
27
+ The Open World Format research package. The core — parse a `.world`
28
+ manifest, fold its session log — is pure standard library, no
29
+ dependencies, and deliberately **no renderer**: this is the format's
30
+ research surface, not a viewer. Embodied-agent benchmarks (tasks as
31
+ predicates over the state document, trajectories in the format's own
32
+ op vocabulary), dataset tooling over session logs (fold a thousand
33
+ recordings, compute one number), and notebooks — none of that wants
34
+ three.js or Bevy; all of it wants `pip install openworldformat` and
35
+ a fold.
36
+
37
+ It is also the fold contract's third independently-built
38
+ implementation, sharing no code or toolchain with the JS reference or
39
+ the Rust one — every conformance world and outcome assertion the `js`
40
+ CI job runs, this package runs too, and the two folds agree
41
+ byte-for-byte on the example packages.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install openworldformat
47
+ ```
48
+
49
+ ## Use
50
+
51
+ ```python
52
+ from pathlib import Path
53
+ from openworldformat import read_package, fold_log, to_manifest
54
+
55
+ # A head-first package: manifest.json is the world now (`head`); the log
56
+ # folds over snapshots/base.json back to it.
57
+ base, entries, head, package = read_package(Path("world"))
58
+
59
+ state = fold_log(base, entries)
60
+ state["entities"] # the world at head revision
61
+ state["applied_edits"] # how many edits the log held
62
+ to_manifest(state) # the whole manifest again — the fold is total
63
+ ```
64
+
65
+ The fold applies the spec's rules: only `edit` ops change the document;
66
+ `tool`, `input`, `state` and `clock` fold to nothing; a `Batch` applies
67
+ all-or-nothing; ids and names stay unique; deleting an entity deletes
68
+ its descendants; it stops at the first entry that no longer applies.
69
+
70
+ Branches are tips: `build_history` gives a log its shape, `fold_path`
71
+ folds any branch of it. Session `state` ops fold separately, over
72
+ `state.json` (`fold_state`). `openworldformat.physics` carries the
73
+ `ext-physics` extension's executable half — the deterministic reference
74
+ solver, trajectory write/fold, and the outcome runner:
75
+
76
+ ```python
77
+ from openworldformat.physics import simulate_physics, fold_trajectories, run_outcomes
78
+
79
+ sim = simulate_physics(manifest, {"until_s": 6}) # deterministic
80
+ sim["contacts"] # impacts: {t_s, body, other, position, normal_speed}
81
+ sim["resting"] # where each dynamic body ended up
82
+ track = fold_trajectories(entries) # scrub recorded motion, no solver
83
+ run_outcomes(manifest, outcomes_doc) # the conformance assertions
84
+ ```
85
+
86
+ The solver is deliberately minimal — spheres against floors and
87
+ axis-aligned statics, fixed 1/120 s semi-implicit Euler, sleep on rest —
88
+ so the conformance outcome assertions run anywhere and in CI.
89
+ Reference-grade, not production physics; deterministic within an
90
+ engine, semantic-only across engines, exactly as the core spec refuses
91
+ bit-exactness.
92
+
93
+ ## Live authoring: agents outside, ops in
94
+
95
+ What a host does with an agent that wants to change the world:
96
+ `ingest` a batch against the fold — names bind to ids (a string where
97
+ an entity id goes is a name), a spawn without an id gets the next one,
98
+ partial transforms and materials merge with what's there, keys the
99
+ format would drop are refused with a path to each, and any failure
100
+ refuses the whole batch. The state you pass in is never touched:
101
+
102
+ ```python
103
+ from openworldformat import fold_log, ingest
104
+
105
+ done = ingest(state, [{"SpawnEntity": {"entity": {"name": "lamp", "parent": "crate"}}}])
106
+ done["ok"] # True
107
+ done["spawned"] # {"lamp": 3} — the ids the batch's names got
108
+ done["ops"] # the committed ops, bound: serialize into an entry and append
109
+ done["state"] # the new world; the old one is unchanged
110
+ ```
111
+
112
+ `merge_patch` is the RFC 7396 helper the merges use; `manifest_text`
113
+ is the canonical pretty form an authority writes to `manifest.json` —
114
+ members sorted, nulls left out, entities by id, plain arrays inline —
115
+ so the same world is always the same bytes.
116
+
117
+ ## Tasks: the benchmark harness
118
+
119
+ A task is a world, a trajectory, and a goal — a list of predicates over
120
+ what the fold produces. Scoring is therefore deterministic and needs no
121
+ engine: fold the log, evaluate the predicates. `examples/tasks/` turns
122
+ the repository's four example packages into a benchmark dataset, each
123
+ package's own log its solution:
124
+
125
+ ```bash
126
+ python -m openworldformat.eval examples/tasks/
127
+ ```
128
+
129
+ ```
130
+ ok the-drop-test: the switch scores (1 edits, 6 ops, 1 authors, revision 1)
131
+ ok forked-exploration: the moat variant wins this branch (3 edits, 4 ops, 3 authors, revision 4)
132
+ ok hello-world: hang the lantern (1 edits, 5 ops, 3 authors, revision 1)
133
+ ok speedrun-fork: noor's run, on its own branch (2 edits, 9 ops, 2 authors, revision 2)
134
+ ```
135
+
136
+ A task file names the world and states the goal; a `tip` folds a branch
137
+ (a run, a variant) instead of the head; a budget caps the cost:
138
+
139
+ ```json
140
+ {
141
+ "task": "hello-world: hang the lantern",
142
+ "world": "../../../examples/hello-world",
143
+ "budget": { "max_edits": 1, "max_entries": 5 },
144
+ "goal": [
145
+ { "exists": { "entity": "lantern" } },
146
+ { "near": { "entity": "lantern", "position": [-12.0, 0.0, 3.0], "tolerance": 0.5 } },
147
+ { "field": { "name": "score.tour", "equals": 1 } }
148
+ ]
149
+ }
150
+ ```
151
+
152
+ | Predicate | Holds when |
153
+ |---|---|
154
+ | `{"exists": {"entity": name}}` | the fold holds an entity by that name |
155
+ | `{"gone": {"entity": name}}` | it doesn't |
156
+ | `{"near": {"entity", "position", "tolerance"=0.15}}` | the entity's folded position is within tolerance |
157
+ | `{"field": {"name", "equals"?, "at_least"?, "at_most"?}}` | the state document's value compares |
158
+
159
+ Budgets (`max_edits`, `max_ops`, `max_entries`, inclusive) are the cost
160
+ side of a task: solve it, but not by a million edits. Metrics report
161
+ the trajectory's shape — edits, ops, entries, authors (the audit
162
+ trail's model-vs-visitor filter is one field), span, revision.
163
+
164
+ To score an agent, don't write files at all — pass its ops to
165
+ `run_task` the moment it emits them:
166
+
167
+ ```python
168
+ from openworldformat.eval import run_task
169
+
170
+ result = run_task(manifest, agent_entries, task, state_doc)
171
+ result["ok"], result["failures"], result["metrics"]
172
+ ```
173
+
174
+ This task language is deliberately library-level, not a spec
175
+ extension; it graduates to `spec/extensions/` when a second implementer
176
+ wants it.
177
+
178
+ ## Agents: the benchmark's other half
179
+
180
+ Scoring a recorded trajectory is one loop; scoring a live agent is the
181
+ same loop with the recording still wet. `run_agent` keeps the entries,
182
+ folds between steps so the agent observes its own edits, and scores
183
+ with `run_task`:
184
+
185
+ ```python
186
+ from openworldformat.agent import run_agent
187
+
188
+ result = run_agent(manifest, task, state_doc) # template_agent inside
189
+ result["ok"], result["metrics"], result["entries"]
190
+ ```
191
+
192
+ An agent is `f(document, values, task, entries) -> entry | None`: the
193
+ folded world and state values as observation, one entry of ops as
194
+ action, `None` to stop. The built-in `template_agent` is the baseline —
195
+ a compiler from goal predicates to ops, not a brain; it exists so
196
+ benchmark authors have a floor to beat and a skeleton to copy. Its
197
+ entries serialize to `ops.jsonl` like any recording: an agent run is a
198
+ log, ready to join a dataset.
199
+
200
+ ## Dataset tooling: fold a corpus, compute one number
201
+
202
+ A folder of `.world` packages is a dataset — each `ops.jsonl` a
203
+ recording, each fold a row:
204
+
205
+ ```bash
206
+ python -m openworldformat.dataset examples/
207
+ ```
208
+
209
+ ```
210
+ world entities edits ops entries authors tips span_s
211
+ forked-exploration 5 4 5 5 host, llm, maya 2 0.4
212
+ hello-world 14 1 5 5 llm, maya, visitor-7 1 1.877
213
+ speedrun-fork 9 2 15 4 kai, maya, noor 2 20.0
214
+ the-drop-test 7 1 6 2 host 1 0.2
215
+ ```
216
+
217
+ The authors column is the audit trail's dimension — model, host and
218
+ visitor in one recording, separable by one field. The tips column
219
+ counts branches; fold each with `fold_path` and the counterfactuals
220
+ come with the corpus. `summarize(dir)` gives the row as a dict for
221
+ notebooks.
222
+
223
+ ## API
224
+
225
+ | Export | What it does |
226
+ |---|---|
227
+ | `parse_manifest(text, strict=False)` | parse and version-check a world document (strict also enforces the schema's key sets) |
228
+ | `parse_log_line(line, strict=False)` | parse one `ops.jsonl` line, ops classified |
229
+ | `classify_op(op)` | recognize an op by shape — edits first |
230
+ | `op_kind_shape_ok(kind)` | the shape collision rule: edits PascalCase, history lowercase |
231
+ | `edit_ops(entry)` | an entry's edits, in order |
232
+ | `canonical_json(value)` | the canonical form for hashing — sorted, whitespace-free |
233
+ | `compute_entry_id(entry)` | an entry's content id, `sha256:<hex>` |
234
+ | `manifest_text(manifest)` | the canonical pretty text an authority writes to `manifest.json` |
235
+ | `fold_log(manifest, entries)` | the document at the last entry (the linear fold; by-name references bind to ids here, at ingestion) |
236
+ | `build_history(entries)` | a log's ids, parents, children and tips |
237
+ | `fold_path(manifest, entries, tip=None)` | the document at any tip (a branch) |
238
+ | `fold_state(state_doc, entries)` | the state document's values at the last entry |
239
+ | `compute_inverse(op, state)` | the inverse of one edit against the state it's about to change — undo is appending it |
240
+ | `merge_branch(state, entries)` | a branch's entries rewritten onto a main fold, colliding ids reallocated |
241
+ | `snapshot_filename(entry_id, revision)` | where a snapshot goes: by entry id, else by revision |
242
+ | `compact_package(package_json, head_revision)` | the package.json of a compaction |
243
+ | `read_package(world_dir)` | a head-first package: `(base, entries, head, package_json)` |
244
+ | `to_manifest(state)` | the fold's state as a whole manifest — `to_manifest(fold_log(m, []))` is `m` again |
245
+ | `ingest(state, batch)` | live authoring: bind, validate and apply a batch whole-or-refused; the state passed in is never touched; `warnings` for what validation warns about in the new world (a behavior whose target the batch deleted) |
246
+ | `merge_patch(current, change)` | JSON merge patch, RFC 7396 — the rule partial transforms merge by |
247
+ | `compact(world_dir, head_revision=None)` | move the base up to the head (or a revision): `snapshots/base.json` rewritten, the entries it holds archived |
248
+ | `ext_provenance(manifest)` | the provenance extension's five lineage fields, or None |
249
+
250
+ `openworldformat.physics` mirrors the npm package's
251
+ `openworldformat/physics`: `collect_physics`, `simulate_physics`,
252
+ `fold_trajectories`, `trajectory_op`, `run_outcomes`.
253
+ `openworldformat.cinematography` mirrors
254
+ `openworldformat/cinematography`: `camera_of`, `frame_of`, `view_of`,
255
+ `project`, `world_origin`, `aim_point_of`, `focus_distance_of`,
256
+ `shot_list`, `run_outcomes`.
257
+ `openworldformat.soundtrack` holds the soundtrack curves — `curve_at`,
258
+ `beat_at`, `section_at`, `modulation_factor` — as plain functions over
259
+ plain numbers, for analysis rather than playback.
260
+ `openworldformat.eval` scores tasks (see above): `run_task`,
261
+ `load_task_file`, and the `python -m openworldformat.eval` CLI.
262
+ `openworldformat.agent` runs agents against them: `run_agent` and the
263
+ `template_agent` baseline. `openworldformat.dataset` summarizes
264
+ recordings — `summarize` and the `python -m openworldformat.dataset`
265
+ report.
266
+
267
+ ## Test
268
+
269
+ ```bash
270
+ python -m unittest discover -s tests -t .
271
+ ```
272
+
273
+ The suite mirrors the JS tests — it folds the repository's own example
274
+ packages, replays the drop-test recording through the solver, and runs
275
+ the conformance outcome assertions — so both references assert the same
276
+ facts.
277
+
278
+ Apache-2.0.
@@ -0,0 +1,12 @@
1
+ openworldformat/__init__.py,sha256=15XrTQ5UeK_flXralR6FWSDeuC-EM11xljM378nA1G4,74489
2
+ openworldformat/agent.py,sha256=Gfi9SmLygZrzx1Ub-jxXCi3QTTHEAWLiwtSkHWXBDFk,6008
3
+ openworldformat/cinematography.py,sha256=RuvJocTn8itRzVZU4MgQ8ixz9Xw3tM1yOlTOzPLibCU,17123
4
+ openworldformat/dataset.py,sha256=wHAZR-GcgO0MpuJw8HquGJdLoDV_kKj2SvparMnw7FU,3075
5
+ openworldformat/entity_refs.py,sha256=Y7yLQVkdCRpiJJJtuATER-gvCN4N51PM3UWf_W0GLkk,1386
6
+ openworldformat/eval.py,sha256=giDCaezYl-uegD3ou8ZM5EW3qCnSz6LpFBWp_l5hm-Q,8887
7
+ openworldformat/physics.py,sha256=9kXSWUw2eyjvxnCINoOoNxdGi6PdoL5i-1NGInthdto,17264
8
+ openworldformat/soundtrack.py,sha256=0QeZ0EfC_yEDgPLjY2rZxbpeya7XcxsGxEY9hgeGyN4,3022
9
+ openworldformat-0.4.4.dist-info/METADATA,sha256=aks4v2SS3F-gUI2Dj3zRyPtGXBsFeNDv0F5I1M_YM8M,12704
10
+ openworldformat-0.4.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ openworldformat-0.4.4.dist-info/top_level.txt,sha256=80p_w3Q5qjS-v3VfN0PwnqKg4cP2WJhMQAZsCiUtZEc,16
12
+ openworldformat-0.4.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ openworldformat